claude-init 1.0.21 → 1.0.23

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.
@@ -7,8 +7,9 @@
7
7
  "Read(**/.env*)",
8
8
  "Edit(**/.env*)",
9
9
  "Write(**/.env*)",
10
- "Bash(**:.env*)"
11
- ]
10
+ "Bash(*.env*)"
11
+ ],
12
+ "defaultMode": "acceptEdits"
12
13
  },
13
14
  "statusLine": {
14
15
  "type": "command",
package/README.md CHANGED
@@ -5,9 +5,11 @@ A CLI tool to initialize Claude development environment with standardized config
5
5
  ## Features
6
6
 
7
7
  - **Smart Setup**: Automatically detects existing files and only creates/updates what's needed
8
- - **Non-destructive**: Never overwrites existing files, only adds missing content
8
+ - **Interactive Prompts**: Asks before overwriting existing command files or installing agents
9
+ - **Non-destructive by Default**: Preserves existing files unless you choose to overwrite
9
10
  - **Progress Feedback**: Clear visual indicators of what's being created, updated, or skipped
10
11
  - **Cross-platform**: Works on Windows, macOS, and Linux
12
+ - **CI-friendly**: Non-interactive mode with sensible defaults when not running in a TTY
11
13
 
12
14
  ## Requirements
13
15
 
@@ -57,12 +59,87 @@ Running `npx claude-init` sets up your current directory with:
57
59
  - **If missing**: Creates with default Claude settings
58
60
  - **If exists**: Skips to preserve your custom settings
59
61
 
60
- #### 📋 .claude/commands & .claude/agents
62
+ #### 📋 .claude/commands
61
63
  - **If missing**: Creates with all template files
62
- - **If exists**: Only adds missing files, preserves existing ones
64
+ - **If exists**: Only adds missing files by default
65
+ - **Interactive prompt**: If existing `.md` files are detected, you'll be asked whether to overwrite them with template versions
66
+ - Answer **Yes** to replace your customized `.md` files with fresh templates (while still adding any missing files)
67
+ - Answer **No** (default) to keep your existing `.md` files and only add missing files
68
+
69
+ #### 🤖 .claude/agents
70
+ - **Interactive prompt**: You'll be asked whether to install `.claude/agents` files
71
+ - Answer **Yes** (default) to install/update agent files
72
+ - Answer **No** to skip agents entirely
73
+
74
+ ### Interactive Prompts
75
+
76
+ When running interactively (in a terminal), `claude-init` will ask you:
77
+
78
+ 1. **Overwrite existing .claude/commands/\*.md?** (only if you have existing `.md` files that match templates)
79
+ - Prompt: `Overwrite existing .claude/commands/*.md with template versions? (y/N):`
80
+ - Default: **No** (preserves your customizations)
81
+ - If **Yes**: Replaces existing `.md` files with templates, still adds missing files
82
+ - If **No**: Keeps your existing files, only adds missing files
83
+
84
+ 2. **Install .claude/agents?**
85
+ - Prompt: `Install .claude/agents files? (Y/n):`
86
+ - Default: **Yes** (installs agents)
87
+ - If **Yes**: Installs/updates agents as normal
88
+ - If **No**: Skips agents entirely (shown in summary)
89
+
90
+ **Non-interactive mode (CI/scripts)**: When `process.stdin.isTTY` is false (e.g., in CI pipelines), prompts are skipped and defaults are used:
91
+ - `.claude/commands/*.md` files are **not** overwritten (preserves customizations)
92
+ - `.claude/agents` **is** installed (matches current behavior)
63
93
 
64
94
  ## Example Output
65
95
 
96
+ ### Interactive Run (First Time)
97
+
98
+ ```
99
+ 🚀 Claude Environment Initializer
100
+ Initializing in: /path/to/your/project
101
+
102
+ Install .claude/agents files? (Y/n): y
103
+
104
+ ✓ Created new CLAUDE.md
105
+ ✓ Created .devcontainer directory
106
+ ✓ Created .claude/settings.json
107
+ ✓ Created .claude/commands with 5 files
108
+ ✓ Created .claude/agents with 3 files
109
+
110
+ 📊 Summary:
111
+ Created: 5 items
112
+ Files added: 8
113
+
114
+ ✨ Claude environment is ready!
115
+ ```
116
+
117
+ ### Interactive Run (With Existing Files)
118
+
119
+ ```
120
+ 🚀 Claude Environment Initializer
121
+ Initializing in: /path/to/your/project
122
+
123
+ Overwrite existing .claude/commands/*.md with template versions? (y/N): n
124
+ Install .claude/agents files? (Y/n): n
125
+
126
+ - CLAUDE.md already contains template content
127
+ - Directory .devcontainer already exists
128
+ - File .claude/settings.json already exists
129
+ ↻ Added 1 missing files to .claude/commands (added: 1)
130
+ - All files in .claude/agents are up to date
131
+
132
+ 📊 Summary:
133
+ Updated: 1 items
134
+ Skipped: 3 items (already exist)
135
+ Files added: 1
136
+ - .claude/agents skipped by user
137
+
138
+ ✅ Claude environment is already up to date!
139
+ ```
140
+
141
+ ### Non-Interactive Run (Original Output)
142
+
66
143
  ```
67
144
  🚀 Claude Environment Initializer
68
145
  Initializing in: /path/to/your/project
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-init",
3
- "version": "1.0.21",
3
+ "version": "1.0.23",
4
4
  "description": "Initialize Claude development environment with configurations and templates",
5
5
  "type": "module",
6
6
  "bin": {
package/src/cli.js CHANGED
@@ -1,23 +1,39 @@
1
1
  import chalk from 'chalk';
2
2
  import ora from 'ora';
3
- import {
3
+ import path from 'path';
4
+ import fs from 'fs-extra';
5
+ import {
4
6
  handleClaudeMarkdown,
5
7
  handleDirectoryMirror,
6
8
  handleSelectiveFileCopy,
7
9
  handleSingleFile
8
10
  } from './fileManager.js';
11
+ import { promptYesNo } from './prompt.js';
12
+ import { getTemplatesPath, getFilesInDirectory } from './utils.js';
9
13
 
10
14
  /**
11
15
  * Format action result with appropriate colors and icons
12
16
  */
13
17
  function formatResult(result) {
14
- const { action, details, filesAdded } = result;
15
-
18
+ const { action, details, filesAdded, filesOverwritten } = result;
19
+
16
20
  switch (action) {
17
21
  case 'created':
18
22
  return chalk.green(`✓ ${details}`);
19
23
  case 'updated':
20
- const fileInfo = filesAdded !== undefined ? ` (${filesAdded} files)` : '';
24
+ let fileInfo = '';
25
+ if (filesAdded !== undefined || filesOverwritten !== undefined) {
26
+ const parts = [];
27
+ if (filesAdded !== undefined && filesAdded > 0) {
28
+ parts.push(`added: ${filesAdded}`);
29
+ }
30
+ if (filesOverwritten !== undefined && filesOverwritten > 0) {
31
+ parts.push(`overwritten: ${filesOverwritten}`);
32
+ }
33
+ if (parts.length > 0) {
34
+ fileInfo = ` (${parts.join(', ')})`;
35
+ }
36
+ }
21
37
  return chalk.yellow(`↻ ${details}${fileInfo}`);
22
38
  case 'skipped':
23
39
  return chalk.gray(`- ${details}`);
@@ -31,11 +47,38 @@ function formatResult(result) {
31
47
  */
32
48
  export async function cli() {
33
49
  const targetDir = process.cwd();
34
-
50
+
35
51
  console.log(chalk.blue.bold('🚀 Claude Environment Initializer'));
36
52
  console.log(chalk.gray(`Initializing in: ${targetDir}`));
37
53
  console.log();
38
-
54
+
55
+ // Determine if we should prompt for overwriting .claude/commands/*.md
56
+ let overwriteCommandsMd = false;
57
+ const commandsTargetPath = path.join(targetDir, '.claude/commands');
58
+ const commandsTemplatePath = path.join(getTemplatesPath(), '.claude/commands');
59
+
60
+ if (await fs.pathExists(commandsTargetPath) && await fs.pathExists(commandsTemplatePath)) {
61
+ const templateFiles = await getFilesInDirectory(commandsTemplatePath);
62
+ const targetFiles = await getFilesInDirectory(commandsTargetPath);
63
+
64
+ // Check if there are any overlapping .md files
65
+ const mdTemplateFiles = templateFiles.filter(f => f.endsWith('.md'));
66
+ const hasOverlappingMd = mdTemplateFiles.some(f => targetFiles.includes(f));
67
+
68
+ if (hasOverlappingMd) {
69
+ overwriteCommandsMd = await promptYesNo(
70
+ 'Overwrite existing .claude/commands/*.md with template versions?',
71
+ false
72
+ );
73
+ }
74
+ }
75
+
76
+ // Determine if we should install .claude/agents
77
+ const includeAgents = await promptYesNo(
78
+ 'Install .claude/agents files?',
79
+ true
80
+ );
81
+
39
82
  const tasks = [
40
83
  {
41
84
  name: 'CLAUDE.md',
@@ -51,13 +94,20 @@ export async function cli() {
51
94
  },
52
95
  {
53
96
  name: '.claude/commands',
54
- handler: () => handleSelectiveFileCopy(targetDir, '.claude/commands')
55
- },
56
- {
57
- name: '.claude/agents',
58
- handler: () => handleSelectiveFileCopy(targetDir, '.claude/agents')
97
+ handler: () => handleSelectiveFileCopy(targetDir, '.claude/commands', {
98
+ overwriteExisting: overwriteCommandsMd,
99
+ filter: (relPath) => relPath.endsWith('.md')
100
+ })
59
101
  }
60
102
  ];
103
+
104
+ // Conditionally add .claude/agents task
105
+ if (includeAgents) {
106
+ tasks.push({
107
+ name: '.claude/agents',
108
+ handler: () => handleSelectiveFileCopy(targetDir, '.claude/agents')
109
+ });
110
+ }
61
111
 
62
112
  let totalActions = { created: 0, updated: 0, skipped: 0, filesAdded: 0 };
63
113
 
@@ -99,9 +149,13 @@ export async function cli() {
99
149
  if (totalActions.filesAdded > 0) {
100
150
  console.log(chalk.cyan(` Files added: ${totalActions.filesAdded}`));
101
151
  }
102
-
152
+
153
+ if (!includeAgents) {
154
+ console.log(chalk.gray(` - .claude/agents skipped by user`));
155
+ }
156
+
103
157
  console.log();
104
-
158
+
105
159
  if (totalActions.created > 0 || totalActions.updated > 0) {
106
160
  console.log(chalk.green.bold('✨ Claude environment is ready!'));
107
161
  } else {
@@ -70,53 +70,84 @@ export async function handleDirectoryMirror(targetDir, relativePath) {
70
70
  }
71
71
 
72
72
  /**
73
- * Handle selective file copying (add missing files only)
73
+ * Handle selective file copying (add missing files only, optionally overwrite existing)
74
+ * @param {string} targetDir - The target directory
75
+ * @param {string} relativePath - The relative path to copy
76
+ * @param {Object} options - Options for the copy operation
77
+ * @param {boolean} options.overwriteExisting - Whether to overwrite existing files (default: false)
78
+ * @param {function} options.filter - Function to filter which files to overwrite (default: all files)
74
79
  */
75
- export async function handleSelectiveFileCopy(targetDir, relativePath) {
80
+ export async function handleSelectiveFileCopy(targetDir, relativePath, options = {}) {
81
+ const { overwriteExisting = false, filter = () => true } = options;
76
82
  const targetPath = path.join(targetDir, relativePath);
77
83
  const templatePath = path.join(getTemplatesPath(), relativePath);
78
-
84
+
79
85
  if (!(await fs.pathExists(targetPath))) {
80
86
  // Directory doesn't exist, create it with all template files
81
87
  await fs.copy(templatePath, targetPath);
82
88
  const files = await getFilesInDirectory(templatePath);
83
- return {
84
- action: 'created',
89
+ return {
90
+ action: 'created',
85
91
  details: `Created ${relativePath} with ${files.length} files`,
86
92
  filesAdded: files.length
87
93
  };
88
94
  }
89
-
90
- // Directory exists, check for missing files
95
+
96
+ // Directory exists, check for missing files and optionally overwrite existing
91
97
  const templateFiles = await getFilesInDirectory(templatePath);
92
98
  const targetFiles = await getFilesInDirectory(targetPath);
93
99
  const missingFiles = templateFiles.filter(file => !targetFiles.includes(file));
94
-
95
- if (missingFiles.length === 0) {
96
- return {
97
- action: 'skipped',
98
- details: `All files in ${relativePath} are up to date`,
99
- filesAdded: 0
100
- };
101
- }
102
-
100
+
101
+ let filesAdded = 0;
102
+ let filesOverwritten = 0;
103
+
103
104
  // Copy missing files
104
- let copiedCount = 0;
105
105
  for (const missingFile of missingFiles) {
106
106
  const sourcePath = path.join(templatePath, missingFile);
107
107
  const destPath = path.join(targetPath, missingFile);
108
-
108
+
109
109
  // Ensure destination directory exists
110
110
  await fs.ensureDir(path.dirname(destPath));
111
111
  await fs.copy(sourcePath, destPath);
112
- copiedCount++;
112
+ filesAdded++;
113
113
  }
114
-
115
- return {
116
- action: 'updated',
117
- details: `Added ${copiedCount} missing files to ${relativePath}`,
118
- filesAdded: copiedCount
114
+
115
+ // Optionally overwrite existing files that match filter
116
+ if (overwriteExisting) {
117
+ const existingFiles = templateFiles.filter(file => targetFiles.includes(file) && filter(file));
118
+
119
+ for (const existingFile of existingFiles) {
120
+ const sourcePath = path.join(templatePath, existingFile);
121
+ const destPath = path.join(targetPath, existingFile);
122
+
123
+ // Ensure destination directory exists
124
+ await fs.ensureDir(path.dirname(destPath));
125
+ await fs.copy(sourcePath, destPath);
126
+ filesOverwritten++;
127
+ }
128
+ }
129
+
130
+ if (filesAdded === 0 && filesOverwritten === 0) {
131
+ return {
132
+ action: 'skipped',
133
+ details: `All files in ${relativePath} are up to date`,
134
+ filesAdded: 0
135
+ };
136
+ }
137
+
138
+ const result = {
139
+ action: 'updated',
140
+ filesAdded
119
141
  };
142
+
143
+ if (overwriteExisting && filesOverwritten > 0) {
144
+ result.filesOverwritten = filesOverwritten;
145
+ result.details = `Added ${filesAdded} missing files, overwritten ${filesOverwritten} files in ${relativePath}`;
146
+ } else {
147
+ result.details = `Added ${filesAdded} missing files to ${relativePath}`;
148
+ }
149
+
150
+ return result;
120
151
  }
121
152
 
122
153
  /**
package/src/prompt.js ADDED
@@ -0,0 +1,44 @@
1
+ import readline from 'readline';
2
+
3
+ /**
4
+ * Prompts the user for a yes/no answer.
5
+ * @param {string} question - The question to ask
6
+ * @param {boolean} defaultAnswer - The default answer if user presses Enter or if non-TTY
7
+ * @returns {Promise<boolean>} - True for yes, false for no
8
+ */
9
+ export async function promptYesNo(question, defaultAnswer) {
10
+ // If not TTY, return default immediately
11
+ if (!process.stdin.isTTY) {
12
+ return defaultAnswer;
13
+ }
14
+
15
+ const rl = readline.createInterface({
16
+ input: process.stdin,
17
+ output: process.stdout
18
+ });
19
+
20
+ return new Promise((resolve) => {
21
+ const defaultHint = defaultAnswer ? 'Y/n' : 'y/N';
22
+ rl.question(`${question} (${defaultHint}): `, (answer) => {
23
+ rl.close();
24
+
25
+ const normalized = answer.trim().toLowerCase();
26
+
27
+ // Empty answer uses default
28
+ if (normalized === '') {
29
+ resolve(defaultAnswer);
30
+ return;
31
+ }
32
+
33
+ // Accept y, yes, n, no
34
+ if (normalized === 'y' || normalized === 'yes') {
35
+ resolve(true);
36
+ } else if (normalized === 'n' || normalized === 'no') {
37
+ resolve(false);
38
+ } else {
39
+ // Invalid input, use default
40
+ resolve(defaultAnswer);
41
+ }
42
+ });
43
+ });
44
+ }