workflow-agent-cli 1.1.6 → 2.0.0

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/index.js CHANGED
@@ -8,7 +8,7 @@ import {
8
8
  hasConfig,
9
9
  loadConfig,
10
10
  validateScopeDefinitions
11
- } from "../chunk-VFN3BY56.js";
11
+ } from "../chunk-4BIDFDSR.js";
12
12
 
13
13
  // src/cli/index.ts
14
14
  import { Command } from "commander";
@@ -16,9 +16,9 @@ import { Command } from "commander";
16
16
  // src/cli/commands/init.ts
17
17
  import * as p from "@clack/prompts";
18
18
  import chalk from "chalk";
19
- import { existsSync } from "fs";
20
- import { writeFile, mkdir } from "fs/promises";
21
- import { join, dirname } from "path";
19
+ import { existsSync as existsSync4 } from "fs";
20
+ import { writeFile as writeFile3, mkdir as mkdir3 } from "fs/promises";
21
+ import { join as join4, dirname } from "path";
22
22
  import { fileURLToPath } from "url";
23
23
 
24
24
  // src/templates/renderer.ts
@@ -189,18 +189,6 @@ async function renderTemplateFile(templatePath, outputPath, context) {
189
189
  await fs.mkdir(path.dirname(outputPath), { recursive: true });
190
190
  await fs.writeFile(outputPath, rendered, "utf-8");
191
191
  }
192
- async function renderTemplateDirectory(templateDir, outputDir, context) {
193
- const files = await fs.readdir(templateDir);
194
- const rendered = [];
195
- for (const file of files) {
196
- if (!file.match(/\.(md|ts|json)$/)) continue;
197
- const templatePath = path.join(templateDir, file);
198
- const outputPath = path.join(outputDir, file);
199
- await renderTemplateFile(templatePath, outputPath, context);
200
- rendered.push(file);
201
- }
202
- return rendered;
203
- }
204
192
  async function getProjectName(projectPath) {
205
193
  try {
206
194
  const pkgPath = path.join(projectPath, "package.json");
@@ -229,13 +217,666 @@ async function validateTemplateDirectory(templateDir) {
229
217
  }
230
218
  }
231
219
 
220
+ // src/templates/metadata.ts
221
+ var templateMetadata = {
222
+ "AGENT_EDITING_INSTRUCTIONS.md": {
223
+ filename: "AGENT_EDITING_INSTRUCTIONS.md",
224
+ displayName: "Agent Editing Instructions",
225
+ mandatory: true,
226
+ category: "workflow",
227
+ validators: ["implementation-plan"],
228
+ description: "Core rules for AI agents: implementation plans, coding standards, architecture"
229
+ },
230
+ "BRANCHING_STRATEGY.md": {
231
+ filename: "BRANCHING_STRATEGY.md",
232
+ displayName: "Branching Strategy",
233
+ mandatory: true,
234
+ category: "workflow",
235
+ validators: ["branch-name", "pr-title"],
236
+ description: "Git branch naming conventions, PR requirements, merge policies"
237
+ },
238
+ "TESTING_STRATEGY.md": {
239
+ filename: "TESTING_STRATEGY.md",
240
+ displayName: "Testing Strategy",
241
+ mandatory: true,
242
+ category: "development",
243
+ validators: ["test-coverage"],
244
+ description: "Testing pyramid, Vitest/Playwright patterns, when tests are required"
245
+ },
246
+ "SELF_IMPROVEMENT_MANDATE.md": {
247
+ filename: "SELF_IMPROVEMENT_MANDATE.md",
248
+ displayName: "Self-Improvement Mandate",
249
+ mandatory: true,
250
+ category: "workflow",
251
+ validators: [],
252
+ description: "Continuous improvement tracking, changelog requirements"
253
+ },
254
+ "SINGLE_SOURCE_OF_TRUTH.md": {
255
+ filename: "SINGLE_SOURCE_OF_TRUTH.md",
256
+ displayName: "Single Source of Truth",
257
+ mandatory: true,
258
+ category: "workflow",
259
+ validators: [],
260
+ description: "Canonical code locations, service patterns, avoiding duplication"
261
+ },
262
+ "COMPONENT_LIBRARY.md": {
263
+ filename: "COMPONENT_LIBRARY.md",
264
+ displayName: "Component Library",
265
+ mandatory: false,
266
+ category: "development",
267
+ validators: [],
268
+ description: "UI component patterns, design tokens, decision tree"
269
+ },
270
+ "DEPLOYMENT_STRATEGY.md": {
271
+ filename: "DEPLOYMENT_STRATEGY.md",
272
+ displayName: "Deployment Strategy",
273
+ mandatory: false,
274
+ category: "development",
275
+ validators: [],
276
+ description: "Deployment workflow, environments, migrations, rollback"
277
+ },
278
+ "LIBRARY_INVENTORY.md": {
279
+ filename: "LIBRARY_INVENTORY.md",
280
+ displayName: "Library Inventory",
281
+ mandatory: false,
282
+ category: "development",
283
+ validators: [],
284
+ description: "Dependency catalog, approved libraries, new library process"
285
+ },
286
+ "SCOPE_CREATION_WORKFLOW.md": {
287
+ filename: "SCOPE_CREATION_WORKFLOW.md",
288
+ displayName: "Scope Creation Workflow",
289
+ mandatory: false,
290
+ category: "workflow",
291
+ validators: [],
292
+ description: "Workflow for AI agents creating custom scopes"
293
+ },
294
+ "CUSTOM_SCOPE_TEMPLATE.md": {
295
+ filename: "CUSTOM_SCOPE_TEMPLATE.md",
296
+ displayName: "Custom Scope Template",
297
+ mandatory: false,
298
+ category: "workflow",
299
+ validators: [],
300
+ description: "Template for defining custom scope packages"
301
+ },
302
+ "PROJECT_TEMPLATE_README.md": {
303
+ filename: "PROJECT_TEMPLATE_README.md",
304
+ displayName: "Project Template README",
305
+ mandatory: false,
306
+ category: "documentation",
307
+ validators: [],
308
+ description: "Meta-document describing project structure"
309
+ },
310
+ "Guidelines.md": {
311
+ filename: "Guidelines.md",
312
+ displayName: "Custom Guidelines",
313
+ mandatory: false,
314
+ category: "documentation",
315
+ validators: [],
316
+ description: "Placeholder for custom user guidelines"
317
+ }
318
+ };
319
+ function getMandatoryTemplates() {
320
+ return Object.values(templateMetadata).filter((t) => t.mandatory);
321
+ }
322
+ function getOptionalTemplates() {
323
+ return Object.values(templateMetadata).filter((t) => !t.mandatory);
324
+ }
325
+ function getMandatoryTemplateFilenames() {
326
+ return getMandatoryTemplates().map((t) => t.filename);
327
+ }
328
+
329
+ // src/utils/hooks.ts
330
+ import { existsSync } from "fs";
331
+ import { readFile, writeFile, unlink, chmod, rename, mkdir } from "fs/promises";
332
+ import { join } from "path";
333
+ function getGitHooksDir(projectPath = process.cwd()) {
334
+ return join(projectPath, ".git", "hooks");
335
+ }
336
+ function hasGitRepo(projectPath = process.cwd()) {
337
+ return existsSync(join(projectPath, ".git"));
338
+ }
339
+ function generatePreCommitHook(config) {
340
+ const checks = config?.preCommit || ["validate-branch", "check-guidelines"];
341
+ const checkCommands = checks.map((check) => {
342
+ switch (check) {
343
+ case "validate-branch":
344
+ return " workflow validate branch";
345
+ case "validate-commit":
346
+ return " workflow validate commit";
347
+ case "check-guidelines":
348
+ return " workflow doctor --check-guidelines-only 2>/dev/null || true";
349
+ default:
350
+ return "";
351
+ }
352
+ }).filter(Boolean).join("\n");
353
+ return `#!/bin/sh
354
+ # Workflow Agent pre-commit hook
355
+ # Auto-generated - do not edit manually
356
+ # To reinstall: workflow hooks install
357
+ # To uninstall: workflow hooks uninstall
358
+
359
+ # Skip in CI environment
360
+ if [ -n "\${CI:-}" ] || [ -n "\${GITHUB_ACTIONS:-}" ] || [ -n "\${GITLAB_CI:-}" ]; then
361
+ exit 0
362
+ fi
363
+
364
+ # Run workflow checks
365
+ ${checkCommands}
366
+
367
+ # Run original hook if it exists
368
+ if [ -f ".git/hooks/pre-commit.original" ]; then
369
+ .git/hooks/pre-commit.original "$@"
370
+ fi
371
+ `;
372
+ }
373
+ function generateCommitMsgHook(config) {
374
+ const checks = config?.commitMsg || ["validate-commit"];
375
+ const checkCommands = checks.map((check) => {
376
+ switch (check) {
377
+ case "validate-commit":
378
+ return ' workflow validate commit "$(cat "$1")"';
379
+ default:
380
+ return "";
381
+ }
382
+ }).filter(Boolean).join("\n");
383
+ return `#!/bin/sh
384
+ # Workflow Agent commit-msg hook
385
+ # Auto-generated - do not edit manually
386
+ # To reinstall: workflow hooks install
387
+ # To uninstall: workflow hooks uninstall
388
+
389
+ # Skip in CI environment
390
+ if [ -n "\${CI:-}" ] || [ -n "\${GITHUB_ACTIONS:-}" ] || [ -n "\${GITLAB_CI:-}" ]; then
391
+ exit 0
392
+ fi
393
+
394
+ # Run workflow checks
395
+ ${checkCommands}
396
+
397
+ # Run original hook if it exists
398
+ if [ -f ".git/hooks/commit-msg.original" ]; then
399
+ .git/hooks/commit-msg.original "$@"
400
+ fi
401
+ `;
402
+ }
403
+ async function isWorkflowHook(hookPath) {
404
+ try {
405
+ const content = await readFile(hookPath, "utf-8");
406
+ return content.includes("Workflow Agent") && content.includes("Auto-generated");
407
+ } catch {
408
+ return false;
409
+ }
410
+ }
411
+ async function getHookStatus(hookType, projectPath = process.cwd()) {
412
+ const hooksDir = getGitHooksDir(projectPath);
413
+ const hookPath = join(hooksDir, hookType);
414
+ const originalPath = join(hooksDir, `${hookType}.original`);
415
+ const status = {
416
+ installed: false,
417
+ hookType,
418
+ hasExistingHook: false,
419
+ wrappedOriginal: false
420
+ };
421
+ if (!existsSync(hookPath)) {
422
+ return status;
423
+ }
424
+ status.hasExistingHook = true;
425
+ if (await isWorkflowHook(hookPath)) {
426
+ status.installed = true;
427
+ status.wrappedOriginal = existsSync(originalPath);
428
+ }
429
+ return status;
430
+ }
431
+ async function getAllHooksStatus(projectPath = process.cwd()) {
432
+ return Promise.all([
433
+ getHookStatus("pre-commit", projectPath),
434
+ getHookStatus("commit-msg", projectPath)
435
+ ]);
436
+ }
437
+ async function installSingleHook(hookType, config, projectPath = process.cwd()) {
438
+ const hooksDir = getGitHooksDir(projectPath);
439
+ const hookPath = join(hooksDir, hookType);
440
+ const originalPath = join(hooksDir, `${hookType}.original`);
441
+ const result = {
442
+ success: false,
443
+ hookType,
444
+ wrappedExisting: false
445
+ };
446
+ try {
447
+ if (!existsSync(hooksDir)) {
448
+ await mkdir(hooksDir, { recursive: true });
449
+ }
450
+ if (existsSync(hookPath)) {
451
+ const isOurs = await isWorkflowHook(hookPath);
452
+ if (!isOurs) {
453
+ await rename(hookPath, originalPath);
454
+ result.wrappedExisting = true;
455
+ }
456
+ }
457
+ const hookContent = hookType === "pre-commit" ? generatePreCommitHook(config) : generateCommitMsgHook(config);
458
+ await writeFile(hookPath, hookContent, "utf-8");
459
+ await chmod(hookPath, 493);
460
+ result.success = true;
461
+ } catch (error) {
462
+ result.error = error instanceof Error ? error.message : String(error);
463
+ }
464
+ return result;
465
+ }
466
+ async function installHooks(config, projectPath = process.cwd()) {
467
+ if (!hasGitRepo(projectPath)) {
468
+ return [{
469
+ success: false,
470
+ hookType: "pre-commit",
471
+ wrappedExisting: false,
472
+ error: "No git repository found. Run git init first."
473
+ }];
474
+ }
475
+ const results = await Promise.all([
476
+ installSingleHook("pre-commit", config, projectPath),
477
+ installSingleHook("commit-msg", config, projectPath)
478
+ ]);
479
+ return results;
480
+ }
481
+ async function uninstallSingleHook(hookType, projectPath = process.cwd()) {
482
+ const hooksDir = getGitHooksDir(projectPath);
483
+ const hookPath = join(hooksDir, hookType);
484
+ const originalPath = join(hooksDir, `${hookType}.original`);
485
+ const result = {
486
+ success: false,
487
+ hookType,
488
+ wrappedExisting: false
489
+ };
490
+ try {
491
+ if (!existsSync(hookPath)) {
492
+ result.success = true;
493
+ return result;
494
+ }
495
+ const isOurs = await isWorkflowHook(hookPath);
496
+ if (!isOurs) {
497
+ result.error = "Hook is not managed by Workflow Agent";
498
+ return result;
499
+ }
500
+ await unlink(hookPath);
501
+ if (existsSync(originalPath)) {
502
+ await rename(originalPath, hookPath);
503
+ result.wrappedExisting = true;
504
+ }
505
+ result.success = true;
506
+ } catch (error) {
507
+ result.error = error instanceof Error ? error.message : String(error);
508
+ }
509
+ return result;
510
+ }
511
+ async function uninstallHooks(projectPath = process.cwd()) {
512
+ return Promise.all([
513
+ uninstallSingleHook("pre-commit", projectPath),
514
+ uninstallSingleHook("commit-msg", projectPath)
515
+ ]);
516
+ }
517
+
518
+ // src/utils/git-repo.ts
519
+ import { execa } from "execa";
520
+ import { existsSync as existsSync2 } from "fs";
521
+ import { readFile as readFile2 } from "fs/promises";
522
+ import { join as join2 } from "path";
523
+ async function isGitRepo(projectPath = process.cwd()) {
524
+ try {
525
+ await execa("git", ["rev-parse", "--git-dir"], { cwd: projectPath });
526
+ return true;
527
+ } catch {
528
+ return false;
529
+ }
530
+ }
531
+ async function getGitRemoteUrl(projectPath = process.cwd()) {
532
+ try {
533
+ const { stdout } = await execa("git", ["remote", "get-url", "origin"], { cwd: projectPath });
534
+ return stdout.trim() || null;
535
+ } catch {
536
+ return null;
537
+ }
538
+ }
539
+ function isGitHubRemote(remoteUrl) {
540
+ if (!remoteUrl) return false;
541
+ return remoteUrl.includes("github.com");
542
+ }
543
+ function parseGitHubUrl(remoteUrl) {
544
+ if (!remoteUrl || !isGitHubRemote(remoteUrl)) return null;
545
+ const match = remoteUrl.match(/github\.com[:/]([^/]+)\/([^/.]+)/);
546
+ if (match) {
547
+ return {
548
+ owner: match[1],
549
+ repo: match[2].replace(/\.git$/, "")
550
+ };
551
+ }
552
+ return null;
553
+ }
554
+ async function getDefaultBranch(projectPath = process.cwd()) {
555
+ try {
556
+ const { stdout } = await execa("git", ["symbolic-ref", "refs/remotes/origin/HEAD"], {
557
+ cwd: projectPath
558
+ });
559
+ return stdout.trim().replace("refs/remotes/origin/", "") || "main";
560
+ } catch {
561
+ try {
562
+ const { stdout } = await execa("git", ["rev-parse", "--abbrev-ref", "HEAD"], {
563
+ cwd: projectPath
564
+ });
565
+ return stdout.trim() || "main";
566
+ } catch {
567
+ return "main";
568
+ }
569
+ }
570
+ }
571
+ async function getRepoInfo(projectPath = process.cwd()) {
572
+ const isRepo = await isGitRepo(projectPath);
573
+ if (!isRepo) {
574
+ return {
575
+ isGitRepo: false,
576
+ remoteUrl: null,
577
+ isGitHub: false,
578
+ github: null,
579
+ defaultBranch: null
580
+ };
581
+ }
582
+ const remoteUrl = await getGitRemoteUrl(projectPath);
583
+ const isGitHub = isGitHubRemote(remoteUrl);
584
+ const github = parseGitHubUrl(remoteUrl);
585
+ const defaultBranch = await getDefaultBranch(projectPath);
586
+ return {
587
+ isGitRepo: true,
588
+ remoteUrl,
589
+ isGitHub,
590
+ github,
591
+ defaultBranch
592
+ };
593
+ }
594
+ async function detectPackageManager(projectPath = process.cwd()) {
595
+ if (existsSync2(join2(projectPath, "pnpm-lock.yaml"))) {
596
+ return "pnpm";
597
+ }
598
+ if (existsSync2(join2(projectPath, "yarn.lock"))) {
599
+ return "yarn";
600
+ }
601
+ if (existsSync2(join2(projectPath, "bun.lockb"))) {
602
+ return "bun";
603
+ }
604
+ if (existsSync2(join2(projectPath, "package-lock.json"))) {
605
+ return "npm";
606
+ }
607
+ try {
608
+ const pkgPath = join2(projectPath, "package.json");
609
+ if (existsSync2(pkgPath)) {
610
+ const pkg = JSON.parse(await readFile2(pkgPath, "utf-8"));
611
+ if (pkg.packageManager) {
612
+ if (pkg.packageManager.startsWith("pnpm")) return "pnpm";
613
+ if (pkg.packageManager.startsWith("yarn")) return "yarn";
614
+ if (pkg.packageManager.startsWith("bun")) return "bun";
615
+ }
616
+ }
617
+ } catch {
618
+ }
619
+ return "npm";
620
+ }
621
+ async function isMonorepo(projectPath = process.cwd()) {
622
+ if (existsSync2(join2(projectPath, "pnpm-workspace.yaml"))) {
623
+ return true;
624
+ }
625
+ try {
626
+ const pkgPath = join2(projectPath, "package.json");
627
+ if (existsSync2(pkgPath)) {
628
+ const pkg = JSON.parse(await readFile2(pkgPath, "utf-8"));
629
+ if (pkg.workspaces) {
630
+ return true;
631
+ }
632
+ }
633
+ } catch {
634
+ }
635
+ if (existsSync2(join2(projectPath, "lerna.json"))) {
636
+ return true;
637
+ }
638
+ if (existsSync2(join2(projectPath, "nx.json"))) {
639
+ return true;
640
+ }
641
+ if (existsSync2(join2(projectPath, "turbo.json"))) {
642
+ return true;
643
+ }
644
+ return false;
645
+ }
646
+ async function getPackageScripts(projectPath = process.cwd()) {
647
+ try {
648
+ const pkgPath = join2(projectPath, "package.json");
649
+ if (!existsSync2(pkgPath)) {
650
+ return {};
651
+ }
652
+ const pkg = JSON.parse(await readFile2(pkgPath, "utf-8"));
653
+ return pkg.scripts || {};
654
+ } catch {
655
+ return {};
656
+ }
657
+ }
658
+ async function getProjectInfo(projectPath = process.cwd()) {
659
+ const packageManager = await detectPackageManager(projectPath);
660
+ const monorepo = await isMonorepo(projectPath);
661
+ const scripts = await getPackageScripts(projectPath);
662
+ return {
663
+ packageManager,
664
+ isMonorepo: monorepo,
665
+ hasLintScript: "lint" in scripts,
666
+ hasTypecheckScript: "typecheck" in scripts || "type-check" in scripts,
667
+ hasFormatScript: "format" in scripts || "format:check" in scripts,
668
+ hasTestScript: "test" in scripts,
669
+ hasBuildScript: "build" in scripts
670
+ };
671
+ }
672
+ function getInstallCommand(packageManager) {
673
+ switch (packageManager) {
674
+ case "pnpm":
675
+ return "pnpm install --frozen-lockfile";
676
+ case "yarn":
677
+ return "yarn install --frozen-lockfile";
678
+ case "bun":
679
+ return "bun install --frozen-lockfile";
680
+ case "npm":
681
+ default:
682
+ return "npm ci";
683
+ }
684
+ }
685
+ function getRunCommand(packageManager, script, isMonorepo2 = false) {
686
+ switch (packageManager) {
687
+ case "pnpm":
688
+ return isMonorepo2 ? `pnpm -r run ${script}` : `pnpm run ${script}`;
689
+ case "yarn":
690
+ return isMonorepo2 ? `yarn workspaces run ${script}` : `yarn run ${script}`;
691
+ case "bun":
692
+ return `bun run ${script}`;
693
+ case "npm":
694
+ default:
695
+ return isMonorepo2 ? `npm run ${script} --workspaces --if-present` : `npm run ${script}`;
696
+ }
697
+ }
698
+
699
+ // src/utils/github-actions.ts
700
+ import { existsSync as existsSync3 } from "fs";
701
+ import { writeFile as writeFile2, mkdir as mkdir2 } from "fs/promises";
702
+ import { join as join3 } from "path";
703
+ function generateCIWorkflowContent(options) {
704
+ const {
705
+ packageManager,
706
+ isMonorepo: isMonorepo2,
707
+ checks,
708
+ nodeVersions,
709
+ defaultBranch,
710
+ hasLintScript,
711
+ hasTypecheckScript,
712
+ hasFormatScript,
713
+ hasTestScript,
714
+ hasBuildScript
715
+ } = options;
716
+ const installCmd = getInstallCommand(packageManager);
717
+ const steps = [];
718
+ steps.push(` - name: Checkout
719
+ uses: actions/checkout@v4`);
720
+ let cacheType = packageManager;
721
+ if (packageManager === "bun") {
722
+ cacheType = "npm";
723
+ }
724
+ const useMatrix = nodeVersions.length > 1;
725
+ const nodeVersionValue = useMatrix ? `\${{ matrix.node-version }}` : nodeVersions[0] || "20";
726
+ steps.push(`
727
+ - name: Setup Node.js
728
+ uses: actions/setup-node@v4
729
+ with:
730
+ node-version: '${nodeVersionValue}'
731
+ cache: '${cacheType}'`);
732
+ if (packageManager === "pnpm") {
733
+ steps.push(`
734
+ - name: Setup pnpm
735
+ uses: pnpm/action-setup@v4
736
+ with:
737
+ version: 9`);
738
+ }
739
+ steps.push(`
740
+ - name: Install dependencies
741
+ run: ${installCmd}`);
742
+ if (checks.includes("lint")) {
743
+ if (hasLintScript) {
744
+ const lintCmd = getRunCommand(packageManager, "lint", isMonorepo2);
745
+ steps.push(`
746
+ - name: Lint
747
+ run: ${lintCmd}`);
748
+ } else {
749
+ steps.push(`
750
+ - name: Lint
751
+ run: echo "No lint script configured - add 'lint' to package.json scripts"`);
752
+ }
753
+ }
754
+ if (checks.includes("typecheck")) {
755
+ if (hasTypecheckScript) {
756
+ const typecheckCmd = getRunCommand(packageManager, "typecheck", isMonorepo2);
757
+ steps.push(`
758
+ - name: Type check
759
+ run: ${typecheckCmd}`);
760
+ } else {
761
+ steps.push(`
762
+ - name: Type check
763
+ run: npx tsc --noEmit`);
764
+ }
765
+ }
766
+ if (checks.includes("format")) {
767
+ if (hasFormatScript) {
768
+ const formatCmd = getRunCommand(packageManager, "format:check", isMonorepo2);
769
+ steps.push(`
770
+ - name: Format check
771
+ run: ${formatCmd} || npx prettier --check "**/*.{ts,tsx,js,jsx,json,md}"`);
772
+ } else {
773
+ steps.push(`
774
+ - name: Format check
775
+ run: npx prettier --check "**/*.{ts,tsx,js,jsx,json,md}"`);
776
+ }
777
+ }
778
+ if (checks.includes("build")) {
779
+ if (hasBuildScript) {
780
+ const buildCmd = getRunCommand(packageManager, "build", isMonorepo2);
781
+ steps.push(`
782
+ - name: Build
783
+ run: ${buildCmd}`);
784
+ } else {
785
+ steps.push(`
786
+ - name: Build
787
+ run: echo "No build script configured - add 'build' to package.json scripts"`);
788
+ }
789
+ }
790
+ if (checks.includes("test")) {
791
+ if (hasTestScript) {
792
+ const testCmd = getRunCommand(packageManager, "test", isMonorepo2);
793
+ steps.push(`
794
+ - name: Test
795
+ run: ${testCmd}`);
796
+ } else {
797
+ steps.push(`
798
+ - name: Test
799
+ run: echo "No test script configured - add 'test' to package.json scripts"`);
800
+ }
801
+ }
802
+ let matrixStrategy = "";
803
+ if (useMatrix) {
804
+ matrixStrategy = `
805
+ strategy:
806
+ matrix:
807
+ node-version: [${nodeVersions.map((v) => `'${v}'`).join(", ")}]`;
808
+ }
809
+ const workflow = `# Workflow Agent CI Pipeline
810
+ # Auto-generated - modifications will be preserved on regeneration
811
+ # To regenerate: workflow github:setup
812
+
813
+ name: CI
814
+
815
+ on:
816
+ push:
817
+ branches: [${defaultBranch}, develop]
818
+ pull_request:
819
+ branches: [${defaultBranch}, develop]
820
+
821
+ jobs:
822
+ ci:
823
+ runs-on: ubuntu-latest${matrixStrategy}
824
+
825
+ steps:
826
+ ${steps.join("\n")}
827
+ `;
828
+ return workflow;
829
+ }
830
+ async function createCIWorkflow(options = {}) {
831
+ const projectPath = options.projectPath || process.cwd();
832
+ const workflowsDir = join3(projectPath, ".github", "workflows");
833
+ const workflowPath = join3(workflowsDir, "ci.yml");
834
+ const result = {
835
+ success: false,
836
+ filePath: workflowPath
837
+ };
838
+ try {
839
+ const projectInfo = await getProjectInfo(projectPath);
840
+ const packageManager = options.packageManager || projectInfo.packageManager;
841
+ const isMonorepo2 = options.isMonorepo ?? projectInfo.isMonorepo;
842
+ const checks = options.ciConfig?.checks || ["lint", "typecheck", "format", "build", "test"];
843
+ const nodeVersions = options.nodeVersions || ["20"];
844
+ const defaultBranch = options.defaultBranch || "main";
845
+ if (!existsSync3(workflowsDir)) {
846
+ await mkdir2(workflowsDir, { recursive: true });
847
+ }
848
+ const content = generateCIWorkflowContent({
849
+ packageManager,
850
+ isMonorepo: isMonorepo2,
851
+ checks,
852
+ nodeVersions,
853
+ defaultBranch,
854
+ hasLintScript: projectInfo.hasLintScript,
855
+ hasTypecheckScript: projectInfo.hasTypecheckScript,
856
+ hasFormatScript: projectInfo.hasFormatScript,
857
+ hasTestScript: projectInfo.hasTestScript,
858
+ hasBuildScript: projectInfo.hasBuildScript
859
+ });
860
+ await writeFile2(workflowPath, content, "utf-8");
861
+ result.success = true;
862
+ } catch (error) {
863
+ result.error = error instanceof Error ? error.message : String(error);
864
+ }
865
+ return result;
866
+ }
867
+ function hasCIWorkflow(projectPath = process.cwd()) {
868
+ const workflowsDir = join3(projectPath, ".github", "workflows");
869
+ const possibleNames = ["ci.yml", "ci.yaml", "main.yml", "main.yaml"];
870
+ return possibleNames.some((name) => existsSync3(join3(workflowsDir, name)));
871
+ }
872
+
232
873
  // src/cli/commands/init.ts
233
874
  var __filename = fileURLToPath(import.meta.url);
234
875
  var __dirname = dirname(__filename);
235
876
  async function initCommand(options) {
236
877
  console.log(chalk.bold.cyan("\n\u{1F680} Workflow Agent Initialization\n"));
237
878
  const cwd = process.cwd();
238
- const isNonInteractive = !!(options.preset && options.name);
879
+ const isNonInteractive = !!(options.preset && options.name) || !!options.yes;
239
880
  if (hasConfig(cwd) && !options.yes && !isNonInteractive) {
240
881
  const shouldContinue = await p.confirm({
241
882
  message: "Workflow Agent is already configured. Continue and overwrite?",
@@ -275,10 +916,10 @@ async function initCommand(options) {
275
916
  try {
276
917
  const presetModule = await import(`@workflow/scopes-${preset}`);
277
918
  scopes = presetModule.scopes || presetModule.default.scopes;
278
- const spinner4 = p.spinner();
279
- spinner4.start(`Loading ${presetModule.default?.name || preset} preset`);
919
+ const spinner5 = p.spinner();
920
+ spinner5.start(`Loading ${presetModule.default?.name || preset} preset`);
280
921
  await new Promise((resolve) => setTimeout(resolve, 500));
281
- spinner4.stop(`\u2713 Loaded ${scopes.length} scopes from preset`);
922
+ spinner5.stop(`\u2713 Loaded ${scopes.length} scopes from preset`);
282
923
  } catch (error) {
283
924
  console.log(chalk.yellow(`
284
925
  \u26A0\uFE0F Could not load preset package. Using basic scopes.`));
@@ -302,54 +943,139 @@ async function initCommand(options) {
302
943
  enforcement: "strict",
303
944
  language: "en"
304
945
  };
305
- const configPath = join(cwd, "workflow.config.json");
306
- await writeFile(configPath, JSON.stringify(config, null, 2));
307
- const workflowDir = join(cwd, ".workflow");
308
- if (!existsSync(workflowDir)) {
309
- await mkdir(workflowDir, { recursive: true });
310
- }
311
- const shouldGenerateGuidelines = await p.confirm({
312
- message: "Generate workflow guidelines from templates?",
313
- initialValue: true
314
- });
315
- if (p.isCancel(shouldGenerateGuidelines)) {
316
- p.cancel("Initialization cancelled");
317
- process.exit(0);
946
+ const configPath = join4(cwd, "workflow.config.json");
947
+ await writeFile3(configPath, JSON.stringify(config, null, 2));
948
+ const workflowDir = join4(cwd, ".workflow");
949
+ if (!existsSync4(workflowDir)) {
950
+ await mkdir3(workflowDir, { recursive: true });
318
951
  }
319
- if (shouldGenerateGuidelines) {
320
- const spinner4 = p.spinner();
321
- spinner4.start("Generating guidelines...");
322
- try {
323
- const templatesDir = join(__dirname, "../../templates");
324
- await validateTemplateDirectory(templatesDir);
325
- const context = await buildTemplateContext(config, cwd);
326
- const guidelinesDir = join(cwd, "guidelines");
327
- await mkdir(guidelinesDir, { recursive: true });
328
- const renderedFiles = await renderTemplateDirectory(templatesDir, guidelinesDir, context);
329
- spinner4.stop(`\u2713 Generated ${renderedFiles.length} guideline documents`);
330
- } catch (error) {
331
- spinner4.stop("\u26A0\uFE0F Could not generate guidelines");
332
- console.log(chalk.yellow(`
333
- Reason: ${error instanceof Error ? error.message : String(error)}`));
334
- console.log(chalk.dim("You can manually copy guidelines later if needed."));
952
+ const repoInfo = await getRepoInfo(cwd);
953
+ const projectInfo = await getProjectInfo(cwd);
954
+ const mandatoryTemplates = getMandatoryTemplates();
955
+ const optionalTemplates = getOptionalTemplates();
956
+ console.log(chalk.dim(`
957
+ \u{1F4CB} Generating ${mandatoryTemplates.length} mandatory guidelines...`));
958
+ const guidelinesDir = join4(cwd, "guidelines");
959
+ const templatesDir = join4(__dirname, "../../templates");
960
+ let mandatoryGenerated = 0;
961
+ let optionalGenerated = 0;
962
+ try {
963
+ await validateTemplateDirectory(templatesDir);
964
+ const context = await buildTemplateContext(config, cwd);
965
+ await mkdir3(guidelinesDir, { recursive: true });
966
+ for (const template of mandatoryTemplates) {
967
+ try {
968
+ const templatePath = join4(templatesDir, template.filename);
969
+ const outputPath = join4(guidelinesDir, template.filename);
970
+ await renderTemplateFile(templatePath, outputPath, context);
971
+ mandatoryGenerated++;
972
+ } catch (error) {
973
+ console.log(chalk.yellow(` \u26A0\uFE0F Could not generate ${template.filename}`));
974
+ }
975
+ }
976
+ console.log(chalk.green(`\u2713 Generated ${mandatoryGenerated} mandatory guidelines`));
977
+ let shouldGenerateOptional = isNonInteractive;
978
+ if (!isNonInteractive) {
979
+ const response = await p.confirm({
980
+ message: `Generate ${optionalTemplates.length} optional guidelines (deployment, library inventory, etc.)?`,
981
+ initialValue: true
982
+ });
983
+ shouldGenerateOptional = !p.isCancel(response) && response;
984
+ }
985
+ if (shouldGenerateOptional) {
986
+ for (const template of optionalTemplates) {
987
+ try {
988
+ const templatePath = join4(templatesDir, template.filename);
989
+ const outputPath = join4(guidelinesDir, template.filename);
990
+ await renderTemplateFile(templatePath, outputPath, context);
991
+ optionalGenerated++;
992
+ } catch {
993
+ }
994
+ }
995
+ console.log(chalk.green(`\u2713 Generated ${optionalGenerated} optional guidelines`));
996
+ }
997
+ } catch (error) {
998
+ console.log(chalk.yellow(`
999
+ \u26A0\uFE0F Could not generate guidelines: ${error instanceof Error ? error.message : String(error)}`));
1000
+ console.log(chalk.dim("You can manually copy guidelines later if needed."));
1001
+ }
1002
+ if (hasGitRepo(cwd)) {
1003
+ let shouldInstallHooks = isNonInteractive;
1004
+ if (!isNonInteractive) {
1005
+ const response = await p.confirm({
1006
+ message: "Install git hooks for pre-commit validation?",
1007
+ initialValue: true
1008
+ });
1009
+ shouldInstallHooks = !p.isCancel(response) && response;
1010
+ }
1011
+ if (shouldInstallHooks) {
1012
+ const hookSpinner = p.spinner();
1013
+ hookSpinner.start("Installing git hooks...");
1014
+ const hookResults = await installHooks(config.hooks, cwd);
1015
+ const allSuccess = hookResults.every((r) => r.success);
1016
+ if (allSuccess) {
1017
+ hookSpinner.stop("\u2713 Installed git hooks");
1018
+ } else {
1019
+ hookSpinner.stop("\u26A0\uFE0F Some hooks could not be installed");
1020
+ }
1021
+ }
1022
+ }
1023
+ if (repoInfo.isGitHub) {
1024
+ const existingCI = hasCIWorkflow(cwd);
1025
+ if (!existingCI) {
1026
+ console.log(chalk.dim("\n\u{1F527} Setting up GitHub Actions CI (mandatory for GitHub repos)..."));
1027
+ const ciResult = await createCIWorkflow({
1028
+ projectPath: cwd,
1029
+ packageManager: projectInfo.packageManager,
1030
+ isMonorepo: projectInfo.isMonorepo,
1031
+ ciConfig: config.ci,
1032
+ defaultBranch: repoInfo.defaultBranch || "main"
1033
+ });
1034
+ if (ciResult.success) {
1035
+ console.log(chalk.green("\u2713 Created GitHub Actions CI workflow"));
1036
+ console.log(chalk.dim(` File: .github/workflows/ci.yml`));
1037
+ } else {
1038
+ console.log(chalk.yellow(`\u26A0\uFE0F Could not create CI workflow: ${ciResult.error}`));
1039
+ }
1040
+ } else {
1041
+ console.log(chalk.dim("\n\u2713 GitHub Actions CI workflow already exists"));
1042
+ }
1043
+ } else if (repoInfo.isGitRepo) {
1044
+ let shouldSetupCI = false;
1045
+ if (!isNonInteractive) {
1046
+ const response = await p.confirm({
1047
+ message: "No GitHub remote detected. Create CI workflow anyway?",
1048
+ initialValue: false
1049
+ });
1050
+ shouldSetupCI = !p.isCancel(response) && response;
1051
+ }
1052
+ if (shouldSetupCI) {
1053
+ const ciResult = await createCIWorkflow({
1054
+ projectPath: cwd,
1055
+ packageManager: projectInfo.packageManager,
1056
+ isMonorepo: projectInfo.isMonorepo,
1057
+ defaultBranch: repoInfo.defaultBranch || "main"
1058
+ });
1059
+ if (ciResult.success) {
1060
+ console.log(chalk.green("\u2713 Created CI workflow"));
1061
+ }
335
1062
  }
336
1063
  }
337
1064
  p.outro(chalk.green("\u2713 Workflow Agent initialized successfully!"));
338
1065
  console.log(chalk.dim("\nNext steps:"));
339
1066
  console.log(chalk.dim(" 1. Review your configuration in workflow.config.json"));
340
- if (shouldGenerateGuidelines) {
341
- console.log(chalk.dim(" 2. Review generated guidelines in guidelines/ directory"));
342
- console.log(chalk.dim(" 3. Run: workflow validate branch"));
343
- console.log(chalk.dim(" 4. Run: workflow doctor (for optimization suggestions)\n"));
344
- } else {
345
- console.log(chalk.dim(" 2. Run: workflow validate branch"));
346
- console.log(chalk.dim(" 3. Run: workflow doctor (for optimization suggestions)\n"));
1067
+ console.log(chalk.dim(" 2. Review generated guidelines in guidelines/ directory"));
1068
+ console.log(chalk.dim(" 3. Run: workflow validate branch"));
1069
+ console.log(chalk.dim(" 4. Run: workflow doctor (for health check)\n"));
1070
+ if (repoInfo.isGitHub) {
1071
+ console.log(chalk.cyan("\u{1F4A1} Recommended: Enable branch protection on GitHub"));
1072
+ console.log(chalk.dim(" Settings \u2192 Branches \u2192 Add rule \u2192 Require status checks\n"));
347
1073
  }
348
1074
  }
349
1075
 
350
1076
  // src/cli/commands/validate.ts
351
1077
  import chalk2 from "chalk";
352
- import { execa } from "execa";
1078
+ import { execa as execa2 } from "execa";
353
1079
  async function validateCommand(type, value, _options = {}) {
354
1080
  const config = await loadConfig();
355
1081
  if (!config) {
@@ -362,7 +1088,7 @@ async function validateCommand(type, value, _options = {}) {
362
1088
  switch (type) {
363
1089
  case "branch": {
364
1090
  if (!targetValue) {
365
- const { stdout } = await execa("git", ["branch", "--show-current"]);
1091
+ const { stdout } = await execa2("git", ["branch", "--show-current"]);
366
1092
  targetValue = stdout.trim();
367
1093
  }
368
1094
  result = await validateBranchName(targetValue, config);
@@ -370,7 +1096,7 @@ async function validateCommand(type, value, _options = {}) {
370
1096
  }
371
1097
  case "commit": {
372
1098
  if (!targetValue) {
373
- const { stdout } = await execa("git", ["log", "-1", "--pretty=%s"]);
1099
+ const { stdout } = await execa2("git", ["log", "-1", "--pretty=%s"]);
374
1100
  targetValue = stdout.trim();
375
1101
  }
376
1102
  result = await validateCommitMessage(targetValue, config);
@@ -464,30 +1190,290 @@ async function suggestCommand(feedback, options = {}) {
464
1190
 
465
1191
  // src/cli/commands/doctor.ts
466
1192
  import chalk5 from "chalk";
467
- async function doctorCommand() {
1193
+
1194
+ // src/validators/guidelines.ts
1195
+ import { existsSync as existsSync5 } from "fs";
1196
+ import { readFile as readFile3, readdir } from "fs/promises";
1197
+ import { join as join5 } from "path";
1198
+ function getEffectiveMandatoryTemplates(guidelinesConfig) {
1199
+ const coreMandatory = getMandatoryTemplateFilenames();
1200
+ if (!guidelinesConfig) {
1201
+ return coreMandatory;
1202
+ }
1203
+ let mandatory = [...coreMandatory];
1204
+ if (guidelinesConfig.additionalMandatory) {
1205
+ for (const template of guidelinesConfig.additionalMandatory) {
1206
+ if (!mandatory.includes(template) && templateMetadata[template]) {
1207
+ mandatory.push(template);
1208
+ }
1209
+ }
1210
+ }
1211
+ if (guidelinesConfig.optionalOverrides) {
1212
+ mandatory = mandatory.filter((t) => !guidelinesConfig.optionalOverrides.includes(t));
1213
+ }
1214
+ return mandatory;
1215
+ }
1216
+ async function validateGuidelinesExist(projectPath = process.cwd(), config) {
1217
+ const guidelinesDir = join5(projectPath, "guidelines");
1218
+ const result = {
1219
+ valid: true,
1220
+ missingMandatory: [],
1221
+ presentMandatory: [],
1222
+ presentOptional: [],
1223
+ errors: []
1224
+ };
1225
+ if (!existsSync5(guidelinesDir)) {
1226
+ const mandatory2 = getEffectiveMandatoryTemplates(config?.guidelines);
1227
+ result.valid = false;
1228
+ result.missingMandatory = mandatory2;
1229
+ result.errors.push("Guidelines directory does not exist. Run: workflow init");
1230
+ return result;
1231
+ }
1232
+ let existingFiles = [];
1233
+ try {
1234
+ existingFiles = await readdir(guidelinesDir);
1235
+ } catch (error) {
1236
+ result.valid = false;
1237
+ result.errors.push(`Cannot read guidelines directory: ${error instanceof Error ? error.message : String(error)}`);
1238
+ return result;
1239
+ }
1240
+ const mandatory = getEffectiveMandatoryTemplates(config?.guidelines);
1241
+ for (const template of mandatory) {
1242
+ if (existingFiles.includes(template)) {
1243
+ result.presentMandatory.push(template);
1244
+ } else {
1245
+ result.missingMandatory.push(template);
1246
+ }
1247
+ }
1248
+ const allTemplateNames = Object.keys(templateMetadata);
1249
+ for (const file of existingFiles) {
1250
+ if (allTemplateNames.includes(file) && !mandatory.includes(file)) {
1251
+ result.presentOptional.push(file);
1252
+ }
1253
+ }
1254
+ if (result.missingMandatory.length > 0) {
1255
+ result.valid = false;
1256
+ result.errors.push(
1257
+ `Missing mandatory guidelines: ${result.missingMandatory.join(", ")}. Run: workflow init`
1258
+ );
1259
+ }
1260
+ return result;
1261
+ }
1262
+ async function validateGitHubActionsSetup(projectPath = process.cwd()) {
1263
+ const workflowsDir = join5(projectPath, ".github", "workflows");
1264
+ const result = {
1265
+ valid: true,
1266
+ hasWorkflowFile: false,
1267
+ hasLintCheck: false,
1268
+ hasTypecheckCheck: false,
1269
+ hasFormatCheck: false,
1270
+ hasBuildCheck: false,
1271
+ hasTestCheck: false,
1272
+ errors: [],
1273
+ warnings: []
1274
+ };
1275
+ if (!existsSync5(workflowsDir)) {
1276
+ result.valid = false;
1277
+ result.errors.push("GitHub Actions workflows directory does not exist. Run: workflow github:setup");
1278
+ return result;
1279
+ }
1280
+ let workflowFiles = [];
1281
+ try {
1282
+ workflowFiles = await readdir(workflowsDir);
1283
+ } catch (error) {
1284
+ result.valid = false;
1285
+ result.errors.push(`Cannot read workflows directory: ${error instanceof Error ? error.message : String(error)}`);
1286
+ return result;
1287
+ }
1288
+ const ciWorkflowNames = ["ci.yml", "ci.yaml", "main.yml", "main.yaml", "build.yml", "build.yaml", "test.yml", "test.yaml"];
1289
+ const ciWorkflows = workflowFiles.filter((f) => ciWorkflowNames.includes(f.toLowerCase()));
1290
+ if (ciWorkflows.length === 0) {
1291
+ result.valid = false;
1292
+ result.errors.push("No CI workflow file found. Run: workflow github:setup");
1293
+ return result;
1294
+ }
1295
+ result.hasWorkflowFile = true;
1296
+ const workflowPath = join5(workflowsDir, ciWorkflows[0]);
1297
+ let workflowContent = "";
1298
+ try {
1299
+ workflowContent = await readFile3(workflowPath, "utf-8");
1300
+ } catch (error) {
1301
+ result.warnings.push(`Cannot read workflow file: ${error instanceof Error ? error.message : String(error)}`);
1302
+ return result;
1303
+ }
1304
+ const contentLower = workflowContent.toLowerCase();
1305
+ if (contentLower.includes("lint") || contentLower.includes("eslint")) {
1306
+ result.hasLintCheck = true;
1307
+ } else {
1308
+ result.warnings.push("CI workflow may be missing lint check");
1309
+ }
1310
+ if (contentLower.includes("typecheck") || contentLower.includes("type-check") || contentLower.includes("tsc")) {
1311
+ result.hasTypecheckCheck = true;
1312
+ } else {
1313
+ result.warnings.push("CI workflow may be missing typecheck");
1314
+ }
1315
+ if (contentLower.includes("format") || contentLower.includes("prettier")) {
1316
+ result.hasFormatCheck = true;
1317
+ } else {
1318
+ result.warnings.push("CI workflow may be missing format check");
1319
+ }
1320
+ if (contentLower.includes("build")) {
1321
+ result.hasBuildCheck = true;
1322
+ } else {
1323
+ result.warnings.push("CI workflow may be missing build step");
1324
+ }
1325
+ if (contentLower.includes("test") && !contentLower.includes("typecheck")) {
1326
+ result.hasTestCheck = true;
1327
+ } else {
1328
+ result.warnings.push("CI workflow may be missing test step");
1329
+ }
1330
+ const mandatoryChecks = [result.hasLintCheck, result.hasTypecheckCheck, result.hasFormatCheck];
1331
+ if (!mandatoryChecks.every(Boolean)) {
1332
+ result.valid = false;
1333
+ result.errors.push("CI workflow is missing mandatory checks (lint, typecheck, format)");
1334
+ }
1335
+ return result;
1336
+ }
1337
+
1338
+ // src/cli/commands/doctor.ts
1339
+ async function doctorCommand(options = {}) {
1340
+ const cwd = process.cwd();
1341
+ if (options.checkGuidelinesOnly) {
1342
+ const result = await validateGuidelinesExist(cwd);
1343
+ if (!result.valid) {
1344
+ console.error(chalk5.red("\u2717 Missing mandatory guidelines"));
1345
+ for (const missing of result.missingMandatory) {
1346
+ console.error(chalk5.red(` \u2022 ${missing}`));
1347
+ }
1348
+ process.exit(1);
1349
+ }
1350
+ process.exit(0);
1351
+ }
468
1352
  console.log(chalk5.bold.cyan("\n\u{1F3E5} Workflow Agent Health Check\n"));
469
1353
  const config = await loadConfig();
1354
+ let hasErrors = false;
1355
+ let hasWarnings = false;
1356
+ console.log(chalk5.bold("\u{1F4CB} Configuration"));
470
1357
  if (!config) {
471
- console.error(chalk5.red("\u2717 No workflow configuration found"));
472
- console.log(chalk5.yellow(" Run: workflow init"));
1358
+ console.error(chalk5.red(" \u2717 No workflow configuration found"));
1359
+ console.log(chalk5.yellow(" Run: workflow init"));
473
1360
  process.exit(1);
474
1361
  }
475
- console.log(chalk5.green("\u2713 Configuration loaded successfully"));
476
- console.log(chalk5.dim(` Project: ${config.projectName}`));
477
- console.log(chalk5.dim(` Scopes: ${config.scopes.length} configured`));
478
- console.log(chalk5.dim(` Enforcement: ${config.enforcement}`));
479
- console.log(chalk5.dim(` Language: ${config.language}`));
480
- console.log(chalk5.cyan("\n\u{1F4A1} Suggestions:\n"));
481
- console.log(chalk5.dim(" \u2022 Health check analysis coming soon"));
482
- console.log(chalk5.dim(" \u2022 Git history analysis coming soon"));
483
- console.log(chalk5.dim(" \u2022 Optimization recommendations coming soon"));
1362
+ console.log(chalk5.green(" \u2713 Configuration loaded successfully"));
1363
+ console.log(chalk5.dim(` Project: ${config.projectName}`));
1364
+ console.log(chalk5.dim(` Scopes: ${config.scopes.length} configured`));
1365
+ console.log(chalk5.dim(` Enforcement: ${config.enforcement}`));
1366
+ console.log(chalk5.dim(` Language: ${config.language}`));
1367
+ console.log(chalk5.bold("\n\u{1F4DA} Guidelines"));
1368
+ const guidelinesResult = await validateGuidelinesExist(cwd, config);
1369
+ if (guidelinesResult.valid) {
1370
+ console.log(chalk5.green(` \u2713 All ${guidelinesResult.presentMandatory.length} mandatory guidelines present`));
1371
+ if (guidelinesResult.presentOptional.length > 0) {
1372
+ console.log(chalk5.dim(` + ${guidelinesResult.presentOptional.length} optional guidelines`));
1373
+ }
1374
+ } else {
1375
+ hasErrors = true;
1376
+ console.log(chalk5.red(` \u2717 Missing ${guidelinesResult.missingMandatory.length} mandatory guidelines:`));
1377
+ for (const missing of guidelinesResult.missingMandatory) {
1378
+ console.log(chalk5.red(` \u2022 ${missing}`));
1379
+ }
1380
+ console.log(chalk5.yellow(" Run: workflow init"));
1381
+ }
1382
+ console.log(chalk5.bold("\n\u{1F517} Git Hooks"));
1383
+ if (!hasGitRepo(cwd)) {
1384
+ console.log(chalk5.yellow(" \u26A0 No git repository found"));
1385
+ hasWarnings = true;
1386
+ } else {
1387
+ const hookStatuses = await getAllHooksStatus(cwd);
1388
+ const installedHooks = hookStatuses.filter((h) => h.installed);
1389
+ if (installedHooks.length === hookStatuses.length) {
1390
+ console.log(chalk5.green(` \u2713 All ${installedHooks.length} hooks installed`));
1391
+ for (const hook of hookStatuses) {
1392
+ const extra = hook.wrappedOriginal ? " (wrapping original)" : "";
1393
+ console.log(chalk5.dim(` \u2022 ${hook.hookType}${extra}`));
1394
+ }
1395
+ } else if (installedHooks.length > 0) {
1396
+ hasWarnings = true;
1397
+ console.log(chalk5.yellow(` \u26A0 ${installedHooks.length}/${hookStatuses.length} hooks installed`));
1398
+ for (const hook of hookStatuses) {
1399
+ if (hook.installed) {
1400
+ console.log(chalk5.green(` \u2713 ${hook.hookType}`));
1401
+ } else {
1402
+ console.log(chalk5.yellow(` \u2717 ${hook.hookType}`));
1403
+ }
1404
+ }
1405
+ console.log(chalk5.yellow(" Run: workflow hooks install"));
1406
+ } else {
1407
+ hasWarnings = true;
1408
+ console.log(chalk5.yellow(" \u26A0 No hooks installed"));
1409
+ console.log(chalk5.yellow(" Run: workflow hooks install"));
1410
+ }
1411
+ }
1412
+ console.log(chalk5.bold("\n\u{1F680} CI/CD Pipeline"));
1413
+ const repoInfo = await getRepoInfo(cwd);
1414
+ if (!repoInfo.isGitRepo) {
1415
+ console.log(chalk5.dim(" \u25CB No git repository (CI check skipped)"));
1416
+ } else if (!repoInfo.isGitHub) {
1417
+ console.log(chalk5.dim(" \u25CB Not a GitHub repository (CI check skipped)"));
1418
+ console.log(chalk5.dim(` Remote: ${repoInfo.remoteUrl || "none"}`));
1419
+ } else {
1420
+ console.log(chalk5.dim(` Repository: ${repoInfo.github?.owner}/${repoInfo.github?.repo}`));
1421
+ const ciResult = await validateGitHubActionsSetup(cwd);
1422
+ if (ciResult.valid) {
1423
+ console.log(chalk5.green(" \u2713 GitHub Actions CI configured correctly"));
1424
+ const checks = [
1425
+ ciResult.hasLintCheck && "lint",
1426
+ ciResult.hasTypecheckCheck && "typecheck",
1427
+ ciResult.hasFormatCheck && "format",
1428
+ ciResult.hasBuildCheck && "build",
1429
+ ciResult.hasTestCheck && "test"
1430
+ ].filter(Boolean);
1431
+ console.log(chalk5.dim(` Checks: ${checks.join(", ")}`));
1432
+ } else if (!ciResult.hasWorkflowFile) {
1433
+ hasErrors = true;
1434
+ console.log(chalk5.red(" \u2717 No CI workflow found"));
1435
+ console.log(chalk5.yellow(" Run: workflow github:setup"));
1436
+ } else {
1437
+ hasWarnings = true;
1438
+ console.log(chalk5.yellow(" \u26A0 CI workflow may be incomplete"));
1439
+ for (const warning of ciResult.warnings) {
1440
+ console.log(chalk5.yellow(` \u2022 ${warning}`));
1441
+ }
1442
+ console.log(chalk5.yellow(" Run: workflow github:setup to regenerate"));
1443
+ }
1444
+ }
1445
+ console.log(chalk5.bold("\n\u{1F4A1} Suggestions"));
1446
+ if (repoInfo.isGitHub && hasCIWorkflow(cwd)) {
1447
+ console.log(chalk5.cyan(" \u2192 Enable branch protection on GitHub"));
1448
+ console.log(chalk5.dim(" Settings \u2192 Branches \u2192 Add rule"));
1449
+ console.log(chalk5.dim(" \u2611 Require status checks to pass before merging"));
1450
+ console.log(chalk5.dim(" \u2611 Require branches to be up to date before merging"));
1451
+ }
1452
+ if (config.enforcement !== "strict") {
1453
+ console.log(chalk5.cyan(` \u2192 Consider switching to 'strict' enforcement`));
1454
+ console.log(chalk5.dim(` Current: ${config.enforcement}`));
1455
+ }
1456
+ if (config.scopes.length < 5) {
1457
+ console.log(chalk5.cyan(" \u2192 Consider adding more scopes for better organization"));
1458
+ console.log(chalk5.dim(" Run: workflow config add scope <name>"));
1459
+ }
1460
+ console.log(chalk5.bold("\n\u{1F4CA} Summary"));
1461
+ if (hasErrors) {
1462
+ console.log(chalk5.red(" \u2717 Health check failed - issues found"));
1463
+ process.exit(1);
1464
+ } else if (hasWarnings) {
1465
+ console.log(chalk5.yellow(" \u26A0 Health check passed with warnings"));
1466
+ } else {
1467
+ console.log(chalk5.green(" \u2713 All checks passed"));
1468
+ }
1469
+ console.log("");
484
1470
  }
485
1471
 
486
1472
  // src/cli/commands/setup.ts
487
1473
  import * as p3 from "@clack/prompts";
488
1474
  import chalk6 from "chalk";
489
- import { readFileSync, writeFileSync, existsSync as existsSync2 } from "fs";
490
- import { join as join2 } from "path";
1475
+ import { readFileSync, writeFileSync, existsSync as existsSync6 } from "fs";
1476
+ import { join as join6 } from "path";
491
1477
  var WORKFLOW_SCRIPTS = {
492
1478
  "workflow:init": "workflow-agent init",
493
1479
  "workflow:validate": "workflow-agent validate",
@@ -497,8 +1483,8 @@ var WORKFLOW_SCRIPTS = {
497
1483
  async function setupCommand() {
498
1484
  p3.intro(chalk6.bgBlue(" workflow-agent setup "));
499
1485
  const cwd = process.cwd();
500
- const packageJsonPath = join2(cwd, "package.json");
501
- if (!existsSync2(packageJsonPath)) {
1486
+ const packageJsonPath = join6(cwd, "package.json");
1487
+ if (!existsSync6(packageJsonPath)) {
502
1488
  p3.cancel("No package.json found in current directory");
503
1489
  process.exit(1);
504
1490
  }
@@ -555,15 +1541,15 @@ async function setupCommand() {
555
1541
  // src/cli/commands/scope-create.ts
556
1542
  import * as p4 from "@clack/prompts";
557
1543
  import chalk7 from "chalk";
558
- import { existsSync as existsSync3 } from "fs";
559
- import { writeFile as writeFile2, mkdir as mkdir2, readFile } from "fs/promises";
560
- import { join as join3 } from "path";
1544
+ import { existsSync as existsSync7 } from "fs";
1545
+ import { writeFile as writeFile4, mkdir as mkdir4, readFile as readFile4 } from "fs/promises";
1546
+ import { join as join7 } from "path";
561
1547
  async function scopeCreateCommand(options) {
562
1548
  console.log(chalk7.bold.cyan("\n\u{1F3A8} Create Custom Scope Package\n"));
563
1549
  const cwd = process.cwd();
564
1550
  const isNonInteractive = !!(options.name && options.scopes && options.presetName);
565
- const isMonorepo = existsSync3(join3(cwd, "pnpm-workspace.yaml"));
566
- if (isMonorepo) {
1551
+ const isMonorepo2 = existsSync7(join7(cwd, "pnpm-workspace.yaml"));
1552
+ if (isMonorepo2) {
567
1553
  console.log(chalk7.dim("\u2713 Detected monorepo workspace\n"));
568
1554
  }
569
1555
  const packageNameInput = isNonInteractive ? options.name : await p4.text({
@@ -696,8 +1682,8 @@ async function scopeCreateCommand(options) {
696
1682
  let outputDir;
697
1683
  if (options.outputDir) {
698
1684
  outputDir = options.outputDir;
699
- } else if (isMonorepo) {
700
- outputDir = join3(cwd, "packages", `scopes-${packageName}`);
1685
+ } else if (isMonorepo2) {
1686
+ outputDir = join7(cwd, "packages", `scopes-${packageName}`);
701
1687
  } else {
702
1688
  const customDir = await p4.text({
703
1689
  message: "Output directory:",
@@ -708,9 +1694,9 @@ async function scopeCreateCommand(options) {
708
1694
  p4.cancel("Operation cancelled");
709
1695
  process.exit(0);
710
1696
  }
711
- outputDir = join3(cwd, customDir);
1697
+ outputDir = join7(cwd, customDir);
712
1698
  }
713
- if (existsSync3(outputDir)) {
1699
+ if (existsSync7(outputDir)) {
714
1700
  const shouldOverwrite = await p4.confirm({
715
1701
  message: `Directory ${outputDir} already exists. Overwrite?`,
716
1702
  initialValue: false
@@ -720,10 +1706,10 @@ async function scopeCreateCommand(options) {
720
1706
  process.exit(0);
721
1707
  }
722
1708
  }
723
- const spinner4 = p4.spinner();
724
- spinner4.start("Creating package structure...");
1709
+ const spinner5 = p4.spinner();
1710
+ spinner5.start("Creating package structure...");
725
1711
  try {
726
- await mkdir2(join3(outputDir, "src"), { recursive: true });
1712
+ await mkdir4(join7(outputDir, "src"), { recursive: true });
727
1713
  const packageJson = {
728
1714
  name: `@workflow/scopes-${packageName}`,
729
1715
  version: "1.0.0",
@@ -763,8 +1749,8 @@ async function scopeCreateCommand(options) {
763
1749
  access: "public"
764
1750
  }
765
1751
  };
766
- await writeFile2(
767
- join3(outputDir, "package.json"),
1752
+ await writeFile4(
1753
+ join7(outputDir, "package.json"),
768
1754
  JSON.stringify(packageJson, null, 2),
769
1755
  "utf-8"
770
1756
  );
@@ -776,8 +1762,8 @@ async function scopeCreateCommand(options) {
776
1762
  },
777
1763
  include: ["src/**/*"]
778
1764
  };
779
- await writeFile2(
780
- join3(outputDir, "tsconfig.json"),
1765
+ await writeFile4(
1766
+ join7(outputDir, "tsconfig.json"),
781
1767
  JSON.stringify(tsconfig, null, 2),
782
1768
  "utf-8"
783
1769
  );
@@ -791,7 +1777,7 @@ export default defineConfig({
791
1777
  sourcemap: true,
792
1778
  });
793
1779
  `;
794
- await writeFile2(join3(outputDir, "tsup.config.ts"), tsupConfig, "utf-8");
1780
+ await writeFile4(join7(outputDir, "tsup.config.ts"), tsupConfig, "utf-8");
795
1781
  const indexTs = `import type { Scope } from '@hawkinside_out/workflow-agent/config';
796
1782
 
797
1783
  export const scopes: Scope[] = ${JSON.stringify(scopes, null, 2)};
@@ -805,7 +1791,7 @@ export const preset = {
805
1791
 
806
1792
  export default preset;
807
1793
  `;
808
- await writeFile2(join3(outputDir, "src", "index.ts"), indexTs, "utf-8");
1794
+ await writeFile4(join7(outputDir, "src", "index.ts"), indexTs, "utf-8");
809
1795
  if (!options.noTest) {
810
1796
  const testFile = `import { describe, it, expect } from 'vitest';
811
1797
  import { scopes, preset } from './index.js';
@@ -846,12 +1832,12 @@ describe('${presetName} Scope Preset', () => {
846
1832
  });
847
1833
  });
848
1834
  `;
849
- await writeFile2(join3(outputDir, "src", "index.test.ts"), testFile, "utf-8");
1835
+ await writeFile4(join7(outputDir, "src", "index.test.ts"), testFile, "utf-8");
850
1836
  }
851
- spinner4.stop("\u2713 Package structure created");
852
- if (isMonorepo) {
853
- const workspaceFile = join3(cwd, "pnpm-workspace.yaml");
854
- const workspaceContent = await readFile(workspaceFile, "utf-8");
1837
+ spinner5.stop("\u2713 Package structure created");
1838
+ if (isMonorepo2) {
1839
+ const workspaceFile = join7(cwd, "pnpm-workspace.yaml");
1840
+ const workspaceContent = await readFile4(workspaceFile, "utf-8");
855
1841
  const packagePath = `packages/scopes-${packageName}`;
856
1842
  if (!workspaceContent.includes(packagePath) && !workspaceContent.includes("packages/*")) {
857
1843
  console.log(chalk7.yellow("\n\u26A0\uFE0F Add the following to pnpm-workspace.yaml:"));
@@ -886,7 +1872,7 @@ describe('${presetName} Scope Preset', () => {
886
1872
  console.log(chalk7.dim(" 4. Use in other projects: pnpm add @workflow/scopes-" + packageName + "\n"));
887
1873
  }
888
1874
  } catch (error) {
889
- spinner4.stop("\u2717 Failed to create package");
1875
+ spinner5.stop("\u2717 Failed to create package");
890
1876
  console.error(chalk7.red("\nError:"), error);
891
1877
  process.exit(1);
892
1878
  }
@@ -895,9 +1881,9 @@ describe('${presetName} Scope Preset', () => {
895
1881
  // src/cli/commands/scope-migrate.ts
896
1882
  import * as p5 from "@clack/prompts";
897
1883
  import chalk8 from "chalk";
898
- import { existsSync as existsSync4 } from "fs";
899
- import { writeFile as writeFile3, mkdir as mkdir3, readFile as readFile2 } from "fs/promises";
900
- import { join as join4 } from "path";
1884
+ import { existsSync as existsSync8 } from "fs";
1885
+ import { writeFile as writeFile5, mkdir as mkdir5, readFile as readFile5 } from "fs/promises";
1886
+ import { join as join8 } from "path";
901
1887
  async function scopeMigrateCommand(options) {
902
1888
  console.log(chalk8.bold.cyan("\n\u{1F504} Migrate Scopes to Custom Package\n"));
903
1889
  const cwd = process.cwd();
@@ -935,8 +1921,8 @@ async function scopeMigrateCommand(options) {
935
1921
  p5.cancel("Migration cancelled");
936
1922
  process.exit(0);
937
1923
  }
938
- const isMonorepo = existsSync4(join4(cwd, "pnpm-workspace.yaml"));
939
- if (isMonorepo) {
1924
+ const isMonorepo2 = existsSync8(join8(cwd, "pnpm-workspace.yaml"));
1925
+ if (isMonorepo2) {
940
1926
  console.log(chalk8.dim("\n\u2713 Detected monorepo workspace\n"));
941
1927
  }
942
1928
  const packageNameInput = options.name || await p5.text({
@@ -980,8 +1966,8 @@ async function scopeMigrateCommand(options) {
980
1966
  let outputDir;
981
1967
  if (options.outputDir) {
982
1968
  outputDir = options.outputDir;
983
- } else if (isMonorepo) {
984
- outputDir = join4(cwd, "packages", `scopes-${packageName}`);
1969
+ } else if (isMonorepo2) {
1970
+ outputDir = join8(cwd, "packages", `scopes-${packageName}`);
985
1971
  } else {
986
1972
  const customDir = await p5.text({
987
1973
  message: "Output directory:",
@@ -992,9 +1978,9 @@ async function scopeMigrateCommand(options) {
992
1978
  p5.cancel("Migration cancelled");
993
1979
  process.exit(0);
994
1980
  }
995
- outputDir = join4(cwd, customDir);
1981
+ outputDir = join8(cwd, customDir);
996
1982
  }
997
- if (existsSync4(outputDir)) {
1983
+ if (existsSync8(outputDir)) {
998
1984
  const shouldOverwrite = await p5.confirm({
999
1985
  message: `Directory ${outputDir} already exists. Overwrite?`,
1000
1986
  initialValue: false
@@ -1004,10 +1990,10 @@ async function scopeMigrateCommand(options) {
1004
1990
  process.exit(0);
1005
1991
  }
1006
1992
  }
1007
- const spinner4 = p5.spinner();
1008
- spinner4.start("Migrating scopes to package...");
1993
+ const spinner5 = p5.spinner();
1994
+ spinner5.start("Migrating scopes to package...");
1009
1995
  try {
1010
- await mkdir3(join4(outputDir, "src"), { recursive: true });
1996
+ await mkdir5(join8(outputDir, "src"), { recursive: true });
1011
1997
  const packageJson = {
1012
1998
  name: `@workflow/scopes-${packageName}`,
1013
1999
  version: "1.0.0",
@@ -1047,8 +2033,8 @@ async function scopeMigrateCommand(options) {
1047
2033
  access: "public"
1048
2034
  }
1049
2035
  };
1050
- await writeFile3(
1051
- join4(outputDir, "package.json"),
2036
+ await writeFile5(
2037
+ join8(outputDir, "package.json"),
1052
2038
  JSON.stringify(packageJson, null, 2),
1053
2039
  "utf-8"
1054
2040
  );
@@ -1060,8 +2046,8 @@ async function scopeMigrateCommand(options) {
1060
2046
  },
1061
2047
  include: ["src/**/*"]
1062
2048
  };
1063
- await writeFile3(
1064
- join4(outputDir, "tsconfig.json"),
2049
+ await writeFile5(
2050
+ join8(outputDir, "tsconfig.json"),
1065
2051
  JSON.stringify(tsconfig, null, 2),
1066
2052
  "utf-8"
1067
2053
  );
@@ -1075,7 +2061,7 @@ export default defineConfig({
1075
2061
  sourcemap: true,
1076
2062
  });
1077
2063
  `;
1078
- await writeFile3(join4(outputDir, "tsup.config.ts"), tsupConfig, "utf-8");
2064
+ await writeFile5(join8(outputDir, "tsup.config.ts"), tsupConfig, "utf-8");
1079
2065
  const indexTs = `import type { Scope } from '@hawkinside_out/workflow-agent/config';
1080
2066
 
1081
2067
  export const scopes: Scope[] = ${JSON.stringify(config.scopes, null, 2)};
@@ -1089,7 +2075,7 @@ export const preset = {
1089
2075
 
1090
2076
  export default preset;
1091
2077
  `;
1092
- await writeFile3(join4(outputDir, "src", "index.ts"), indexTs, "utf-8");
2078
+ await writeFile5(join8(outputDir, "src", "index.ts"), indexTs, "utf-8");
1093
2079
  const testFile = `import { describe, it, expect } from 'vitest';
1094
2080
  import { scopes, preset } from './index.js';
1095
2081
  import { ScopeSchema } from '@hawkinside_out/workflow-agent/config';
@@ -1132,11 +2118,11 @@ describe('${presetName} Scope Preset (Migrated)', () => {
1132
2118
  });
1133
2119
  });
1134
2120
  `;
1135
- await writeFile3(join4(outputDir, "src", "index.test.ts"), testFile, "utf-8");
1136
- spinner4.stop("\u2713 Package created from migrated scopes");
1137
- if (isMonorepo) {
1138
- const workspaceFile = join4(cwd, "pnpm-workspace.yaml");
1139
- const workspaceContent = await readFile2(workspaceFile, "utf-8");
2121
+ await writeFile5(join8(outputDir, "src", "index.test.ts"), testFile, "utf-8");
2122
+ spinner5.stop("\u2713 Package created from migrated scopes");
2123
+ if (isMonorepo2) {
2124
+ const workspaceFile = join8(cwd, "pnpm-workspace.yaml");
2125
+ const workspaceContent = await readFile5(workspaceFile, "utf-8");
1140
2126
  const packagePath = `packages/scopes-${packageName}`;
1141
2127
  if (!workspaceContent.includes(packagePath) && !workspaceContent.includes("packages/*")) {
1142
2128
  console.log(chalk8.yellow("\n\u26A0\uFE0F Add the following to pnpm-workspace.yaml:"));
@@ -1150,7 +2136,7 @@ describe('${presetName} Scope Preset (Migrated)', () => {
1150
2136
  initialValue: false
1151
2137
  });
1152
2138
  if (!p5.isCancel(keepConfig) && !keepConfig) {
1153
- const configPath = join4(cwd, "workflow.config.json");
2139
+ const configPath = join8(cwd, "workflow.config.json");
1154
2140
  const updatedConfig = {
1155
2141
  ...config,
1156
2142
  scopes: [],
@@ -1158,7 +2144,7 @@ describe('${presetName} Scope Preset (Migrated)', () => {
1158
2144
  preset: `scopes-${packageName}`
1159
2145
  // Reference the new package
1160
2146
  };
1161
- await writeFile3(configPath, JSON.stringify(updatedConfig, null, 2), "utf-8");
2147
+ await writeFile5(configPath, JSON.stringify(updatedConfig, null, 2), "utf-8");
1162
2148
  console.log(chalk8.green("\u2713 Updated workflow.config.json"));
1163
2149
  console.log(chalk8.dim(" \u2022 Cleared inline scopes"));
1164
2150
  console.log(chalk8.dim(` \u2022 Added preset reference: scopes-${packageName}
@@ -1185,12 +2171,249 @@ describe('${presetName} Scope Preset (Migrated)', () => {
1185
2171
  }
1186
2172
  console.log(chalk8.dim("\u{1F4A1} Tip: You can now reuse this scope package across multiple projects!\n"));
1187
2173
  } catch (error) {
1188
- spinner4.stop("\u2717 Migration failed");
2174
+ spinner5.stop("\u2717 Migration failed");
1189
2175
  console.error(chalk8.red("\nError:"), error);
1190
2176
  process.exit(1);
1191
2177
  }
1192
2178
  }
1193
2179
 
2180
+ // src/cli/commands/hooks.ts
2181
+ import chalk9 from "chalk";
2182
+ async function hooksCommand(action) {
2183
+ const cwd = process.cwd();
2184
+ switch (action) {
2185
+ case "install":
2186
+ await installHooksAction(cwd);
2187
+ break;
2188
+ case "uninstall":
2189
+ await uninstallHooksAction(cwd);
2190
+ break;
2191
+ case "status":
2192
+ await statusHooksAction(cwd);
2193
+ break;
2194
+ default:
2195
+ console.error(chalk9.red(`Unknown action: ${action}`));
2196
+ console.log(chalk9.dim("Available actions: install, uninstall, status"));
2197
+ process.exit(1);
2198
+ }
2199
+ }
2200
+ async function installHooksAction(cwd) {
2201
+ console.log(chalk9.bold.cyan("\n\u{1F517} Installing Workflow Agent Git Hooks\n"));
2202
+ if (!hasGitRepo(cwd)) {
2203
+ console.error(chalk9.red("\u2717 No git repository found"));
2204
+ console.log(chalk9.yellow(" Run: git init"));
2205
+ process.exit(1);
2206
+ }
2207
+ const config = await loadConfig();
2208
+ const hooksConfig = config?.hooks;
2209
+ const results = await installHooks(hooksConfig, cwd);
2210
+ let hasErrors = false;
2211
+ for (const result of results) {
2212
+ if (result.success) {
2213
+ if (result.wrappedExisting) {
2214
+ console.log(chalk9.green(`\u2713 Installed ${result.hookType} hook (wrapped existing hook)`));
2215
+ } else {
2216
+ console.log(chalk9.green(`\u2713 Installed ${result.hookType} hook`));
2217
+ }
2218
+ } else {
2219
+ console.error(chalk9.red(`\u2717 Failed to install ${result.hookType}: ${result.error}`));
2220
+ hasErrors = true;
2221
+ }
2222
+ }
2223
+ if (!hasErrors) {
2224
+ console.log(chalk9.green("\n\u2713 Git hooks installed successfully"));
2225
+ console.log(chalk9.dim("\nHooks will run automatically on commit."));
2226
+ console.log(chalk9.dim("They will be skipped in CI environments."));
2227
+ } else {
2228
+ process.exit(1);
2229
+ }
2230
+ }
2231
+ async function uninstallHooksAction(cwd) {
2232
+ console.log(chalk9.bold.cyan("\n\u{1F513} Uninstalling Workflow Agent Git Hooks\n"));
2233
+ if (!hasGitRepo(cwd)) {
2234
+ console.error(chalk9.red("\u2717 No git repository found"));
2235
+ process.exit(1);
2236
+ }
2237
+ const results = await uninstallHooks(cwd);
2238
+ let hasErrors = false;
2239
+ for (const result of results) {
2240
+ if (result.success) {
2241
+ if (result.wrappedExisting) {
2242
+ console.log(chalk9.green(`\u2713 Removed ${result.hookType} hook (restored original)`));
2243
+ } else {
2244
+ console.log(chalk9.green(`\u2713 Removed ${result.hookType} hook`));
2245
+ }
2246
+ } else if (result.error) {
2247
+ console.error(chalk9.red(`\u2717 Failed to remove ${result.hookType}: ${result.error}`));
2248
+ hasErrors = true;
2249
+ }
2250
+ }
2251
+ if (!hasErrors) {
2252
+ console.log(chalk9.green("\n\u2713 Git hooks uninstalled successfully"));
2253
+ } else {
2254
+ process.exit(1);
2255
+ }
2256
+ }
2257
+ async function statusHooksAction(cwd) {
2258
+ console.log(chalk9.bold.cyan("\n\u{1F4CA} Workflow Agent Git Hooks Status\n"));
2259
+ if (!hasGitRepo(cwd)) {
2260
+ console.error(chalk9.red("\u2717 No git repository found"));
2261
+ process.exit(1);
2262
+ }
2263
+ const statuses = await getAllHooksStatus(cwd);
2264
+ for (const status of statuses) {
2265
+ const icon = status.installed ? "\u2713" : "\u2717";
2266
+ const color = status.installed ? chalk9.green : chalk9.yellow;
2267
+ let message = `${icon} ${status.hookType}`;
2268
+ if (status.installed) {
2269
+ message += " - installed";
2270
+ if (status.wrappedOriginal) {
2271
+ message += " (wrapping original hook)";
2272
+ }
2273
+ } else if (status.hasExistingHook) {
2274
+ message += " - existing hook (not managed by Workflow Agent)";
2275
+ } else {
2276
+ message += " - not installed";
2277
+ }
2278
+ console.log(color(message));
2279
+ }
2280
+ const allInstalled = statuses.every((s) => s.installed);
2281
+ if (!allInstalled) {
2282
+ console.log(chalk9.dim("\nTo install hooks: workflow hooks install"));
2283
+ }
2284
+ }
2285
+
2286
+ // src/cli/commands/github-actions.ts
2287
+ import chalk10 from "chalk";
2288
+ import * as p6 from "@clack/prompts";
2289
+ async function githubCommand(action) {
2290
+ const cwd = process.cwd();
2291
+ switch (action) {
2292
+ case "setup":
2293
+ await setupAction(cwd);
2294
+ break;
2295
+ case "check":
2296
+ await checkAction(cwd);
2297
+ break;
2298
+ default:
2299
+ console.error(chalk10.red(`Unknown action: ${action}`));
2300
+ console.log(chalk10.dim("Available actions: setup, check"));
2301
+ process.exit(1);
2302
+ }
2303
+ }
2304
+ async function setupAction(cwd) {
2305
+ console.log(chalk10.bold.cyan("\n\u{1F527} Setting Up GitHub Actions CI\n"));
2306
+ const repoInfo = await getRepoInfo(cwd);
2307
+ if (!repoInfo.isGitRepo) {
2308
+ console.error(chalk10.red("\u2717 No git repository found"));
2309
+ console.log(chalk10.yellow(" Run: git init"));
2310
+ process.exit(1);
2311
+ }
2312
+ if (!repoInfo.isGitHub) {
2313
+ console.log(chalk10.yellow("\u26A0\uFE0F No GitHub remote detected"));
2314
+ const shouldContinue = await p6.confirm({
2315
+ message: "Create GitHub Actions workflow anyway?",
2316
+ initialValue: true
2317
+ });
2318
+ if (p6.isCancel(shouldContinue) || !shouldContinue) {
2319
+ p6.cancel("Setup cancelled");
2320
+ process.exit(0);
2321
+ }
2322
+ } else {
2323
+ console.log(chalk10.dim(`Repository: ${repoInfo.github?.owner}/${repoInfo.github?.repo}`));
2324
+ }
2325
+ if (hasCIWorkflow(cwd)) {
2326
+ const shouldOverwrite = await p6.confirm({
2327
+ message: "CI workflow already exists. Overwrite?",
2328
+ initialValue: false
2329
+ });
2330
+ if (p6.isCancel(shouldOverwrite) || !shouldOverwrite) {
2331
+ p6.cancel("Setup cancelled");
2332
+ process.exit(0);
2333
+ }
2334
+ }
2335
+ const projectInfo = await getProjectInfo(cwd);
2336
+ console.log(chalk10.dim(`Package manager: ${projectInfo.packageManager}`));
2337
+ console.log(chalk10.dim(`Monorepo: ${projectInfo.isMonorepo ? "yes" : "no"}`));
2338
+ const config = await loadConfig();
2339
+ const ciConfig = config?.ci;
2340
+ const spinner5 = p6.spinner();
2341
+ spinner5.start("Creating CI workflow...");
2342
+ const result = await createCIWorkflow({
2343
+ projectPath: cwd,
2344
+ packageManager: projectInfo.packageManager,
2345
+ isMonorepo: projectInfo.isMonorepo,
2346
+ ciConfig,
2347
+ defaultBranch: repoInfo.defaultBranch || "main"
2348
+ });
2349
+ if (result.success) {
2350
+ spinner5.stop(chalk10.green("\u2713 Created CI workflow"));
2351
+ console.log(chalk10.dim(` File: ${result.filePath}`));
2352
+ console.log(chalk10.green("\n\u2713 GitHub Actions CI setup complete"));
2353
+ console.log(chalk10.dim("\nThe workflow will run on:"));
2354
+ console.log(chalk10.dim(" \u2022 Push to main/develop branches"));
2355
+ console.log(chalk10.dim(" \u2022 Pull requests to main/develop branches"));
2356
+ console.log(chalk10.dim("\nChecks included:"));
2357
+ const checks = ciConfig?.checks || ["lint", "typecheck", "format", "build", "test"];
2358
+ for (const check of checks) {
2359
+ const hasScript = check === "lint" ? projectInfo.hasLintScript : check === "typecheck" ? projectInfo.hasTypecheckScript : check === "format" ? projectInfo.hasFormatScript : check === "test" ? projectInfo.hasTestScript : check === "build" ? projectInfo.hasBuildScript : false;
2360
+ const status = hasScript ? chalk10.green("\u2713") : chalk10.yellow("\u26A0");
2361
+ const note = hasScript ? "" : " (add script to package.json)";
2362
+ console.log(chalk10.dim(` ${status} ${check}${note}`));
2363
+ }
2364
+ console.log(chalk10.cyan("\n\u{1F4A1} Recommended: Enable branch protection"));
2365
+ console.log(chalk10.dim(" Go to GitHub \u2192 Settings \u2192 Branches \u2192 Add rule"));
2366
+ console.log(chalk10.dim(' Enable "Require status checks to pass before merging"'));
2367
+ console.log(chalk10.dim(' Select the "ci" status check'));
2368
+ } else {
2369
+ spinner5.stop(chalk10.red("\u2717 Failed to create CI workflow"));
2370
+ console.error(chalk10.red(` Error: ${result.error}`));
2371
+ process.exit(1);
2372
+ }
2373
+ }
2374
+ async function checkAction(cwd) {
2375
+ console.log(chalk10.bold.cyan("\n\u{1F50D} Checking GitHub Actions CI Setup\n"));
2376
+ const result = await validateGitHubActionsSetup(cwd);
2377
+ if (result.hasWorkflowFile) {
2378
+ console.log(chalk10.green("\u2713 CI workflow file found"));
2379
+ } else {
2380
+ console.log(chalk10.red("\u2717 No CI workflow file found"));
2381
+ console.log(chalk10.yellow(" Run: workflow github:setup"));
2382
+ process.exit(1);
2383
+ }
2384
+ const checks = [
2385
+ { name: "Lint", present: result.hasLintCheck },
2386
+ { name: "Type check", present: result.hasTypecheckCheck },
2387
+ { name: "Format", present: result.hasFormatCheck },
2388
+ { name: "Build", present: result.hasBuildCheck },
2389
+ { name: "Test", present: result.hasTestCheck }
2390
+ ];
2391
+ console.log(chalk10.dim("\nCI checks:"));
2392
+ for (const check of checks) {
2393
+ const icon = check.present ? chalk10.green("\u2713") : chalk10.yellow("\u26A0");
2394
+ console.log(` ${icon} ${check.name}`);
2395
+ }
2396
+ if (result.errors.length > 0) {
2397
+ console.log(chalk10.red("\nErrors:"));
2398
+ for (const error of result.errors) {
2399
+ console.log(chalk10.red(` \u2022 ${error}`));
2400
+ }
2401
+ }
2402
+ if (result.warnings.length > 0) {
2403
+ console.log(chalk10.yellow("\nWarnings:"));
2404
+ for (const warning of result.warnings) {
2405
+ console.log(chalk10.yellow(` \u2022 ${warning}`));
2406
+ }
2407
+ }
2408
+ if (result.valid) {
2409
+ console.log(chalk10.green("\n\u2713 CI setup is valid"));
2410
+ } else {
2411
+ console.log(chalk10.red("\n\u2717 CI setup has issues"));
2412
+ console.log(chalk10.yellow(" Run: workflow github:setup"));
2413
+ process.exit(1);
2414
+ }
2415
+ }
2416
+
1194
2417
  // src/cli/index.ts
1195
2418
  var program = new Command();
1196
2419
  program.name("workflow").description("A self-evolving workflow management system for AI agent development").version("1.0.0");
@@ -1199,7 +2422,9 @@ program.command("validate <type>").description("Validate branch name, commit mes
1199
2422
  program.command("config <action>").description("Manage workflow configuration").argument("<action>", "Action: get, set, add, remove").argument("[key]", "Config key").argument("[value]", "Config value").action(configCommand);
1200
2423
  program.command("suggest").description("Submit an improvement suggestion").argument("<feedback>", "Your improvement suggestion").option("--author <author>", "Your name or username").option("--category <category>", "Category: feature, bug, documentation, performance, other").action(suggestCommand);
1201
2424
  program.command("setup").description("Add workflow scripts to package.json").action(setupCommand);
1202
- program.command("doctor").description("Run health check and get optimization suggestions").action(doctorCommand);
2425
+ program.command("doctor").description("Run health check and get optimization suggestions").option("--check-guidelines-only", "Only check mandatory guidelines exist (exits 0 or 1)").action(doctorCommand);
2426
+ program.command("hooks").description("Manage git hooks (install, uninstall, status)").argument("<action>", "Action: install, uninstall, status").action(hooksCommand);
2427
+ program.command("github").description("Manage GitHub Actions CI (setup, check)").argument("<action>", "Action: setup, check").action(githubCommand);
1203
2428
  program.command("scope:create").description("Create a custom scope package").option("--name <name>", 'Package name (e.g., "fintech", "gaming")').option("--scopes <scopes>", "Comma-separated scopes (format: name:description:emoji:category)").option("--preset-name <preset>", "Preset display name").option("--output-dir <dir>", "Output directory").option("--no-test", "Skip test file generation").action(scopeCreateCommand);
1204
2429
  program.command("scope:migrate").description("Migrate inline scopes to a custom package").option("--name <name>", "Package name for the preset").option("--output-dir <dir>", "Output directory").option("--keep-config", "Keep inline scopes in config after migration").action(scopeMigrateCommand);
1205
2430
  program.parse();