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.
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "version": "2.0.0",
3
- "generatedAt": "2026-01-16T00:30:47.177Z",
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 { 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,14 +1,19 @@
1
1
  {
2
2
  "name": "workspace-architect",
3
- "version": "2.2.0",
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": "./bin/cli.js",
7
- "wsa": "./bin/cli.js"
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
  }