@garyr/pt-cli 1.1.1 → 1.3.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/CHANGELOG.md +47 -0
- package/README.md +26 -27
- package/dist/commands/completionCommand.js +49 -12
- package/dist/commands/initCommand.js +476 -181
- package/dist/index.js +22 -4
- package/dist/substitute.js +45 -19
- package/doc/configuration.md +7 -5
- package/doc/usage.md +22 -5
- package/package.json +1 -1
- package/skills/agency-pt-operator/SKILL.md +5 -4
- package/src/commands/completionCommand.ts +49 -12
- package/src/commands/initCommand.ts +495 -194
- package/src/index.ts +20 -4
- package/src/substitute.ts +48 -21
- package/tests/modularity.test.ts +605 -0
package/dist/index.js
CHANGED
|
@@ -65,15 +65,33 @@ program
|
|
|
65
65
|
}
|
|
66
66
|
});
|
|
67
67
|
program
|
|
68
|
-
.command('init [
|
|
69
|
-
.description('Initialize a new project from
|
|
68
|
+
.command('init [args...]')
|
|
69
|
+
.description('Initialize a new project from one or more learned templates')
|
|
70
70
|
.option('-f, --file <jsonPath>', 'Initialize directly from a JSON template file without adding it to local config')
|
|
71
71
|
.option('--skip-post-config', 'Skip running post-config tasks')
|
|
72
72
|
.option('--dry-run', 'Show what would be created without making changes')
|
|
73
73
|
.option('-y, --yes', 'Automatically answer yes to prompts')
|
|
74
74
|
.option('--vars <variables>', 'Comma-separated key=value variables (e.g. key1=val1,key2=val2)')
|
|
75
|
-
.
|
|
76
|
-
|
|
75
|
+
.option('--collision <mode>', 'File collision resolution strategy (overwrite, newest)', 'overwrite')
|
|
76
|
+
.option('--json', 'Output result as JSON')
|
|
77
|
+
.action(async (args, options) => {
|
|
78
|
+
try {
|
|
79
|
+
await init(args, options);
|
|
80
|
+
}
|
|
81
|
+
catch (err) {
|
|
82
|
+
if (options.json) {
|
|
83
|
+
process.stdout.write(JSON.stringify({
|
|
84
|
+
status: 'error',
|
|
85
|
+
message: err.message || String(err)
|
|
86
|
+
}) + '\n', () => {
|
|
87
|
+
process.exit(1);
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
else {
|
|
91
|
+
console.error(chalk.red(`Error: ${err.message || err}`));
|
|
92
|
+
process.exit(1);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
77
95
|
});
|
|
78
96
|
program
|
|
79
97
|
.command('config [templateName]')
|
package/dist/substitute.js
CHANGED
|
@@ -34,44 +34,62 @@ export function substituteVariables(content, variables, maxIterations = 10) {
|
|
|
34
34
|
/**
|
|
35
35
|
* Processes copy_files tasks from a template.
|
|
36
36
|
*/
|
|
37
|
-
export async function processCopyFiles(templateRoot, resolvedDest, template, variables, dryRun = false) {
|
|
37
|
+
export async function processCopyFiles(templateRoot, resolvedDest, template, variables, dryRun = false, collisionMode = 'overwrite', silent = false) {
|
|
38
38
|
if (!template.copy_files)
|
|
39
39
|
return;
|
|
40
40
|
for (const copyFile of template.copy_files) {
|
|
41
41
|
const srcPath = path.join(templateRoot, copyFile.src);
|
|
42
42
|
const destPath = path.join(resolvedDest, sanitizePath(copyFile.dest));
|
|
43
43
|
if (!fs.existsSync(srcPath)) {
|
|
44
|
-
|
|
44
|
+
if (!silent)
|
|
45
|
+
console.warn(chalk.yellow(`Warning: ${copyFile.src} not found in template`));
|
|
45
46
|
continue;
|
|
46
47
|
}
|
|
47
48
|
const stat = fs.statSync(srcPath);
|
|
48
49
|
if (stat.isDirectory()) {
|
|
49
50
|
// Recursive directory copy
|
|
50
51
|
if (dryRun) {
|
|
51
|
-
|
|
52
|
+
if (!silent)
|
|
53
|
+
console.log(chalk.gray(` [DRY RUN] Would recursively copy directory ${copyFile.src} → ${copyFile.dest}`));
|
|
52
54
|
}
|
|
53
55
|
else {
|
|
54
56
|
const dirSubstitute = !!(copyFile.substitute_variables === true || (copyFile.substitute_variables === undefined &&
|
|
55
57
|
template.variables &&
|
|
56
58
|
template.variables.length > 0 &&
|
|
57
59
|
Object.keys(variables).length > 0));
|
|
58
|
-
copyDirRecursive(srcPath, destPath, variables, dirSubstitute, copyFile.chmod);
|
|
60
|
+
copyDirRecursive(srcPath, destPath, variables, dirSubstitute, copyFile.chmod, collisionMode, silent);
|
|
59
61
|
}
|
|
60
|
-
|
|
62
|
+
if (!silent)
|
|
63
|
+
console.log(chalk.green(` ✓ ${copyFile.dest} (recursive)`));
|
|
61
64
|
}
|
|
62
65
|
else {
|
|
66
|
+
// Check collision mode
|
|
67
|
+
if (collisionMode === 'newest' && fs.existsSync(destPath)) {
|
|
68
|
+
const destStat = fs.statSync(destPath);
|
|
69
|
+
if (destStat.mtimeMs > stat.mtimeMs) {
|
|
70
|
+
if (dryRun && !silent) {
|
|
71
|
+
console.log(chalk.yellow(` [DRY RUN] [COLLISION] Destination is newer, keeping ${copyFile.dest}`));
|
|
72
|
+
}
|
|
73
|
+
else if (!silent) {
|
|
74
|
+
console.log(chalk.yellow(` [COLLISION] Destination is newer, keeping ${copyFile.dest}`));
|
|
75
|
+
}
|
|
76
|
+
continue;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
63
79
|
// Single file copy
|
|
64
80
|
if (dryRun) {
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
81
|
+
if (!silent) {
|
|
82
|
+
console.log(chalk.gray(` [DRY RUN] Would copy ${copyFile.src} → ${copyFile.dest}`));
|
|
83
|
+
const drySubstitute = !!(copyFile.substitute_variables === true || (copyFile.substitute_variables === undefined &&
|
|
84
|
+
template.variables &&
|
|
85
|
+
template.variables.length > 0 &&
|
|
86
|
+
Object.keys(variables).length > 0));
|
|
87
|
+
if (drySubstitute) {
|
|
88
|
+
console.log(chalk.gray(` [DRY RUN] Would substitute variables in ${copyFile.dest}`));
|
|
89
|
+
}
|
|
90
|
+
if (copyFile.chmod) {
|
|
91
|
+
console.log(chalk.gray(` [DRY RUN] Would chmod ${copyFile.chmod} ${copyFile.dest}`));
|
|
92
|
+
}
|
|
75
93
|
}
|
|
76
94
|
continue;
|
|
77
95
|
}
|
|
@@ -93,25 +111,33 @@ export async function processCopyFiles(templateRoot, resolvedDest, template, var
|
|
|
93
111
|
fs.chmodSync(destPath, parseInt(copyFile.chmod, 8));
|
|
94
112
|
}
|
|
95
113
|
catch (e) {
|
|
96
|
-
if (process.platform !== 'win32') {
|
|
114
|
+
if (process.platform !== 'win32' && !silent) {
|
|
97
115
|
console.error(chalk.red(`Failed to set chmod ${copyFile.chmod} on ${copyFile.dest}`));
|
|
98
116
|
}
|
|
99
117
|
}
|
|
100
118
|
}
|
|
101
|
-
|
|
119
|
+
if (!silent)
|
|
120
|
+
console.log(chalk.green(` ✓ ${copyFile.dest}`));
|
|
102
121
|
}
|
|
103
122
|
}
|
|
104
123
|
}
|
|
105
|
-
function copyDirRecursive(src, dest, variables, substitute, chmod) {
|
|
124
|
+
function copyDirRecursive(src, dest, variables, substitute, chmod, collisionMode = 'overwrite', silent = false) {
|
|
106
125
|
fs.mkdirSync(dest, { recursive: true });
|
|
107
126
|
const entries = fs.readdirSync(src, { withFileTypes: true });
|
|
108
127
|
for (const entry of entries) {
|
|
109
128
|
const srcPath = path.join(src, entry.name);
|
|
110
129
|
const destPath = path.join(dest, entry.name);
|
|
111
130
|
if (entry.isDirectory()) {
|
|
112
|
-
copyDirRecursive(srcPath, destPath, variables, substitute, chmod);
|
|
131
|
+
copyDirRecursive(srcPath, destPath, variables, substitute, chmod, collisionMode, silent);
|
|
113
132
|
}
|
|
114
133
|
else {
|
|
134
|
+
if (collisionMode === 'newest' && fs.existsSync(destPath)) {
|
|
135
|
+
const destStat = fs.statSync(destPath);
|
|
136
|
+
const srcStat = fs.statSync(srcPath);
|
|
137
|
+
if (destStat.mtimeMs > srcStat.mtimeMs) {
|
|
138
|
+
continue;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
115
141
|
let content = fs.readFileSync(srcPath, 'utf-8');
|
|
116
142
|
if (substitute) {
|
|
117
143
|
content = substituteVariables(content, variables);
|
package/doc/configuration.md
CHANGED
|
@@ -161,14 +161,16 @@ Each task supports:
|
|
|
161
161
|
| `script` | Path to script relative to template root |
|
|
162
162
|
| `cross_platform` | If `true`, use platform-safe runner |
|
|
163
163
|
|
|
164
|
-
**Interaction flow** during `pt init
|
|
164
|
+
**Interaction flow** during `pt init` (single or multiple templates):
|
|
165
165
|
|
|
166
166
|
1. Folder structure created
|
|
167
|
-
2. If template
|
|
167
|
+
2. If template(s) have `post_config`:
|
|
168
168
|
- Filter tasks by project type
|
|
169
|
-
-
|
|
170
|
-
-
|
|
171
|
-
-
|
|
169
|
+
- **Aggregate security check** — all templates' warnings shown in one prompt
|
|
170
|
+
- If warnings exist: prompt `Security warnings found in N template(s). Run post-config tasks anyway? (y/N)`
|
|
171
|
+
- Show unified task list with checkboxes (duplicates merged, template attribution shown)
|
|
172
|
+
- Prompt: `Select post-config tasks to run:`
|
|
173
|
+
- Run selected tasks, show ✓/✗ per task
|
|
172
174
|
3. If no `post_config`, suggest baked-in defaults:
|
|
173
175
|
- Prompt: `No post-config defined. Use suggested tasks?`
|
|
174
176
|
4. If `--skip-post-config` flag: skip entirely
|
package/doc/usage.md
CHANGED
|
@@ -124,22 +124,39 @@ project=MyProject
|
|
|
124
124
|
## Initialize a project
|
|
125
125
|
|
|
126
126
|
```bash
|
|
127
|
-
# Initialize from
|
|
128
|
-
pt init <template_name> /path/to/new/PROJECT
|
|
127
|
+
# Initialize from one or more templates (auto-suggests post-config tasks)
|
|
128
|
+
pt init <template_name> [template_name2...] /path/to/new/PROJECT
|
|
129
129
|
|
|
130
130
|
# Skip post-config tasks
|
|
131
|
-
pt init <template_name> /path/to/new/PROJECT --skip-post-config
|
|
131
|
+
pt init <template_name> [template_name2...] /path/to/new/PROJECT --skip-post-config
|
|
132
132
|
|
|
133
133
|
# Dry run (preview actions without execution)
|
|
134
|
-
pt init <template_name> /path/to/new/PROJECT --dry-run
|
|
134
|
+
pt init <template_name> [template_name2...] /path/to/new/PROJECT --dry-run
|
|
135
135
|
|
|
136
136
|
# Non-interactive mode with variables (useful for an API or AI agents)
|
|
137
|
-
pt init <template_name> /path/to/new/PROJECT --yes --vars project_name=foo,author=bar
|
|
137
|
+
pt init <template_name> [template_name2...] /path/to/new/PROJECT --yes --vars project_name=foo,author=bar
|
|
138
138
|
|
|
139
139
|
# Initialize directly from a JSON template file (no config.yaml registration)
|
|
140
140
|
pt init /path/to/new/PROJECT --file my-template.json --yes
|
|
141
141
|
```
|
|
142
142
|
|
|
143
|
+
### Multi-Template Initialization
|
|
144
|
+
|
|
145
|
+
When you provide multiple template names, `pt` combines them into a single project:
|
|
146
|
+
|
|
147
|
+
- **Folder structures merged** — duplicate folder names are merged recursively
|
|
148
|
+
- **Variables merged** — duplicate variable names use the last template's defaults
|
|
149
|
+
- **Copy files merged** — files with same destination prompt for collision resolution
|
|
150
|
+
- **Post-config tasks deduplicated** — identical tasks (same command + description) run once, with template attribution shown in selection
|
|
151
|
+
|
|
152
|
+
```bash
|
|
153
|
+
# Combine base template with addon
|
|
154
|
+
pt init base-template caddy-addon /path/to/new/PROJECT
|
|
155
|
+
|
|
156
|
+
# Interactive: you'll see one security prompt for all templates, then a unified task list
|
|
157
|
+
# --yes: all deduplicated tasks run automatically
|
|
158
|
+
```
|
|
159
|
+
|
|
143
160
|
### Direct JSON Scaffolding (`--file`)
|
|
144
161
|
|
|
145
162
|
The `--file` option allows you to scaffold a project directly from a JSON template file **without** registering it in your local `~/.pt/config.yaml`. This is ideal for:
|
package/package.json
CHANGED
|
@@ -15,13 +15,14 @@ As an agent equipped with this skill, you have the ability to rapidly scaffold,
|
|
|
15
15
|
|
|
16
16
|
2. **Scaffolding (`pt init`):**
|
|
17
17
|
When a matching template exists, initialize it using the non-interactive flags. URL targets (GitHub, Gitea, etc.) are automatically translated to tarball downloads.
|
|
18
|
-
- **Command:** `pt init <template_name> <destination_path> --yes`
|
|
19
|
-
-
|
|
18
|
+
- **Command:** `pt init <template_name> [template_name2...] <destination_path> --yes`
|
|
19
|
+
- Multiple templates can be combined: `pt init base-template caddy-addon /path/to/new/PROJECT --yes`
|
|
20
|
+
- If templates require variables, pass them: `pt init <template_name> [template_name2...] <destination_path> --yes --vars key1=value1,key2=value2`
|
|
20
21
|
- **Direct JSON scaffolding:** To scaffold from a JSON template file without registering it in `config.yaml`:
|
|
21
22
|
`pt init <destination_path> --file <json_path> --yes`
|
|
22
23
|
- *Never* run `pt init` without `--yes`, as interactive prompts will block you.
|
|
23
|
-
- **Dry-run:** Preview what would be created without making changes: `pt init <template_name> <destination_path> --yes --dry-run`
|
|
24
|
-
- **Skip post-config:** Skip running post-config tasks: `pt init <template_name> <destination_path> --yes --skip-post-config`
|
|
24
|
+
- **Dry-run:** Preview what would be created without making changes: `pt init <template_name> [template_name2...] <destination_path> --yes --dry-run`
|
|
25
|
+
- **Skip post-config:** Skip running post-config tasks: `pt init <template_name> [template_name2...] <destination_path> --yes --skip-post-config`
|
|
25
26
|
- Note any errors from auto-executed post-config tasks (like `npm install` failing) and correct them if necessary.
|
|
26
27
|
|
|
27
28
|
3. **Capturing Knowledge (`pt learn`):**
|
|
@@ -66,11 +66,30 @@ _pt_completions() {
|
|
|
66
66
|
;;
|
|
67
67
|
init)
|
|
68
68
|
if [[ "$cur" == -* ]]; then
|
|
69
|
-
COMPREPLY=( $(compgen -W "-f --file --skip-post-config --dry-run -y --yes --vars -h --help" -- "$cur") )
|
|
70
|
-
elif [[ $cword -
|
|
69
|
+
COMPREPLY=( $(compgen -W "-f --file --skip-post-config --dry-run -y --yes --vars --collision --json -h --help" -- "$cur") )
|
|
70
|
+
elif [[ $cword -ge 2 ]]; then
|
|
71
|
+
# Try template name completion first
|
|
71
72
|
local templates
|
|
72
73
|
templates=$(pt completion --templates 2>/dev/null)
|
|
73
|
-
|
|
74
|
+
local template_matches
|
|
75
|
+
template_matches=$(compgen -W "$templates" -- "$cur")
|
|
76
|
+
|
|
77
|
+
# If we're at a position where destination could be (cword >= 2 for first arg after init,
|
|
78
|
+
# or cword >= 3 with templates already specified),
|
|
79
|
+
# also try directory completion. This handles cases like "pt init Base SPA PARK<TAB>"
|
|
80
|
+
local dir_matches
|
|
81
|
+
if [[ $cword -ge 2 ]]; then
|
|
82
|
+
dir_matches=$(compgen -d -- "$cur")
|
|
83
|
+
fi
|
|
84
|
+
|
|
85
|
+
# Combine matches - template matches first, then directory matches
|
|
86
|
+
COMPREPLY=()
|
|
87
|
+
if [[ -n "$template_matches" ]]; then
|
|
88
|
+
COMPREPLY=( $template_matches )
|
|
89
|
+
fi
|
|
90
|
+
if [[ -n "$dir_matches" ]]; then
|
|
91
|
+
COMPREPLY+=( $dir_matches )
|
|
92
|
+
fi
|
|
74
93
|
fi
|
|
75
94
|
;;
|
|
76
95
|
config)
|
|
@@ -194,15 +213,16 @@ _pt() {
|
|
|
194
213
|
'2:sourcePath:_files -/'
|
|
195
214
|
;;
|
|
196
215
|
init)
|
|
197
|
-
_arguments
|
|
198
|
-
'(-f --file)'{-f,--file}'[Initialize directly from a JSON template file]:file:_files'
|
|
199
|
-
'--skip-post-config[Skip running post-config tasks]'
|
|
200
|
-
'--dry-run[Show what would be created without making changes]'
|
|
201
|
-
'(-y --yes)'{-y,--yes}'[Automatically answer yes to prompts]'
|
|
202
|
-
'--vars=[Comma-separated key=value variables]:variables:'
|
|
203
|
-
'
|
|
204
|
-
'
|
|
205
|
-
'
|
|
216
|
+
_arguments \
|
|
217
|
+
'(-f --file)'{-f,--file}'[Initialize directly from a JSON template file]:file:_files' \
|
|
218
|
+
'--skip-post-config[Skip running post-config tasks]' \
|
|
219
|
+
'--dry-run[Show what would be created without making changes]' \
|
|
220
|
+
'(-y --yes)'{-y,--yes}'[Automatically answer yes to prompts]' \
|
|
221
|
+
'--vars=[Comma-separated key=value variables]:variables:' \
|
|
222
|
+
'--collision=[File collision resolution strategy]:mode:(overwrite newest)' \
|
|
223
|
+
'--json[Output result as JSON]' \
|
|
224
|
+
'(-h --help)'{-h,--help}'[display help for command]' \
|
|
225
|
+
'*: :(_pt_templates _directories)'
|
|
206
226
|
;;
|
|
207
227
|
config)
|
|
208
228
|
_arguments \\
|
|
@@ -324,11 +344,28 @@ complete -c pt -n '__fish_pt_using_command update' -l no-diff -d 'Disable additi
|
|
|
324
344
|
|
|
325
345
|
# init
|
|
326
346
|
complete -c pt -n '__fish_pt_using_command init' -a '(__fish_pt_templates)' -d 'Template name'
|
|
347
|
+
complete -c pt -n '__fish_pt_using_command init' -F -d 'Target directory' --wraps=pt --condition=__fish_pt_init_dir
|
|
327
348
|
complete -c pt -n '__fish_pt_using_command init' -s f -l file -d 'Initialize directly from a JSON template file without adding it to local config'
|
|
328
349
|
complete -c pt -n '__fish_pt_using_command init' -l skip-post-config -d 'Skip running post-config tasks'
|
|
329
350
|
complete -c pt -n '__fish_pt_using_command init' -l dry-run -d 'Show what would be created without making changes'
|
|
330
351
|
complete -c pt -n '__fish_pt_using_command init' -s y -l yes -d 'Automatically answer yes to prompts'
|
|
331
352
|
complete -c pt -n '__fish_pt_using_command init' -l vars -d 'Comma-separated key=value variables'
|
|
353
|
+
complete -c pt -n '__fish_pt_using_command init' -l collision -a 'overwrite newest' -d 'File collision resolution strategy'
|
|
354
|
+
complete -c pt -n '__fish_pt_using_command init' -l json -d 'Output result as JSON'
|
|
355
|
+
|
|
356
|
+
# Helper function for init directory completion (only when last positional arg looks like a path)
|
|
357
|
+
function __fish_pt_init_dir
|
|
358
|
+
set -l cmd (commandline -opc)
|
|
359
|
+
# Count non-flag positional arguments after 'init'
|
|
360
|
+
set -l args (string match -r '(^[^ ]+ )?init( .+)?' <<< "$cmd")
|
|
361
|
+
# Simple approach: if the current token contains / or starts with ~ or ., complete as directory
|
|
362
|
+
set -l cur (commandline -ct)
|
|
363
|
+
if string match -q '*/' "$cur"; or string match -q '~*' "$cur"; or string match -q '.*' "$cur"
|
|
364
|
+
return 0
|
|
365
|
+
else
|
|
366
|
+
return 1
|
|
367
|
+
end
|
|
368
|
+
end
|
|
332
369
|
|
|
333
370
|
# config
|
|
334
371
|
complete -c pt -n '__fish_pt_using_command config' -a '(__fish_pt_templates)' -d 'Template name'
|