@garyr/pt-cli 0.31.0 → 0.32.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/README.md +15 -0
- package/dist/commands/configCommand.js +2 -2
- package/dist/config.js +16 -12
- package/package.json +2 -1
- package/skills/agency-pt-operator/SKILL.md +41 -10
- package/src/commands/configCommand.ts +2 -2
- package/src/config.ts +17 -12
- package/tests/config-u.ts +4 -0
- package/tests/config-utils.test.ts +11 -11
- package/tests/config.test.ts +118 -88
- package/tests/init.test.ts +1 -1
- package/tests/learn.test.ts +3 -3
package/README.md
CHANGED
|
@@ -119,3 +119,18 @@ pt init ./new-project --file my-template.json --yes
|
|
|
119
119
|
An official agent skill is included in this repository: [`skills/agency-pt-operator/SKILL.md`](skills/agency-pt-operator/SKILL.md).
|
|
120
120
|
|
|
121
121
|
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.
|
|
122
|
+
|
|
123
|
+
## Warning
|
|
124
|
+
|
|
125
|
+
**⚠️ Beta Software Warning**: This project is in beta stage. While stable for most use cases, please make regular backups of your template configurations before running updates or major changes.
|
|
126
|
+
|
|
127
|
+
**💾 Backup Paths**:
|
|
128
|
+
|
|
129
|
+
- **Linux/macOS**: `~/.pt/` or `/home/username/.pt/` and `/Users/username/.pt/`
|
|
130
|
+
- **Windows**: `%USERPROFILE%\.pt\` (typically `C:\Users\Username\.pt\`)
|
|
131
|
+
|
|
132
|
+
Always back up these directories before installing new versions or making significant changes.
|
|
133
|
+
|
|
134
|
+
## Example Templates
|
|
135
|
+
|
|
136
|
+
There are a couple of [example templates](https://github.com/search?q=topic%3Atemplate-project+org%3Agaryritchie&type=Repositories) for you to try. These include utilities written in python to help streamline common file management tasks.
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import chalk from 'chalk';
|
|
2
|
-
import { loadConfig, getTemplateNames,
|
|
2
|
+
import { loadConfig, getTemplateNames, getConfigPath } from '../config.js';
|
|
3
3
|
export function configCommand(templateName, options = {}) {
|
|
4
4
|
const config = loadConfig();
|
|
5
5
|
if (options.json) {
|
|
@@ -22,7 +22,7 @@ export function configCommand(templateName, options = {}) {
|
|
|
22
22
|
return;
|
|
23
23
|
}
|
|
24
24
|
const names = getTemplateNames(config);
|
|
25
|
-
console.log(chalk.cyan('Config Location:'),
|
|
25
|
+
console.log(chalk.cyan('Config Location:'), getConfigPath());
|
|
26
26
|
console.log(chalk.cyan('\nLearned Templates:'));
|
|
27
27
|
if (names.length === 0) {
|
|
28
28
|
console.log(chalk.gray(' (none)'));
|
package/dist/config.js
CHANGED
|
@@ -3,16 +3,20 @@ import fs from 'fs';
|
|
|
3
3
|
import path from 'path';
|
|
4
4
|
import os from 'os';
|
|
5
5
|
import chalk from 'chalk';
|
|
6
|
-
export
|
|
7
|
-
|
|
6
|
+
export function getHomeDir() {
|
|
7
|
+
return path.join(os.homedir(), '.pt');
|
|
8
|
+
}
|
|
9
|
+
export function getConfigPath() {
|
|
10
|
+
return path.join(getHomeDir(), 'config.yaml');
|
|
11
|
+
}
|
|
8
12
|
export function ensureConfigDir() {
|
|
9
|
-
if (!fs.existsSync(
|
|
10
|
-
fs.mkdirSync(
|
|
13
|
+
if (!fs.existsSync(getHomeDir())) {
|
|
14
|
+
fs.mkdirSync(getHomeDir(), { recursive: true });
|
|
11
15
|
}
|
|
12
16
|
}
|
|
13
17
|
export function loadConfig() {
|
|
14
18
|
ensureConfigDir();
|
|
15
|
-
if (!fs.existsSync(
|
|
19
|
+
if (!fs.existsSync(getConfigPath())) {
|
|
16
20
|
const defaultConfig = {
|
|
17
21
|
version: '3.0',
|
|
18
22
|
templates: {},
|
|
@@ -25,7 +29,7 @@ export function loadConfig() {
|
|
|
25
29
|
return defaultConfig;
|
|
26
30
|
}
|
|
27
31
|
try {
|
|
28
|
-
const content = fs.readFileSync(
|
|
32
|
+
const content = fs.readFileSync(getConfigPath(), 'utf-8');
|
|
29
33
|
if (!content.trim()) {
|
|
30
34
|
throw new Error("Config file is empty");
|
|
31
35
|
}
|
|
@@ -81,7 +85,7 @@ export function loadConfig() {
|
|
|
81
85
|
const error = err;
|
|
82
86
|
console.error(chalk.red(`\nError loading config: ${error.message}`));
|
|
83
87
|
// If we have a backup, maybe suggest using it
|
|
84
|
-
const backupPath =
|
|
88
|
+
const backupPath = getConfigPath() + '.bak';
|
|
85
89
|
if (fs.existsSync(backupPath)) {
|
|
86
90
|
console.error(chalk.yellow(`A backup exists at ${backupPath}. You may want to restore it.`));
|
|
87
91
|
}
|
|
@@ -113,17 +117,17 @@ export function saveConfig(config) {
|
|
|
113
117
|
}
|
|
114
118
|
}
|
|
115
119
|
const content = YAML.stringify(config);
|
|
116
|
-
const tempPath =
|
|
117
|
-
const backupPath =
|
|
120
|
+
const tempPath = getConfigPath() + '.tmp';
|
|
121
|
+
const backupPath = getConfigPath() + '.bak';
|
|
118
122
|
try {
|
|
119
123
|
// 1. Create a backup of the current valid config if it exists
|
|
120
|
-
if (fs.existsSync(
|
|
121
|
-
fs.copyFileSync(
|
|
124
|
+
if (fs.existsSync(getConfigPath())) {
|
|
125
|
+
fs.copyFileSync(getConfigPath(), backupPath);
|
|
122
126
|
}
|
|
123
127
|
// 2. Write to a temporary file first (atomic save)
|
|
124
128
|
fs.writeFileSync(tempPath, content);
|
|
125
129
|
// 3. Rename temp file to actual config path
|
|
126
|
-
fs.renameSync(tempPath,
|
|
130
|
+
fs.renameSync(tempPath, getConfigPath());
|
|
127
131
|
}
|
|
128
132
|
catch (err) {
|
|
129
133
|
const error = err;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@garyr/pt-cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.32.1",
|
|
4
4
|
"description": "Project Template CLI - Learn structures and initialize projects",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"bin": {
|
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
"start": "tsx src/index.ts",
|
|
13
13
|
"dev": "tsx src/index.ts",
|
|
14
14
|
"test": "node --import tsx --test tests/**/*.test.ts",
|
|
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",
|
|
15
16
|
"prepublishOnly": "npm run build",
|
|
16
17
|
"build:linux": "bun build ./src/index.ts --compile --target=bun-linux-x64 --outfile ../BUILD/pt-cli/pt-linux",
|
|
17
18
|
"build:macos": "bun build ./src/index.ts --compile --target=bun-darwin-x64 --outfile ../BUILD/pt-cli/pt-macos",
|
|
@@ -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
|
|
|
@@ -80,6 +84,11 @@ The complete portable template workflow:
|
|
|
80
84
|
2. Copy `.pt-template.json` into the template source directory
|
|
81
85
|
3. Share the directory (zip, git repo, etc.)
|
|
82
86
|
4. Recipient: `pt learn /path/to/shared-dir --yes` — auto-detects everything
|
|
87
|
+
5. Or scaffold directly without registering: `pt init ./new-project --file .pt-template.json --yes`
|
|
88
|
+
|
|
89
|
+
### JSON Output for Sharing
|
|
90
|
+
|
|
91
|
+
You can output a template as JSON for sharing without saving it: `pt learn <source_path> --json`
|
|
83
92
|
|
|
84
93
|
## Default Post-Config
|
|
85
94
|
|
|
@@ -101,7 +110,7 @@ Global variables are defined in `~/.pt/config.yaml` under `variables`. They act
|
|
|
101
110
|
- **Purpose:** They ensure that common metadata fields (like `author`, `license`, `project_version`) are consistently offered for inclusion in every new template you create.
|
|
102
111
|
- **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
112
|
- **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 '...'`.
|
|
113
|
+
- **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
114
|
|
|
106
115
|
## Workflow Optimization
|
|
107
116
|
|
|
@@ -109,3 +118,25 @@ Global variables are defined in `~/.pt/config.yaml` under `variables`. They act
|
|
|
109
118
|
1. Identify the closest matching template (`pt config`).
|
|
110
119
|
2. Scaffold it non-interactively (`pt init ... --yes`).
|
|
111
120
|
3. Make the specific manual code/configuration changes requested by the user on top of that scaffolded base.
|
|
121
|
+
4. If the scaffold is close but needs updates, use `pt update <template_name> . --yes` to update the saved template with your changes.
|
|
122
|
+
|
|
123
|
+
## Additional Notes
|
|
124
|
+
|
|
125
|
+
- JSON variables take precedence over other variable sources during `pt update`.
|
|
126
|
+
- Optional whitespace is allowed around template variables in the substitute function.
|
|
127
|
+
- Trailing `.git` is automatically stripped from repository URLs.
|
|
128
|
+
- Atomic saves, backups, and safe initialization logic prevent data loss.
|
|
129
|
+
- Platform-specific post-config scripts (`.sh`/`.bat`) are executed automatically based on the OS.
|
|
130
|
+
|
|
131
|
+
## CLI Reference
|
|
132
|
+
|
|
133
|
+
| Command | Description |
|
|
134
|
+
|---------|-------------|
|
|
135
|
+
| `pt learn <path>` | Learn a project structure from an existing directory |
|
|
136
|
+
| `pt update <template> [path]` | Update an existing template with new structure/files |
|
|
137
|
+
| `pt init [template] [dest]` | Initialize a new project from a learned template |
|
|
138
|
+
| `pt config [template]` | Show current config location and list templates, or export a specific template |
|
|
139
|
+
| `pt variables [pairs]` | View or set global variables (comma-separated key=value) |
|
|
140
|
+
| `pt default-post-config` | View or set default post-config tasks |
|
|
141
|
+
| `pt add <name> [json]` | Import/add a template from a JSON string or file |
|
|
142
|
+
| `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
|
@@ -4,8 +4,13 @@ import path from 'path';
|
|
|
4
4
|
import os from 'os';
|
|
5
5
|
import chalk from 'chalk';
|
|
6
6
|
|
|
7
|
-
export
|
|
8
|
-
|
|
7
|
+
export function getHomeDir(): string {
|
|
8
|
+
return path.join(os.homedir(), '.pt');
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function getConfigPath(): string {
|
|
12
|
+
return path.join(getHomeDir(), 'config.yaml');
|
|
13
|
+
}
|
|
9
14
|
|
|
10
15
|
export interface FolderNode {
|
|
11
16
|
name: string;
|
|
@@ -63,14 +68,14 @@ export interface PtConfig {
|
|
|
63
68
|
}
|
|
64
69
|
|
|
65
70
|
export function ensureConfigDir() {
|
|
66
|
-
if (!fs.existsSync(
|
|
67
|
-
fs.mkdirSync(
|
|
71
|
+
if (!fs.existsSync(getHomeDir())) {
|
|
72
|
+
fs.mkdirSync(getHomeDir(), { recursive: true });
|
|
68
73
|
}
|
|
69
74
|
}
|
|
70
75
|
|
|
71
76
|
export function loadConfig(): PtConfig {
|
|
72
77
|
ensureConfigDir();
|
|
73
|
-
if (!fs.existsSync(
|
|
78
|
+
if (!fs.existsSync(getConfigPath())) {
|
|
74
79
|
const defaultConfig: PtConfig = {
|
|
75
80
|
version: '3.0',
|
|
76
81
|
templates: {},
|
|
@@ -84,7 +89,7 @@ export function loadConfig(): PtConfig {
|
|
|
84
89
|
}
|
|
85
90
|
|
|
86
91
|
try {
|
|
87
|
-
const content = fs.readFileSync(
|
|
92
|
+
const content = fs.readFileSync(getConfigPath(), 'utf-8');
|
|
88
93
|
if (!content.trim()) {
|
|
89
94
|
throw new Error("Config file is empty");
|
|
90
95
|
}
|
|
@@ -142,7 +147,7 @@ export function loadConfig(): PtConfig {
|
|
|
142
147
|
const error = err as Error;
|
|
143
148
|
console.error(chalk.red(`\nError loading config: ${error.message}`));
|
|
144
149
|
// If we have a backup, maybe suggest using it
|
|
145
|
-
const backupPath =
|
|
150
|
+
const backupPath = getConfigPath() + '.bak';
|
|
146
151
|
if (fs.existsSync(backupPath)) {
|
|
147
152
|
console.error(chalk.yellow(`A backup exists at ${backupPath}. You may want to restore it.`));
|
|
148
153
|
}
|
|
@@ -175,20 +180,20 @@ export function saveConfig(config: PtConfig) {
|
|
|
175
180
|
}
|
|
176
181
|
|
|
177
182
|
const content = YAML.stringify(config);
|
|
178
|
-
const tempPath =
|
|
179
|
-
const backupPath =
|
|
183
|
+
const tempPath = getConfigPath() + '.tmp';
|
|
184
|
+
const backupPath = getConfigPath() + '.bak';
|
|
180
185
|
|
|
181
186
|
try {
|
|
182
187
|
// 1. Create a backup of the current valid config if it exists
|
|
183
|
-
if (fs.existsSync(
|
|
184
|
-
fs.copyFileSync(
|
|
188
|
+
if (fs.existsSync(getConfigPath())) {
|
|
189
|
+
fs.copyFileSync(getConfigPath(), backupPath);
|
|
185
190
|
}
|
|
186
191
|
|
|
187
192
|
// 2. Write to a temporary file first (atomic save)
|
|
188
193
|
fs.writeFileSync(tempPath, content);
|
|
189
194
|
|
|
190
195
|
// 3. Rename temp file to actual config path
|
|
191
|
-
fs.renameSync(tempPath,
|
|
196
|
+
fs.renameSync(tempPath, getConfigPath());
|
|
192
197
|
} catch (err) {
|
|
193
198
|
const error = err as Error;
|
|
194
199
|
console.error(chalk.red(`\nFailed to save config: ${error.message}`));
|
|
@@ -16,7 +16,7 @@ import {
|
|
|
16
16
|
shouldExcludeFile,
|
|
17
17
|
sanitizePath,
|
|
18
18
|
DEFAULT_EXCLUDES,
|
|
19
|
-
|
|
19
|
+
getHomeDir,
|
|
20
20
|
PtConfig,
|
|
21
21
|
} from '../src/config.js';
|
|
22
22
|
|
|
@@ -31,30 +31,30 @@ after(() => {
|
|
|
31
31
|
|
|
32
32
|
test('ensureConfigDir creates dir when it does not exist', () => {
|
|
33
33
|
// Make sure the dir does NOT exist before the test
|
|
34
|
-
if (fs.existsSync(
|
|
35
|
-
fs.rmSync(
|
|
34
|
+
if (fs.existsSync(getHomeDir())) {
|
|
35
|
+
fs.rmSync(getHomeDir(), { recursive: true, force: true });
|
|
36
36
|
}
|
|
37
|
-
assert.ok(!fs.existsSync(
|
|
37
|
+
assert.ok(!fs.existsSync(getHomeDir()), 'Precondition: getHomeDir() should not exist');
|
|
38
38
|
|
|
39
39
|
ensureConfigDir();
|
|
40
40
|
|
|
41
|
-
assert.ok(fs.existsSync(
|
|
42
|
-
assert.ok(fs.statSync(
|
|
41
|
+
assert.ok(fs.existsSync(getHomeDir()), 'getHomeDir() should be created');
|
|
42
|
+
assert.ok(fs.statSync(getHomeDir()).isDirectory(), 'getHomeDir() should be a directory');
|
|
43
43
|
});
|
|
44
44
|
|
|
45
45
|
test('ensureConfigDir does nothing when dir already exists', () => {
|
|
46
46
|
// Ensure directory exists first
|
|
47
|
-
if (!fs.existsSync(
|
|
48
|
-
fs.mkdirSync(
|
|
47
|
+
if (!fs.existsSync(getHomeDir())) {
|
|
48
|
+
fs.mkdirSync(getHomeDir(), { recursive: true });
|
|
49
49
|
}
|
|
50
50
|
// Place a marker file inside to prove the dir is not recreated
|
|
51
|
-
const markerPath = path.join(
|
|
51
|
+
const markerPath = path.join(getHomeDir(), '.marker');
|
|
52
52
|
fs.writeFileSync(markerPath, 'exists');
|
|
53
53
|
|
|
54
54
|
ensureConfigDir();
|
|
55
55
|
|
|
56
|
-
assert.ok(fs.existsSync(
|
|
57
|
-
assert.ok(fs.existsSync(markerPath), 'Marker file inside
|
|
56
|
+
assert.ok(fs.existsSync(getHomeDir()), 'getHomeDir() should still exist');
|
|
57
|
+
assert.ok(fs.existsSync(markerPath), 'Marker file inside getHomeDir() should still exist');
|
|
58
58
|
|
|
59
59
|
// Clean up marker
|
|
60
60
|
fs.unlinkSync(markerPath);
|
package/tests/config.test.ts
CHANGED
|
@@ -1,13 +1,15 @@
|
|
|
1
|
-
import { test,
|
|
1
|
+
import { test, after } from 'node:test';
|
|
2
2
|
import assert from 'node:assert';
|
|
3
3
|
import fs from 'fs';
|
|
4
4
|
import path from 'path';
|
|
5
5
|
|
|
6
6
|
// Force a temporary home directory for testing before importing anything from the CLI
|
|
7
7
|
const testHome = path.join(process.cwd(), '.test-home-config');
|
|
8
|
+
|
|
9
|
+
// Set HOME environment variable before importing (needed for dynamic getters)
|
|
8
10
|
process.env.HOME = testHome;
|
|
9
11
|
|
|
10
|
-
import { normalizeVariable, saveConfig, loadConfig, PtConfig,
|
|
12
|
+
import { normalizeVariable, saveConfig, loadConfig, PtConfig, getConfigPath, getHomeDir } from '../src/config.js';
|
|
11
13
|
|
|
12
14
|
test('normalizeVariable key ordering', () => {
|
|
13
15
|
const variableInput = {
|
|
@@ -28,15 +30,15 @@ test('normalizeVariable key ordering', () => {
|
|
|
28
30
|
test('saveConfig and loadConfig roundtrip', () => {
|
|
29
31
|
// Save the current config to restore later
|
|
30
32
|
let savedConfig: PtConfig | null = null;
|
|
31
|
-
if (fs.existsSync(
|
|
33
|
+
if (fs.existsSync(getConfigPath())) {
|
|
32
34
|
savedConfig = loadConfig();
|
|
33
35
|
}
|
|
34
36
|
|
|
35
37
|
// Clean up any existing test files
|
|
36
|
-
if (fs.existsSync(
|
|
37
|
-
fs.unlinkSync(
|
|
38
|
+
if (fs.existsSync(getConfigPath())) {
|
|
39
|
+
fs.unlinkSync(getConfigPath());
|
|
38
40
|
}
|
|
39
|
-
const backupPath =
|
|
41
|
+
const backupPath = getConfigPath() + '.bak';
|
|
40
42
|
if (fs.existsSync(backupPath)) {
|
|
41
43
|
fs.unlinkSync(backupPath);
|
|
42
44
|
}
|
|
@@ -64,11 +66,11 @@ test('saveConfig and loadConfig roundtrip', () => {
|
|
|
64
66
|
// Save the config
|
|
65
67
|
saveConfig(testConfig);
|
|
66
68
|
|
|
67
|
-
// Assert
|
|
68
|
-
assert.ok(fs.existsSync(
|
|
69
|
+
// Assert getConfigPath() exists and has been created
|
|
70
|
+
assert.ok(fs.existsSync(getConfigPath()), 'Config file should be created');
|
|
69
71
|
|
|
70
72
|
// Verify key order in YAML string
|
|
71
|
-
const yamlContent = fs.readFileSync(
|
|
73
|
+
const yamlContent = fs.readFileSync(getConfigPath(), 'utf-8');
|
|
72
74
|
assert.ok(yamlContent.includes('name: myVar'), 'Should contain myVar variable');
|
|
73
75
|
|
|
74
76
|
// Load the config back
|
|
@@ -92,8 +94,8 @@ test('saveConfig and loadConfig roundtrip', () => {
|
|
|
92
94
|
assert.ok(backupContent.includes("version: 1.0.0") || backupContent.includes("version: '1.0.0'"), 'Backup should contain previous version');
|
|
93
95
|
|
|
94
96
|
// Clean up
|
|
95
|
-
if (fs.existsSync(
|
|
96
|
-
fs.unlinkSync(
|
|
97
|
+
if (fs.existsSync(getConfigPath())) {
|
|
98
|
+
fs.unlinkSync(getConfigPath());
|
|
97
99
|
}
|
|
98
100
|
if (fs.existsSync(backupPath)) {
|
|
99
101
|
fs.unlinkSync(backupPath);
|
|
@@ -105,7 +107,7 @@ test('saveConfig and loadConfig roundtrip', () => {
|
|
|
105
107
|
}
|
|
106
108
|
|
|
107
109
|
if (fs.existsSync(testHome)) {
|
|
108
|
-
fs.
|
|
110
|
+
fs.rmSync(testHome, { recursive: true, force: true });
|
|
109
111
|
}
|
|
110
112
|
});
|
|
111
113
|
|
|
@@ -144,15 +146,15 @@ test('normalizeVariable with all fields', () => {
|
|
|
144
146
|
test('saveConfig creates backup on overwrite', () => {
|
|
145
147
|
// Save the current config to restore later
|
|
146
148
|
let savedConfig: PtConfig | null = null;
|
|
147
|
-
if (fs.existsSync(
|
|
149
|
+
if (fs.existsSync(getConfigPath())) {
|
|
148
150
|
savedConfig = loadConfig();
|
|
149
151
|
}
|
|
150
152
|
|
|
151
153
|
// Clean up any existing test files
|
|
152
|
-
if (fs.existsSync(
|
|
153
|
-
fs.unlinkSync(
|
|
154
|
+
if (fs.existsSync(getConfigPath())) {
|
|
155
|
+
fs.unlinkSync(getConfigPath());
|
|
154
156
|
}
|
|
155
|
-
const backupPath =
|
|
157
|
+
const backupPath = getConfigPath() + '.bak';
|
|
156
158
|
if (fs.existsSync(backupPath)) {
|
|
157
159
|
fs.unlinkSync(backupPath);
|
|
158
160
|
}
|
|
@@ -168,7 +170,7 @@ test('saveConfig creates backup on overwrite', () => {
|
|
|
168
170
|
|
|
169
171
|
// Save first config
|
|
170
172
|
saveConfig(config1);
|
|
171
|
-
assert.ok(fs.existsSync(
|
|
173
|
+
assert.ok(fs.existsSync(getConfigPath()), 'Config file should be created');
|
|
172
174
|
|
|
173
175
|
// Save second config (should create backup)
|
|
174
176
|
saveConfig(config2);
|
|
@@ -179,8 +181,8 @@ test('saveConfig creates backup on overwrite', () => {
|
|
|
179
181
|
assert.ok(backupContent.includes("version: 1.0.0") || backupContent.includes("version: '1.0.0'"), 'Backup should contain previous version');
|
|
180
182
|
|
|
181
183
|
// Clean up
|
|
182
|
-
if (fs.existsSync(
|
|
183
|
-
fs.unlinkSync(
|
|
184
|
+
if (fs.existsSync(getConfigPath())) {
|
|
185
|
+
fs.unlinkSync(getConfigPath());
|
|
184
186
|
}
|
|
185
187
|
if (fs.existsSync(backupPath)) {
|
|
186
188
|
fs.unlinkSync(backupPath);
|
|
@@ -192,22 +194,22 @@ test('saveConfig creates backup on overwrite', () => {
|
|
|
192
194
|
}
|
|
193
195
|
|
|
194
196
|
if (fs.existsSync(testHome)) {
|
|
195
|
-
fs.
|
|
197
|
+
fs.rmSync(testHome, { recursive: true, force: true });
|
|
196
198
|
}
|
|
197
199
|
});
|
|
198
200
|
|
|
199
201
|
test('saveConfig atomic write behavior', () => {
|
|
200
202
|
// Save the current config to restore later
|
|
201
203
|
let savedConfig: PtConfig | null = null;
|
|
202
|
-
if (fs.existsSync(
|
|
204
|
+
if (fs.existsSync(getConfigPath())) {
|
|
203
205
|
savedConfig = loadConfig();
|
|
204
206
|
}
|
|
205
207
|
|
|
206
208
|
// Clean up any existing test files
|
|
207
|
-
if (fs.existsSync(
|
|
208
|
-
fs.unlinkSync(
|
|
209
|
+
if (fs.existsSync(getConfigPath())) {
|
|
210
|
+
fs.unlinkSync(getConfigPath());
|
|
209
211
|
}
|
|
210
|
-
const backupPath =
|
|
212
|
+
const backupPath = getConfigPath() + '.bak';
|
|
211
213
|
if (fs.existsSync(backupPath)) {
|
|
212
214
|
fs.unlinkSync(backupPath);
|
|
213
215
|
}
|
|
@@ -221,15 +223,15 @@ test('saveConfig atomic write behavior', () => {
|
|
|
221
223
|
saveConfig(config);
|
|
222
224
|
|
|
223
225
|
// Verify config exists
|
|
224
|
-
assert.ok(fs.existsSync(
|
|
226
|
+
assert.ok(fs.existsSync(getConfigPath()), 'Config file should be created');
|
|
225
227
|
|
|
226
228
|
// Verify no temp file remains
|
|
227
|
-
const tempPath =
|
|
229
|
+
const tempPath = getConfigPath() + '.tmp';
|
|
228
230
|
assert.ok(!fs.existsSync(tempPath), 'Temp file should not remain');
|
|
229
231
|
|
|
230
232
|
// Clean up
|
|
231
|
-
if (fs.existsSync(
|
|
232
|
-
fs.unlinkSync(
|
|
233
|
+
if (fs.existsSync(getConfigPath())) {
|
|
234
|
+
fs.unlinkSync(getConfigPath());
|
|
233
235
|
}
|
|
234
236
|
if (fs.existsSync(backupPath)) {
|
|
235
237
|
fs.unlinkSync(backupPath);
|
|
@@ -241,22 +243,22 @@ test('saveConfig atomic write behavior', () => {
|
|
|
241
243
|
}
|
|
242
244
|
|
|
243
245
|
if (fs.existsSync(testHome)) {
|
|
244
|
-
fs.
|
|
246
|
+
fs.rmSync(testHome, { recursive: true, force: true });
|
|
245
247
|
}
|
|
246
248
|
});
|
|
247
249
|
|
|
248
250
|
test('loadConfig returns default config when file missing', () => {
|
|
249
251
|
// Save the current config to restore later
|
|
250
252
|
let savedConfig: PtConfig | null = null;
|
|
251
|
-
if (fs.existsSync(
|
|
253
|
+
if (fs.existsSync(getConfigPath())) {
|
|
252
254
|
savedConfig = loadConfig();
|
|
253
255
|
}
|
|
254
256
|
|
|
255
257
|
// Clean up any existing test files
|
|
256
|
-
if (fs.existsSync(
|
|
257
|
-
fs.unlinkSync(
|
|
258
|
+
if (fs.existsSync(getConfigPath())) {
|
|
259
|
+
fs.unlinkSync(getConfigPath());
|
|
258
260
|
}
|
|
259
|
-
const backupPath =
|
|
261
|
+
const backupPath = getConfigPath() + '.bak';
|
|
260
262
|
if (fs.existsSync(backupPath)) {
|
|
261
263
|
fs.unlinkSync(backupPath);
|
|
262
264
|
}
|
|
@@ -271,7 +273,7 @@ test('loadConfig returns default config when file missing', () => {
|
|
|
271
273
|
|
|
272
274
|
// Clean up
|
|
273
275
|
if (fs.existsSync(testHome)) {
|
|
274
|
-
fs.
|
|
276
|
+
fs.rmSync(testHome, { recursive: true, force: true });
|
|
275
277
|
}
|
|
276
278
|
|
|
277
279
|
// Restore the original config if it existed
|
|
@@ -283,15 +285,15 @@ test('loadConfig returns default config when file missing', () => {
|
|
|
283
285
|
test('saveConfig with variables normalizes key order', () => {
|
|
284
286
|
// Save the current config to restore later
|
|
285
287
|
let savedConfig: PtConfig | null = null;
|
|
286
|
-
if (fs.existsSync(
|
|
288
|
+
if (fs.existsSync(getConfigPath())) {
|
|
287
289
|
savedConfig = loadConfig();
|
|
288
290
|
}
|
|
289
291
|
|
|
290
292
|
// Clean up any existing test files
|
|
291
|
-
if (fs.existsSync(
|
|
292
|
-
fs.unlinkSync(
|
|
293
|
+
if (fs.existsSync(getConfigPath())) {
|
|
294
|
+
fs.unlinkSync(getConfigPath());
|
|
293
295
|
}
|
|
294
|
-
const backupPath =
|
|
296
|
+
const backupPath = getConfigPath() + '.bak';
|
|
295
297
|
if (fs.existsSync(backupPath)) {
|
|
296
298
|
fs.unlinkSync(backupPath);
|
|
297
299
|
}
|
|
@@ -332,8 +334,8 @@ test('saveConfig with variables normalizes key order', () => {
|
|
|
332
334
|
assert.strictEqual(vars[1].name, 'var2', 'Second variable name should be var2');
|
|
333
335
|
|
|
334
336
|
// Clean up
|
|
335
|
-
if (fs.existsSync(
|
|
336
|
-
fs.unlinkSync(
|
|
337
|
+
if (fs.existsSync(getConfigPath())) {
|
|
338
|
+
fs.unlinkSync(getConfigPath());
|
|
337
339
|
}
|
|
338
340
|
if (fs.existsSync(backupPath)) {
|
|
339
341
|
fs.unlinkSync(backupPath);
|
|
@@ -345,28 +347,33 @@ test('saveConfig with variables normalizes key order', () => {
|
|
|
345
347
|
}
|
|
346
348
|
|
|
347
349
|
if (fs.existsSync(testHome)) {
|
|
348
|
-
fs.
|
|
350
|
+
fs.rmSync(testHome, { recursive: true, force: true });
|
|
349
351
|
}
|
|
350
352
|
});
|
|
351
353
|
|
|
352
354
|
test('saveConfig with empty config file', () => {
|
|
353
355
|
// Save the current config to restore later
|
|
354
356
|
let savedConfig: PtConfig | null = null;
|
|
355
|
-
if (fs.existsSync(
|
|
357
|
+
if (fs.existsSync(getConfigPath())) {
|
|
356
358
|
savedConfig = loadConfig();
|
|
357
359
|
}
|
|
358
360
|
|
|
359
361
|
// Clean up any existing test files
|
|
360
|
-
if (fs.existsSync(
|
|
361
|
-
fs.unlinkSync(
|
|
362
|
+
if (fs.existsSync(getConfigPath())) {
|
|
363
|
+
fs.unlinkSync(getConfigPath());
|
|
362
364
|
}
|
|
363
|
-
const backupPath =
|
|
365
|
+
const backupPath = getConfigPath() + '.bak';
|
|
364
366
|
if (fs.existsSync(backupPath)) {
|
|
365
367
|
fs.unlinkSync(backupPath);
|
|
366
368
|
}
|
|
367
369
|
|
|
370
|
+
// Ensure the config directory exists
|
|
371
|
+
if (!fs.existsSync(getHomeDir())) {
|
|
372
|
+
fs.mkdirSync(getHomeDir(), { recursive: true });
|
|
373
|
+
}
|
|
374
|
+
|
|
368
375
|
// Create empty config file
|
|
369
|
-
fs.writeFileSync(
|
|
376
|
+
fs.writeFileSync(getConfigPath(), '');
|
|
370
377
|
|
|
371
378
|
// loadConfig calls process.exit(1) on error, so we need to mock it
|
|
372
379
|
// to prevent the test runner from dying
|
|
@@ -390,8 +397,8 @@ test('saveConfig with empty config file', () => {
|
|
|
390
397
|
}
|
|
391
398
|
|
|
392
399
|
// Clean up
|
|
393
|
-
if (fs.existsSync(
|
|
394
|
-
fs.unlinkSync(
|
|
400
|
+
if (fs.existsSync(getConfigPath())) {
|
|
401
|
+
fs.unlinkSync(getConfigPath());
|
|
395
402
|
}
|
|
396
403
|
if (fs.existsSync(backupPath)) {
|
|
397
404
|
fs.unlinkSync(backupPath);
|
|
@@ -403,7 +410,7 @@ test('saveConfig with empty config file', () => {
|
|
|
403
410
|
}
|
|
404
411
|
|
|
405
412
|
if (fs.existsSync(testHome)) {
|
|
406
|
-
fs.
|
|
413
|
+
fs.rmSync(testHome, { recursive: true, force: true });
|
|
407
414
|
}
|
|
408
415
|
});
|
|
409
416
|
|
|
@@ -425,15 +432,15 @@ test('normalizeVariable preserves field order', () => {
|
|
|
425
432
|
test('saveConfig handles multiple templates', () => {
|
|
426
433
|
// Save the current config to restore later
|
|
427
434
|
let savedConfig: PtConfig | null = null;
|
|
428
|
-
if (fs.existsSync(
|
|
435
|
+
if (fs.existsSync(getConfigPath())) {
|
|
429
436
|
savedConfig = loadConfig();
|
|
430
437
|
}
|
|
431
438
|
|
|
432
439
|
// Clean up any existing test files
|
|
433
|
-
if (fs.existsSync(
|
|
434
|
-
fs.unlinkSync(
|
|
440
|
+
if (fs.existsSync(getConfigPath())) {
|
|
441
|
+
fs.unlinkSync(getConfigPath());
|
|
435
442
|
}
|
|
436
|
-
const backupPath =
|
|
443
|
+
const backupPath = getConfigPath() + '.bak';
|
|
437
444
|
if (fs.existsSync(backupPath)) {
|
|
438
445
|
fs.unlinkSync(backupPath);
|
|
439
446
|
}
|
|
@@ -462,8 +469,8 @@ test('saveConfig handles multiple templates', () => {
|
|
|
462
469
|
assert.strictEqual(loaded.templates['template2'].description, 'Template 2');
|
|
463
470
|
|
|
464
471
|
// Clean up
|
|
465
|
-
if (fs.existsSync(
|
|
466
|
-
fs.unlinkSync(
|
|
472
|
+
if (fs.existsSync(getConfigPath())) {
|
|
473
|
+
fs.unlinkSync(getConfigPath());
|
|
467
474
|
}
|
|
468
475
|
if (fs.existsSync(backupPath)) {
|
|
469
476
|
fs.unlinkSync(backupPath);
|
|
@@ -475,22 +482,22 @@ test('saveConfig handles multiple templates', () => {
|
|
|
475
482
|
}
|
|
476
483
|
|
|
477
484
|
if (fs.existsSync(testHome)) {
|
|
478
|
-
fs.
|
|
485
|
+
fs.rmSync(testHome, { recursive: true, force: true });
|
|
479
486
|
}
|
|
480
487
|
});
|
|
481
488
|
|
|
482
489
|
test('saveConfig handles templates with variables', () => {
|
|
483
490
|
// Save the current config to restore later
|
|
484
491
|
let savedConfig: PtConfig | null = null;
|
|
485
|
-
if (fs.existsSync(
|
|
492
|
+
if (fs.existsSync(getConfigPath())) {
|
|
486
493
|
savedConfig = loadConfig();
|
|
487
494
|
}
|
|
488
495
|
|
|
489
496
|
// Clean up any existing test files
|
|
490
|
-
if (fs.existsSync(
|
|
491
|
-
fs.unlinkSync(
|
|
497
|
+
if (fs.existsSync(getConfigPath())) {
|
|
498
|
+
fs.unlinkSync(getConfigPath());
|
|
492
499
|
}
|
|
493
|
-
const backupPath =
|
|
500
|
+
const backupPath = getConfigPath() + '.bak';
|
|
494
501
|
if (fs.existsSync(backupPath)) {
|
|
495
502
|
fs.unlinkSync(backupPath);
|
|
496
503
|
}
|
|
@@ -528,8 +535,8 @@ test('saveConfig handles templates with variables', () => {
|
|
|
528
535
|
assert.strictEqual(template.variables?.[0].required, true, 'Variable should be required');
|
|
529
536
|
|
|
530
537
|
// Clean up
|
|
531
|
-
if (fs.existsSync(
|
|
532
|
-
fs.unlinkSync(
|
|
538
|
+
if (fs.existsSync(getConfigPath())) {
|
|
539
|
+
fs.unlinkSync(getConfigPath());
|
|
533
540
|
}
|
|
534
541
|
if (fs.existsSync(backupPath)) {
|
|
535
542
|
fs.unlinkSync(backupPath);
|
|
@@ -541,26 +548,31 @@ test('saveConfig handles templates with variables', () => {
|
|
|
541
548
|
}
|
|
542
549
|
|
|
543
550
|
if (fs.existsSync(testHome)) {
|
|
544
|
-
fs.
|
|
551
|
+
fs.rmSync(testHome, { recursive: true, force: true });
|
|
545
552
|
}
|
|
546
553
|
});
|
|
547
554
|
|
|
548
555
|
test('loadConfig handles migration from v2.0', () => {
|
|
549
556
|
// Save the current config to restore later
|
|
550
557
|
let savedConfig: PtConfig | null = null;
|
|
551
|
-
if (fs.existsSync(
|
|
558
|
+
if (fs.existsSync(getConfigPath())) {
|
|
552
559
|
savedConfig = loadConfig();
|
|
553
560
|
}
|
|
554
561
|
|
|
555
562
|
// Clean up any existing test files
|
|
556
|
-
if (fs.existsSync(
|
|
557
|
-
fs.unlinkSync(
|
|
563
|
+
if (fs.existsSync(getConfigPath())) {
|
|
564
|
+
fs.unlinkSync(getConfigPath());
|
|
558
565
|
}
|
|
559
|
-
const backupPath =
|
|
566
|
+
const backupPath = getConfigPath() + '.bak';
|
|
560
567
|
if (fs.existsSync(backupPath)) {
|
|
561
568
|
fs.unlinkSync(backupPath);
|
|
562
569
|
}
|
|
563
570
|
|
|
571
|
+
// Ensure the config directory exists
|
|
572
|
+
if (!fs.existsSync(getHomeDir())) {
|
|
573
|
+
fs.mkdirSync(getHomeDir(), { recursive: true });
|
|
574
|
+
}
|
|
575
|
+
|
|
564
576
|
// Create a v2.0 config
|
|
565
577
|
const v2Config = {
|
|
566
578
|
version: '2.0',
|
|
@@ -573,7 +585,7 @@ test('loadConfig handles migration from v2.0', () => {
|
|
|
573
585
|
}
|
|
574
586
|
};
|
|
575
587
|
|
|
576
|
-
fs.writeFileSync(
|
|
588
|
+
fs.writeFileSync(getConfigPath(), JSON.stringify(v2Config));
|
|
577
589
|
|
|
578
590
|
// Load config - should migrate to v3.0
|
|
579
591
|
const config = loadConfig();
|
|
@@ -584,8 +596,8 @@ test('loadConfig handles migration from v2.0', () => {
|
|
|
584
596
|
assert.ok(!config.templates['test-template'].type, 'Type should be removed');
|
|
585
597
|
|
|
586
598
|
// Clean up
|
|
587
|
-
if (fs.existsSync(
|
|
588
|
-
fs.unlinkSync(
|
|
599
|
+
if (fs.existsSync(getConfigPath())) {
|
|
600
|
+
fs.unlinkSync(getConfigPath());
|
|
589
601
|
}
|
|
590
602
|
if (fs.existsSync(backupPath)) {
|
|
591
603
|
fs.unlinkSync(backupPath);
|
|
@@ -597,26 +609,31 @@ test('loadConfig handles migration from v2.0', () => {
|
|
|
597
609
|
}
|
|
598
610
|
|
|
599
611
|
if (fs.existsSync(testHome)) {
|
|
600
|
-
fs.
|
|
612
|
+
fs.rmSync(testHome, { recursive: true, force: true });
|
|
601
613
|
}
|
|
602
614
|
});
|
|
603
615
|
|
|
604
616
|
test('loadConfig handles migration from v2.0 with global_post_config', () => {
|
|
605
617
|
// Save the current config to restore later
|
|
606
618
|
let savedConfig: PtConfig | null = null;
|
|
607
|
-
if (fs.existsSync(
|
|
619
|
+
if (fs.existsSync(getConfigPath())) {
|
|
608
620
|
savedConfig = loadConfig();
|
|
609
621
|
}
|
|
610
622
|
|
|
611
623
|
// Clean up any existing test files
|
|
612
|
-
if (fs.existsSync(
|
|
613
|
-
fs.unlinkSync(
|
|
624
|
+
if (fs.existsSync(getConfigPath())) {
|
|
625
|
+
fs.unlinkSync(getConfigPath());
|
|
614
626
|
}
|
|
615
|
-
const backupPath =
|
|
627
|
+
const backupPath = getConfigPath() + '.bak';
|
|
616
628
|
if (fs.existsSync(backupPath)) {
|
|
617
629
|
fs.unlinkSync(backupPath);
|
|
618
630
|
}
|
|
619
631
|
|
|
632
|
+
// Ensure the config directory exists
|
|
633
|
+
if (!fs.existsSync(getHomeDir())) {
|
|
634
|
+
fs.mkdirSync(getHomeDir(), { recursive: true });
|
|
635
|
+
}
|
|
636
|
+
|
|
620
637
|
// Create a v2.0 config with global_post_config
|
|
621
638
|
const v2Config = {
|
|
622
639
|
version: '2.0',
|
|
@@ -629,7 +646,7 @@ test('loadConfig handles migration from v2.0 with global_post_config', () => {
|
|
|
629
646
|
]
|
|
630
647
|
};
|
|
631
648
|
|
|
632
|
-
fs.writeFileSync(
|
|
649
|
+
fs.writeFileSync(getConfigPath(), JSON.stringify(v2Config));
|
|
633
650
|
|
|
634
651
|
// Load config - should migrate to v3.0
|
|
635
652
|
const config = loadConfig();
|
|
@@ -645,8 +662,8 @@ test('loadConfig handles migration from v2.0 with global_post_config', () => {
|
|
|
645
662
|
assert.ok(!config.global_post_config, 'global_post_config should be removed');
|
|
646
663
|
|
|
647
664
|
// Clean up
|
|
648
|
-
if (fs.existsSync(
|
|
649
|
-
fs.unlinkSync(
|
|
665
|
+
if (fs.existsSync(getConfigPath())) {
|
|
666
|
+
fs.unlinkSync(getConfigPath());
|
|
650
667
|
}
|
|
651
668
|
if (fs.existsSync(backupPath)) {
|
|
652
669
|
fs.unlinkSync(backupPath);
|
|
@@ -658,26 +675,31 @@ test('loadConfig handles migration from v2.0 with global_post_config', () => {
|
|
|
658
675
|
}
|
|
659
676
|
|
|
660
677
|
if (fs.existsSync(testHome)) {
|
|
661
|
-
fs.
|
|
678
|
+
fs.rmSync(testHome, { recursive: true, force: true });
|
|
662
679
|
}
|
|
663
680
|
});
|
|
664
681
|
|
|
665
682
|
test('loadConfig handles migration from v2.0 with variables', () => {
|
|
666
683
|
// Save the current config to restore later
|
|
667
684
|
let savedConfig: PtConfig | null = null;
|
|
668
|
-
if (fs.existsSync(
|
|
685
|
+
if (fs.existsSync(getConfigPath())) {
|
|
669
686
|
savedConfig = loadConfig();
|
|
670
687
|
}
|
|
671
688
|
|
|
672
689
|
// Clean up any existing test files
|
|
673
|
-
if (fs.existsSync(
|
|
674
|
-
fs.unlinkSync(
|
|
690
|
+
if (fs.existsSync(getConfigPath())) {
|
|
691
|
+
fs.unlinkSync(getConfigPath());
|
|
675
692
|
}
|
|
676
|
-
const backupPath =
|
|
693
|
+
const backupPath = getConfigPath() + '.bak';
|
|
677
694
|
if (fs.existsSync(backupPath)) {
|
|
678
695
|
fs.unlinkSync(backupPath);
|
|
679
696
|
}
|
|
680
697
|
|
|
698
|
+
// Ensure the config directory exists
|
|
699
|
+
if (!fs.existsSync(getHomeDir())) {
|
|
700
|
+
fs.mkdirSync(getHomeDir(), { recursive: true });
|
|
701
|
+
}
|
|
702
|
+
|
|
681
703
|
// Create a v2.0 config with variables as Record<string, string>
|
|
682
704
|
const v2Config = {
|
|
683
705
|
version: '2.0',
|
|
@@ -688,7 +710,7 @@ test('loadConfig handles migration from v2.0 with variables', () => {
|
|
|
688
710
|
}
|
|
689
711
|
};
|
|
690
712
|
|
|
691
|
-
fs.writeFileSync(
|
|
713
|
+
fs.writeFileSync(getConfigPath(), JSON.stringify(v2Config));
|
|
692
714
|
|
|
693
715
|
// Load config - should migrate to v3.0
|
|
694
716
|
const config = loadConfig();
|
|
@@ -702,8 +724,8 @@ test('loadConfig handles migration from v2.0 with variables', () => {
|
|
|
702
724
|
assert.strictEqual(config.variables[1].default, 'web', 'Second variable default should be web');
|
|
703
725
|
|
|
704
726
|
// Clean up
|
|
705
|
-
if (fs.existsSync(
|
|
706
|
-
fs.unlinkSync(
|
|
727
|
+
if (fs.existsSync(getConfigPath())) {
|
|
728
|
+
fs.unlinkSync(getConfigPath());
|
|
707
729
|
}
|
|
708
730
|
if (fs.existsSync(backupPath)) {
|
|
709
731
|
fs.unlinkSync(backupPath);
|
|
@@ -715,7 +737,7 @@ test('loadConfig handles migration from v2.0 with variables', () => {
|
|
|
715
737
|
}
|
|
716
738
|
|
|
717
739
|
if (fs.existsSync(testHome)) {
|
|
718
|
-
fs.
|
|
740
|
+
fs.rmSync(testHome, { recursive: true, force: true });
|
|
719
741
|
}
|
|
720
742
|
});
|
|
721
743
|
|
|
@@ -733,7 +755,7 @@ test('loadConfig handles existing config file', () => {
|
|
|
733
755
|
test('saveConfig preserves existing config when not deleting', () => {
|
|
734
756
|
// Save the current config to restore later
|
|
735
757
|
let savedConfig: PtConfig | null = null;
|
|
736
|
-
if (fs.existsSync(
|
|
758
|
+
if (fs.existsSync(getConfigPath())) {
|
|
737
759
|
savedConfig = loadConfig();
|
|
738
760
|
}
|
|
739
761
|
|
|
@@ -760,4 +782,12 @@ test('saveConfig preserves existing config when not deleting', () => {
|
|
|
760
782
|
if (savedConfig) {
|
|
761
783
|
saveConfig(savedConfig);
|
|
762
784
|
}
|
|
785
|
+
});
|
|
786
|
+
|
|
787
|
+
// Cleanup: restore original os.homedir function and clean up test directory
|
|
788
|
+
after(() => {
|
|
789
|
+
// Clean up test home directory if it exists
|
|
790
|
+
if (fs.existsSync(testHome)) {
|
|
791
|
+
fs.rmSync(testHome, { recursive: true, force: true });
|
|
792
|
+
}
|
|
763
793
|
});
|
package/tests/init.test.ts
CHANGED
|
@@ -8,7 +8,7 @@ const testHome = path.join(process.cwd(), '.test-home-init');
|
|
|
8
8
|
process.env.HOME = testHome;
|
|
9
9
|
|
|
10
10
|
import { init } from '../src/commands/initCommand.js';
|
|
11
|
-
import { loadConfig, saveConfig, PtConfig,
|
|
11
|
+
import { loadConfig, saveConfig, PtConfig, getConfigPath } from '../src/config.js';
|
|
12
12
|
|
|
13
13
|
// Helper to clean up test directories
|
|
14
14
|
function cleanup(...paths: string[]) {
|
package/tests/learn.test.ts
CHANGED
|
@@ -9,7 +9,7 @@ const testHome = path.join(process.cwd(), '.test-home-learn');
|
|
|
9
9
|
process.env.HOME = testHome;
|
|
10
10
|
|
|
11
11
|
import { learn } from '../src/commands/learnCommand.js';
|
|
12
|
-
import { loadConfig, saveConfig, PtConfig,
|
|
12
|
+
import { loadConfig, saveConfig, PtConfig, getConfigPath } from '../src/config.js';
|
|
13
13
|
|
|
14
14
|
// ---------------------------------------------------------------------------
|
|
15
15
|
// Helpers
|
|
@@ -31,8 +31,8 @@ function rmrf(p: string) {
|
|
|
31
31
|
|
|
32
32
|
/** Ensure a clean test config state (no leftover config files). */
|
|
33
33
|
function cleanConfig() {
|
|
34
|
-
if (fs.existsSync(
|
|
35
|
-
const bak =
|
|
34
|
+
if (fs.existsSync(getConfigPath())) fs.unlinkSync(getConfigPath());
|
|
35
|
+
const bak = getConfigPath() + '.bak';
|
|
36
36
|
if (fs.existsSync(bak)) fs.unlinkSync(bak);
|
|
37
37
|
}
|
|
38
38
|
|