jdm-plugin-template 1.0.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 +178 -0
- package/bin/guard.js +37 -0
- package/lib/commands/build.js +109 -0
- package/lib/commands/clean.js +74 -0
- package/lib/commands/create.js +228 -0
- package/lib/commands/dev.js +147 -0
- package/lib/commands/install.js +102 -0
- package/lib/compat.js +43 -0
- package/lib/config.js +150 -0
- package/lib/index.js +107 -0
- package/lib/logger.js +45 -0
- package/package.json +104 -0
package/README.md
ADDED
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
# jdm-plugin-template
|
|
2
|
+
|
|
3
|
+
A starter template for building **jdm-cli** plugins. This repo is also self-bootstrapping — the `create` command clones this very repo into a new project, giving contributors a clean slate to start from.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## What is this?
|
|
8
|
+
|
|
9
|
+
`jdm-plugin-template` is the **official base template** for all jdm-cli plugins. It ships with a working plugin structure, a set of standard commands (`create`, `dev`, `build`, `clean`, `install`), shared logging helpers, and a version compatibility system — so you spend time building your plugin, not scaffolding it.
|
|
10
|
+
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
## Quick Start
|
|
14
|
+
|
|
15
|
+
### Use this template to scaffold a new plugin
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
# Install the CLI (if you haven't already)
|
|
19
|
+
npm install -g jdm-cli
|
|
20
|
+
jdm-cli add plugin-template
|
|
21
|
+
|
|
22
|
+
# Scaffold a new plugin project
|
|
23
|
+
jdm-cli plugin-template create
|
|
24
|
+
|
|
25
|
+
# Then follow the prompts, or use flags:
|
|
26
|
+
jdm-cli plugin-template create --name my-plugin
|
|
27
|
+
jdm-cli plugin-template create --name my-plugin --install
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
This will:
|
|
31
|
+
1. Clone this repo into a folder named `my-plugin` (or `.` for current dir)
|
|
32
|
+
2. Strip `.git` so it's a clean project — not a fork
|
|
33
|
+
3. Optionally run `npm install` for you
|
|
34
|
+
|
|
35
|
+
---
|
|
36
|
+
|
|
37
|
+
## Project Structure
|
|
38
|
+
|
|
39
|
+
```
|
|
40
|
+
jdm-plugin-template/
|
|
41
|
+
├── lib/
|
|
42
|
+
│ ├── index.js # Entry point — namespace, command map, dispatcher
|
|
43
|
+
│ ├── config.js # Config read/write and compatibility guard
|
|
44
|
+
│ ├── compat.js # Plugin version + per-command compatibility ranges
|
|
45
|
+
│ ├── logger.js # Shared logging helpers (ok, fail, warn, info, step, header)
|
|
46
|
+
│ └── commands/
|
|
47
|
+
│ ├── create.js # Scaffold a new project (clones this repo)
|
|
48
|
+
│ ├── dev.js # Start development environment
|
|
49
|
+
│ ├── build.js # Compile / package the project
|
|
50
|
+
│ ├── clean.js # Remove build artifacts
|
|
51
|
+
│ └── install.js # Install dependencies
|
|
52
|
+
├── package.json
|
|
53
|
+
└── README.md
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
---
|
|
57
|
+
|
|
58
|
+
## Built-in Commands
|
|
59
|
+
|
|
60
|
+
| Command | Description | Key Flags |
|
|
61
|
+
|-----------|------------------------------------------|----------------------------------------|
|
|
62
|
+
| `create` | Scaffold a new project from this template | `--name <name>`, `--install` |
|
|
63
|
+
| `dev` | Start development environment | _(wire up your own processes)_ |
|
|
64
|
+
| `build` | Compile / package the project | `--frontend`, `--backend`, `--full` |
|
|
65
|
+
| `clean` | Remove build artifacts | `--dry` (preview without deleting) |
|
|
66
|
+
| `install` | Install dependencies | _(none)_ |
|
|
67
|
+
|
|
68
|
+
---
|
|
69
|
+
|
|
70
|
+
## Contributing / Building Your Own Plugin
|
|
71
|
+
|
|
72
|
+
This repo is the starting point. Here's how to get going:
|
|
73
|
+
|
|
74
|
+
### 1. Scaffold a copy
|
|
75
|
+
|
|
76
|
+
```bash
|
|
77
|
+
jdm-cli plugin-template create --name my-plugin --install
|
|
78
|
+
cd my-plugin
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
Or clone manually:
|
|
82
|
+
|
|
83
|
+
```bash
|
|
84
|
+
git clone https://github.com/JDM-Github/jdm-plugin-template my-plugin
|
|
85
|
+
cd my-plugin
|
|
86
|
+
rm -rf .git
|
|
87
|
+
npm install
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
### 2. Set your namespace
|
|
91
|
+
|
|
92
|
+
In **three places**, replace `"plugin-template"` with your plugin's name:
|
|
93
|
+
|
|
94
|
+
| File | What to change |
|
|
95
|
+
|---|---|
|
|
96
|
+
| `lib/index.js` | `export const namespace = "plugin-template"` |
|
|
97
|
+
| `lib/config.js` | `const PLUGIN_NAME = "plugin-template"` |
|
|
98
|
+
| `lib/logger.js` | `const ns = "plugin-template"` inside `header()` |
|
|
99
|
+
| `package.json` | `"name"`, `"jdmPlugin.namespace"`, `"jdmPlugin.description"` |
|
|
100
|
+
|
|
101
|
+
### 3. Add your commands
|
|
102
|
+
|
|
103
|
+
1. Create `lib/commands/my-command.js` with a default export
|
|
104
|
+
2. Import it in `lib/index.js` and add it to the `commands` map
|
|
105
|
+
3. If it needs interactive prompts (`ask(rl, ...)`), add the name to `INTERACTIVE_COMMANDS`
|
|
106
|
+
4. Add it to `showDesign()` for the help screen
|
|
107
|
+
5. Add it to the `jdmPlugin.commands` array in `package.json`
|
|
108
|
+
|
|
109
|
+
### 4. Wire up `dev` and `build`
|
|
110
|
+
|
|
111
|
+
Both files have clearly marked `// TODO` blocks — drop in your actual build commands, server launchers, or watchers there. Cross-platform terminal helpers (Windows Terminal, CMD, gnome-terminal, osascript) are already included in `dev.js`.
|
|
112
|
+
|
|
113
|
+
### 5. Update compatibility (when needed)
|
|
114
|
+
|
|
115
|
+
When you make breaking changes to the template structure, bump `COMPAT` in `lib/compat.js` so existing projects get a clear error instead of a silent failure:
|
|
116
|
+
|
|
117
|
+
```js
|
|
118
|
+
// lib/compat.js
|
|
119
|
+
export const pluginVersion = "1.1.0";
|
|
120
|
+
|
|
121
|
+
export const COMPAT = {
|
|
122
|
+
global: ">=1.1.0", // bump when ALL commands need a newer project
|
|
123
|
+
commands: {
|
|
124
|
+
build: ">=1.1.0", // or target a specific command
|
|
125
|
+
},
|
|
126
|
+
};
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
---
|
|
130
|
+
|
|
131
|
+
## Logger Helpers
|
|
132
|
+
|
|
133
|
+
All commands share the same logging helpers from `lib/logger.js`:
|
|
134
|
+
|
|
135
|
+
```js
|
|
136
|
+
import { ok, fail, warn, info, step, header, divider } from "../logger.js";
|
|
137
|
+
|
|
138
|
+
ok(chalk, "Thing worked"); // ✔ Thing worked (green)
|
|
139
|
+
fail(chalk, "Thing broke"); // ✖ Thing broke (red)
|
|
140
|
+
warn(chalk, "Watch out"); // ⚠ Watch out (yellow)
|
|
141
|
+
info(chalk, "Just so you know"); // · Just so you know (gray)
|
|
142
|
+
|
|
143
|
+
step(chalk, 1, 3, "Doing X"); // [1/3] Doing X
|
|
144
|
+
header(chalk, "my-command"); // jdm / plugin-template / my-command
|
|
145
|
+
divider(chalk); // ─────────────────────────────────────
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
---
|
|
149
|
+
|
|
150
|
+
## Config System
|
|
151
|
+
|
|
152
|
+
Every scaffolded project gets a `.jdm-config.json` file. This allows commands to verify they're running inside a compatible project:
|
|
153
|
+
|
|
154
|
+
```json
|
|
155
|
+
{
|
|
156
|
+
"plugin-template": {
|
|
157
|
+
"pluginVersion": "1.0.0",
|
|
158
|
+
"createdAt": "2025-01-01T00:00:00.000Z",
|
|
159
|
+
"projectName": "my-plugin"
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
Use `checkCompat(chalk, "command-name")` at the top of any command that requires a valid project context. It will warn if the config is missing, or error with a helpful message if the version is out of range.
|
|
165
|
+
|
|
166
|
+
---
|
|
167
|
+
|
|
168
|
+
## Requirements
|
|
169
|
+
|
|
170
|
+
- Node.js 18+
|
|
171
|
+
- Git (for the `create` command)
|
|
172
|
+
- jdm-cli installed globally
|
|
173
|
+
|
|
174
|
+
---
|
|
175
|
+
|
|
176
|
+
## License
|
|
177
|
+
|
|
178
|
+
MIT — [JDM-Github](https://github.com/JDM-Github)
|
package/bin/guard.js
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import chalk from "chalk";
|
|
3
|
+
import stringWidth from "string-width";
|
|
4
|
+
import stripAnsi from "strip-ansi";
|
|
5
|
+
|
|
6
|
+
const box = (lines) => {
|
|
7
|
+
const cleaned = lines.map(l => stripAnsi(l));
|
|
8
|
+
|
|
9
|
+
const width = Math.max(...cleaned.map(stringWidth));
|
|
10
|
+
|
|
11
|
+
const horizontal = width + 2;
|
|
12
|
+
|
|
13
|
+
const top = "┌" + "─".repeat(horizontal) + "┐";
|
|
14
|
+
const bottom = "└" + "─".repeat(horizontal) + "┘";
|
|
15
|
+
|
|
16
|
+
const mid = lines.map((l, i) => {
|
|
17
|
+
const rawWidth = stringWidth(cleaned[i]);
|
|
18
|
+
const pad = horizontal - rawWidth - 1;
|
|
19
|
+
return "│ " + l + " ".repeat(pad) + "│";
|
|
20
|
+
});
|
|
21
|
+
return [top, ...mid, bottom].join("\n");
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
const output = box([
|
|
25
|
+
chalk.red("⛔ Direct usage is not allowed."),
|
|
26
|
+
"",
|
|
27
|
+
"This package is a jdm-cli plugin-template.",
|
|
28
|
+
"Install jdm-cli first, then register this plugin:",
|
|
29
|
+
"",
|
|
30
|
+
"npm install -g jdm-cli",
|
|
31
|
+
"jdm-cli add plugin-template",
|
|
32
|
+
"",
|
|
33
|
+
"Then use it via:",
|
|
34
|
+
"jdm-cli plugin-template <command>",
|
|
35
|
+
]);
|
|
36
|
+
|
|
37
|
+
console.log("\n" + output + "\n");
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
// ─────────────────────────────────────────────────────────────
|
|
2
|
+
// jdm-plugin-template — lib/commands/build.js
|
|
3
|
+
//
|
|
4
|
+
// The "build" command compiles / packages your project.
|
|
5
|
+
//
|
|
6
|
+
// This template shows:
|
|
7
|
+
// ✔ Multi-flag build stages (--frontend, --backend, --full)
|
|
8
|
+
// ✔ How to shell out to build tools (npm run build, etc.)
|
|
9
|
+
// ✔ How to copy output files to a destination folder
|
|
10
|
+
// ✔ Graceful error handling per stage
|
|
11
|
+
//
|
|
12
|
+
// Rename to compile.js / bundle.js / package.js — whatever
|
|
13
|
+
// makes sense for your plugin.
|
|
14
|
+
// ─────────────────────────────────────────────────────────────
|
|
15
|
+
|
|
16
|
+
import fs from "fs";
|
|
17
|
+
import path from "path";
|
|
18
|
+
import { execSync } from "child_process";
|
|
19
|
+
import { checkCompat } from "../config.js";
|
|
20
|
+
import { ok, fail, info, warn, step, header, divider } from "../logger.js";
|
|
21
|
+
|
|
22
|
+
// ─────────────────────────────────────────────────────────────
|
|
23
|
+
// Internal exec helper
|
|
24
|
+
// ─────────────────────────────────────────────────────────────
|
|
25
|
+
function exec(cmd, opts = {}) {
|
|
26
|
+
try {
|
|
27
|
+
execSync(cmd, { ...opts, stdio: "pipe" });
|
|
28
|
+
} catch (err) {
|
|
29
|
+
const out = [err.stdout?.toString(), err.stderr?.toString()].filter(Boolean).join("\n");
|
|
30
|
+
throw new Error(out || err.message);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// ─────────────────────────────────────────────────────────────
|
|
35
|
+
// Build stages (each is its own function — easy to compose)
|
|
36
|
+
// ─────────────────────────────────────────────────────────────
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Example "frontend" stage.
|
|
40
|
+
* Replace with your actual frontend build command + output path.
|
|
41
|
+
*/
|
|
42
|
+
function buildFrontend(chalk, root) {
|
|
43
|
+
// const frontendDir = path.join(root, "frontend");
|
|
44
|
+
// info(chalk, "Building frontend...");
|
|
45
|
+
// exec("npm run build", { cwd: frontendDir });
|
|
46
|
+
// ok(chalk, "Frontend built");
|
|
47
|
+
|
|
48
|
+
// Placeholder
|
|
49
|
+
info(chalk, "buildFrontend placeholder — wire up your build command.");
|
|
50
|
+
ok(chalk, "Frontend stage done (placeholder)");
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Example "backend" stage.
|
|
55
|
+
* Replace with your backend compile/package command.
|
|
56
|
+
*/
|
|
57
|
+
function buildBackend(chalk, root) {
|
|
58
|
+
// const backendDir = path.join(root, "backend");
|
|
59
|
+
// info(chalk, "Packaging backend...");
|
|
60
|
+
// exec("python build.py", { cwd: backendDir });
|
|
61
|
+
// ok(chalk, "Backend packaged");
|
|
62
|
+
|
|
63
|
+
info(chalk, "buildBackend placeholder — wire up your build command.");
|
|
64
|
+
ok(chalk, "Backend stage done (placeholder)");
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// ─────────────────────────────────────────────────────────────
|
|
68
|
+
// Main export
|
|
69
|
+
// Signature: build(chalk, args)
|
|
70
|
+
// ─────────────────────────────────────────────────────────────
|
|
71
|
+
export default async function build(chalk, args = []) {
|
|
72
|
+
header(chalk, "build");
|
|
73
|
+
|
|
74
|
+
// ── Guard ─────────────────────────────────────────────────
|
|
75
|
+
if (!checkCompat(chalk, "build")) return;
|
|
76
|
+
|
|
77
|
+
// ── Parse flags ───────────────────────────────────────────
|
|
78
|
+
// --frontend build only the frontend
|
|
79
|
+
// --backend build only the backend
|
|
80
|
+
// --full build everything (same as --frontend --backend)
|
|
81
|
+
const doFrontend = args.includes("--frontend") || args.includes("--full");
|
|
82
|
+
const doBackend = args.includes("--backend") || args.includes("--full");
|
|
83
|
+
const doAll = !doFrontend && !doBackend; // no flags → build all
|
|
84
|
+
|
|
85
|
+
const root = process.cwd();
|
|
86
|
+
let stageN = 1;
|
|
87
|
+
const total = (doAll ? 2 : 0) + (doFrontend ? 1 : 0) + (doBackend ? 1 : 0);
|
|
88
|
+
|
|
89
|
+
try {
|
|
90
|
+
if (doFrontend || doAll) {
|
|
91
|
+
step(chalk, stageN++, total, "Frontend");
|
|
92
|
+
buildFrontend(chalk, root);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
if (doBackend || doAll) {
|
|
96
|
+
step(chalk, stageN++, total, "Backend");
|
|
97
|
+
buildBackend(chalk, root);
|
|
98
|
+
}
|
|
99
|
+
} catch (err) {
|
|
100
|
+
fail(chalk, `Build failed: ${err.message}`);
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
console.log();
|
|
105
|
+
divider(chalk);
|
|
106
|
+
console.log(chalk.green(" ✔ Build complete!"));
|
|
107
|
+
divider(chalk);
|
|
108
|
+
console.log();
|
|
109
|
+
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
// ─────────────────────────────────────────────────────────────
|
|
2
|
+
// jdm-plugin-template — lib/commands/clean.js
|
|
3
|
+
//
|
|
4
|
+
// The "clean" command removes build artifacts and temp files.
|
|
5
|
+
//
|
|
6
|
+
// This template shows:
|
|
7
|
+
// ✔ How to remove folders/files with existence checks
|
|
8
|
+
// ✔ How to give the user a dry-run option (--dry)
|
|
9
|
+
// ✔ Pattern for defining what to clean in one place
|
|
10
|
+
// ─────────────────────────────────────────────────────────────
|
|
11
|
+
|
|
12
|
+
import fs from "fs";
|
|
13
|
+
import path from "path";
|
|
14
|
+
import { checkCompat } from "../config.js";
|
|
15
|
+
import { ok, warn, info, header, divider } from "../logger.js";
|
|
16
|
+
|
|
17
|
+
// ── Define everything that should be cleaned here ─────────────
|
|
18
|
+
// Each entry is relative to process.cwd().
|
|
19
|
+
// Add/remove paths to match your plugin's output structure.
|
|
20
|
+
const CLEAN_TARGETS = [
|
|
21
|
+
"dist",
|
|
22
|
+
"build",
|
|
23
|
+
".cache",
|
|
24
|
+
// "some-other-artifact-folder",
|
|
25
|
+
];
|
|
26
|
+
|
|
27
|
+
// ─────────────────────────────────────────────────────────────
|
|
28
|
+
// Main export
|
|
29
|
+
// Signature: clean(chalk, args)
|
|
30
|
+
// ─────────────────────────────────────────────────────────────
|
|
31
|
+
export default async function clean(chalk, args = []) {
|
|
32
|
+
header(chalk, "clean");
|
|
33
|
+
|
|
34
|
+
// ── Guard: must be inside a valid project ─────────────────
|
|
35
|
+
if (!checkCompat(chalk, "clean")) return;
|
|
36
|
+
|
|
37
|
+
// ── Flags ─────────────────────────────────────────────────
|
|
38
|
+
// --dry Print what would be deleted without deleting it
|
|
39
|
+
const dry = args.includes("--dry");
|
|
40
|
+
if (dry) info(chalk, chalk.yellow("Dry run — nothing will be deleted"));
|
|
41
|
+
|
|
42
|
+
const root = process.cwd();
|
|
43
|
+
let removed = 0;
|
|
44
|
+
|
|
45
|
+
for (const target of CLEAN_TARGETS) {
|
|
46
|
+
const fullPath = path.join(root, target);
|
|
47
|
+
|
|
48
|
+
if (!fs.existsSync(fullPath)) {
|
|
49
|
+
// Skip silently — already clean
|
|
50
|
+
continue;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
if (dry) {
|
|
54
|
+
warn(chalk, `Would remove: ${chalk.cyan(target)}`);
|
|
55
|
+
} else {
|
|
56
|
+
fs.rmSync(fullPath, { recursive: true, force: true });
|
|
57
|
+
ok(chalk, `Removed ${chalk.cyan(target)}`);
|
|
58
|
+
removed++;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
if (removed === 0 && !dry) {
|
|
63
|
+
info(chalk, "Nothing to clean.");
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
console.log();
|
|
67
|
+
divider(chalk);
|
|
68
|
+
console.log(dry
|
|
69
|
+
? chalk.yellow(" ⚠ Dry run complete — no files deleted")
|
|
70
|
+
: chalk.green(" ✔ Clean complete!")
|
|
71
|
+
);
|
|
72
|
+
divider(chalk);
|
|
73
|
+
console.log();
|
|
74
|
+
}
|
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
// ─────────────────────────────────────────────────────────────
|
|
2
|
+
// jdm-plugin-template — lib/commands/create.js
|
|
3
|
+
//
|
|
4
|
+
// The "create" command scaffolds a new project for your plugin.
|
|
5
|
+
//
|
|
6
|
+
// This template shows:
|
|
7
|
+
// ✔ How to use the header / step / ok / fail / info helpers
|
|
8
|
+
// ✔ How to prompt the user interactively with rl (readline)
|
|
9
|
+
// ✔ How to use the --name flag to skip the prompt
|
|
10
|
+
// ✔ How to clone a GitHub repo and strip .git
|
|
11
|
+
// ✔ How to write a local config file (.jdm-config.json)
|
|
12
|
+
// ✔ How to use an install.log for error output
|
|
13
|
+
//
|
|
14
|
+
// Remove or replace anything you don't need.
|
|
15
|
+
// ─────────────────────────────────────────────────────────────
|
|
16
|
+
|
|
17
|
+
import fs from "fs";
|
|
18
|
+
import path from "path";
|
|
19
|
+
import { execSync } from "child_process";
|
|
20
|
+
import { writeConfig } from "../config.js";
|
|
21
|
+
import { ok, fail, warn, info, step, header, divider } from "../logger.js";
|
|
22
|
+
|
|
23
|
+
// ── Replace this with your actual GitHub template repo URL ────
|
|
24
|
+
const TEMPLATE_REPO = "https://github.com/JDM-Github/jdm-plugin-template";
|
|
25
|
+
|
|
26
|
+
// ── Folders that would conflict in cwd install ────────────────
|
|
27
|
+
const CONFLICT_FOLDERS = ["src", "dist"]; // ← adjust as needed
|
|
28
|
+
|
|
29
|
+
// ─────────────────────────────────────────────────────────────
|
|
30
|
+
// Install log (written to <targetDir>/install.log on error)
|
|
31
|
+
// ─────────────────────────────────────────────────────────────
|
|
32
|
+
let logPath = null;
|
|
33
|
+
|
|
34
|
+
function initLog(targetDir) {
|
|
35
|
+
logPath = path.join(targetDir, "install.log");
|
|
36
|
+
fs.writeFileSync(logPath, `[install log — ${new Date().toISOString()}]\n\n`, "utf8");
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function appendLog(line) {
|
|
40
|
+
if (logPath) fs.appendFileSync(logPath, line + "\n", "utf8");
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function cleanLog() {
|
|
44
|
+
if (logPath && fs.existsSync(logPath)) {
|
|
45
|
+
fs.unlinkSync(logPath);
|
|
46
|
+
logPath = null;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// ─────────────────────────────────────────────────────────────
|
|
51
|
+
// Exec wrapper (captures stdout/stderr into install.log)
|
|
52
|
+
// ─────────────────────────────────────────────────────────────
|
|
53
|
+
function exec(cmd, opts = {}) {
|
|
54
|
+
try {
|
|
55
|
+
const result = execSync(cmd, { ...opts, stdio: "pipe" });
|
|
56
|
+
if (result) appendLog(`[OK] ${cmd}\n${result.toString()}`);
|
|
57
|
+
return result;
|
|
58
|
+
} catch (err) {
|
|
59
|
+
appendLog([
|
|
60
|
+
`[FAIL] ${cmd}`,
|
|
61
|
+
err.stdout?.toString() ?? "",
|
|
62
|
+
err.stderr?.toString() ?? "",
|
|
63
|
+
].join("\n"));
|
|
64
|
+
throw err;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// ─────────────────────────────────────────────────────────────
|
|
69
|
+
// rl helper (wraps readline.question as a Promise)
|
|
70
|
+
// ─────────────────────────────────────────────────────────────
|
|
71
|
+
function ask(rl, question) {
|
|
72
|
+
return new Promise((resolve) => rl.question(question, resolve));
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// ─────────────────────────────────────────────────────────────
|
|
76
|
+
// Main export
|
|
77
|
+
// Signature must match what index.js passes:
|
|
78
|
+
// create(chalk, rl, args)
|
|
79
|
+
// ─────────────────────────────────────────────────────────────
|
|
80
|
+
export default async function create(chalk, rl, args = []) {
|
|
81
|
+
header(chalk, "create");
|
|
82
|
+
|
|
83
|
+
// ── Parse flags ───────────────────────────────────────────
|
|
84
|
+
// --name my-project → skip the name prompt
|
|
85
|
+
// --install → run npm install / pip install after clone
|
|
86
|
+
const nameIdx = args.indexOf("--name");
|
|
87
|
+
const nameArg = nameIdx !== -1 ? args[nameIdx + 1] : null;
|
|
88
|
+
const shouldInstall = args.includes("--install");
|
|
89
|
+
|
|
90
|
+
// ── Step 1: resolve target directory ─────────────────────
|
|
91
|
+
step(chalk, 1, 3, "Target Directory");
|
|
92
|
+
|
|
93
|
+
const answer = nameArg
|
|
94
|
+
?? (await ask(rl, chalk.white("\n Project name (or . for current folder): "))).trim();
|
|
95
|
+
|
|
96
|
+
let targetDir;
|
|
97
|
+
|
|
98
|
+
if (answer === ".") {
|
|
99
|
+
// ── Install into current directory ────────────────────
|
|
100
|
+
targetDir = process.cwd();
|
|
101
|
+
info(chalk, `Using current directory: ${chalk.cyan(targetDir)}`);
|
|
102
|
+
|
|
103
|
+
const entries = fs.readdirSync(targetDir);
|
|
104
|
+
const conflicts = entries.filter(
|
|
105
|
+
(e) => CONFLICT_FOLDERS.includes(e) && fs.statSync(path.join(targetDir, e)).isDirectory()
|
|
106
|
+
);
|
|
107
|
+
|
|
108
|
+
if (conflicts.length > 0) {
|
|
109
|
+
fail(chalk, `Conflicting folders: ${conflicts.map((c) => chalk.red(c)).join(", ")}`);
|
|
110
|
+
const confirm = (await ask(rl, chalk.white(" Remove them and continue? [y/N]: "))).trim().toLowerCase();
|
|
111
|
+
if (confirm !== "y") {
|
|
112
|
+
console.log(chalk.gray("\n Aborted.\n"));
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
for (const c of conflicts) {
|
|
116
|
+
fs.rmSync(path.join(targetDir, c), { recursive: true, force: true });
|
|
117
|
+
ok(chalk, `Removed ${chalk.red(c)}`);
|
|
118
|
+
}
|
|
119
|
+
} else if (entries.length > 0) {
|
|
120
|
+
warn(chalk, "Current folder is not empty.");
|
|
121
|
+
const confirm = (await ask(rl, chalk.white(" Continue anyway? [y/N]: "))).trim().toLowerCase();
|
|
122
|
+
if (confirm !== "y") {
|
|
123
|
+
console.log(chalk.gray("\n Aborted.\n"));
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
} else {
|
|
129
|
+
// ── Create a named subfolder ──────────────────────────
|
|
130
|
+
if (!answer || answer.includes("/") || answer.includes("\\")) {
|
|
131
|
+
fail(chalk, "Invalid project name.");
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
targetDir = path.join(process.cwd(), answer);
|
|
135
|
+
if (fs.existsSync(targetDir)) {
|
|
136
|
+
warn(chalk, `Folder ${chalk.cyan(answer)} already exists.`);
|
|
137
|
+
const confirm = (await ask(rl, chalk.white(" Continue anyway? [y/N]: "))).trim().toLowerCase();
|
|
138
|
+
if (confirm !== "y") {
|
|
139
|
+
console.log(chalk.gray("\n Aborted.\n"));
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
142
|
+
} else {
|
|
143
|
+
fs.mkdirSync(targetDir, { recursive: true });
|
|
144
|
+
ok(chalk, `Created folder: ${chalk.cyan(targetDir)}`);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
initLog(targetDir);
|
|
149
|
+
|
|
150
|
+
// ── Step 2: clone template ────────────────────────────────
|
|
151
|
+
step(chalk, 2, 3, "Cloning template");
|
|
152
|
+
|
|
153
|
+
try {
|
|
154
|
+
info(chalk, `Cloning from ${chalk.cyan(TEMPLATE_REPO)}...`);
|
|
155
|
+
|
|
156
|
+
// Clone into a temp subfolder first to avoid git's "already exists
|
|
157
|
+
// and is not an empty directory" error — which always fires when the
|
|
158
|
+
// template repo is cloning itself into its own working directory.
|
|
159
|
+
const tmpDir = path.join(targetDir, "__jdm_tmp__");
|
|
160
|
+
if (fs.existsSync(tmpDir)) fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
161
|
+
|
|
162
|
+
exec(`git clone ${TEMPLATE_REPO} "${tmpDir}"`);
|
|
163
|
+
|
|
164
|
+
// Strip .git so this becomes a clean project, not a fork
|
|
165
|
+
const gitDir = path.join(tmpDir, ".git");
|
|
166
|
+
if (fs.existsSync(gitDir)) {
|
|
167
|
+
fs.rmSync(gitDir, { recursive: true, force: true });
|
|
168
|
+
info(chalk, "Removed .git (clean slate)");
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// Move all cloned files from tmpDir into targetDir,
|
|
172
|
+
// overwriting anything that was already there.
|
|
173
|
+
for (const entry of fs.readdirSync(tmpDir)) {
|
|
174
|
+
const src = path.join(tmpDir, entry);
|
|
175
|
+
const dest = path.join(targetDir, entry);
|
|
176
|
+
if (fs.existsSync(dest)) fs.rmSync(dest, { recursive: true, force: true });
|
|
177
|
+
fs.renameSync(src, dest);
|
|
178
|
+
}
|
|
179
|
+
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
180
|
+
|
|
181
|
+
ok(chalk, "Template cloned");
|
|
182
|
+
} catch (err) {
|
|
183
|
+
fail(chalk, `Clone failed: ${err.message}`);
|
|
184
|
+
console.log(chalk.yellow("\n Full output written to: ") + chalk.white("install.log"));
|
|
185
|
+
return;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// ── Step 3: optional dependency install ──────────────────
|
|
189
|
+
step(chalk, 3, 3, "Dependencies");
|
|
190
|
+
|
|
191
|
+
if (shouldInstall) {
|
|
192
|
+
try {
|
|
193
|
+
info(chalk, "Running npm install...");
|
|
194
|
+
exec("npm install", { cwd: targetDir });
|
|
195
|
+
ok(chalk, "Dependencies installed");
|
|
196
|
+
} catch (err) {
|
|
197
|
+
fail(chalk, `npm install failed: ${err.message}`);
|
|
198
|
+
console.log(chalk.yellow("\n Full output written to: ") + chalk.white("install.log"));
|
|
199
|
+
return;
|
|
200
|
+
}
|
|
201
|
+
} else {
|
|
202
|
+
info(chalk, "Skipped (pass --install to auto-install)");
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// ── Write local config ────────────────────────────────────
|
|
206
|
+
writeConfig(targetDir, { projectName: path.basename(targetDir) });
|
|
207
|
+
info(chalk, `Created ${chalk.cyan(".jdm-config.json")}`);
|
|
208
|
+
|
|
209
|
+
cleanLog();
|
|
210
|
+
|
|
211
|
+
// ── Done ──────────────────────────────────────────────────
|
|
212
|
+
console.log();
|
|
213
|
+
divider(chalk);
|
|
214
|
+
console.log(chalk.green(" ✔ Project ready!"));
|
|
215
|
+
console.log(chalk.gray(` Location: ${targetDir}`));
|
|
216
|
+
divider(chalk);
|
|
217
|
+
console.log();
|
|
218
|
+
|
|
219
|
+
if (!shouldInstall) {
|
|
220
|
+
console.log(chalk.white(" Next steps:"));
|
|
221
|
+
console.log(chalk.gray(" jdm-cli <namespace> install → install dependencies"));
|
|
222
|
+
console.log(chalk.gray(" jdm-cli <namespace> dev → start development"));
|
|
223
|
+
} else {
|
|
224
|
+
console.log(chalk.white(" Next steps:"));
|
|
225
|
+
console.log(chalk.gray(" jdm-cli <namespace> dev → start development"));
|
|
226
|
+
}
|
|
227
|
+
console.log();
|
|
228
|
+
}
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
// ─────────────────────────────────────────────────────────────
|
|
2
|
+
// jdm-plugin-template — lib/commands/dev.js
|
|
3
|
+
//
|
|
4
|
+
// The "dev" command starts your development environment.
|
|
5
|
+
//
|
|
6
|
+
// This template shows:
|
|
7
|
+
// ✔ How to guard a command with checkCompat
|
|
8
|
+
// ✔ How to spawn processes in new terminal windows
|
|
9
|
+
// (Windows Terminal → CMD fallback → Unix terminals)
|
|
10
|
+
// ✔ How to allocate free ports dynamically
|
|
11
|
+
// ✔ How to pass env vars to spawned processes
|
|
12
|
+
//
|
|
13
|
+
// Replace the process launch blocks with whatever your plugin
|
|
14
|
+
// needs to start (servers, watchers, proxies, etc.)
|
|
15
|
+
// ─────────────────────────────────────────────────────────────
|
|
16
|
+
|
|
17
|
+
import fs from "fs";
|
|
18
|
+
import path from "path";
|
|
19
|
+
import net from "net";
|
|
20
|
+
import { spawn } from "child_process";
|
|
21
|
+
import { checkCompat } from "../config.js";
|
|
22
|
+
import { ok, fail, info, header, divider } from "../logger.js";
|
|
23
|
+
|
|
24
|
+
// ─────────────────────────────────────────────────────────────
|
|
25
|
+
// Port allocation
|
|
26
|
+
// ─────────────────────────────────────────────────────────────
|
|
27
|
+
|
|
28
|
+
/** Returns a free port on 127.0.0.1 by binding to port 0. */
|
|
29
|
+
function getFreePort() {
|
|
30
|
+
return new Promise((resolve, reject) => {
|
|
31
|
+
const server = net.createServer();
|
|
32
|
+
server.listen(0, "127.0.0.1", () => {
|
|
33
|
+
const port = server.address().port;
|
|
34
|
+
server.close(() => resolve(port));
|
|
35
|
+
});
|
|
36
|
+
server.on("error", reject);
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// ─────────────────────────────────────────────────────────────
|
|
41
|
+
// Cross-platform terminal launchers
|
|
42
|
+
// ─────────────────────────────────────────────────────────────
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Launch a command in a new Windows Terminal tab.
|
|
46
|
+
* Falls back to launchInCmdWindow if wt.exe isn't available.
|
|
47
|
+
*/
|
|
48
|
+
function launchInWindowsTerminal(title, cwd, command, args, env = {}) {
|
|
49
|
+
const envPrefix = Object.entries(env).map(([k, v]) => `set ${k}=${v} &&`).join(" ");
|
|
50
|
+
const fullCmd = `cd /d "${cwd}" && ${envPrefix} ${command} ${args.join(" ")}`;
|
|
51
|
+
|
|
52
|
+
const proc = spawn("wt.exe", [
|
|
53
|
+
"-w", "0", "new-tab", "--title", title,
|
|
54
|
+
"--", "cmd.exe", "/k", fullCmd,
|
|
55
|
+
], { detached: true, stdio: "ignore", shell: false });
|
|
56
|
+
|
|
57
|
+
proc.unref();
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** Launch in a plain new CMD window (fallback when wt.exe is absent). */
|
|
61
|
+
function launchInCmdWindow(cwd, command, args, env = {}) {
|
|
62
|
+
const envPrefix = Object.entries(env).map(([k, v]) => `set ${k}=${v} &&`).join(" ");
|
|
63
|
+
const fullCmd = `cd /d "${cwd}" && ${envPrefix} ${command} ${args.join(" ")}`;
|
|
64
|
+
|
|
65
|
+
const proc = spawn("cmd.exe", ["/c", "start", "cmd.exe", "/k", fullCmd], {
|
|
66
|
+
detached: true, stdio: "ignore", shell: false,
|
|
67
|
+
});
|
|
68
|
+
proc.unref();
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/** Try Windows Terminal; return false if unavailable. */
|
|
72
|
+
function tryWindowsTerminal(title, cwd, command, args, env = {}) {
|
|
73
|
+
try { launchInWindowsTerminal(title, cwd, command, args, env); return true; }
|
|
74
|
+
catch { return false; }
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/** Try common Unix/macOS terminals in priority order. */
|
|
78
|
+
function launchInUnixTerminal(title, cwd, command, args, env = {}) {
|
|
79
|
+
const envPrefix = Object.entries(env).map(([k, v]) => `${k}=${v}`).join(" ");
|
|
80
|
+
const fullCmd = `cd "${cwd}" && ${envPrefix} ${command} ${args.join(" ")}; exec $SHELL`;
|
|
81
|
+
|
|
82
|
+
const terminals = [
|
|
83
|
+
["gnome-terminal", ["--title", title, "--", "bash", "-c", fullCmd]],
|
|
84
|
+
["xterm", ["-title", title, "-e", `bash -c '${fullCmd}'`]],
|
|
85
|
+
["osascript", ["-e", `tell application "Terminal" to do script "cd \\"${cwd}\\" && ${envPrefix} ${command} ${args.join(" ")}"`]],
|
|
86
|
+
];
|
|
87
|
+
|
|
88
|
+
for (const [term, termArgs] of terminals) {
|
|
89
|
+
try {
|
|
90
|
+
spawn(term, termArgs, { detached: true, stdio: "ignore" }).unref();
|
|
91
|
+
return true;
|
|
92
|
+
} catch { /* try next */ }
|
|
93
|
+
}
|
|
94
|
+
return false;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// ─────────────────────────────────────────────────────────────
|
|
98
|
+
// Main export
|
|
99
|
+
// Signature: dev(chalk, args)
|
|
100
|
+
// No rl — dev doesn't need interactive prompts.
|
|
101
|
+
// ─────────────────────────────────────────────────────────────
|
|
102
|
+
export default async function dev(chalk, args = []) {
|
|
103
|
+
header(chalk, "dev");
|
|
104
|
+
|
|
105
|
+
// ── Guard: must be inside a valid project ─────────────────
|
|
106
|
+
if (!checkCompat(chalk, "dev")) return;
|
|
107
|
+
|
|
108
|
+
const root = process.cwd();
|
|
109
|
+
|
|
110
|
+
// ── TODO: define your service directories here ────────────
|
|
111
|
+
// const backendDir = path.join(root, "backend");
|
|
112
|
+
// const frontendDir = path.join(root, "frontend");
|
|
113
|
+
//
|
|
114
|
+
// Example existence check:
|
|
115
|
+
// if (!fs.existsSync(backendDir)) {
|
|
116
|
+
// fail(chalk, `backend/ not found in ${chalk.cyan(root)}`);
|
|
117
|
+
// return;
|
|
118
|
+
// }
|
|
119
|
+
|
|
120
|
+
// ── Allocate ports (remove if your plugin doesn't use them)
|
|
121
|
+
// const port = await getFreePort();
|
|
122
|
+
// info(chalk, `Allocated port ${port}`);
|
|
123
|
+
|
|
124
|
+
// ── TODO: launch your dev processes ──────────────────────
|
|
125
|
+
//
|
|
126
|
+
// Windows example:
|
|
127
|
+
// const launched = tryWindowsTerminal("My Dev Server", root, "npm", ["run", "dev"], {});
|
|
128
|
+
// if (!launched) launchInCmdWindow(root, "npm", ["run", "dev"], {});
|
|
129
|
+
//
|
|
130
|
+
// Unix example:
|
|
131
|
+
// const success = launchInUnixTerminal("My Dev Server", root, "npm", ["run", "dev"], {});
|
|
132
|
+
// if (!success) {
|
|
133
|
+
// fail(chalk, "Could not open terminal. Run manually:");
|
|
134
|
+
// console.log(chalk.gray(` cd "${root}" && npm run dev`));
|
|
135
|
+
// return;
|
|
136
|
+
// }
|
|
137
|
+
|
|
138
|
+
// ── Placeholder output — replace when you wire up processes
|
|
139
|
+
info(chalk, "Dev command placeholder — wire up your processes above.");
|
|
140
|
+
ok(chalk, "Nothing launched yet (template mode)");
|
|
141
|
+
|
|
142
|
+
console.log();
|
|
143
|
+
divider(chalk);
|
|
144
|
+
console.log(chalk.green(" ✔ Dev environment ready!"));
|
|
145
|
+
divider(chalk);
|
|
146
|
+
console.log();
|
|
147
|
+
}
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
// ─────────────────────────────────────────────────────────────
|
|
2
|
+
// jdm-plugin-template — lib/commands/install.js
|
|
3
|
+
//
|
|
4
|
+
// The "install" command installs dependencies for the project.
|
|
5
|
+
//
|
|
6
|
+
// This template shows:
|
|
7
|
+
// ✔ Running npm install / pip install per sub-folder
|
|
8
|
+
// ✔ Checking for package.json / requirements.txt before running
|
|
9
|
+
// ✔ Reporting success/failure per package manager
|
|
10
|
+
// ─────────────────────────────────────────────────────────────
|
|
11
|
+
|
|
12
|
+
import fs from "fs";
|
|
13
|
+
import path from "path";
|
|
14
|
+
import { execSync } from "child_process";
|
|
15
|
+
import { checkCompat } from "../config.js";
|
|
16
|
+
import { ok, fail, warn, info, step, header, divider } from "../logger.js";
|
|
17
|
+
|
|
18
|
+
// ── Directories to install deps for ───────────────────────────
|
|
19
|
+
// Each entry:
|
|
20
|
+
// dir → subfolder relative to project root
|
|
21
|
+
// type → "npm" | "pip"
|
|
22
|
+
//
|
|
23
|
+
// Add, remove, or reorder as your plugin needs.
|
|
24
|
+
const INSTALL_TARGETS = [
|
|
25
|
+
{ dir: ".", type: "npm" }
|
|
26
|
+
// { dir: "frontend", type: "npm" },
|
|
27
|
+
// { dir: "backend", type: "pip" },
|
|
28
|
+
];
|
|
29
|
+
|
|
30
|
+
// ─────────────────────────────────────────────────────────────
|
|
31
|
+
function exec(cmd, opts = {}) {
|
|
32
|
+
try {
|
|
33
|
+
execSync(cmd, { ...opts, stdio: "pipe" });
|
|
34
|
+
} catch (err) {
|
|
35
|
+
const out = [err.stdout?.toString(), err.stderr?.toString()].filter(Boolean).join("\n");
|
|
36
|
+
throw new Error(out || err.message);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// ─────────────────────────────────────────────────────────────
|
|
41
|
+
// Main export
|
|
42
|
+
// Signature: install(chalk, args)
|
|
43
|
+
// ─────────────────────────────────────────────────────────────
|
|
44
|
+
export default async function install(chalk, args = []) {
|
|
45
|
+
header(chalk, "install");
|
|
46
|
+
|
|
47
|
+
// ── Guard ─────────────────────────────────────────────────
|
|
48
|
+
if (!checkCompat(chalk, "install")) return;
|
|
49
|
+
|
|
50
|
+
const root = process.cwd();
|
|
51
|
+
|
|
52
|
+
if (INSTALL_TARGETS.length === 0) {
|
|
53
|
+
info(chalk, "No install targets defined yet.");
|
|
54
|
+
info(chalk, "Edit INSTALL_TARGETS in lib/commands/install.js to add your dirs.");
|
|
55
|
+
console.log();
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
for (let i = 0; i < INSTALL_TARGETS.length; i++) {
|
|
60
|
+
const { dir, type } = INSTALL_TARGETS[i];
|
|
61
|
+
step(chalk, i + 1, INSTALL_TARGETS.length, `Installing ${dir}`);
|
|
62
|
+
|
|
63
|
+
const fullDir = path.join(root, dir);
|
|
64
|
+
|
|
65
|
+
if (!fs.existsSync(fullDir)) {
|
|
66
|
+
warn(chalk, `${dir}/ not found — skipping`);
|
|
67
|
+
continue;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
try {
|
|
71
|
+
if (type === "npm") {
|
|
72
|
+
const pkgJson = path.join(fullDir, "package.json");
|
|
73
|
+
if (!fs.existsSync(pkgJson)) {
|
|
74
|
+
warn(chalk, `No package.json in ${dir}/ — skipping`);
|
|
75
|
+
continue;
|
|
76
|
+
}
|
|
77
|
+
info(chalk, `Running npm install in ${chalk.cyan(dir)}/...`);
|
|
78
|
+
exec("npm install", { cwd: fullDir });
|
|
79
|
+
ok(chalk, "npm dependencies installed");
|
|
80
|
+
|
|
81
|
+
} else if (type === "pip") {
|
|
82
|
+
const req = path.join(fullDir, "requirements.txt");
|
|
83
|
+
if (!fs.existsSync(req)) {
|
|
84
|
+
warn(chalk, `No requirements.txt in ${dir}/ — skipping`);
|
|
85
|
+
continue;
|
|
86
|
+
}
|
|
87
|
+
info(chalk, `Running pip install in ${chalk.cyan(dir)}/...`);
|
|
88
|
+
exec("pip install -r requirements.txt", { cwd: fullDir });
|
|
89
|
+
ok(chalk, "Python dependencies installed");
|
|
90
|
+
}
|
|
91
|
+
} catch (err) {
|
|
92
|
+
fail(chalk, `Failed to install ${dir}: ${err.message}`);
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
console.log();
|
|
98
|
+
divider(chalk);
|
|
99
|
+
console.log(chalk.green(" ✔ All dependencies installed!"));
|
|
100
|
+
divider(chalk);
|
|
101
|
+
console.log();
|
|
102
|
+
}
|
package/lib/compat.js
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
// ─────────────────────────────────────────────────────────────
|
|
2
|
+
// jdm-plugin-template — lib/compat.js
|
|
3
|
+
//
|
|
4
|
+
// Single source of truth for version compatibility.
|
|
5
|
+
//
|
|
6
|
+
// HOW IT WORKS
|
|
7
|
+
// ─────────────
|
|
8
|
+
// • `pluginVersion` – the version of THIS plugin (bump on releases)
|
|
9
|
+
// • `COMPAT.global` – minimum project version required by ALL commands
|
|
10
|
+
// • `COMPAT.commands`– per-command overrides (takes priority over global)
|
|
11
|
+
//
|
|
12
|
+
// RANGE SYNTAX (no external deps)
|
|
13
|
+
// ────────────────────────────────
|
|
14
|
+
// ">=1.0.0" project created with plugin 1.0.0 or newer
|
|
15
|
+
// "<=2.0.0" project created with plugin 2.0.0 or older
|
|
16
|
+
// "1.0.0" exact version only
|
|
17
|
+
// ">=1.0.0||<=0.9.5" union (rare — use sparingly)
|
|
18
|
+
//
|
|
19
|
+
// WHEN TO BUMP
|
|
20
|
+
// ─────────────
|
|
21
|
+
// • You change a template repo structure that breaks an existing command
|
|
22
|
+
// → bump COMPAT.commands[thatCommand] to ">=<new-plugin-version>"
|
|
23
|
+
// • You change something that affects ALL commands (e.g. config layout)
|
|
24
|
+
// → bump COMPAT.global
|
|
25
|
+
// • You add a brand-new command that doesn't touch existing templates
|
|
26
|
+
// → no bump needed
|
|
27
|
+
// ─────────────────────────────────────────────────────────────
|
|
28
|
+
|
|
29
|
+
export const pluginVersion = "1.0.0";
|
|
30
|
+
|
|
31
|
+
export const COMPAT = {
|
|
32
|
+
|
|
33
|
+
// Every command: project must have been created with >= this version.
|
|
34
|
+
// Set to null to disable the global check.
|
|
35
|
+
global: ">=1.0.0",
|
|
36
|
+
|
|
37
|
+
// Per-command overrides.
|
|
38
|
+
// Only add an entry here when a command needs a tighter requirement
|
|
39
|
+
// than the global range.
|
|
40
|
+
commands: {
|
|
41
|
+
// "build": ">=1.0.0",
|
|
42
|
+
},
|
|
43
|
+
};
|
package/lib/config.js
ADDED
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
// ─────────────────────────────────────────────────────────────
|
|
2
|
+
// jdm-plugin-template — lib/config.js
|
|
3
|
+
// ─────────────────────────────────────────────────────────────
|
|
4
|
+
|
|
5
|
+
import fs from "fs";
|
|
6
|
+
import path from "path";
|
|
7
|
+
import { COMPAT, pluginVersion } from "./compat.js";
|
|
8
|
+
|
|
9
|
+
export const CONFIG_FILE = ".jdm-config.json";
|
|
10
|
+
|
|
11
|
+
// ── CHANGE THIS to match your plugin's namespace ──────────────
|
|
12
|
+
const PLUGIN_NAME = "plugin-template"; // ← CHANGE THIS
|
|
13
|
+
|
|
14
|
+
// ─────────────────────────────────────────────────────────────
|
|
15
|
+
// Version helpers
|
|
16
|
+
// ─────────────────────────────────────────────────────────────
|
|
17
|
+
function parseVer(v) {
|
|
18
|
+
return String(v).split(".").map(Number);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function cmpVer(a, b) {
|
|
22
|
+
const pa = parseVer(a);
|
|
23
|
+
const pb = parseVer(b);
|
|
24
|
+
for (let i = 0; i < 3; i++) {
|
|
25
|
+
const diff = (pa[i] ?? 0) - (pb[i] ?? 0);
|
|
26
|
+
if (diff !== 0) return diff < 0 ? -1 : 1;
|
|
27
|
+
}
|
|
28
|
+
return 0;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function satisfies(version, range) {
|
|
32
|
+
if (!range) return true;
|
|
33
|
+
const rangeList = range.split("||").map(r => r.trim());
|
|
34
|
+
return rangeList.some(r => {
|
|
35
|
+
if (r.startsWith(">=")) return cmpVer(version, r.slice(2)) >= 0;
|
|
36
|
+
if (r.startsWith("<=")) return cmpVer(version, r.slice(2)) <= 0;
|
|
37
|
+
if (r.startsWith(">")) return cmpVer(version, r.slice(1)) > 0;
|
|
38
|
+
if (r.startsWith("<")) return cmpVer(version, r.slice(1)) < 0;
|
|
39
|
+
return cmpVer(version, r) === 0; // exact match
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// ─────────────────────────────────────────────────────────────
|
|
44
|
+
// Config read / write
|
|
45
|
+
// ─────────────────────────────────────────────────────────────
|
|
46
|
+
export function configExists(root = process.cwd()) {
|
|
47
|
+
return fs.existsSync(path.join(root, CONFIG_FILE));
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Read the full .jdm-config.json and return only this plugin's slice.
|
|
52
|
+
* Returns null → file is missing or unparseable.
|
|
53
|
+
* Returns {} → file exists but has no entry for this plugin yet.
|
|
54
|
+
*/
|
|
55
|
+
export function readConfig(root = process.cwd()) {
|
|
56
|
+
const p = path.join(root, CONFIG_FILE);
|
|
57
|
+
if (!fs.existsSync(p)) return null;
|
|
58
|
+
try {
|
|
59
|
+
const full = JSON.parse(fs.readFileSync(p, "utf8"));
|
|
60
|
+
return full[PLUGIN_NAME] ?? {};
|
|
61
|
+
} catch {
|
|
62
|
+
return null;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Merge this plugin's data into .jdm-config.json under its own key.
|
|
68
|
+
* Other plugins' keys are left untouched.
|
|
69
|
+
*
|
|
70
|
+
* Resulting shape:
|
|
71
|
+
* {
|
|
72
|
+
* "plugin-template": { pluginVersion, createdAt, ...extra },
|
|
73
|
+
* "other-plugin": { ... } <- untouched
|
|
74
|
+
* }
|
|
75
|
+
*/
|
|
76
|
+
export function writeConfig(root = process.cwd(), extra = {}) {
|
|
77
|
+
const p = path.join(root, CONFIG_FILE);
|
|
78
|
+
|
|
79
|
+
// Preserve any existing keys from other plugins
|
|
80
|
+
let full = {};
|
|
81
|
+
if (fs.existsSync(p)) {
|
|
82
|
+
try { full = JSON.parse(fs.readFileSync(p, "utf8")); } catch { /* start fresh */ }
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const pluginData = {
|
|
86
|
+
pluginVersion,
|
|
87
|
+
createdAt: new Date().toISOString(),
|
|
88
|
+
...extra,
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
full[PLUGIN_NAME] = pluginData;
|
|
92
|
+
fs.writeFileSync(p, JSON.stringify(full, null, 2) + "\n", "utf8");
|
|
93
|
+
return pluginData;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// ─────────────────────────────────────────────────────────────
|
|
97
|
+
// checkCompat()
|
|
98
|
+
//
|
|
99
|
+
// Call at the top of any command that requires the user to be
|
|
100
|
+
// inside a project scaffolded by this plugin.
|
|
101
|
+
//
|
|
102
|
+
// Returns true → safe to proceed
|
|
103
|
+
// Returns false → caller should return early
|
|
104
|
+
// ─────────────────────────────────────────────────────────────
|
|
105
|
+
export function checkCompat(chalk, command) {
|
|
106
|
+
const root = process.cwd();
|
|
107
|
+
const cfg = readConfig(root);
|
|
108
|
+
|
|
109
|
+
// cfg === null → file missing entirely
|
|
110
|
+
// cfg === {} → file exists but has no entry for this plugin yet
|
|
111
|
+
if (cfg === null || !cfg.pluginVersion) {
|
|
112
|
+
console.log();
|
|
113
|
+
console.log(chalk.yellow(" ⚠ No .jdm-config.json entry found for this plugin."));
|
|
114
|
+
console.log(chalk.gray(` This may not be a jdm-${PLUGIN_NAME} project,`));
|
|
115
|
+
console.log(chalk.gray(" or it was created before config tracking was introduced."));
|
|
116
|
+
const globalRange = COMPAT.global ?? null;
|
|
117
|
+
if (globalRange) {
|
|
118
|
+
console.log(chalk.gray(` Expected a project created with plugin ${chalk.white(globalRange)}.`));
|
|
119
|
+
}
|
|
120
|
+
console.log(chalk.gray(" Proceeding anyway — things may not work as expected.\n"));
|
|
121
|
+
return true;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// ── Version range check ───────────────────────────────────
|
|
125
|
+
const projectVer = cfg.pluginVersion;
|
|
126
|
+
const range = COMPAT.commands?.[command] ?? COMPAT.global ?? null;
|
|
127
|
+
if (!range) return true;
|
|
128
|
+
|
|
129
|
+
if (!satisfies(projectVer, range)) {
|
|
130
|
+
const cmdLabel = COMPAT.commands?.[command] ? `"${command}"` : "this plugin";
|
|
131
|
+
console.log();
|
|
132
|
+
console.log(chalk.red(` ✖ Compatibility error for command: ${chalk.bold(command)}`));
|
|
133
|
+
console.log(
|
|
134
|
+
chalk.gray(" Project was created with plugin version ") +
|
|
135
|
+
chalk.cyan(projectVer) +
|
|
136
|
+
chalk.gray(",")
|
|
137
|
+
);
|
|
138
|
+
console.log(
|
|
139
|
+
chalk.gray(` but ${cmdLabel} requires `) +
|
|
140
|
+
chalk.white(range) +
|
|
141
|
+
chalk.gray(".")
|
|
142
|
+
);
|
|
143
|
+
console.log();
|
|
144
|
+
console.log(chalk.yellow(" Tip: ") + chalk.gray("Re-scaffold with ") + chalk.white(`jdm-cli ${PLUGIN_NAME} create`));
|
|
145
|
+
console.log(chalk.gray(" or update the plugin to a version that supports your project.\n"));
|
|
146
|
+
return false;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
return true;
|
|
150
|
+
}
|
package/lib/index.js
ADDED
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
// ─────────────────────────────────────────────────────────────
|
|
2
|
+
// jdm-plugin-template — lib/index.js
|
|
3
|
+
//
|
|
4
|
+
// Main entry point for the plugin.
|
|
5
|
+
// jdm-cli imports this file and calls run(command, args, chalk, rl).
|
|
6
|
+
//
|
|
7
|
+
// Required exports:
|
|
8
|
+
// namespace (string) — the CLI prefix: jdm <namespace> <command>
|
|
9
|
+
// commands (object) — map of command name → handler function
|
|
10
|
+
// run (fn) — dispatcher called by jdm-cli
|
|
11
|
+
// showDesign (fn) — prints the help screen
|
|
12
|
+
//
|
|
13
|
+
// To add a new command:
|
|
14
|
+
// 1. Create lib/commands/my-command.js with a default export
|
|
15
|
+
// 2. Import it here
|
|
16
|
+
// 3. Add it to the `commands` map
|
|
17
|
+
// 4. Add it to showDesign()
|
|
18
|
+
// 5. Add it to the jdmPlugin.commands array in package.json
|
|
19
|
+
// ─────────────────────────────────────────────────────────────
|
|
20
|
+
|
|
21
|
+
import create from "./commands/create.js";
|
|
22
|
+
import dev from "./commands/dev.js";
|
|
23
|
+
import build from "./commands/build.js";
|
|
24
|
+
import clean from "./commands/clean.js";
|
|
25
|
+
import install from "./commands/install.js";
|
|
26
|
+
|
|
27
|
+
// ── Namespace ─────────────────────────────────────────────────
|
|
28
|
+
// This is the prefix used in the CLI: jdm <namespace> <command>
|
|
29
|
+
// Change this to match your plugin's jdmPlugin.namespace in package.json
|
|
30
|
+
export const namespace = "plugin-template"; // ← CHANGE THIS
|
|
31
|
+
|
|
32
|
+
// ── Command map ───────────────────────────────────────────────
|
|
33
|
+
// Keys are the exact command strings the user types.
|
|
34
|
+
// Values are the imported handler functions.
|
|
35
|
+
//
|
|
36
|
+
// Commands that need interactive prompts receive (chalk, rl, args).
|
|
37
|
+
// Commands that don't need prompts receive (chalk, args).
|
|
38
|
+
// See the run() dispatcher below for how this is handled.
|
|
39
|
+
export const commands = {
|
|
40
|
+
create,
|
|
41
|
+
dev,
|
|
42
|
+
build,
|
|
43
|
+
clean,
|
|
44
|
+
install,
|
|
45
|
+
|
|
46
|
+
// ── Add your own commands here ────────────────────────────
|
|
47
|
+
// "my-command": myCommand,
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
// ── Commands that need the readline interface (rl) ────────────
|
|
51
|
+
// List any command names that call ask(rl, ...) for user input.
|
|
52
|
+
const INTERACTIVE_COMMANDS = ["create"];
|
|
53
|
+
|
|
54
|
+
// ─────────────────────────────────────────────────────────────
|
|
55
|
+
// run() — called by jdm-cli for every invocation
|
|
56
|
+
//
|
|
57
|
+
// command (string) — e.g. "create", "dev", "build"
|
|
58
|
+
// args (string[]) — remaining CLI args / flags
|
|
59
|
+
// chalk (object) — chalk instance from jdm-cli
|
|
60
|
+
// rl (object) — readline interface from jdm-cli
|
|
61
|
+
// ─────────────────────────────────────────────────────────────
|
|
62
|
+
export async function run(command, args, chalk, rl) {
|
|
63
|
+
|
|
64
|
+
// ── Help shortcut ─────────────────────────────────────────
|
|
65
|
+
if (command === "help" || command === "--help" || command === "-h" || !command) {
|
|
66
|
+
return showDesign(chalk);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// ── Lookup ────────────────────────────────────────────────
|
|
70
|
+
const fn = commands[command];
|
|
71
|
+
if (!fn) {
|
|
72
|
+
console.log(chalk.red(`\n ✖ Unknown command: "${command}"`));
|
|
73
|
+
console.log(chalk.gray(` Available: ${Object.keys(commands).join(", ")}`));
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// ── Dispatch ──────────────────────────────────────────────
|
|
78
|
+
if (INTERACTIVE_COMMANDS.includes(command)) {
|
|
79
|
+
return fn(chalk, rl, args);
|
|
80
|
+
}
|
|
81
|
+
return fn(chalk, args);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// ─────────────────────────────────────────────────────────────
|
|
85
|
+
// showDesign() — the help / overview screen
|
|
86
|
+
//
|
|
87
|
+
// Customize this to describe your plugin's commands.
|
|
88
|
+
// ─────────────────────────────────────────────────────────────
|
|
89
|
+
export async function showDesign(chalk) {
|
|
90
|
+
console.log(chalk.cyan(`\n ⚡ ${namespace} Plugin`));
|
|
91
|
+
// ← Replace the tagline with your plugin's description
|
|
92
|
+
console.log(chalk.gray(" Your plugin tagline goes here."));
|
|
93
|
+
console.log(chalk.gray(" Available commands:\n"));
|
|
94
|
+
|
|
95
|
+
// ── List your commands here ───────────────────────────────
|
|
96
|
+
// Format: command name (padded) + short description
|
|
97
|
+
console.log(` ${chalk.green("create")} ${chalk.dim("Scaffold a new project")}`);
|
|
98
|
+
console.log(` ${chalk.green("dev")} ${chalk.dim("Start development environment")}`);
|
|
99
|
+
console.log(` ${chalk.green("build")} ${chalk.dim("Compile / package the project")}`);
|
|
100
|
+
console.log(` ${chalk.green("clean")} ${chalk.dim("Remove build artifacts")}`);
|
|
101
|
+
console.log(` ${chalk.green("install")} ${chalk.dim("Install dependencies")}`);
|
|
102
|
+
|
|
103
|
+
// ── Add your own commands to the list above ───────────────
|
|
104
|
+
// console.log(` ${chalk.green("my-command")} ${chalk.dim("Does something useful")}`);
|
|
105
|
+
|
|
106
|
+
console.log();
|
|
107
|
+
}
|
package/lib/logger.js
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
// ─────────────────────────────────────────────────────────────
|
|
2
|
+
// jdm-plugin-template — lib/logger.js
|
|
3
|
+
//
|
|
4
|
+
// Shared logging helpers used across all commands.
|
|
5
|
+
// Import what you need:
|
|
6
|
+
// import { ok, fail, info, warn, step, header } from "../logger.js";
|
|
7
|
+
//
|
|
8
|
+
// All functions take (chalk, msg) so chalk stays injectable
|
|
9
|
+
// and the plugin doesn't need to manage a global chalk ref.
|
|
10
|
+
// ─────────────────────────────────────────────────────────────
|
|
11
|
+
|
|
12
|
+
// ── Pretty-print helpers ──────────────────────────────────────
|
|
13
|
+
export function ok(chalk, msg) { console.log(chalk.green(" ✔ ") + msg); }
|
|
14
|
+
export function fail(chalk, msg) { console.log(chalk.red(" ✖ ") + msg); }
|
|
15
|
+
export function warn(chalk, msg) { console.log(chalk.yellow(" ⚠ ") + msg); }
|
|
16
|
+
export function info(chalk, msg) { console.log(chalk.gray(" · ") + msg); }
|
|
17
|
+
|
|
18
|
+
// ── Step counter line ─────────────────────────────────────────
|
|
19
|
+
// step(chalk, 1, 3, "Doing the thing") → [1/3] Doing the thing
|
|
20
|
+
export function step(chalk, n, total, label) {
|
|
21
|
+
console.log();
|
|
22
|
+
console.log(chalk.cyan(` [${n}/${total}]`) + " " + chalk.bold(label));
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// ── Command header ────────────────────────────────────────────
|
|
26
|
+
// header(chalk, "create") → jdm / your-namespace / create
|
|
27
|
+
//
|
|
28
|
+
// Customize the namespace string to match your plugin.
|
|
29
|
+
export function header(chalk, command = "") {
|
|
30
|
+
const ns = "plugin-template"; // ← replace with your namespace
|
|
31
|
+
console.log();
|
|
32
|
+
console.log(
|
|
33
|
+
chalk.cyan(" jdm") +
|
|
34
|
+
chalk.gray(" / ") +
|
|
35
|
+
chalk.white(ns) +
|
|
36
|
+
(command ? chalk.gray(" / ") + chalk.bold(command) : "")
|
|
37
|
+
);
|
|
38
|
+
console.log(chalk.gray(" ─────────────────────────────────────"));
|
|
39
|
+
console.log();
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// ── Section divider ───────────────────────────────────────────
|
|
43
|
+
export function divider(chalk) {
|
|
44
|
+
console.log(chalk.gray(" ─────────────────────────────────────"));
|
|
45
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "jdm-plugin-template",
|
|
3
|
+
"type": "module",
|
|
4
|
+
"version": "1.0.0",
|
|
5
|
+
"description": "A starter template for building jdm-cli plugins",
|
|
6
|
+
"main": "./lib/index.js",
|
|
7
|
+
"scripts": {
|
|
8
|
+
"test": "echo \"No tests yet\""
|
|
9
|
+
},
|
|
10
|
+
"dependencies": {
|
|
11
|
+
"chalk": "^5.3.0",
|
|
12
|
+
"ora": "^9.4.0",
|
|
13
|
+
"string-width": "^8.2.0",
|
|
14
|
+
"strip-ansi": "^7.2.0"
|
|
15
|
+
},
|
|
16
|
+
"keywords": [
|
|
17
|
+
"jdm",
|
|
18
|
+
"plugin",
|
|
19
|
+
"template"
|
|
20
|
+
],
|
|
21
|
+
"author": "JDM-Github",
|
|
22
|
+
"license": "MIT",
|
|
23
|
+
"jdmPlugin": {
|
|
24
|
+
"namespace": "plugin-template",
|
|
25
|
+
"description": "Your plugin description goes here",
|
|
26
|
+
"commands": [
|
|
27
|
+
{
|
|
28
|
+
"name": "create",
|
|
29
|
+
"description": "Scaffold a new project",
|
|
30
|
+
"fields": [
|
|
31
|
+
{
|
|
32
|
+
"key": "name",
|
|
33
|
+
"label": "Project Name",
|
|
34
|
+
"flag": "--name",
|
|
35
|
+
"type": "text",
|
|
36
|
+
"placeholder": "my-project",
|
|
37
|
+
"required": true
|
|
38
|
+
},
|
|
39
|
+
{
|
|
40
|
+
"key": "install",
|
|
41
|
+
"label": "Install Dependencies",
|
|
42
|
+
"flag": "--install",
|
|
43
|
+
"type": "boolean",
|
|
44
|
+
"default": false
|
|
45
|
+
}
|
|
46
|
+
]
|
|
47
|
+
},
|
|
48
|
+
{
|
|
49
|
+
"name": "dev",
|
|
50
|
+
"description": "Start development environment",
|
|
51
|
+
"fields": []
|
|
52
|
+
},
|
|
53
|
+
{
|
|
54
|
+
"name": "build",
|
|
55
|
+
"description": "Compile / package the project",
|
|
56
|
+
"fields": [
|
|
57
|
+
{
|
|
58
|
+
"key": "frontend",
|
|
59
|
+
"label": "Build Frontend",
|
|
60
|
+
"flag": "--frontend",
|
|
61
|
+
"type": "boolean",
|
|
62
|
+
"default": false,
|
|
63
|
+
"description": "Build only the frontend"
|
|
64
|
+
},
|
|
65
|
+
{
|
|
66
|
+
"key": "backend",
|
|
67
|
+
"label": "Build Backend",
|
|
68
|
+
"flag": "--backend",
|
|
69
|
+
"type": "boolean",
|
|
70
|
+
"default": false,
|
|
71
|
+
"description": "Build only the backend"
|
|
72
|
+
},
|
|
73
|
+
{
|
|
74
|
+
"key": "full",
|
|
75
|
+
"label": "Full Build",
|
|
76
|
+
"flag": "--full",
|
|
77
|
+
"type": "boolean",
|
|
78
|
+
"default": false,
|
|
79
|
+
"description": "Build everything (frontend + backend)"
|
|
80
|
+
}
|
|
81
|
+
]
|
|
82
|
+
},
|
|
83
|
+
{
|
|
84
|
+
"name": "clean",
|
|
85
|
+
"description": "Remove build artifacts",
|
|
86
|
+
"fields": [
|
|
87
|
+
{
|
|
88
|
+
"key": "dry",
|
|
89
|
+
"label": "Dry Run",
|
|
90
|
+
"flag": "--dry",
|
|
91
|
+
"type": "boolean",
|
|
92
|
+
"default": false,
|
|
93
|
+
"description": "Preview what would be deleted without deleting"
|
|
94
|
+
}
|
|
95
|
+
]
|
|
96
|
+
},
|
|
97
|
+
{
|
|
98
|
+
"name": "install",
|
|
99
|
+
"description": "Install dependencies",
|
|
100
|
+
"fields": []
|
|
101
|
+
}
|
|
102
|
+
]
|
|
103
|
+
}
|
|
104
|
+
}
|