config-fs-utils 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (4) hide show
  1. package/README.md +348 -0
  2. package/index.js +259 -0
  3. package/index.test.js +376 -0
  4. package/package.json +33 -0
package/README.md ADDED
@@ -0,0 +1,348 @@
1
+ # config-fs-utils
2
+
3
+ File system utilities for configuration file management
4
+
5
+ [![npm version](https://badge.fury.io/js/config-fs-utils.svg)](https://www.npmjs.com/package/config-fs-utils)
6
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
7
+
8
+ ## Features
9
+
10
+ - ✅ **Zero dependencies** - Uses only Node.js built-in modules
11
+ - ✅ **Simple & Flexible** - Two API styles for different use cases
12
+ - ✅ **Safe file operations** - Automatic backups and permission management
13
+ - ✅ **Tilde expansion** - Automatically expands `~` to home directory
14
+ - ✅ **Recursive directory creation** - Creates nested directories automatically
15
+ - ✅ **Perfect for config files** - Designed for configuration management
16
+
17
+ ## Installation
18
+
19
+ ```bash
20
+ npm install config-fs-utils
21
+ ```
22
+
23
+ ## Quick Start
24
+
25
+ ### Simple API (Recommended for most users)
26
+
27
+ ```javascript
28
+ const { setupStandardMuttDirs, writeConfigFiles } = require('config-fs-utils');
29
+
30
+ // Create standard directory structure
31
+ const dirs = await setupStandardMuttDirs();
32
+ // Creates: ~/.config/mutt/, ~/.local/etc/oauth-tokens/, ~/.cache/mutt/, etc.
33
+
34
+ // Write config files with automatic backups and secure permissions
35
+ await writeConfigFiles({
36
+ '~/.config/myapp/config.ini': 'config content',
37
+ '~/.config/myapp/settings.json': JSON.stringify({ key: 'value' })
38
+ });
39
+ // Files are created with 0o600 permissions
40
+ // Existing files are backed up automatically
41
+ ```
42
+
43
+ ### Flexible API (For advanced use cases)
44
+
45
+ ```javascript
46
+ const { ensureDirectories, writeFile } = require('config-fs-utils');
47
+
48
+ // Create custom directories
49
+ await ensureDirectories([
50
+ '/custom/path/one',
51
+ '/custom/path/two',
52
+ '~/relative/path'
53
+ ]);
54
+
55
+ // Write file with custom options
56
+ await writeFile('/path/to/file', 'content', {
57
+ backup: false,
58
+ permissions: 0o644
59
+ });
60
+ ```
61
+
62
+ ## API Reference
63
+
64
+ ### Simple API
65
+
66
+ #### `setupStandardMuttDirs(options)`
67
+
68
+ Creates standard Mutt/Neomutt directory structure.
69
+
70
+ **Parameters:**
71
+ - `options.baseDir` (string, optional): Base directory (defaults to `~`)
72
+
73
+ **Returns:** Object with created directory paths
74
+
75
+ ```javascript
76
+ const dirs = await setupStandardMuttDirs();
77
+ // {
78
+ // config: '/Users/username/.config/mutt',
79
+ // accounts: '/Users/username/.config/mutt/accounts',
80
+ // tokens: '/Users/username/.local/etc/oauth-tokens',
81
+ // cacheHeaders: '/Users/username/.cache/mutt/headers',
82
+ // cacheBodies: '/Users/username/.cache/mutt/bodies'
83
+ // }
84
+ ```
85
+
86
+ #### `writeConfigFiles(files, options)`
87
+
88
+ Writes multiple config files with secure defaults.
89
+
90
+ **Parameters:**
91
+ - `files` (Object): Map of filepath to content
92
+ - `options.backup` (boolean, default: `true`): Create backups of existing files
93
+ - `options.permissions` (number, default: `0o600`): File permissions
94
+
95
+ **Returns:** Array of result objects
96
+
97
+ ```javascript
98
+ const results = await writeConfigFiles({
99
+ '~/.config/app/config.yml': yamlContent,
100
+ '~/.config/app/secrets.json': secretsContent
101
+ });
102
+ // Each result: { path, backup, created }
103
+ ```
104
+
105
+ ### Flexible API
106
+
107
+ #### `ensureDirectory(dir)`
108
+
109
+ Creates a single directory (and parents if needed).
110
+
111
+ ```javascript
112
+ await ensureDirectory('~/my/nested/directory');
113
+ ```
114
+
115
+ #### `ensureDirectories(dirs)`
116
+
117
+ Creates multiple directories.
118
+
119
+ ```javascript
120
+ await ensureDirectories([
121
+ '~/dir1',
122
+ '~/dir2/nested',
123
+ '/absolute/path'
124
+ ]);
125
+ ```
126
+
127
+ #### `writeFile(filepath, content, options)`
128
+
129
+ Writes a single file with options.
130
+
131
+ **Options:**
132
+ - `backup` (boolean): Create backup if file exists
133
+ - `permissions` (number): File permissions (e.g., `0o600`)
134
+
135
+ ```javascript
136
+ const result = await writeFile('~/config.txt', 'content', {
137
+ backup: true,
138
+ permissions: 0o600
139
+ });
140
+ // { path: '/Users/username/config.txt', backup: '...backup-2026-01-26...', created: false }
141
+ ```
142
+
143
+ #### `writeFiles(files, options)`
144
+
145
+ Writes multiple files.
146
+
147
+ ```javascript
148
+ await writeFiles({
149
+ '~/file1.txt': 'content1',
150
+ '~/file2.txt': 'content2'
151
+ }, {
152
+ backup: true,
153
+ permissions: 0o644
154
+ });
155
+ ```
156
+
157
+ #### `backupFile(filepath)`
158
+
159
+ Creates a backup of a file if it exists.
160
+
161
+ ```javascript
162
+ const backupPath = await backupFile('~/important.txt');
163
+ // Returns: '~/important.txt.backup-2026-01-26T12-34-56-789Z' or null
164
+ ```
165
+
166
+ ### Utility Functions
167
+
168
+ #### `expandHome(filepath)`
169
+
170
+ Expands `~` to home directory.
171
+
172
+ ```javascript
173
+ expandHome('~/Documents');
174
+ // Returns: '/Users/username/Documents'
175
+ ```
176
+
177
+ #### `exists(filepath)`
178
+
179
+ Checks if a file or directory exists.
180
+
181
+ ```javascript
182
+ const fileExists = await exists('~/config.txt');
183
+ // Returns: true or false
184
+ ```
185
+
186
+ #### `getStats(filepath)`
187
+
188
+ Gets file/directory stats.
189
+
190
+ ```javascript
191
+ const stats = await getStats('~/file.txt');
192
+ // Returns: fs.Stats object or null
193
+ ```
194
+
195
+ #### `ensureDirectoriesFromPaths(paths)`
196
+
197
+ Creates directories from a paths object (like from `mutt-config-core`).
198
+
199
+ ```javascript
200
+ const { getConfigPaths } = require('mutt-config-core');
201
+ const paths = getConfigPaths('user@gmail.com');
202
+
203
+ await ensureDirectoriesFromPaths(paths);
204
+ // Creates all necessary parent directories
205
+ ```
206
+
207
+ ### Constants
208
+
209
+ #### `STANDARD_MUTT_DIRS`
210
+
211
+ Array of standard Mutt directory paths:
212
+
213
+ ```javascript
214
+ [
215
+ '.config/mutt',
216
+ '.config/mutt/accounts',
217
+ '.local/etc/oauth-tokens',
218
+ '.cache/mutt/headers',
219
+ '.cache/mutt/bodies'
220
+ ]
221
+ ```
222
+
223
+ ## Integration with mutt-config-core
224
+
225
+ Perfect companion for [`mutt-config-core`](https://www.npmjs.com/package/mutt-config-core):
226
+
227
+ ```javascript
228
+ const { generateMuttConfigs, getConfigPaths } = require('mutt-config-core');
229
+ const { setupStandardMuttDirs, writeConfigFiles } = require('config-fs-utils');
230
+
231
+ // 1. Generate configurations
232
+ const configs = generateMuttConfigs({
233
+ email: 'user@gmail.com',
234
+ realName: 'John Doe',
235
+ editor: 'nvim',
236
+ locale: 'en'
237
+ });
238
+
239
+ // 2. Setup directories
240
+ const dirs = await setupStandardMuttDirs();
241
+
242
+ // 3. Write config files
243
+ const paths = getConfigPaths('user@gmail.com');
244
+ await writeConfigFiles({
245
+ [`~/${paths.accountMuttrc}`]: configs.accountMuttrc,
246
+ [`~/${paths.mainMuttrc}`]: configs.mainMuttrc
247
+ });
248
+
249
+ console.log('Setup complete!');
250
+ ```
251
+
252
+ ## Use Cases
253
+
254
+ ### Configuration Management
255
+
256
+ ```javascript
257
+ // Safely update config files with automatic backups
258
+ await writeConfigFiles({
259
+ '~/.bashrc': bashrcContent,
260
+ '~/.vimrc': vimrcContent,
261
+ '~/.gitconfig': gitconfigContent
262
+ });
263
+ ```
264
+
265
+ ### Application Setup
266
+
267
+ ```javascript
268
+ // Create app directory structure
269
+ await ensureDirectories([
270
+ '~/.config/myapp',
271
+ '~/.config/myapp/plugins',
272
+ '~/.local/share/myapp',
273
+ '~/.cache/myapp'
274
+ ]);
275
+
276
+ // Write initial config
277
+ await writeFile('~/.config/myapp/config.json', JSON.stringify({
278
+ version: '1.0.0',
279
+ settings: {}
280
+ }, null, 2));
281
+ ```
282
+
283
+ ### Secure File Operations
284
+
285
+ ```javascript
286
+ // Write sensitive files with restricted permissions
287
+ await writeFile('~/.ssh/id_rsa', privateKey, {
288
+ permissions: 0o600 // Owner read/write only
289
+ });
290
+ ```
291
+
292
+ ## Error Handling
293
+
294
+ All async functions can throw errors. Always use try-catch:
295
+
296
+ ```javascript
297
+ try {
298
+ await writeConfigFiles({
299
+ '/protected/path': 'content'
300
+ });
301
+ } catch (error) {
302
+ console.error('Failed to write config:', error.message);
303
+ }
304
+ ```
305
+
306
+ ## Development
307
+
308
+ ```bash
309
+ # Install dependencies
310
+ npm install
311
+
312
+ # Run tests
313
+ npm test
314
+
315
+ # Watch mode
316
+ npm run test:watch
317
+
318
+ # Coverage
319
+ npm run test:coverage
320
+ ```
321
+
322
+ ## Testing
323
+
324
+ Fully tested with 29 test cases covering:
325
+ - Directory creation (nested, recursive)
326
+ - File operations (write, backup, permissions)
327
+ - Tilde expansion
328
+ - Path utilities
329
+ - Integration scenarios
330
+
331
+ ## Related Projects
332
+
333
+ - [mutt-config-core](https://github.com/a-lost-social-misfit/mutt-config-core) - Configuration generator
334
+ - [create-neomutt-gmail](https://github.com/a-lost-social-misfit/create-neomutt-gmail) - CLI tool (coming soon)
335
+
336
+ ## Contributing
337
+
338
+ Contributions are welcome! Please feel free to submit a Pull Request.
339
+
340
+ ## License
341
+
342
+ MIT © a-lost-social-misfit
343
+
344
+ ## Author
345
+
346
+ Created by [a-lost-social-misfit](https://github.com/a-lost-social-misfit)
347
+
348
+ Part of a suite of tools for managing Neomutt + Gmail + OAuth2.0 setup.
package/index.js ADDED
@@ -0,0 +1,259 @@
1
+ /**
2
+ * config-fs-utils
3
+ * File system utilities for configuration file management
4
+ *
5
+ * No external dependencies. Uses Node.js built-in modules only.
6
+ */
7
+
8
+ const fs = require("fs").promises;
9
+ const path = require("path");
10
+ const os = require("os");
11
+
12
+ /**
13
+ * Standard Mutt directory structure
14
+ */
15
+ const STANDARD_MUTT_DIRS = [
16
+ ".config/mutt",
17
+ ".config/mutt/accounts",
18
+ ".local/etc/oauth-tokens",
19
+ ".cache/mutt/headers",
20
+ ".cache/mutt/bodies",
21
+ ];
22
+
23
+ /**
24
+ * Expand tilde (~) to home directory
25
+ * @param {string} filepath - Path that may contain ~
26
+ * @returns {string} Expanded absolute path
27
+ */
28
+ function expandHome(filepath) {
29
+ if (filepath.startsWith("~/") || filepath === "~") {
30
+ return path.join(os.homedir(), filepath.slice(2));
31
+ }
32
+ return filepath;
33
+ }
34
+
35
+ /**
36
+ * Ensure a single directory exists
37
+ * @param {string} dir - Directory path
38
+ * @returns {Promise<string>} Created directory path
39
+ */
40
+ async function ensureDirectory(dir) {
41
+ const expandedDir = expandHome(dir);
42
+ await fs.mkdir(expandedDir, { recursive: true });
43
+ return expandedDir;
44
+ }
45
+
46
+ /**
47
+ * Ensure multiple directories exist
48
+ * @param {string[]} dirs - Array of directory paths
49
+ * @returns {Promise<string[]>} Array of created directory paths
50
+ */
51
+ async function ensureDirectories(dirs) {
52
+ const results = [];
53
+ for (const dir of dirs) {
54
+ const created = await ensureDirectory(dir);
55
+ results.push(created);
56
+ }
57
+ return results;
58
+ }
59
+
60
+ /**
61
+ * Setup standard Mutt directory structure
62
+ * @param {Object} options - Options
63
+ * @param {string} [options.baseDir='~'] - Base directory (defaults to home)
64
+ * @returns {Promise<Object>} Created directory paths
65
+ */
66
+ async function setupStandardMuttDirs(options = {}) {
67
+ const baseDir = options.baseDir || "~";
68
+ const dirs = STANDARD_MUTT_DIRS.map((dir) => path.join(baseDir, dir));
69
+
70
+ const created = await ensureDirectories(dirs);
71
+
72
+ return {
73
+ config: created[0],
74
+ accounts: created[1],
75
+ tokens: created[2],
76
+ cacheHeaders: created[3],
77
+ cacheBodies: created[4],
78
+ };
79
+ }
80
+
81
+ /**
82
+ * Create backup of a file if it exists
83
+ * @param {string} filepath - File to backup
84
+ * @returns {Promise<string|null>} Backup file path or null if file doesn't exist
85
+ */
86
+ async function backupFile(filepath) {
87
+ const expandedPath = expandHome(filepath);
88
+
89
+ try {
90
+ await fs.access(expandedPath);
91
+ // File exists, create backup
92
+ const timestamp = new Date().toISOString().replace(/[:.]/g, "-");
93
+ const backupPath = `${expandedPath}.backup-${timestamp}`;
94
+ await fs.copyFile(expandedPath, backupPath);
95
+ return backupPath;
96
+ } catch (error) {
97
+ // File doesn't exist, no backup needed
98
+ return null;
99
+ }
100
+ }
101
+
102
+ /**
103
+ * Write a single file with options
104
+ * @param {string} filepath - File path
105
+ * @param {string} content - File content
106
+ * @param {Object} options - Options
107
+ * @param {boolean} [options.backup=false] - Create backup if file exists
108
+ * @param {number} [options.permissions] - File permissions (e.g., 0o600)
109
+ * @returns {Promise<Object>} Result object
110
+ */
111
+ async function writeFile(filepath, content, options = {}) {
112
+ const expandedPath = expandHome(filepath);
113
+ const dir = path.dirname(expandedPath);
114
+
115
+ // Ensure directory exists
116
+ await ensureDirectory(dir);
117
+
118
+ // Backup if requested
119
+ let backupPath = null;
120
+ if (options.backup) {
121
+ backupPath = await backupFile(expandedPath);
122
+ }
123
+
124
+ // Write file
125
+ await fs.writeFile(expandedPath, content, "utf8");
126
+
127
+ // Set permissions if specified
128
+ if (options.permissions !== undefined) {
129
+ await fs.chmod(expandedPath, options.permissions);
130
+ }
131
+
132
+ return {
133
+ path: expandedPath,
134
+ backup: backupPath,
135
+ created: backupPath === null,
136
+ };
137
+ }
138
+
139
+ /**
140
+ * Write multiple files at once
141
+ * @param {Object} files - Map of filepath to content
142
+ * @param {Object} options - Options
143
+ * @param {boolean} [options.backup=false] - Create backups
144
+ * @param {number} [options.permissions] - File permissions
145
+ * @returns {Promise<Object[]>} Array of result objects
146
+ */
147
+ async function writeFiles(files, options = {}) {
148
+ const results = [];
149
+
150
+ for (const [filepath, content] of Object.entries(files)) {
151
+ const result = await writeFile(filepath, content, options);
152
+ results.push(result);
153
+ }
154
+
155
+ return results;
156
+ }
157
+
158
+ /**
159
+ * Write config files with standard permissions (0o600)
160
+ * @param {Object} files - Map of filepath to content
161
+ * @param {Object} options - Options
162
+ * @param {boolean} [options.backup=true] - Create backups (default: true)
163
+ * @returns {Promise<Object[]>} Array of result objects
164
+ */
165
+ async function writeConfigFiles(files, options = {}) {
166
+ const defaultOptions = {
167
+ backup: true,
168
+ permissions: 0o600,
169
+ ...options,
170
+ };
171
+
172
+ return writeFiles(files, defaultOptions);
173
+ }
174
+
175
+ /**
176
+ * Check if a file or directory exists
177
+ * @param {string} filepath - Path to check
178
+ * @returns {Promise<boolean>} True if exists
179
+ */
180
+ async function exists(filepath) {
181
+ const expandedPath = expandHome(filepath);
182
+ try {
183
+ await fs.access(expandedPath);
184
+ return true;
185
+ } catch {
186
+ return false;
187
+ }
188
+ }
189
+
190
+ /**
191
+ * Get file stats
192
+ * @param {string} filepath - File path
193
+ * @returns {Promise<Object|null>} Stats object or null if doesn't exist
194
+ */
195
+ async function getStats(filepath) {
196
+ const expandedPath = expandHome(filepath);
197
+ try {
198
+ return await fs.stat(expandedPath);
199
+ } catch {
200
+ return null;
201
+ }
202
+ }
203
+
204
+ /**
205
+ * Create directories from mutt-config-core paths object
206
+ * @param {Object} paths - Paths object from mutt-config-core
207
+ * @returns {Promise<string[]>} Created directories
208
+ */
209
+ async function ensureDirectoriesFromPaths(paths) {
210
+ const dirs = new Set();
211
+
212
+ // Keys that represent files (not directories)
213
+ const fileKeys = ["accountmuttrc", "mainmuttrc"];
214
+
215
+ for (const [key, filepath] of Object.entries(paths)) {
216
+ const expandedPath = expandHome(filepath);
217
+ const normalizedKey = key.toLowerCase();
218
+
219
+ // Check if this is a file path or directory path
220
+ if (fileKeys.includes(normalizedKey)) {
221
+ // It's a file, create its parent directory
222
+ const dir = path.dirname(expandedPath);
223
+ dirs.add(dir);
224
+ } else {
225
+ // It's a directory path, create it directly
226
+ dirs.add(expandedPath);
227
+ }
228
+ }
229
+
230
+ return ensureDirectories(Array.from(dirs));
231
+ }
232
+
233
+ // Export API
234
+ module.exports = {
235
+ // Simple API (most users)
236
+ setupStandardMuttDirs,
237
+ writeConfigFiles,
238
+
239
+ // Flexible API (advanced users)
240
+ ensureDirectory,
241
+ ensureDirectories,
242
+ writeFile,
243
+ writeFiles,
244
+ backupFile,
245
+
246
+ // Utility functions
247
+ expandHome,
248
+ exists,
249
+ getStats,
250
+ ensureDirectoriesFromPaths,
251
+
252
+ // Constants
253
+ STANDARD_MUTT_DIRS,
254
+ };
255
+
256
+ // For testing
257
+ module.exports._internal = {
258
+ expandHome,
259
+ };
package/index.test.js ADDED
@@ -0,0 +1,376 @@
1
+ /**
2
+ * Tests for config-fs-utils
3
+ *
4
+ * Run with: npm test
5
+ */
6
+
7
+ const fs = require("fs").promises;
8
+ const path = require("path");
9
+ const os = require("os");
10
+ const {
11
+ setupStandardMuttDirs,
12
+ writeConfigFiles,
13
+ ensureDirectory,
14
+ ensureDirectories,
15
+ writeFile,
16
+ writeFiles,
17
+ backupFile,
18
+ expandHome,
19
+ exists,
20
+ getStats,
21
+ ensureDirectoriesFromPaths,
22
+ STANDARD_MUTT_DIRS,
23
+ _internal,
24
+ } = require("./index");
25
+
26
+ // Test utilities
27
+ const TEST_DIR = path.join(os.tmpdir(), "config-fs-utils-test");
28
+
29
+ beforeEach(async () => {
30
+ // Clean up test directory before each test
31
+ try {
32
+ await fs.rm(TEST_DIR, { recursive: true, force: true });
33
+ } catch (error) {
34
+ // Directory might not exist, ignore
35
+ }
36
+ await fs.mkdir(TEST_DIR, { recursive: true });
37
+ });
38
+
39
+ afterAll(async () => {
40
+ // Clean up after all tests
41
+ try {
42
+ await fs.rm(TEST_DIR, { recursive: true, force: true });
43
+ } catch (error) {
44
+ // Ignore cleanup errors
45
+ }
46
+ });
47
+
48
+ describe("config-fs-utils", () => {
49
+ describe("expandHome", () => {
50
+ test("expands ~ to home directory", () => {
51
+ const result = expandHome("~/test/path");
52
+ expect(result).toBe(path.join(os.homedir(), "test/path"));
53
+ });
54
+
55
+ test("expands ~ alone", () => {
56
+ const result = expandHome("~");
57
+ expect(result).toBe(os.homedir());
58
+ });
59
+
60
+ test("does not modify absolute paths", () => {
61
+ const result = expandHome("/absolute/path");
62
+ expect(result).toBe("/absolute/path");
63
+ });
64
+
65
+ test("does not modify relative paths", () => {
66
+ const result = expandHome("relative/path");
67
+ expect(result).toBe("relative/path");
68
+ });
69
+ });
70
+
71
+ describe("ensureDirectory", () => {
72
+ test("creates a single directory", async () => {
73
+ const testPath = path.join(TEST_DIR, "test-dir");
74
+ await ensureDirectory(testPath);
75
+
76
+ const stat = await fs.stat(testPath);
77
+ expect(stat.isDirectory()).toBe(true);
78
+ });
79
+
80
+ test("creates nested directories", async () => {
81
+ const testPath = path.join(TEST_DIR, "nested/deep/directory");
82
+ await ensureDirectory(testPath);
83
+
84
+ const stat = await fs.stat(testPath);
85
+ expect(stat.isDirectory()).toBe(true);
86
+ });
87
+
88
+ test("does not error if directory already exists", async () => {
89
+ const testPath = path.join(TEST_DIR, "existing-dir");
90
+ await fs.mkdir(testPath);
91
+
92
+ await expect(ensureDirectory(testPath)).resolves.not.toThrow();
93
+ });
94
+ });
95
+
96
+ describe("ensureDirectories", () => {
97
+ test("creates multiple directories", async () => {
98
+ const dirs = [
99
+ path.join(TEST_DIR, "dir1"),
100
+ path.join(TEST_DIR, "dir2"),
101
+ path.join(TEST_DIR, "dir3"),
102
+ ];
103
+
104
+ const created = await ensureDirectories(dirs);
105
+ expect(created).toHaveLength(3);
106
+
107
+ for (const dir of dirs) {
108
+ const stat = await fs.stat(dir);
109
+ expect(stat.isDirectory()).toBe(true);
110
+ }
111
+ });
112
+
113
+ test("returns created directory paths", async () => {
114
+ const dirs = [path.join(TEST_DIR, "test1"), path.join(TEST_DIR, "test2")];
115
+
116
+ const created = await ensureDirectories(dirs);
117
+ expect(created).toEqual(dirs);
118
+ });
119
+ });
120
+
121
+ describe("setupStandardMuttDirs", () => {
122
+ test("creates standard Mutt directory structure", async () => {
123
+ const result = await setupStandardMuttDirs({ baseDir: TEST_DIR });
124
+
125
+ expect(result).toHaveProperty("config");
126
+ expect(result).toHaveProperty("accounts");
127
+ expect(result).toHaveProperty("tokens");
128
+ expect(result).toHaveProperty("cacheHeaders");
129
+ expect(result).toHaveProperty("cacheBodies");
130
+
131
+ // Verify directories exist
132
+ for (const dir of Object.values(result)) {
133
+ const stat = await fs.stat(dir);
134
+ expect(stat.isDirectory()).toBe(true);
135
+ }
136
+ });
137
+
138
+ test("creates directories in home by default", async () => {
139
+ // We can't test actual home directory creation, but we can test the structure
140
+ const result = await setupStandardMuttDirs({ baseDir: TEST_DIR });
141
+
142
+ expect(result.config).toContain(".config/mutt");
143
+ expect(result.accounts).toContain(".config/mutt/accounts");
144
+ expect(result.tokens).toContain(".local/etc/oauth-tokens");
145
+ });
146
+ });
147
+
148
+ describe("backupFile", () => {
149
+ test("creates backup if file exists", async () => {
150
+ const testFile = path.join(TEST_DIR, "test.txt");
151
+ await fs.writeFile(testFile, "original content");
152
+
153
+ const backupPath = await backupFile(testFile);
154
+
155
+ expect(backupPath).not.toBeNull();
156
+ expect(backupPath).toContain(".backup-");
157
+
158
+ const backupContent = await fs.readFile(backupPath, "utf8");
159
+ expect(backupContent).toBe("original content");
160
+ });
161
+
162
+ test("returns null if file does not exist", async () => {
163
+ const testFile = path.join(TEST_DIR, "nonexistent.txt");
164
+ const backupPath = await backupFile(testFile);
165
+
166
+ expect(backupPath).toBeNull();
167
+ });
168
+ });
169
+
170
+ describe("writeFile", () => {
171
+ test("writes file with content", async () => {
172
+ const testFile = path.join(TEST_DIR, "test.txt");
173
+ await writeFile(testFile, "test content");
174
+
175
+ const content = await fs.readFile(testFile, "utf8");
176
+ expect(content).toBe("test content");
177
+ });
178
+
179
+ test("creates parent directories", async () => {
180
+ const testFile = path.join(TEST_DIR, "nested/deep/test.txt");
181
+ await writeFile(testFile, "test content");
182
+
183
+ const content = await fs.readFile(testFile, "utf8");
184
+ expect(content).toBe("test content");
185
+ });
186
+
187
+ test("creates backup when option is true", async () => {
188
+ const testFile = path.join(TEST_DIR, "test.txt");
189
+ await fs.writeFile(testFile, "original");
190
+
191
+ const result = await writeFile(testFile, "new content", { backup: true });
192
+
193
+ expect(result.backup).not.toBeNull();
194
+ expect(result.created).toBe(false);
195
+
196
+ const backupContent = await fs.readFile(result.backup, "utf8");
197
+ expect(backupContent).toBe("original");
198
+ });
199
+
200
+ test("sets file permissions", async () => {
201
+ const testFile = path.join(TEST_DIR, "test.txt");
202
+ await writeFile(testFile, "content", { permissions: 0o600 });
203
+
204
+ const stat = await fs.stat(testFile);
205
+ expect(stat.mode & 0o777).toBe(0o600);
206
+ });
207
+
208
+ test("returns result object", async () => {
209
+ const testFile = path.join(TEST_DIR, "test.txt");
210
+ const result = await writeFile(testFile, "content");
211
+
212
+ expect(result).toHaveProperty("path");
213
+ expect(result).toHaveProperty("backup");
214
+ expect(result).toHaveProperty("created");
215
+ expect(result.created).toBe(true);
216
+ });
217
+ });
218
+
219
+ describe("writeFiles", () => {
220
+ test("writes multiple files", async () => {
221
+ const files = {
222
+ [path.join(TEST_DIR, "file1.txt")]: "content1",
223
+ [path.join(TEST_DIR, "file2.txt")]: "content2",
224
+ [path.join(TEST_DIR, "file3.txt")]: "content3",
225
+ };
226
+
227
+ await writeFiles(files);
228
+
229
+ for (const [filepath, expectedContent] of Object.entries(files)) {
230
+ const content = await fs.readFile(filepath, "utf8");
231
+ expect(content).toBe(expectedContent);
232
+ }
233
+ });
234
+
235
+ test("returns array of results", async () => {
236
+ const files = {
237
+ [path.join(TEST_DIR, "file1.txt")]: "content1",
238
+ [path.join(TEST_DIR, "file2.txt")]: "content2",
239
+ };
240
+
241
+ const results = await writeFiles(files);
242
+
243
+ expect(results).toHaveLength(2);
244
+ expect(results[0]).toHaveProperty("path");
245
+ expect(results[1]).toHaveProperty("path");
246
+ });
247
+ });
248
+
249
+ describe("writeConfigFiles", () => {
250
+ test("writes files with 0o600 permissions by default", async () => {
251
+ const files = {
252
+ [path.join(TEST_DIR, "config.txt")]: "config content",
253
+ };
254
+
255
+ await writeConfigFiles(files);
256
+
257
+ const stat = await fs.stat(path.join(TEST_DIR, "config.txt"));
258
+ expect(stat.mode & 0o777).toBe(0o600);
259
+ });
260
+
261
+ test("creates backups by default", async () => {
262
+ const testFile = path.join(TEST_DIR, "config.txt");
263
+ await fs.writeFile(testFile, "original");
264
+
265
+ const results = await writeConfigFiles({
266
+ [testFile]: "new content",
267
+ });
268
+
269
+ expect(results[0].backup).not.toBeNull();
270
+ });
271
+ });
272
+
273
+ describe("exists", () => {
274
+ test("returns true for existing file", async () => {
275
+ const testFile = path.join(TEST_DIR, "exists.txt");
276
+ await fs.writeFile(testFile, "content");
277
+
278
+ const result = await exists(testFile);
279
+ expect(result).toBe(true);
280
+ });
281
+
282
+ test("returns false for non-existing file", async () => {
283
+ const testFile = path.join(TEST_DIR, "nonexistent.txt");
284
+ const result = await exists(testFile);
285
+ expect(result).toBe(false);
286
+ });
287
+
288
+ test("returns true for existing directory", async () => {
289
+ const result = await exists(TEST_DIR);
290
+ expect(result).toBe(true);
291
+ });
292
+ });
293
+
294
+ describe("getStats", () => {
295
+ test("returns stats for existing file", async () => {
296
+ const testFile = path.join(TEST_DIR, "test.txt");
297
+ await fs.writeFile(testFile, "content");
298
+
299
+ const stats = await getStats(testFile);
300
+ expect(stats).not.toBeNull();
301
+ expect(stats.isFile()).toBe(true);
302
+ });
303
+
304
+ test("returns null for non-existing file", async () => {
305
+ const testFile = path.join(TEST_DIR, "nonexistent.txt");
306
+ const stats = await getStats(testFile);
307
+ expect(stats).toBeNull();
308
+ });
309
+ });
310
+
311
+ describe("ensureDirectoriesFromPaths", () => {
312
+ test("creates directories from paths object", async () => {
313
+ const paths = {
314
+ accountMuttrc: path.join(
315
+ TEST_DIR,
316
+ ".config/mutt/accounts/test@gmail.com.muttrc"
317
+ ),
318
+ mainMuttrc: path.join(TEST_DIR, ".config/mutt/muttrc"),
319
+ oauthTokens: path.join(TEST_DIR, ".local/etc/oauth-tokens"),
320
+ cacheHeaders: path.join(TEST_DIR, ".cache/mutt/headers"),
321
+ cacheBodies: path.join(TEST_DIR, ".cache/mutt/bodies"),
322
+ };
323
+
324
+ await ensureDirectoriesFromPaths(paths);
325
+
326
+ // Check that parent directories were created for files
327
+ const accountsDir = await fs.stat(
328
+ path.join(TEST_DIR, ".config/mutt/accounts")
329
+ );
330
+ expect(accountsDir.isDirectory()).toBe(true);
331
+
332
+ const configDir = await fs.stat(path.join(TEST_DIR, ".config/mutt"));
333
+ expect(configDir.isDirectory()).toBe(true);
334
+
335
+ // Check that directory paths were created directly
336
+ const tokensDir = await fs.stat(
337
+ path.join(TEST_DIR, ".local/etc/oauth-tokens")
338
+ );
339
+ expect(tokensDir.isDirectory()).toBe(true);
340
+
341
+ const headersDir = await fs.stat(
342
+ path.join(TEST_DIR, ".cache/mutt/headers")
343
+ );
344
+ expect(headersDir.isDirectory()).toBe(true);
345
+ });
346
+ });
347
+
348
+ describe("Integration tests", () => {
349
+ test("complete workflow: setup dirs and write configs", async () => {
350
+ // Setup standard directories
351
+ const dirs = await setupStandardMuttDirs({ baseDir: TEST_DIR });
352
+
353
+ // Write config files
354
+ const files = {
355
+ [path.join(dirs.config, "muttrc")]: "main config",
356
+ [path.join(dirs.accounts, "test@gmail.com.muttrc")]: "account config",
357
+ };
358
+
359
+ const results = await writeConfigFiles(files);
360
+
361
+ // Verify files exist with correct content
362
+ const mainContent = await fs.readFile(
363
+ path.join(dirs.config, "muttrc"),
364
+ "utf8"
365
+ );
366
+ const accountContent = await fs.readFile(
367
+ path.join(dirs.accounts, "test@gmail.com.muttrc"),
368
+ "utf8"
369
+ );
370
+
371
+ expect(mainContent).toBe("main config");
372
+ expect(accountContent).toBe("account config");
373
+ expect(results).toHaveLength(2);
374
+ });
375
+ });
376
+ });
package/package.json ADDED
@@ -0,0 +1,33 @@
1
+ {
2
+ "name": "config-fs-utils",
3
+ "version": "0.1.0",
4
+ "description": "File system utilities for configuration file management",
5
+ "main": "index.js",
6
+ "scripts": {
7
+ "test": "jest",
8
+ "test:watch": "jest --watch",
9
+ "test:coverage": "jest --coverage"
10
+ },
11
+ "keywords": [
12
+ "config",
13
+ "filesystem",
14
+ "utilities",
15
+ "backup",
16
+ "permissions",
17
+ "mutt",
18
+ "neomutt"
19
+ ],
20
+ "author": "a-lost-social-misfit",
21
+ "license": "MIT",
22
+ "repository": {
23
+ "type": "git",
24
+ "url": "git+https://github.com/a-lost-social-misfit/config-fs-utils.git"
25
+ },
26
+ "devDependencies": {
27
+ "jest": "^30.2.0",
28
+ "mutt-config-core": "^0.1.0"
29
+ },
30
+ "engines": {
31
+ "node": ">=14.0.0"
32
+ }
33
+ }