@garyr/pt-cli 0.38.0 → 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/dist/commands/addCommand.js +98 -8
- package/dist/commands/configCommand.js +11 -4
- package/dist/commands/learnCommand.js +8 -4
- package/dist/config.js +12 -2
- package/dist/index.js +6 -3
- package/doc/configuration.md +18 -8
- package/doc/usage.md +5 -0
- package/package.json +1 -1
- package/src/commands/addCommand.ts +109 -8
- package/src/commands/configCommand.ts +11 -4
- package/src/commands/learnCommand.ts +12 -7
- package/src/config.ts +14 -2
- package/src/index.ts +6 -3
|
@@ -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
|
-
|
|
9
|
-
|
|
80
|
+
if (isFile) {
|
|
81
|
+
// Validate file first
|
|
10
82
|
const filePath = path.resolve(options.file);
|
|
11
|
-
|
|
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
|
-
|
|
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
|
-
|
|
21
|
-
|
|
22
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
16
|
-
|
|
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
|
-
|
|
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
|
}
|
|
@@ -535,7 +535,11 @@ export async function learn(sourcePath, updateTemplate = null, options = {}) {
|
|
|
535
535
|
name: targetName,
|
|
536
536
|
...templateConfig
|
|
537
537
|
};
|
|
538
|
-
|
|
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
|
+
});
|
|
539
543
|
return;
|
|
540
544
|
}
|
|
541
545
|
config.templates[targetName] = templateConfig;
|
|
@@ -577,10 +581,10 @@ function extractStructure(dirPath, rootPath, ignorePatterns) {
|
|
|
577
581
|
let info = "";
|
|
578
582
|
const gitkeepPath = path.join(fullPath, '.gitkeep.md');
|
|
579
583
|
const infoPath = path.join(fullPath, '.info.md');
|
|
580
|
-
if (fs.existsSync(
|
|
581
|
-
info = fs.readFileSync(gitkeepPath, 'utf-8').trim();
|
|
582
|
-
else if (fs.existsSync(infoPath))
|
|
584
|
+
if (fs.existsSync(infoPath))
|
|
583
585
|
info = fs.readFileSync(infoPath, 'utf-8').trim();
|
|
586
|
+
else if (fs.existsSync(gitkeepPath))
|
|
587
|
+
info = fs.readFileSync(gitkeepPath, 'utf-8').trim();
|
|
584
588
|
nodes.push({ name: entry.name, info: info, children: children });
|
|
585
589
|
}
|
|
586
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
|
-
|
|
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
|
-
|
|
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
|
package/doc/configuration.md
CHANGED
|
@@ -39,17 +39,20 @@ project=MyProject
|
|
|
39
39
|
```
|
|
40
40
|
|
|
41
41
|
During initialization, the system will:
|
|
42
|
+
|
|
42
43
|
1. Load `prefix='rst_{{ project }}'` from `.env`
|
|
43
44
|
2. Detect that `prefix` contains a `{{ project }}` placeholder
|
|
44
45
|
3. Resolve `{{ project }}` to `MyProject`
|
|
45
46
|
4. Set `prefix` to `rst_MyProject`
|
|
46
47
|
|
|
47
48
|
This is particularly useful for:
|
|
49
|
+
|
|
48
50
|
- **Project naming conventions**: `prefix='app_{{ env }}'` + `env=prod` → `app_prod`
|
|
49
51
|
- **Path templates**: `template_path='docs/{{ project }}'` + `project=wiki` → `docs/wiki`
|
|
50
52
|
- **Multi-level configurations**: Combine multiple `.env` files with nested references
|
|
51
53
|
|
|
52
54
|
**How it works:**
|
|
55
|
+
|
|
53
56
|
- Variables are expanded iteratively (up to 10 passes) to prevent infinite loops
|
|
54
57
|
- Circular references are detected and stopped gracefully
|
|
55
58
|
- Missing nested variables remain as `{{ variable }}` placeholders
|
|
@@ -64,6 +67,7 @@ This is particularly useful for:
|
|
|
64
67
|
- **Team collaboration**: Share common variable values across team projects
|
|
65
68
|
|
|
66
69
|
**Example:**
|
|
70
|
+
|
|
67
71
|
```bash
|
|
68
72
|
# Project structure:
|
|
69
73
|
my-project/
|
|
@@ -75,6 +79,7 @@ my-project/
|
|
|
75
79
|
When you run `pt init my-template sub-project`, the `prefix` variable will be pre-filled with `rst_` from the parent `.env` file.
|
|
76
80
|
|
|
77
81
|
**Behavior:**
|
|
82
|
+
|
|
78
83
|
- Scans from the current directory up to 3 parent levels
|
|
79
84
|
- Uses values from `.env` as defaults (still prompts if not in `.env`)
|
|
80
85
|
- `--vars` CLI option overrides `.env` values
|
|
@@ -109,6 +114,7 @@ If the directory contains a `.pt-template.json` or `template.json` file with a `
|
|
|
109
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.
|
|
110
115
|
|
|
111
116
|
**Security Note:** All post-config commands are subject to security validation:
|
|
117
|
+
|
|
112
118
|
- Dangerous commands (e.g., `curl`, `python`, `chmod`) trigger warnings with 5-second cancellation
|
|
113
119
|
- Absolute blocks (e.g., `sudo`, `rm -rf`, `dd`) are never allowed
|
|
114
120
|
- Rate limiting prevents runaway execution (50 commands per run)
|
|
@@ -177,7 +183,7 @@ Each task supports:
|
|
|
177
183
|
|
|
178
184
|
## Default Post-Config
|
|
179
185
|
|
|
180
|
-
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`.
|
|
181
187
|
|
|
182
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.
|
|
183
189
|
|
|
@@ -189,21 +195,21 @@ default_post_config:
|
|
|
189
195
|
description: "Initialize git repository"
|
|
190
196
|
- command: "git add -A && git commit -m 'Initial commit'"
|
|
191
197
|
description: "Initial git commit"
|
|
192
|
-
checked: false
|
|
198
|
+
checked: false # default on, but user must manually check
|
|
193
199
|
- command: "git lfs install"
|
|
194
200
|
description: "Install git-lfs hooks"
|
|
195
|
-
type: "godot"
|
|
201
|
+
type: "godot" # only applies to godot projects
|
|
196
202
|
```
|
|
197
203
|
|
|
198
204
|
### Fields
|
|
199
205
|
|
|
200
206
|
Each default task supports the same fields as template post-config:
|
|
201
207
|
|
|
202
|
-
| Field | Description
|
|
203
|
-
| ------------- |
|
|
204
|
-
| `command` | Shell command to run
|
|
205
|
-
| `description` | Shown to user during interactive selection
|
|
206
|
-
| `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 |
|
|
207
213
|
| `type` | Filter by project type (e.g. `"javascript"`); if set, task only applies when template type matches |
|
|
208
214
|
|
|
209
215
|
### Behavior
|
|
@@ -215,6 +221,7 @@ Each default task supports the same fields as template post-config:
|
|
|
215
221
|
|
|
216
222
|
You can view current default tasks using `pt config` or `pt default-post-config`.
|
|
217
223
|
To update default tasks programmatically or via CLI, use the `pt default-post-config` command:
|
|
224
|
+
|
|
218
225
|
- `pt default-post-config`: List current default post-config tasks.
|
|
219
226
|
- `pt default-post-config --set --json '...'`: Replace the default post-config tasks list via a JSON string or file.
|
|
220
227
|
|
|
@@ -299,6 +306,7 @@ Each entry supports:
|
|
|
299
306
|
A plausible scenario for customizing a new project's `package.json` and `README.md`:
|
|
300
307
|
|
|
301
308
|
**1. Define in `config.yaml`**:
|
|
309
|
+
|
|
302
310
|
```yaml
|
|
303
311
|
templates:
|
|
304
312
|
node_web_app:
|
|
@@ -317,6 +325,7 @@ templates:
|
|
|
317
325
|
```
|
|
318
326
|
|
|
319
327
|
**2. Template source (`templates/package.json.tmpl`)**:
|
|
328
|
+
|
|
320
329
|
```json
|
|
321
330
|
{
|
|
322
331
|
"name": "{{project_name}}",
|
|
@@ -327,6 +336,7 @@ templates:
|
|
|
327
336
|
|
|
328
337
|
**3. Resulting project file**:
|
|
329
338
|
If the user enters `my-service` and `Jane Doe`, the file `package.json` will be created with:
|
|
339
|
+
|
|
330
340
|
```json
|
|
331
341
|
{
|
|
332
342
|
"name": "my-service",
|
package/doc/usage.md
CHANGED
|
@@ -60,12 +60,14 @@ pt init my-template my-new-project
|
|
|
60
60
|
```
|
|
61
61
|
|
|
62
62
|
**Key Features:**
|
|
63
|
+
|
|
63
64
|
- **Automatic Scanning:** Scans up to 3 parent directories for `.env` files
|
|
64
65
|
- **Variable Pre-filling:** Values from `.env` are used as defaults in prompts
|
|
65
66
|
- **Nested Variables:** Supports nested placeholders like `prefix='app_{{ env }}'`
|
|
66
67
|
- **Override Support:** Use `--vars` to override `.env` values: `--vars project=OverriddenProject`
|
|
67
68
|
|
|
68
69
|
**Example with Nested Variables:**
|
|
70
|
+
|
|
69
71
|
```bash
|
|
70
72
|
# .env file
|
|
71
73
|
prefix='app_{{ env }}'
|
|
@@ -79,6 +81,7 @@ project=MyApp
|
|
|
79
81
|
```
|
|
80
82
|
|
|
81
83
|
**Security Note:** `.env` files are not committed to version control. Use `.gitignore` to exclude them:
|
|
84
|
+
|
|
82
85
|
```bash
|
|
83
86
|
# .gitignore
|
|
84
87
|
.env
|
|
@@ -102,11 +105,13 @@ project=MyProject
|
|
|
102
105
|
```
|
|
103
106
|
|
|
104
107
|
**Use Cases:**
|
|
108
|
+
|
|
105
109
|
- **Project naming conventions:** `prefix='app_{{ env }}'` + `env=prod` → `app_prod`
|
|
106
110
|
- **Path templates:** `template_path='docs/{{ project }}'` + `project=wiki` → `docs/wiki`
|
|
107
111
|
- **Multi-level configurations:** Combine multiple `.env` files with nested references
|
|
108
112
|
|
|
109
113
|
**How it works:**
|
|
114
|
+
|
|
110
115
|
- Variables are expanded iteratively (up to 10 passes) to prevent infinite loops
|
|
111
116
|
- Circular references are detected and stopped gracefully
|
|
112
117
|
- Missing nested variables remain as `{{ variable }}` placeholders
|
package/package.json
CHANGED
|
@@ -7,23 +7,124 @@ export interface AddOptions {
|
|
|
7
7
|
file?: string;
|
|
8
8
|
}
|
|
9
9
|
|
|
10
|
+
/**
|
|
11
|
+
* Validate JSON file exists and contains valid JSON
|
|
12
|
+
*/
|
|
13
|
+
function validateJsonFile(filePath: string): { valid: boolean; data?: any; error?: string } {
|
|
14
|
+
try {
|
|
15
|
+
// Check if file exists
|
|
16
|
+
if (!fs.existsSync(filePath)) {
|
|
17
|
+
return {
|
|
18
|
+
valid: false,
|
|
19
|
+
error: `File not found: ${filePath}`
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// Check file size (reasonable limit to prevent reading huge files)
|
|
24
|
+
const stats = fs.statSync(filePath);
|
|
25
|
+
if (stats.size > 10 * 1024 * 1024) { // 10MB limit
|
|
26
|
+
return {
|
|
27
|
+
valid: false,
|
|
28
|
+
error: `File too large (${stats.size} bytes). Maximum size is 10MB.`
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// Read and parse JSON
|
|
33
|
+
const content = fs.readFileSync(filePath, 'utf8');
|
|
34
|
+
if (!content.trim()) {
|
|
35
|
+
return {
|
|
36
|
+
valid: false,
|
|
37
|
+
error: 'File is empty'
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const data = JSON.parse(content);
|
|
42
|
+
return { valid: true, data };
|
|
43
|
+
} catch (e) {
|
|
44
|
+
const error = e as Error;
|
|
45
|
+
return {
|
|
46
|
+
valid: false,
|
|
47
|
+
error: `JSON parse error: ${error.message}`
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Validate template structure
|
|
54
|
+
*/
|
|
55
|
+
function validateTemplateStructure(data: any): { valid: boolean; error?: string } {
|
|
56
|
+
if (!data || typeof data !== 'object') {
|
|
57
|
+
return {
|
|
58
|
+
valid: false,
|
|
59
|
+
error: 'Template data must be a JSON object'
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// Basic structure validation
|
|
64
|
+
if (data.description && typeof data.description !== 'string') {
|
|
65
|
+
return {
|
|
66
|
+
valid: false,
|
|
67
|
+
error: 'Template description must be a string'
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
if (data.variables && Array.isArray(data.variables)) {
|
|
72
|
+
for (let i = 0; i < data.variables.length; i++) {
|
|
73
|
+
const v = data.variables[i];
|
|
74
|
+
if (!v.name || typeof v.name !== 'string') {
|
|
75
|
+
return {
|
|
76
|
+
valid: false,
|
|
77
|
+
error: `Variable at index ${i} must have a string 'name' field`
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
return { valid: true };
|
|
84
|
+
}
|
|
85
|
+
|
|
10
86
|
export function addCommand(name: string, jsonStr: string | undefined, options: AddOptions = {}) {
|
|
11
87
|
const config = loadConfig();
|
|
88
|
+
|
|
89
|
+
// Determine if we're reading from file or string
|
|
90
|
+
const isFile = !!options.file;
|
|
91
|
+
let data: any;
|
|
92
|
+
|
|
12
93
|
try {
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
const filePath = path.resolve(options.file);
|
|
16
|
-
|
|
94
|
+
if (isFile) {
|
|
95
|
+
// Validate file first
|
|
96
|
+
const filePath = path.resolve(options.file!);
|
|
97
|
+
const validation = validateJsonFile(filePath);
|
|
98
|
+
|
|
99
|
+
if (!validation.valid) {
|
|
100
|
+
console.error(chalk.red(`Error: ${validation.error}`));
|
|
101
|
+
console.error(chalk.gray(`File: ${filePath}`));
|
|
102
|
+
process.exit(1);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
data = validation.data;
|
|
17
106
|
} else if (jsonStr) {
|
|
18
|
-
|
|
107
|
+
// Parse JSON string directly
|
|
108
|
+
try {
|
|
109
|
+
data = JSON.parse(jsonStr);
|
|
110
|
+
} catch (e) {
|
|
111
|
+
const error = e as Error;
|
|
112
|
+
console.error(chalk.red(`Error: Invalid JSON string - ${error.message}`));
|
|
113
|
+
process.exit(1);
|
|
114
|
+
}
|
|
19
115
|
} else {
|
|
20
116
|
console.error('Error: Either a JSON string or --file <path> must be provided.');
|
|
21
117
|
process.exit(1);
|
|
22
118
|
}
|
|
23
119
|
|
|
24
|
-
|
|
120
|
+
// Validate template structure
|
|
121
|
+
const structureValidation = validateTemplateStructure(data);
|
|
122
|
+
if (!structureValidation.valid) {
|
|
123
|
+
console.error(chalk.red(`Error: Invalid template structure - ${structureValidation.error}`));
|
|
124
|
+
process.exit(1);
|
|
125
|
+
}
|
|
25
126
|
|
|
26
|
-
//
|
|
127
|
+
// Check for full config object
|
|
27
128
|
if (data && data.templates && typeof data.templates === 'object') {
|
|
28
129
|
console.error(chalk.red('Error: The provided JSON appears to be a full configuration file, not a single template.'));
|
|
29
130
|
console.error(chalk.gray('If you want to import a specific template from it, extract that template object first.'));
|
|
@@ -35,7 +136,7 @@ export function addCommand(name: string, jsonStr: string | undefined, options: A
|
|
|
35
136
|
console.log(chalk.green(`✓ Template "${name}" saved successfully.`));
|
|
36
137
|
} catch (e) {
|
|
37
138
|
const error = e as Error;
|
|
38
|
-
console.error(chalk.red(`Failed to
|
|
139
|
+
console.error(chalk.red(`Failed to process template: ${error.message}`));
|
|
39
140
|
process.exit(1);
|
|
40
141
|
}
|
|
41
142
|
}
|
|
@@ -15,13 +15,20 @@ export function configCommand(templateName: string | undefined, options: ConfigO
|
|
|
15
15
|
name: templateName,
|
|
16
16
|
...config.templates[templateName]
|
|
17
17
|
};
|
|
18
|
-
|
|
18
|
+
// Safely drain stdout before allowing the process to close
|
|
19
|
+
process.stdout.write(JSON.stringify(output, null, 2) + '\n', () => {
|
|
20
|
+
process.exit(0);
|
|
21
|
+
});
|
|
19
22
|
} else {
|
|
20
|
-
|
|
21
|
-
|
|
23
|
+
process.stderr.write(chalk.red(`Error: Template "${templateName}" not found.\n`), () => {
|
|
24
|
+
process.exit(1);
|
|
25
|
+
});
|
|
22
26
|
}
|
|
23
27
|
} else {
|
|
24
|
-
|
|
28
|
+
// Safely drain the entire global config payload
|
|
29
|
+
process.stdout.write(JSON.stringify(config, null, 2) + '\n', () => {
|
|
30
|
+
process.exit(0);
|
|
31
|
+
});
|
|
25
32
|
}
|
|
26
33
|
return;
|
|
27
34
|
}
|
|
@@ -542,11 +542,16 @@ export async function learn(sourcePath: string, updateTemplate: string | null =
|
|
|
542
542
|
}
|
|
543
543
|
|
|
544
544
|
if (options.json) {
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
545
|
+
const output = {
|
|
546
|
+
name: targetName,
|
|
547
|
+
...templateConfig
|
|
548
|
+
};
|
|
549
|
+
|
|
550
|
+
// Force the application to wait until every single byte of this JSON string
|
|
551
|
+
// safely clears the operating system's pipe buffer before letting the process die.
|
|
552
|
+
process.stdout.write(JSON.stringify(output, null, 2) + '\n', () => {
|
|
553
|
+
process.exit(0);
|
|
554
|
+
});
|
|
550
555
|
return;
|
|
551
556
|
}
|
|
552
557
|
|
|
@@ -588,8 +593,8 @@ function extractStructure(dirPath: string, rootPath: string, ignorePatterns?: st
|
|
|
588
593
|
let info = "";
|
|
589
594
|
const gitkeepPath = path.join(fullPath, '.gitkeep.md');
|
|
590
595
|
const infoPath = path.join(fullPath, '.info.md');
|
|
591
|
-
if (fs.existsSync(
|
|
592
|
-
else if (fs.existsSync(
|
|
596
|
+
if (fs.existsSync(infoPath)) info = fs.readFileSync(infoPath, 'utf-8').trim();
|
|
597
|
+
else if (fs.existsSync(gitkeepPath)) info = fs.readFileSync(gitkeepPath, 'utf-8').trim();
|
|
593
598
|
nodes.push({ name: entry.name, info: info, children: children });
|
|
594
599
|
}
|
|
595
600
|
} catch (e) {}
|
package/src/config.ts
CHANGED
|
@@ -148,12 +148,24 @@ export function loadConfig(): PtConfig {
|
|
|
148
148
|
} catch (err) {
|
|
149
149
|
const error = err as Error;
|
|
150
150
|
console.error(chalk.red(`\nError loading config: ${error.message}`));
|
|
151
|
-
// If we have a backup, maybe suggest using it
|
|
152
151
|
const backupPath = getConfigPath() + '.bak';
|
|
153
152
|
if (fs.existsSync(backupPath)) {
|
|
154
153
|
console.error(chalk.yellow(`A backup exists at ${backupPath}. You may want to restore it.`));
|
|
155
154
|
}
|
|
156
|
-
|
|
155
|
+
|
|
156
|
+
// Allow the event loop to flush console streams out to Godot before dying
|
|
157
|
+
setTimeout(() => {
|
|
158
|
+
process.exit(1);
|
|
159
|
+
}, 5);
|
|
160
|
+
|
|
161
|
+
// 👇 Add this return statement to satisfy the TypeScript compiler
|
|
162
|
+
// The application will terminate before this empty config can be used.
|
|
163
|
+
return {
|
|
164
|
+
version: '3.0',
|
|
165
|
+
templates: {},
|
|
166
|
+
default_post_config: [],
|
|
167
|
+
variables: []
|
|
168
|
+
};
|
|
157
169
|
}
|
|
158
170
|
}
|
|
159
171
|
|
package/src/index.ts
CHANGED
|
@@ -39,14 +39,17 @@ program
|
|
|
39
39
|
await learn(pathArg || '.', null, options);
|
|
40
40
|
} catch (err: any) {
|
|
41
41
|
if (options.json) {
|
|
42
|
-
|
|
42
|
+
// Fix: Ensure the error JSON payload isn't truncated before exiting
|
|
43
|
+
process.stdout.write(JSON.stringify({
|
|
43
44
|
type: 'error',
|
|
44
45
|
message: err.message || String(err)
|
|
45
|
-
}))
|
|
46
|
+
}) + '\n', () => {
|
|
47
|
+
process.exit(1);
|
|
48
|
+
});
|
|
46
49
|
} else {
|
|
47
50
|
console.error(chalk.red(`Error: ${err.message || err}`));
|
|
51
|
+
process.exit(1);
|
|
48
52
|
}
|
|
49
|
-
process.exit(1);
|
|
50
53
|
}
|
|
51
54
|
});
|
|
52
55
|
|