@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/README.md +112 -0
- package/dist/commands/addCommand.js +37 -0
- package/dist/commands/configCommand.js +96 -0
- package/dist/commands/ignoreCommand.js +12 -0
- package/dist/commands/initCommand.js +294 -0
- package/dist/commands/learnCommand.js +473 -0
- package/dist/commands/removeCommand.js +28 -0
- package/dist/commands/variablesCommand.js +62 -0
- package/dist/config.js +283 -0
- package/dist/index.js +75 -0
- package/dist/postconfig.js +73 -0
- package/dist/substitute.js +98 -0
- package/doc/configuration.md +333 -0
- package/doc/exclusions.md +35 -0
- package/doc/usage.md +119 -0
- package/doc/variable_substitution_example.md +78 -0
- package/package.json +36 -0
- package/skills/agency-pt-operator/SKILL.md +61 -0
- package/src/commands/addCommand.ts +41 -0
- package/src/commands/configCommand.ts +102 -0
- package/src/commands/ignoreCommand.ts +17 -0
- package/src/commands/initCommand.ts +311 -0
- package/src/commands/learnCommand.ts +492 -0
- package/src/commands/removeCommand.ts +35 -0
- package/src/commands/variablesCommand.ts +67 -0
- package/src/config.ts +356 -0
- package/src/index.ts +92 -0
- package/src/postconfig.ts +87 -0
- package/src/substitute.ts +122 -0
- package/tsconfig.json +16 -0
package/README.md
ADDED
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
# pt - Project Template CLI
|
|
2
|
+
|
|
3
|
+
A CLI tool to record directory structures as templates and initialize new projects from them.
|
|
4
|
+
|
|
5
|
+
```mermaid
|
|
6
|
+
graph LR
|
|
7
|
+
subgraph Inputs ["Source & Configuration"]
|
|
8
|
+
Existing[Existing Project]
|
|
9
|
+
Config[(Template Config)]
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
Engine[[pt-cli]]
|
|
13
|
+
|
|
14
|
+
subgraph Outputs ["Generated Scaffolding"]
|
|
15
|
+
RSA[Replicated Structure A]
|
|
16
|
+
RSB[Replicated Structure B]
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
%% Flow logic
|
|
20
|
+
Existing -- Learn --> Engine
|
|
21
|
+
Config -- Read/Write --> Engine
|
|
22
|
+
Engine -- Initialize --> RSA
|
|
23
|
+
Engine -- Initialize --> RSB
|
|
24
|
+
|
|
25
|
+
%% Separate the Update logic to avoid crossing lines
|
|
26
|
+
Existing -. Update .-> Engine
|
|
27
|
+
|
|
28
|
+
style Engine fill:#f9f,stroke:#333,stroke-width:2px,color:#000
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
## The Pipeline Benefit
|
|
32
|
+
|
|
33
|
+
`pt-cli` is built to reduce boilerplate setup and ensure consistency across your workspaces. In a production pipeline, standardization is key to lowering the friction of cognitive load. `pt` helps by:
|
|
34
|
+
|
|
35
|
+
- **Instantly replicating proven architectures:** Stop recreating folder structures manually. `pt learn` saves the shape of any project.
|
|
36
|
+
- **Automating the setup grind:** With post-config tasks, `pt init` can run commands like `npm install`, `git init`, or setup python virtual environments for you.
|
|
37
|
+
- **Global post-config:** Configure shared tasks (e.g. `git init`, `git lfs install`) once in `~/.pt/config.yaml` and have them apply to every new project automatically.
|
|
38
|
+
- **Agentic automation:** Fully supports headless operation via non-interactive flags and includes a skill for integration with AI agents.
|
|
39
|
+
- **File copying & templating:** Beyond directories, it allows injecting variables into key files (`package.json`, `README.md`, etc.) and automatically ports over executable scripts.
|
|
40
|
+
|
|
41
|
+
## Features at a Glance
|
|
42
|
+
|
|
43
|
+
- Learn any directory structure and save it as a reusable template
|
|
44
|
+
- Initialize new projects from learned templates
|
|
45
|
+
- Define template variables for dynamic file customization
|
|
46
|
+
- **Automatic Variable Detection:** Scans text files for `{{ var }}` syntax during `learn`/`update`
|
|
47
|
+
- Auto-detect and suggest post-config setup tasks
|
|
48
|
+
- Configure global post-config tasks in `~/.pt/config.yaml` (apply to all projects)
|
|
49
|
+
- Baked-in defaults for common project types (javascript, python, godot, etc.)
|
|
50
|
+
- Share templates or use as an API with JSON export/import
|
|
51
|
+
- Fully supports non-interactive mode (`--yes`, `--vars`) for AI agent automation
|
|
52
|
+
|
|
53
|
+
## Quick Start
|
|
54
|
+
|
|
55
|
+
### Installation
|
|
56
|
+
|
|
57
|
+
```bash
|
|
58
|
+
# Clone this repository
|
|
59
|
+
cd pt-cli
|
|
60
|
+
npm install
|
|
61
|
+
npm run build
|
|
62
|
+
|
|
63
|
+
# Link for global use
|
|
64
|
+
npm link
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
### Basic Commands
|
|
68
|
+
|
|
69
|
+
```bash
|
|
70
|
+
# Learn an existing project structure
|
|
71
|
+
pt learn /path/to/PROJECT
|
|
72
|
+
|
|
73
|
+
# Scaffold a new project from a template
|
|
74
|
+
pt init <template_name> /path/to/NEW_PROJECT
|
|
75
|
+
|
|
76
|
+
# List available templates and configurations
|
|
77
|
+
pt config
|
|
78
|
+
|
|
79
|
+
# Export an existing template as JSON
|
|
80
|
+
pt config my-template --json > my-template.json
|
|
81
|
+
|
|
82
|
+
# Import a template from JSON
|
|
83
|
+
pt add my-new-template --file my-new-template.json
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
## Documentation
|
|
87
|
+
|
|
88
|
+
- [Detailed Usage](doc/usage.md) - Learn, Initialize, Update, and Remove commands.
|
|
89
|
+
- [Configuration Guide](doc/configuration.md) - Template variables, post-config tasks, file copying, and more.
|
|
90
|
+
- [Exclusions](doc/exclusions.md) - Learn about default ignored files and how to set custom patterns.
|
|
91
|
+
|
|
92
|
+
## Development
|
|
93
|
+
|
|
94
|
+
### Project Structure
|
|
95
|
+
|
|
96
|
+
- `src/index.ts`: Entry point and command registration.
|
|
97
|
+
- `src/commands/`: Individual command handler modules.
|
|
98
|
+
- `src/config.ts`: Configuration loading, saving, and type definitions.
|
|
99
|
+
|
|
100
|
+
### Technical Notes
|
|
101
|
+
|
|
102
|
+
- **ESM Migration**: The project is now pure ESM. All internal imports must use the `.js` extension.
|
|
103
|
+
- **Development Tooling**: Use `tsx` for running `.ts` files directly (`npm run dev`).
|
|
104
|
+
- **Building**: Use `tsc` to compile to `dist/`.
|
|
105
|
+
|
|
106
|
+
## Agent Integration
|
|
107
|
+
|
|
108
|
+
`pt-cli` is compatible with AI agents. By utilizing the non-interactive flags (`--yes`, `--vars`, `--name`, `--desc`), agents can autonomously scaffold and learn projects without hanging on interactive terminal prompts.
|
|
109
|
+
|
|
110
|
+
An official agent skill is included in this repository: [`skills/agency-pt-operator/SKILL.md`](skills/agency-pt-operator/SKILL.md).
|
|
111
|
+
|
|
112
|
+
Equipping your agent with this skill allows it to automatically use `pt-cli` to lay down standardized boilerplate and capture new architectures you develop together.
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import chalk from 'chalk';
|
|
4
|
+
import { loadConfig, saveConfig } from '../config.js';
|
|
5
|
+
export function addCommand(name, jsonStr, options = {}) {
|
|
6
|
+
const config = loadConfig();
|
|
7
|
+
try {
|
|
8
|
+
let data;
|
|
9
|
+
if (options.file) {
|
|
10
|
+
const filePath = path.resolve(options.file);
|
|
11
|
+
data = JSON.parse(fs.readFileSync(filePath, 'utf8'));
|
|
12
|
+
}
|
|
13
|
+
else if (jsonStr) {
|
|
14
|
+
data = JSON.parse(jsonStr);
|
|
15
|
+
}
|
|
16
|
+
else {
|
|
17
|
+
console.error('Error: Either a JSON string or --file <path> must be provided.');
|
|
18
|
+
process.exit(1);
|
|
19
|
+
}
|
|
20
|
+
if (!config.templates)
|
|
21
|
+
config.templates = {};
|
|
22
|
+
// Basic validation: ensure we aren't accidentally adding a full config object
|
|
23
|
+
if (data && data.templates && typeof data.templates === 'object') {
|
|
24
|
+
console.error(chalk.red('Error: The provided JSON appears to be a full configuration file, not a single template.'));
|
|
25
|
+
console.error(chalk.gray('If you want to import a specific template from it, extract that template object first.'));
|
|
26
|
+
process.exit(1);
|
|
27
|
+
}
|
|
28
|
+
config.templates[name] = data;
|
|
29
|
+
saveConfig(config);
|
|
30
|
+
console.log(chalk.green(`✓ Template "${name}" saved successfully.`));
|
|
31
|
+
}
|
|
32
|
+
catch (e) {
|
|
33
|
+
const error = e;
|
|
34
|
+
console.error(chalk.red(`Failed to parse template JSON: ${error.message}`));
|
|
35
|
+
process.exit(1);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
import { loadConfig, getTemplateNames, CONFIG_PATH } from '../config.js';
|
|
3
|
+
export function configCommand(templateName, options = {}) {
|
|
4
|
+
const config = loadConfig();
|
|
5
|
+
if (options.json) {
|
|
6
|
+
if (templateName) {
|
|
7
|
+
if (config.templates && config.templates[templateName]) {
|
|
8
|
+
const output = {
|
|
9
|
+
name: templateName,
|
|
10
|
+
...config.templates[templateName]
|
|
11
|
+
};
|
|
12
|
+
console.log(JSON.stringify(output, null, 2));
|
|
13
|
+
}
|
|
14
|
+
else {
|
|
15
|
+
console.error(chalk.red(`Error: Template "${templateName}" not found.`));
|
|
16
|
+
process.exit(1);
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
else {
|
|
20
|
+
console.log(JSON.stringify(config, null, 2));
|
|
21
|
+
}
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
24
|
+
const names = getTemplateNames(config);
|
|
25
|
+
console.log(chalk.cyan('Config Location:'), CONFIG_PATH);
|
|
26
|
+
console.log(chalk.cyan('\nLearned Templates:'));
|
|
27
|
+
if (names.length === 0) {
|
|
28
|
+
console.log(chalk.gray(' (none)'));
|
|
29
|
+
}
|
|
30
|
+
else {
|
|
31
|
+
for (const name of names) {
|
|
32
|
+
const t = config.templates[name];
|
|
33
|
+
if (!t)
|
|
34
|
+
continue;
|
|
35
|
+
console.log(chalk.white(` - ${name}`), chalk.gray(`(${t.description})`));
|
|
36
|
+
if (t.templateRoot) {
|
|
37
|
+
console.log(chalk.gray(` Source: ${t.templateRoot}`));
|
|
38
|
+
}
|
|
39
|
+
if (t.post_config && t.post_config.length > 0) {
|
|
40
|
+
console.log(chalk.cyan(' Post-config:'));
|
|
41
|
+
for (const task of t.post_config) {
|
|
42
|
+
const cmd = task.command || task.script || '(unknown)';
|
|
43
|
+
const typeFilter = task.type ? ` [type: ${task.type}]` : '';
|
|
44
|
+
console.log(chalk.gray(` - ${cmd}${typeFilter}`));
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
if (t.post_copy && t.post_copy.length > 0) {
|
|
48
|
+
console.log(chalk.cyan(' post_copy:'));
|
|
49
|
+
for (const f of t.post_copy) {
|
|
50
|
+
console.log(chalk.gray(` - ${f.src} → ${(f.dest || f.src)}`));
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
// Show global ignore patterns
|
|
56
|
+
if (config.ignore && config.ignore.length > 0) {
|
|
57
|
+
console.log(chalk.cyan('\nIgnore Patterns (pt learn):'));
|
|
58
|
+
for (const p of config.ignore) {
|
|
59
|
+
console.log(chalk.gray(` - ${p}`));
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
// Show default post-config tasks
|
|
63
|
+
if (config.default_post_config && config.default_post_config.length > 0) {
|
|
64
|
+
console.log(chalk.cyan('\nDefault Post-Config Tasks:'));
|
|
65
|
+
for (const task of config.default_post_config) {
|
|
66
|
+
const cmd = task.command || task.script || '(unknown)';
|
|
67
|
+
const desc = task.description ? ` — ${task.description}` : '';
|
|
68
|
+
const checked = task.checked !== false ? '[default: on]' : '[default: off]';
|
|
69
|
+
const typeFilter = task.type ? ` [type: ${task.type}]` : '';
|
|
70
|
+
console.log(chalk.gray(` - ${cmd}${desc}`));
|
|
71
|
+
console.log(chalk.gray(` ${checked}${typeFilter}`));
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
// Show global variables
|
|
75
|
+
if (config.variables && config.variables.length > 0) {
|
|
76
|
+
console.log(chalk.cyan('\nGlobal Variables:'));
|
|
77
|
+
for (const v of config.variables) {
|
|
78
|
+
console.log(chalk.white(` - ${v.name}:`), chalk.gray(v.default || '(no default)'));
|
|
79
|
+
if (v.prompt)
|
|
80
|
+
console.log(chalk.gray(` Prompt: ${v.prompt}`));
|
|
81
|
+
if (v.required)
|
|
82
|
+
console.log(chalk.yellow(` [Required]`));
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
console.log(chalk.cyan('\nExample post-config in config.yaml:'));
|
|
86
|
+
console.log(chalk.gray(`
|
|
87
|
+
my_template:
|
|
88
|
+
description: "My standard web project"
|
|
89
|
+
post_config:
|
|
90
|
+
- command: "git init"
|
|
91
|
+
description: "Initialize git repository"
|
|
92
|
+
- command: "npm install"
|
|
93
|
+
description: "Install npm dependencies"
|
|
94
|
+
type: "javascript"
|
|
95
|
+
`));
|
|
96
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { loadConfig, saveConfig } from '../config.js';
|
|
2
|
+
export function ignoreCommand(patterns, options = {}) {
|
|
3
|
+
const config = loadConfig();
|
|
4
|
+
if (options.set) {
|
|
5
|
+
config.ignore = patterns ? patterns.split(',').map((s) => s.trim()).filter((s) => s !== '') : [];
|
|
6
|
+
saveConfig(config);
|
|
7
|
+
console.log('Ignore patterns updated:', config.ignore);
|
|
8
|
+
}
|
|
9
|
+
else {
|
|
10
|
+
console.log('Current ignore patterns:', config.ignore || []);
|
|
11
|
+
}
|
|
12
|
+
}
|
|
@@ -0,0 +1,294 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import inquirer from 'inquirer';
|
|
4
|
+
import { loadConfig } from '../config.js';
|
|
5
|
+
import chalk from 'chalk';
|
|
6
|
+
import { processCopyFiles } from '../substitute.js';
|
|
7
|
+
import { execSync } from 'child_process';
|
|
8
|
+
export async function init(targetName, destPath, options = {}) {
|
|
9
|
+
const config = loadConfig();
|
|
10
|
+
let typeName = targetName;
|
|
11
|
+
// If no name provided, list templates
|
|
12
|
+
if (!typeName) {
|
|
13
|
+
const names = Object.keys(config.templates);
|
|
14
|
+
if (names.length === 0) {
|
|
15
|
+
console.log(chalk.red("No templates found. Run 'pt learn <path>' first."));
|
|
16
|
+
return;
|
|
17
|
+
}
|
|
18
|
+
if (options.yes) {
|
|
19
|
+
console.error(chalk.red("No project type specified and running in non-interactive mode."));
|
|
20
|
+
process.exit(1);
|
|
21
|
+
}
|
|
22
|
+
const { selected } = await inquirer.prompt({
|
|
23
|
+
type: 'list',
|
|
24
|
+
name: 'selected',
|
|
25
|
+
message: 'Select Project Type:',
|
|
26
|
+
loop: false,
|
|
27
|
+
theme: {
|
|
28
|
+
icon: {
|
|
29
|
+
cursor: chalk.green('[x] ')
|
|
30
|
+
}
|
|
31
|
+
},
|
|
32
|
+
choices: names.map(n => ({ name: n, value: n }))
|
|
33
|
+
});
|
|
34
|
+
typeName = selected;
|
|
35
|
+
}
|
|
36
|
+
const template = config.templates[typeName];
|
|
37
|
+
if (!template) {
|
|
38
|
+
console.error(chalk.red(`Template "${typeName}" not found.`));
|
|
39
|
+
process.exit(1);
|
|
40
|
+
}
|
|
41
|
+
let dest = destPath;
|
|
42
|
+
if (!dest) {
|
|
43
|
+
if (options.yes) {
|
|
44
|
+
console.error(chalk.red("No destination path specified and running in non-interactive mode."));
|
|
45
|
+
process.exit(1);
|
|
46
|
+
}
|
|
47
|
+
const { name } = await inquirer.prompt({
|
|
48
|
+
type: 'input',
|
|
49
|
+
name: 'name',
|
|
50
|
+
message: 'Project path/folder name:'
|
|
51
|
+
});
|
|
52
|
+
dest = name;
|
|
53
|
+
}
|
|
54
|
+
const resolvedDest = path.resolve(dest);
|
|
55
|
+
if (fs.existsSync(resolvedDest) && !options.dryRun) {
|
|
56
|
+
console.error(chalk.red(`Error: Destination "${resolvedDest}" already exists.`));
|
|
57
|
+
process.exit(1);
|
|
58
|
+
}
|
|
59
|
+
if (options.dryRun) {
|
|
60
|
+
console.log(chalk.yellow(`\n[DRY RUN] Initializing project "${template.description}" at: ${resolvedDest}`));
|
|
61
|
+
}
|
|
62
|
+
else {
|
|
63
|
+
console.log(chalk.cyan(`\nInitializing project "${template.description}" at: ${resolvedDest}`));
|
|
64
|
+
}
|
|
65
|
+
// Handle Variables
|
|
66
|
+
let variables = {};
|
|
67
|
+
if (template.variables && template.variables.length > 0) {
|
|
68
|
+
if (options.vars) {
|
|
69
|
+
// Parse --vars "key=val,key2=val2"
|
|
70
|
+
const pairs = options.vars.split(',').map((p) => p.trim());
|
|
71
|
+
for (const pair of pairs) {
|
|
72
|
+
const [k, ...v] = pair.split('=');
|
|
73
|
+
if (k && v.length > 0) {
|
|
74
|
+
variables[k.trim()] = v.join('=').trim();
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
if (!options.yes) {
|
|
79
|
+
// Prompt for any missing variables
|
|
80
|
+
for (const v of template.variables) {
|
|
81
|
+
if (!variables[v.name]) {
|
|
82
|
+
const answer = await inquirer.prompt({
|
|
83
|
+
type: 'input',
|
|
84
|
+
name: v.name,
|
|
85
|
+
message: v.prompt || `Enter ${v.name}:`,
|
|
86
|
+
default: v.default || ''
|
|
87
|
+
});
|
|
88
|
+
variables[v.name] = answer[v.name];
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
else {
|
|
93
|
+
// Non-interactive mode: check required
|
|
94
|
+
for (const v of template.variables) {
|
|
95
|
+
if (!variables[v.name]) {
|
|
96
|
+
if (v.required) {
|
|
97
|
+
console.error(chalk.red(`Error: Variable "${v.name}" is required but was not provided in non-interactive mode. Use --vars ${v.name}=value`));
|
|
98
|
+
process.exit(1);
|
|
99
|
+
}
|
|
100
|
+
else {
|
|
101
|
+
variables[v.name] = v.default || '';
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
// 1. Create structure
|
|
108
|
+
createStructure(resolvedDest, template.folders, options.dryRun);
|
|
109
|
+
// Check if templateRoot exists (if it's defined)
|
|
110
|
+
const templateRootExists = template.templateRoot && fs.existsSync(template.templateRoot);
|
|
111
|
+
if (template.templateRoot && !templateRootExists) {
|
|
112
|
+
console.warn(chalk.yellow(`\nWarning: Template source directory not found: ${template.templateRoot}`));
|
|
113
|
+
console.warn(chalk.gray("Folder structure created, but files/boilerplate will be skipped."));
|
|
114
|
+
}
|
|
115
|
+
// 2. Process copy_files
|
|
116
|
+
if (template.copy_files && templateRootExists) {
|
|
117
|
+
if (options.dryRun)
|
|
118
|
+
console.log(chalk.yellow("[DRY RUN] Processing copy_files..."));
|
|
119
|
+
else
|
|
120
|
+
console.log(chalk.cyan("Processing copy_files..."));
|
|
121
|
+
await processCopyFiles(template.templateRoot, resolvedDest, template, variables, options.dryRun);
|
|
122
|
+
}
|
|
123
|
+
// 3. Process post_copy (executable scripts)
|
|
124
|
+
if (template.post_copy && templateRootExists) {
|
|
125
|
+
if (options.dryRun)
|
|
126
|
+
console.log(chalk.yellow("[DRY RUN] Processing post_copy..."));
|
|
127
|
+
else
|
|
128
|
+
console.log(chalk.cyan("Processing post_copy..."));
|
|
129
|
+
for (const file of template.post_copy) {
|
|
130
|
+
const srcPath = path.join(template.templateRoot, file.src);
|
|
131
|
+
const destPath = path.join(resolvedDest, file.dest || file.src);
|
|
132
|
+
if (fs.existsSync(srcPath)) {
|
|
133
|
+
if (options.dryRun) {
|
|
134
|
+
console.log(chalk.gray(` [DRY RUN] Would copy ${file.src} → ${file.dest || file.src}`));
|
|
135
|
+
const ext = path.extname(file.src);
|
|
136
|
+
if (['.sh', '.py', '.bash', '.bat'].includes(ext)) {
|
|
137
|
+
console.log(chalk.gray(` [DRY RUN] Would chmod +x ${file.dest || file.src}`));
|
|
138
|
+
}
|
|
139
|
+
continue;
|
|
140
|
+
}
|
|
141
|
+
const fileContent = fs.readFileSync(srcPath, 'utf-8');
|
|
142
|
+
const destDir = path.dirname(destPath);
|
|
143
|
+
fs.mkdirSync(destDir, { recursive: true });
|
|
144
|
+
fs.writeFileSync(destPath, fileContent);
|
|
145
|
+
// Auto-chmod for executables
|
|
146
|
+
const ext = path.extname(file.src);
|
|
147
|
+
if (['.sh', '.py', '.bash', '.bat'].includes(ext)) {
|
|
148
|
+
try {
|
|
149
|
+
fs.chmodSync(destPath, 0o755);
|
|
150
|
+
}
|
|
151
|
+
catch (e) {
|
|
152
|
+
// chmod not available (Windows)
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
console.log(chalk.green(" ✓ " + (file.dest || file.src)));
|
|
156
|
+
}
|
|
157
|
+
else {
|
|
158
|
+
console.warn(chalk.yellow(" ! " + file.src + " not found, skipping"));
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
// Write .info.md
|
|
163
|
+
if (!options.dryRun) {
|
|
164
|
+
const infoContent = `# ${typeName}\n\n${template.description || ''}\n`;
|
|
165
|
+
fs.writeFileSync(path.join(resolvedDest, '.info.md'), infoContent);
|
|
166
|
+
}
|
|
167
|
+
else {
|
|
168
|
+
console.log(chalk.gray(` [DRY RUN] Would create .info.md`));
|
|
169
|
+
}
|
|
170
|
+
// Use template post_config tasks
|
|
171
|
+
const allTasks = template.post_config?.filter(t => !t.type || t.type === typeName) || [];
|
|
172
|
+
if (allTasks.length > 0) {
|
|
173
|
+
// Determine which tasks to include
|
|
174
|
+
let selectedTaskNames = [];
|
|
175
|
+
if (options.skipPostConfig) {
|
|
176
|
+
// Skip entirely
|
|
177
|
+
selectedTaskNames = [];
|
|
178
|
+
}
|
|
179
|
+
else if (options.dryRun) {
|
|
180
|
+
// In dry-run, select all (for display)
|
|
181
|
+
selectedTaskNames = allTasks.map(t => t.command || t.script || '');
|
|
182
|
+
console.log(chalk.yellow(`\n[DRY RUN] Applicable post-config tasks:`));
|
|
183
|
+
for (const t of allTasks) {
|
|
184
|
+
const desc = t.description ? ` (${t.description})` : '';
|
|
185
|
+
console.log(chalk.gray(` [template] - ${t.command || t.script}${desc}`));
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
else if (options.yes) {
|
|
189
|
+
// All tasks selected
|
|
190
|
+
selectedTaskNames = allTasks.map(t => t.command || t.script || '');
|
|
191
|
+
}
|
|
192
|
+
else if (allTasks.length === 0) {
|
|
193
|
+
selectedTaskNames = [];
|
|
194
|
+
}
|
|
195
|
+
else {
|
|
196
|
+
// Checkbox prompt
|
|
197
|
+
const choices = [];
|
|
198
|
+
for (const t of allTasks) {
|
|
199
|
+
const cmd = t.command || t.script || '(no command)';
|
|
200
|
+
const desc = t.description ? ` (${t.description})` : '';
|
|
201
|
+
choices.push({
|
|
202
|
+
name: `${cmd}${desc}`,
|
|
203
|
+
value: cmd,
|
|
204
|
+
checked: true
|
|
205
|
+
});
|
|
206
|
+
}
|
|
207
|
+
const response = await inquirer.prompt({
|
|
208
|
+
type: 'checkbox',
|
|
209
|
+
name: 'selected',
|
|
210
|
+
message: 'Select post-config tasks to run:',
|
|
211
|
+
loop: false,
|
|
212
|
+
theme: {
|
|
213
|
+
icon: {
|
|
214
|
+
cursor: chalk.green('[x] ')
|
|
215
|
+
}
|
|
216
|
+
},
|
|
217
|
+
choices
|
|
218
|
+
});
|
|
219
|
+
selectedTaskNames = response.selected || [];
|
|
220
|
+
}
|
|
221
|
+
// Write post_config scripts for selected tasks
|
|
222
|
+
if (selectedTaskNames.length > 0 && !options.dryRun) {
|
|
223
|
+
let bashContent = '#!/bin/bash\n# Auto-generated post_config script\n\n';
|
|
224
|
+
let batContent = '@echo off\n:: Auto-generated post_config script\n\n';
|
|
225
|
+
for (const t of allTasks) {
|
|
226
|
+
// Determine the actual command/script to use
|
|
227
|
+
let cmd = '';
|
|
228
|
+
if (t.command) {
|
|
229
|
+
cmd = t.command;
|
|
230
|
+
}
|
|
231
|
+
else if (t.script) {
|
|
232
|
+
cmd = `./${t.script}`;
|
|
233
|
+
}
|
|
234
|
+
// Match against selected names (use command if available, else script)
|
|
235
|
+
const taskKey = t.command || (t.script ? `./${t.script}` : '');
|
|
236
|
+
if (selectedTaskNames.includes(taskKey)) {
|
|
237
|
+
if (cmd) {
|
|
238
|
+
bashContent += `echo "Running: ${t.description || taskKey}"\n${cmd}\n`;
|
|
239
|
+
batContent += `echo Running: ${t.description || taskKey}\n${cmd}\n`;
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
fs.writeFileSync(path.join(resolvedDest, 'post_config.sh'), bashContent);
|
|
244
|
+
try {
|
|
245
|
+
fs.chmodSync(path.join(resolvedDest, 'post_config.sh'), 0o755);
|
|
246
|
+
}
|
|
247
|
+
catch (e) { }
|
|
248
|
+
fs.writeFileSync(path.join(resolvedDest, 'post_config.bat'), batContent);
|
|
249
|
+
// Execute the appropriate script
|
|
250
|
+
console.log(chalk.cyan("\nExecuting post-config tasks..."));
|
|
251
|
+
try {
|
|
252
|
+
const scriptCmd = process.platform === 'win32' ? 'post_config.bat' : './post_config.sh';
|
|
253
|
+
execSync(scriptCmd, {
|
|
254
|
+
cwd: resolvedDest,
|
|
255
|
+
stdio: 'inherit'
|
|
256
|
+
});
|
|
257
|
+
}
|
|
258
|
+
catch (e) {
|
|
259
|
+
console.error(chalk.red("\nError: Some post-config tasks failed. Check the output above."));
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
if (options.dryRun) {
|
|
264
|
+
console.log(chalk.yellow(`\n[DRY RUN] Project initialization preview complete.`));
|
|
265
|
+
}
|
|
266
|
+
else {
|
|
267
|
+
console.log(chalk.green(`\n✓ Project created successfully.`));
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
function createStructure(dirPath, folders, dryRun = false) {
|
|
271
|
+
for (const folder of folders) {
|
|
272
|
+
const fullDirPath = path.join(dirPath, folder.name);
|
|
273
|
+
if (dryRun) {
|
|
274
|
+
console.log(chalk.gray(` [DRY RUN] Would create directory: ${fullDirPath}`));
|
|
275
|
+
}
|
|
276
|
+
else {
|
|
277
|
+
fs.mkdirSync(fullDirPath, { recursive: true });
|
|
278
|
+
}
|
|
279
|
+
// Create .info.md if content exists
|
|
280
|
+
if (folder.info) {
|
|
281
|
+
const infoPath = path.join(fullDirPath, '.info.md');
|
|
282
|
+
if (dryRun) {
|
|
283
|
+
console.log(chalk.gray(` [DRY RUN] Would create info file: ${infoPath}`));
|
|
284
|
+
}
|
|
285
|
+
else {
|
|
286
|
+
fs.writeFileSync(infoPath, folder.info);
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
// Recurse children
|
|
290
|
+
if (folder.children && folder.children.length > 0) {
|
|
291
|
+
createStructure(fullDirPath, folder.children, dryRun);
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
}
|