@garyr/pt-cli 0.32.1 → 0.36.4
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 +2 -2
- package/dist/commands/defaultPostConfigCommand.js +2 -1
- package/dist/commands/initCommand.js +47 -15
- package/dist/commands/learnCommand.js +1 -1
- package/dist/commands/securityResponseCommand.js +12 -0
- package/dist/config.js +17 -0
- package/dist/index.js +31 -2
- package/dist/postconfig.js +49 -6
- package/dist/remote.js +50 -2
- package/dist/safety.js +280 -0
- package/dist/substitute.js +17 -3
- package/doc/configuration.md +11 -0
- package/doc/security.md +132 -0
- package/doc/testing.md +21 -1
- package/doc/usage.md +28 -14
- package/package.json +5 -5
- package/skills/agency-pt-operator/SKILL.md +64 -6
- package/src/commands/defaultPostConfigCommand.ts +2 -1
- package/src/commands/initCommand.ts +50 -14
- package/src/commands/learnCommand.ts +2 -1
- package/src/commands/securityResponseCommand.ts +18 -0
- package/src/config.ts +21 -0
- package/src/index.ts +28 -2
- package/src/postconfig.ts +66 -5
- package/src/remote.ts +56 -2
- package/src/safety.ts +331 -0
- package/src/substitute.ts +23 -3
- package/test-direct-template.json +0 -24
package/dist/substitute.js
CHANGED
|
@@ -30,7 +30,11 @@ export async function processCopyFiles(templateRoot, resolvedDest, template, var
|
|
|
30
30
|
console.log(chalk.gray(` [DRY RUN] Would recursively copy directory ${copyFile.src} → ${copyFile.dest}`));
|
|
31
31
|
}
|
|
32
32
|
else {
|
|
33
|
-
|
|
33
|
+
const dirSubstitute = !!(copyFile.substitute_variables === true || (copyFile.substitute_variables === undefined &&
|
|
34
|
+
template.variables &&
|
|
35
|
+
template.variables.length > 0 &&
|
|
36
|
+
Object.keys(variables).length > 0));
|
|
37
|
+
copyDirRecursive(srcPath, destPath, variables, dirSubstitute, copyFile.chmod);
|
|
34
38
|
}
|
|
35
39
|
console.log(chalk.green(` ✓ ${copyFile.dest} (recursive)`));
|
|
36
40
|
}
|
|
@@ -38,7 +42,11 @@ export async function processCopyFiles(templateRoot, resolvedDest, template, var
|
|
|
38
42
|
// Single file copy
|
|
39
43
|
if (dryRun) {
|
|
40
44
|
console.log(chalk.gray(` [DRY RUN] Would copy ${copyFile.src} → ${copyFile.dest}`));
|
|
41
|
-
|
|
45
|
+
const drySubstitute = !!(copyFile.substitute_variables === true || (copyFile.substitute_variables === undefined &&
|
|
46
|
+
template.variables &&
|
|
47
|
+
template.variables.length > 0 &&
|
|
48
|
+
Object.keys(variables).length > 0));
|
|
49
|
+
if (drySubstitute) {
|
|
42
50
|
console.log(chalk.gray(` [DRY RUN] Would substitute variables in ${copyFile.dest}`));
|
|
43
51
|
}
|
|
44
52
|
if (copyFile.chmod) {
|
|
@@ -49,7 +57,13 @@ export async function processCopyFiles(templateRoot, resolvedDest, template, var
|
|
|
49
57
|
// Ensure destination directory exists
|
|
50
58
|
fs.mkdirSync(path.dirname(destPath), { recursive: true });
|
|
51
59
|
let content = fs.readFileSync(srcPath, 'utf-8');
|
|
52
|
-
if
|
|
60
|
+
// Default to substituting if substitute_variables is true, OR if it's undefined AND the template defines variables.
|
|
61
|
+
// If substitute_variables is explicitly false, do not substitute.
|
|
62
|
+
const shouldSubstitute = !!(copyFile.substitute_variables === true || (copyFile.substitute_variables === undefined &&
|
|
63
|
+
template.variables &&
|
|
64
|
+
template.variables.length > 0 &&
|
|
65
|
+
Object.keys(variables).length > 0));
|
|
66
|
+
if (shouldSubstitute) {
|
|
53
67
|
content = substituteVariables(content, variables);
|
|
54
68
|
}
|
|
55
69
|
fs.writeFileSync(destPath, content);
|
package/doc/configuration.md
CHANGED
|
@@ -8,6 +8,10 @@ Config is stored at `~/.pt/config.yaml` and contains:
|
|
|
8
8
|
- `ignore`: Global folder ignore patterns for `pt learn`
|
|
9
9
|
- `variables`: Global variable suggestions for `pt learn` (name, prompt, default, required)
|
|
10
10
|
|
|
11
|
+
## Security Policy
|
|
12
|
+
|
|
13
|
+
Please see [[security]].
|
|
14
|
+
|
|
11
15
|
## Template Variables
|
|
12
16
|
|
|
13
17
|
When learning a template, you can define variables that will be prompted during initialization:
|
|
@@ -52,6 +56,13 @@ If the directory contains a `.pt-template.json` or `template.json` file with a `
|
|
|
52
56
|
2. Manually edit `~/.pt/config.yaml` to refine `copy_files`, `post_config` commands, or add specific `chmod` requirements.
|
|
53
57
|
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.
|
|
54
58
|
|
|
59
|
+
**Security Note:** All post-config commands are subject to security validation:
|
|
60
|
+
- Dangerous commands (e.g., `curl`, `python`, `chmod`) trigger warnings with 5-second cancellation
|
|
61
|
+
- Absolute blocks (e.g., `sudo`, `rm -rf`, `dd`) are never allowed
|
|
62
|
+
- Rate limiting prevents runaway execution (50 commands per run)
|
|
63
|
+
- Execution timeout (30 seconds) prevents hung processes
|
|
64
|
+
- All events are logged to `~/.pt/security-audit.log`
|
|
65
|
+
|
|
55
66
|
```
|
|
56
67
|
javascript: [git init, npm install]
|
|
57
68
|
python: [git init, python -m venv .venv, pip install -r requirements.txt]
|
package/doc/security.md
ADDED
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
# Security Guide
|
|
2
|
+
|
|
3
|
+
## Overview
|
|
4
|
+
|
|
5
|
+
`pt-cli` implements a multi-layered security model to protect users when running post-config commands and downloading remote templates. The system uses a warning-based approach rather than strict blocking, allowing legitimate workflows while providing clear warnings for potentially dangerous operations.
|
|
6
|
+
|
|
7
|
+
## Security Policy Configuration
|
|
8
|
+
|
|
9
|
+
Security settings are configured in `~/.pt/config.yaml` under the `security` key:
|
|
10
|
+
|
|
11
|
+
```yaml
|
|
12
|
+
security:
|
|
13
|
+
securityLevel: "warn" # "warn" (default) or "strict"
|
|
14
|
+
trustedSources:
|
|
15
|
+
- "github.com/garyritchie"
|
|
16
|
+
- "git.lyonritchie.com/garyritchie"
|
|
17
|
+
- "github.com/lyonritchie"
|
|
18
|
+
maxExecutionTime: 30000 # 30 seconds per command
|
|
19
|
+
maxCommandsPerRun: 50 # rate limit per init session
|
|
20
|
+
enableAuditLogging: true # write events to security-audit.log
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
### Security Levels
|
|
24
|
+
|
|
25
|
+
- **`warn`** (default): Warning-based approach with cancellation prompts
|
|
26
|
+
- **`strict`**: More conservative defaults, enabled by default for new installations
|
|
27
|
+
|
|
28
|
+
### Trusted Sources
|
|
29
|
+
|
|
30
|
+
When downloading templates from remote URLs, `pt-cli` verifies the source against the `trustedSources` list. Untrusted sources trigger a warning and require explicit user confirmation before proceeding.
|
|
31
|
+
|
|
32
|
+
## Command Security
|
|
33
|
+
|
|
34
|
+
### Absolute Blocks (Never Allowed)
|
|
35
|
+
|
|
36
|
+
The following commands are **always blocked** regardless of security level:
|
|
37
|
+
|
|
38
|
+
- `sudo`, `su`, `su -` (privilege escalation)
|
|
39
|
+
- `dd`, `mkfs`, `fdisk` (disk operations)
|
|
40
|
+
- `rm -rf /`, `rm -r --no-preserve-root` (massive deletion)
|
|
41
|
+
- `eval`, `exec`, `source` (code execution)
|
|
42
|
+
|
|
43
|
+
### Dangerous Commands (Warning Only)
|
|
44
|
+
|
|
45
|
+
The following commands trigger a **5-second countdown** with CTRL+C cancellation:
|
|
46
|
+
|
|
47
|
+
- `curl`, `wget`, `wget -O` (remote downloads)
|
|
48
|
+
- `bash`, `sh`, `python`, `python3`, `node -e`, `node -p` (script execution)
|
|
49
|
+
- `chmod 777`, `chmod -R`, `chmod +x`, `chmod 755`, `chmod 644` (permission changes)
|
|
50
|
+
|
|
51
|
+
**Example interaction:**
|
|
52
|
+
|
|
53
|
+
```bash
|
|
54
|
+
⚠️ WARNING: This command may be dangerous: npm install
|
|
55
|
+
Press CTRL+C to cancel, or wait 5s to continue...
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
### Rate Limiting
|
|
59
|
+
|
|
60
|
+
- **50 commands per run**: Prevents runaway command execution
|
|
61
|
+
- If limit is reached, subsequent commands are skipped with a warning
|
|
62
|
+
|
|
63
|
+
### Execution Timeout
|
|
64
|
+
|
|
65
|
+
- **30 seconds per command**: Prevents hung processes
|
|
66
|
+
- Timed-out commands are logged and skipped
|
|
67
|
+
|
|
68
|
+
## Remote Template Security
|
|
69
|
+
|
|
70
|
+
When downloading templates from remote URLs:
|
|
71
|
+
|
|
72
|
+
1. **Source Verification**: Checks against `trustedSources` list
|
|
73
|
+
2. **File Size Validation**: Maximum 50MB download limit
|
|
74
|
+
3. **Archive Extraction**: Extracts to secure temporary directory
|
|
75
|
+
4. **Audit Logging**: All downloads are logged with timestamps and outcomes
|
|
76
|
+
|
|
77
|
+
## Audit Logging
|
|
78
|
+
|
|
79
|
+
All security events are logged to `~/.pt/security-audit.log`:
|
|
80
|
+
|
|
81
|
+
```
|
|
82
|
+
2026-06-27T10:01:23.456Z [WARNING] dangerous_command: npm install | type: javascript | status: warning
|
|
83
|
+
2026-06-27T10:01:24.123Z [BLOCKED] command_blocked: sudo rm -rf / | type: all | status: blocked
|
|
84
|
+
2026-06-27T10:01:25.789Z [INFO] template_loaded: https://github.com/user/template | type: remote | status: success
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
## Security Best Practices
|
|
88
|
+
|
|
89
|
+
### For Users
|
|
90
|
+
|
|
91
|
+
1. **Review post-config tasks**: Always review commands before executing
|
|
92
|
+
2. **Use trusted sources**: Only download templates from known repositories
|
|
93
|
+
3. **Monitor audit logs**: Check `~/.pt/security-audit.log` for suspicious activity
|
|
94
|
+
4. **Update regularly**: Keep `pt-cli` updated for latest security improvements
|
|
95
|
+
|
|
96
|
+
### For Template Authors
|
|
97
|
+
|
|
98
|
+
1. **Avoid dangerous commands**: Don't include `sudo`, `rm -rf`, or privilege escalation in templates
|
|
99
|
+
2. **Use safe defaults**: Prefer `npm install` over custom scripts
|
|
100
|
+
3. **Provide clear descriptions**: Explain what each post-config task does
|
|
101
|
+
4. **Test thoroughly**: Verify templates work in isolated environments
|
|
102
|
+
|
|
103
|
+
## Troubleshooting
|
|
104
|
+
|
|
105
|
+
### Security Events Not Logging
|
|
106
|
+
|
|
107
|
+
1. Check write permissions to `~/.pt/` directory
|
|
108
|
+
2. Verify `enableAuditLogging: true` in config
|
|
109
|
+
3. Check for disk space issues
|
|
110
|
+
|
|
111
|
+
### Commands Blocked Unexpectedly
|
|
112
|
+
|
|
113
|
+
1. Check if command matches absolute blocklist
|
|
114
|
+
2. Review security policy configuration
|
|
115
|
+
3. Consult audit log for specific reasons
|
|
116
|
+
|
|
117
|
+
### Remote Template Download Failed
|
|
118
|
+
|
|
119
|
+
1. Verify URL is in `trustedSources` list
|
|
120
|
+
2. Check network connectivity
|
|
121
|
+
3. Verify file size is under 50MB limit
|
|
122
|
+
4. Check for valid archive format
|
|
123
|
+
|
|
124
|
+
## Security Policy Reference
|
|
125
|
+
|
|
126
|
+
| Setting | Type | Default | Description |
|
|
127
|
+
|---------------------|---------|---------|--------------------------------------|
|
|
128
|
+
| `securityLevel` | string | `"warn"`| Security enforcement level |
|
|
129
|
+
| `trustedSources` | array | [] | List of trusted template sources |
|
|
130
|
+
| `maxExecutionTime` | number | 30000 | Max seconds per command (30s default)|
|
|
131
|
+
| `maxCommandsPerRun` | number | 50 | Rate limit per init session |
|
|
132
|
+
| `enableAuditLogging`| boolean | true | Enable security event logging |
|
package/doc/testing.md
CHANGED
|
@@ -8,18 +8,36 @@ The test suite uses Node.js's native test runner (`node:test`) and assertion lib
|
|
|
8
8
|
|
|
9
9
|
## Running Tests
|
|
10
10
|
|
|
11
|
+
### 0. Security Testing
|
|
12
|
+
|
|
13
|
+
Security features can be tested by:
|
|
14
|
+
|
|
15
|
+
1. **Testing command blocks**: Try running templates with dangerous commands like `sudo rm -rf` or `dd`
|
|
16
|
+
2. **Testing remote downloads**: Use untrusted URLs to verify source verification
|
|
17
|
+
3. **Testing rate limiting**: Execute more than 50 commands in a single init session
|
|
18
|
+
4. **Testing timeouts**: Run commands that hang to verify timeout behavior
|
|
19
|
+
5. **Reviewing audit logs**: Check `~/.pt/security-audit.log` for security events
|
|
20
|
+
|
|
21
|
+
For more details, see the [Security Guide](security.md).
|
|
22
|
+
|
|
11
23
|
### 1. Run the Entire Test Suite
|
|
24
|
+
|
|
12
25
|
To execute all tests:
|
|
26
|
+
|
|
13
27
|
```bash
|
|
14
28
|
npm test
|
|
15
29
|
```
|
|
30
|
+
|
|
16
31
|
This runs the underlying command:
|
|
32
|
+
|
|
17
33
|
```bash
|
|
18
34
|
node --import tsx --test tests/**/*.test.ts
|
|
19
35
|
```
|
|
20
36
|
|
|
21
37
|
### 2. Run Individual Test Files
|
|
38
|
+
|
|
22
39
|
To run a specific test suite, use `tsx`:
|
|
40
|
+
|
|
23
41
|
```bash
|
|
24
42
|
npx tsx --test tests/config.test.ts
|
|
25
43
|
npx tsx --test tests/learn.test.ts
|
|
@@ -27,7 +45,9 @@ npx tsx --test tests/substitute.test.ts
|
|
|
27
45
|
```
|
|
28
46
|
|
|
29
47
|
### 3. Run with Test Coverage
|
|
48
|
+
|
|
30
49
|
To generate a test coverage report directly in the terminal:
|
|
50
|
+
|
|
31
51
|
```bash
|
|
32
52
|
node --experimental-test-coverage --import tsx --test tests/**/*.test.ts
|
|
33
53
|
```
|
|
@@ -42,7 +62,7 @@ When writing new tests, please adhere to these guidelines:
|
|
|
42
62
|
2. **ESM Imports**: Since this is an ESM (ECMAScript Modules) project, file imports within tests must use the `.js` extension (e.g., `import { learn } from '../src/commands/learnCommand.js';`).
|
|
43
63
|
3. **Environment Isolation**: The configuration path relies on `process.env.HOME`. To prevent tests from polluting your user config directory, override the home directory before importing any CLI files:
|
|
44
64
|
```typescript
|
|
45
|
-
const testHome = path.join(process.cwd(),
|
|
65
|
+
const testHome = path.join(process.cwd(), ".test-home-custom");
|
|
46
66
|
process.env.HOME = testHome;
|
|
47
67
|
```
|
|
48
68
|
4. **Cleanup**: Always ensure temporary files, workspace directories, and test home directories are deleted after tests complete (e.g., in a `finally` block or `after` hook).
|
package/doc/usage.md
CHANGED
|
@@ -22,26 +22,24 @@ pt learn /path/to/PROJECT --name my_template --desc "My new template" --yes
|
|
|
22
22
|
|
|
23
23
|
### Remote Template Learning
|
|
24
24
|
|
|
25
|
-
`pt learn` supports learning templates directly from a remote Git repository or tarball archive by passing an `http://` or `https://` URL
|
|
25
|
+
`pt learn` supports learning templates directly from a remote Git repository or tarball archive by passing an `http://` or `https://` URL. **Security is enforced**: only downloads from `trustedSources` (configured in `~/.pt/config.yaml`) are allowed by default. Untrusted sources trigger a warning and require explicit confirmation.
|
|
26
26
|
|
|
27
27
|
```bash
|
|
28
28
|
# Learn a template directly from a GitHub repository
|
|
29
29
|
pt learn https://github.com/username/my-template
|
|
30
|
-
|
|
31
|
-
# Learn a template from a Gitea repository
|
|
32
|
-
pt learn https://gitea.example.com/username/my-template
|
|
33
30
|
```
|
|
34
31
|
|
|
35
32
|
#### How it works:
|
|
36
|
-
1. **URL Translation:** If a GitHub or Gitea URL is provided, `pt` automatically translates the repository URL to its corresponding tarball download endpoint (e.g., `/archive/refs/heads/main.tar.gz`).
|
|
37
|
-
2. **Download & Extraction:** The tool downloads the archive into a secure temporary folder and extracts it.
|
|
38
|
-
3. **Template Discovery:** The extracted directory is scanned for metadata (`.pt-template.json`, `template.json`, `.info.md`, `post_config.sh`, `post_config.bat`) and variable placeholders (`{{ var }}`), matching local learn functionality exactly.
|
|
39
|
-
4. **Save Config:** The template config (skeleton structure, files, variables) is saved to the local configuration, pointing to the temporary folder as the `templateRoot`.
|
|
40
33
|
|
|
34
|
+
1. **URL Translation:** If a GitHub or Gitea URL is provided, `pt` automatically translates the repository URL to its corresponding tarball download endpoint (e.g., `/archive/refs/heads/main.tar.gz`).
|
|
35
|
+
2. **Security Verification:** Downloads are restricted to trusted sources (configured in `~/.pt/config.yaml`). Untrusted sources trigger a warning and require explicit confirmation.
|
|
36
|
+
3. **Download & Extraction:** The tool downloads the archive into a secure temporary folder and extracts it. File size is validated (max 50MB).
|
|
37
|
+
4. **Template Discovery:** The extracted directory is scanned for metadata (`.pt-template.json`, `template.json`, `.info.md`, `post_config.sh`, `post_config.bat`) and variable placeholders (`{{ var }}`), matching local learn functionality exactly.
|
|
38
|
+
5. **Save Config:** The template config (skeleton structure, files, variables) is saved to the local configuration, pointing to the temporary folder as the `templateRoot`.
|
|
41
39
|
|
|
42
40
|
### Automatic Variable Detection
|
|
43
41
|
|
|
44
|
-
During `pt learn` or `pt update`, the tool automatically scans text files at the root and in the first-level subdirectories for variable placeholders using the `{{ variable_name }}` syntax.
|
|
42
|
+
During `pt learn` or `pt update`, the tool automatically scans text files at the root and in the first-level subdirectories for variable placeholders using the `{{ variable_name }}` syntax.
|
|
45
43
|
|
|
46
44
|
- **Detection Range:** Root files and 1st-level subfolder files (e.g., `README.md`, `.makerc`, `DOC/closedown.md`).
|
|
47
45
|
- **Registration:** Any detected variables are automatically added to the template's configuration with default prompts (e.g., `Enter variable_name:`).
|
|
@@ -141,7 +139,12 @@ The JSON template config file is the recommended way to make a template director
|
|
|
141
139
|
"name": "my-web-app",
|
|
142
140
|
"description": "A Node.js web application with Express",
|
|
143
141
|
"variables": [
|
|
144
|
-
{
|
|
142
|
+
{
|
|
143
|
+
"name": "project_name",
|
|
144
|
+
"prompt": "Project name:",
|
|
145
|
+
"default": "my-app",
|
|
146
|
+
"required": true
|
|
147
|
+
},
|
|
145
148
|
{ "name": "author", "prompt": "Author name:", "default": "" }
|
|
146
149
|
],
|
|
147
150
|
"folders": [
|
|
@@ -149,16 +152,18 @@ The JSON template config file is the recommended way to make a template director
|
|
|
149
152
|
{ "name": "tests", "children": [] }
|
|
150
153
|
],
|
|
151
154
|
"copy_files": [
|
|
152
|
-
{
|
|
155
|
+
{
|
|
156
|
+
"src": "package.json",
|
|
157
|
+
"dest": "package.json",
|
|
158
|
+
"substitute_variables": true
|
|
159
|
+
},
|
|
153
160
|
{ "src": "README.md", "dest": "README.md", "substitute_variables": true }
|
|
154
161
|
],
|
|
155
162
|
"post_config": [
|
|
156
163
|
{ "command": "git init", "description": "Initialize git repository" },
|
|
157
164
|
{ "command": "npm install", "description": "Install dependencies" }
|
|
158
165
|
],
|
|
159
|
-
"post_copy": [
|
|
160
|
-
{ "src": "bin/start.sh", "dest": "bin/start.sh" }
|
|
161
|
-
]
|
|
166
|
+
"post_copy": [{ "src": "bin/start.sh", "dest": "bin/start.sh" }]
|
|
162
167
|
}
|
|
163
168
|
```
|
|
164
169
|
|
|
@@ -189,30 +194,39 @@ pt init ./new-project --file .pt-template.json --yes
|
|
|
189
194
|
For a more portable, text-based approach, you can export and import templates as JSON strings or files.
|
|
190
195
|
|
|
191
196
|
#### Exporting a Template to JSON
|
|
197
|
+
|
|
192
198
|
To export an existing template from your configuration as JSON:
|
|
199
|
+
|
|
193
200
|
```bash
|
|
194
201
|
pt config <template_name> --json > my_template.json
|
|
195
202
|
```
|
|
196
203
|
|
|
197
204
|
#### Importing a Template from JSON
|
|
205
|
+
|
|
198
206
|
To add a template from a JSON file:
|
|
207
|
+
|
|
199
208
|
```bash
|
|
200
209
|
pt add <template_name> --file my_template.json
|
|
201
210
|
```
|
|
202
211
|
|
|
203
212
|
Or from a JSON string:
|
|
213
|
+
|
|
204
214
|
```bash
|
|
205
215
|
pt add <template_name> '{"description":"My Template","files":{...}}'
|
|
206
216
|
```
|
|
207
217
|
|
|
208
218
|
#### Direct JSON Scaffolding (no config registration)
|
|
219
|
+
|
|
209
220
|
To scaffold a project directly from a JSON file without adding the template to your config:
|
|
221
|
+
|
|
210
222
|
```bash
|
|
211
223
|
pt init ./destination --file my_template.json --yes
|
|
212
224
|
```
|
|
213
225
|
|
|
214
226
|
#### Exporting Full Config
|
|
227
|
+
|
|
215
228
|
To see your entire configuration (including all templates) in JSON format:
|
|
229
|
+
|
|
216
230
|
```bash
|
|
217
231
|
pt config --json
|
|
218
232
|
```
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@garyr/pt-cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.36.4",
|
|
4
4
|
"description": "Project Template CLI - Learn structures and initialize projects",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"bin": {
|
|
@@ -14,9 +14,9 @@
|
|
|
14
14
|
"test": "node --import tsx --test tests/**/*.test.ts",
|
|
15
15
|
"test:sequential": "node --import tsx --test tests/config.test.ts && node --import tsx --test tests/init.test.ts && node --import tsx --test tests/learn.test.ts && node --import tsx --test tests/substitute.test.ts && node --import tsx --test tests/config-utils.test.ts",
|
|
16
16
|
"prepublishOnly": "npm run build",
|
|
17
|
-
"build:linux": "bun build ./src/index.ts --compile --target=bun-linux-x64 --outfile ../BUILD/pt-cli/pt-linux",
|
|
18
|
-
"build:macos": "bun build ./src/index.ts --compile --target=bun-darwin-x64 --outfile ../BUILD/pt-cli/pt-macos",
|
|
19
|
-
"build:windows": "bun build ./src/index.ts --compile --target=bun-windows-x64 --outfile ../BUILD/pt-cli/pt-win.exe",
|
|
17
|
+
"build:linux": "bun build ./src/index.ts --compile --minify --target=bun-linux-x64 --outfile ../BUILD/pt-cli/pt-linux",
|
|
18
|
+
"build:macos": "bun build ./src/index.ts --compile --minify --target=bun-darwin-x64 --outfile ../BUILD/pt-cli/pt-macos",
|
|
19
|
+
"build:windows": "bun build ./src/index.ts --compile --minify --target=bun-windows-x64 --outfile ../BUILD/pt-cli/pt-win.exe",
|
|
20
20
|
"build:all": "npm run build:linux && npm run build:macos && npm run build:windows"
|
|
21
21
|
},
|
|
22
22
|
"license": "MIT",
|
|
@@ -43,4 +43,4 @@
|
|
|
43
43
|
"tsx": "^4.21.0",
|
|
44
44
|
"typescript": "^5.6.0"
|
|
45
45
|
}
|
|
46
|
-
}
|
|
46
|
+
}
|
|
@@ -79,17 +79,25 @@ pt init ./new-project --file my-template.json --yes
|
|
|
79
79
|
|
|
80
80
|
### Round-trip Workflow
|
|
81
81
|
|
|
82
|
-
The complete portable template workflow:
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
82
|
+
The complete portable template workflow, perfect for sharing:
|
|
83
|
+
|
|
84
|
+
1. **Create template:** `pt init <template> <dest> --yes --skip-post-config` — creates structure without auto-executing post-config tasks (e.g., `git init`)
|
|
85
|
+
2. **Export config:** `pt config <template> --json > <dest>/.pt-template.json` — exports portable JSON config, required to include variables
|
|
86
|
+
3. **Share directory/JSON:** Share the directory or JSON file
|
|
87
|
+
4. **Recipient:** `pt learn <path> --name <template> --yes` — imports the template
|
|
88
|
+
5. **Or scaffold directly:** `pt init ./new-project --file .pt-template.json --yes`
|
|
88
89
|
|
|
89
90
|
### JSON Output for Sharing
|
|
90
91
|
|
|
91
92
|
You can output a template as JSON for sharing without saving it: `pt learn <source_path> --json`
|
|
92
93
|
|
|
94
|
+
### Important Notes
|
|
95
|
+
|
|
96
|
+
- **Never use manual `mkdir`/`cp` steps** — `pt init` already scaffolds directory structures, and manual file copying bypasses template registration
|
|
97
|
+
- **`--skip-post-config` flag** prevents premature task execution during template creation
|
|
98
|
+
- **JSON export captures all template metadata**, variables, and post-config tasks for maximum portability
|
|
99
|
+
- The workflow ensures portability by bundling metadata and files together, allowing recipients to import via `pt learn` without manual directory manipulation
|
|
100
|
+
|
|
93
101
|
## Default Post-Config
|
|
94
102
|
|
|
95
103
|
Default post-config tasks are stored in `~/.pt/config.yaml` under `default_post_config`. They are used as suggestions during `pt learn` to apply to the newly created template. Each task supports:
|
|
@@ -128,6 +136,56 @@ Global variables are defined in `~/.pt/config.yaml` under `variables`. They act
|
|
|
128
136
|
- Atomic saves, backups, and safe initialization logic prevent data loss.
|
|
129
137
|
- Platform-specific post-config scripts (`.sh`/`.bat`) are executed automatically based on the OS.
|
|
130
138
|
|
|
139
|
+
## Security Configuration
|
|
140
|
+
|
|
141
|
+
### Security Policy Settings
|
|
142
|
+
|
|
143
|
+
Security settings are configured in `~/.pt/config.yaml` under the `security` key:
|
|
144
|
+
|
|
145
|
+
```yaml
|
|
146
|
+
security:
|
|
147
|
+
securityLevel: "warn" # "warn" (default) or "strict"
|
|
148
|
+
trustedSources:
|
|
149
|
+
- "github.com/garyritchie"
|
|
150
|
+
- "git.lyonritchie.com"
|
|
151
|
+
- "github.com/lyonritchie"
|
|
152
|
+
maxExecutionTime: 30000 # 30 seconds per command
|
|
153
|
+
maxCommandsPerRun: 50 # rate limit per init session
|
|
154
|
+
enableAuditLogging: true # write events to security-audit.log
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
### Security Levels
|
|
158
|
+
|
|
159
|
+
- **`warn`** (default): Warning-based approach with cancellation prompts
|
|
160
|
+
- **`strict`**: More conservative defaults, enabled by default for new installations
|
|
161
|
+
|
|
162
|
+
### Trusted Sources
|
|
163
|
+
|
|
164
|
+
When downloading templates from remote URLs, `pt-cli` verifies the source against the `trustedSources` list. Untrusted sources trigger a warning and require explicit user confirmation before proceeding.
|
|
165
|
+
|
|
166
|
+
### Command Security
|
|
167
|
+
|
|
168
|
+
- **Absolute blocks**: Commands like `sudo`, `rm -rf`, `dd` are always blocked
|
|
169
|
+
- **Dangerous commands**: Commands like `curl`, `python`, `chmod` trigger warnings
|
|
170
|
+
- **Rate limiting**: 50 commands per run prevents runaway execution
|
|
171
|
+
- **Execution timeout**: 30 seconds per command prevents hung processes
|
|
172
|
+
|
|
173
|
+
### Audit Logging
|
|
174
|
+
|
|
175
|
+
All security events are logged to `~/.pt/security-audit.log` for monitoring and troubleshooting.
|
|
176
|
+
|
|
177
|
+
## Security Testing
|
|
178
|
+
|
|
179
|
+
Security features can be tested by:
|
|
180
|
+
|
|
181
|
+
1. **Testing command blocks**: Try running templates with dangerous commands like `sudo rm -rf` or `dd`
|
|
182
|
+
2. **Testing remote downloads**: Use untrusted URLs to verify source verification
|
|
183
|
+
3. **Testing rate limiting**: Execute more than 50 commands in a single init session
|
|
184
|
+
4. **Testing timeouts**: Run commands that hang to verify timeout behavior
|
|
185
|
+
5. **Reviewing audit logs**: Check `~/.pt/security-audit.log` for security events
|
|
186
|
+
|
|
187
|
+
For more details, see the [Security Guide](security.md).
|
|
188
|
+
|
|
131
189
|
## CLI Reference
|
|
132
190
|
|
|
133
191
|
| Command | Description |
|
|
@@ -27,6 +27,7 @@ export function defaultPostConfigCommand(options: DefaultPostConfigOptions = {})
|
|
|
27
27
|
|
|
28
28
|
console.error('You must provide --json <data> to set the default post-config array.');
|
|
29
29
|
} else {
|
|
30
|
-
|
|
30
|
+
const tasks = config.default_post_config || [];
|
|
31
|
+
console.log(JSON.stringify(tasks, null, 2));
|
|
31
32
|
}
|
|
32
33
|
}
|
|
@@ -172,26 +172,29 @@ export async function init(targetName: string | undefined, destPath: string | un
|
|
|
172
172
|
if (fs.existsSync(srcPath)) {
|
|
173
173
|
if (options.dryRun) {
|
|
174
174
|
console.log(chalk.gray(` [DRY RUN] Would copy ${file.src} → ${file.dest || file.src}`));
|
|
175
|
-
|
|
176
|
-
if (['.sh', '.py', '.bash', '.bat'].includes(ext)) {
|
|
177
|
-
console.log(chalk.gray(` [DRY RUN] Would chmod +x ${file.dest || file.src}`));
|
|
178
|
-
}
|
|
175
|
+
console.log(chalk.gray(` [DRY RUN] Would chmod +x ${file.dest || file.src}`));
|
|
179
176
|
continue;
|
|
180
177
|
}
|
|
181
178
|
|
|
182
|
-
|
|
179
|
+
let fileContent = fs.readFileSync(srcPath, 'utf-8');
|
|
180
|
+
|
|
181
|
+
// Substitute variables in post_copy files if template has variables
|
|
182
|
+
if (template.variables && template.variables.length > 0) {
|
|
183
|
+
const { substituteVariables } = await import('../substitute.js');
|
|
184
|
+
fileContent = substituteVariables(fileContent, variables);
|
|
185
|
+
}
|
|
186
|
+
|
|
183
187
|
const destDir = path.dirname(destPath);
|
|
184
188
|
fs.mkdirSync(destDir, { recursive: true });
|
|
185
189
|
fs.writeFileSync(destPath, fileContent);
|
|
186
190
|
|
|
187
|
-
//
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
}
|
|
191
|
+
// post_copy files are executables by definition — always chmod
|
|
192
|
+
try {
|
|
193
|
+
// Check if source had execute permissions, otherwise default to 0o755
|
|
194
|
+
const srcStat = fs.statSync(srcPath);
|
|
195
|
+
fs.chmodSync(destPath, srcStat.mode & 0o111 ? srcStat.mode : 0o755);
|
|
196
|
+
} catch (e) {
|
|
197
|
+
// chmod not available (Windows)
|
|
195
198
|
}
|
|
196
199
|
console.log(chalk.green(" ✓ " + (file.dest || file.src)));
|
|
197
200
|
} else {
|
|
@@ -210,7 +213,40 @@ export async function init(targetName: string | undefined, destPath: string | un
|
|
|
210
213
|
// Use template post_config tasks
|
|
211
214
|
const allTasks = template.post_config?.filter(t => !t.type || t.type === typeName!) || [];
|
|
212
215
|
|
|
213
|
-
if (allTasks.length > 0) {
|
|
216
|
+
if (allTasks.length > 0 && !options.skipPostConfig) {
|
|
217
|
+
// SECURITY CHECK: Validate template safety before running post_config tasks
|
|
218
|
+
const { validateTemplateSecurity } = await import('../safety.js');
|
|
219
|
+
const { valid, errors, warnings } = validateTemplateSecurity(template);
|
|
220
|
+
|
|
221
|
+
if (!valid) {
|
|
222
|
+
console.error(chalk.red("\n❌ SECURITY ERROR: Aborting post_config execution due to blocked commands:"));
|
|
223
|
+
for (const err of errors) {
|
|
224
|
+
console.error(chalk.red(` - ${err}`));
|
|
225
|
+
}
|
|
226
|
+
process.exit(1);
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
if (warnings.length > 0) {
|
|
230
|
+
console.warn(chalk.yellow("\n⚠️ SECURITY WARNING: Post-config contains dangerous or suspicious commands:"));
|
|
231
|
+
for (const warn of warnings) {
|
|
232
|
+
console.warn(chalk.yellow(` - ${warn}`));
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
if (!options.yes) {
|
|
236
|
+
const { proceed } = await inquirer.prompt({
|
|
237
|
+
type: 'confirm',
|
|
238
|
+
name: 'proceed',
|
|
239
|
+
message: chalk.red('Are you sure you want to run these post-config tasks?'),
|
|
240
|
+
default: false
|
|
241
|
+
});
|
|
242
|
+
if (!proceed) {
|
|
243
|
+
console.log(chalk.yellow("Post-config tasks aborted by user."));
|
|
244
|
+
return;
|
|
245
|
+
}
|
|
246
|
+
} else {
|
|
247
|
+
console.warn(chalk.yellow("Proceeding anyway (non-interactive mode with auto-confirm enabled)."));
|
|
248
|
+
}
|
|
249
|
+
}
|
|
214
250
|
// Determine which tasks to include
|
|
215
251
|
let selectedTaskNames: string[] = [];
|
|
216
252
|
|
|
@@ -11,6 +11,7 @@ export interface LearnOptions {
|
|
|
11
11
|
name?: string;
|
|
12
12
|
desc?: string;
|
|
13
13
|
json?: boolean;
|
|
14
|
+
allowUntrusted?: boolean;
|
|
14
15
|
}
|
|
15
16
|
|
|
16
17
|
|
|
@@ -20,7 +21,7 @@ export async function learn(sourcePath: string, updateTemplate: string | null =
|
|
|
20
21
|
// Phase 1: Remote Check
|
|
21
22
|
if (sourcePath.startsWith('http')) {
|
|
22
23
|
console.log(chalk.cyan(`Downloading remote template from: ${sourcePath}...`));
|
|
23
|
-
resolvedPath = await downloadAndExtract(sourcePath);
|
|
24
|
+
resolvedPath = await downloadAndExtract(sourcePath, options.json || false, options.allowUntrusted || false);
|
|
24
25
|
} else {
|
|
25
26
|
resolvedPath = path.resolve(sourcePath);
|
|
26
27
|
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
// pt-cli/src/commands/securityResponseCommand.ts
|
|
2
|
+
// CLI command to handle security responses from GUI
|
|
3
|
+
|
|
4
|
+
import { handleSecurityResponse } from '../safety.js';
|
|
5
|
+
|
|
6
|
+
export interface SecurityResponseOptions {
|
|
7
|
+
response: string;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export async function securityResponseCommand(response: string, options: SecurityResponseOptions = { response: '' }): Promise<void> {
|
|
11
|
+
const result = await handleSecurityResponse(response);
|
|
12
|
+
|
|
13
|
+
if (result) {
|
|
14
|
+
console.log('SECURITY_RESPONSE:ALLOWED');
|
|
15
|
+
} else {
|
|
16
|
+
console.log('SECURITY_RESPONSE:DENIED');
|
|
17
|
+
}
|
|
18
|
+
}
|
package/src/config.ts
CHANGED
|
@@ -3,6 +3,7 @@ import fs from 'fs';
|
|
|
3
3
|
import path from 'path';
|
|
4
4
|
import os from 'os';
|
|
5
5
|
import chalk from 'chalk';
|
|
6
|
+
import { SecurityPolicy } from './safety.js';
|
|
6
7
|
|
|
7
8
|
export function getHomeDir(): string {
|
|
8
9
|
return path.join(os.homedir(), '.pt');
|
|
@@ -65,6 +66,7 @@ export interface PtConfig {
|
|
|
65
66
|
ignore?: string[]; // top-level folder ignore patterns for pt learn
|
|
66
67
|
default_post_config?: PostConfigTask[]; // default post-config tasks applied to all projects
|
|
67
68
|
variables?: TemplateVariable[]; // global variables for substitution
|
|
69
|
+
security?: SecurityPolicy; // security policy configuration
|
|
68
70
|
}
|
|
69
71
|
|
|
70
72
|
export function ensureConfigDir() {
|
|
@@ -221,6 +223,25 @@ export function getDefaultPostConfig(config: PtConfig): PostConfigTask[] {
|
|
|
221
223
|
}));
|
|
222
224
|
}
|
|
223
225
|
|
|
226
|
+
/**
|
|
227
|
+
* Get security policy from config or use defaults
|
|
228
|
+
*/
|
|
229
|
+
export function getSecurityPolicy(config: PtConfig): SecurityPolicy {
|
|
230
|
+
const defaultPolicy: SecurityPolicy = {
|
|
231
|
+
maxExecutionTime: 30000, // 30 seconds
|
|
232
|
+
enableAuditLogging: true,
|
|
233
|
+
trustedSources: [
|
|
234
|
+
'github.com/garyritchie',
|
|
235
|
+
'gitea.lyonritchie.com/garyritchie',
|
|
236
|
+
'github.com/lyonritchie',
|
|
237
|
+
],
|
|
238
|
+
maxCommandsPerRun: 50,
|
|
239
|
+
securityLevel: 'warn',
|
|
240
|
+
};
|
|
241
|
+
|
|
242
|
+
return config.security || defaultPolicy;
|
|
243
|
+
}
|
|
244
|
+
|
|
224
245
|
// Default exclusions for template scanning
|
|
225
246
|
export const DEFAULT_EXCLUDES = [
|
|
226
247
|
'.git',
|