@garyr/pt-cli 0.36.4 → 0.39.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.
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
 
@@ -2,24 +2,114 @@ import fs from 'fs';
2
2
  import path from 'path';
3
3
  import chalk from 'chalk';
4
4
  import { loadConfig, saveConfig } from '../config.js';
5
+ /**
6
+ * Validate JSON file exists and contains valid JSON
7
+ */
8
+ function validateJsonFile(filePath) {
9
+ try {
10
+ // Check if file exists
11
+ if (!fs.existsSync(filePath)) {
12
+ return {
13
+ valid: false,
14
+ error: `File not found: ${filePath}`
15
+ };
16
+ }
17
+ // Check file size (reasonable limit to prevent reading huge files)
18
+ const stats = fs.statSync(filePath);
19
+ if (stats.size > 10 * 1024 * 1024) { // 10MB limit
20
+ return {
21
+ valid: false,
22
+ error: `File too large (${stats.size} bytes). Maximum size is 10MB.`
23
+ };
24
+ }
25
+ // Read and parse JSON
26
+ const content = fs.readFileSync(filePath, 'utf8');
27
+ if (!content.trim()) {
28
+ return {
29
+ valid: false,
30
+ error: 'File is empty'
31
+ };
32
+ }
33
+ const data = JSON.parse(content);
34
+ return { valid: true, data };
35
+ }
36
+ catch (e) {
37
+ const error = e;
38
+ return {
39
+ valid: false,
40
+ error: `JSON parse error: ${error.message}`
41
+ };
42
+ }
43
+ }
44
+ /**
45
+ * Validate template structure
46
+ */
47
+ function validateTemplateStructure(data) {
48
+ if (!data || typeof data !== 'object') {
49
+ return {
50
+ valid: false,
51
+ error: 'Template data must be a JSON object'
52
+ };
53
+ }
54
+ // Basic structure validation
55
+ if (data.description && typeof data.description !== 'string') {
56
+ return {
57
+ valid: false,
58
+ error: 'Template description must be a string'
59
+ };
60
+ }
61
+ if (data.variables && Array.isArray(data.variables)) {
62
+ for (let i = 0; i < data.variables.length; i++) {
63
+ const v = data.variables[i];
64
+ if (!v.name || typeof v.name !== 'string') {
65
+ return {
66
+ valid: false,
67
+ error: `Variable at index ${i} must have a string 'name' field`
68
+ };
69
+ }
70
+ }
71
+ }
72
+ return { valid: true };
73
+ }
5
74
  export function addCommand(name, jsonStr, options = {}) {
6
75
  const config = loadConfig();
76
+ // Determine if we're reading from file or string
77
+ const isFile = !!options.file;
78
+ let data;
7
79
  try {
8
- let data;
9
- if (options.file) {
80
+ if (isFile) {
81
+ // Validate file first
10
82
  const filePath = path.resolve(options.file);
11
- data = JSON.parse(fs.readFileSync(filePath, 'utf8'));
83
+ const validation = validateJsonFile(filePath);
84
+ if (!validation.valid) {
85
+ console.error(chalk.red(`Error: ${validation.error}`));
86
+ console.error(chalk.gray(`File: ${filePath}`));
87
+ process.exit(1);
88
+ }
89
+ data = validation.data;
12
90
  }
13
91
  else if (jsonStr) {
14
- data = JSON.parse(jsonStr);
92
+ // Parse JSON string directly
93
+ try {
94
+ data = JSON.parse(jsonStr);
95
+ }
96
+ catch (e) {
97
+ const error = e;
98
+ console.error(chalk.red(`Error: Invalid JSON string - ${error.message}`));
99
+ process.exit(1);
100
+ }
15
101
  }
16
102
  else {
17
103
  console.error('Error: Either a JSON string or --file <path> must be provided.');
18
104
  process.exit(1);
19
105
  }
20
- if (!config.templates)
21
- config.templates = {};
22
- // Basic validation: ensure we aren't accidentally adding a full config object
106
+ // Validate template structure
107
+ const structureValidation = validateTemplateStructure(data);
108
+ if (!structureValidation.valid) {
109
+ console.error(chalk.red(`Error: Invalid template structure - ${structureValidation.error}`));
110
+ process.exit(1);
111
+ }
112
+ // Check for full config object
23
113
  if (data && data.templates && typeof data.templates === 'object') {
24
114
  console.error(chalk.red('Error: The provided JSON appears to be a full configuration file, not a single template.'));
25
115
  console.error(chalk.gray('If you want to import a specific template from it, extract that template object first.'));
@@ -31,7 +121,7 @@ export function addCommand(name, jsonStr, options = {}) {
31
121
  }
32
122
  catch (e) {
33
123
  const error = e;
34
- console.error(chalk.red(`Failed to parse template JSON: ${error.message}`));
124
+ console.error(chalk.red(`Failed to process template: ${error.message}`));
35
125
  process.exit(1);
36
126
  }
37
127
  }
@@ -9,15 +9,22 @@ export function configCommand(templateName, options = {}) {
9
9
  name: templateName,
10
10
  ...config.templates[templateName]
11
11
  };
12
- console.log(JSON.stringify(output, null, 2));
12
+ // Safely drain stdout before allowing the process to close
13
+ process.stdout.write(JSON.stringify(output, null, 2) + '\n', () => {
14
+ process.exit(0);
15
+ });
13
16
  }
14
17
  else {
15
- console.error(chalk.red(`Error: Template "${templateName}" not found.`));
16
- process.exit(1);
18
+ process.stderr.write(chalk.red(`Error: Template "${templateName}" not found.\n`), () => {
19
+ process.exit(1);
20
+ });
17
21
  }
18
22
  }
19
23
  else {
20
- console.log(JSON.stringify(config, null, 2));
24
+ // Safely drain the entire global config payload
25
+ process.stdout.write(JSON.stringify(config, null, 2) + '\n', () => {
26
+ process.exit(0);
27
+ });
21
28
  }
22
29
  return;
23
30
  }
@@ -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];
@@ -466,7 +535,11 @@ export async function learn(sourcePath, updateTemplate = null, options = {}) {
466
535
  name: targetName,
467
536
  ...templateConfig
468
537
  };
469
- console.log(JSON.stringify(output, null, 2));
538
+ // Force the application to wait until every single byte of this JSON string
539
+ // safely clears the operating system's pipe buffer before letting the process die.
540
+ process.stdout.write(JSON.stringify(output, null, 2) + '\n', () => {
541
+ process.exit(0);
542
+ });
470
543
  return;
471
544
  }
472
545
  config.templates[targetName] = templateConfig;
@@ -508,10 +581,10 @@ function extractStructure(dirPath, rootPath, ignorePatterns) {
508
581
  let info = "";
509
582
  const gitkeepPath = path.join(fullPath, '.gitkeep.md');
510
583
  const infoPath = path.join(fullPath, '.info.md');
511
- if (fs.existsSync(gitkeepPath))
512
- info = fs.readFileSync(gitkeepPath, 'utf-8').trim();
513
- else if (fs.existsSync(infoPath))
584
+ if (fs.existsSync(infoPath))
514
585
  info = fs.readFileSync(infoPath, 'utf-8').trim();
586
+ else if (fs.existsSync(gitkeepPath))
587
+ info = fs.readFileSync(gitkeepPath, 'utf-8').trim();
515
588
  nodes.push({ name: entry.name, info: info, children: children });
516
589
  }
517
590
  }
package/dist/config.js CHANGED
@@ -84,12 +84,22 @@ export function loadConfig() {
84
84
  catch (err) {
85
85
  const error = err;
86
86
  console.error(chalk.red(`\nError loading config: ${error.message}`));
87
- // If we have a backup, maybe suggest using it
88
87
  const backupPath = getConfigPath() + '.bak';
89
88
  if (fs.existsSync(backupPath)) {
90
89
  console.error(chalk.yellow(`A backup exists at ${backupPath}. You may want to restore it.`));
91
90
  }
92
- process.exit(1);
91
+ // Allow the event loop to flush console streams out to Godot before dying
92
+ setTimeout(() => {
93
+ process.exit(1);
94
+ }, 5);
95
+ // 👇 Add this return statement to satisfy the TypeScript compiler
96
+ // The application will terminate before this empty config can be used.
97
+ return {
98
+ version: '3.0',
99
+ templates: {},
100
+ default_post_config: [],
101
+ variables: []
102
+ };
93
103
  }
94
104
  }
95
105
  export function normalizeVariable(v) {
package/dist/index.js CHANGED
@@ -32,15 +32,18 @@ program
32
32
  }
33
33
  catch (err) {
34
34
  if (options.json) {
35
- console.log(JSON.stringify({
35
+ // Fix: Ensure the error JSON payload isn't truncated before exiting
36
+ process.stdout.write(JSON.stringify({
36
37
  type: 'error',
37
38
  message: err.message || String(err)
38
- }));
39
+ }) + '\n', () => {
40
+ process.exit(1);
41
+ });
39
42
  }
40
43
  else {
41
44
  console.error(chalk.red(`Error: ${err.message || err}`));
45
+ process.exit(1);
42
46
  }
43
- process.exit(1);
44
47
  }
45
48
  });
46
49
  program
@@ -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,63 @@ 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
+
43
+ 1. Load `prefix='rst_{{ project }}'` from `.env`
44
+ 2. Detect that `prefix` contains a `{{ project }}` placeholder
45
+ 3. Resolve `{{ project }}` to `MyProject`
46
+ 4. Set `prefix` to `rst_MyProject`
47
+
48
+ This is particularly useful for:
49
+
50
+ - **Project naming conventions**: `prefix='app_{{ env }}'` + `env=prod` → `app_prod`
51
+ - **Path templates**: `template_path='docs/{{ project }}'` + `project=wiki` → `docs/wiki`
52
+ - **Multi-level configurations**: Combine multiple `.env` files with nested references
53
+
54
+ **How it works:**
55
+
56
+ - Variables are expanded iteratively (up to 10 passes) to prevent infinite loops
57
+ - Circular references are detected and stopped gracefully
58
+ - Missing nested variables remain as `{{ variable }}` placeholders
59
+ - Whitespace is preserved: `{{ unknown }}` stays as `{{ unknown }}` (not `{{unknown}}`)
60
+
61
+ ### Parent Directory `.env` File Scanning
62
+
63
+ `pt` automatically scans parent directories for `.env` files and uses their values as defaults during initialization. This enables:
64
+
65
+ - **Project-wide defaults**: Store common values in a parent `.env` file
66
+ - **Environment-specific configurations**: Use different `.env` files for dev/staging/prod
67
+ - **Team collaboration**: Share common variable values across team projects
68
+
69
+ **Example:**
70
+
71
+ ```bash
72
+ # Project structure:
73
+ my-project/
74
+ ├── .env # Contains: prefix='rst_'
75
+ ├── sub-project/ # Initialize here
76
+ │ └── ...
77
+ ```
78
+
79
+ When you run `pt init my-template sub-project`, the `prefix` variable will be pre-filled with `rst_` from the parent `.env` file.
80
+
81
+ **Behavior:**
82
+
83
+ - Scans from the current directory up to 3 parent levels
84
+ - Uses values from `.env` as defaults (still prompts if not in `.env`)
85
+ - `--vars` CLI option overrides `.env` values
86
+ - `.env` files are not committed to version control (use `.gitignore`)
87
+
31
88
  ## Post-Config Tasks
32
89
 
33
90
  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.
@@ -57,6 +114,7 @@ If the directory contains a `.pt-template.json` or `template.json` file with a `
57
114
  3. Alternatively, initialize a temporary project from your learned template (`pt init`), refine it manually, and then use `pt update` from that directory to "re-learn" the refined state.
58
115
 
59
116
  **Security Note:** All post-config commands are subject to security validation:
117
+
60
118
  - Dangerous commands (e.g., `curl`, `python`, `chmod`) trigger warnings with 5-second cancellation
61
119
  - Absolute blocks (e.g., `sudo`, `rm -rf`, `dd`) are never allowed
62
120
  - Rate limiting prevents runaway execution (50 commands per run)
@@ -125,7 +183,7 @@ Each task supports:
125
183
 
126
184
  ## Default Post-Config
127
185
 
128
- Default post-config tasks are defined at the top level of `~/.pt/config.yaml` under `default_post_config`. They serve as suggestions when creating or updating templates via `pt learn`.
186
+ Default post-config tasks are defined at the top level of `~/.pt/config.yaml` under `default_post_config`. They serve as suggestions when creating or updating templates via `pt learn`.
129
187
 
130
188
  Unlike previous versions, default tasks are **not** automatically applied during `pt init`. Instead, you select which ones to include when learning a template, and those selections are baked into the template's `post_config` list. This eliminates the need to repeat boilerplate setup (e.g. `git init`) across templates while keeping each template fully self-contained.
131
189
 
@@ -137,21 +195,21 @@ default_post_config:
137
195
  description: "Initialize git repository"
138
196
  - command: "git add -A && git commit -m 'Initial commit'"
139
197
  description: "Initial git commit"
140
- checked: false # default on, but user must manually check
198
+ checked: false # default on, but user must manually check
141
199
  - command: "git lfs install"
142
200
  description: "Install git-lfs hooks"
143
- type: "godot" # only applies to godot projects
201
+ type: "godot" # only applies to godot projects
144
202
  ```
145
203
 
146
204
  ### Fields
147
205
 
148
206
  Each default task supports the same fields as template post-config:
149
207
 
150
- | Field | Description |
151
- | ------------- | -------------------------------------------------------------------------------- |
152
- | `command` | Shell command to run |
153
- | `description` | Shown to user during interactive selection |
154
- | `checked` | Default checkbox state (`true` by default); set `false` to require manual opt-in |
208
+ | Field | Description |
209
+ | ------------- | -------------------------------------------------------------------------------------------------- |
210
+ | `command` | Shell command to run |
211
+ | `description` | Shown to user during interactive selection |
212
+ | `checked` | Default checkbox state (`true` by default); set `false` to require manual opt-in |
155
213
  | `type` | Filter by project type (e.g. `"javascript"`); if set, task only applies when template type matches |
156
214
 
157
215
  ### Behavior
@@ -163,6 +221,7 @@ Each default task supports the same fields as template post-config:
163
221
 
164
222
  You can view current default tasks using `pt config` or `pt default-post-config`.
165
223
  To update default tasks programmatically or via CLI, use the `pt default-post-config` command:
224
+
166
225
  - `pt default-post-config`: List current default post-config tasks.
167
226
  - `pt default-post-config --set --json '...'`: Replace the default post-config tasks list via a JSON string or file.
168
227
 
@@ -247,6 +306,7 @@ Each entry supports:
247
306
  A plausible scenario for customizing a new project's `package.json` and `README.md`:
248
307
 
249
308
  **1. Define in `config.yaml`**:
309
+
250
310
  ```yaml
251
311
  templates:
252
312
  node_web_app:
@@ -265,6 +325,7 @@ templates:
265
325
  ```
266
326
 
267
327
  **2. Template source (`templates/package.json.tmpl`)**:
328
+
268
329
  ```json
269
330
  {
270
331
  "name": "{{project_name}}",
@@ -275,6 +336,7 @@ templates:
275
336
 
276
337
  **3. Resulting project file**:
277
338
  If the user enters `my-service` and `Jane Doe`, the file `package.json` will be created with:
339
+
278
340
  ```json
279
341
  {
280
342
  "name": "my-service",