@garyr/pt-cli 0.36.4 → 0.38.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.
package/README.md CHANGED
@@ -45,13 +45,15 @@ graph LR
45
45
  - Initialize new projects from learned templates
46
46
  - Define template variables for dynamic file customization
47
47
  - **Automatic Variable Detection:** Scans text files for `{{ var }}` syntax during `learn`/`update`
48
+ - **Nested Variable Expansion (v0.36.0+):** Variables can contain other variable placeholders that are resolved iteratively
49
+ - **Parent Directory `.env` File Scanning:** Automatically scans parent directories for `.env` files and uses their values as defaults
48
50
  - Auto-detect and suggest post-config setup tasks
49
51
  - Configure global post-config tasks in `~/.pt/config.yaml` (apply to all projects)
50
52
  - Baked-in defaults for common project types (javascript, python, godot, etc.)
51
53
  - Share templates or use as an API with JSON export/import
52
54
  - **Direct JSON scaffolding:** Initialize projects from a JSON file without registering in `config.yaml`
53
55
  - **Portable template configs:** `.pt-template.json` files make shared directories fully self-describing
54
- - Fully supports non-interactive mode (`--yes`, `--vars`) for AI agent automation
56
+ - Fully supports non-interactive mode (`--yes`, `--vars`, `--name`, `--desc`) for AI agent automation
55
57
 
56
58
  ## Quick Start
57
59
 
@@ -5,6 +5,61 @@ import { loadConfig, sanitizePath } from '../config.js';
5
5
  import chalk from 'chalk';
6
6
  import { processCopyFiles } from '../substitute.js';
7
7
  import { execSync } from 'child_process';
8
+ /**
9
+ * Scan parent directories for .env files and parse their variables.
10
+ * Returns a map of variable names to their values, supporting:
11
+ * - KEY=VALUE format
12
+ * - KEY="VALUE with spaces" format
13
+ * - KEY='VALUE with spaces' format
14
+ * - Comments (lines starting with #)
15
+ * - Empty lines
16
+ */
17
+ function scanEnvForVariables(targetPath) {
18
+ const envVars = {};
19
+ let currentDir = path.resolve(targetPath);
20
+ // Scan up to 5 parent directories for .env files
21
+ const maxDepth = 5;
22
+ for (let depth = 0; depth < maxDepth; depth++) {
23
+ const envPath = path.join(currentDir, '.env');
24
+ if (fs.existsSync(envPath)) {
25
+ try {
26
+ const content = fs.readFileSync(envPath, 'utf-8');
27
+ const lines = content.split('\n');
28
+ for (const line of lines) {
29
+ const trimmed = line.trim();
30
+ // Skip empty lines and comments
31
+ if (!trimmed || trimmed.startsWith('#')) {
32
+ continue;
33
+ }
34
+ // Match KEY=VALUE patterns
35
+ const match = trimmed.match(/^([A-Za-z_][A-Za-z0-9_]*)\s*=\s*(.+)$/);
36
+ if (match) {
37
+ const key = match[1];
38
+ let value = match[2];
39
+ // Remove surrounding quotes if present
40
+ if ((value.startsWith('"') && value.endsWith('"')) ||
41
+ (value.startsWith("'") && value.endsWith("'"))) {
42
+ value = value.slice(1, -1);
43
+ }
44
+ envVars[key] = value;
45
+ }
46
+ }
47
+ }
48
+ catch (err) {
49
+ // Silently skip unreadable .env files
50
+ continue;
51
+ }
52
+ }
53
+ // Move to parent directory
54
+ const parentDir = path.dirname(currentDir);
55
+ if (parentDir === currentDir) {
56
+ // Reached filesystem root
57
+ break;
58
+ }
59
+ currentDir = parentDir;
60
+ }
61
+ return envVars;
62
+ }
8
63
  export async function init(targetName, destPath, options = {}) {
9
64
  const config = loadConfig();
10
65
  let typeName = targetName;
@@ -86,6 +141,16 @@ export async function init(targetName, destPath, options = {}) {
86
141
  // Handle Variables
87
142
  let variables = {};
88
143
  if (template.variables && template.variables.length > 0) {
144
+ // Scan parent directories for .env files and pre-fill variables
145
+ const envVars = scanEnvForVariables(resolvedDest);
146
+ // Merge .env variables into variables (with lower priority than --vars)
147
+ if (Object.keys(envVars).length > 0) {
148
+ for (const [key, value] of Object.entries(envVars)) {
149
+ if (!variables[key]) {
150
+ variables[key] = value;
151
+ }
152
+ }
153
+ }
89
154
  if (options.vars) {
90
155
  // Parse --vars "key=val,key2=val2"
91
156
  const pairs = options.vars.split(',').map((p) => p.trim());
@@ -9,7 +9,16 @@ export async function learn(sourcePath, updateTemplate = null, options = {}) {
9
9
  // Phase 1: Remote Check
10
10
  if (sourcePath.startsWith('http')) {
11
11
  console.log(chalk.cyan(`Downloading remote template from: ${sourcePath}...`));
12
- resolvedPath = await downloadAndExtract(sourcePath, options.json || false, options.allowUntrusted || false);
12
+ try {
13
+ resolvedPath = await downloadAndExtract(sourcePath, options.json || false, options.allowUntrusted || false);
14
+ }
15
+ catch (err) {
16
+ if (err.message === 'Download cancelled by user due to untrusted source') {
17
+ console.log(chalk.yellow('Download cancelled. Exiting.'));
18
+ process.exit(0);
19
+ }
20
+ throw err;
21
+ }
13
22
  }
14
23
  else {
15
24
  resolvedPath = path.resolve(sourcePath);
@@ -65,6 +74,8 @@ export async function learn(sourcePath, updateTemplate = null, options = {}) {
65
74
  }
66
75
  }
67
76
  else {
77
+ // Track whether name came from .info.md or JSON (not user-provided)
78
+ const nameFromSource = !options.name && (fileTemplateConfig.name || infoName);
68
79
  if (options.name) {
69
80
  targetName = options.name;
70
81
  }
@@ -92,6 +103,34 @@ export async function learn(sourcePath, updateTemplate = null, options = {}) {
92
103
  targetName = newName;
93
104
  }
94
105
  }
106
+ // If name came from .info.md or JSON, prompt to confirm/edit
107
+ // This prevents accidental overwrites when creating new templates based on existing ones
108
+ if (nameFromSource && !options.json) {
109
+ if (options.yes) {
110
+ // In --yes mode, keep the auto-detected name but warn
111
+ const existingNames = getTemplateNames(config);
112
+ if (existingNames.includes(targetName)) {
113
+ console.warn(chalk.yellow(`⚠ Warning: "${targetName}" already exists. Using this name will overwrite the existing template.`));
114
+ }
115
+ }
116
+ else {
117
+ const { confirmName } = await inquirer.prompt({
118
+ type: 'confirm',
119
+ name: 'confirmName',
120
+ message: `Use "${targetName}" as the template name?`,
121
+ default: true
122
+ });
123
+ if (!confirmName) {
124
+ const { newName } = await inquirer.prompt({
125
+ type: 'input',
126
+ name: 'newName',
127
+ message: 'Name this template:',
128
+ default: targetName
129
+ });
130
+ targetName = newName;
131
+ }
132
+ }
133
+ }
95
134
  }
96
135
  let description = '';
97
136
  if (options.desc) {
@@ -124,6 +163,8 @@ export async function learn(sourcePath, updateTemplate = null, options = {}) {
124
163
  }
125
164
  }
126
165
  else {
166
+ // Track whether description came from .info.md or JSON (not user-provided)
167
+ const descFromSource = !options.desc && (fileTemplateConfig.description || infoDesc);
127
168
  if (fileTemplateConfig.description) {
128
169
  description = fileTemplateConfig.description;
129
170
  if (!options.json)
@@ -146,6 +187,34 @@ export async function learn(sourcePath, updateTemplate = null, options = {}) {
146
187
  });
147
188
  description = newDesc;
148
189
  }
190
+ // If description came from .info.md or JSON, prompt to confirm/edit
191
+ // This prevents accidental overwrites when creating new templates based on existing ones
192
+ if (descFromSource && !options.json) {
193
+ if (options.yes) {
194
+ // In --yes mode, keep the auto-detected description but warn
195
+ const existingNames = getTemplateNames(config);
196
+ if (existingNames.includes(targetName)) {
197
+ console.warn(chalk.yellow(`⚠ Warning: "${targetName}" already exists. Using this name will overwrite the existing template.`));
198
+ }
199
+ }
200
+ else {
201
+ const { confirmDesc } = await inquirer.prompt({
202
+ type: 'confirm',
203
+ name: 'confirmDesc',
204
+ message: `Use "${description}" as the template description?`,
205
+ default: true
206
+ });
207
+ if (!confirmDesc) {
208
+ const { newDesc } = await inquirer.prompt({
209
+ type: 'input',
210
+ name: 'newDesc',
211
+ message: 'Purpose/Description of this template:',
212
+ default: description
213
+ });
214
+ description = newDesc;
215
+ }
216
+ }
217
+ }
149
218
  }
150
219
  const cliIgnore = options.ignore ? options.ignore.split(',').map((s) => s.trim()).filter(Boolean) : [];
151
220
  const ignorePatterns = [...(config.ignore || []), ...cliIgnore];
@@ -4,11 +4,32 @@ import path from 'path';
4
4
  import { sanitizePath } from './config.js';
5
5
  /**
6
6
  * Replaces all {{var}} patterns in the content with values from the variables object.
7
+ * Supports nested variable expansion - if a variable's value contains {{other_var}},
8
+ * it will be expanded iteratively until no more placeholders remain or maxIterations is reached.
7
9
  */
8
- export function substituteVariables(content, variables) {
9
- return content.replace(/\{\{\s*(\w+)\s*\}\}/g, (_, varName) => {
10
- return variables[varName] ?? `{{${varName}}}`;
11
- });
10
+ export function substituteVariables(content, variables, maxIterations = 10) {
11
+ let result = content;
12
+ let iteration = 0;
13
+ // Keep expanding until no more placeholders remain or we hit the limit
14
+ while (/\{\{[^}]+\}\}/.test(result) && iteration < maxIterations) {
15
+ // Use a more complex regex that captures the full placeholder including spaces
16
+ result = result.replace(/(\{\{\s*)(\w+)(\s*\}\})/g, (_, prefix, varName, suffix) => {
17
+ const val = variables[varName];
18
+ // If variable not found, leave placeholder as-is with original spacing
19
+ if (val === undefined) {
20
+ return `${prefix}${varName}${suffix}`;
21
+ }
22
+ // Return the value (which may contain more placeholders to expand)
23
+ return val;
24
+ });
25
+ iteration++;
26
+ // Prevent infinite loops by checking if we're stuck
27
+ if (iteration > 1 && result === content) {
28
+ console.warn(chalk.yellow(`Warning: Potential infinite loop detected in variable expansion, stopping after ${iteration} iterations`));
29
+ break;
30
+ }
31
+ }
32
+ return result;
12
33
  }
13
34
  /**
14
35
  * Processes copy_files tasks from a template.
@@ -28,6 +28,58 @@ Instead of manual definition, `pt learn` and `pt update` automatically scan for
28
28
 
29
29
  These variables are then used during `copy_files` operations to replace `{{variable_name}}` placeholders in copied files.
30
30
 
31
+ ### Nested Variable Expansion (v0.36.0+)
32
+
33
+ Starting with v0.36.0, `pt` supports **nested variable expansion** — variables can contain other variable placeholders that are resolved iteratively. This enables powerful configuration patterns like:
34
+
35
+ ```bash
36
+ # In ~/.env or parent directory .env file:
37
+ prefix='rst_{{ project }}'
38
+ project=MyProject
39
+ ```
40
+
41
+ During initialization, the system will:
42
+ 1. Load `prefix='rst_{{ project }}'` from `.env`
43
+ 2. Detect that `prefix` contains a `{{ project }}` placeholder
44
+ 3. Resolve `{{ project }}` to `MyProject`
45
+ 4. Set `prefix` to `rst_MyProject`
46
+
47
+ This is particularly useful for:
48
+ - **Project naming conventions**: `prefix='app_{{ env }}'` + `env=prod` → `app_prod`
49
+ - **Path templates**: `template_path='docs/{{ project }}'` + `project=wiki` → `docs/wiki`
50
+ - **Multi-level configurations**: Combine multiple `.env` files with nested references
51
+
52
+ **How it works:**
53
+ - Variables are expanded iteratively (up to 10 passes) to prevent infinite loops
54
+ - Circular references are detected and stopped gracefully
55
+ - Missing nested variables remain as `{{ variable }}` placeholders
56
+ - Whitespace is preserved: `{{ unknown }}` stays as `{{ unknown }}` (not `{{unknown}}`)
57
+
58
+ ### Parent Directory `.env` File Scanning
59
+
60
+ `pt` automatically scans parent directories for `.env` files and uses their values as defaults during initialization. This enables:
61
+
62
+ - **Project-wide defaults**: Store common values in a parent `.env` file
63
+ - **Environment-specific configurations**: Use different `.env` files for dev/staging/prod
64
+ - **Team collaboration**: Share common variable values across team projects
65
+
66
+ **Example:**
67
+ ```bash
68
+ # Project structure:
69
+ my-project/
70
+ ├── .env # Contains: prefix='rst_'
71
+ ├── sub-project/ # Initialize here
72
+ │ └── ...
73
+ ```
74
+
75
+ When you run `pt init my-template sub-project`, the `prefix` variable will be pre-filled with `rst_` from the parent `.env` file.
76
+
77
+ **Behavior:**
78
+ - Scans from the current directory up to 3 parent levels
79
+ - Uses values from `.env` as defaults (still prompts if not in `.env`)
80
+ - `--vars` CLI option overrides `.env` values
81
+ - `.env` files are not committed to version control (use `.gitignore`)
82
+
31
83
  ## Post-Config Tasks
32
84
 
33
85
  Post-config tasks are optional commands that run after a project is initialized. They can be defined in a template or auto-detected from the source directory.
package/doc/usage.md CHANGED
@@ -46,6 +46,72 @@ During `pt learn` or `pt update`, the tool automatically scans text files at the
46
46
  - **Global Suggestions:** Your global variables (defined in `~/.pt/config.yaml`) are automatically injected as additional suggestions during the learn process.
47
47
  - **Updating:** You can add new placeholders to a project folder and run `pt update <template_name>` to automatically register them in your existing template.
48
48
 
49
+ ### Using `.env` Files for Variable Defaults (v0.36.0+)
50
+
51
+ Starting with v0.36.0, `pt` automatically scans parent directories for `.env` files and uses their values as defaults during initialization. This enables powerful configuration patterns:
52
+
53
+ ```bash
54
+ # Create a .env file in your project directory
55
+ echo "prefix='rst_'" > .env
56
+ echo "project='MyProject'" >> .env
57
+
58
+ # Initialize a project - variables are pre-filled from .env
59
+ pt init my-template my-new-project
60
+ ```
61
+
62
+ **Key Features:**
63
+ - **Automatic Scanning:** Scans up to 3 parent directories for `.env` files
64
+ - **Variable Pre-filling:** Values from `.env` are used as defaults in prompts
65
+ - **Nested Variables:** Supports nested placeholders like `prefix='app_{{ env }}'`
66
+ - **Override Support:** Use `--vars` to override `.env` values: `--vars project=OverriddenProject`
67
+
68
+ **Example with Nested Variables:**
69
+ ```bash
70
+ # .env file
71
+ prefix='app_{{ env }}'
72
+ env=prod
73
+ project=MyApp
74
+
75
+ # Template README.md.tmpl
76
+ # Content: {{prefix}}_{{project}}
77
+
78
+ # Result: app_prod_MyApp
79
+ ```
80
+
81
+ **Security Note:** `.env` files are not committed to version control. Use `.gitignore` to exclude them:
82
+ ```bash
83
+ # .gitignore
84
+ .env
85
+ *.env
86
+ ```
87
+
88
+ ### Nested Variable Expansion (v0.36.0+)
89
+
90
+ The `pt` CLI now supports **nested variable expansion** — variables can contain other variable placeholders that are resolved iteratively. This enables powerful configuration patterns:
91
+
92
+ ```bash
93
+ # In .env file:
94
+ prefix='rst_{{ project }}'
95
+ project=MyProject
96
+
97
+ # During initialization, the system will:
98
+ # 1. Load prefix='rst_{{ project }}' from .env
99
+ # 2. Detect that prefix contains a {{ project }} placeholder
100
+ # 3. Resolve {{ project }} to MyProject
101
+ # 4. Set prefix to rst_MyProject
102
+ ```
103
+
104
+ **Use Cases:**
105
+ - **Project naming conventions:** `prefix='app_{{ env }}'` + `env=prod` → `app_prod`
106
+ - **Path templates:** `template_path='docs/{{ project }}'` + `project=wiki` → `docs/wiki`
107
+ - **Multi-level configurations:** Combine multiple `.env` files with nested references
108
+
109
+ **How it works:**
110
+ - Variables are expanded iteratively (up to 10 passes) to prevent infinite loops
111
+ - Circular references are detected and stopped gracefully
112
+ - Missing nested variables remain as `{{ variable }}` placeholders
113
+ - Whitespace is preserved: `{{ unknown }}` stays as `{{ unknown }}` (not `{{unknown}}`)
114
+
49
115
  ## Initialize a project
50
116
 
51
117
  ```bash
@@ -76,3 +76,67 @@ The resulting `package.json` in `my-new-project/` will be:
76
76
  "license": "MIT"
77
77
  }
78
78
  ```
79
+
80
+ ## 4. Nested Variable Expansion (v0.36.0+)
81
+
82
+ Starting with v0.36.0, you can use **nested variables** for more complex configurations. Create a `.env` file in your project directory or parent directory:
83
+
84
+ ```bash
85
+ # .env file in parent directory
86
+ prefix='rst_'
87
+ project=MyProject
88
+ ```
89
+
90
+ Then use these variables in your template files:
91
+
92
+ **`templates/README.md.tmpl`**:
93
+ ```markdown
94
+ # {{prefix}}{{project}}
95
+
96
+ This is a nested variable example where:
97
+ - prefix = 'rst_'
98
+ - project = 'MyProject'
99
+ - Result: 'rst_MyProject'
100
+ ```
101
+
102
+ **Even more complex nesting:**
103
+ ```bash
104
+ # .env file
105
+ prefix='app_{{ env }}'
106
+ env=prod
107
+ project=MyApp
108
+ version=2.0
109
+ ```
110
+
111
+ Then in your template:
112
+ ```json
113
+ {
114
+ "name": "{{prefix}}_{{project}}",
115
+ "version": "{{version}}"
116
+ }
117
+ ```
118
+
119
+ This will resolve to:
120
+ ```json
121
+ {
122
+ "name": "app_prod_MyApp",
123
+ "version": "2.0"
124
+ }
125
+ ```
126
+
127
+ **How it works:**
128
+ 1. `pt` scans parent directories for `.env` files
129
+ 2. Loads variables from `.env` as defaults
130
+ 3. Expands nested placeholders iteratively (up to 10 passes)
131
+ 4. Resolves circular references gracefully
132
+
133
+ **Example with nested placeholders:**
134
+ ```bash
135
+ # .env file
136
+ template_path='docs/{{ project }}'
137
+ project=wiki
138
+ ```
139
+
140
+ Result: `template_path` becomes `docs/wiki`
141
+
142
+ **Important:** Missing nested variables remain as `{{ variable }}` placeholders (with preserved whitespace) to help identify configuration issues.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@garyr/pt-cli",
3
- "version": "0.36.4",
3
+ "version": "0.38.0",
4
4
  "description": "Project Template CLI - Learn structures and initialize projects",
5
5
  "main": "dist/index.js",
6
6
  "bin": {
@@ -14,6 +14,71 @@ export interface InitOptions {
14
14
  file?: string;
15
15
  }
16
16
 
17
+ /**
18
+ * Scan parent directories for .env files and parse their variables.
19
+ * Returns a map of variable names to their values, supporting:
20
+ * - KEY=VALUE format
21
+ * - KEY="VALUE with spaces" format
22
+ * - KEY='VALUE with spaces' format
23
+ * - Comments (lines starting with #)
24
+ * - Empty lines
25
+ */
26
+ function scanEnvForVariables(targetPath: string): Record<string, string> {
27
+ const envVars: Record<string, string> = {};
28
+ let currentDir = path.resolve(targetPath);
29
+
30
+ // Scan up to 5 parent directories for .env files
31
+ const maxDepth = 5;
32
+
33
+ for (let depth = 0; depth < maxDepth; depth++) {
34
+ const envPath = path.join(currentDir, '.env');
35
+
36
+ if (fs.existsSync(envPath)) {
37
+ try {
38
+ const content = fs.readFileSync(envPath, 'utf-8');
39
+ const lines = content.split('\n');
40
+
41
+ for (const line of lines) {
42
+ const trimmed = line.trim();
43
+
44
+ // Skip empty lines and comments
45
+ if (!trimmed || trimmed.startsWith('#')) {
46
+ continue;
47
+ }
48
+
49
+ // Match KEY=VALUE patterns
50
+ const match = trimmed.match(/^([A-Za-z_][A-Za-z0-9_]*)\s*=\s*(.+)$/);
51
+ if (match) {
52
+ const key = match[1];
53
+ let value = match[2];
54
+
55
+ // Remove surrounding quotes if present
56
+ if ((value.startsWith('"') && value.endsWith('"')) ||
57
+ (value.startsWith("'") && value.endsWith("'"))) {
58
+ value = value.slice(1, -1);
59
+ }
60
+
61
+ envVars[key] = value;
62
+ }
63
+ }
64
+ } catch (err) {
65
+ // Silently skip unreadable .env files
66
+ continue;
67
+ }
68
+ }
69
+
70
+ // Move to parent directory
71
+ const parentDir = path.dirname(currentDir);
72
+ if (parentDir === currentDir) {
73
+ // Reached filesystem root
74
+ break;
75
+ }
76
+ currentDir = parentDir;
77
+ }
78
+
79
+ return envVars;
80
+ }
81
+
17
82
  export async function init(targetName: string | undefined, destPath: string | undefined, options: InitOptions = {}) {
18
83
  const config = loadConfig();
19
84
 
@@ -103,6 +168,18 @@ export async function init(targetName: string | undefined, destPath: string | un
103
168
  // Handle Variables
104
169
  let variables: Record<string, string> = {};
105
170
  if (template.variables && template.variables.length > 0) {
171
+ // Scan parent directories for .env files and pre-fill variables
172
+ const envVars = scanEnvForVariables(resolvedDest);
173
+
174
+ // Merge .env variables into variables (with lower priority than --vars)
175
+ if (Object.keys(envVars).length > 0) {
176
+ for (const [key, value] of Object.entries(envVars)) {
177
+ if (!variables[key]) {
178
+ variables[key] = value;
179
+ }
180
+ }
181
+ }
182
+
106
183
  if (options.vars) {
107
184
  // Parse --vars "key=val,key2=val2"
108
185
  const pairs = options.vars.split(',').map((p: string) => p.trim());
@@ -21,7 +21,15 @@ export async function learn(sourcePath: string, updateTemplate: string | null =
21
21
  // Phase 1: Remote Check
22
22
  if (sourcePath.startsWith('http')) {
23
23
  console.log(chalk.cyan(`Downloading remote template from: ${sourcePath}...`));
24
- resolvedPath = await downloadAndExtract(sourcePath, options.json || false, options.allowUntrusted || false);
24
+ try {
25
+ resolvedPath = await downloadAndExtract(sourcePath, options.json || false, options.allowUntrusted || false);
26
+ } catch (err) {
27
+ if ((err as Error).message === 'Download cancelled by user due to untrusted source') {
28
+ console.log(chalk.yellow('Download cancelled. Exiting.'));
29
+ process.exit(0);
30
+ }
31
+ throw err;
32
+ }
25
33
  } else {
26
34
  resolvedPath = path.resolve(sourcePath);
27
35
  }
@@ -78,6 +86,9 @@ export async function learn(sourcePath: string, updateTemplate: string | null =
78
86
  process.exit(1);
79
87
  }
80
88
  } else {
89
+ // Track whether name came from .info.md or JSON (not user-provided)
90
+ const nameFromSource = !options.name && (fileTemplateConfig.name || infoName);
91
+
81
92
  if (options.name) {
82
93
  targetName = options.name;
83
94
  } else if (fileTemplateConfig.name) {
@@ -99,6 +110,34 @@ export async function learn(sourcePath: string, updateTemplate: string | null =
99
110
  targetName = newName;
100
111
  }
101
112
  }
113
+
114
+ // If name came from .info.md or JSON, prompt to confirm/edit
115
+ // This prevents accidental overwrites when creating new templates based on existing ones
116
+ if (nameFromSource && !options.json) {
117
+ if (options.yes) {
118
+ // In --yes mode, keep the auto-detected name but warn
119
+ const existingNames = getTemplateNames(config);
120
+ if (existingNames.includes(targetName)) {
121
+ console.warn(chalk.yellow(`⚠ Warning: "${targetName}" already exists. Using this name will overwrite the existing template.`));
122
+ }
123
+ } else {
124
+ const { confirmName } = await inquirer.prompt({
125
+ type: 'confirm',
126
+ name: 'confirmName',
127
+ message: `Use "${targetName}" as the template name?`,
128
+ default: true
129
+ });
130
+ if (!confirmName) {
131
+ const { newName } = await inquirer.prompt({
132
+ type: 'input',
133
+ name: 'newName',
134
+ message: 'Name this template:',
135
+ default: targetName
136
+ });
137
+ targetName = newName;
138
+ }
139
+ }
140
+ }
102
141
  }
103
142
 
104
143
  let description = '';
@@ -129,6 +168,9 @@ export async function learn(sourcePath: string, updateTemplate: string | null =
129
168
  }
130
169
  }
131
170
  } else {
171
+ // Track whether description came from .info.md or JSON (not user-provided)
172
+ const descFromSource = !options.desc && (fileTemplateConfig.description || infoDesc);
173
+
132
174
  if (fileTemplateConfig.description) {
133
175
  description = fileTemplateConfig.description;
134
176
  if (!options.json) console.log(chalk.cyan(`Auto-detected template description from JSON: ${description}`));
@@ -146,6 +188,34 @@ export async function learn(sourcePath: string, updateTemplate: string | null =
146
188
  });
147
189
  description = newDesc;
148
190
  }
191
+
192
+ // If description came from .info.md or JSON, prompt to confirm/edit
193
+ // This prevents accidental overwrites when creating new templates based on existing ones
194
+ if (descFromSource && !options.json) {
195
+ if (options.yes) {
196
+ // In --yes mode, keep the auto-detected description but warn
197
+ const existingNames = getTemplateNames(config);
198
+ if (existingNames.includes(targetName)) {
199
+ console.warn(chalk.yellow(`⚠ Warning: "${targetName}" already exists. Using this name will overwrite the existing template.`));
200
+ }
201
+ } else {
202
+ const { confirmDesc } = await inquirer.prompt({
203
+ type: 'confirm',
204
+ name: 'confirmDesc',
205
+ message: `Use "${description}" as the template description?`,
206
+ default: true
207
+ });
208
+ if (!confirmDesc) {
209
+ const { newDesc } = await inquirer.prompt({
210
+ type: 'input',
211
+ name: 'newDesc',
212
+ message: 'Purpose/Description of this template:',
213
+ default: description
214
+ });
215
+ description = newDesc;
216
+ }
217
+ }
218
+ }
149
219
  }
150
220
 
151
221
  const cliIgnore = options.ignore ? options.ignore.split(',').map((s: string) => s.trim()).filter(Boolean) : [];