1use std::fs;
5use std::path::Path;
6
7pub const PLATFORMS: &[&str] = &[
9 "claude-code",
10 "cursor",
11 "pawan",
12 "codex",
13 "opencode",
14 "gemini",
15];
16
17pub fn init_plugin(dir: &str, name: &str, description: &str, author: &str) -> anyhow::Result<()> {
19 let base = Path::new(dir);
20 fs::create_dir_all(base)?;
21
22 eprintln!("Initializing dstack plugin '{}' in {}", name, dir);
23
24 write_package_json(base, name, description, author)?;
26 write_claude_md(base, name)?;
27 write_agents_md(base, name)?;
28 write_changelog(base, name)?;
29
30 write_claude_plugin(base, name, description, author)?;
32 write_cursor_plugin(base, name, description, author)?;
33 write_pawan_plugin(base, name, description)?;
34 write_gemini_extension(base, name, description)?;
35 write_gemini_md(base)?;
36
37 write_codex_install(base, name)?;
39 write_opencode_install(base, name)?;
40 write_pawan_install(base, name)?;
41 write_opencode_shim(base, name)?;
42
43 write_hooks(base)?;
45
46 write_skeleton_skill(base, name)?;
48
49 eprintln!("Created plugin scaffold with {} platform configs", PLATFORMS.len());
50 eprintln!("\nGenerated:");
51 eprintln!(" .claude-plugin/plugin.json (Claude Code)");
52 eprintln!(" .cursor-plugin/plugin.json (Cursor)");
53 eprintln!(" .pawan/plugin.toml (Pawan)");
54 eprintln!(" .codex/INSTALL.md (Codex)");
55 eprintln!(" .opencode/INSTALL.md (OpenCode)");
56 eprintln!(" gemini-extension.json (Gemini CLI)");
57 eprintln!(" hooks/hooks.json (Claude Code hooks)");
58 eprintln!(" hooks/hooks-cursor.json (Cursor hooks)");
59 eprintln!(" hooks/session-start (session bootstrap)");
60 eprintln!(" skills/using-{}/SKILL.md (starter skill)", name);
61
62 Ok(())
63}
64
65pub fn init_dry_run(dir: &str, name: &str) {
67 eprintln!("Would generate dstack plugin '{}' in {}:", name, dir);
68 let files = [
69 "package.json",
70 "CLAUDE.md",
71 "AGENTS.md",
72 "GEMINI.md",
73 "CHANGELOG.md",
74 ".claude-plugin/plugin.json",
75 ".cursor-plugin/plugin.json",
76 ".pawan/plugin.toml",
77 ".pawan/INSTALL.md",
78 ".codex/INSTALL.md",
79 ".opencode/INSTALL.md",
80 ".opencode/plugins/{name}.js",
81 "gemini-extension.json",
82 "hooks/hooks.json",
83 "hooks/hooks-cursor.json",
84 "hooks/session-start",
85 "skills/using-{name}/SKILL.md",
86 ];
87 for f in &files {
88 eprintln!(" {}", f.replace("{name}", name));
89 }
90}
91
92fn write_package_json(base: &Path, name: &str, desc: &str, author: &str) -> anyhow::Result<()> {
95 let content = format!(
96 r#"{{
97 "name": "{}",
98 "version": "0.1.0",
99 "description": "{}",
100 "author": "{}",
101 "license": "MIT",
102 "homepage": "https://github.com/{}/{}",
103 "repository": {{
104 "type": "git",
105 "url": "https://github.com/{}/{}"
106 }},
107 "keywords": ["dstack-plugin"]
108}}
109"#,
110 name, desc, author, author, name, author, name
111 );
112 fs::write(base.join("package.json"), content)?;
113 Ok(())
114}
115
116fn write_claude_plugin(base: &Path, name: &str, desc: &str, author: &str) -> anyhow::Result<()> {
117 let dir = base.join(".claude-plugin");
118 fs::create_dir_all(&dir)?;
119 let content = format!(
120 r#"{{
121 "name": "{}",
122 "description": "{}",
123 "version": "0.1.0",
124 "author": {{
125 "name": "{}"
126 }},
127 "license": "MIT",
128 "keywords": ["dstack-plugin"]
129}}
130"#,
131 name, desc, author
132 );
133 fs::write(dir.join("plugin.json"), content)?;
134 Ok(())
135}
136
137fn write_cursor_plugin(base: &Path, name: &str, desc: &str, author: &str) -> anyhow::Result<()> {
138 let dir = base.join(".cursor-plugin");
139 fs::create_dir_all(&dir)?;
140 let content = format!(
141 r#"{{
142 "name": "{}",
143 "displayName": "{}",
144 "description": "{}",
145 "version": "0.1.0",
146 "author": {{
147 "name": "{}"
148 }},
149 "license": "MIT",
150 "skills": "./skills/",
151 "commands": "./commands/",
152 "hooks": "./hooks/hooks-cursor.json"
153}}
154"#,
155 name, name, desc, author
156 );
157 fs::write(dir.join("plugin.json"), content)?;
158 Ok(())
159}
160
161fn write_pawan_plugin(base: &Path, name: &str, desc: &str) -> anyhow::Result<()> {
162 let dir = base.join(".pawan");
163 fs::create_dir_all(&dir)?;
164 let content = format!(
165 r#"# {} plugin configuration for Pawan coding agent
166
167[plugin]
168name = "{}"
169version = "0.1.0"
170description = "{}"
171
172[skills]
173path = "../skills"
174
175[commands]
176path = "../commands"
177
178[hooks]
179session_start = "../hooks/session-start"
180"#,
181 name, name, desc
182 );
183 fs::write(dir.join("plugin.toml"), content)?;
184 Ok(())
185}
186
187fn write_gemini_extension(base: &Path, name: &str, desc: &str) -> anyhow::Result<()> {
188 let content = format!(
189 r#"{{
190 "name": "{}",
191 "description": "{}",
192 "version": "0.1.0",
193 "contextFileName": "GEMINI.md"
194}}
195"#,
196 name, desc
197 );
198 fs::write(base.join("gemini-extension.json"), content)?;
199 Ok(())
200}
201
202fn write_gemini_md(base: &Path) -> anyhow::Result<()> {
203 fs::write(
204 base.join("GEMINI.md"),
205 "@./skills/using-dstack/SKILL.md\n",
206 )?;
207 Ok(())
208}
209
210fn write_claude_md(base: &Path, name: &str) -> anyhow::Result<()> {
211 let content = format!(
212 "# {} Plugin\n\nThis plugin provides skills and hooks for AI-assisted development.\n\n## Available Skills\n\nRun `/skill list` to see available skills.\n",
213 name
214 );
215 fs::write(base.join("CLAUDE.md"), content)?;
216 Ok(())
217}
218
219fn write_agents_md(base: &Path, name: &str) -> anyhow::Result<()> {
220 let content = format!(
221 "# {} Plugin\n\nThis plugin provides skills and hooks for AI-assisted development.\n\n## Available Skills\n\nRun `/skill list` to see available skills.\n",
222 name
223 );
224 fs::write(base.join("AGENTS.md"), content)?;
225 Ok(())
226}
227
228fn write_changelog(base: &Path, name: &str) -> anyhow::Result<()> {
229 let content = format!(
230 "# Changelog\n\n## v0.1.0\n\n- Initial {} plugin scaffold\n- Generated by `dstack init`\n",
231 name
232 );
233 fs::write(base.join("CHANGELOG.md"), content)?;
234 Ok(())
235}
236
237fn write_codex_install(base: &Path, name: &str) -> anyhow::Result<()> {
238 let dir = base.join(".codex");
239 fs::create_dir_all(&dir)?;
240 let content = format!(
241 r#"# Installing {} for Codex
242
243## Installation
244
2451. **Clone the repository:**
246 ```bash
247 git clone https://github.com/OWNER/{}.git ~/.codex/{}
248 ```
249
2502. **Create the skills symlink:**
251 ```bash
252 mkdir -p ~/.agents/skills
253 ln -s ~/.codex/{}/skills ~/.agents/skills/{}
254 ```
255
2563. **Restart Codex** to discover the skills.
257
258## Updating
259
260```bash
261cd ~/.codex/{} && git pull
262```
263"#,
264 name, name, name, name, name, name
265 );
266 fs::write(dir.join("INSTALL.md"), content)?;
267 Ok(())
268}
269
270fn write_opencode_install(base: &Path, name: &str) -> anyhow::Result<()> {
271 let dir = base.join(".opencode");
272 fs::create_dir_all(&dir)?;
273 let content = format!(
274 r#"# Installing {} for OpenCode
275
276## Installation
277
278Add to the `plugin` array in your `opencode.json`:
279
280```json
281{{
282 "plugin": ["{}@git+https://github.com/OWNER/{}.git"]
283}}
284```
285
286Restart OpenCode. The plugin auto-installs and registers all skills.
287"#,
288 name, name, name
289 );
290 fs::write(dir.join("INSTALL.md"), content)?;
291 Ok(())
292}
293
294fn write_pawan_install(base: &Path, name: &str) -> anyhow::Result<()> {
295 let dir = base.join(".pawan");
296 fs::create_dir_all(&dir)?;
297 let content = format!(
298 r#"# Installing {} for Pawan
299
300## Installation
301
302```bash
303mkdir -p ~/.config/pawan/plugins
304git clone https://github.com/OWNER/{}.git ~/.config/pawan/plugins/{}
305```
306
307Or symlink:
308
309```bash
310ln -s /path/to/{}/plugin ~/.config/pawan/plugins/{}
311```
312
313Restart Pawan to discover skills.
314"#,
315 name, name, name, name, name
316 );
317 fs::write(dir.join("INSTALL.md"), content)?;
318 Ok(())
319}
320
321fn write_opencode_shim(base: &Path, name: &str) -> anyhow::Result<()> {
322 let dir = base.join(".opencode").join("plugins");
323 fs::create_dir_all(&dir)?;
324 let content = format!(
325 r#"// {} plugin for OpenCode.ai
326const path = require("path");
327const fs = require("fs");
328
329module.exports = {{
330 name: "{}",
331 version: "0.1.0",
332 init(context) {{
333 const skillsDir = path.join(path.dirname(__dirname), "..", "skills");
334 if (context.registerSkillsPath) {{
335 context.registerSkillsPath("{}", skillsDir);
336 }}
337 }},
338 toolMapping: {{
339 Bash: "bash", Read: "read", Edit: "edit",
340 Write: "write", Glob: "glob", Grep: "grep",
341 }},
342}};
343"#,
344 name, name, name
345 );
346 fs::write(dir.join(format!("{}.js", name)), content)?;
347 Ok(())
348}
349
350fn write_hooks(base: &Path) -> anyhow::Result<()> {
351 let dir = base.join("hooks");
352 fs::create_dir_all(&dir)?;
353
354 let hooks_json = r#"{
356 "hooks": {
357 "SessionStart": [
358 {
359 "matcher": "startup|clear|compact",
360 "hooks": [
361 {
362 "type": "command",
363 "command": "./hooks/session-start"
364 }
365 ]
366 }
367 ]
368 }
369}
370"#;
371 fs::write(dir.join("hooks.json"), hooks_json)?;
372
373 let cursor_hooks = r#"{
375 "version": 1,
376 "hooks": {
377 "sessionStart": [
378 {
379 "command": "./hooks/session-start"
380 }
381 ]
382 }
383}
384"#;
385 fs::write(dir.join("hooks-cursor.json"), cursor_hooks)?;
386
387 let session_start = r#"#!/bin/bash
389# Session bootstrap — loaded at conversation start
390echo '{"hookSpecificOutput": "dstack plugin loaded. Use /skill list to see available skills."}'
391"#;
392 fs::write(dir.join("session-start"), session_start)?;
393
394 #[cfg(unix)]
396 {
397 use std::os::unix::fs::PermissionsExt;
398 fs::set_permissions(dir.join("session-start"), fs::Permissions::from_mode(0o755))?;
399 }
400
401 Ok(())
402}
403
404fn write_skeleton_skill(base: &Path, name: &str) -> anyhow::Result<()> {
405 let skill_name = format!("using-{}", name);
406 let dir = base.join("skills").join(&skill_name);
407 fs::create_dir_all(&dir)?;
408 let content = format!(
409 r#"---
410name: using-{}
411description: Session orientation — available skills and commands
412---
413
414# Using {}
415
416This plugin provides skills for AI-assisted development.
417
418## Available Skills
419
420Use the Skill tool to invoke any skill by name.
421
422## Getting Started
423
424Start by describing what you want to accomplish. The plugin will suggest relevant skills.
425"#,
426 name, name
427 );
428 fs::write(dir.join("SKILL.md"), content)?;
429 Ok(())
430}