workspace-architect 2.2.1 → 2.2.3
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/assets-manifest.json +103 -1
- package/bin/cli-functions.js +490 -0
- package/bin/cli.js +4 -478
- package/package.json +13 -2
package/assets-manifest.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": "2.0.0",
|
|
3
|
-
"generatedAt": "2026-01-
|
|
3
|
+
"generatedAt": "2026-01-22T00:32:32.273Z",
|
|
4
4
|
"assets": {
|
|
5
5
|
"agents": {
|
|
6
6
|
"4.1-Beast": {
|
|
@@ -159,6 +159,24 @@
|
|
|
159
159
|
"title": "blueprint-mode",
|
|
160
160
|
"type": "agents"
|
|
161
161
|
},
|
|
162
|
+
"cast-imaging-impact-analysis": {
|
|
163
|
+
"path": "assets/agents/cast-imaging-impact-analysis.agent.md",
|
|
164
|
+
"description": "Specialized agent for comprehensive change impact assessment and risk analysis in software systems using CAST Imaging",
|
|
165
|
+
"title": "cast-imaging-impact-analysis",
|
|
166
|
+
"type": "agents"
|
|
167
|
+
},
|
|
168
|
+
"cast-imaging-software-discovery": {
|
|
169
|
+
"path": "assets/agents/cast-imaging-software-discovery.agent.md",
|
|
170
|
+
"description": "Specialized agent for comprehensive software application discovery and architectural mapping through static code analysis using CAST Imaging",
|
|
171
|
+
"title": "cast-imaging-software-discovery",
|
|
172
|
+
"type": "agents"
|
|
173
|
+
},
|
|
174
|
+
"cast-imaging-structural-quality-advisor": {
|
|
175
|
+
"path": "assets/agents/cast-imaging-structural-quality-advisor.agent.md",
|
|
176
|
+
"description": "Specialized agent for identifying, analyzing, and providing remediation guidance for code quality issues using CAST Imaging",
|
|
177
|
+
"title": "cast-imaging-structural-quality-advisor",
|
|
178
|
+
"type": "agents"
|
|
179
|
+
},
|
|
162
180
|
"clojure-interactive-programming": {
|
|
163
181
|
"path": "assets/agents/clojure-interactive-programming.agent.md",
|
|
164
182
|
"description": "Expert Clojure pair programmer with REPL-first methodology, architectural oversight, and interactive problem-solving. Enforces quality standards, prevents workarounds, and develops solutions incrementally through live REPL evaluation before file modifications.",
|
|
@@ -483,6 +501,12 @@
|
|
|
483
501
|
"title": "octopus-deploy-release-notes-mcp",
|
|
484
502
|
"type": "agents"
|
|
485
503
|
},
|
|
504
|
+
"openapi-to-application": {
|
|
505
|
+
"path": "assets/agents/openapi-to-application.agent.md",
|
|
506
|
+
"description": "Expert assistant for generating working applications from OpenAPI specifications",
|
|
507
|
+
"title": "openapi-to-application",
|
|
508
|
+
"type": "agents"
|
|
509
|
+
},
|
|
486
510
|
"pagerduty-incident-responder": {
|
|
487
511
|
"path": "assets/agents/pagerduty-incident-responder.agent.md",
|
|
488
512
|
"description": "Responds to PagerDuty incidents by analyzing incident context, identifying recent code changes, and suggesting fixes via GitHub PRs.",
|
|
@@ -1789,6 +1813,12 @@
|
|
|
1789
1813
|
"title": "ai-prompt-engineering-safety-review",
|
|
1790
1814
|
"type": "prompts"
|
|
1791
1815
|
},
|
|
1816
|
+
"apple-appstore-reviewer": {
|
|
1817
|
+
"path": "assets/prompts/apple-appstore-reviewer.prompt.md",
|
|
1818
|
+
"description": "Serves as a reviewer of the codebase with instructions on looking for Apple App Store optimizations or rejection reasons.",
|
|
1819
|
+
"title": "apple-appstore-reviewer",
|
|
1820
|
+
"type": "prompts"
|
|
1821
|
+
},
|
|
1792
1822
|
"architecture-blueprint-generator": {
|
|
1793
1823
|
"path": "assets/prompts/architecture-blueprint-generator.prompt.md",
|
|
1794
1824
|
"description": "Comprehensive project architecture blueprint generator that analyzes codebases to create detailed architectural documentation. Automatically detects technology stacks and architectural patterns, generates visual diagrams, documents implementation patterns, and provides extensible blueprints for maintaining architectural consistency and guiding new development.",
|
|
@@ -2287,6 +2317,12 @@
|
|
|
2287
2317
|
"title": "next-intl-add-language",
|
|
2288
2318
|
"type": "prompts"
|
|
2289
2319
|
},
|
|
2320
|
+
"openapi-to-application-code": {
|
|
2321
|
+
"path": "assets/prompts/openapi-to-application-code.prompt.md",
|
|
2322
|
+
"description": "Generate a complete, production-ready application from an OpenAPI specification",
|
|
2323
|
+
"title": "openapi-to-application-code",
|
|
2324
|
+
"type": "prompts"
|
|
2325
|
+
},
|
|
2290
2326
|
"php-mcp-server-generator": {
|
|
2291
2327
|
"path": "assets/prompts/php-mcp-server-generator.prompt.md",
|
|
2292
2328
|
"description": "Generate a complete PHP Model Context Protocol server project with tools, resources, prompts, and tests using the official PHP SDK",
|
|
@@ -2890,6 +2926,17 @@
|
|
|
2890
2926
|
"prompts:az-cost-optimize"
|
|
2891
2927
|
]
|
|
2892
2928
|
},
|
|
2929
|
+
"cast-imaging": {
|
|
2930
|
+
"path": "assets/collections/cast-imaging.collection.yml",
|
|
2931
|
+
"description": "A comprehensive collection of specialized agents for software analysis, impact assessment, structural quality advisories, and architectural review using CAST Imaging.",
|
|
2932
|
+
"title": "CAST Imaging Agents",
|
|
2933
|
+
"type": "collections",
|
|
2934
|
+
"items": [
|
|
2935
|
+
"agents:cast-imaging-software-discovery",
|
|
2936
|
+
"agents:cast-imaging-impact-analysis",
|
|
2937
|
+
"agents:cast-imaging-structural-quality-advisor"
|
|
2938
|
+
]
|
|
2939
|
+
},
|
|
2893
2940
|
"claude-skills-starter": {
|
|
2894
2941
|
"path": "assets/collections/claude-skills-starter.json",
|
|
2895
2942
|
"description": "Essential Claude Skills for software development, planning, and automation workflows.",
|
|
@@ -3560,6 +3607,61 @@
|
|
|
3560
3607
|
"prompts:technology-stack-blueprint-generator"
|
|
3561
3608
|
]
|
|
3562
3609
|
},
|
|
3610
|
+
"openapi-to-application-csharp-dotnet": {
|
|
3611
|
+
"path": "assets/collections/openapi-to-application-csharp-dotnet.collection.yml",
|
|
3612
|
+
"description": "Generate production-ready .NET applications from OpenAPI specifications. Includes ASP.NET Core project scaffolding, controller generation, entity framework integration, and C# best practices.",
|
|
3613
|
+
"title": "OpenAPI to Application - C# .NET",
|
|
3614
|
+
"type": "collections",
|
|
3615
|
+
"items": [
|
|
3616
|
+
"agents:openapi-to-application",
|
|
3617
|
+
"instructions:csharp",
|
|
3618
|
+
"prompts:openapi-to-application-code"
|
|
3619
|
+
]
|
|
3620
|
+
},
|
|
3621
|
+
"openapi-to-application-go": {
|
|
3622
|
+
"path": "assets/collections/openapi-to-application-go.collection.yml",
|
|
3623
|
+
"description": "Generate production-ready Go applications from OpenAPI specifications. Includes project scaffolding, handler generation, middleware setup, and Go best practices for REST APIs.",
|
|
3624
|
+
"title": "OpenAPI to Application - Go",
|
|
3625
|
+
"type": "collections",
|
|
3626
|
+
"items": [
|
|
3627
|
+
"agents:openapi-to-application",
|
|
3628
|
+
"instructions:go",
|
|
3629
|
+
"prompts:openapi-to-application-code"
|
|
3630
|
+
]
|
|
3631
|
+
},
|
|
3632
|
+
"openapi-to-application-java-spring-boot": {
|
|
3633
|
+
"path": "assets/collections/openapi-to-application-java-spring-boot.collection.yml",
|
|
3634
|
+
"description": "Generate production-ready Spring Boot applications from OpenAPI specifications. Includes project scaffolding, REST controller generation, service layer organization, and Spring Boot best practices.",
|
|
3635
|
+
"title": "OpenAPI to Application - Java Spring Boot",
|
|
3636
|
+
"type": "collections",
|
|
3637
|
+
"items": [
|
|
3638
|
+
"agents:openapi-to-application",
|
|
3639
|
+
"instructions:springboot",
|
|
3640
|
+
"prompts:openapi-to-application-code"
|
|
3641
|
+
]
|
|
3642
|
+
},
|
|
3643
|
+
"openapi-to-application-nodejs-nestjs": {
|
|
3644
|
+
"path": "assets/collections/openapi-to-application-nodejs-nestjs.collection.yml",
|
|
3645
|
+
"description": "Generate production-ready NestJS applications from OpenAPI specifications. Includes project scaffolding, controller and service generation, TypeScript best practices, and enterprise patterns.",
|
|
3646
|
+
"title": "OpenAPI to Application - Node.js NestJS",
|
|
3647
|
+
"type": "collections",
|
|
3648
|
+
"items": [
|
|
3649
|
+
"agents:openapi-to-application",
|
|
3650
|
+
"instructions:nestjs",
|
|
3651
|
+
"prompts:openapi-to-application-code"
|
|
3652
|
+
]
|
|
3653
|
+
},
|
|
3654
|
+
"openapi-to-application-python-fastapi": {
|
|
3655
|
+
"path": "assets/collections/openapi-to-application-python-fastapi.collection.yml",
|
|
3656
|
+
"description": "Generate production-ready FastAPI applications from OpenAPI specifications. Includes project scaffolding, route generation, dependency injection, and Python best practices for async APIs.",
|
|
3657
|
+
"title": "OpenAPI to Application - Python FastAPI",
|
|
3658
|
+
"type": "collections",
|
|
3659
|
+
"items": [
|
|
3660
|
+
"agents:openapi-to-application",
|
|
3661
|
+
"instructions:python",
|
|
3662
|
+
"prompts:openapi-to-application-code"
|
|
3663
|
+
]
|
|
3664
|
+
},
|
|
3563
3665
|
"partners": {
|
|
3564
3666
|
"path": "assets/collections/partners.collection.yml",
|
|
3565
3667
|
"description": "Custom agents that have been created by GitHub partners",
|
|
@@ -0,0 +1,490 @@
|
|
|
1
|
+
import fs from 'fs-extra';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import chalk from 'chalk';
|
|
4
|
+
import { fileURLToPath } from 'url';
|
|
5
|
+
|
|
6
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
7
|
+
const __dirname = path.dirname(__filename);
|
|
8
|
+
const ROOT_DIR = path.join(__dirname, '..');
|
|
9
|
+
const ASSETS_DIR = path.join(ROOT_DIR, 'assets');
|
|
10
|
+
const MANIFEST_PATH = path.join(ROOT_DIR, 'assets-manifest.json');
|
|
11
|
+
|
|
12
|
+
// Check if running in local development mode (assets folder exists)
|
|
13
|
+
// Made async function to avoid top-level await issues in tests
|
|
14
|
+
let _isLocalCache = null;
|
|
15
|
+
export async function isLocal() {
|
|
16
|
+
if (_isLocalCache === null) {
|
|
17
|
+
_isLocalCache = await fs.pathExists(ASSETS_DIR);
|
|
18
|
+
}
|
|
19
|
+
return _isLocalCache;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// For backward compatibility
|
|
23
|
+
export const IS_LOCAL = await fs.pathExists(ASSETS_DIR);
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Normalize collection items to flat array format for processing.
|
|
27
|
+
* Supports both old flat array format and new nested object format.
|
|
28
|
+
*
|
|
29
|
+
* Old format: ["instructions:reactjs", "prompts:code-review"]
|
|
30
|
+
* New format: { "instructions": ["reactjs"], "prompts": ["code-review"] }
|
|
31
|
+
*
|
|
32
|
+
* @param {Array|Object} items - Collection items in either format
|
|
33
|
+
* @returns {Array} Flat array of items in "type:name" format
|
|
34
|
+
*/
|
|
35
|
+
export function normalizeCollectionItems(items) {
|
|
36
|
+
if (!items) return [];
|
|
37
|
+
|
|
38
|
+
// If it's already an array (old format), return as-is
|
|
39
|
+
if (Array.isArray(items)) {
|
|
40
|
+
return items;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// If it's an object (new format), convert to flat array
|
|
44
|
+
if (typeof items === 'object') {
|
|
45
|
+
const flatItems = [];
|
|
46
|
+
for (const [type, names] of Object.entries(items)) {
|
|
47
|
+
if (Array.isArray(names)) {
|
|
48
|
+
for (const name of names) {
|
|
49
|
+
flatItems.push(`${type}:${name}`);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
return flatItems;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
return [];
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Convert YAML collection items format to flat array format.
|
|
61
|
+
* YAML format: [{ path: "agents/foo.agent.md", kind: "agent" }, ...]
|
|
62
|
+
* Flat format: ["agents:foo", ...]
|
|
63
|
+
*
|
|
64
|
+
* @param {Array} items - YAML collection items
|
|
65
|
+
* @returns {Array} Flat array of items in "type:name" format
|
|
66
|
+
*/
|
|
67
|
+
export function convertYamlItemsToFlat(items) {
|
|
68
|
+
if (!Array.isArray(items)) return [];
|
|
69
|
+
|
|
70
|
+
// Mapping of singular to plural forms for asset types
|
|
71
|
+
const pluralMap = {
|
|
72
|
+
'agent': 'agents',
|
|
73
|
+
'instruction': 'instructions',
|
|
74
|
+
'prompt': 'prompts',
|
|
75
|
+
'skill': 'skills',
|
|
76
|
+
'collection': 'collections'
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
const flatItems = [];
|
|
80
|
+
for (const item of items) {
|
|
81
|
+
if (!item.path || !item.kind) continue;
|
|
82
|
+
|
|
83
|
+
// Extract the type and name from the path
|
|
84
|
+
// Path format: "agents/foo.agent.md" or "instructions/bar.instructions.md"
|
|
85
|
+
const pathParts = item.path.split('/');
|
|
86
|
+
if (pathParts.length < 2) continue;
|
|
87
|
+
|
|
88
|
+
const fileName = pathParts[pathParts.length - 1];
|
|
89
|
+
const type = pluralMap[item.kind] || item.kind + 's'; // Use mapping or fallback to simple pluralization
|
|
90
|
+
|
|
91
|
+
// Extract name by removing extension
|
|
92
|
+
let name = fileName
|
|
93
|
+
.replace('.agent.md', '')
|
|
94
|
+
.replace('.instructions.md', '')
|
|
95
|
+
.replace('.prompt.md', '')
|
|
96
|
+
.replace('.md', '');
|
|
97
|
+
|
|
98
|
+
flatItems.push(`${type}:${name}`);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
return flatItems;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export async function getManifest() {
|
|
105
|
+
if (await fs.pathExists(MANIFEST_PATH)) {
|
|
106
|
+
return fs.readJson(MANIFEST_PATH);
|
|
107
|
+
}
|
|
108
|
+
throw new Error('Manifest file not found. Please report this issue.');
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
export async function listAssets(type) {
|
|
112
|
+
if (IS_LOCAL) {
|
|
113
|
+
// Local Development Mode
|
|
114
|
+
const matter = (await import('gray-matter')).default;
|
|
115
|
+
const YAML = (await import('yaml')).default;
|
|
116
|
+
// All types use assets/<type> directory
|
|
117
|
+
const dirPath = path.join(ASSETS_DIR, type);
|
|
118
|
+
|
|
119
|
+
if (!await fs.pathExists(dirPath)) {
|
|
120
|
+
console.log(chalk.yellow(`No assets found for type: ${type}`));
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
const files = await fs.readdir(dirPath);
|
|
125
|
+
if (files.length === 0) {
|
|
126
|
+
console.log(chalk.yellow(`No assets found for type: ${type}`));
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
console.log(chalk.blue.bold(`\nAvailable ${type}:`));
|
|
131
|
+
for (const file of files) {
|
|
132
|
+
// Skip .upstream-sync.json files
|
|
133
|
+
if (file === '.upstream-sync.json') continue;
|
|
134
|
+
|
|
135
|
+
const filePath = path.join(dirPath, file);
|
|
136
|
+
const stat = await fs.stat(filePath);
|
|
137
|
+
let description = '';
|
|
138
|
+
|
|
139
|
+
try {
|
|
140
|
+
if (type === 'skills' && stat.isDirectory()) {
|
|
141
|
+
// For Skills, read SKILL.md from directory
|
|
142
|
+
const skillMdPath = path.join(filePath, 'SKILL.md');
|
|
143
|
+
if (await fs.pathExists(skillMdPath)) {
|
|
144
|
+
const content = await fs.readFile(skillMdPath, 'utf8');
|
|
145
|
+
const parsed = matter(content);
|
|
146
|
+
description = parsed.data.description || '';
|
|
147
|
+
}
|
|
148
|
+
} else if (type === 'collections') {
|
|
149
|
+
if (file.endsWith('.json')) {
|
|
150
|
+
const content = await fs.readJson(filePath);
|
|
151
|
+
description = content.description || '';
|
|
152
|
+
} else if (file.endsWith('.yml') || file.endsWith('.yaml')) {
|
|
153
|
+
const content = await fs.readFile(filePath, 'utf8');
|
|
154
|
+
const parsed = YAML.parse(content);
|
|
155
|
+
description = parsed.description || '';
|
|
156
|
+
}
|
|
157
|
+
} else if (!stat.isDirectory()) {
|
|
158
|
+
const content = await fs.readFile(filePath, 'utf8');
|
|
159
|
+
const parsed = matter(content);
|
|
160
|
+
description = parsed.data.description || '';
|
|
161
|
+
}
|
|
162
|
+
} catch (_error) {
|
|
163
|
+
// Ignore errors reading metadata
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// Extract clean name without extensions
|
|
167
|
+
let name;
|
|
168
|
+
if (type === 'skills' && stat.isDirectory()) {
|
|
169
|
+
name = file;
|
|
170
|
+
} else if (type === 'collections') {
|
|
171
|
+
name = file.replace(/\.(collection\.)?(yml|yaml|json)$/, '');
|
|
172
|
+
} else if (type === 'instructions') {
|
|
173
|
+
name = file.replace('.instructions.md', '');
|
|
174
|
+
} else if (type === 'prompts') {
|
|
175
|
+
name = file.replace('.prompt.md', '');
|
|
176
|
+
} else if (type === 'agents') {
|
|
177
|
+
name = file.replace('.agent.md', '');
|
|
178
|
+
} else {
|
|
179
|
+
name = path.parse(file).name;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
console.log(` - ${name}${description ? ` - ${description}` : ''}`);
|
|
183
|
+
}
|
|
184
|
+
} else {
|
|
185
|
+
// Production Mode (Manifest)
|
|
186
|
+
const manifest = await getManifest();
|
|
187
|
+
const typeAssets = manifest.assets[type] || {};
|
|
188
|
+
const assets = Object.entries(typeAssets)
|
|
189
|
+
.map(([id, asset]) => ({
|
|
190
|
+
id,
|
|
191
|
+
...asset
|
|
192
|
+
}));
|
|
193
|
+
|
|
194
|
+
if (assets.length === 0) {
|
|
195
|
+
console.log(chalk.yellow(`No assets found for type: ${type}`));
|
|
196
|
+
return;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
console.log(chalk.blue.bold(`\nAvailable ${type}:`));
|
|
200
|
+
for (const asset of assets) {
|
|
201
|
+
const versionInfo = asset.metadata?.version ? ` (v${asset.metadata.version})` : '';
|
|
202
|
+
console.log(` - ${asset.id}${versionInfo}${asset.description ? ` - ${asset.description}` : ''}`);
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
export async function downloadSkill(name, options) {
|
|
208
|
+
const skillName = name;
|
|
209
|
+
let skillFiles = [];
|
|
210
|
+
let skillPath = '';
|
|
211
|
+
|
|
212
|
+
if (IS_LOCAL) {
|
|
213
|
+
// Local mode: copy from assets/skills
|
|
214
|
+
skillPath = path.join(ASSETS_DIR, 'skills', skillName);
|
|
215
|
+
|
|
216
|
+
if (!await fs.pathExists(skillPath)) {
|
|
217
|
+
throw new Error(`Skill not found: skills/${skillName}`);
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
// Get all files in the skill directory
|
|
221
|
+
const getAllFiles = async (dir, baseDir = dir) => {
|
|
222
|
+
const files = [];
|
|
223
|
+
const entries = await fs.readdir(dir, { withFileTypes: true });
|
|
224
|
+
|
|
225
|
+
for (const entry of entries) {
|
|
226
|
+
const fullPath = path.join(dir, entry.name);
|
|
227
|
+
if (entry.isDirectory()) {
|
|
228
|
+
const subFiles = await getAllFiles(fullPath, baseDir);
|
|
229
|
+
files.push(...subFiles);
|
|
230
|
+
} else {
|
|
231
|
+
const relativePath = path.relative(baseDir, fullPath);
|
|
232
|
+
files.push({ relative: relativePath, full: fullPath });
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
return files;
|
|
236
|
+
};
|
|
237
|
+
|
|
238
|
+
skillFiles = await getAllFiles(skillPath);
|
|
239
|
+
} else {
|
|
240
|
+
// Production mode: fetch from manifest and download from GitHub
|
|
241
|
+
const manifest = await getManifest();
|
|
242
|
+
const asset = manifest.assets.skills?.[skillName];
|
|
243
|
+
|
|
244
|
+
if (!asset) {
|
|
245
|
+
throw new Error(`Skill not found: ${skillName}`);
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
skillPath = asset.path;
|
|
249
|
+
skillFiles = asset.files.map(file => ({
|
|
250
|
+
relative: file,
|
|
251
|
+
url: `https://raw.githubusercontent.com/archubbuck/workspace-architect/main/${asset.path}/${file}`
|
|
252
|
+
}));
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
// Determine destination
|
|
256
|
+
let destDir;
|
|
257
|
+
if (options.output) {
|
|
258
|
+
destDir = path.resolve(process.cwd(), options.output);
|
|
259
|
+
} else {
|
|
260
|
+
destDir = path.join(process.cwd(), '.github', 'skills', skillName);
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
if (options.dryRun) {
|
|
264
|
+
console.log(chalk.cyan(`[Dry Run] Would create skill directory at ${destDir}`));
|
|
265
|
+
for (const file of skillFiles) {
|
|
266
|
+
console.log(chalk.cyan(` Would copy: ${file.relative}`));
|
|
267
|
+
}
|
|
268
|
+
return;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
// Check if skill already exists
|
|
272
|
+
if (await fs.pathExists(destDir) && !options.force) {
|
|
273
|
+
const inquirer = (await import('inquirer')).default;
|
|
274
|
+
const { overwrite } = await inquirer.prompt([
|
|
275
|
+
{
|
|
276
|
+
type: 'confirm',
|
|
277
|
+
name: 'overwrite',
|
|
278
|
+
message: `Skill ${skillName} already exists in ${destDir}. Overwrite?`,
|
|
279
|
+
default: false
|
|
280
|
+
}
|
|
281
|
+
]);
|
|
282
|
+
|
|
283
|
+
if (!overwrite) {
|
|
284
|
+
console.log(chalk.yellow('Operation cancelled.'));
|
|
285
|
+
return;
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
console.log(chalk.blue(`Downloading skill: ${skillName}`));
|
|
290
|
+
|
|
291
|
+
// Create skill directory
|
|
292
|
+
await fs.ensureDir(destDir);
|
|
293
|
+
|
|
294
|
+
// Download/copy all files
|
|
295
|
+
for (const file of skillFiles) {
|
|
296
|
+
const destPath = path.join(destDir, file.relative);
|
|
297
|
+
const destFileDir = path.dirname(destPath);
|
|
298
|
+
await fs.ensureDir(destFileDir);
|
|
299
|
+
|
|
300
|
+
if (IS_LOCAL) {
|
|
301
|
+
// Copy from local assets
|
|
302
|
+
await fs.copyFile(file.full, destPath);
|
|
303
|
+
} else {
|
|
304
|
+
// Download from GitHub
|
|
305
|
+
const response = await fetch(file.url);
|
|
306
|
+
if (!response.ok) {
|
|
307
|
+
console.warn(chalk.yellow(`Warning: Failed to download ${file.relative}`));
|
|
308
|
+
continue;
|
|
309
|
+
}
|
|
310
|
+
const content = await response.text();
|
|
311
|
+
await fs.writeFile(destPath, content);
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
console.log(chalk.dim(` Downloaded: ${file.relative}`));
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
console.log(chalk.green(`Successfully downloaded skill ${skillName} to ${destDir} (${skillFiles.length} files)`));
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
export async function downloadAsset(id, options) {
|
|
321
|
+
const [type, name] = id.split(':');
|
|
322
|
+
|
|
323
|
+
if (!type || !name) {
|
|
324
|
+
throw new Error('Invalid ID format. Use type:name (e.g., instructions:basic-setup)');
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
const validTypes = ['instructions', 'prompts', 'agents', 'skills', 'collections'];
|
|
328
|
+
if (!validTypes.includes(type)) {
|
|
329
|
+
throw new Error(`Invalid type: ${type}. Valid types are: ${validTypes.join(', ')}`);
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
// Handle Collections
|
|
333
|
+
if (type === 'collections') {
|
|
334
|
+
let items = [];
|
|
335
|
+
|
|
336
|
+
if (IS_LOCAL) {
|
|
337
|
+
const YAML = (await import('yaml')).default;
|
|
338
|
+
|
|
339
|
+
// Try to find the collection file with various extensions
|
|
340
|
+
const possibleFileNames = [
|
|
341
|
+
`${name}.json`,
|
|
342
|
+
`${name}.collection.yml`,
|
|
343
|
+
`${name}.collection.yaml`,
|
|
344
|
+
`${name}.yml`,
|
|
345
|
+
`${name}.yaml`
|
|
346
|
+
];
|
|
347
|
+
|
|
348
|
+
let sourcePath = null;
|
|
349
|
+
let isYaml = false;
|
|
350
|
+
|
|
351
|
+
for (const fileName of possibleFileNames) {
|
|
352
|
+
const testPath = path.join(ASSETS_DIR, 'collections', fileName);
|
|
353
|
+
if (await fs.pathExists(testPath)) {
|
|
354
|
+
sourcePath = testPath;
|
|
355
|
+
isYaml = fileName.endsWith('.yml') || fileName.endsWith('.yaml');
|
|
356
|
+
break;
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
if (!sourcePath) {
|
|
361
|
+
throw new Error(`Collection not found: ${type}/${name}`);
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
if (isYaml) {
|
|
365
|
+
const content = await fs.readFile(sourcePath, 'utf8');
|
|
366
|
+
const parsed = YAML.parse(content);
|
|
367
|
+
// Convert YAML format items to flat array
|
|
368
|
+
items = convertYamlItemsToFlat(parsed.items || []);
|
|
369
|
+
} else {
|
|
370
|
+
const collectionContent = await fs.readJson(sourcePath);
|
|
371
|
+
const rawItems = collectionContent.items || (Array.isArray(collectionContent) ? collectionContent : []);
|
|
372
|
+
items = normalizeCollectionItems(rawItems);
|
|
373
|
+
}
|
|
374
|
+
} else {
|
|
375
|
+
const manifest = await getManifest();
|
|
376
|
+
const asset = manifest.assets[type]?.[name];
|
|
377
|
+
|
|
378
|
+
if (!asset) {
|
|
379
|
+
throw new Error(`Collection not found: ${id}`);
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
items = normalizeCollectionItems(asset.items || []);
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
console.log(chalk.blue(`Downloading collection: ${name}`));
|
|
386
|
+
for (const assetId of items) {
|
|
387
|
+
try {
|
|
388
|
+
await downloadAsset(assetId, options);
|
|
389
|
+
} catch (error) {
|
|
390
|
+
console.error(chalk.red(`Failed to download ${assetId} from collection:`), error.message);
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
return;
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
// Handle Skills (multi-file folder-based assets)
|
|
397
|
+
if (type === 'skills') {
|
|
398
|
+
await downloadSkill(name, options);
|
|
399
|
+
return;
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
// Handle Single Asset
|
|
403
|
+
let content = '';
|
|
404
|
+
let fileName = '';
|
|
405
|
+
|
|
406
|
+
if (IS_LOCAL) {
|
|
407
|
+
// Try to find the file with various extensions
|
|
408
|
+
const potentialFileNames = [
|
|
409
|
+
name,
|
|
410
|
+
name + '.md'
|
|
411
|
+
];
|
|
412
|
+
if (type === 'agents') potentialFileNames.push(name + '.agent.md');
|
|
413
|
+
if (type === 'prompts') potentialFileNames.push(name + '.prompt.md');
|
|
414
|
+
if (type === 'instructions') potentialFileNames.push(name + '.instructions.md');
|
|
415
|
+
|
|
416
|
+
// All types use assets/<type> directory
|
|
417
|
+
const baseDir = path.join(ASSETS_DIR, type);
|
|
418
|
+
|
|
419
|
+
let sourcePath = null;
|
|
420
|
+
for (const fname of potentialFileNames) {
|
|
421
|
+
const p = path.join(baseDir, fname);
|
|
422
|
+
if (await fs.pathExists(p)) {
|
|
423
|
+
sourcePath = p;
|
|
424
|
+
fileName = fname;
|
|
425
|
+
break;
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
if (!sourcePath) {
|
|
430
|
+
throw new Error(`Asset not found: ${type}/${name}`);
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
content = await fs.readFile(sourcePath, 'utf8');
|
|
434
|
+
} else {
|
|
435
|
+
const manifest = await getManifest();
|
|
436
|
+
const asset = manifest.assets[type]?.[name];
|
|
437
|
+
|
|
438
|
+
if (!asset) {
|
|
439
|
+
throw new Error(`Asset not found: ${id}`);
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
fileName = path.basename(asset.path);
|
|
443
|
+
const url = `https://raw.githubusercontent.com/archubbuck/workspace-architect/main/${asset.path}`;
|
|
444
|
+
|
|
445
|
+
console.log(chalk.dim(`Fetching ${url}...`));
|
|
446
|
+
const response = await fetch(url);
|
|
447
|
+
if (!response.ok) {
|
|
448
|
+
throw new Error(`Failed to fetch ${url}: ${response.statusText}`);
|
|
449
|
+
}
|
|
450
|
+
content = await response.text();
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
// Determine Destination
|
|
454
|
+
let destDir;
|
|
455
|
+
if (options.output) {
|
|
456
|
+
destDir = path.resolve(process.cwd(), options.output);
|
|
457
|
+
} else {
|
|
458
|
+
destDir = path.join(process.cwd(), '.github', type);
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
const destPath = path.join(destDir, fileName);
|
|
462
|
+
|
|
463
|
+
if (options.dryRun) {
|
|
464
|
+
console.log(chalk.cyan(`[Dry Run] Would write to ${destPath}`));
|
|
465
|
+
return;
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
// Ensure destination directory exists
|
|
469
|
+
await fs.ensureDir(destDir);
|
|
470
|
+
|
|
471
|
+
if (await fs.pathExists(destPath) && !options.force) {
|
|
472
|
+
const inquirer = (await import('inquirer')).default;
|
|
473
|
+
const { overwrite } = await inquirer.prompt([
|
|
474
|
+
{
|
|
475
|
+
type: 'confirm',
|
|
476
|
+
name: 'overwrite',
|
|
477
|
+
message: `File ${fileName} already exists in ${destDir}. Overwrite?`,
|
|
478
|
+
default: false
|
|
479
|
+
}
|
|
480
|
+
]);
|
|
481
|
+
|
|
482
|
+
if (!overwrite) {
|
|
483
|
+
console.log(chalk.yellow('Operation cancelled.'));
|
|
484
|
+
return;
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
await fs.writeFile(destPath, content);
|
|
489
|
+
console.log(chalk.green(`Successfully downloaded ${fileName} to ${destDir}`));
|
|
490
|
+
}
|
package/bin/cli.js
CHANGED
|
@@ -1,99 +1,11 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
import { program } from 'commander';
|
|
4
|
-
import inquirer from 'inquirer';
|
|
5
|
-
import fs from 'fs-extra';
|
|
6
|
-
import path from 'path';
|
|
7
4
|
import chalk from 'chalk';
|
|
8
|
-
import {
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
const ROOT_DIR = path.join(__dirname, '..');
|
|
13
|
-
const ASSETS_DIR = path.join(ROOT_DIR, 'assets');
|
|
14
|
-
const MANIFEST_PATH = path.join(ROOT_DIR, 'assets-manifest.json');
|
|
15
|
-
|
|
16
|
-
// Check if running in local development mode (assets folder exists)
|
|
17
|
-
const IS_LOCAL = await fs.pathExists(ASSETS_DIR);
|
|
18
|
-
|
|
19
|
-
/**
|
|
20
|
-
* Normalize collection items to flat array format for processing.
|
|
21
|
-
* Supports both old flat array format and new nested object format.
|
|
22
|
-
*
|
|
23
|
-
* Old format: ["instructions:reactjs", "prompts:code-review"]
|
|
24
|
-
* New format: { "instructions": ["reactjs"], "prompts": ["code-review"] }
|
|
25
|
-
*
|
|
26
|
-
* @param {Array|Object} items - Collection items in either format
|
|
27
|
-
* @returns {Array} Flat array of items in "type:name" format
|
|
28
|
-
*/
|
|
29
|
-
function normalizeCollectionItems(items) {
|
|
30
|
-
if (!items) return [];
|
|
31
|
-
|
|
32
|
-
// If it's already an array (old format), return as-is
|
|
33
|
-
if (Array.isArray(items)) {
|
|
34
|
-
return items;
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
// If it's an object (new format), convert to flat array
|
|
38
|
-
if (typeof items === 'object') {
|
|
39
|
-
const flatItems = [];
|
|
40
|
-
for (const [type, names] of Object.entries(items)) {
|
|
41
|
-
if (Array.isArray(names)) {
|
|
42
|
-
for (const name of names) {
|
|
43
|
-
flatItems.push(`${type}:${name}`);
|
|
44
|
-
}
|
|
45
|
-
}
|
|
46
|
-
}
|
|
47
|
-
return flatItems;
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
return [];
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
/**
|
|
54
|
-
* Convert YAML collection items format to flat array format.
|
|
55
|
-
* YAML format: [{ path: "agents/foo.agent.md", kind: "agent" }, ...]
|
|
56
|
-
* Flat format: ["agents:foo", ...]
|
|
57
|
-
*
|
|
58
|
-
* @param {Array} items - YAML collection items
|
|
59
|
-
* @returns {Array} Flat array of items in "type:name" format
|
|
60
|
-
*/
|
|
61
|
-
function convertYamlItemsToFlat(items) {
|
|
62
|
-
if (!Array.isArray(items)) return [];
|
|
63
|
-
|
|
64
|
-
// Mapping of singular to plural forms for asset types
|
|
65
|
-
const pluralMap = {
|
|
66
|
-
'agent': 'agents',
|
|
67
|
-
'instruction': 'instructions',
|
|
68
|
-
'prompt': 'prompts',
|
|
69
|
-
'skill': 'skills',
|
|
70
|
-
'collection': 'collections'
|
|
71
|
-
};
|
|
72
|
-
|
|
73
|
-
const flatItems = [];
|
|
74
|
-
for (const item of items) {
|
|
75
|
-
if (!item.path || !item.kind) continue;
|
|
76
|
-
|
|
77
|
-
// Extract the type and name from the path
|
|
78
|
-
// Path format: "agents/foo.agent.md" or "instructions/bar.instructions.md"
|
|
79
|
-
const pathParts = item.path.split('/');
|
|
80
|
-
if (pathParts.length < 2) continue;
|
|
81
|
-
|
|
82
|
-
const fileName = pathParts[pathParts.length - 1];
|
|
83
|
-
const type = pluralMap[item.kind] || item.kind + 's'; // Use mapping or fallback to simple pluralization
|
|
84
|
-
|
|
85
|
-
// Extract name by removing extension
|
|
86
|
-
let name = fileName
|
|
87
|
-
.replace('.agent.md', '')
|
|
88
|
-
.replace('.instructions.md', '')
|
|
89
|
-
.replace('.prompt.md', '')
|
|
90
|
-
.replace('.md', '');
|
|
91
|
-
|
|
92
|
-
flatItems.push(`${type}:${name}`);
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
return flatItems;
|
|
96
|
-
}
|
|
5
|
+
import {
|
|
6
|
+
listAssets,
|
|
7
|
+
downloadAsset,
|
|
8
|
+
} from './cli-functions.js';
|
|
97
9
|
|
|
98
10
|
program
|
|
99
11
|
.name('workspace-architect')
|
|
@@ -153,390 +65,4 @@ program
|
|
|
153
65
|
}
|
|
154
66
|
});
|
|
155
67
|
|
|
156
|
-
async function getManifest() {
|
|
157
|
-
if (await fs.pathExists(MANIFEST_PATH)) {
|
|
158
|
-
return fs.readJson(MANIFEST_PATH);
|
|
159
|
-
}
|
|
160
|
-
throw new Error('Manifest file not found. Please report this issue.');
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
async function listAssets(type) {
|
|
164
|
-
if (IS_LOCAL) {
|
|
165
|
-
// Local Development Mode
|
|
166
|
-
const matter = (await import('gray-matter')).default;
|
|
167
|
-
const YAML = (await import('yaml')).default;
|
|
168
|
-
// All types use assets/<type> directory
|
|
169
|
-
const dirPath = path.join(ASSETS_DIR, type);
|
|
170
|
-
|
|
171
|
-
if (!await fs.pathExists(dirPath)) {
|
|
172
|
-
console.log(chalk.yellow(`No assets found for type: ${type}`));
|
|
173
|
-
return;
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
const files = await fs.readdir(dirPath);
|
|
177
|
-
if (files.length === 0) {
|
|
178
|
-
console.log(chalk.yellow(`No assets found for type: ${type}`));
|
|
179
|
-
return;
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
console.log(chalk.blue.bold(`\nAvailable ${type}:`));
|
|
183
|
-
for (const file of files) {
|
|
184
|
-
// Skip .upstream-sync.json files
|
|
185
|
-
if (file === '.upstream-sync.json') continue;
|
|
186
|
-
|
|
187
|
-
const filePath = path.join(dirPath, file);
|
|
188
|
-
const stat = await fs.stat(filePath);
|
|
189
|
-
let description = '';
|
|
190
|
-
|
|
191
|
-
try {
|
|
192
|
-
if (type === 'skills' && stat.isDirectory()) {
|
|
193
|
-
// For Skills, read SKILL.md from directory
|
|
194
|
-
const skillMdPath = path.join(filePath, 'SKILL.md');
|
|
195
|
-
if (await fs.pathExists(skillMdPath)) {
|
|
196
|
-
const content = await fs.readFile(skillMdPath, 'utf8');
|
|
197
|
-
const parsed = matter(content);
|
|
198
|
-
description = parsed.data.description || '';
|
|
199
|
-
}
|
|
200
|
-
} else if (type === 'collections') {
|
|
201
|
-
if (file.endsWith('.json')) {
|
|
202
|
-
const content = await fs.readJson(filePath);
|
|
203
|
-
description = content.description || '';
|
|
204
|
-
} else if (file.endsWith('.yml') || file.endsWith('.yaml')) {
|
|
205
|
-
const content = await fs.readFile(filePath, 'utf8');
|
|
206
|
-
const parsed = YAML.parse(content);
|
|
207
|
-
description = parsed.description || '';
|
|
208
|
-
}
|
|
209
|
-
} else if (!stat.isDirectory()) {
|
|
210
|
-
const content = await fs.readFile(filePath, 'utf8');
|
|
211
|
-
const parsed = matter(content);
|
|
212
|
-
description = parsed.data.description || '';
|
|
213
|
-
}
|
|
214
|
-
} catch (e) {
|
|
215
|
-
// Ignore errors reading metadata
|
|
216
|
-
}
|
|
217
|
-
|
|
218
|
-
// Extract clean name without extensions
|
|
219
|
-
let name;
|
|
220
|
-
if (type === 'skills' && stat.isDirectory()) {
|
|
221
|
-
name = file;
|
|
222
|
-
} else if (type === 'collections') {
|
|
223
|
-
name = file.replace(/\.(collection\.)?(yml|yaml|json)$/, '');
|
|
224
|
-
} else if (type === 'instructions') {
|
|
225
|
-
name = file.replace('.instructions.md', '');
|
|
226
|
-
} else if (type === 'prompts') {
|
|
227
|
-
name = file.replace('.prompt.md', '');
|
|
228
|
-
} else if (type === 'agents') {
|
|
229
|
-
name = file.replace('.agent.md', '');
|
|
230
|
-
} else {
|
|
231
|
-
name = path.parse(file).name;
|
|
232
|
-
}
|
|
233
|
-
|
|
234
|
-
console.log(` - ${name}${description ? ` - ${description}` : ''}`);
|
|
235
|
-
}
|
|
236
|
-
} else {
|
|
237
|
-
// Production Mode (Manifest)
|
|
238
|
-
const manifest = await getManifest();
|
|
239
|
-
const typeAssets = manifest.assets[type] || {};
|
|
240
|
-
const assets = Object.entries(typeAssets)
|
|
241
|
-
.map(([id, asset]) => ({
|
|
242
|
-
id,
|
|
243
|
-
...asset
|
|
244
|
-
}));
|
|
245
|
-
|
|
246
|
-
if (assets.length === 0) {
|
|
247
|
-
console.log(chalk.yellow(`No assets found for type: ${type}`));
|
|
248
|
-
return;
|
|
249
|
-
}
|
|
250
|
-
|
|
251
|
-
console.log(chalk.blue.bold(`\nAvailable ${type}:`));
|
|
252
|
-
for (const asset of assets) {
|
|
253
|
-
const versionInfo = asset.metadata?.version ? ` (v${asset.metadata.version})` : '';
|
|
254
|
-
console.log(` - ${asset.id}${versionInfo}${asset.description ? ` - ${asset.description}` : ''}`);
|
|
255
|
-
}
|
|
256
|
-
}
|
|
257
|
-
}
|
|
258
|
-
|
|
259
|
-
async function downloadAsset(id, options) {
|
|
260
|
-
const [type, name] = id.split(':');
|
|
261
|
-
|
|
262
|
-
if (!type || !name) {
|
|
263
|
-
throw new Error('Invalid ID format. Use type:name (e.g., instructions:basic-setup)');
|
|
264
|
-
}
|
|
265
|
-
|
|
266
|
-
const validTypes = ['instructions', 'prompts', 'agents', 'skills', 'collections'];
|
|
267
|
-
if (!validTypes.includes(type)) {
|
|
268
|
-
throw new Error(`Invalid type: ${type}. Valid types are: ${validTypes.join(', ')}`);
|
|
269
|
-
}
|
|
270
|
-
|
|
271
|
-
// Handle Collections
|
|
272
|
-
if (type === 'collections') {
|
|
273
|
-
let items = [];
|
|
274
|
-
|
|
275
|
-
if (IS_LOCAL) {
|
|
276
|
-
const YAML = (await import('yaml')).default;
|
|
277
|
-
|
|
278
|
-
// Try to find the collection file with various extensions
|
|
279
|
-
const possibleFileNames = [
|
|
280
|
-
`${name}.json`,
|
|
281
|
-
`${name}.collection.yml`,
|
|
282
|
-
`${name}.collection.yaml`,
|
|
283
|
-
`${name}.yml`,
|
|
284
|
-
`${name}.yaml`
|
|
285
|
-
];
|
|
286
|
-
|
|
287
|
-
let sourcePath = null;
|
|
288
|
-
let isYaml = false;
|
|
289
|
-
|
|
290
|
-
for (const fileName of possibleFileNames) {
|
|
291
|
-
const testPath = path.join(ASSETS_DIR, 'collections', fileName);
|
|
292
|
-
if (await fs.pathExists(testPath)) {
|
|
293
|
-
sourcePath = testPath;
|
|
294
|
-
isYaml = fileName.endsWith('.yml') || fileName.endsWith('.yaml');
|
|
295
|
-
break;
|
|
296
|
-
}
|
|
297
|
-
}
|
|
298
|
-
|
|
299
|
-
if (!sourcePath) {
|
|
300
|
-
throw new Error(`Collection not found: ${type}/${name}`);
|
|
301
|
-
}
|
|
302
|
-
|
|
303
|
-
if (isYaml) {
|
|
304
|
-
const content = await fs.readFile(sourcePath, 'utf8');
|
|
305
|
-
const parsed = YAML.parse(content);
|
|
306
|
-
// Convert YAML format items to flat array
|
|
307
|
-
items = convertYamlItemsToFlat(parsed.items || []);
|
|
308
|
-
} else {
|
|
309
|
-
const collectionContent = await fs.readJson(sourcePath);
|
|
310
|
-
const rawItems = collectionContent.items || (Array.isArray(collectionContent) ? collectionContent : []);
|
|
311
|
-
items = normalizeCollectionItems(rawItems);
|
|
312
|
-
}
|
|
313
|
-
} else {
|
|
314
|
-
const manifest = await getManifest();
|
|
315
|
-
const asset = manifest.assets[type]?.[name];
|
|
316
|
-
|
|
317
|
-
if (!asset) {
|
|
318
|
-
throw new Error(`Collection not found: ${id}`);
|
|
319
|
-
}
|
|
320
|
-
|
|
321
|
-
items = normalizeCollectionItems(asset.items || []);
|
|
322
|
-
}
|
|
323
|
-
|
|
324
|
-
console.log(chalk.blue(`Downloading collection: ${name}`));
|
|
325
|
-
for (const assetId of items) {
|
|
326
|
-
try {
|
|
327
|
-
await downloadAsset(assetId, options);
|
|
328
|
-
} catch (error) {
|
|
329
|
-
console.error(chalk.red(`Failed to download ${assetId} from collection:`), error.message);
|
|
330
|
-
}
|
|
331
|
-
}
|
|
332
|
-
return;
|
|
333
|
-
}
|
|
334
|
-
|
|
335
|
-
// Handle Skills (multi-file folder-based assets)
|
|
336
|
-
if (type === 'skills') {
|
|
337
|
-
await downloadSkill(name, options);
|
|
338
|
-
return;
|
|
339
|
-
}
|
|
340
|
-
|
|
341
|
-
// Handle Single Asset
|
|
342
|
-
let content = '';
|
|
343
|
-
let fileName = '';
|
|
344
|
-
|
|
345
|
-
if (IS_LOCAL) {
|
|
346
|
-
// Try to find the file with various extensions
|
|
347
|
-
const potentialFileNames = [
|
|
348
|
-
name,
|
|
349
|
-
name + '.md'
|
|
350
|
-
];
|
|
351
|
-
if (type === 'agents') potentialFileNames.push(name + '.agent.md');
|
|
352
|
-
if (type === 'prompts') potentialFileNames.push(name + '.prompt.md');
|
|
353
|
-
if (type === 'instructions') potentialFileNames.push(name + '.instructions.md');
|
|
354
|
-
|
|
355
|
-
// All types use assets/<type> directory
|
|
356
|
-
const baseDir = path.join(ASSETS_DIR, type);
|
|
357
|
-
|
|
358
|
-
let sourcePath = null;
|
|
359
|
-
for (const fname of potentialFileNames) {
|
|
360
|
-
const p = path.join(baseDir, fname);
|
|
361
|
-
if (await fs.pathExists(p)) {
|
|
362
|
-
sourcePath = p;
|
|
363
|
-
fileName = fname;
|
|
364
|
-
break;
|
|
365
|
-
}
|
|
366
|
-
}
|
|
367
|
-
|
|
368
|
-
if (!sourcePath) {
|
|
369
|
-
throw new Error(`Asset not found: ${type}/${name}`);
|
|
370
|
-
}
|
|
371
|
-
|
|
372
|
-
content = await fs.readFile(sourcePath, 'utf8');
|
|
373
|
-
} else {
|
|
374
|
-
const manifest = await getManifest();
|
|
375
|
-
const asset = manifest.assets[type]?.[name];
|
|
376
|
-
|
|
377
|
-
if (!asset) {
|
|
378
|
-
throw new Error(`Asset not found: ${id}`);
|
|
379
|
-
}
|
|
380
|
-
|
|
381
|
-
fileName = path.basename(asset.path);
|
|
382
|
-
const url = `https://raw.githubusercontent.com/archubbuck/workspace-architect/main/${asset.path}`;
|
|
383
|
-
|
|
384
|
-
console.log(chalk.dim(`Fetching ${url}...`));
|
|
385
|
-
const response = await fetch(url);
|
|
386
|
-
if (!response.ok) {
|
|
387
|
-
throw new Error(`Failed to fetch ${url}: ${response.statusText}`);
|
|
388
|
-
}
|
|
389
|
-
content = await response.text();
|
|
390
|
-
}
|
|
391
|
-
|
|
392
|
-
// Determine Destination
|
|
393
|
-
let destDir;
|
|
394
|
-
if (options.output) {
|
|
395
|
-
destDir = path.resolve(process.cwd(), options.output);
|
|
396
|
-
} else {
|
|
397
|
-
destDir = path.join(process.cwd(), '.github', type);
|
|
398
|
-
}
|
|
399
|
-
|
|
400
|
-
const destPath = path.join(destDir, fileName);
|
|
401
|
-
|
|
402
|
-
if (options.dryRun) {
|
|
403
|
-
console.log(chalk.cyan(`[Dry Run] Would write to ${destPath}`));
|
|
404
|
-
return;
|
|
405
|
-
}
|
|
406
|
-
|
|
407
|
-
// Ensure destination directory exists
|
|
408
|
-
await fs.ensureDir(destDir);
|
|
409
|
-
|
|
410
|
-
if (await fs.pathExists(destPath) && !options.force) {
|
|
411
|
-
const { overwrite } = await inquirer.prompt([
|
|
412
|
-
{
|
|
413
|
-
type: 'confirm',
|
|
414
|
-
name: 'overwrite',
|
|
415
|
-
message: `File ${fileName} already exists in ${destDir}. Overwrite?`,
|
|
416
|
-
default: false
|
|
417
|
-
}
|
|
418
|
-
]);
|
|
419
|
-
|
|
420
|
-
if (!overwrite) {
|
|
421
|
-
console.log(chalk.yellow('Operation cancelled.'));
|
|
422
|
-
return;
|
|
423
|
-
}
|
|
424
|
-
}
|
|
425
|
-
|
|
426
|
-
await fs.writeFile(destPath, content);
|
|
427
|
-
console.log(chalk.green(`Successfully downloaded ${fileName} to ${destDir}`));
|
|
428
|
-
}
|
|
429
|
-
|
|
430
|
-
async function downloadSkill(name, options) {
|
|
431
|
-
const skillName = name;
|
|
432
|
-
let skillFiles = [];
|
|
433
|
-
let skillPath = '';
|
|
434
|
-
|
|
435
|
-
if (IS_LOCAL) {
|
|
436
|
-
// Local mode: copy from assets/skills
|
|
437
|
-
skillPath = path.join(ASSETS_DIR, 'skills', skillName);
|
|
438
|
-
|
|
439
|
-
if (!await fs.pathExists(skillPath)) {
|
|
440
|
-
throw new Error(`Skill not found: skills/${skillName}`);
|
|
441
|
-
}
|
|
442
|
-
|
|
443
|
-
// Get all files in the skill directory
|
|
444
|
-
const getAllFiles = async (dir, baseDir = dir) => {
|
|
445
|
-
const files = [];
|
|
446
|
-
const entries = await fs.readdir(dir, { withFileTypes: true });
|
|
447
|
-
|
|
448
|
-
for (const entry of entries) {
|
|
449
|
-
const fullPath = path.join(dir, entry.name);
|
|
450
|
-
if (entry.isDirectory()) {
|
|
451
|
-
const subFiles = await getAllFiles(fullPath, baseDir);
|
|
452
|
-
files.push(...subFiles);
|
|
453
|
-
} else {
|
|
454
|
-
const relativePath = path.relative(baseDir, fullPath);
|
|
455
|
-
files.push({ relative: relativePath, full: fullPath });
|
|
456
|
-
}
|
|
457
|
-
}
|
|
458
|
-
return files;
|
|
459
|
-
};
|
|
460
|
-
|
|
461
|
-
skillFiles = await getAllFiles(skillPath);
|
|
462
|
-
} else {
|
|
463
|
-
// Production mode: fetch from manifest and download from GitHub
|
|
464
|
-
const manifest = await getManifest();
|
|
465
|
-
const asset = manifest.assets.skills?.[skillName];
|
|
466
|
-
|
|
467
|
-
if (!asset) {
|
|
468
|
-
throw new Error(`Skill not found: ${skillName}`);
|
|
469
|
-
}
|
|
470
|
-
|
|
471
|
-
skillPath = asset.path;
|
|
472
|
-
skillFiles = asset.files.map(file => ({
|
|
473
|
-
relative: file,
|
|
474
|
-
url: `https://raw.githubusercontent.com/archubbuck/workspace-architect/main/${asset.path}/${file}`
|
|
475
|
-
}));
|
|
476
|
-
}
|
|
477
|
-
|
|
478
|
-
// Determine destination
|
|
479
|
-
let destDir;
|
|
480
|
-
if (options.output) {
|
|
481
|
-
destDir = path.resolve(process.cwd(), options.output);
|
|
482
|
-
} else {
|
|
483
|
-
destDir = path.join(process.cwd(), '.github', 'skills', skillName);
|
|
484
|
-
}
|
|
485
|
-
|
|
486
|
-
if (options.dryRun) {
|
|
487
|
-
console.log(chalk.cyan(`[Dry Run] Would create skill directory at ${destDir}`));
|
|
488
|
-
for (const file of skillFiles) {
|
|
489
|
-
console.log(chalk.cyan(` Would copy: ${file.relative}`));
|
|
490
|
-
}
|
|
491
|
-
return;
|
|
492
|
-
}
|
|
493
|
-
|
|
494
|
-
// Check if skill already exists
|
|
495
|
-
if (await fs.pathExists(destDir) && !options.force) {
|
|
496
|
-
const { overwrite } = await inquirer.prompt([
|
|
497
|
-
{
|
|
498
|
-
type: 'confirm',
|
|
499
|
-
name: 'overwrite',
|
|
500
|
-
message: `Skill ${skillName} already exists in ${destDir}. Overwrite?`,
|
|
501
|
-
default: false
|
|
502
|
-
}
|
|
503
|
-
]);
|
|
504
|
-
|
|
505
|
-
if (!overwrite) {
|
|
506
|
-
console.log(chalk.yellow('Operation cancelled.'));
|
|
507
|
-
return;
|
|
508
|
-
}
|
|
509
|
-
}
|
|
510
|
-
|
|
511
|
-
console.log(chalk.blue(`Downloading skill: ${skillName}`));
|
|
512
|
-
|
|
513
|
-
// Create skill directory
|
|
514
|
-
await fs.ensureDir(destDir);
|
|
515
|
-
|
|
516
|
-
// Download/copy all files
|
|
517
|
-
for (const file of skillFiles) {
|
|
518
|
-
const destPath = path.join(destDir, file.relative);
|
|
519
|
-
const destFileDir = path.dirname(destPath);
|
|
520
|
-
await fs.ensureDir(destFileDir);
|
|
521
|
-
|
|
522
|
-
if (IS_LOCAL) {
|
|
523
|
-
// Copy from local assets
|
|
524
|
-
await fs.copyFile(file.full, destPath);
|
|
525
|
-
} else {
|
|
526
|
-
// Download from GitHub
|
|
527
|
-
const response = await fetch(file.url);
|
|
528
|
-
if (!response.ok) {
|
|
529
|
-
console.warn(chalk.yellow(`Warning: Failed to download ${file.relative}`));
|
|
530
|
-
continue;
|
|
531
|
-
}
|
|
532
|
-
const content = await response.text();
|
|
533
|
-
await fs.writeFile(destPath, content);
|
|
534
|
-
}
|
|
535
|
-
|
|
536
|
-
console.log(chalk.dim(` Downloaded: ${file.relative}`));
|
|
537
|
-
}
|
|
538
|
-
|
|
539
|
-
console.log(chalk.green(`Successfully downloaded skill ${skillName} to ${destDir} (${skillFiles.length} files)`));
|
|
540
|
-
}
|
|
541
|
-
|
|
542
68
|
program.parse();
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "workspace-architect",
|
|
3
|
-
"version": "2.2.
|
|
3
|
+
"version": "2.2.3",
|
|
4
4
|
"description": "A comprehensive library of specialized AI agents and personas for GitHub Copilot, ranging from architectural planning and specific tech stacks to advanced cognitive reasoning models.",
|
|
5
5
|
"bin": {
|
|
6
6
|
"workspace-architect": "bin/cli.js",
|
|
@@ -8,7 +8,12 @@
|
|
|
8
8
|
},
|
|
9
9
|
"scripts": {
|
|
10
10
|
"start:registry": "verdaccio --config verdaccio/config.yaml",
|
|
11
|
+
"test": "vitest run",
|
|
12
|
+
"test:watch": "vitest",
|
|
13
|
+
"test:coverage": "vitest run --coverage",
|
|
11
14
|
"test:local": "node bin/cli.js list && node bin/cli.js download instructions a11y --dry-run",
|
|
15
|
+
"lint": "eslint .",
|
|
16
|
+
"lint:fix": "eslint . --fix",
|
|
12
17
|
"analyze": "node scripts/analysis/analyze-collections.js",
|
|
13
18
|
"generate-manifest": "node scripts/generation/generate-manifest.js",
|
|
14
19
|
"migrate-collections": "node scripts/generation/migrate-collections-format.js",
|
|
@@ -78,10 +83,16 @@
|
|
|
78
83
|
"yaml": "^2.8.2"
|
|
79
84
|
},
|
|
80
85
|
"devDependencies": {
|
|
86
|
+
"@eslint/js": "^9.39.2",
|
|
81
87
|
"@release-it/conventional-changelog": "^8.0.2",
|
|
88
|
+
"@vitest/coverage-v8": "^4.0.17",
|
|
82
89
|
"conventional-changelog-conventionalcommits": "^7.0.2",
|
|
90
|
+
"eslint": "^9.39.2",
|
|
91
|
+
"globals": "^17.0.0",
|
|
92
|
+
"memfs": "^4.52.0",
|
|
83
93
|
"release-it": "^17.11.0",
|
|
84
|
-
"verdaccio": "^5.29.0"
|
|
94
|
+
"verdaccio": "^5.29.0",
|
|
95
|
+
"vitest": "^4.0.17"
|
|
85
96
|
},
|
|
86
97
|
"type": "module"
|
|
87
98
|
}
|