@garyr/pt-cli 0.25.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/src/config.ts ADDED
@@ -0,0 +1,356 @@
1
+ import YAML from 'yaml';
2
+ import fs from 'fs';
3
+ import path from 'path';
4
+ import os from 'os';
5
+ import chalk from 'chalk';
6
+
7
+ export const HOME_DIR = path.join(os.homedir(), '.pt');
8
+ export const CONFIG_PATH = path.join(HOME_DIR, 'config.yaml');
9
+
10
+ export interface FolderNode {
11
+ name: string;
12
+ info: string;
13
+ children?: FolderNode[];
14
+ is_file?: boolean;
15
+ }
16
+
17
+ export interface PostConfigTask {
18
+ command?: string; // shell command to run
19
+ description: string; // shown to user
20
+ type?: string; // only run for matching project type (optional)
21
+ always_prompt?: boolean; // if true, ask per-task even if user says "yes"
22
+ script?: string; // path to script relative to template root
23
+ cross_platform?: boolean; // if true, use platform-safe runner
24
+ checked?: boolean; // default checkbox state (only for global tasks)
25
+ }
26
+
27
+ export interface CopyFileEntry {
28
+ src: string; // relative to template root
29
+ dest: string; // relative to project root
30
+ substitute_variables?: boolean;
31
+ chmod?: string; // e.g., "0755"
32
+ }
33
+
34
+
35
+ export interface PostCopyFile {
36
+ src: string; // relative to templateRoot
37
+ dest?: string; // relative to project root (defaults to src)
38
+ }
39
+ export interface TemplateConfig {
40
+ description: string;
41
+ templateRoot?: string; // path to source directory (set by `pt learn`)
42
+ variables?: TemplateVariable[];
43
+ folders: FolderNode[];
44
+ exclude?: string[];
45
+ copy_files?: CopyFileEntry[];
46
+ post_copy?: PostCopyFile[]; // auto-detected executables/scripts
47
+ post_config?: PostConfigTask[]; // NEW
48
+ }
49
+
50
+ export interface TemplateVariable {
51
+ name: string;
52
+ prompt: string;
53
+ default?: string;
54
+ required?: boolean;
55
+ }
56
+
57
+ export interface PtConfig {
58
+ version: string;
59
+ templates: Record<string, TemplateConfig>;
60
+ ignore?: string[]; // top-level folder ignore patterns for pt learn
61
+ default_post_config?: PostConfigTask[]; // default post-config tasks applied to all projects
62
+ variables?: TemplateVariable[]; // global variables for substitution
63
+ }
64
+
65
+ export function ensureConfigDir() {
66
+ if (!fs.existsSync(HOME_DIR)) {
67
+ fs.mkdirSync(HOME_DIR, { recursive: true });
68
+ }
69
+ }
70
+
71
+ export function loadConfig(): PtConfig {
72
+ ensureConfigDir();
73
+ if (!fs.existsSync(CONFIG_PATH)) {
74
+ const defaultConfig: PtConfig = {
75
+ version: '3.0',
76
+ templates: {},
77
+ default_post_config: [],
78
+ variables: []
79
+ };
80
+ // Don't automatically save a default config on load.
81
+ // This prevents accidental creation of mostly-empty configs
82
+ // if the home directory is temporarily mapped incorrectly.
83
+ return defaultConfig;
84
+ }
85
+
86
+ try {
87
+ const content = fs.readFileSync(CONFIG_PATH, 'utf-8');
88
+ if (!content.trim()) {
89
+ throw new Error("Config file is empty");
90
+ }
91
+ const config: PtConfig = YAML.parse(content);
92
+
93
+ // Initialize ignore for legacy configs that don't have it
94
+ if (config.ignore === undefined) {
95
+ config.ignore = [];
96
+ }
97
+
98
+ // Initialize default_post_config for configs that don't have it
99
+ if (config.default_post_config === undefined) {
100
+ // Migrate from global_post_config if it exists
101
+ if ((config as any).global_post_config !== undefined) {
102
+ config.default_post_config = (config as any).global_post_config;
103
+ delete (config as any).global_post_config;
104
+ saveConfig(config);
105
+ } else {
106
+ config.default_post_config = [];
107
+ }
108
+ }
109
+
110
+ // Initialize variables for configs that don't have it
111
+ if (config.variables === undefined) {
112
+ config.variables = [];
113
+ } else if (!Array.isArray(config.variables)) {
114
+ // Migrate from Record<string, string> to TemplateVariable[]
115
+ const oldVars = config.variables as unknown as Record<string, string>;
116
+ config.variables = Object.entries(oldVars).map(([name, value]) => ({
117
+ name,
118
+ prompt: `Enter ${name}:`,
119
+ default: value
120
+ }));
121
+ }
122
+
123
+ // Migration from 2.0 to 3.0
124
+ if (config.version === '2.0') {
125
+ for (const key in config.templates) {
126
+ const t = config.templates[key];
127
+ if (t.description === undefined && (t as any).name) {
128
+ t.description = (t as any).name;
129
+ delete (t as any).name;
130
+ }
131
+ if ((t as any).type) {
132
+ delete (t as any).type;
133
+ }
134
+ }
135
+ config.version = '3.0';
136
+ saveConfig(config);
137
+ console.log(`\nConfig migrated to version 3.0 (renamed 'name' to 'description', removed 'type')`);
138
+ }
139
+
140
+ return config;
141
+ } catch (err) {
142
+ const error = err as Error;
143
+ console.error(chalk.red(`\nError loading config: ${error.message}`));
144
+ // If we have a backup, maybe suggest using it
145
+ const backupPath = CONFIG_PATH + '.bak';
146
+ if (fs.existsSync(backupPath)) {
147
+ console.error(chalk.yellow(`A backup exists at ${backupPath}. You may want to restore it.`));
148
+ }
149
+ process.exit(1);
150
+ }
151
+ }
152
+
153
+ export function saveConfig(config: PtConfig) {
154
+ ensureConfigDir();
155
+ const content = YAML.stringify(config);
156
+ const tempPath = CONFIG_PATH + '.tmp';
157
+ const backupPath = CONFIG_PATH + '.bak';
158
+
159
+ try {
160
+ // 1. Create a backup of the current valid config if it exists
161
+ if (fs.existsSync(CONFIG_PATH)) {
162
+ fs.copyFileSync(CONFIG_PATH, backupPath);
163
+ }
164
+
165
+ // 2. Write to a temporary file first (atomic save)
166
+ fs.writeFileSync(tempPath, content);
167
+
168
+ // 3. Rename temp file to actual config path
169
+ fs.renameSync(tempPath, CONFIG_PATH);
170
+ } catch (err) {
171
+ const error = err as Error;
172
+ console.error(chalk.red(`\nFailed to save config: ${error.message}`));
173
+ if (fs.existsSync(tempPath)) {
174
+ try { fs.unlinkSync(tempPath); } catch (e) {}
175
+ }
176
+ throw error;
177
+ }
178
+ }
179
+
180
+ export function getTemplateNames(config: PtConfig): string[] {
181
+ return Object.keys(config.templates || {});
182
+ }
183
+
184
+ /**
185
+ * Get default post-config tasks with defaults applied for unchecked entries.
186
+ * Default tasks that have checked=true (or undefined, which defaults to true)
187
+ * will be auto-checked. Tasks with checked=false stay unchecked by default.
188
+ */
189
+ export function getDefaultPostConfig(config: PtConfig): PostConfigTask[] {
190
+ const defaults = config.default_post_config || [];
191
+ return defaults.map(t => ({
192
+ ...t,
193
+ checked: t.checked !== false // default to true if not explicitly false
194
+ }));
195
+ }
196
+
197
+ // Default exclusions for template scanning
198
+ export const DEFAULT_EXCLUDES = [
199
+ '.git',
200
+ '.gitea',
201
+ '.vscode',
202
+ 'node_modules',
203
+ 'dist',
204
+ 'build',
205
+ 'bin',
206
+ '.DS_Store',
207
+ 'Thumbs.db',
208
+ '.stignore',
209
+ '.stfolder',
210
+ '.stversions',
211
+ ];
212
+
213
+ // Check if a path should be excluded
214
+ export function shouldExclude(dirPath: string, fullPath: string, excludes?: string[]): boolean {
215
+ const name = path.basename(fullPath); // Check the entry name, not the parent dir
216
+ const allExcludes = [...DEFAULT_EXCLUDES, ...(excludes || [])];
217
+
218
+ // Check if any entry is a git submodule
219
+ if (name === '.git' && fs.existsSync(path.join(fullPath, 'modules'))) {
220
+ return true;
221
+ }
222
+
223
+ // Check for submodules in the parent
224
+ const gitmodulesPath = path.join(fullPath, '..', '.gitmodules');
225
+ if (fs.existsSync(gitmodulesPath)) {
226
+ try {
227
+ const gitmodules = fs.readFileSync(gitmodulesPath, 'utf-8');
228
+ const regex = new RegExp(`path = ${name}\\s*$`, 'm');
229
+ if (regex.test(gitmodules)) {
230
+ return true;
231
+ }
232
+ } catch (e) {
233
+ // Ignore errors reading gitmodules
234
+ }
235
+ }
236
+
237
+ return allExcludes.includes(name);
238
+ }
239
+
240
+ // Check if a folder should be ignored based on ignore patterns.
241
+ // Patterns support glob-style wildcards:
242
+ // DAILIES/* - ignore everything inside DAILIES (DAILIES itself is kept)
243
+ // DAILIES/** - same as DAILIES/* (deep match from root)
244
+ // **/FOLDER/ - ignore any folder named FOLDER at any depth
245
+ // FOLDER - ignore this specific folder (at root or as name)
246
+ export function shouldIgnore(folderName: string, relativePath: string, ignorePatterns?: string[]): boolean {
247
+ if (!ignorePatterns || ignorePatterns.length === 0) return false;
248
+
249
+ // Normalize relativePath to use forward slashes for consistent pattern matching
250
+ const normalizedPath = relativePath.split(path.sep).join('/');
251
+ const parts = normalizedPath.split('/');
252
+
253
+ for (let pattern of ignorePatterns) {
254
+ // Normalize pattern slashes
255
+ pattern = pattern.split(/[/\\]/).join('/');
256
+
257
+ // 1. Deep match: "**/NAME" or "**/NAME/"
258
+ if (pattern.startsWith('**/')) {
259
+ let target = pattern.substring(3);
260
+ if (target.endsWith('/')) target = target.slice(0, -1);
261
+
262
+ // Match if any segment of the path matches the target
263
+ if (parts.includes(target)) return true;
264
+ continue;
265
+ }
266
+
267
+ // 2. Wildcard children at root: "FOLDER/*" or "FOLDER/**"
268
+ // Matches children of the named folder, NOT the folder itself
269
+ if (pattern.endsWith('/*') || pattern.endsWith('/**')) {
270
+ const suffix = pattern.endsWith('/**') ? 3 : 2;
271
+ const parentName = pattern.slice(0, -suffix);
272
+ if (parts[0] === parentName && parts.length > 1) {
273
+ return true;
274
+ }
275
+ continue;
276
+ }
277
+
278
+ // 3. Exact match (name or path)
279
+ let cleanPattern = pattern;
280
+ if (cleanPattern.endsWith('/')) cleanPattern = cleanPattern.slice(0, -1);
281
+
282
+ if (folderName === cleanPattern || normalizedPath === cleanPattern) {
283
+ return true;
284
+ }
285
+ }
286
+ return false;
287
+ }
288
+
289
+ // Check if a file should be excluded (e.g., .gitignore patterns)
290
+ export function shouldExcludeFile(fileName: string): boolean {
291
+ const excludePatterns = [
292
+ '*.pyc',
293
+ '*.pyo',
294
+ '*.pyd',
295
+ '.Python',
296
+ '*.egg-info',
297
+ '*.egg',
298
+ '*.whl',
299
+ '*.so',
300
+ '*.dll',
301
+ '*.dylib',
302
+ '*.exe',
303
+ '*.o',
304
+ '*.a',
305
+ '*.lib',
306
+ '*.class',
307
+ '*.jar',
308
+ '*.war',
309
+ '*.ear',
310
+ '*.log',
311
+ '*.tmp',
312
+ '*.swp',
313
+ '*.swo',
314
+ '*~',
315
+ '.bak',
316
+ '*.md',
317
+ '*.txt',
318
+ '*.json',
319
+ '*.yaml',
320
+ '*.yml',
321
+ '*.ini',
322
+ '*.conf',
323
+ '*.config',
324
+ '.gitconfig',
325
+ '.makerc',
326
+ 'Gemfile.lock',
327
+ 'package.json',
328
+ 'package-lock.json',
329
+ 'yarn.lock',
330
+ 'pnpm-lock.yaml',
331
+ 'composer.json',
332
+ 'composer.lock',
333
+ ];
334
+
335
+ for (const pattern of excludePatterns) {
336
+ if (pattern.startsWith('*')) {
337
+ const ext = pattern.substring(1);
338
+ if (fileName.endsWith(ext)) {
339
+ return true;
340
+ }
341
+ } else if (fileName === pattern) {
342
+ return true;
343
+ }
344
+ }
345
+
346
+ return false;
347
+ }
348
+ // Sanitize path to prevent traversal
349
+ export function sanitizePath(p: string): string {
350
+ // Remove any path segments that attempt to go up, and trim whitespace
351
+ const segments = p.split(/[/\\]/);
352
+ const safeSegments = segments
353
+ .map(s => s.trim())
354
+ .filter(s => s !== '..' && s !== '.' && s !== '');
355
+ return safeSegments.join(path.sep);
356
+ }
package/src/index.ts ADDED
@@ -0,0 +1,92 @@
1
+ #!/usr/bin/env node
2
+ import { Command } from 'commander';
3
+ import chalk from 'chalk';
4
+ import fs from 'fs';
5
+ import path from 'path';
6
+ import { fileURLToPath } from 'url';
7
+
8
+ // Command imports
9
+ import { learn } from './commands/learnCommand.js';
10
+ import { init } from './commands/initCommand.js';
11
+ import { configCommand } from './commands/configCommand.js';
12
+ import { ignoreCommand } from './commands/ignoreCommand.js';
13
+ import { variablesCommand } from './commands/variablesCommand.js';
14
+ import { addCommand } from './commands/addCommand.js';
15
+ import { removeCommand } from './commands/removeCommand.js';
16
+
17
+ import pkg from '../package.json' with { type: 'json' };
18
+
19
+ const program = new Command();
20
+
21
+ program
22
+ .name('pt')
23
+ .description('Project Template CLI - Learn project structures and initialize new ones')
24
+ .version(pkg.version, '-v', 'output the version number');
25
+
26
+ program
27
+ .command('learn [path]')
28
+ .description('Learn a project structure from an existing directory')
29
+ .option('--ignore <patterns>', 'Folder patterns to ignore (comma-separated)')
30
+ .option('-y, --yes', 'Automatically confirm prompts')
31
+ .option('--name <name>', 'Template name (skip prompt)')
32
+ .option('--desc <description>', 'Template description (skip prompt)')
33
+ .option('--json', 'Output template structure as JSON for sharing instead of saving')
34
+ .action(async (pathArg: string | undefined, options) => {
35
+ await learn(pathArg || '.', null, options);
36
+ });
37
+
38
+ program
39
+ .command('update <templateName> [sourcePath]')
40
+ .description('Update an existing template with new structure/files')
41
+ .option('--ignore <patterns>', 'Folder patterns to ignore (comma-separated)')
42
+ .option('-y, --yes', 'Automatically confirm prompts')
43
+ .option('--desc <description>', 'Template description (skip prompt)')
44
+ .action(async (templateName: string, sourcePath: string | undefined, options) => {
45
+ await learn(sourcePath || '.', templateName, options);
46
+ });
47
+
48
+ program
49
+ .command('init [templateName] [destPath]')
50
+ .description('Initialize a new project from a learned template')
51
+ .option('--skip-post-config', 'Skip running post-config tasks')
52
+ .option('--dry-run', 'Show what would be created without making changes')
53
+ .option('-y, --yes', 'Automatically answer yes to prompts')
54
+ .option('--vars <variables>', 'Comma-separated key=value variables (e.g. key1=val1,key2=val2)')
55
+ .action(async (typeName: string | undefined, destPath: string | undefined, options) => {
56
+ await init(typeName, destPath, options);
57
+ });
58
+
59
+ program
60
+ .command('config [templateName]')
61
+ .description('Show current config location and list templates, or export a specific template')
62
+ .option('--json', 'Output config or specific template as JSON')
63
+ .action(configCommand);
64
+
65
+ program
66
+ .command('ignore [patterns]')
67
+ .description('View or set global ignore patterns (comma-separated)')
68
+ .option('--set', 'Set the ignore patterns to the provided value')
69
+ .action(ignoreCommand);
70
+
71
+ program
72
+ .command('variables [pairs]')
73
+ .description('View or set global variables (comma-separated key=value)')
74
+ .option('--set', 'Set the variables to the provided pairs')
75
+ .option('--json <data>', 'Set variables via JSON string or file')
76
+ .option('--delete <key>', 'Delete a specific global variable')
77
+ .action(variablesCommand);
78
+
79
+ program
80
+ .command('add <name> [json]')
81
+ .description('Import/add a template from a JSON string or file')
82
+ .option('-f, --file <path>', 'Path to JSON file containing template data')
83
+ .action(addCommand);
84
+
85
+ program
86
+ .command('remove <template>')
87
+ .alias('rm')
88
+ .description('Remove a learned template from the config')
89
+ .option('-y, --yes', 'Automatically confirm removal')
90
+ .action(removeCommand);
91
+
92
+ program.parse(process.argv);
@@ -0,0 +1,87 @@
1
+ import fs from 'fs';
2
+ import path from 'path';
3
+ import { execSync } from 'child_process';
4
+ import chalk from 'chalk';
5
+ import inquirer from 'inquirer';
6
+ import { PostConfigTask } from './config.js';
7
+
8
+ export interface PostConfigOptions {
9
+ skipPostConfig?: boolean;
10
+ dryRun?: boolean;
11
+ yes?: boolean;
12
+ }
13
+
14
+ /**
15
+ * Runs post-configuration tasks for a project.
16
+ */
17
+ export async function runPostConfig(
18
+ destPath: string,
19
+ tasks: PostConfigTask[],
20
+ projectType: string,
21
+ options: PostConfigOptions = {}
22
+ ): Promise<void> {
23
+ if (options.skipPostConfig) return;
24
+
25
+ // 1. Filter tasks by type
26
+ const applicableTasks = tasks.filter(t => !t.type || t.type === projectType);
27
+
28
+ if (applicableTasks.length === 0) {
29
+ return;
30
+ }
31
+
32
+ // 2. Ask user (skip in dryRun or yes mode)
33
+ let run = false;
34
+ if (options.dryRun) {
35
+ console.log(chalk.yellow(`\n[DRY RUN] Applicable post-config tasks:`));
36
+ run = true;
37
+ } else if (options.yes) {
38
+ run = true;
39
+ } else {
40
+ const response = await inquirer.prompt({
41
+ type: 'confirm',
42
+ name: 'run',
43
+ message: 'Run post-config tasks?',
44
+ default: false
45
+ });
46
+ run = response.run;
47
+ }
48
+
49
+ if (!run) return;
50
+
51
+ // 3. Show and run each task
52
+ for (let i = 0; i < applicableTasks.length; i++) {
53
+ const task = applicableTasks[i];
54
+ const progress = `[${i + 1}/${applicableTasks.length}]`;
55
+
56
+ if (task.command) {
57
+ if (options.dryRun) {
58
+ console.log(chalk.gray(` [DRY RUN] Would run: ${task.command}`));
59
+ } else {
60
+ try {
61
+ console.log(chalk.yellow(`\n${progress} Running: ${task.command}`));
62
+ execSync(task.command, {
63
+ cwd: destPath,
64
+ stdio: 'inherit'
65
+ });
66
+ console.log(chalk.green(' ✓ Command completed successfully'));
67
+ } catch (err) {
68
+ console.log(chalk.red(' ✗ Command failed'));
69
+ }
70
+ }
71
+ }
72
+
73
+ if (task.script) {
74
+ if (options.dryRun) {
75
+ console.log(chalk.gray(` [DRY RUN] Would run script: ${task.script}`));
76
+ } else {
77
+ try {
78
+ process.stdout.write(`${progress} ${task.script} `);
79
+ // Logic to run script would go here
80
+ console.log(chalk.yellow('(not yet implemented)'));
81
+ } catch (err) {
82
+ console.log(chalk.red('✗'));
83
+ }
84
+ }
85
+ }
86
+ }
87
+ }
@@ -0,0 +1,122 @@
1
+ import chalk from 'chalk';
2
+ import { execSync } from 'child_process';
3
+ import fs from 'fs';
4
+ import path from 'path';
5
+ import { TemplateConfig } from './config.js';
6
+ import { sanitizePath } from './config.js';
7
+
8
+ /**
9
+ * Replaces all {{var}} patterns in the content with values from the variables object.
10
+ */
11
+ export function substituteVariables(
12
+ content: string,
13
+ variables: Record<string, string>
14
+ ): string {
15
+ return content.replace(/\{\{\s*(\w+)\s*\}\}/g, (_, varName) => {
16
+ return variables[varName] ?? `{{${varName}}}`;
17
+ });
18
+ }
19
+
20
+ /**
21
+ * Processes copy_files tasks from a template.
22
+ */
23
+ export async function processCopyFiles(
24
+ templateRoot: string,
25
+ resolvedDest: string,
26
+ template: TemplateConfig,
27
+ variables: Record<string, string>,
28
+ dryRun: boolean = false
29
+ ): Promise<void> {
30
+ if (!template.copy_files) return;
31
+
32
+ for (const copyFile of template.copy_files) {
33
+ const srcPath = path.join(templateRoot, copyFile.src);
34
+ const destPath = path.join(resolvedDest, sanitizePath(copyFile.dest));
35
+
36
+ if (!fs.existsSync(srcPath)) {
37
+ console.warn(chalk.yellow(`Warning: ${copyFile.src} not found in template`));
38
+ continue;
39
+ }
40
+
41
+ const stat = fs.statSync(srcPath);
42
+ if (stat.isDirectory()) {
43
+ // Recursive directory copy
44
+ if (dryRun) {
45
+ console.log(chalk.gray(` [DRY RUN] Would recursively copy directory ${copyFile.src} → ${copyFile.dest}`));
46
+ } else {
47
+ copyDirRecursive(srcPath, destPath, variables, copyFile.substitute_variables || false, copyFile.chmod);
48
+ }
49
+ console.log(chalk.green(` ✓ ${copyFile.dest} (recursive)`));
50
+ } else {
51
+ // Single file copy
52
+ if (dryRun) {
53
+ console.log(chalk.gray(` [DRY RUN] Would copy ${copyFile.src} → ${copyFile.dest}`));
54
+ if (copyFile.substitute_variables) {
55
+ console.log(chalk.gray(` [DRY RUN] Would substitute variables in ${copyFile.dest}`));
56
+ }
57
+ if (copyFile.chmod) {
58
+ console.log(chalk.gray(` [DRY RUN] Would chmod ${copyFile.chmod} ${copyFile.dest}`));
59
+ }
60
+ continue;
61
+ }
62
+
63
+ // Ensure destination directory exists
64
+ fs.mkdirSync(path.dirname(destPath), { recursive: true });
65
+
66
+ let content = fs.readFileSync(srcPath, 'utf-8');
67
+ if (copyFile.substitute_variables) {
68
+ content = substituteVariables(content, variables);
69
+ }
70
+
71
+ fs.writeFileSync(destPath, content);
72
+
73
+ if (copyFile.chmod) {
74
+ try {
75
+ fs.chmodSync(destPath, parseInt(copyFile.chmod, 8));
76
+ } catch (e) {
77
+ if (process.platform !== 'win32') {
78
+ console.error(chalk.red(`Failed to set chmod ${copyFile.chmod} on ${copyFile.dest}`));
79
+ }
80
+ }
81
+ }
82
+
83
+ console.log(chalk.green(` ✓ ${copyFile.dest}`));
84
+ }
85
+ }
86
+ }
87
+
88
+ function copyDirRecursive(
89
+ src: string,
90
+ dest: string,
91
+ variables: Record<string, string>,
92
+ substitute: boolean,
93
+ chmod?: string
94
+ ) {
95
+ fs.mkdirSync(dest, { recursive: true });
96
+ const entries = fs.readdirSync(src, { withFileTypes: true });
97
+
98
+ for (const entry of entries) {
99
+ const srcPath = path.join(src, entry.name);
100
+ const destPath = path.join(dest, entry.name);
101
+
102
+ if (entry.isDirectory()) {
103
+ copyDirRecursive(srcPath, destPath, variables, substitute, chmod);
104
+ } else {
105
+ let content = fs.readFileSync(srcPath, 'utf-8');
106
+ if (substitute) {
107
+ content = substituteVariables(content, variables);
108
+ }
109
+ fs.writeFileSync(destPath, content);
110
+
111
+ // Preserve execute permission if it exists in source
112
+ try {
113
+ const srcStat = fs.statSync(srcPath);
114
+ if (srcStat.mode & 0o111) {
115
+ fs.chmodSync(destPath, 0o755);
116
+ } else if (chmod) {
117
+ fs.chmodSync(destPath, parseInt(chmod, 8));
118
+ }
119
+ } catch (e) {}
120
+ }
121
+ }
122
+ }
package/tsconfig.json ADDED
@@ -0,0 +1,16 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2020",
4
+ "module": "NodeNext",
5
+ "lib": ["ES2020"],
6
+ "outDir": "./dist",
7
+ "rootDir": "./src",
8
+ "strict": true,
9
+ "esModuleInterop": true,
10
+ "skipLibCheck": true,
11
+ "resolveJsonModule": true,
12
+ "moduleResolution": "NodeNext"
13
+ },
14
+ "include": ["src/**/*"],
15
+ "exclude": ["node_modules", "dist"]
16
+ }