@garyr/pt-cli 0.38.0 → 0.40.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.
@@ -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
  }
@@ -535,7 +535,11 @@ export async function learn(sourcePath, updateTemplate = null, options = {}) {
535
535
  name: targetName,
536
536
  ...templateConfig
537
537
  };
538
- 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
+ });
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(gitkeepPath))
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
  }