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.
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "version": "2.0.0",
3
- "generatedAt": "2026-01-16T01:10:43.866Z",
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 { fileURLToPath } from 'url';
9
-
10
- const __filename = fileURLToPath(import.meta.url);
11
- const __dirname = path.dirname(__filename);
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.1",
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
  }