cli-profile-manager 0.0.1

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.
@@ -0,0 +1,102 @@
1
+ import { homedir } from 'os';
2
+ import { join } from 'path';
3
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'fs';
4
+
5
+ const HOME = homedir();
6
+
7
+ /**
8
+ * Find Claude directory - checks project root first (codespaces), then home
9
+ */
10
+ function findClaudeDir() {
11
+ const candidates = [
12
+ join(process.cwd(), '.claude'), // Project root (codespaces, dev environments)
13
+ join(HOME, '.claude'), // Home directory (standard local install)
14
+ ];
15
+
16
+ for (const dir of candidates) {
17
+ if (existsSync(dir)) {
18
+ return dir;
19
+ }
20
+ }
21
+
22
+ // Default to home directory if not found
23
+ return join(HOME, '.claude');
24
+ }
25
+
26
+ // Default paths
27
+ const DEFAULTS = {
28
+ claudeDir: findClaudeDir(),
29
+ profilesDir: join(HOME, '.claude-profiles'),
30
+ cacheDir: join(HOME, '.claude-profiles', '.cache'),
31
+ configFile: join(HOME, '.claude-profiles', 'config.json'),
32
+ marketplaceRepo: 'brrichards/cli-profile-manager'
33
+ };
34
+
35
+ /**
36
+ * Ensure required directories exist
37
+ */
38
+ export function ensureDirs() {
39
+ const dirs = [DEFAULTS.profilesDir, DEFAULTS.cacheDir];
40
+ for (const dir of dirs) {
41
+ if (!existsSync(dir)) {
42
+ mkdirSync(dir, { recursive: true });
43
+ }
44
+ }
45
+ }
46
+
47
+ /**
48
+ * Get current configuration
49
+ */
50
+ export async function getConfig() {
51
+ ensureDirs();
52
+
53
+ let userConfig = {};
54
+
55
+ if (existsSync(DEFAULTS.configFile)) {
56
+ try {
57
+ userConfig = JSON.parse(readFileSync(DEFAULTS.configFile, 'utf-8'));
58
+ } catch (e) {
59
+ // Ignore invalid config
60
+ }
61
+ }
62
+
63
+ return {
64
+ ...DEFAULTS,
65
+ ...userConfig
66
+ };
67
+ }
68
+
69
+ /**
70
+ * Update configuration
71
+ */
72
+ export async function updateConfig(updates) {
73
+ ensureDirs();
74
+
75
+ const current = await getConfig();
76
+ const newConfig = { ...current, ...updates };
77
+
78
+ // Only save user-configurable options
79
+ const toSave = {
80
+ marketplaceRepo: newConfig.marketplaceRepo
81
+ };
82
+
83
+ writeFileSync(DEFAULTS.configFile, JSON.stringify(toSave, null, 2));
84
+
85
+ return newConfig;
86
+ }
87
+
88
+ /**
89
+ * Get the path for a local profile
90
+ */
91
+ export function getProfilePath(name) {
92
+ return join(DEFAULTS.profilesDir, name);
93
+ }
94
+
95
+ /**
96
+ * Check if Claude directory exists
97
+ */
98
+ export function claudeDirExists() {
99
+ return existsSync(DEFAULTS.claudeDir);
100
+ }
101
+
102
+ export { DEFAULTS };
@@ -0,0 +1,421 @@
1
+ import { existsSync, mkdirSync, readdirSync, statSync, readFileSync, writeFileSync, cpSync, rmSync } from 'fs';
2
+ import { join, dirname, sep } from 'path';
3
+ import { getConfig, DEFAULTS } from './config.js';
4
+
5
+ // Files/patterns to exclude by default (secrets, caches, infra)
6
+ const DEFAULT_EXCLUDES = [
7
+ '.credentials',
8
+ '.auth',
9
+ '*.key',
10
+ '*.pem',
11
+ '*.secret',
12
+ 'oauth_token*',
13
+ '.cache',
14
+ 'node_modules',
15
+ '.git',
16
+ // Claude Code plugin infrastructure -- present in every install,
17
+ // not user-authored content. Excluded from snapshots and preserved
18
+ // during profile installs (cleanProfileContent).
19
+ 'plugins/cache',
20
+ 'plugins/install-counts-cache',
21
+ 'plugins/installed_plugins',
22
+ 'plugins/known_marketplaces',
23
+ 'plugins/marketplaces'
24
+ ];
25
+
26
+ // Plugin subdirectories that are Claude Code infrastructure.
27
+ // These must be preserved when cleaning profile content.
28
+ const PLUGIN_INFRA_DIRS = [
29
+ 'cache',
30
+ 'install-counts-cache',
31
+ 'installed_plugins',
32
+ 'known_marketplaces',
33
+ 'marketplaces'
34
+ ];
35
+
36
+ // Files that are safe to include (functional customizations only)
37
+ const SAFE_INCLUDES = [
38
+ 'CLAUDE.md',
39
+ 'commands',
40
+ 'commands/**',
41
+ 'skills',
42
+ 'skills/**',
43
+ 'hooks',
44
+ 'hooks/**',
45
+ 'plugins',
46
+ 'plugins/**',
47
+ 'mcp.json',
48
+ 'mcp_servers',
49
+ 'mcp_servers/**',
50
+ 'agents',
51
+ 'agents/**'
52
+ ];
53
+
54
+ /**
55
+ * Create a snapshot of the .claude folder by copying files directly
56
+ */
57
+ export async function createSnapshot(profileName, options = {}) {
58
+ const config = await getConfig();
59
+ const claudeDir = config.claudeDir;
60
+ const profileDir = join(config.profilesDir, profileName);
61
+
62
+ if (!existsSync(claudeDir)) {
63
+ throw new Error(`Claude directory not found: ${claudeDir}`);
64
+ }
65
+
66
+ // Create profile directory
67
+ if (existsSync(profileDir)) {
68
+ throw new Error(`Profile "${profileName}" already exists. Use a different name or delete the existing one.`);
69
+ }
70
+
71
+ mkdirSync(profileDir, { recursive: true });
72
+
73
+ const metadataPath = join(profileDir, 'profile.json');
74
+
75
+ // Create metadata
76
+ const metadata = {
77
+ name: profileName,
78
+ version: '1.0.0',
79
+ description: options.description || '',
80
+ tags: options.tags ? options.tags.split(',').map(t => t.trim()) : [],
81
+ createdAt: new Date().toISOString(),
82
+ claudeVersion: await getClaudeVersion(),
83
+ platform: process.platform,
84
+ includesSecrets: options.includeSecrets || false,
85
+ files: []
86
+ };
87
+
88
+ // Get list of files to include
89
+ const files = getFilesToArchive(claudeDir, options.includeSecrets);
90
+ metadata.files = files;
91
+
92
+ // Copy each file into the profile directory
93
+ for (const file of files) {
94
+ const srcPath = join(claudeDir, file);
95
+ const destPath = join(profileDir, file);
96
+
97
+ // Ensure parent directory exists
98
+ mkdirSync(dirname(destPath), { recursive: true });
99
+
100
+ const content = readFileSync(srcPath);
101
+ writeFileSync(destPath, content);
102
+ }
103
+
104
+ // Derive structured contents from file list
105
+ metadata.contents = deriveContentsWithMcp(metadata.files, claudeDir);
106
+
107
+ // Save metadata
108
+ writeFileSync(metadataPath, JSON.stringify(metadata, null, 2));
109
+
110
+ return { profileDir, metadata };
111
+ }
112
+
113
+ /**
114
+ * Derive a structured contents summary from a list of file paths.
115
+ * Returns an object with category keys mapping to arrays of item names.
116
+ */
117
+ export function deriveContents(files) {
118
+ const contents = {};
119
+
120
+ for (const file of files) {
121
+ const normalized = file.split(sep).join('/');
122
+
123
+ if (normalized === 'CLAUDE.md') {
124
+ if (!contents.instructions) contents.instructions = [];
125
+ contents.instructions.push('CLAUDE.md');
126
+ continue;
127
+ }
128
+
129
+ if (normalized === 'mcp.json') {
130
+ if (!contents.mcp) contents.mcp = [];
131
+ contents.mcp.push('mcp.json');
132
+ continue;
133
+ }
134
+
135
+ const parts = normalized.split('/');
136
+ if (parts.length >= 2) {
137
+ const category = parts[0];
138
+ const itemName = parts[1].replace(/\.[^.]+$/, ''); // strip extension
139
+ if (!contents[category]) contents[category] = [];
140
+ if (!contents[category].includes(itemName)) {
141
+ contents[category].push(itemName);
142
+ }
143
+ }
144
+ }
145
+
146
+ return contents;
147
+ }
148
+
149
+ /**
150
+ * Derive contents and try to enrich MCP server names from the actual mcp.json file
151
+ */
152
+ function deriveContentsWithMcp(files, claudeDir) {
153
+ const contents = deriveContents(files);
154
+
155
+ if (contents.mcp && claudeDir) {
156
+ try {
157
+ const mcpPath = join(claudeDir, 'mcp.json');
158
+ if (existsSync(mcpPath)) {
159
+ const mcpData = JSON.parse(readFileSync(mcpPath, 'utf-8'));
160
+ const serverNames = Object.keys(mcpData.mcpServers || mcpData);
161
+ if (serverNames.length > 0) {
162
+ contents.mcp = serverNames;
163
+ }
164
+ }
165
+ } catch {
166
+ // Keep the fallback
167
+ }
168
+ }
169
+
170
+ return contents;
171
+ }
172
+
173
+ /**
174
+ * Get list of files to archive (using allowlist approach)
175
+ */
176
+ export function getFilesToArchive(dir, includeSecrets = false) {
177
+ const files = [];
178
+ const excludes = includeSecrets ? [] : DEFAULT_EXCLUDES;
179
+
180
+ function walk(currentDir, relativePath = '') {
181
+ const entries = readdirSync(currentDir);
182
+
183
+ for (const entry of entries) {
184
+ const fullPath = join(currentDir, entry);
185
+ const relPath = relativePath ? join(relativePath, entry) : entry;
186
+
187
+ if (!isAllowed(entry, relPath)) {
188
+ continue;
189
+ }
190
+
191
+ if (shouldExclude(entry, relPath, excludes)) {
192
+ continue;
193
+ }
194
+
195
+ const stat = statSync(fullPath);
196
+
197
+ if (stat.isDirectory()) {
198
+ walk(fullPath, relPath);
199
+ } else {
200
+ // Always use forward slashes for portable metadata
201
+ files.push(relPath.split(sep).join('/'));
202
+ }
203
+ }
204
+ }
205
+
206
+ walk(dir);
207
+ return files;
208
+ }
209
+
210
+ /**
211
+ * Check if a file/folder is in the allowlist
212
+ */
213
+ function isAllowed(name, path) {
214
+ const normalizedPath = path.split(sep).join('/');
215
+
216
+ for (const pattern of SAFE_INCLUDES) {
217
+ if (pattern.endsWith('/**')) {
218
+ const dirName = pattern.slice(0, -3);
219
+ if (normalizedPath.startsWith(dirName + '/') || normalizedPath === dirName) {
220
+ return true;
221
+ }
222
+ } else if (name === pattern || normalizedPath === pattern) {
223
+ return true;
224
+ }
225
+ }
226
+ return false;
227
+ }
228
+
229
+ /**
230
+ * Check if a file/folder should be excluded
231
+ */
232
+ function shouldExclude(name, path, excludes) {
233
+ const normalizedPath = path.split(sep).join('/');
234
+ for (const pattern of excludes) {
235
+ if (pattern.startsWith('*.')) {
236
+ const ext = pattern.slice(1);
237
+ if (name.endsWith(ext)) return true;
238
+ } else if (name === pattern || normalizedPath === pattern) {
239
+ return true;
240
+ } else if (pattern.endsWith('*') && name.startsWith(pattern.slice(0, -1))) {
241
+ return true;
242
+ }
243
+ }
244
+ return false;
245
+ }
246
+
247
+ /**
248
+ * Extract a profile to the .claude folder by copying files directly.
249
+ * Uses a merge strategy to work even when Claude Code is running.
250
+ */
251
+ export async function extractSnapshot(profileName, options = {}) {
252
+ const config = await getConfig();
253
+ const profileDir = join(config.profilesDir, profileName);
254
+ const claudeDir = config.claudeDir;
255
+
256
+ if (!existsSync(profileDir) || !existsSync(join(profileDir, 'profile.json'))) {
257
+ throw new Error(`Profile "${profileName}" not found or corrupted`);
258
+ }
259
+
260
+ // Backup existing .claude if requested
261
+ if (options.backup && existsSync(claudeDir)) {
262
+ const backupName = `.claude-backup-${Date.now()}`;
263
+ const backupPath = join(DEFAULTS.profilesDir, backupName);
264
+ cpSync(claudeDir, backupPath, { recursive: true });
265
+ }
266
+
267
+ // Check if we need force flag
268
+ if (existsSync(claudeDir) && !options.force) {
269
+ throw new Error('Claude directory exists. Use --force to overwrite or --backup to save current config.');
270
+ }
271
+
272
+ // Ensure .claude directory exists
273
+ mkdirSync(claudeDir, { recursive: true });
274
+
275
+ // Clean out old profile content before installing new files
276
+ cleanProfileContent(claudeDir);
277
+
278
+ // Copy profile files (excluding profile.json) into .claude
279
+ copyProfileFiles(profileDir, claudeDir);
280
+
281
+ return { claudeDir };
282
+ }
283
+
284
+ /**
285
+ * Remove existing profile content from a .claude directory.
286
+ * Only removes items in the SAFE_INCLUDES allowlist (commands/, hooks/, etc.)
287
+ * so non-profile files (settings, credentials) are preserved.
288
+ */
289
+ export function cleanProfileContent(claudeDir) {
290
+ // Directories to wipe entirely
291
+ const contentDirs = ['commands', 'skills', 'hooks', 'mcp_servers', 'agents'];
292
+ for (const dir of contentDirs) {
293
+ const dirPath = join(claudeDir, dir);
294
+ if (existsSync(dirPath)) {
295
+ rmSync(dirPath, { recursive: true, force: true });
296
+ }
297
+ }
298
+
299
+ // Plugins: only remove user-authored content, preserve Claude Code infra
300
+ const pluginsDir = join(claudeDir, 'plugins');
301
+ if (existsSync(pluginsDir)) {
302
+ for (const entry of readdirSync(pluginsDir)) {
303
+ if (!PLUGIN_INFRA_DIRS.includes(entry)) {
304
+ rmSync(join(pluginsDir, entry), { recursive: true, force: true });
305
+ }
306
+ }
307
+ }
308
+
309
+ // Individual files to remove
310
+ const contentFiles = ['CLAUDE.md', 'mcp.json'];
311
+ for (const file of contentFiles) {
312
+ const filePath = join(claudeDir, file);
313
+ if (existsSync(filePath)) {
314
+ rmSync(filePath, { force: true });
315
+ }
316
+ }
317
+ }
318
+
319
+ /**
320
+ * Copy profile content files (not profile.json) from source to destination.
321
+ */
322
+ function copyProfileFiles(srcDir, destDir) {
323
+ const entries = readdirSync(srcDir, { withFileTypes: true });
324
+
325
+ for (const entry of entries) {
326
+ // Skip profile.json -- it's metadata, not a profile file
327
+ if (entry.name === 'profile.json') continue;
328
+
329
+ const srcPath = join(srcDir, entry.name);
330
+ const destPath = join(destDir, entry.name);
331
+
332
+ if (entry.isDirectory()) {
333
+ mkdirSync(destPath, { recursive: true });
334
+ copyDirMerge(srcPath, destPath);
335
+ } else {
336
+ try {
337
+ const content = readFileSync(srcPath);
338
+ writeFileSync(destPath, content);
339
+ } catch (err) {
340
+ if (err.code === 'EBUSY') {
341
+ throw new Error(`Cannot write to ${entry.name} - file is locked. Please close Claude Code and try again.`);
342
+ }
343
+ throw err;
344
+ }
345
+ }
346
+ }
347
+ }
348
+
349
+ /**
350
+ * Recursively copy/merge directory contents, overwriting files.
351
+ * This works even when the target directory has open file handles.
352
+ */
353
+ export function copyDirMerge(src, dest) {
354
+ const entries = readdirSync(src, { withFileTypes: true });
355
+
356
+ for (const entry of entries) {
357
+ const srcPath = join(src, entry.name);
358
+ const destPath = join(dest, entry.name);
359
+
360
+ if (entry.isDirectory()) {
361
+ mkdirSync(destPath, { recursive: true });
362
+ copyDirMerge(srcPath, destPath);
363
+ } else {
364
+ try {
365
+ const content = readFileSync(srcPath);
366
+ writeFileSync(destPath, content);
367
+ } catch (err) {
368
+ if (err.code === 'EBUSY') {
369
+ throw new Error(`Cannot write to ${entry.name} - file is locked. Please close Claude Code and try again.`);
370
+ }
371
+ throw err;
372
+ }
373
+ }
374
+ }
375
+ }
376
+
377
+ /**
378
+ * Get Claude CLI version if installed
379
+ */
380
+ async function getClaudeVersion() {
381
+ try {
382
+ const { execSync } = await import('child_process');
383
+ const version = execSync('claude --version', { encoding: 'utf-8' }).trim();
384
+ return version;
385
+ } catch {
386
+ return 'unknown';
387
+ }
388
+ }
389
+
390
+ /**
391
+ * Read profile metadata
392
+ */
393
+ export function readProfileMetadata(profileName) {
394
+ const config = DEFAULTS;
395
+ const metadataPath = join(config.profilesDir, profileName, 'profile.json');
396
+
397
+ if (!existsSync(metadataPath)) {
398
+ return null;
399
+ }
400
+
401
+ return JSON.parse(readFileSync(metadataPath, 'utf-8'));
402
+ }
403
+
404
+ /**
405
+ * List all local profiles
406
+ */
407
+ export function listLocalProfileNames() {
408
+ const profilesDir = DEFAULTS.profilesDir;
409
+
410
+ if (!existsSync(profilesDir)) {
411
+ return [];
412
+ }
413
+
414
+ return readdirSync(profilesDir)
415
+ .filter(name => {
416
+ if (name.startsWith('.')) return false;
417
+ const profilePath = join(profilesDir, name);
418
+ const stat = statSync(profilePath);
419
+ return stat.isDirectory() && existsSync(join(profilePath, 'profile.json'));
420
+ });
421
+ }
@@ -0,0 +1,22 @@
1
+ import { describe, it } from 'node:test';
2
+ import assert from 'node:assert';
3
+ import { execSync } from 'child_process';
4
+ import { join } from 'path';
5
+
6
+ const CLI = join(import.meta.dirname, '..', 'src', 'cli.js');
7
+
8
+ describe('CLI smoke test', () => {
9
+ it('--help exits 0 and shows expected commands', () => {
10
+ const output = execSync(`node ${CLI} --help`, { encoding: 'utf-8' });
11
+ assert.ok(output.includes('save'), 'should list save command');
12
+ assert.ok(output.includes('load'), 'should list load command');
13
+ assert.ok(output.includes('install'), 'should list install command');
14
+ assert.ok(output.includes('publish'), 'should list publish command');
15
+ assert.ok(output.includes('list'), 'should list list command');
16
+ });
17
+
18
+ it('--version outputs a version string', () => {
19
+ const output = execSync(`node ${CLI} --version`, { encoding: 'utf-8' });
20
+ assert.match(output.trim(), /^\d+\.\d+\.\d+$/);
21
+ });
22
+ });
@@ -0,0 +1,15 @@
1
+ import { describe, it } from 'node:test';
2
+ import assert from 'node:assert';
3
+
4
+ describe('config defaults', () => {
5
+ it('marketplaceRepo defaults to brrichards/cli-profile-manager', async () => {
6
+ const { DEFAULTS } = await import('../src/utils/config.js');
7
+ assert.strictEqual(DEFAULTS.marketplaceRepo, 'brrichards/cli-profile-manager');
8
+ });
9
+
10
+ it('getConfig returns the new marketplace repo', async () => {
11
+ const { getConfig } = await import('../src/utils/config.js');
12
+ const config = await getConfig();
13
+ assert.strictEqual(config.marketplaceRepo, 'brrichards/cli-profile-manager');
14
+ });
15
+ });
@@ -0,0 +1,39 @@
1
+ import { describe, it } from 'node:test';
2
+ import assert from 'node:assert';
3
+ import { readFileSync, readdirSync, statSync } from 'fs';
4
+ import { join } from 'path';
5
+
6
+ const ROOT = join(import.meta.dirname, '..');
7
+ const OLD_REPO = 'brennanr9/claude-profile-manager';
8
+
9
+ function collectFiles(dir, extensions) {
10
+ const results = [];
11
+ for (const entry of readdirSync(dir, { withFileTypes: true })) {
12
+ const full = join(dir, entry.name);
13
+ if (entry.name === 'node_modules' || entry.name === '.git') continue;
14
+ if (entry.isDirectory()) {
15
+ results.push(...collectFiles(full, extensions));
16
+ } else if (extensions.some(ext => entry.name.endsWith(ext))) {
17
+ results.push(full);
18
+ }
19
+ }
20
+ return results;
21
+ }
22
+
23
+ describe('repo reference cleanup', () => {
24
+ const extensions = ['.js', '.mjs', '.json', '.md', '.yml'];
25
+ const files = collectFiles(ROOT, extensions);
26
+
27
+ it('no source files reference the old repo', () => {
28
+ const violations = [];
29
+ for (const file of files) {
30
+ // Skip test files themselves and package-lock
31
+ if (file.includes('/test/') || file.includes('package-lock')) continue;
32
+ const content = readFileSync(file, 'utf-8');
33
+ if (content.includes(OLD_REPO)) {
34
+ violations.push(file.replace(ROOT + '/', ''));
35
+ }
36
+ }
37
+ assert.deepStrictEqual(violations, [], `Files still referencing ${OLD_REPO}: ${violations.join(', ')}`);
38
+ });
39
+ });
@@ -0,0 +1,61 @@
1
+ import { describe, it } from 'node:test';
2
+ import assert from 'node:assert';
3
+
4
+ describe('deriveContents', () => {
5
+ it('categorizes files into correct content buckets', async () => {
6
+ const { deriveContents } = await import('../src/utils/snapshot.js');
7
+
8
+ const files = [
9
+ 'CLAUDE.md',
10
+ 'commands/review.md',
11
+ 'commands/test-gen.md',
12
+ 'hooks/pre-commit.md',
13
+ 'mcp.json',
14
+ 'skills/debugging/SKILL.md'
15
+ ];
16
+
17
+ const contents = deriveContents(files);
18
+
19
+ assert.deepStrictEqual(contents.instructions, ['CLAUDE.md']);
20
+ assert.deepStrictEqual(contents.commands, ['review', 'test-gen']);
21
+ assert.deepStrictEqual(contents.hooks, ['pre-commit']);
22
+ assert.deepStrictEqual(contents.mcp, ['mcp.json']);
23
+ assert.deepStrictEqual(contents.skills, ['debugging']);
24
+ });
25
+
26
+ it('returns empty object for empty file list', async () => {
27
+ const { deriveContents } = await import('../src/utils/snapshot.js');
28
+ const contents = deriveContents([]);
29
+ assert.deepStrictEqual(contents, {});
30
+ });
31
+ });
32
+
33
+ describe('getFilesToArchive', () => {
34
+ it('only includes files matching the allowlist', async () => {
35
+ const { getFilesToArchive } = await import('../src/utils/snapshot.js');
36
+ const { mkdirSync, writeFileSync, rmSync } = await import('fs');
37
+ const { join } = await import('path');
38
+ const { tmpdir } = await import('os');
39
+
40
+ // Create a temp .claude-like directory
41
+ const testDir = join(tmpdir(), `cpm-test-${Date.now()}`);
42
+ mkdirSync(join(testDir, 'commands'), { recursive: true });
43
+ mkdirSync(join(testDir, 'secrets'), { recursive: true });
44
+ writeFileSync(join(testDir, 'CLAUDE.md'), 'instructions');
45
+ writeFileSync(join(testDir, 'commands', 'foo.md'), 'command');
46
+ writeFileSync(join(testDir, 'secrets', 'key.pem'), 'secret');
47
+ writeFileSync(join(testDir, '.credentials'), 'creds');
48
+ writeFileSync(join(testDir, 'random.txt'), 'not allowed');
49
+
50
+ try {
51
+ const files = getFilesToArchive(testDir);
52
+ assert.ok(files.includes('CLAUDE.md'), 'should include CLAUDE.md');
53
+ assert.ok(files.includes('commands/foo.md'), 'should include commands/foo.md');
54
+ assert.ok(!files.includes('secrets/key.pem'), 'should not include secrets/');
55
+ assert.ok(!files.includes('.credentials'), 'should not include .credentials');
56
+ assert.ok(!files.includes('random.txt'), 'should not include random.txt');
57
+ } finally {
58
+ rmSync(testDir, { recursive: true, force: true });
59
+ }
60
+ });
61
+ });