@agiflowai/aicode-toolkit 0.6.0 → 1.0.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/dist/cli.cjs +401 -123
- package/dist/cli.js +401 -124
- package/dist/index.cjs +1 -1
- package/dist/index.d.cts +15 -1
- package/dist/index.d.ts +15 -1
- package/dist/index.js +1 -1
- package/dist/{services-s1vmufE4.cjs → services-C6lqyioO.cjs} +243 -26
- package/dist/{services-DNldrNnu.js → services-zrdafWTg.js} +257 -60
- package/package.json +6 -4
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
import path from "node:path";
|
|
2
2
|
import { ProjectType, messages, print } from "@agiflowai/aicode-utils";
|
|
3
|
-
import * as fs from "fs-extra";
|
|
3
|
+
import * as fs$1 from "fs-extra";
|
|
4
4
|
import chalk from "chalk";
|
|
5
5
|
import gradient from "gradient-string";
|
|
6
6
|
import { execa } from "execa";
|
|
7
|
-
import { CLAUDE_CODE, CODEX, ClaudeCodeService, CodexService, GEMINI_CLI, GeminiCliService, NONE } from "@agiflowai/coding-agent-bridge";
|
|
7
|
+
import { CLAUDE_CODE, CODEX, CURSOR, ClaudeCodeService, CodexService, CursorService, GEMINI_CLI, GITHUB_COPILOT, GeminiCliService, GitHubCopilotService, NONE } from "@agiflowai/coding-agent-bridge";
|
|
8
|
+
import fs from "node:fs/promises";
|
|
9
|
+
import { Liquid } from "liquidjs";
|
|
8
10
|
import os from "node:os";
|
|
9
11
|
|
|
10
12
|
//#region src/constants/theme.ts
|
|
@@ -144,7 +146,7 @@ async function findWorkspaceRoot(startPath = process.cwd()) {
|
|
|
144
146
|
const rootPath = path.parse(currentPath).root;
|
|
145
147
|
while (true) {
|
|
146
148
|
const gitPath = path.join(currentPath, ".git");
|
|
147
|
-
if (await fs.pathExists(gitPath)) return currentPath;
|
|
149
|
+
if (await fs$1.pathExists(gitPath)) return currentPath;
|
|
148
150
|
if (currentPath === rootPath) return null;
|
|
149
151
|
currentPath = path.dirname(currentPath);
|
|
150
152
|
}
|
|
@@ -163,11 +165,11 @@ function parseGitHubUrl(url) {
|
|
|
163
165
|
if (treeMatch || blobMatch) {
|
|
164
166
|
const match = treeMatch || blobMatch;
|
|
165
167
|
return {
|
|
166
|
-
owner: match[1],
|
|
167
|
-
repo: match[2],
|
|
168
|
-
repoUrl: `https://github.com/${match[1]}/${match[2]}.git`,
|
|
169
|
-
branch: match[3],
|
|
170
|
-
subdirectory: match[4],
|
|
168
|
+
owner: match?.[1],
|
|
169
|
+
repo: match?.[2],
|
|
170
|
+
repoUrl: `https://github.com/${match?.[1]}/${match?.[2]}.git`,
|
|
171
|
+
branch: match?.[3],
|
|
172
|
+
subdirectory: match?.[4],
|
|
171
173
|
isSubdirectory: true
|
|
172
174
|
};
|
|
173
175
|
}
|
|
@@ -201,7 +203,7 @@ async function cloneSubdirectory(repoUrl, branch, subdirectory, targetFolder) {
|
|
|
201
203
|
"true"
|
|
202
204
|
], tempFolder);
|
|
203
205
|
const sparseCheckoutFile = path.join(tempFolder, ".git", "info", "sparse-checkout");
|
|
204
|
-
await fs.writeFile(sparseCheckoutFile, `${subdirectory}\n`);
|
|
206
|
+
await fs$1.writeFile(sparseCheckoutFile, `${subdirectory}\n`);
|
|
205
207
|
await execGit([
|
|
206
208
|
"pull",
|
|
207
209
|
"--depth=1",
|
|
@@ -209,12 +211,12 @@ async function cloneSubdirectory(repoUrl, branch, subdirectory, targetFolder) {
|
|
|
209
211
|
branch
|
|
210
212
|
], tempFolder);
|
|
211
213
|
const sourceDir = path.join(tempFolder, subdirectory);
|
|
212
|
-
if (!await fs.pathExists(sourceDir)) throw new Error(`Subdirectory '${subdirectory}' not found in repository at branch '${branch}'`);
|
|
213
|
-
if (await fs.pathExists(targetFolder)) throw new Error(`Target folder already exists: ${targetFolder}`);
|
|
214
|
-
await fs.move(sourceDir, targetFolder);
|
|
215
|
-
await fs.remove(tempFolder);
|
|
214
|
+
if (!await fs$1.pathExists(sourceDir)) throw new Error(`Subdirectory '${subdirectory}' not found in repository at branch '${branch}'`);
|
|
215
|
+
if (await fs$1.pathExists(targetFolder)) throw new Error(`Target folder already exists: ${targetFolder}`);
|
|
216
|
+
await fs$1.move(sourceDir, targetFolder);
|
|
217
|
+
await fs$1.remove(tempFolder);
|
|
216
218
|
} catch (error) {
|
|
217
|
-
if (await fs.pathExists(tempFolder)) await fs.remove(tempFolder);
|
|
219
|
+
if (await fs$1.pathExists(tempFolder)) await fs$1.remove(tempFolder);
|
|
218
220
|
throw error;
|
|
219
221
|
}
|
|
220
222
|
}
|
|
@@ -228,7 +230,7 @@ async function cloneRepository(repoUrl, targetFolder) {
|
|
|
228
230
|
targetFolder
|
|
229
231
|
]);
|
|
230
232
|
const gitFolder = path.join(targetFolder, ".git");
|
|
231
|
-
if (await fs.pathExists(gitFolder)) await fs.remove(gitFolder);
|
|
233
|
+
if (await fs$1.pathExists(gitFolder)) await fs$1.remove(gitFolder);
|
|
232
234
|
}
|
|
233
235
|
/**
|
|
234
236
|
* Fetch directory listing from GitHub API
|
|
@@ -258,12 +260,14 @@ var CodingAgentService = class {
|
|
|
258
260
|
}
|
|
259
261
|
/**
|
|
260
262
|
* Detect which coding agent is enabled in the workspace
|
|
261
|
-
* Checks for Claude Code, Codex,
|
|
263
|
+
* Checks for Claude Code, Codex, Gemini CLI, GitHub Copilot, and Cursor installations
|
|
262
264
|
* @param workspaceRoot - The workspace root directory
|
|
263
265
|
* @returns Promise resolving to detected agent ID or null
|
|
264
266
|
*/
|
|
265
267
|
static async detectCodingAgent(workspaceRoot) {
|
|
266
268
|
if (await new ClaudeCodeService({ workspaceRoot }).isEnabled()) return CLAUDE_CODE;
|
|
269
|
+
if (await new CursorService({ workspaceRoot }).isEnabled()) return CURSOR;
|
|
270
|
+
if (await new GitHubCopilotService({ workspaceRoot }).isEnabled()) return GITHUB_COPILOT;
|
|
267
271
|
if (await new CodexService({ workspaceRoot }).isEnabled()) return CODEX;
|
|
268
272
|
if (await new GeminiCliService({ workspaceRoot }).isEnabled()) return GEMINI_CLI;
|
|
269
273
|
return null;
|
|
@@ -278,6 +282,16 @@ var CodingAgentService = class {
|
|
|
278
282
|
name: "Claude Code",
|
|
279
283
|
description: "Anthropic Claude Code CLI agent"
|
|
280
284
|
},
|
|
285
|
+
{
|
|
286
|
+
value: CURSOR,
|
|
287
|
+
name: "Cursor",
|
|
288
|
+
description: "Cursor AI-first code editor"
|
|
289
|
+
},
|
|
290
|
+
{
|
|
291
|
+
value: GITHUB_COPILOT,
|
|
292
|
+
name: "GitHub Copilot",
|
|
293
|
+
description: "GitHub Copilot coding agent and CLI"
|
|
294
|
+
},
|
|
281
295
|
{
|
|
282
296
|
value: CODEX,
|
|
283
297
|
name: "Codex",
|
|
@@ -296,6 +310,48 @@ var CodingAgentService = class {
|
|
|
296
310
|
];
|
|
297
311
|
}
|
|
298
312
|
/**
|
|
313
|
+
* Get the coding agent service instance
|
|
314
|
+
* @param agent - The coding agent to get service for
|
|
315
|
+
* @returns The service instance or null if not supported
|
|
316
|
+
*/
|
|
317
|
+
getCodingAgentService(agent) {
|
|
318
|
+
if (agent === CLAUDE_CODE) return new ClaudeCodeService({ workspaceRoot: this.workspaceRoot });
|
|
319
|
+
if (agent === CURSOR) return new CursorService({ workspaceRoot: this.workspaceRoot });
|
|
320
|
+
if (agent === GITHUB_COPILOT) return new GitHubCopilotService({ workspaceRoot: this.workspaceRoot });
|
|
321
|
+
if (agent === CODEX) return new CodexService({ workspaceRoot: this.workspaceRoot });
|
|
322
|
+
if (agent === GEMINI_CLI) return new GeminiCliService({ workspaceRoot: this.workspaceRoot });
|
|
323
|
+
return null;
|
|
324
|
+
}
|
|
325
|
+
/**
|
|
326
|
+
* Update custom instructions/prompts for the coding agent
|
|
327
|
+
* Appends custom instruction prompt to the agent's configuration
|
|
328
|
+
* @param agent - The coding agent to update
|
|
329
|
+
* @param instructionPrompt - The instruction prompt to append
|
|
330
|
+
* @param customInstructionFile - Optional custom file path to write instructions to (e.g., '.claude/aicode-instructions.md')
|
|
331
|
+
*/
|
|
332
|
+
async updateCustomInstructions(agent, instructionPrompt, customInstructionFile) {
|
|
333
|
+
if (agent === NONE) {
|
|
334
|
+
print.info("Skipping custom instruction update");
|
|
335
|
+
return;
|
|
336
|
+
}
|
|
337
|
+
print.info(`\nUpdating custom instructions for ${agent}...`);
|
|
338
|
+
const service = this.getCodingAgentService(agent);
|
|
339
|
+
if (!service) {
|
|
340
|
+
print.info(`Custom instruction update for ${agent} is not yet supported.`);
|
|
341
|
+
print.info("Please manually add the instructions to your agent configuration.");
|
|
342
|
+
print.info("\nInstruction prompt to add:");
|
|
343
|
+
print.info(instructionPrompt);
|
|
344
|
+
return;
|
|
345
|
+
}
|
|
346
|
+
await service.updatePrompt({
|
|
347
|
+
systemPrompt: instructionPrompt,
|
|
348
|
+
customInstructionFile,
|
|
349
|
+
marker: true
|
|
350
|
+
});
|
|
351
|
+
if (customInstructionFile) print.success(`Custom instructions written to ${customInstructionFile} and referenced in CLAUDE.md and AGENTS.md`);
|
|
352
|
+
else print.success(`Custom instructions appended to CLAUDE.md and AGENTS.md`);
|
|
353
|
+
}
|
|
354
|
+
/**
|
|
299
355
|
* Setup MCP configuration for the selected coding agent
|
|
300
356
|
* @param agent - The coding agent to configure
|
|
301
357
|
*/
|
|
@@ -305,19 +361,22 @@ var CodingAgentService = class {
|
|
|
305
361
|
return;
|
|
306
362
|
}
|
|
307
363
|
print.info(`\nSetting up MCP for ${agent}...`);
|
|
308
|
-
|
|
364
|
+
const service = this.getCodingAgentService(agent);
|
|
309
365
|
let configLocation = "";
|
|
310
366
|
let restartInstructions = "";
|
|
311
367
|
if (agent === CLAUDE_CODE) {
|
|
312
|
-
service = new ClaudeCodeService({ workspaceRoot: this.workspaceRoot });
|
|
313
368
|
configLocation = ".mcp.json";
|
|
314
369
|
restartInstructions = "Restart Claude Code to load the new MCP servers";
|
|
370
|
+
} else if (agent === CURSOR) {
|
|
371
|
+
configLocation = "~/.cursor/mcp.json (or .cursor/mcp.json for workspace)";
|
|
372
|
+
restartInstructions = "Restart Cursor to load the new MCP servers";
|
|
373
|
+
} else if (agent === GITHUB_COPILOT) {
|
|
374
|
+
configLocation = "~/.copilot/config.json (CLI) or GitHub UI (Coding Agent)";
|
|
375
|
+
restartInstructions = "Restart GitHub Copilot CLI or configure via GitHub repository settings";
|
|
315
376
|
} else if (agent === CODEX) {
|
|
316
|
-
service = new CodexService({ workspaceRoot: this.workspaceRoot });
|
|
317
377
|
configLocation = "~/.codex/config.toml";
|
|
318
378
|
restartInstructions = "Restart Codex CLI to load the new MCP servers";
|
|
319
379
|
} else if (agent === GEMINI_CLI) {
|
|
320
|
-
service = new GeminiCliService({ workspaceRoot: this.workspaceRoot });
|
|
321
380
|
configLocation = "~/.gemini/settings.json";
|
|
322
381
|
restartInstructions = "Restart Gemini CLI to load the new MCP servers";
|
|
323
382
|
}
|
|
@@ -431,8 +490,7 @@ var NewProjectService = class {
|
|
|
431
490
|
*/
|
|
432
491
|
async createProjectDirectory(projectPath, projectName) {
|
|
433
492
|
try {
|
|
434
|
-
await fs.mkdir(projectPath, { recursive: false });
|
|
435
|
-
print.success(`Created project directory: ${projectPath}`);
|
|
493
|
+
await fs$1.mkdir(projectPath, { recursive: false });
|
|
436
494
|
} catch (error) {
|
|
437
495
|
if (error.code === "EEXIST") throw new Error(`Directory '${projectName}' already exists. Please choose a different name.`);
|
|
438
496
|
throw error;
|
|
@@ -444,14 +502,12 @@ var NewProjectService = class {
|
|
|
444
502
|
* @param projectPath - Destination path for the cloned repository
|
|
445
503
|
*/
|
|
446
504
|
async cloneExistingRepository(repoUrl, projectPath) {
|
|
447
|
-
print.info("Cloning repository...");
|
|
448
505
|
try {
|
|
449
506
|
const parsed = parseGitHubUrl(repoUrl.trim());
|
|
450
507
|
if (parsed.isSubdirectory && parsed.branch && parsed.subdirectory) await cloneSubdirectory(parsed.repoUrl, parsed.branch, parsed.subdirectory, projectPath);
|
|
451
508
|
else await cloneRepository(parsed.repoUrl, projectPath);
|
|
452
|
-
print.success("Repository cloned successfully");
|
|
453
509
|
} catch (error) {
|
|
454
|
-
await fs.remove(projectPath);
|
|
510
|
+
await fs$1.remove(projectPath);
|
|
455
511
|
throw new Error(`Failed to clone repository: ${error.message}`);
|
|
456
512
|
}
|
|
457
513
|
}
|
|
@@ -460,10 +516,8 @@ var NewProjectService = class {
|
|
|
460
516
|
* @param projectPath - Path where git repository should be initialized
|
|
461
517
|
*/
|
|
462
518
|
async initializeGitRepository(projectPath) {
|
|
463
|
-
print.info("Initializing Git repository...");
|
|
464
519
|
try {
|
|
465
520
|
await gitInit(projectPath);
|
|
466
|
-
print.success("Git repository initialized");
|
|
467
521
|
} catch (error) {
|
|
468
522
|
messages.warning(`Failed to initialize Git: ${error.message}`);
|
|
469
523
|
}
|
|
@@ -480,6 +534,153 @@ var NewProjectService = class {
|
|
|
480
534
|
}
|
|
481
535
|
};
|
|
482
536
|
|
|
537
|
+
//#endregion
|
|
538
|
+
//#region src/instructions/specs/openspec.md?raw
|
|
539
|
+
var openspec_default = "When working on this project, follow the OpenSpec spec-driven development workflow{% if scaffoldMcp or architectMcp %} integrated with MCP tools{% endif %}.\n\n{% if scaffoldMcp %}\n\n## {% if scaffoldMcp %}1{% else %}1{% endif %}. Create Proposals with scaffold-mcp\n\nWhen implementing new features or changes, use scaffold-mcp MCP tools:\n\n**For new projects/features:**\n1. Use `list-boilerplates` MCP tool to see available templates\n2. Use `use-boilerplate` MCP tool to scaffold new projects{% if projectType == 'monolith' %} (set `monolith: true`){% elsif projectType == 'monorepo' %} (omit `monolith` parameter){% endif %}\n3. Use `list-scaffolding-methods` MCP tool to understand which methods can be used to add features\n4. Create OpenSpec proposal with available scaffolding methods in mind: \"Create an OpenSpec proposal for [feature description]\"\n\n**For adding features to existing code:**\n1. Use `list-scaffolding-methods` MCP tool with projectPath to see available features{% if projectType == 'monolith' %} (`projectPath` = workspace root){% elsif projectType == 'monorepo' %} (`projectPath` = project directory with project.json){% endif %}\n2. Review available methods and plan which ones to use for the feature\n3. Use `use-scaffold-method` MCP tool to generate boilerplate code\n4. Create OpenSpec proposal to capture the specs\n{% if projectType %}\n\n**Path Mapping ({% if projectType == 'monolith' %}Monolith{% else %}Monorepo{% endif %} Project):**\n{% if projectType == 'monolith' %}\n*This is a monolith project:*\n- Single project at workspace root\n- Config file: `toolkit.yaml` at root\n- All code in workspace root (src/, lib/, etc.)\n- When scaffolding: Use `monolith: true` parameter\n- When using scaffold methods: `projectPath` = workspace root\n\nExample: For workspace at `/path/to/project`:\n- Project config: `/path/to/project/toolkit.yaml`\n- Source code: `/path/to/project/src/`\n- OpenSpec: `/path/to/project/openspec/`\n{% else %}\n*This is a monorepo project:*\n- Multiple projects in subdirectories\n- Config file: `project.json` in each project\n- Projects in apps/, packages/, libs/, etc.\n- When scaffolding: Omit `monolith` parameter (defaults to false)\n- When using scaffold methods: `projectPath` = path to specific project\n\nExample: For workspace at `/path/to/workspace`:\n- App config: `/path/to/workspace/apps/my-app/project.json`\n- App source: `/path/to/workspace/apps/my-app/src/`\n- OpenSpec: `/path/to/workspace/openspec/` (workspace-level)\n{% endif %}\n{% endif %}\n\nAI will scaffold: openspec/changes/[feature-name]/ with proposal.md, tasks.md, and spec deltas\n{% endif %}\n{% if architectMcp %}\n\n## {% if scaffoldMcp %}2{% else %}1{% endif %}. Review & Validate with architect-mcp\n\nBefore and after editing files, use architect-mcp MCP tools:\n\n**Before editing:**\n- Use `get-file-design-pattern` MCP tool to understand:\n - Applicable design patterns from architect.yaml\n - Coding rules from RULES.yaml (must_do, should_do, must_not_do)\n - Code examples showing the patterns\n\n**After editing:**\n- Use `review-code-change` MCP tool to check for:\n - Must not do violations (critical issues)\n - Must do missing (required patterns not followed)\n - Should do suggestions (best practices)\n\n**Validate OpenSpec specs:**\n- Use `openspec validate [feature-name]` to check spec formatting\n- Iterate with AI until specs are agreed upon\n{% endif %}\n\n## {% if scaffoldMcp and architectMcp %}3{% elsif scaffoldMcp or architectMcp %}2{% else %}1{% endif %}. Implement{% if architectMcp %} with MCP-Guided Development{% endif %}\n\nDuring implementation:\n1. Ask AI to implement: \"Apply the OpenSpec change [feature-name]\"\n{% if architectMcp %}\n2. **Before each file edit**: Use `get-file-design-pattern` to understand patterns\n3. AI implements tasks from tasks.md following design patterns\n4. **After each file edit**: Use `review-code-change` to verify compliance\n5. Fix any violations before proceeding\n{% else %}\n2. AI implements tasks from tasks.md following the agreed specs\n{% endif %}\n\n## {% if scaffoldMcp and architectMcp %}4{% elsif scaffoldMcp or architectMcp %}3{% else %}2{% endif %}. Archive Completed Changes\n{% if scaffoldMcp or architectMcp %}\n\n## MCP Tools Reference\n{% endif %}\n{% if scaffoldMcp %}\n\n### scaffold-mcp\n- `list-boilerplates` - List available project templates\n- `use-boilerplate` - Create new project from template\n- `list-scaffolding-methods` - List features for existing project\n- `use-scaffold-method` - Add feature to existing project\n{% endif %}\n{% if architectMcp %}\n\n### architect-mcp\n- `get-file-design-pattern` - Get design patterns for file\n- `review-code-change` - Review code for violations\n{% endif %}\n\n## Workflow Summary\n\n{% if scaffoldMcp %}\n1. **Plan**: Use scaffold-mcp to generate boilerplate + OpenSpec proposal for specs\n{% else %}\n1. **Plan**: Create OpenSpec proposal with specs\n{% endif %}\n{% if architectMcp %}\n{% if scaffoldMcp %}2{% else %}2{% endif %}. **Design**: Use architect-mcp to understand patterns before editing\n{% endif %}\n{% if scaffoldMcp and architectMcp %}3{% elsif scaffoldMcp or architectMcp %}{% if architectMcp %}3{% else %}2{% endif %}{% else %}2{% endif %}. **Implement**: Follow specs{% if architectMcp %} and patterns{% endif %}\n{% if architectMcp %}\n{% if scaffoldMcp %}4{% else %}4{% endif %}. **Review**: Use architect-mcp to validate code quality\n{% endif %}\n{% if scaffoldMcp and architectMcp %}5{% elsif scaffoldMcp or architectMcp %}{% if architectMcp %}5{% else %}3{% endif %}{% else %}3{% endif %}. **Archive**: Merge specs into source of truth\n";
|
|
540
|
+
|
|
541
|
+
//#endregion
|
|
542
|
+
//#region src/specs/openspec.ts
|
|
543
|
+
/**
|
|
544
|
+
* OpenSpec configuration
|
|
545
|
+
*/
|
|
546
|
+
const OPENSPEC_CONFIG = {
|
|
547
|
+
folderName: "openspec",
|
|
548
|
+
packageName: "@fission-ai/openspec",
|
|
549
|
+
name: "OpenSpec",
|
|
550
|
+
description: "Spec-driven development for AI coding assistants"
|
|
551
|
+
};
|
|
552
|
+
/**
|
|
553
|
+
* Bridge implementation for OpenSpec
|
|
554
|
+
*
|
|
555
|
+
* Provides integration with OpenSpec spec tool through a standardized interface.
|
|
556
|
+
*/
|
|
557
|
+
var OpenSpecBridge = class {
|
|
558
|
+
/**
|
|
559
|
+
* Check if OpenSpec is enabled/installed in the workspace
|
|
560
|
+
*
|
|
561
|
+
* @param workspaceRoot - Absolute path to the workspace root directory
|
|
562
|
+
* @returns Promise resolving to true if OpenSpec folder exists, false otherwise
|
|
563
|
+
*/
|
|
564
|
+
async isEnabled(workspaceRoot) {
|
|
565
|
+
try {
|
|
566
|
+
const openspecPath = path.join(workspaceRoot, OPENSPEC_CONFIG.folderName);
|
|
567
|
+
await fs.access(openspecPath);
|
|
568
|
+
return true;
|
|
569
|
+
} catch {
|
|
570
|
+
return false;
|
|
571
|
+
}
|
|
572
|
+
}
|
|
573
|
+
/**
|
|
574
|
+
* Initialize OpenSpec in the workspace
|
|
575
|
+
*
|
|
576
|
+
* Runs `npx @fission-ai/openspec init` interactively, allowing the user
|
|
577
|
+
* to configure OpenSpec during setup.
|
|
578
|
+
*
|
|
579
|
+
* @param workspaceRoot - Absolute path to the workspace root directory
|
|
580
|
+
* @throws Error if initialization fails
|
|
581
|
+
*/
|
|
582
|
+
async initialize(workspaceRoot) {
|
|
583
|
+
try {
|
|
584
|
+
await execa("npx", [OPENSPEC_CONFIG.packageName, "init"], {
|
|
585
|
+
cwd: workspaceRoot,
|
|
586
|
+
stdio: "inherit"
|
|
587
|
+
});
|
|
588
|
+
} catch (error) {
|
|
589
|
+
throw new Error(`Failed to initialize ${OPENSPEC_CONFIG.name}: ${error.message}`);
|
|
590
|
+
}
|
|
591
|
+
}
|
|
592
|
+
/**
|
|
593
|
+
* Generate agent instruction prompt for OpenSpec with MCP integration
|
|
594
|
+
*
|
|
595
|
+
* Creates a comprehensive instruction prompt that guides AI agents to use
|
|
596
|
+
* spec-driven development workflow with OpenSpec, leveraging enabled MCP servers.
|
|
597
|
+
*
|
|
598
|
+
* @param enabledMcps - Configuration of which MCP servers are enabled
|
|
599
|
+
* @returns Promise resolving to the instruction prompt string
|
|
600
|
+
*/
|
|
601
|
+
async updateInstruction(enabledMcps) {
|
|
602
|
+
return (await new Liquid().parseAndRender(openspec_default, enabledMcps)).trim();
|
|
603
|
+
}
|
|
604
|
+
};
|
|
605
|
+
|
|
606
|
+
//#endregion
|
|
607
|
+
//#region src/services/SpecToolService.ts
|
|
608
|
+
/**
|
|
609
|
+
* Available spec tools
|
|
610
|
+
*/
|
|
611
|
+
let SpecTool = /* @__PURE__ */ function(SpecTool$1) {
|
|
612
|
+
SpecTool$1["OPENSPEC"] = "openspec";
|
|
613
|
+
return SpecTool$1;
|
|
614
|
+
}({});
|
|
615
|
+
/**
|
|
616
|
+
* Spec tool information
|
|
617
|
+
*/
|
|
618
|
+
const SPEC_TOOL_INFO = { [SpecTool.OPENSPEC]: {
|
|
619
|
+
name: "OpenSpec",
|
|
620
|
+
description: "Spec-driven development for AI coding assistants",
|
|
621
|
+
docUrl: "https://github.com/Fission-AI/OpenSpec"
|
|
622
|
+
} };
|
|
623
|
+
/**
|
|
624
|
+
* Service for managing spec tools (e.g., OpenSpec)
|
|
625
|
+
*
|
|
626
|
+
* This service acts as a facade for spec tool operations, using bridge
|
|
627
|
+
* implementations to interact with specific spec tools.
|
|
628
|
+
*/
|
|
629
|
+
var SpecToolService = class {
|
|
630
|
+
bridge;
|
|
631
|
+
constructor(workspaceRoot, specTool = SpecTool.OPENSPEC, codingAgentService) {
|
|
632
|
+
this.workspaceRoot = workspaceRoot;
|
|
633
|
+
this.codingAgentService = codingAgentService;
|
|
634
|
+
this.bridge = this.createBridge(specTool);
|
|
635
|
+
}
|
|
636
|
+
/**
|
|
637
|
+
* Create a bridge instance for the specified spec tool
|
|
638
|
+
*/
|
|
639
|
+
createBridge(specTool) {
|
|
640
|
+
switch (specTool) {
|
|
641
|
+
case SpecTool.OPENSPEC: return new OpenSpecBridge();
|
|
642
|
+
default: throw new Error(`Unsupported spec tool: ${specTool}`);
|
|
643
|
+
}
|
|
644
|
+
}
|
|
645
|
+
/**
|
|
646
|
+
* Detect if a spec tool is installed in the workspace
|
|
647
|
+
*/
|
|
648
|
+
async detectSpecTool() {
|
|
649
|
+
try {
|
|
650
|
+
return await this.bridge.isEnabled(this.workspaceRoot) ? SpecTool.OPENSPEC : null;
|
|
651
|
+
} catch (error) {
|
|
652
|
+
print.error(`Error detecting spec tool: ${error.message}`);
|
|
653
|
+
return null;
|
|
654
|
+
}
|
|
655
|
+
}
|
|
656
|
+
/**
|
|
657
|
+
* Initialize the spec tool in the workspace
|
|
658
|
+
*/
|
|
659
|
+
async initializeSpec() {
|
|
660
|
+
await this.bridge.initialize(this.workspaceRoot);
|
|
661
|
+
}
|
|
662
|
+
/**
|
|
663
|
+
* Update spec tool agent instructions
|
|
664
|
+
*
|
|
665
|
+
* Generates instruction prompt and updates the coding agent's custom instructions
|
|
666
|
+
*
|
|
667
|
+
* @param enabledMcps - Configuration of which MCP servers are enabled
|
|
668
|
+
* @param codingAgent - The coding agent to update instructions for
|
|
669
|
+
* @param customInstructionFile - Optional custom file to write instructions to (e.g., '.claude/aicode-instructions.md')
|
|
670
|
+
* @returns Promise resolving to the instruction prompt string
|
|
671
|
+
*/
|
|
672
|
+
async updateInstructions(enabledMcps, codingAgent, customInstructionFile) {
|
|
673
|
+
const prompt = await this.bridge.updateInstruction(enabledMcps);
|
|
674
|
+
if (this.codingAgentService && codingAgent) await this.codingAgentService.updateCustomInstructions(codingAgent, prompt, customInstructionFile ?? ".claude/aicode-instructions.md");
|
|
675
|
+
else {
|
|
676
|
+
print.info("\nGenerated OpenSpec instruction prompt:");
|
|
677
|
+
print.info("\nYou can append this to your CLAUDE.md or agent config file:");
|
|
678
|
+
print.info(`\n${prompt}`);
|
|
679
|
+
}
|
|
680
|
+
return prompt;
|
|
681
|
+
}
|
|
682
|
+
};
|
|
683
|
+
|
|
483
684
|
//#endregion
|
|
484
685
|
//#region src/services/TemplateSelectionService.ts
|
|
485
686
|
var TemplateSelectionService = class {
|
|
@@ -493,9 +694,8 @@ var TemplateSelectionService = class {
|
|
|
493
694
|
* @returns Path to the tmp directory containing templates
|
|
494
695
|
*/
|
|
495
696
|
async downloadTemplatesToTmp(repoConfig) {
|
|
496
|
-
print.info(`Downloading templates from ${repoConfig.owner}/${repoConfig.repo}...`);
|
|
497
697
|
try {
|
|
498
|
-
await fs.ensureDir(this.tmpDir);
|
|
698
|
+
await fs$1.ensureDir(this.tmpDir);
|
|
499
699
|
const contents = await fetchGitHubDirectoryContents(repoConfig.owner, repoConfig.repo, repoConfig.path, repoConfig.branch);
|
|
500
700
|
const templateDirs = contents.filter((item) => item.type === "dir");
|
|
501
701
|
const globalFiles = contents.filter((item) => item.type === "file" && item.name === "RULES.yaml");
|
|
@@ -514,7 +714,7 @@ var TemplateSelectionService = class {
|
|
|
514
714
|
const response = await fetch(rulesUrl);
|
|
515
715
|
if (response.ok) {
|
|
516
716
|
const content = await response.text();
|
|
517
|
-
await fs.writeFile(targetFile, content, "utf-8");
|
|
717
|
+
await fs$1.writeFile(targetFile, content, "utf-8");
|
|
518
718
|
print.success("Downloaded global RULES.yaml");
|
|
519
719
|
}
|
|
520
720
|
}
|
|
@@ -531,7 +731,7 @@ var TemplateSelectionService = class {
|
|
|
531
731
|
*/
|
|
532
732
|
async listTemplates() {
|
|
533
733
|
try {
|
|
534
|
-
const entries = await fs.readdir(this.tmpDir, { withFileTypes: true });
|
|
734
|
+
const entries = await fs$1.readdir(this.tmpDir, { withFileTypes: true });
|
|
535
735
|
const templates = [];
|
|
536
736
|
for (const entry of entries) if (entry.isDirectory()) {
|
|
537
737
|
const templatePath = path.join(this.tmpDir, entry.name);
|
|
@@ -557,27 +757,27 @@ var TemplateSelectionService = class {
|
|
|
557
757
|
async copyTemplates(templateNames, destinationPath, projectType, selectedMcpServers) {
|
|
558
758
|
try {
|
|
559
759
|
if (projectType === ProjectType.MONOLITH && templateNames.length > 1) throw new Error("Monolith projects can only use a single template");
|
|
560
|
-
await fs.ensureDir(destinationPath);
|
|
760
|
+
await fs$1.ensureDir(destinationPath);
|
|
561
761
|
print.info(`\nCopying templates to ${destinationPath}...`);
|
|
562
762
|
for (const templateName of templateNames) {
|
|
563
763
|
const sourcePath = path.join(this.tmpDir, templateName);
|
|
564
764
|
const targetPath = path.join(destinationPath, templateName);
|
|
565
|
-
if (!await fs.pathExists(sourcePath)) throw new Error(`Template '${templateName}' not found in downloaded templates`);
|
|
566
|
-
if (await fs.pathExists(targetPath)) {
|
|
765
|
+
if (!await fs$1.pathExists(sourcePath)) throw new Error(`Template '${templateName}' not found in downloaded templates`);
|
|
766
|
+
if (await fs$1.pathExists(targetPath)) {
|
|
567
767
|
print.info(`Skipping ${templateName} (already exists)`);
|
|
568
768
|
continue;
|
|
569
769
|
}
|
|
570
770
|
print.info(`Copying ${templateName}...`);
|
|
571
771
|
if (selectedMcpServers && selectedMcpServers.length > 0) await this.copyTemplateWithMcpFilter(sourcePath, targetPath, selectedMcpServers);
|
|
572
|
-
else await fs.copy(sourcePath, targetPath);
|
|
772
|
+
else await fs$1.copy(sourcePath, targetPath);
|
|
573
773
|
print.success(`Copied ${templateName}`);
|
|
574
774
|
}
|
|
575
775
|
const globalRulesSource = path.join(this.tmpDir, "RULES.yaml");
|
|
576
776
|
const globalRulesTarget = path.join(destinationPath, "RULES.yaml");
|
|
577
|
-
if (await fs.pathExists(globalRulesSource)) {
|
|
578
|
-
if (!await fs.pathExists(globalRulesTarget)) {
|
|
777
|
+
if (await fs$1.pathExists(globalRulesSource)) {
|
|
778
|
+
if (!await fs$1.pathExists(globalRulesTarget)) {
|
|
579
779
|
print.info("Copying global RULES.yaml...");
|
|
580
|
-
await fs.copy(globalRulesSource, globalRulesTarget);
|
|
780
|
+
await fs$1.copy(globalRulesSource, globalRulesTarget);
|
|
581
781
|
print.success("Copied global RULES.yaml");
|
|
582
782
|
}
|
|
583
783
|
}
|
|
@@ -597,19 +797,19 @@ var TemplateSelectionService = class {
|
|
|
597
797
|
const architectFiles = MCP_CONFIG_FILES$1[MCPServer$1.ARCHITECT];
|
|
598
798
|
const hasArchitect = selectedMcpServers.includes(MCPServer$1.ARCHITECT);
|
|
599
799
|
const hasScaffold = selectedMcpServers.includes(MCPServer$1.SCAFFOLD);
|
|
600
|
-
await fs.ensureDir(targetPath);
|
|
601
|
-
const entries = await fs.readdir(sourcePath, { withFileTypes: true });
|
|
800
|
+
await fs$1.ensureDir(targetPath);
|
|
801
|
+
const entries = await fs$1.readdir(sourcePath, { withFileTypes: true });
|
|
602
802
|
for (const entry of entries) {
|
|
603
803
|
const entrySourcePath = path.join(sourcePath, entry.name);
|
|
604
804
|
const entryTargetPath = path.join(targetPath, entry.name);
|
|
605
805
|
const isArchitectFile = architectFiles.includes(entry.name);
|
|
606
|
-
if (hasArchitect && hasScaffold) if (entry.isDirectory()) await fs.copy(entrySourcePath, entryTargetPath);
|
|
607
|
-
else await fs.copy(entrySourcePath, entryTargetPath);
|
|
806
|
+
if (hasArchitect && hasScaffold) if (entry.isDirectory()) await fs$1.copy(entrySourcePath, entryTargetPath);
|
|
807
|
+
else await fs$1.copy(entrySourcePath, entryTargetPath);
|
|
608
808
|
else if (hasArchitect && !hasScaffold) {
|
|
609
|
-
if (isArchitectFile) await fs.copy(entrySourcePath, entryTargetPath);
|
|
809
|
+
if (isArchitectFile) await fs$1.copy(entrySourcePath, entryTargetPath);
|
|
610
810
|
} else if (!hasArchitect && hasScaffold) {
|
|
611
|
-
if (!isArchitectFile) if (entry.isDirectory()) await fs.copy(entrySourcePath, entryTargetPath);
|
|
612
|
-
else await fs.copy(entrySourcePath, entryTargetPath);
|
|
811
|
+
if (!isArchitectFile) if (entry.isDirectory()) await fs$1.copy(entrySourcePath, entryTargetPath);
|
|
812
|
+
else await fs$1.copy(entrySourcePath, entryTargetPath);
|
|
613
813
|
}
|
|
614
814
|
}
|
|
615
815
|
}
|
|
@@ -621,15 +821,15 @@ var TemplateSelectionService = class {
|
|
|
621
821
|
async readTemplateDescription(templatePath) {
|
|
622
822
|
try {
|
|
623
823
|
const scaffoldYamlPath = path.join(templatePath, "scaffold.yaml");
|
|
624
|
-
if (await fs.pathExists(scaffoldYamlPath)) {
|
|
824
|
+
if (await fs$1.pathExists(scaffoldYamlPath)) {
|
|
625
825
|
const yaml = await import("js-yaml");
|
|
626
|
-
const content = await fs.readFile(scaffoldYamlPath, "utf-8");
|
|
826
|
+
const content = await fs$1.readFile(scaffoldYamlPath, "utf-8");
|
|
627
827
|
const scaffoldConfig = yaml.load(content);
|
|
628
828
|
if (scaffoldConfig?.description) return scaffoldConfig.description;
|
|
629
829
|
if (scaffoldConfig?.boilerplate?.[0]?.description) return scaffoldConfig.boilerplate[0].description;
|
|
630
830
|
}
|
|
631
831
|
const readmePath = path.join(templatePath, "README.md");
|
|
632
|
-
if (await fs.pathExists(readmePath)) return (await fs.readFile(readmePath, "utf-8")).split("\n\n")[0].substring(0, 200).trim();
|
|
832
|
+
if (await fs$1.pathExists(readmePath)) return (await fs$1.readFile(readmePath, "utf-8")).split("\n\n")[0].substring(0, 200).trim();
|
|
633
833
|
return;
|
|
634
834
|
} catch {
|
|
635
835
|
return;
|
|
@@ -646,10 +846,7 @@ var TemplateSelectionService = class {
|
|
|
646
846
|
*/
|
|
647
847
|
async cleanup() {
|
|
648
848
|
try {
|
|
649
|
-
if (await fs.pathExists(this.tmpDir))
|
|
650
|
-
await fs.remove(this.tmpDir);
|
|
651
|
-
print.info("Cleaned up temporary files");
|
|
652
|
-
}
|
|
849
|
+
if (await fs$1.pathExists(this.tmpDir)) await fs$1.remove(this.tmpDir);
|
|
653
850
|
} catch (error) {
|
|
654
851
|
print.warning(`Warning: Failed to clean up tmp directory: ${error.message}`);
|
|
655
852
|
}
|
|
@@ -673,19 +870,19 @@ var TemplatesService = class {
|
|
|
673
870
|
return;
|
|
674
871
|
}
|
|
675
872
|
print.info(`Found ${templateDirs.length} template(s)`);
|
|
676
|
-
let
|
|
677
|
-
let
|
|
873
|
+
let _downloaded = 0;
|
|
874
|
+
let _skipped = 0;
|
|
678
875
|
for (const template of templateDirs) {
|
|
679
876
|
const targetFolder = path.join(templatesPath, template.name);
|
|
680
|
-
if (await fs.pathExists(targetFolder)) {
|
|
877
|
+
if (await fs$1.pathExists(targetFolder)) {
|
|
681
878
|
print.info(`Skipping ${template.name} (already exists)`);
|
|
682
|
-
|
|
879
|
+
_skipped++;
|
|
683
880
|
continue;
|
|
684
881
|
}
|
|
685
882
|
print.info(`Downloading ${template.name}...`);
|
|
686
883
|
await cloneSubdirectory(`https://github.com/${repoConfig.owner}/${repoConfig.repo}.git`, repoConfig.branch, template.path, targetFolder);
|
|
687
884
|
print.success(`Downloaded ${template.name}`);
|
|
688
|
-
|
|
885
|
+
_downloaded++;
|
|
689
886
|
}
|
|
690
887
|
print.success("\nAll templates downloaded successfully!");
|
|
691
888
|
} catch (error) {
|
|
@@ -697,8 +894,8 @@ var TemplatesService = class {
|
|
|
697
894
|
* @param templatesPath - Path where templates folder should be created
|
|
698
895
|
*/
|
|
699
896
|
async initializeTemplatesFolder(templatesPath) {
|
|
700
|
-
await fs.ensureDir(templatesPath);
|
|
701
|
-
await fs.writeFile(path.join(templatesPath, "README.md"), `# Templates
|
|
897
|
+
await fs$1.ensureDir(templatesPath);
|
|
898
|
+
await fs$1.writeFile(path.join(templatesPath, "README.md"), `# Templates
|
|
702
899
|
|
|
703
900
|
This folder contains boilerplate templates and scaffolding methods for your projects.
|
|
704
901
|
|
|
@@ -736,4 +933,4 @@ See existing templates for examples and documentation for more details.
|
|
|
736
933
|
};
|
|
737
934
|
|
|
738
935
|
//#endregion
|
|
739
|
-
export { BANNER_GRADIENT, CodingAgentService, NewProjectService, THEME, TemplateSelectionService, TemplatesService, cloneRepository, cloneSubdirectory, displayBanner, displayCompactBanner, fetchGitHubDirectoryContents, findWorkspaceRoot, gitInit, parseGitHubUrl };
|
|
936
|
+
export { BANNER_GRADIENT, CodingAgentService, NewProjectService, SPEC_TOOL_INFO, SpecTool, SpecToolService, THEME, TemplateSelectionService, TemplatesService, cloneRepository, cloneSubdirectory, displayBanner, displayCompactBanner, fetchGitHubDirectoryContents, findWorkspaceRoot, gitInit, parseGitHubUrl };
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@agiflowai/aicode-toolkit",
|
|
3
3
|
"description": "AI-powered code toolkit CLI for scaffolding, architecture management, and development workflows",
|
|
4
|
-
"version": "0.
|
|
4
|
+
"version": "1.0.2",
|
|
5
5
|
"license": "AGPL-3.0",
|
|
6
6
|
"author": "AgiflowIO",
|
|
7
7
|
"repository": {
|
|
@@ -46,12 +46,13 @@
|
|
|
46
46
|
"gradient-string": "^3.0.0",
|
|
47
47
|
"js-yaml": "4.1.0",
|
|
48
48
|
"liquidjs": "10.21.1",
|
|
49
|
+
"ora": "^9.0.0",
|
|
49
50
|
"pino": "^10.0.0",
|
|
50
51
|
"pino-pretty": "^13.1.1",
|
|
51
52
|
"xstate": "^5.23.0",
|
|
52
53
|
"zod": "3.25.76",
|
|
53
|
-
"@agiflowai/aicode-utils": "1.0.
|
|
54
|
-
"@agiflowai/coding-agent-bridge": "1.0.
|
|
54
|
+
"@agiflowai/aicode-utils": "1.0.2",
|
|
55
|
+
"@agiflowai/coding-agent-bridge": "1.0.2"
|
|
55
56
|
},
|
|
56
57
|
"devDependencies": {
|
|
57
58
|
"@types/express": "^5.0.0",
|
|
@@ -59,7 +60,8 @@
|
|
|
59
60
|
"@types/js-yaml": "^4.0.9",
|
|
60
61
|
"@types/node": "^22.0.0",
|
|
61
62
|
"tsdown": "^0.15.6",
|
|
62
|
-
"typescript": "5.9.3"
|
|
63
|
+
"typescript": "5.9.3",
|
|
64
|
+
"unplugin-raw": "^0.6.3"
|
|
63
65
|
},
|
|
64
66
|
"publishConfig": {
|
|
65
67
|
"access": "public"
|