@markjaquith/agency 1.11.1 → 2.1.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 (132) hide show
  1. package/README.md +313 -117
  2. package/cli.ts +124 -420
  3. package/index.ts +1 -1
  4. package/package.json +12 -17
  5. package/skills/agency/SKILL.md +266 -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 +326 -399
  24. package/src/commands/work.ts +153 -153
  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/work-target.test.ts +77 -0
  53. package/src/workbase/work-target.ts +180 -0
  54. package/src/workbase/worktree-command.test.ts +61 -0
  55. package/src/workbase/worktree-command.ts +52 -0
  56. package/src/commands/base.test.ts +0 -198
  57. package/src/commands/base.ts +0 -198
  58. package/src/commands/clean.test.ts +0 -299
  59. package/src/commands/clean.ts +0 -274
  60. package/src/commands/completions.test.ts +0 -51
  61. package/src/commands/completions.ts +0 -308
  62. package/src/commands/emit.integration.test.ts +0 -549
  63. package/src/commands/emit.test.ts +0 -576
  64. package/src/commands/emit.ts +0 -700
  65. package/src/commands/emitted.test.ts +0 -226
  66. package/src/commands/emitted.ts +0 -57
  67. package/src/commands/loop.test.ts +0 -621
  68. package/src/commands/loop.ts +0 -406
  69. package/src/commands/merge.integration.test.ts +0 -195
  70. package/src/commands/merge.test.ts +0 -246
  71. package/src/commands/merge.ts +0 -244
  72. package/src/commands/pull.test.ts +0 -395
  73. package/src/commands/pull.ts +0 -205
  74. package/src/commands/push.test.ts +0 -641
  75. package/src/commands/push.ts +0 -482
  76. package/src/commands/rebase.test.ts +0 -521
  77. package/src/commands/rebase.ts +0 -232
  78. package/src/commands/save.test.ts +0 -247
  79. package/src/commands/save.ts +0 -162
  80. package/src/commands/source.test.ts +0 -195
  81. package/src/commands/source.ts +0 -72
  82. package/src/commands/switch.test.ts +0 -194
  83. package/src/commands/switch.ts +0 -84
  84. package/src/commands/task-branching.test.ts +0 -633
  85. package/src/commands/task-continue.test.ts +0 -311
  86. package/src/commands/task-edit.test.ts +0 -235
  87. package/src/commands/task-main.test.ts +0 -1066
  88. package/src/commands/task-squash.test.ts +0 -266
  89. package/src/commands/tasks.test.ts +0 -391
  90. package/src/commands/tasks.ts +0 -161
  91. package/src/commands/template-delete.test.ts +0 -178
  92. package/src/commands/template-delete.ts +0 -98
  93. package/src/commands/template-list.test.ts +0 -135
  94. package/src/commands/template-list.ts +0 -87
  95. package/src/commands/template-view.test.ts +0 -158
  96. package/src/commands/template-view.ts +0 -86
  97. package/src/commands/template.test.ts +0 -32
  98. package/src/commands/template.ts +0 -96
  99. package/src/commands/use.test.ts +0 -87
  100. package/src/commands/use.ts +0 -97
  101. package/src/constants.ts +0 -10
  102. package/src/errors.ts +0 -17
  103. package/src/schemas.ts +0 -33
  104. package/src/services/AgencyMetadataService.ts +0 -311
  105. package/src/services/ClaudeService.test.ts +0 -184
  106. package/src/services/ClaudeService.ts +0 -91
  107. package/src/services/ConfigService.ts +0 -115
  108. package/src/services/FilterRepoService.ts +0 -164
  109. package/src/services/FormatterService.test.ts +0 -432
  110. package/src/services/FormatterService.ts +0 -219
  111. package/src/services/GitService.ts +0 -1187
  112. package/src/services/MockFilterRepoService.ts +0 -140
  113. package/src/services/OpencodeService.ts +0 -263
  114. package/src/services/PromptService.ts +0 -183
  115. package/src/services/TemplateService.ts +0 -81
  116. package/src/types/native-exec.d.ts +0 -8
  117. package/src/utils/colors.ts +0 -178
  118. package/src/utils/git-path.ts +0 -6
  119. package/src/utils/glob.test.ts +0 -161
  120. package/src/utils/glob.ts +0 -78
  121. package/src/utils/paths.ts +0 -51
  122. package/src/utils/pr-branch.test.ts +0 -376
  123. package/src/utils/pr-branch.ts +0 -508
  124. package/src/utils/spinner.test.ts +0 -45
  125. package/src/utils/spinner.ts +0 -96
  126. package/src/utils/task-parser.test.ts +0 -172
  127. package/src/utils/task-parser.ts +0 -93
  128. package/templates/AGENCY.md +0 -22
  129. package/templates/AGENTS.md +0 -11
  130. package/templates/CLAUDE.md +0 -3
  131. package/templates/TASK.md +0 -5
  132. 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
- }