@garyr/pt-cli 0.32.0 → 0.33.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 +8 -2
- package/dist/commands/configCommand.js +2 -2
- package/dist/config.js +33 -12
- package/dist/postconfig.js +49 -6
- package/dist/remote.js +33 -1
- package/dist/safety.js +250 -0
- 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 +4 -4
- package/skills/agency-pt-operator/SKILL.md +104 -15
- package/src/commands/configCommand.ts +2 -2
- package/src/config.ts +38 -12
- package/src/postconfig.ts +66 -5
- package/src/remote.ts +40 -1
- package/src/safety.ts +299 -0
- package/tests/config-u.ts +4 -0
- package/tests/config-utils.test.ts +11 -11
- package/tests/config.test.ts +111 -96
- package/tests/init.test.ts +1 -1
- package/tests/learn.test.ts +3 -3
- package/test-direct-template.json +0 -24
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.33.0",
|
|
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",
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: agency-pt-operator
|
|
3
|
-
description: Specialist in using pt-cli to scaffold project templates, capture boilerplate, and maintain standardized directory structures. Includes knowledge of default_post_config, global variables,
|
|
3
|
+
description: Specialist in using pt-cli to scaffold project templates, capture boilerplate, and maintain standardized directory structures. Includes knowledge of default_post_config, global variables, automatic variable detection, JSON template configs, and portable template workflows.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# `pt-cli` Operator Skill
|
|
@@ -14,26 +14,30 @@ As an agent equipped with this skill, you have the ability to rapidly scaffold,
|
|
|
14
14
|
- Run `pt config` to view available templates, their post-config tasks, and default post-config tasks.
|
|
15
15
|
|
|
16
16
|
2. **Scaffolding (`pt init`):**
|
|
17
|
-
When a matching template exists, initialize it using the non-interactive flags.
|
|
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
18
|
- **Command:** `pt init <template_name> <destination_path> --yes`
|
|
19
19
|
- If the template requires variables, pass them: `pt init <template_name> <destination_path> --yes --vars key1=value1,key2=value2`
|
|
20
20
|
- **Direct JSON scaffolding:** To scaffold from a JSON template file without registering it in `config.yaml`:
|
|
21
21
|
`pt init <destination_path> --file <json_path> --yes`
|
|
22
22
|
- *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`
|
|
23
25
|
- Note any errors from auto-executed post-config tasks (like `npm install` failing) and correct them if necessary.
|
|
24
26
|
|
|
25
27
|
3. **Capturing Knowledge (`pt learn`):**
|
|
26
|
-
If you spend time establishing a new, complex directory structure or configuration (e.g., a specific flavor of an Express backend with testing hooks), save it!
|
|
28
|
+
If you spend time establishing a new, complex directory structure or configuration (e.g., a specific flavor of an Express backend with testing hooks), save it! Remote URLs (GitHub, Gitea, etc.) are automatically translated to tarball downloads.
|
|
27
29
|
- **Command:** `pt learn <source_path> --name <template_name> --desc "<Description>" --yes`
|
|
28
|
-
- **
|
|
30
|
+
- **Update existing template:** Update an existing template with new structure/files: `pt update <template_name> <source_path> --yes`
|
|
31
|
+
- **Remote Templates:** Learn from a remote Git repository or archive URL directly! Pass the HTTP/HTTPS URL as the `<source_path>`:
|
|
29
32
|
`pt learn https://github.com/username/my-template --name my_template --desc "Description" --yes`
|
|
30
33
|
- Explain to the user that you've captured this template for future use.
|
|
31
34
|
- **Automatic Variable Detection:** `pt learn` and `pt update` automatically scan text files (at root and one level deep) for `{{ variable_name }}` placeholders. You can add these to files (e.g., `README.md`, `.makerc`) and run `pt update <template> . --yes` to have them registered as template variables without manual configuration.
|
|
32
|
-
- **JSON Template Config:** If the source directory contains a `.pt-template.json` or `template.json` file, `pt learn` will auto-detect name, description, variables, folders, copy_files, post_config, and post_copy from it — skipping the corresponding interactive prompts.
|
|
35
|
+
- **JSON Template Config:** If the source directory contains a `.pt-template.json` or `template.json` file, `pt learn` will auto-detect name, description, variables, folders, copy_files, post_config, and post_copy from it — skipping the corresponding interactive prompts. JSON config takes precedence over `.info.md` and shell scripts.
|
|
36
|
+
- **JSON Output:** Output template structure as JSON for sharing instead of saving: `pt learn <source_path> --json`
|
|
33
37
|
|
|
34
38
|
4. **Template Maintenance (`pt rm`):**
|
|
35
39
|
If a template is obsolete or requested for deletion, use `pt rm`.
|
|
36
|
-
- **Command:** `pt rm <template_name> --yes`
|
|
40
|
+
- **Command:** `pt rm <template_name> --yes` or `pt remove <template_name> --yes`
|
|
37
41
|
|
|
38
42
|
## Template Sharing & Portability
|
|
39
43
|
|
|
@@ -41,7 +45,7 @@ Templates are designed to be fully portable. There are two approaches to sharing
|
|
|
41
45
|
|
|
42
46
|
### 1. Directory-based Sharing (recommended for complete templates)
|
|
43
47
|
|
|
44
|
-
Place a `.pt-template.json` at the root of your template directory. This file can include all template metadata:
|
|
48
|
+
Place a `.pt-template.json` or `template.json` at the root of your template directory. This file can include all template metadata:
|
|
45
49
|
|
|
46
50
|
```json
|
|
47
51
|
{
|
|
@@ -56,9 +60,9 @@ Place a `.pt-template.json` at the root of your template directory. This file ca
|
|
|
56
60
|
}
|
|
57
61
|
```
|
|
58
62
|
|
|
59
|
-
When someone runs `pt learn /path/to/shared-dir --yes`, all metadata is auto-detected from this file.
|
|
63
|
+
When someone runs `pt learn /path/to/shared-dir --yes`, all metadata is auto-detected from this file. The priority order is: `.pt-template.json` > `template.json` > `.info.md` > `post_config.sh`/`.bat`.
|
|
60
64
|
|
|
61
|
-
|
|
65
|
+
JSON config files also take precedence over shell scripts for post_config tasks.
|
|
62
66
|
|
|
63
67
|
### 2. JSON Export/Import (for config-only sharing)
|
|
64
68
|
|
|
@@ -75,11 +79,24 @@ pt init ./new-project --file my-template.json --yes
|
|
|
75
79
|
|
|
76
80
|
### Round-trip Workflow
|
|
77
81
|
|
|
78
|
-
The complete portable template workflow:
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
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`
|
|
89
|
+
|
|
90
|
+
### JSON Output for Sharing
|
|
91
|
+
|
|
92
|
+
You can output a template as JSON for sharing without saving it: `pt learn <source_path> --json`
|
|
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
|
|
83
100
|
|
|
84
101
|
## Default Post-Config
|
|
85
102
|
|
|
@@ -101,7 +118,7 @@ Global variables are defined in `~/.pt/config.yaml` under `variables`. They act
|
|
|
101
118
|
- **Purpose:** They ensure that common metadata fields (like `author`, `license`, `project_version`) are consistently offered for inclusion in every new template you create.
|
|
102
119
|
- **Inheritance:** When you `learn` a new project structure, these global variables are automatically merged with any detected variables (`{{ var }}`) and offered as part of the new template's variable list.
|
|
103
120
|
- **Localization:** Once a template is saved, its variables are "stamped" in. Subsequent changes to global variables in the config will **not** retroactively affect old templates, ensuring stability and portability.
|
|
104
|
-
- **Management:** Use `pt variables --set key=value` to update your global defaults. For bulk updates, use `pt variables --set --json '...'`.
|
|
121
|
+
- **Management:** Use `pt variables --set key=value` to update your global defaults. For bulk updates, use `pt variables --set --json '...'`. To delete a global variable, use `pt variables --delete key`.
|
|
105
122
|
|
|
106
123
|
## Workflow Optimization
|
|
107
124
|
|
|
@@ -109,3 +126,75 @@ Global variables are defined in `~/.pt/config.yaml` under `variables`. They act
|
|
|
109
126
|
1. Identify the closest matching template (`pt config`).
|
|
110
127
|
2. Scaffold it non-interactively (`pt init ... --yes`).
|
|
111
128
|
3. Make the specific manual code/configuration changes requested by the user on top of that scaffolded base.
|
|
129
|
+
4. If the scaffold is close but needs updates, use `pt update <template_name> . --yes` to update the saved template with your changes.
|
|
130
|
+
|
|
131
|
+
## Additional Notes
|
|
132
|
+
|
|
133
|
+
- JSON variables take precedence over other variable sources during `pt update`.
|
|
134
|
+
- Optional whitespace is allowed around template variables in the substitute function.
|
|
135
|
+
- Trailing `.git` is automatically stripped from repository URLs.
|
|
136
|
+
- Atomic saves, backups, and safe initialization logic prevent data loss.
|
|
137
|
+
- Platform-specific post-config scripts (`.sh`/`.bat`) are executed automatically based on the OS.
|
|
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
|
+
|
|
189
|
+
## CLI Reference
|
|
190
|
+
|
|
191
|
+
| Command | Description |
|
|
192
|
+
|---------|-------------|
|
|
193
|
+
| `pt learn <path>` | Learn a project structure from an existing directory |
|
|
194
|
+
| `pt update <template> [path]` | Update an existing template with new structure/files |
|
|
195
|
+
| `pt init [template] [dest]` | Initialize a new project from a learned template |
|
|
196
|
+
| `pt config [template]` | Show current config location and list templates, or export a specific template |
|
|
197
|
+
| `pt variables [pairs]` | View or set global variables (comma-separated key=value) |
|
|
198
|
+
| `pt default-post-config` | View or set default post-config tasks |
|
|
199
|
+
| `pt add <name> [json]` | Import/add a template from a JSON string or file |
|
|
200
|
+
| `pt remove <template>` / `pt rm <template>` | Remove a learned template from the config |
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import chalk from 'chalk';
|
|
2
|
-
import { loadConfig, getTemplateNames,
|
|
2
|
+
import { loadConfig, getTemplateNames, getConfigPath } from '../config.js';
|
|
3
3
|
|
|
4
4
|
export interface ConfigOptions {
|
|
5
5
|
json?: boolean;
|
|
@@ -28,7 +28,7 @@ export function configCommand(templateName: string | undefined, options: ConfigO
|
|
|
28
28
|
|
|
29
29
|
const names = getTemplateNames(config);
|
|
30
30
|
|
|
31
|
-
console.log(chalk.cyan('Config Location:'),
|
|
31
|
+
console.log(chalk.cyan('Config Location:'), getConfigPath());
|
|
32
32
|
console.log(chalk.cyan('\nLearned Templates:'));
|
|
33
33
|
if (names.length === 0) {
|
|
34
34
|
console.log(chalk.gray(' (none)'));
|
package/src/config.ts
CHANGED
|
@@ -3,9 +3,15 @@ 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
|
-
export
|
|
8
|
-
|
|
8
|
+
export function getHomeDir(): string {
|
|
9
|
+
return path.join(os.homedir(), '.pt');
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function getConfigPath(): string {
|
|
13
|
+
return path.join(getHomeDir(), 'config.yaml');
|
|
14
|
+
}
|
|
9
15
|
|
|
10
16
|
export interface FolderNode {
|
|
11
17
|
name: string;
|
|
@@ -60,17 +66,18 @@ export interface PtConfig {
|
|
|
60
66
|
ignore?: string[]; // top-level folder ignore patterns for pt learn
|
|
61
67
|
default_post_config?: PostConfigTask[]; // default post-config tasks applied to all projects
|
|
62
68
|
variables?: TemplateVariable[]; // global variables for substitution
|
|
69
|
+
security?: SecurityPolicy; // security policy configuration
|
|
63
70
|
}
|
|
64
71
|
|
|
65
72
|
export function ensureConfigDir() {
|
|
66
|
-
if (!fs.existsSync(
|
|
67
|
-
fs.mkdirSync(
|
|
73
|
+
if (!fs.existsSync(getHomeDir())) {
|
|
74
|
+
fs.mkdirSync(getHomeDir(), { recursive: true });
|
|
68
75
|
}
|
|
69
76
|
}
|
|
70
77
|
|
|
71
78
|
export function loadConfig(): PtConfig {
|
|
72
79
|
ensureConfigDir();
|
|
73
|
-
if (!fs.existsSync(
|
|
80
|
+
if (!fs.existsSync(getConfigPath())) {
|
|
74
81
|
const defaultConfig: PtConfig = {
|
|
75
82
|
version: '3.0',
|
|
76
83
|
templates: {},
|
|
@@ -84,7 +91,7 @@ export function loadConfig(): PtConfig {
|
|
|
84
91
|
}
|
|
85
92
|
|
|
86
93
|
try {
|
|
87
|
-
const content = fs.readFileSync(
|
|
94
|
+
const content = fs.readFileSync(getConfigPath(), 'utf-8');
|
|
88
95
|
if (!content.trim()) {
|
|
89
96
|
throw new Error("Config file is empty");
|
|
90
97
|
}
|
|
@@ -142,7 +149,7 @@ export function loadConfig(): PtConfig {
|
|
|
142
149
|
const error = err as Error;
|
|
143
150
|
console.error(chalk.red(`\nError loading config: ${error.message}`));
|
|
144
151
|
// If we have a backup, maybe suggest using it
|
|
145
|
-
const backupPath =
|
|
152
|
+
const backupPath = getConfigPath() + '.bak';
|
|
146
153
|
if (fs.existsSync(backupPath)) {
|
|
147
154
|
console.error(chalk.yellow(`A backup exists at ${backupPath}. You may want to restore it.`));
|
|
148
155
|
}
|
|
@@ -175,20 +182,20 @@ export function saveConfig(config: PtConfig) {
|
|
|
175
182
|
}
|
|
176
183
|
|
|
177
184
|
const content = YAML.stringify(config);
|
|
178
|
-
const tempPath =
|
|
179
|
-
const backupPath =
|
|
185
|
+
const tempPath = getConfigPath() + '.tmp';
|
|
186
|
+
const backupPath = getConfigPath() + '.bak';
|
|
180
187
|
|
|
181
188
|
try {
|
|
182
189
|
// 1. Create a backup of the current valid config if it exists
|
|
183
|
-
if (fs.existsSync(
|
|
184
|
-
fs.copyFileSync(
|
|
190
|
+
if (fs.existsSync(getConfigPath())) {
|
|
191
|
+
fs.copyFileSync(getConfigPath(), backupPath);
|
|
185
192
|
}
|
|
186
193
|
|
|
187
194
|
// 2. Write to a temporary file first (atomic save)
|
|
188
195
|
fs.writeFileSync(tempPath, content);
|
|
189
196
|
|
|
190
197
|
// 3. Rename temp file to actual config path
|
|
191
|
-
fs.renameSync(tempPath,
|
|
198
|
+
fs.renameSync(tempPath, getConfigPath());
|
|
192
199
|
} catch (err) {
|
|
193
200
|
const error = err as Error;
|
|
194
201
|
console.error(chalk.red(`\nFailed to save config: ${error.message}`));
|
|
@@ -216,6 +223,25 @@ export function getDefaultPostConfig(config: PtConfig): PostConfigTask[] {
|
|
|
216
223
|
}));
|
|
217
224
|
}
|
|
218
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
|
+
|
|
219
245
|
// Default exclusions for template scanning
|
|
220
246
|
export const DEFAULT_EXCLUDES = [
|
|
221
247
|
'.git',
|
package/src/postconfig.ts
CHANGED
|
@@ -1,9 +1,19 @@
|
|
|
1
1
|
import fs from 'fs';
|
|
2
2
|
import path from 'path';
|
|
3
|
+
import os from 'os';
|
|
3
4
|
import { execSync } from 'child_process';
|
|
4
5
|
import chalk from 'chalk';
|
|
5
6
|
import inquirer from 'inquirer';
|
|
6
7
|
import { PostConfigTask } from './config.js';
|
|
8
|
+
import {
|
|
9
|
+
isBlockedCommand,
|
|
10
|
+
isDangerousCommand,
|
|
11
|
+
executeWithTimeout,
|
|
12
|
+
logSecurityEvent,
|
|
13
|
+
canExecute,
|
|
14
|
+
showDangerousCommandWarning,
|
|
15
|
+
getSecurityPolicy,
|
|
16
|
+
} from './safety.js';
|
|
7
17
|
|
|
8
18
|
export interface PostConfigOptions {
|
|
9
19
|
skipPostConfig?: boolean;
|
|
@@ -22,6 +32,16 @@ export async function runPostConfig(
|
|
|
22
32
|
): Promise<void> {
|
|
23
33
|
if (options.skipPostConfig) return;
|
|
24
34
|
|
|
35
|
+
// Load security policy
|
|
36
|
+
const configPath = path.join(process.env.HOME || os.homedir(), '.pt', 'config.yaml');
|
|
37
|
+
const securityPolicy = getSecurityPolicy(configPath);
|
|
38
|
+
|
|
39
|
+
// Check rate limiting
|
|
40
|
+
if (!canExecute('init', securityPolicy.maxCommandsPerRun)) {
|
|
41
|
+
console.log(chalk.yellow('⚠️ Rate limit reached: max commands per run exceeded'));
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
|
|
25
45
|
// 1. Filter tasks by type
|
|
26
46
|
const applicableTasks = tasks.filter(t => !t.type || t.type === projectType);
|
|
27
47
|
|
|
@@ -54,18 +74,59 @@ export async function runPostConfig(
|
|
|
54
74
|
const progress = `[${i + 1}/${applicableTasks.length}]`;
|
|
55
75
|
|
|
56
76
|
if (task.command) {
|
|
77
|
+
// SECURITY CHECK 1: Blocklist check (NEVER allow these)
|
|
78
|
+
if (isBlockedCommand(task.command)) {
|
|
79
|
+
console.log(chalk.red(`${progress} ⚠️ BLOCKED: ${task.command}`));
|
|
80
|
+
logSecurityEvent('command_blocked', task.command, projectType, 'blocked');
|
|
81
|
+
continue;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// SECURITY CHECK 2: Dangerous command warning (but allow execution)
|
|
85
|
+
if (isDangerousCommand(task.command)) {
|
|
86
|
+
console.log(chalk.yellow(`${progress} ⚠️ WARNING: This command may be dangerous: ${task.command}`));
|
|
87
|
+
console.log(chalk.yellow(` Press CTRL+C to cancel, or wait 5s to continue...`));
|
|
88
|
+
|
|
89
|
+
// Wait for user to cancel or timeout
|
|
90
|
+
const allowContinue = await showDangerousCommandWarning(task.command, 5);
|
|
91
|
+
if (!allowContinue) {
|
|
92
|
+
console.log(chalk.yellow(`${progress} ⊘ Command cancelled by user`));
|
|
93
|
+
logSecurityEvent('command_blocked', task.command, projectType, 'blocked');
|
|
94
|
+
continue;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// SECURITY CHECK 3: Rate limiting
|
|
99
|
+
if (!canExecute(task.command, securityPolicy.maxCommandsPerRun)) {
|
|
100
|
+
console.log(chalk.red(`${progress} ⚠️ Rate limited: too many commands executed`));
|
|
101
|
+
continue;
|
|
102
|
+
}
|
|
103
|
+
|
|
57
104
|
if (options.dryRun) {
|
|
58
105
|
console.log(chalk.gray(` [DRY RUN] Would run: ${task.command}`));
|
|
59
106
|
} else {
|
|
60
107
|
try {
|
|
61
108
|
console.log(chalk.yellow(`\n${progress} Running: ${task.command}`));
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
109
|
+
|
|
110
|
+
// SECURITY CHECK 4: Execution timeout
|
|
111
|
+
const result = await executeWithTimeout(
|
|
112
|
+
task.command,
|
|
113
|
+
destPath,
|
|
114
|
+
securityPolicy.maxExecutionTime
|
|
115
|
+
);
|
|
116
|
+
|
|
117
|
+
if (result.timedOut) {
|
|
118
|
+
console.log(chalk.red(` ✗ Command timed out after ${securityPolicy.maxExecutionTime / 1000}s`));
|
|
119
|
+
logSecurityEvent('command_timed_out', task.command, projectType, 'timedout');
|
|
120
|
+
} else if (result.success) {
|
|
121
|
+
console.log(chalk.green(' ✓ Command completed successfully'));
|
|
122
|
+
logSecurityEvent('command_executed', task.command, projectType, 'success');
|
|
123
|
+
} else {
|
|
124
|
+
console.log(chalk.red(` ✗ Command failed: ${result.stderr}`));
|
|
125
|
+
logSecurityEvent('command_executed', task.command, projectType, 'failed');
|
|
126
|
+
}
|
|
67
127
|
} catch (err) {
|
|
68
128
|
console.log(chalk.red(' ✗ Command failed'));
|
|
129
|
+
logSecurityEvent('command_executed', task.command, projectType, 'failed');
|
|
69
130
|
}
|
|
70
131
|
}
|
|
71
132
|
}
|
package/src/remote.ts
CHANGED
|
@@ -5,8 +5,36 @@ import os from 'os';
|
|
|
5
5
|
import { Readable } from 'stream';
|
|
6
6
|
import { finished } from 'stream/promises';
|
|
7
7
|
import { extract } from 'tar'; // You'll need: npm install tar
|
|
8
|
+
import chalk from 'chalk';
|
|
9
|
+
import { isTrustedSource, logSecurityEvent, getSecurityPolicy } from './safety.js';
|
|
10
|
+
import { loadConfig } from './config.js';
|
|
8
11
|
|
|
9
12
|
export async function downloadAndExtract(url: string): Promise<string> {
|
|
13
|
+
// Load security policy
|
|
14
|
+
const configPath = path.join(process.env.HOME || os.homedir(), '.pt', 'config.yaml');
|
|
15
|
+
const config = loadConfig();
|
|
16
|
+
const securityPolicy = config.security || {
|
|
17
|
+
trustedSources: ['github.com/garyritchie', 'gitea.lyonritchie.com/garyritchie', 'github.com/lyonritchie'],
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
// SECURITY CHECK: Verify source is trusted
|
|
21
|
+
if (!isTrustedSource(url, securityPolicy.trustedSources)) {
|
|
22
|
+
console.log(chalk.yellow(`⚠️ Warning: Template from untrusted source: ${url}`));
|
|
23
|
+
console.log(chalk.yellow(' Only use templates from trusted sources'));
|
|
24
|
+
|
|
25
|
+
const inquirer = (await import('inquirer')).default;
|
|
26
|
+
const response = await inquirer.prompt({
|
|
27
|
+
type: 'confirm',
|
|
28
|
+
name: 'proceed',
|
|
29
|
+
message: chalk.red('Continue anyway?'),
|
|
30
|
+
default: false
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
if (!response.proceed) {
|
|
34
|
+
throw new Error('Download cancelled by user due to untrusted source');
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
10
38
|
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'pt-template-'));
|
|
11
39
|
let downloadUrl = url;
|
|
12
40
|
|
|
@@ -27,10 +55,21 @@ export async function downloadAndExtract(url: string): Promise<string> {
|
|
|
27
55
|
const fileStream = fs.createWriteStream(dest);
|
|
28
56
|
await finished(Readable.fromWeb(response.body as any).pipe(fileStream));
|
|
29
57
|
|
|
58
|
+
// SECURITY: Validate downloaded file before extraction
|
|
59
|
+
const stats = fs.statSync(dest);
|
|
60
|
+
if (stats.size > 50 * 1024 * 1024) { // 50MB limit
|
|
61
|
+
throw new Error('Downloaded template is too large (>50MB)');
|
|
62
|
+
}
|
|
63
|
+
|
|
30
64
|
// Extract tarball
|
|
31
65
|
await extract({ file: dest, cwd: tempDir });
|
|
32
66
|
|
|
33
67
|
// Find the actual content folder (archives usually wrap content in a folder)
|
|
34
68
|
const dirs = fs.readdirSync(tempDir).filter(f => fs.statSync(path.join(tempDir, f)).isDirectory());
|
|
35
|
-
|
|
69
|
+
const extractedPath = path.join(tempDir, dirs[0]);
|
|
70
|
+
|
|
71
|
+
// SECURITY: Log successful download
|
|
72
|
+
logSecurityEvent('template_loaded', downloadUrl, 'remote', 'success');
|
|
73
|
+
|
|
74
|
+
return extractedPath;
|
|
36
75
|
}
|