@markjaquith/agency 1.11.1 → 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.
Files changed (130) hide show
  1. package/README.md +302 -113
  2. package/cli.ts +123 -420
  3. package/index.ts +1 -1
  4. package/package.json +12 -17
  5. package/skills/agency/SKILL.md +258 -0
  6. package/src/cli.test.ts +290 -0
  7. package/src/commands/description.test.ts +103 -0
  8. package/src/commands/epic.test.ts +103 -0
  9. package/src/commands/epic.ts +101 -0
  10. package/src/commands/init.test.ts +42 -290
  11. package/src/commands/init.ts +15 -123
  12. package/src/commands/phase.ts +120 -0
  13. package/src/commands/pr.test.ts +19 -170
  14. package/src/commands/pr.ts +23 -179
  15. package/src/commands/repo.test.ts +80 -0
  16. package/src/commands/repo.ts +83 -0
  17. package/src/commands/status.test.ts +35 -477
  18. package/src/commands/status.ts +24 -213
  19. package/src/commands/task-phase.test.ts +206 -0
  20. package/src/commands/task.ts +83 -1285
  21. package/src/commands/validate.test.ts +91 -0
  22. package/src/commands/validate.ts +50 -0
  23. package/src/commands/work.test.ts +172 -419
  24. package/src/commands/work.ts +47 -166
  25. package/src/services/EpicService.test.ts +71 -0
  26. package/src/services/EpicService.ts +135 -0
  27. package/src/services/FileSystemService.ts +61 -92
  28. package/src/services/PhaseService.ts +323 -0
  29. package/src/services/PullRequestService.test.ts +386 -0
  30. package/src/services/PullRequestService.ts +143 -0
  31. package/src/services/RepositoryService.test.ts +106 -0
  32. package/src/services/RepositoryService.ts +159 -0
  33. package/src/services/TaskPhaseService.test.ts +217 -0
  34. package/src/services/TaskService.ts +192 -0
  35. package/src/services/WorkbaseService.test.ts +446 -0
  36. package/src/services/WorkbaseService.ts +493 -0
  37. package/src/services/WorktreeService.test.ts +599 -0
  38. package/src/services/WorktreeService.ts +357 -0
  39. package/src/test-utils.ts +41 -438
  40. package/src/types.ts +1 -215
  41. package/src/utils/command.ts +1 -5
  42. package/src/utils/effect.test.ts +24 -0
  43. package/src/utils/effect.ts +3 -273
  44. package/src/utils/process.test.ts +0 -15
  45. package/src/utils/process.ts +1 -60
  46. package/src/workbase/frontmatter.test.ts +50 -0
  47. package/src/workbase/frontmatter.ts +77 -0
  48. package/src/workbase/repository-reference.test.ts +25 -0
  49. package/src/workbase/repository-reference.ts +18 -0
  50. package/src/workbase/schemas.test.ts +146 -0
  51. package/src/workbase/schemas.ts +78 -0
  52. package/src/workbase/worktree-command.test.ts +61 -0
  53. package/src/workbase/worktree-command.ts +52 -0
  54. package/src/commands/base.test.ts +0 -198
  55. package/src/commands/base.ts +0 -198
  56. package/src/commands/clean.test.ts +0 -299
  57. package/src/commands/clean.ts +0 -274
  58. package/src/commands/completions.test.ts +0 -51
  59. package/src/commands/completions.ts +0 -308
  60. package/src/commands/emit.integration.test.ts +0 -549
  61. package/src/commands/emit.test.ts +0 -576
  62. package/src/commands/emit.ts +0 -700
  63. package/src/commands/emitted.test.ts +0 -226
  64. package/src/commands/emitted.ts +0 -57
  65. package/src/commands/loop.test.ts +0 -621
  66. package/src/commands/loop.ts +0 -406
  67. package/src/commands/merge.integration.test.ts +0 -195
  68. package/src/commands/merge.test.ts +0 -246
  69. package/src/commands/merge.ts +0 -244
  70. package/src/commands/pull.test.ts +0 -395
  71. package/src/commands/pull.ts +0 -205
  72. package/src/commands/push.test.ts +0 -641
  73. package/src/commands/push.ts +0 -482
  74. package/src/commands/rebase.test.ts +0 -521
  75. package/src/commands/rebase.ts +0 -232
  76. package/src/commands/save.test.ts +0 -247
  77. package/src/commands/save.ts +0 -162
  78. package/src/commands/source.test.ts +0 -195
  79. package/src/commands/source.ts +0 -72
  80. package/src/commands/switch.test.ts +0 -194
  81. package/src/commands/switch.ts +0 -84
  82. package/src/commands/task-branching.test.ts +0 -633
  83. package/src/commands/task-continue.test.ts +0 -311
  84. package/src/commands/task-edit.test.ts +0 -235
  85. package/src/commands/task-main.test.ts +0 -1066
  86. package/src/commands/task-squash.test.ts +0 -266
  87. package/src/commands/tasks.test.ts +0 -391
  88. package/src/commands/tasks.ts +0 -161
  89. package/src/commands/template-delete.test.ts +0 -178
  90. package/src/commands/template-delete.ts +0 -98
  91. package/src/commands/template-list.test.ts +0 -135
  92. package/src/commands/template-list.ts +0 -87
  93. package/src/commands/template-view.test.ts +0 -158
  94. package/src/commands/template-view.ts +0 -86
  95. package/src/commands/template.test.ts +0 -32
  96. package/src/commands/template.ts +0 -96
  97. package/src/commands/use.test.ts +0 -87
  98. package/src/commands/use.ts +0 -97
  99. package/src/constants.ts +0 -10
  100. package/src/errors.ts +0 -17
  101. package/src/schemas.ts +0 -33
  102. package/src/services/AgencyMetadataService.ts +0 -311
  103. package/src/services/ClaudeService.test.ts +0 -184
  104. package/src/services/ClaudeService.ts +0 -91
  105. package/src/services/ConfigService.ts +0 -115
  106. package/src/services/FilterRepoService.ts +0 -164
  107. package/src/services/FormatterService.test.ts +0 -432
  108. package/src/services/FormatterService.ts +0 -219
  109. package/src/services/GitService.ts +0 -1187
  110. package/src/services/MockFilterRepoService.ts +0 -140
  111. package/src/services/OpencodeService.ts +0 -263
  112. package/src/services/PromptService.ts +0 -183
  113. package/src/services/TemplateService.ts +0 -81
  114. package/src/types/native-exec.d.ts +0 -8
  115. package/src/utils/colors.ts +0 -178
  116. package/src/utils/git-path.ts +0 -6
  117. package/src/utils/glob.test.ts +0 -161
  118. package/src/utils/glob.ts +0 -78
  119. package/src/utils/paths.ts +0 -51
  120. package/src/utils/pr-branch.test.ts +0 -376
  121. package/src/utils/pr-branch.ts +0 -508
  122. package/src/utils/spinner.test.ts +0 -45
  123. package/src/utils/spinner.ts +0 -96
  124. package/src/utils/task-parser.test.ts +0 -172
  125. package/src/utils/task-parser.ts +0 -93
  126. package/templates/AGENCY.md +0 -22
  127. package/templates/AGENTS.md +0 -11
  128. package/templates/CLAUDE.md +0 -3
  129. package/templates/TASK.md +0 -5
  130. package/templates/opencode.json +0 -4
@@ -1,178 +0,0 @@
1
- /**
2
- * Color highlighting utilities for CLI output
3
- *
4
- * This module provides centralized color management for highlighting
5
- * meaningful values in CLI feedback. All highlight types use the same
6
- * base color initially, but are separated into buckets to allow for
7
- * future customization.
8
- *
9
- * Colors can be disabled via:
10
- * - NO_COLOR environment variable (standard)
11
- * - Setting colorsEnabled to false
12
- */
13
-
14
- // ANSI color codes
15
- const RESET = "\x1b[0m"
16
- const CYAN_BRIGHT = "\x1b[96m"
17
- const GREEN = "\x1b[32m"
18
- const YELLOW = "\x1b[33m"
19
-
20
- /**
21
- * Central color configuration
22
- * All buckets use the same color initially (bright cyan)
23
- * but can be easily changed independently in the future
24
- */
25
- const COLORS = {
26
- branch: CYAN_BRIGHT,
27
- template: CYAN_BRIGHT,
28
- file: CYAN_BRIGHT,
29
- setting: CYAN_BRIGHT,
30
- value: CYAN_BRIGHT,
31
- commit: CYAN_BRIGHT,
32
- pattern: CYAN_BRIGHT,
33
- remote: YELLOW,
34
- } as const
35
-
36
- /**
37
- * Global flag to enable/disable colors
38
- * Defaults to enabled unless NO_COLOR environment variable is set
39
- */
40
- let colorsEnabled = !process.env.NO_COLOR
41
-
42
- /**
43
- * Enable or disable color output
44
- * @param enabled - Whether to enable colors
45
- */
46
- export function setColorsEnabled(enabled: boolean): void {
47
- colorsEnabled = enabled
48
- }
49
-
50
- /**
51
- * Check if colors are currently enabled
52
- */
53
- function areColorsEnabled(): boolean {
54
- return colorsEnabled
55
- }
56
-
57
- /**
58
- * Internal helper to apply color formatting
59
- * Returns plain text if colors are disabled
60
- */
61
- function colorize(text: string, color: string): string {
62
- if (!colorsEnabled) {
63
- return text
64
- }
65
- return `${color}${text}${RESET}`
66
- }
67
-
68
- /**
69
- * Highlight a branch name
70
- * @example highlight.branch("main") -> "\x1b[96mmain\x1b[0m"
71
- */
72
- function branch(name: string): string {
73
- return colorize(name, COLORS.branch)
74
- }
75
-
76
- /**
77
- * Highlight a template name
78
- * @example highlight.template("my-template") -> "\x1b[96mmy-template\x1b[0m"
79
- */
80
- function template(name: string): string {
81
- return colorize(name, COLORS.template)
82
- }
83
-
84
- /**
85
- * Highlight a file name or path
86
- * @example highlight.file("AGENTS.md") -> "\x1b[96mAGENTS.md\x1b[0m"
87
- */
88
- function file(name: string): string {
89
- return colorize(name, COLORS.file)
90
- }
91
-
92
- /**
93
- * Highlight a setting name
94
- * @example highlight.setting("agency.template") -> "\x1b[96magency.template\x1b[0m"
95
- */
96
- function setting(name: string): string {
97
- return colorize(name, COLORS.setting)
98
- }
99
-
100
- /**
101
- * Highlight a numeric value or count
102
- * @example highlight.value("3") -> "\x1b[96m3\x1b[0m"
103
- */
104
- function value(val: string | number): string {
105
- return colorize(String(val), COLORS.value)
106
- }
107
-
108
- /**
109
- * Highlight a commit hash
110
- * @example highlight.commit("abc123") -> "\x1b[96mabc123\x1b[0m"
111
- */
112
- function commit(hash: string): string {
113
- return colorize(hash, COLORS.commit)
114
- }
115
-
116
- /**
117
- * Highlight a pattern or placeholder
118
- * @example highlight.pattern("{task}") -> "\x1b[96m{task}\x1b[0m"
119
- */
120
- function pattern(text: string): string {
121
- return colorize(text, COLORS.pattern)
122
- }
123
-
124
- /**
125
- * Highlight a git remote name
126
- * @example highlight.remote("origin") -> "\x1b[33morigin\x1b[0m"
127
- */
128
- function remote(name: string): string {
129
- return colorize(name, COLORS.remote)
130
- }
131
-
132
- /**
133
- * Prepends a green checkmark to a message for success output
134
- * Uses the same checkmark symbol and color as ora for consistency
135
- * @param message - The message to prepend the checkmark to
136
- * @example log(done(`Merged ${highlight.branch("main")}`))
137
- * @example log(done("Operation completed"))
138
- */
139
- export function done(message: string): string {
140
- const checkmark = colorsEnabled ? `\x1b[32m✔\x1b[0m` : "✔"
141
- return `${checkmark} ${message}`
142
- }
143
-
144
- /**
145
- * Prepends an info icon to a message for informational output
146
- * @param message - The message to prepend the info icon to
147
- * @example log(info("Skipping task description"))
148
- * @example log(info(`File already exists`))
149
- */
150
- export function info(message: string): string {
151
- return `ⓘ ${message}`
152
- }
153
-
154
- /**
155
- * Returns "s" if count is not 1, empty string otherwise
156
- * Useful for pluralizing words in messages
157
- * @param count - The count to check
158
- * @example `Committed ${count} file${plural(count)}` -> "Committed 1 file" or "Committed 3 files"
159
- */
160
- export function plural(count: number): string {
161
- return count === 1 ? "" : "s"
162
- }
163
-
164
- /**
165
- * Default export as namespace for convenient usage
166
- * @example import highlight from "./colors"
167
- * @example console.log(`Switched to ${highlight.branch("main")}`)
168
- */
169
- export default {
170
- branch,
171
- template,
172
- file,
173
- setting,
174
- value,
175
- commit,
176
- pattern,
177
- remote,
178
- }
@@ -1,6 +0,0 @@
1
- import { isAbsolute, resolve } from "path"
2
-
3
- export const resolveGitInternalPath = (
4
- gitRoot: string,
5
- gitPath: string,
6
- ): string => (isAbsolute(gitPath) ? gitPath : resolve(gitRoot, gitPath))
@@ -1,161 +0,0 @@
1
- import { test, expect, describe, beforeAll, afterAll } from "bun:test"
2
- import { mkdtemp, rm, mkdir, writeFile } from "node:fs/promises"
3
- import { tmpdir } from "node:os"
4
- import { join } from "node:path"
5
- import {
6
- isGlobPattern,
7
- matchesGlob,
8
- expandGlobs,
9
- dirToGlobPattern,
10
- getTopLevelDir,
11
- } from "./glob"
12
-
13
- describe("isGlobPattern", () => {
14
- test("returns true for patterns with asterisk", () => {
15
- expect(isGlobPattern("*.ts")).toBe(true)
16
- expect(isGlobPattern("**/*.ts")).toBe(true)
17
- expect(isGlobPattern("plans/*")).toBe(true)
18
- expect(isGlobPattern("plans/**")).toBe(true)
19
- })
20
-
21
- test("returns true for patterns with question mark", () => {
22
- expect(isGlobPattern("file?.ts")).toBe(true)
23
- })
24
-
25
- test("returns true for patterns with character classes", () => {
26
- expect(isGlobPattern("[abc].ts")).toBe(true)
27
- expect(isGlobPattern("file[0-9].ts")).toBe(true)
28
- })
29
-
30
- test("returns false for plain file paths", () => {
31
- expect(isGlobPattern("foo.ts")).toBe(false)
32
- expect(isGlobPattern("plans/README.md")).toBe(false)
33
- expect(isGlobPattern("src/utils/glob.ts")).toBe(false)
34
- })
35
- })
36
-
37
- describe("matchesGlob", () => {
38
- test("matches files with simple wildcard", () => {
39
- expect(matchesGlob("*.ts", "file.ts")).toBe(true)
40
- expect(matchesGlob("*.ts", "file.js")).toBe(false)
41
- })
42
-
43
- test("matches files in directories with **", () => {
44
- expect(matchesGlob("plans/**", "plans/foo.md")).toBe(true)
45
- expect(matchesGlob("plans/**", "plans/sub/bar.md")).toBe(true)
46
- expect(matchesGlob("plans/**", "other/foo.md")).toBe(false)
47
- })
48
-
49
- test("matches nested directory patterns", () => {
50
- expect(matchesGlob("src/**/*.ts", "src/utils/glob.ts")).toBe(true)
51
- expect(matchesGlob("src/**/*.ts", "src/glob.ts")).toBe(true)
52
- expect(matchesGlob("src/**/*.ts", "lib/glob.ts")).toBe(false)
53
- })
54
- })
55
-
56
- describe("dirToGlobPattern", () => {
57
- test("converts directory to glob pattern", () => {
58
- expect(dirToGlobPattern("plans")).toBe("plans/**")
59
- expect(dirToGlobPattern("src/components")).toBe("src/components/**")
60
- })
61
-
62
- test("removes trailing slash before adding glob", () => {
63
- expect(dirToGlobPattern("plans/")).toBe("plans/**")
64
- expect(dirToGlobPattern("plans//")).toBe("plans/**")
65
- })
66
- })
67
-
68
- describe("getTopLevelDir", () => {
69
- test("returns top-level directory from path", () => {
70
- expect(getTopLevelDir("plans/foo.md")).toBe("plans")
71
- expect(getTopLevelDir("plans/sub/bar.md")).toBe("plans")
72
- expect(getTopLevelDir("src/utils/glob.ts")).toBe("src")
73
- })
74
-
75
- test("returns null for root-level files", () => {
76
- expect(getTopLevelDir("README.md")).toBe(null)
77
- expect(getTopLevelDir("file.ts")).toBe(null)
78
- })
79
- })
80
-
81
- describe("expandGlobs", () => {
82
- let tempDir: string
83
-
84
- beforeAll(async () => {
85
- // Create temp directory with test files
86
- tempDir = await mkdtemp(join(tmpdir(), "glob-test-"))
87
-
88
- // Create directory structure:
89
- // tempDir/
90
- // plans/
91
- // foo.md
92
- // sub/
93
- // bar.md
94
- // other/
95
- // baz.md
96
- // root.txt
97
- await mkdir(join(tempDir, "plans"))
98
- await mkdir(join(tempDir, "plans", "sub"))
99
- await mkdir(join(tempDir, "other"))
100
- await mkdir(join(tempDir, ".agents", "foo"), { recursive: true })
101
-
102
- await writeFile(join(tempDir, "plans", "foo.md"), "foo")
103
- await writeFile(join(tempDir, "plans", "sub", "bar.md"), "bar")
104
- await writeFile(join(tempDir, "other", "baz.md"), "baz")
105
- await writeFile(join(tempDir, ".agents", "foo", "bar.whatever"), "dot")
106
- await writeFile(join(tempDir, "root.txt"), "root")
107
- })
108
-
109
- afterAll(async () => {
110
- await rm(tempDir, { recursive: true })
111
- })
112
-
113
- test("expands glob pattern to matching files", async () => {
114
- const files = await expandGlobs(["plans/**"], tempDir)
115
- expect(files.sort()).toEqual(["plans/foo.md", "plans/sub/bar.md"].sort())
116
- })
117
-
118
- test("preserves non-glob paths as-is", async () => {
119
- const files = await expandGlobs(["root.txt"], tempDir)
120
- expect(files).toEqual(["root.txt"])
121
- })
122
-
123
- test("combines glob and non-glob patterns", async () => {
124
- const files = await expandGlobs(["plans/**", "root.txt"], tempDir)
125
- expect(files.sort()).toEqual(
126
- ["plans/foo.md", "plans/sub/bar.md", "root.txt"].sort(),
127
- )
128
- })
129
-
130
- test("deduplicates results", async () => {
131
- const files = await expandGlobs(
132
- ["plans/**", "plans/foo.md", "plans/**"],
133
- tempDir,
134
- )
135
- // Should not have duplicates
136
- const uniqueFiles = [...new Set(files)]
137
- expect(files.length).toBe(uniqueFiles.length)
138
- })
139
-
140
- test("returns non-glob paths even if they don't exist", async () => {
141
- const files = await expandGlobs(["nonexistent.txt"], tempDir)
142
- expect(files).toEqual(["nonexistent.txt"])
143
- })
144
-
145
- test("returns empty array for non-matching glob", async () => {
146
- const files = await expandGlobs(["nonexistent/**"], tempDir)
147
- expect(files).toEqual([])
148
- })
149
-
150
- test("handles multiple glob patterns", async () => {
151
- const files = await expandGlobs(["plans/**", "other/**"], tempDir)
152
- expect(files.sort()).toEqual(
153
- ["plans/foo.md", "plans/sub/bar.md", "other/baz.md"].sort(),
154
- )
155
- })
156
-
157
- test("expands glob patterns inside dot-directories", async () => {
158
- const files = await expandGlobs([".agents/**"], tempDir)
159
- expect(files).toEqual([".agents/foo/bar.whatever"])
160
- })
161
- })
package/src/utils/glob.ts DELETED
@@ -1,78 +0,0 @@
1
- /**
2
- * Utilities for glob pattern matching and expansion.
3
- * Uses Bun's built-in Glob API.
4
- */
5
-
6
- /**
7
- * Check if a string contains glob pattern characters.
8
- * This detects wildcards like *, **, ?, and character classes like [abc].
9
- */
10
- export function isGlobPattern(str: string): boolean {
11
- return /[*?[\]]/.test(str)
12
- }
13
-
14
- /**
15
- * Check if a path matches a glob pattern.
16
- * Uses Bun.Glob for pattern matching.
17
- */
18
- export function matchesGlob(pattern: string, path: string): boolean {
19
- const glob = new Bun.Glob(pattern)
20
- return glob.match(path)
21
- }
22
-
23
- /**
24
- * Expand glob patterns to actual file paths.
25
- * Non-glob paths are returned as-is.
26
- *
27
- * @param patterns - Array of file paths or glob patterns
28
- * @param cwd - Working directory to resolve patterns against
29
- * @returns Array of expanded file paths (deduplicated)
30
- */
31
- export async function expandGlobs(
32
- patterns: string[],
33
- cwd: string,
34
- ): Promise<string[]> {
35
- const files = new Set<string>()
36
-
37
- for (const pattern of patterns) {
38
- if (isGlobPattern(pattern)) {
39
- // Expand glob pattern
40
- const glob = new Bun.Glob(pattern)
41
- for await (const file of glob.scan({ cwd, dot: true })) {
42
- files.add(file)
43
- }
44
- } else {
45
- // Non-glob path, add as-is
46
- files.add(pattern)
47
- }
48
- }
49
-
50
- return Array.from(files)
51
- }
52
-
53
- /**
54
- * Convert a directory path to a glob pattern.
55
- * Appends /** to match all files recursively in the directory.
56
- *
57
- * @param dirPath - Directory path (e.g., "plans" or "plans/")
58
- * @returns Glob pattern (e.g., "plans/**")
59
- */
60
- export function dirToGlobPattern(dirPath: string): string {
61
- // Remove trailing slash if present
62
- const normalized = dirPath.replace(/\/+$/, "")
63
- return `${normalized}/**`
64
- }
65
-
66
- /**
67
- * Extract the top-level directory from a file path.
68
- *
69
- * @param filePath - File path (e.g., "plans/foo/bar.md")
70
- * @returns Top-level directory name (e.g., "plans") or null if no directory
71
- */
72
- export function getTopLevelDir(filePath: string): string | null {
73
- const parts = filePath.split("/")
74
- if (parts.length > 1) {
75
- return parts[0] || null
76
- }
77
- return null
78
- }
@@ -1,51 +0,0 @@
1
- /**
2
- * Utilities for resolving agency configuration paths.
3
- */
4
-
5
- import { homedir } from "node:os"
6
- import { join } from "node:path"
7
-
8
- /**
9
- * Get the agency configuration directory.
10
- * Defaults to ~/.config/agency, can be overridden via AGENCY_CONFIG_DIR env var.
11
- * @param override - Optional override for the config directory (used in testing)
12
- */
13
- export function getAgencyConfigDir(override?: string): string {
14
- return (
15
- override ||
16
- process.env.AGENCY_CONFIG_DIR ||
17
- join(homedir(), ".config", "agency")
18
- )
19
- }
20
-
21
- /**
22
- * Get the path to the agency config file (agency.json).
23
- * Can be overridden via AGENCY_CONFIG_PATH env var.
24
- * @param configDir - Optional config directory override
25
- */
26
- export function getAgencyConfigPath(configDir?: string): string {
27
- return (
28
- process.env.AGENCY_CONFIG_PATH ||
29
- join(getAgencyConfigDir(configDir), "agency.json")
30
- )
31
- }
32
-
33
- /**
34
- * Get the templates directory path.
35
- * @param configDir - Optional config directory override
36
- */
37
- export function getTemplatesDir(configDir?: string): string {
38
- return join(getAgencyConfigDir(configDir), "templates")
39
- }
40
-
41
- /**
42
- * Get the path to a specific template directory.
43
- * @param templateName - Name of the template
44
- * @param configDir - Optional config directory override
45
- */
46
- export function getTemplateDir(
47
- templateName: string,
48
- configDir?: string,
49
- ): string {
50
- return join(getTemplatesDir(configDir), templateName)
51
- }