workspace-architect 2.2.0 → 2.2.2
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 +74 -1
- package/bin/cli-functions.js +490 -0
- package/bin/cli.js +4 -478
- package/package.json +15 -4
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-19T01:37:53.240Z",
|
|
4
4
|
"assets": {
|
|
5
5
|
"agents": {
|
|
6
6
|
"4.1-Beast": {
|
|
@@ -483,6 +483,12 @@
|
|
|
483
483
|
"title": "octopus-deploy-release-notes-mcp",
|
|
484
484
|
"type": "agents"
|
|
485
485
|
},
|
|
486
|
+
"openapi-to-application": {
|
|
487
|
+
"path": "assets/agents/openapi-to-application.agent.md",
|
|
488
|
+
"description": "Expert assistant for generating working applications from OpenAPI specifications",
|
|
489
|
+
"title": "openapi-to-application",
|
|
490
|
+
"type": "agents"
|
|
491
|
+
},
|
|
486
492
|
"pagerduty-incident-responder": {
|
|
487
493
|
"path": "assets/agents/pagerduty-incident-responder.agent.md",
|
|
488
494
|
"description": "Responds to PagerDuty incidents by analyzing incident context, identifying recent code changes, and suggesting fixes via GitHub PRs.",
|
|
@@ -1789,6 +1795,12 @@
|
|
|
1789
1795
|
"title": "ai-prompt-engineering-safety-review",
|
|
1790
1796
|
"type": "prompts"
|
|
1791
1797
|
},
|
|
1798
|
+
"apple-appstore-reviewer": {
|
|
1799
|
+
"path": "assets/prompts/apple-appstore-reviewer.prompt.md",
|
|
1800
|
+
"description": "Serves as a reviewer of the codebase with instructions on looking for Apple App Store optimizations or rejection reasons.",
|
|
1801
|
+
"title": "apple-appstore-reviewer",
|
|
1802
|
+
"type": "prompts"
|
|
1803
|
+
},
|
|
1792
1804
|
"architecture-blueprint-generator": {
|
|
1793
1805
|
"path": "assets/prompts/architecture-blueprint-generator.prompt.md",
|
|
1794
1806
|
"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 +2299,12 @@
|
|
|
2287
2299
|
"title": "next-intl-add-language",
|
|
2288
2300
|
"type": "prompts"
|
|
2289
2301
|
},
|
|
2302
|
+
"openapi-to-application-code": {
|
|
2303
|
+
"path": "assets/prompts/openapi-to-application-code.prompt.md",
|
|
2304
|
+
"description": "Generate a complete, production-ready application from an OpenAPI specification",
|
|
2305
|
+
"title": "openapi-to-application-code",
|
|
2306
|
+
"type": "prompts"
|
|
2307
|
+
},
|
|
2290
2308
|
"php-mcp-server-generator": {
|
|
2291
2309
|
"path": "assets/prompts/php-mcp-server-generator.prompt.md",
|
|
2292
2310
|
"description": "Generate a complete PHP Model Context Protocol server project with tools, resources, prompts, and tests using the official PHP SDK",
|
|
@@ -3560,6 +3578,61 @@
|
|
|
3560
3578
|
"prompts:technology-stack-blueprint-generator"
|
|
3561
3579
|
]
|
|
3562
3580
|
},
|
|
3581
|
+
"openapi-to-application-csharp-dotnet": {
|
|
3582
|
+
"path": "assets/collections/openapi-to-application-csharp-dotnet.collection.yml",
|
|
3583
|
+
"description": "Generate production-ready .NET applications from OpenAPI specifications. Includes ASP.NET Core project scaffolding, controller generation, entity framework integration, and C# best practices.",
|
|
3584
|
+
"title": "OpenAPI to Application - C# .NET",
|
|
3585
|
+
"type": "collections",
|
|
3586
|
+
"items": [
|
|
3587
|
+
"agents:openapi-to-application",
|
|
3588
|
+
"instructions:csharp",
|
|
3589
|
+
"prompts:openapi-to-application-code"
|
|
3590
|
+
]
|
|
3591
|
+
},
|
|
3592
|
+
"openapi-to-application-go": {
|
|
3593
|
+
"path": "assets/collections/openapi-to-application-go.collection.yml",
|
|
3594
|
+
"description": "Generate production-ready Go applications from OpenAPI specifications. Includes project scaffolding, handler generation, middleware setup, and Go best practices for REST APIs.",
|
|
3595
|
+
"title": "OpenAPI to Application - Go",
|
|
3596
|
+
"type": "collections",
|
|
3597
|
+
"items": [
|
|
3598
|
+
"agents:openapi-to-application",
|
|
3599
|
+
"instructions:go",
|
|
3600
|
+
"prompts:openapi-to-application-code"
|
|
3601
|
+
]
|
|
3602
|
+
},
|
|
3603
|
+
"openapi-to-application-java-spring-boot": {
|
|
3604
|
+
"path": "assets/collections/openapi-to-application-java-spring-boot.collection.yml",
|
|
3605
|
+
"description": "Generate production-ready Spring Boot applications from OpenAPI specifications. Includes project scaffolding, REST controller generation, service layer organization, and Spring Boot best practices.",
|
|
3606
|
+
"title": "OpenAPI to Application - Java Spring Boot",
|
|
3607
|
+
"type": "collections",
|
|
3608
|
+
"items": [
|
|
3609
|
+
"agents:openapi-to-application",
|
|
3610
|
+
"instructions:springboot",
|
|
3611
|
+
"prompts:openapi-to-application-code"
|
|
3612
|
+
]
|
|
3613
|
+
},
|
|
3614
|
+
"openapi-to-application-nodejs-nestjs": {
|
|
3615
|
+
"path": "assets/collections/openapi-to-application-nodejs-nestjs.collection.yml",
|
|
3616
|
+
"description": "Generate production-ready NestJS applications from OpenAPI specifications. Includes project scaffolding, controller and service generation, TypeScript best practices, and enterprise patterns.",
|
|
3617
|
+
"title": "OpenAPI to Application - Node.js NestJS",
|
|
3618
|
+
"type": "collections",
|
|
3619
|
+
"items": [
|
|
3620
|
+
"agents:openapi-to-application",
|
|
3621
|
+
"instructions:nestjs",
|
|
3622
|
+
"prompts:openapi-to-application-code"
|
|
3623
|
+
]
|
|
3624
|
+
},
|
|
3625
|
+
"openapi-to-application-python-fastapi": {
|
|
3626
|
+
"path": "assets/collections/openapi-to-application-python-fastapi.collection.yml",
|
|
3627
|
+
"description": "Generate production-ready FastAPI applications from OpenAPI specifications. Includes project scaffolding, route generation, dependency injection, and Python best practices for async APIs.",
|
|
3628
|
+
"title": "OpenAPI to Application - Python FastAPI",
|
|
3629
|
+
"type": "collections",
|
|
3630
|
+
"items": [
|
|
3631
|
+
"agents:openapi-to-application",
|
|
3632
|
+
"instructions:python",
|
|
3633
|
+
"prompts:openapi-to-application-code"
|
|
3634
|
+
]
|
|
3635
|
+
},
|
|
3563
3636
|
"partners": {
|
|
3564
3637
|
"path": "assets/collections/partners.collection.yml",
|
|
3565
3638
|
"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,14 +1,19 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "workspace-architect",
|
|
3
|
-
"version": "2.2.
|
|
3
|
+
"version": "2.2.2",
|
|
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
|
-
"workspace-architect": "
|
|
7
|
-
"wsa": "
|
|
6
|
+
"workspace-architect": "bin/cli.js",
|
|
7
|
+
"wsa": "bin/cli.js"
|
|
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
|
}
|