@markjaquith/agency 0.5.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 (75) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +109 -0
  3. package/cli.ts +569 -0
  4. package/index.ts +1 -0
  5. package/package.json +65 -0
  6. package/src/commands/base.test.ts +198 -0
  7. package/src/commands/base.ts +198 -0
  8. package/src/commands/clean.test.ts +299 -0
  9. package/src/commands/clean.ts +320 -0
  10. package/src/commands/emit.test.ts +412 -0
  11. package/src/commands/emit.ts +521 -0
  12. package/src/commands/emitted.test.ts +226 -0
  13. package/src/commands/emitted.ts +57 -0
  14. package/src/commands/init.test.ts +311 -0
  15. package/src/commands/init.ts +140 -0
  16. package/src/commands/merge.test.ts +365 -0
  17. package/src/commands/merge.ts +253 -0
  18. package/src/commands/pull.test.ts +385 -0
  19. package/src/commands/pull.ts +205 -0
  20. package/src/commands/push.test.ts +394 -0
  21. package/src/commands/push.ts +346 -0
  22. package/src/commands/save.test.ts +247 -0
  23. package/src/commands/save.ts +162 -0
  24. package/src/commands/source.test.ts +195 -0
  25. package/src/commands/source.ts +72 -0
  26. package/src/commands/status.test.ts +489 -0
  27. package/src/commands/status.ts +258 -0
  28. package/src/commands/switch.test.ts +194 -0
  29. package/src/commands/switch.ts +84 -0
  30. package/src/commands/task-branching.test.ts +334 -0
  31. package/src/commands/task-edit.test.ts +141 -0
  32. package/src/commands/task-main.test.ts +872 -0
  33. package/src/commands/task.ts +712 -0
  34. package/src/commands/tasks.test.ts +335 -0
  35. package/src/commands/tasks.ts +155 -0
  36. package/src/commands/template-delete.test.ts +178 -0
  37. package/src/commands/template-delete.ts +98 -0
  38. package/src/commands/template-list.test.ts +135 -0
  39. package/src/commands/template-list.ts +87 -0
  40. package/src/commands/template-view.test.ts +158 -0
  41. package/src/commands/template-view.ts +86 -0
  42. package/src/commands/template.test.ts +32 -0
  43. package/src/commands/template.ts +96 -0
  44. package/src/commands/use.test.ts +87 -0
  45. package/src/commands/use.ts +97 -0
  46. package/src/commands/work.test.ts +462 -0
  47. package/src/commands/work.ts +193 -0
  48. package/src/errors.ts +17 -0
  49. package/src/schemas.ts +33 -0
  50. package/src/services/AgencyMetadataService.ts +287 -0
  51. package/src/services/ClaudeService.test.ts +184 -0
  52. package/src/services/ClaudeService.ts +91 -0
  53. package/src/services/ConfigService.ts +115 -0
  54. package/src/services/FileSystemService.ts +222 -0
  55. package/src/services/GitService.ts +751 -0
  56. package/src/services/OpencodeService.ts +263 -0
  57. package/src/services/PromptService.ts +183 -0
  58. package/src/services/TemplateService.ts +75 -0
  59. package/src/test-utils.ts +362 -0
  60. package/src/types/native-exec.d.ts +8 -0
  61. package/src/types.ts +216 -0
  62. package/src/utils/colors.ts +178 -0
  63. package/src/utils/command.ts +17 -0
  64. package/src/utils/effect.ts +281 -0
  65. package/src/utils/exec.ts +48 -0
  66. package/src/utils/paths.ts +51 -0
  67. package/src/utils/pr-branch.test.ts +372 -0
  68. package/src/utils/pr-branch.ts +473 -0
  69. package/src/utils/process.ts +110 -0
  70. package/src/utils/spinner.ts +82 -0
  71. package/templates/AGENCY.md +20 -0
  72. package/templates/AGENTS.md +11 -0
  73. package/templates/CLAUDE.md +3 -0
  74. package/templates/TASK.md +5 -0
  75. package/templates/opencode.json +4 -0
package/package.json ADDED
@@ -0,0 +1,65 @@
1
+ {
2
+ "name": "@markjaquith/agency",
3
+ "version": "0.5.0",
4
+ "description": "Manages personal agents files",
5
+ "license": "MIT",
6
+ "author": "Mark Jaquith",
7
+ "module": "index.ts",
8
+ "type": "module",
9
+ "bin": {
10
+ "agency": "cli.ts"
11
+ },
12
+ "scripts": {
13
+ "test": "bun test",
14
+ "format": "prettier --write .",
15
+ "format:check": "prettier --check .",
16
+ "knip": "knip --production",
17
+ "pushable": "./scripts/pushable",
18
+ "check-commit-msg": "./scripts/check-commit-msg",
19
+ "changeset": "changeset",
20
+ "version": "changeset version",
21
+ "release": "bun run build && changeset publish --provenance",
22
+ "build": "bun build cli.ts --outdir dist --target node --minify"
23
+ },
24
+ "exports": {
25
+ ".": {
26
+ "import": "./index.ts",
27
+ "types": "./index.ts"
28
+ }
29
+ },
30
+ "files": [
31
+ "index.ts",
32
+ "cli.ts",
33
+ "src",
34
+ "templates",
35
+ "README.md",
36
+ "LICENSE"
37
+ ],
38
+ "keywords": [
39
+ "agents",
40
+ "bun",
41
+ "typescript",
42
+ "personal-agents"
43
+ ],
44
+ "devDependencies": {
45
+ "@changesets/cli": "^2.29.8",
46
+ "@types/bun": "latest",
47
+ "knip": "^5.70.1",
48
+ "prettier": "^3.6.2"
49
+ },
50
+ "peerDependencies": {
51
+ "typescript": "^5"
52
+ },
53
+ "engines": {
54
+ "bun": ">=1.0.0"
55
+ },
56
+ "dependencies": {
57
+ "@effect/schema": "^0.75.5",
58
+ "effect": "^3.19.6",
59
+ "jsonc-parser": "^3.3.1",
60
+ "ora": "^8.1.1"
61
+ },
62
+ "trustedDependencies": [
63
+ "@triggi/native-exec"
64
+ ]
65
+ }
@@ -0,0 +1,198 @@
1
+ import { describe, test, expect, beforeEach, afterEach } from "bun:test"
2
+ import { base } from "./base"
3
+ import {
4
+ createTempDir,
5
+ cleanupTempDir,
6
+ initGitRepo,
7
+ getGitConfig,
8
+ runTestEffect,
9
+ } from "../test-utils"
10
+ import { getBaseBranchFromMetadata, writeAgencyMetadata } from "../types"
11
+
12
+ describe("base", () => {
13
+ let testDir: string
14
+ let originalCwd: string
15
+
16
+ beforeEach(async () => {
17
+ originalCwd = process.cwd()
18
+ testDir = await createTempDir()
19
+ await initGitRepo(testDir)
20
+ process.chdir(testDir)
21
+ })
22
+
23
+ afterEach(async () => {
24
+ process.chdir(originalCwd)
25
+ await cleanupTempDir(testDir)
26
+ })
27
+
28
+ describe("base set", () => {
29
+ test("sets base branch for current branch", async () => {
30
+ // Create a feature branch
31
+ await Bun.spawn(["git", "checkout", "-b", "feature"], {
32
+ cwd: testDir,
33
+ stdout: "pipe",
34
+ stderr: "pipe",
35
+ }).exited
36
+
37
+ // Create agency.json first
38
+ await writeAgencyMetadata(testDir, {
39
+ version: 1 as const,
40
+ template: "test",
41
+ injectedFiles: [],
42
+ createdAt: new Date().toISOString(),
43
+ } as any)
44
+
45
+ // Set base branch
46
+ await runTestEffect(
47
+ base({
48
+ subcommand: "set",
49
+ args: ["main"],
50
+ silent: true,
51
+ }),
52
+ )
53
+
54
+ // Verify it was saved
55
+ const savedBase = await getBaseBranchFromMetadata(testDir)
56
+ expect(savedBase).toBe("main")
57
+ })
58
+
59
+ test("sets repository-level default base branch", async () => {
60
+ // Set repo-level base branch
61
+ await runTestEffect(
62
+ base({
63
+ subcommand: "set",
64
+ args: ["main"],
65
+ repo: true,
66
+ silent: true,
67
+ }),
68
+ )
69
+
70
+ // Verify it was saved to git config
71
+ const savedBase = await getGitConfig("agency.baseBranch", testDir)
72
+ expect(savedBase).toBe("main")
73
+ })
74
+
75
+ test("throws error if base branch does not exist", async () => {
76
+ await expect(
77
+ runTestEffect(
78
+ base({
79
+ subcommand: "set",
80
+ args: ["nonexistent"],
81
+ silent: true,
82
+ }),
83
+ ),
84
+ ).rejects.toThrow("does not exist")
85
+ })
86
+ })
87
+
88
+ describe("base get", () => {
89
+ test("gets base branch for current branch", async () => {
90
+ // Create a feature branch
91
+ await Bun.spawn(["git", "checkout", "-b", "feature"], {
92
+ cwd: testDir,
93
+ stdout: "pipe",
94
+ stderr: "pipe",
95
+ }).exited
96
+
97
+ // Create agency.json with base branch
98
+ await writeAgencyMetadata(testDir, {
99
+ version: 1,
100
+ template: "test",
101
+ injectedFiles: [],
102
+ baseBranch: "main",
103
+ createdAt: new Date().toISOString(),
104
+ } as any)
105
+
106
+ // Mock console.log to capture output
107
+ const logs: string[] = []
108
+ const originalLog = console.log
109
+ console.log = (...args: any[]) => logs.push(args.join(" "))
110
+
111
+ try {
112
+ await runTestEffect(
113
+ base({
114
+ subcommand: "get",
115
+ args: [],
116
+ silent: false,
117
+ }),
118
+ )
119
+ expect(logs[0]).toBe("main")
120
+ } finally {
121
+ console.log = originalLog
122
+ }
123
+ })
124
+
125
+ test("throws error if no base branch configured", async () => {
126
+ // Create a feature branch
127
+ await Bun.spawn(["git", "checkout", "-b", "feature"], {
128
+ cwd: testDir,
129
+ stdout: "pipe",
130
+ stderr: "pipe",
131
+ }).exited
132
+
133
+ // Create agency.json without base branch
134
+ await writeAgencyMetadata(testDir, {
135
+ version: 1,
136
+ template: "test",
137
+ injectedFiles: [],
138
+ createdAt: new Date().toISOString(),
139
+ } as any)
140
+
141
+ await expect(
142
+ runTestEffect(
143
+ base({
144
+ subcommand: "get",
145
+ args: [],
146
+ silent: true,
147
+ }),
148
+ ),
149
+ ).rejects.toThrow("No base branch configured")
150
+ })
151
+ })
152
+
153
+ describe("base command", () => {
154
+ test("requires subcommand", async () => {
155
+ await expect(
156
+ runTestEffect(base({ args: [], silent: true })),
157
+ ).rejects.toThrow("Subcommand is required")
158
+ })
159
+
160
+ test("handles 'set' subcommand", async () => {
161
+ await Bun.spawn(["git", "checkout", "-b", "feature"], {
162
+ cwd: testDir,
163
+ stdout: "pipe",
164
+ stderr: "pipe",
165
+ }).exited
166
+
167
+ await writeAgencyMetadata(testDir, {
168
+ version: 1,
169
+ template: "test",
170
+ injectedFiles: [],
171
+ createdAt: new Date().toISOString(),
172
+ } as any)
173
+
174
+ await runTestEffect(
175
+ base({
176
+ subcommand: "set",
177
+ args: ["main"],
178
+ silent: true,
179
+ }),
180
+ )
181
+
182
+ const savedBase = await getBaseBranchFromMetadata(testDir)
183
+ expect(savedBase).toBe("main")
184
+ })
185
+
186
+ test("throws error for unknown subcommand", async () => {
187
+ await expect(
188
+ runTestEffect(
189
+ base({
190
+ subcommand: "unknown",
191
+ args: [],
192
+ silent: true,
193
+ }),
194
+ ),
195
+ ).rejects.toThrow("Unknown subcommand")
196
+ })
197
+ })
198
+ })
@@ -0,0 +1,198 @@
1
+ import { Effect } from "effect"
2
+ import { GitService } from "../services/GitService"
3
+ import { setBaseBranchInMetadata, getBaseBranchFromMetadata } from "../types"
4
+ import highlight, { done } from "../utils/colors"
5
+ import {
6
+ createLoggers,
7
+ ensureGitRepo,
8
+ ensureBranchExists,
9
+ } from "../utils/effect"
10
+
11
+ interface BaseOptions {
12
+ subcommand?: string
13
+ args: string[]
14
+ repo?: boolean
15
+ silent?: boolean
16
+ verbose?: boolean
17
+ }
18
+
19
+ interface BaseSetOptions {
20
+ baseBranch: string
21
+ repo?: boolean
22
+ silent?: boolean
23
+ verbose?: boolean
24
+ }
25
+
26
+ interface BaseGetOptions {
27
+ repo?: boolean
28
+ silent?: boolean
29
+ verbose?: boolean
30
+ }
31
+
32
+ // Effect-based implementation
33
+ const baseSetEffect = (options: BaseSetOptions) =>
34
+ Effect.gen(function* () {
35
+ const { baseBranch, repo = false } = options
36
+ const { log, verboseLog } = createLoggers(options)
37
+
38
+ const git = yield* GitService
39
+ const gitRoot = yield* ensureGitRepo()
40
+
41
+ // Validate that the base branch exists
42
+ yield* ensureBranchExists(
43
+ gitRoot,
44
+ baseBranch,
45
+ `Base branch ${highlight.branch(baseBranch)} does not exist. Please provide a valid branch name.`,
46
+ )
47
+
48
+ if (repo) {
49
+ // Set repository-level default base branch in git config
50
+ yield* git.setDefaultBaseBranchConfig(baseBranch, gitRoot)
51
+ log(
52
+ done(
53
+ `Set repository-level default base branch to ${highlight.branch(baseBranch)}`,
54
+ ),
55
+ )
56
+ } else {
57
+ // Set branch-specific base branch in agency.json
58
+ const currentBranch = yield* git.getCurrentBranch(gitRoot)
59
+ verboseLog(`Current branch: ${highlight.branch(currentBranch)}`)
60
+
61
+ // Use Effect.tryPromise for metadata functions
62
+ yield* Effect.tryPromise({
63
+ try: () => setBaseBranchInMetadata(gitRoot, baseBranch),
64
+ catch: (error) =>
65
+ new Error(`Failed to set base branch in metadata: ${error}`),
66
+ })
67
+ log(
68
+ done(
69
+ `Set base branch to ${highlight.branch(baseBranch)} for ${highlight.branch(currentBranch)}`,
70
+ ),
71
+ )
72
+ }
73
+ })
74
+
75
+ // Effect-based implementation
76
+ const baseGetEffect = (options: BaseGetOptions) =>
77
+ Effect.gen(function* () {
78
+ const { repo = false } = options
79
+ const { log, verboseLog } = createLoggers(options)
80
+
81
+ const git = yield* GitService
82
+ const gitRoot = yield* ensureGitRepo()
83
+
84
+ let currentBase: string | null
85
+
86
+ if (repo) {
87
+ // Get repository-level default base branch from git config
88
+ verboseLog("Reading repository-level default base branch from git config")
89
+ currentBase = yield* git.getDefaultBaseBranchConfig(gitRoot)
90
+
91
+ if (!currentBase) {
92
+ return yield* Effect.fail(
93
+ new Error(
94
+ "No repository-level base branch configured. Use 'agency base set --repo <branch>' to set one.",
95
+ ),
96
+ )
97
+ }
98
+ } else {
99
+ // Get current branch
100
+ const currentBranch = yield* git.getCurrentBranch(gitRoot)
101
+ verboseLog(`Current branch: ${highlight.branch(currentBranch)}`)
102
+
103
+ // Get branch-specific base branch from agency.json
104
+ verboseLog("Reading branch-specific base branch from agency.json")
105
+ currentBase = yield* Effect.tryPromise({
106
+ try: () => getBaseBranchFromMetadata(gitRoot),
107
+ catch: (error) =>
108
+ new Error(`Failed to get base branch from metadata: ${error}`),
109
+ })
110
+
111
+ if (!currentBase) {
112
+ return yield* Effect.fail(
113
+ new Error(
114
+ `No base branch configured for ${highlight.branch(currentBranch)}. Use 'agency base set <branch>' to set one.`,
115
+ ),
116
+ )
117
+ }
118
+ }
119
+
120
+ log(currentBase)
121
+ })
122
+
123
+ export const base = (options: BaseOptions) =>
124
+ Effect.gen(function* () {
125
+ const {
126
+ subcommand,
127
+ args,
128
+ repo = false,
129
+ silent = false,
130
+ verbose = false,
131
+ } = options
132
+
133
+ if (!subcommand) {
134
+ return yield* Effect.fail(
135
+ new Error("Subcommand is required. Usage: agency base <subcommand>"),
136
+ )
137
+ }
138
+
139
+ switch (subcommand) {
140
+ case "set": {
141
+ if (!args[0]) {
142
+ return yield* Effect.fail(
143
+ new Error(
144
+ "Base branch argument is required. Usage: agency base set <branch>",
145
+ ),
146
+ )
147
+ }
148
+ return yield* baseSetEffect({
149
+ baseBranch: args[0],
150
+ repo,
151
+ silent,
152
+ verbose,
153
+ })
154
+ }
155
+ case "get": {
156
+ return yield* baseGetEffect({
157
+ repo,
158
+ silent,
159
+ verbose,
160
+ })
161
+ }
162
+ default:
163
+ return yield* Effect.fail(
164
+ new Error(
165
+ `Unknown subcommand '${subcommand}'. Available subcommands: set, get`,
166
+ ),
167
+ )
168
+ }
169
+ })
170
+
171
+ export const help = `
172
+ Usage: agency base <subcommand> [options]
173
+
174
+ Get or set the base branch for the current feature branch.
175
+
176
+ Subcommands:
177
+ set <branch> Set the base branch for the current feature branch
178
+ get Get the configured base branch
179
+
180
+ Options:
181
+ --repo Use repository-level default instead of branch-specific
182
+ -h, --help Show this help message
183
+ -s, --silent Suppress output messages
184
+ -v, --verbose Show verbose output
185
+
186
+ Examples:
187
+ agency base set origin/main # Set base branch for current branch
188
+ agency base set --repo origin/main # Set repository-level default for all branches
189
+ agency base get # Get branch-specific base branch
190
+ agency base get --repo # Get repository-level default base branch
191
+
192
+ Notes:
193
+ - The base branch must exist in the repository
194
+ - Branch-specific base branch is saved in agency.json (committed with the branch)
195
+ - Repository-level default is saved in .git/config (not committed)
196
+ - Branch-specific settings take precedence over repository-level defaults
197
+ - Base branch configuration is used by 'agency emit' when creating emit branches
198
+ `