install-claude-workflow-v2 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 +40 -0
- package/bin/cli.js +32 -0
- package/lib/installer.js +194 -0
- package/package.json +37 -0
package/README.md
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# install-claude-workflow-v2
|
|
2
|
+
|
|
3
|
+
Install the Claude Code workflow plugin with a single command.
|
|
4
|
+
|
|
5
|
+
## Usage
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npx install-claude-workflow-v2
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
That's it. Run `claude` to start.
|
|
12
|
+
|
|
13
|
+
## What Gets Installed
|
|
14
|
+
|
|
15
|
+
```
|
|
16
|
+
.claude/
|
|
17
|
+
├── agents/ # 7 specialized subagents
|
|
18
|
+
├── skills/ # 10 knowledge domains
|
|
19
|
+
├── commands/ # 17 slash commands
|
|
20
|
+
└── hooks/ # 9 automation scripts
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
## Features
|
|
24
|
+
|
|
25
|
+
- **Additive install** - Preserves your existing `.claude/` files
|
|
26
|
+
- **No git history** - Downloads clean tarball, not full repo
|
|
27
|
+
- **Single command** - No configuration needed
|
|
28
|
+
|
|
29
|
+
## Requirements
|
|
30
|
+
|
|
31
|
+
- Node.js 16+
|
|
32
|
+
- [Claude Code CLI](https://claude.ai/code)
|
|
33
|
+
|
|
34
|
+
## Repository
|
|
35
|
+
|
|
36
|
+
https://github.com/CloudAI-X/claude-workflow
|
|
37
|
+
|
|
38
|
+
## License
|
|
39
|
+
|
|
40
|
+
MIT
|
package/bin/cli.js
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
const { install } = require("../lib/installer");
|
|
4
|
+
|
|
5
|
+
const REPO = "CloudAI-X/claude-workflow";
|
|
6
|
+
const NAME = "claude-workflow-v2";
|
|
7
|
+
|
|
8
|
+
// Parse command line arguments
|
|
9
|
+
const args = process.argv.slice(2);
|
|
10
|
+
|
|
11
|
+
// Show help
|
|
12
|
+
if (args.includes("--help") || args.includes("-h")) {
|
|
13
|
+
console.log(`
|
|
14
|
+
install-claude-workflow-v2 - Install Claude Code workflow plugin
|
|
15
|
+
|
|
16
|
+
Usage:
|
|
17
|
+
npx install-claude-workflow-v2
|
|
18
|
+
|
|
19
|
+
Installs agents, skills, commands, and hooks to .claude/ in current directory.
|
|
20
|
+
|
|
21
|
+
After install, run 'claude' to start.
|
|
22
|
+
|
|
23
|
+
Repository: https://github.com/CloudAI-X/claude-workflow
|
|
24
|
+
`);
|
|
25
|
+
process.exit(0);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// Run the installer
|
|
29
|
+
install(REPO, NAME).catch((err) => {
|
|
30
|
+
console.error(`\n❌ ${err.message}`);
|
|
31
|
+
process.exit(1);
|
|
32
|
+
});
|
package/lib/installer.js
ADDED
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
const degit = require("degit");
|
|
2
|
+
const path = require("path");
|
|
3
|
+
const fs = require("fs");
|
|
4
|
+
|
|
5
|
+
const TIMEOUT_MS = 60000; // 60 second timeout for downloads
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Validate GitHub repository format
|
|
9
|
+
* @param {string} repo - Repository string to validate
|
|
10
|
+
* @returns {boolean} True if valid format
|
|
11
|
+
*/
|
|
12
|
+
function isValidRepoFormat(repo) {
|
|
13
|
+
// Must be "owner/repo" format with safe characters only
|
|
14
|
+
// Allows: letters, numbers, hyphens, underscores, dots
|
|
15
|
+
return /^[\w.-]+\/[\w.-]+$/.test(repo);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Install a Claude Code plugin from GitHub (additive merge)
|
|
20
|
+
* @param {string} repo - GitHub repo in format "owner/repo"
|
|
21
|
+
* @param {string} name - Plugin name for display
|
|
22
|
+
* @throws {Error} If plugin already installed or download fails
|
|
23
|
+
*/
|
|
24
|
+
async function install(repo, name) {
|
|
25
|
+
// HIGH-1 FIX: Validate repository format to prevent path traversal
|
|
26
|
+
if (!isValidRepoFormat(repo)) {
|
|
27
|
+
throw new Error(`Invalid repository format: "${repo}". Expected: owner/repo`);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const cwd = process.cwd();
|
|
31
|
+
const target = path.join(cwd, ".claude");
|
|
32
|
+
// MEDIUM-1 FIX: Use PID-based temp directory to prevent race conditions
|
|
33
|
+
const tempTarget = path.join(cwd, `.claude-temp-install-${process.pid}`);
|
|
34
|
+
|
|
35
|
+
// Check if plugin manifest already exists
|
|
36
|
+
if (fs.existsSync(path.join(target, ".claude-plugin"))) {
|
|
37
|
+
throw new Error(
|
|
38
|
+
`Plugin already installed at .claude/\nTo update: rm -rf .claude/.claude-plugin && npx add-skill ${name}`
|
|
39
|
+
);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const hasExisting = fs.existsSync(target);
|
|
43
|
+
if (hasExisting) {
|
|
44
|
+
console.log(`\n📁 Found existing .claude/ - will merge (nothing deleted)`);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
console.log(`\n📦 Installing ${name}...`);
|
|
48
|
+
|
|
49
|
+
// Clean up any previous failed install
|
|
50
|
+
if (fs.existsSync(tempTarget)) {
|
|
51
|
+
fs.rmSync(tempTarget, { recursive: true, force: true });
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// Download to temp directory with timeout and error handling
|
|
55
|
+
const emitter = degit(repo, {
|
|
56
|
+
cache: false,
|
|
57
|
+
force: true,
|
|
58
|
+
verbose: false,
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
try {
|
|
62
|
+
const timeoutPromise = new Promise((_, reject) =>
|
|
63
|
+
setTimeout(() => reject(new Error("Download timed out after 60 seconds")), TIMEOUT_MS)
|
|
64
|
+
);
|
|
65
|
+
await Promise.race([emitter.clone(tempTarget), timeoutPromise]);
|
|
66
|
+
} catch (err) {
|
|
67
|
+
// Clean up temp on failure
|
|
68
|
+
if (fs.existsSync(tempTarget)) {
|
|
69
|
+
fs.rmSync(tempTarget, { recursive: true, force: true });
|
|
70
|
+
}
|
|
71
|
+
// MEDIUM-3 FIX: Enhanced error messages with actionable context
|
|
72
|
+
if (err.code === "ENOTFOUND" || err.message.includes("getaddrinfo")) {
|
|
73
|
+
throw new Error(
|
|
74
|
+
`Network error accessing github.com/${repo}\n` +
|
|
75
|
+
` Check: internet connection, firewall, proxy settings`
|
|
76
|
+
);
|
|
77
|
+
}
|
|
78
|
+
if (err.message.includes("could not find commit")) {
|
|
79
|
+
throw new Error(
|
|
80
|
+
`Repository not found: github.com/${repo}\n` +
|
|
81
|
+
` Verify the repository exists and is public`
|
|
82
|
+
);
|
|
83
|
+
}
|
|
84
|
+
throw new Error(`Download failed for ${repo}: ${err.message}`);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Create target if it doesn't exist
|
|
88
|
+
if (!fs.existsSync(target)) {
|
|
89
|
+
fs.mkdirSync(target, { recursive: true });
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// Merge: copy from temp to target, preserving existing files
|
|
93
|
+
const stats = { added: 0, skipped: 0 };
|
|
94
|
+
mergeDirectories(tempTarget, target, stats);
|
|
95
|
+
|
|
96
|
+
// Clean up temp
|
|
97
|
+
fs.rmSync(tempTarget, { recursive: true, force: true });
|
|
98
|
+
|
|
99
|
+
// Count installed components
|
|
100
|
+
const components = {
|
|
101
|
+
agents: countFiles(path.join(target, "agents"), ".md"),
|
|
102
|
+
skills: countDirs(path.join(target, "skills")),
|
|
103
|
+
commands: countFiles(path.join(target, "commands"), ".md"),
|
|
104
|
+
hooks: countFiles(path.join(target, "hooks", "scripts"), ".py"),
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
console.log(`\n✅ Installed to .claude/\n`);
|
|
108
|
+
console.log(
|
|
109
|
+
` ${components.agents} agents | ${components.skills} skills | ${components.commands} commands | ${components.hooks} hooks`
|
|
110
|
+
);
|
|
111
|
+
if (stats.skipped > 0) {
|
|
112
|
+
console.log(` (${stats.skipped} existing files preserved)`);
|
|
113
|
+
}
|
|
114
|
+
console.log(`\n Run 'claude' to start.`);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Recursively merge source directory into target, preserving existing files
|
|
119
|
+
* Handles regular files, directories, and symlinks
|
|
120
|
+
*/
|
|
121
|
+
function mergeDirectories(src, dest, stats) {
|
|
122
|
+
const entries = fs.readdirSync(src, { withFileTypes: true });
|
|
123
|
+
|
|
124
|
+
for (const entry of entries) {
|
|
125
|
+
const srcPath = path.join(src, entry.name);
|
|
126
|
+
const destPath = path.join(dest, entry.name);
|
|
127
|
+
|
|
128
|
+
if (entry.isSymbolicLink()) {
|
|
129
|
+
// HIGH-2 FIX: Validate symlink targets to prevent path traversal attacks
|
|
130
|
+
if (!fs.existsSync(destPath)) {
|
|
131
|
+
const linkTarget = fs.readlinkSync(srcPath);
|
|
132
|
+
const resolvedTarget = path.resolve(path.dirname(destPath), linkTarget);
|
|
133
|
+
const resolvedDest = path.resolve(dest);
|
|
134
|
+
|
|
135
|
+
// Only allow symlinks that point inside the .claude directory
|
|
136
|
+
if (!resolvedTarget.startsWith(resolvedDest + path.sep) && resolvedTarget !== resolvedDest) {
|
|
137
|
+
// Unsafe symlink - skip it with warning
|
|
138
|
+
console.warn(` ⚠️ Skipping unsafe symlink: ${entry.name} -> ${linkTarget}`);
|
|
139
|
+
stats.skipped++;
|
|
140
|
+
continue;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
fs.symlinkSync(linkTarget, destPath);
|
|
144
|
+
stats.added++;
|
|
145
|
+
} else {
|
|
146
|
+
stats.skipped++;
|
|
147
|
+
}
|
|
148
|
+
} else if (entry.isDirectory()) {
|
|
149
|
+
// Create directory if it doesn't exist
|
|
150
|
+
if (!fs.existsSync(destPath)) {
|
|
151
|
+
fs.mkdirSync(destPath, { recursive: true });
|
|
152
|
+
}
|
|
153
|
+
// Recursively merge
|
|
154
|
+
mergeDirectories(srcPath, destPath, stats);
|
|
155
|
+
} else if (entry.isFile()) {
|
|
156
|
+
// Only copy file if it doesn't exist in destination
|
|
157
|
+
if (!fs.existsSync(destPath)) {
|
|
158
|
+
fs.copyFileSync(srcPath, destPath);
|
|
159
|
+
stats.added++;
|
|
160
|
+
} else {
|
|
161
|
+
stats.skipped++;
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
// Skip other types (sockets, fifos, etc.)
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Count files with a specific extension in a directory
|
|
170
|
+
*/
|
|
171
|
+
function countFiles(dir, ext) {
|
|
172
|
+
try {
|
|
173
|
+
if (!fs.existsSync(dir)) return 0;
|
|
174
|
+
return fs.readdirSync(dir).filter((f) => f.endsWith(ext)).length;
|
|
175
|
+
} catch {
|
|
176
|
+
return 0;
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* Count subdirectories in a directory
|
|
182
|
+
*/
|
|
183
|
+
function countDirs(dir) {
|
|
184
|
+
try {
|
|
185
|
+
if (!fs.existsSync(dir)) return 0;
|
|
186
|
+
return fs
|
|
187
|
+
.readdirSync(dir, { withFileTypes: true })
|
|
188
|
+
.filter((d) => d.isDirectory()).length;
|
|
189
|
+
} catch {
|
|
190
|
+
return 0;
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
module.exports = { install };
|
package/package.json
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "install-claude-workflow-v2",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"type": "commonjs",
|
|
5
|
+
"description": "Install Claude Code skills and plugins via npx",
|
|
6
|
+
"bin": {
|
|
7
|
+
"install-claude-workflow-v2": "./bin/cli.js"
|
|
8
|
+
},
|
|
9
|
+
"files": [
|
|
10
|
+
"bin",
|
|
11
|
+
"lib"
|
|
12
|
+
],
|
|
13
|
+
"keywords": [
|
|
14
|
+
"claude",
|
|
15
|
+
"claude-code",
|
|
16
|
+
"skills",
|
|
17
|
+
"plugin",
|
|
18
|
+
"ai",
|
|
19
|
+
"anthropic"
|
|
20
|
+
],
|
|
21
|
+
"author": "CloudAI-X",
|
|
22
|
+
"license": "MIT",
|
|
23
|
+
"repository": {
|
|
24
|
+
"type": "git",
|
|
25
|
+
"url": "git+https://github.com/CloudAI-X/claude-workflow.git"
|
|
26
|
+
},
|
|
27
|
+
"bugs": {
|
|
28
|
+
"url": "https://github.com/CloudAI-X/claude-workflow/issues"
|
|
29
|
+
},
|
|
30
|
+
"homepage": "https://github.com/CloudAI-X/claude-workflow#readme",
|
|
31
|
+
"engines": {
|
|
32
|
+
"node": ">=16.0.0"
|
|
33
|
+
},
|
|
34
|
+
"dependencies": {
|
|
35
|
+
"degit": "^2.8.4"
|
|
36
|
+
}
|
|
37
|
+
}
|