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 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
+ });
@@ -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
+ }