@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.
- package/LICENSE +21 -0
- package/README.md +109 -0
- package/cli.ts +569 -0
- package/index.ts +1 -0
- package/package.json +65 -0
- package/src/commands/base.test.ts +198 -0
- package/src/commands/base.ts +198 -0
- package/src/commands/clean.test.ts +299 -0
- package/src/commands/clean.ts +320 -0
- package/src/commands/emit.test.ts +412 -0
- package/src/commands/emit.ts +521 -0
- package/src/commands/emitted.test.ts +226 -0
- package/src/commands/emitted.ts +57 -0
- package/src/commands/init.test.ts +311 -0
- package/src/commands/init.ts +140 -0
- package/src/commands/merge.test.ts +365 -0
- package/src/commands/merge.ts +253 -0
- package/src/commands/pull.test.ts +385 -0
- package/src/commands/pull.ts +205 -0
- package/src/commands/push.test.ts +394 -0
- package/src/commands/push.ts +346 -0
- package/src/commands/save.test.ts +247 -0
- package/src/commands/save.ts +162 -0
- package/src/commands/source.test.ts +195 -0
- package/src/commands/source.ts +72 -0
- package/src/commands/status.test.ts +489 -0
- package/src/commands/status.ts +258 -0
- package/src/commands/switch.test.ts +194 -0
- package/src/commands/switch.ts +84 -0
- package/src/commands/task-branching.test.ts +334 -0
- package/src/commands/task-edit.test.ts +141 -0
- package/src/commands/task-main.test.ts +872 -0
- package/src/commands/task.ts +712 -0
- package/src/commands/tasks.test.ts +335 -0
- package/src/commands/tasks.ts +155 -0
- package/src/commands/template-delete.test.ts +178 -0
- package/src/commands/template-delete.ts +98 -0
- package/src/commands/template-list.test.ts +135 -0
- package/src/commands/template-list.ts +87 -0
- package/src/commands/template-view.test.ts +158 -0
- package/src/commands/template-view.ts +86 -0
- package/src/commands/template.test.ts +32 -0
- package/src/commands/template.ts +96 -0
- package/src/commands/use.test.ts +87 -0
- package/src/commands/use.ts +97 -0
- package/src/commands/work.test.ts +462 -0
- package/src/commands/work.ts +193 -0
- package/src/errors.ts +17 -0
- package/src/schemas.ts +33 -0
- package/src/services/AgencyMetadataService.ts +287 -0
- package/src/services/ClaudeService.test.ts +184 -0
- package/src/services/ClaudeService.ts +91 -0
- package/src/services/ConfigService.ts +115 -0
- package/src/services/FileSystemService.ts +222 -0
- package/src/services/GitService.ts +751 -0
- package/src/services/OpencodeService.ts +263 -0
- package/src/services/PromptService.ts +183 -0
- package/src/services/TemplateService.ts +75 -0
- package/src/test-utils.ts +362 -0
- package/src/types/native-exec.d.ts +8 -0
- package/src/types.ts +216 -0
- package/src/utils/colors.ts +178 -0
- package/src/utils/command.ts +17 -0
- package/src/utils/effect.ts +281 -0
- package/src/utils/exec.ts +48 -0
- package/src/utils/paths.ts +51 -0
- package/src/utils/pr-branch.test.ts +372 -0
- package/src/utils/pr-branch.ts +473 -0
- package/src/utils/process.ts +110 -0
- package/src/utils/spinner.ts +82 -0
- package/templates/AGENCY.md +20 -0
- package/templates/AGENTS.md +11 -0
- package/templates/CLAUDE.md +3 -0
- package/templates/TASK.md +5 -0
- package/templates/opencode.json +4 -0
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import { resolve } from "path"
|
|
2
|
+
import { rm } from "node:fs/promises"
|
|
3
|
+
import { Effect } from "effect"
|
|
4
|
+
import type { BaseCommandOptions } from "../utils/command"
|
|
5
|
+
import { TemplateService } from "../services/TemplateService"
|
|
6
|
+
import { FileSystemService } from "../services/FileSystemService"
|
|
7
|
+
import { RepositoryNotInitializedError } from "../errors"
|
|
8
|
+
import highlight, { done } from "../utils/colors"
|
|
9
|
+
import { createLoggers, ensureGitRepo, getTemplateName } from "../utils/effect"
|
|
10
|
+
|
|
11
|
+
interface DeleteOptions extends BaseCommandOptions {
|
|
12
|
+
files?: string[]
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export const templateDelete = (options: DeleteOptions = {}) =>
|
|
16
|
+
Effect.gen(function* () {
|
|
17
|
+
const { files: filesToDelete = [] } = options
|
|
18
|
+
const { log, verboseLog } = createLoggers(options)
|
|
19
|
+
|
|
20
|
+
const templateService = yield* TemplateService
|
|
21
|
+
const fs = yield* FileSystemService
|
|
22
|
+
|
|
23
|
+
const gitRoot = yield* ensureGitRepo()
|
|
24
|
+
|
|
25
|
+
// Get template name from git config
|
|
26
|
+
const templateName = yield* getTemplateName(gitRoot)
|
|
27
|
+
if (!templateName) {
|
|
28
|
+
return yield* Effect.fail(new RepositoryNotInitializedError())
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
if (filesToDelete.length === 0) {
|
|
32
|
+
return yield* Effect.fail(
|
|
33
|
+
new Error(
|
|
34
|
+
"No files specified. Usage: agency template delete <file> [file ...]",
|
|
35
|
+
),
|
|
36
|
+
)
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
verboseLog(`Deleting from template: ${highlight.template(templateName)}`)
|
|
40
|
+
|
|
41
|
+
// Get template directory
|
|
42
|
+
const templateDir = yield* templateService.getTemplateDir(templateName)
|
|
43
|
+
|
|
44
|
+
// Delete each file
|
|
45
|
+
for (const filePath of filesToDelete) {
|
|
46
|
+
const templateFilePath = resolve(templateDir, filePath)
|
|
47
|
+
|
|
48
|
+
// Check if file exists
|
|
49
|
+
const exists = yield* fs.exists(templateFilePath)
|
|
50
|
+
if (!exists) {
|
|
51
|
+
verboseLog(`Skipping ${filePath} (does not exist in template)`)
|
|
52
|
+
continue
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// Delete the file/directory
|
|
56
|
+
yield* Effect.tryPromise({
|
|
57
|
+
try: () => rm(templateFilePath, { recursive: true, force: true }),
|
|
58
|
+
catch: (error) => new Error(`Failed to delete ${filePath}: ${error}`),
|
|
59
|
+
})
|
|
60
|
+
|
|
61
|
+
log(
|
|
62
|
+
done(
|
|
63
|
+
`Deleted ${highlight.file(filePath)} from ${highlight.template(templateName)} template`,
|
|
64
|
+
),
|
|
65
|
+
)
|
|
66
|
+
}
|
|
67
|
+
})
|
|
68
|
+
|
|
69
|
+
// Help text for reference (not exported as it's handled by template command)
|
|
70
|
+
const help = `
|
|
71
|
+
Usage: agency template delete <file> [file ...] [options]
|
|
72
|
+
|
|
73
|
+
Delete specified files from the configured template directory.
|
|
74
|
+
|
|
75
|
+
This command removes files from the template directory configured in
|
|
76
|
+
.git/config (agency.template).
|
|
77
|
+
|
|
78
|
+
Arguments:
|
|
79
|
+
<file> File path to delete (relative to template root)
|
|
80
|
+
[file ...] Additional files to delete
|
|
81
|
+
|
|
82
|
+
Options:
|
|
83
|
+
-h, --help Show this help message
|
|
84
|
+
-s, --silent Suppress output messages
|
|
85
|
+
-v, --verbose Show verbose output
|
|
86
|
+
|
|
87
|
+
Examples:
|
|
88
|
+
agency template delete AGENTS.md # Delete specific file
|
|
89
|
+
agency template delete docs/ src/ # Delete directories
|
|
90
|
+
agency template delete file1 file2 file3 # Delete multiple files
|
|
91
|
+
|
|
92
|
+
Notes:
|
|
93
|
+
- Requires agency.template to be set (run 'agency init' first)
|
|
94
|
+
- At least one file must be specified
|
|
95
|
+
- Files are deleted from ~/.config/agency/templates/{template-name}/
|
|
96
|
+
- Non-existent files are skipped with a warning in verbose mode
|
|
97
|
+
- Directories are deleted recursively
|
|
98
|
+
`
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
import { describe, expect, test, beforeEach, afterEach } from "bun:test"
|
|
2
|
+
import { templateList } from "./template-list"
|
|
3
|
+
import { mkdir, writeFile, rm } from "node:fs/promises"
|
|
4
|
+
import { join } from "node:path"
|
|
5
|
+
import { tmpdir } from "node:os"
|
|
6
|
+
import { runTestEffect } from "../test-utils"
|
|
7
|
+
|
|
8
|
+
describe("template list command", () => {
|
|
9
|
+
let testDir: string
|
|
10
|
+
let gitRoot: string
|
|
11
|
+
let templateDir: string
|
|
12
|
+
|
|
13
|
+
beforeEach(async () => {
|
|
14
|
+
// Create temporary test directory
|
|
15
|
+
testDir = join(tmpdir(), `agency-test-list-${Date.now()}`)
|
|
16
|
+
await mkdir(testDir, { recursive: true })
|
|
17
|
+
gitRoot = join(testDir, "repo")
|
|
18
|
+
await mkdir(gitRoot, { recursive: true })
|
|
19
|
+
|
|
20
|
+
// Initialize git repo
|
|
21
|
+
await Bun.spawn(["git", "init"], {
|
|
22
|
+
cwd: gitRoot,
|
|
23
|
+
stdout: "ignore",
|
|
24
|
+
stderr: "ignore",
|
|
25
|
+
}).exited
|
|
26
|
+
|
|
27
|
+
// Set up config dir
|
|
28
|
+
const configDir = join(testDir, "config")
|
|
29
|
+
templateDir = join(configDir, "templates", "test-template")
|
|
30
|
+
await mkdir(templateDir, { recursive: true })
|
|
31
|
+
|
|
32
|
+
// Set environment variable for config
|
|
33
|
+
process.env.AGENCY_CONFIG_DIR = configDir
|
|
34
|
+
|
|
35
|
+
// Set git config
|
|
36
|
+
await Bun.spawn(["git", "config", "agency.template", "test-template"], {
|
|
37
|
+
cwd: gitRoot,
|
|
38
|
+
stdout: "ignore",
|
|
39
|
+
stderr: "ignore",
|
|
40
|
+
}).exited
|
|
41
|
+
})
|
|
42
|
+
|
|
43
|
+
afterEach(async () => {
|
|
44
|
+
// Clean up
|
|
45
|
+
await rm(testDir, { recursive: true, force: true })
|
|
46
|
+
delete process.env.AGENCY_CONFIG_DIR
|
|
47
|
+
})
|
|
48
|
+
|
|
49
|
+
test("lists files in template directory", async () => {
|
|
50
|
+
// Create test files in template
|
|
51
|
+
await writeFile(join(templateDir, "AGENTS.md"), "# Agents")
|
|
52
|
+
await writeFile(join(templateDir, "opencode.json"), "{}")
|
|
53
|
+
await mkdir(join(templateDir, "docs"), { recursive: true })
|
|
54
|
+
await writeFile(join(templateDir, "docs", "README.md"), "# Docs")
|
|
55
|
+
|
|
56
|
+
// Capture output
|
|
57
|
+
const output: string[] = []
|
|
58
|
+
const originalLog = console.log
|
|
59
|
+
console.log = (...args: any[]) => {
|
|
60
|
+
output.push(args.join(" "))
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const originalCwd = process.cwd()
|
|
64
|
+
process.chdir(gitRoot)
|
|
65
|
+
|
|
66
|
+
try {
|
|
67
|
+
await runTestEffect(templateList({ silent: false }))
|
|
68
|
+
} finally {
|
|
69
|
+
process.chdir(originalCwd)
|
|
70
|
+
console.log = originalLog
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
expect(output.length).toBeGreaterThan(0)
|
|
74
|
+
expect(output.some((line) => line.includes("AGENTS.md"))).toBe(true)
|
|
75
|
+
expect(output.some((line) => line.includes("opencode.json"))).toBe(true)
|
|
76
|
+
expect(output.some((line) => line.includes("docs/README.md"))).toBe(true)
|
|
77
|
+
})
|
|
78
|
+
|
|
79
|
+
test("handles empty template directory", async () => {
|
|
80
|
+
// Capture output
|
|
81
|
+
const output: string[] = []
|
|
82
|
+
const originalLog = console.log
|
|
83
|
+
console.log = (...args: any[]) => {
|
|
84
|
+
output.push(args.join(" "))
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const originalCwd = process.cwd()
|
|
88
|
+
process.chdir(gitRoot)
|
|
89
|
+
|
|
90
|
+
try {
|
|
91
|
+
await runTestEffect(templateList({ silent: false }))
|
|
92
|
+
} finally {
|
|
93
|
+
process.chdir(originalCwd)
|
|
94
|
+
console.log = originalLog
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
expect(output.some((line) => line.includes("has no files"))).toBe(true)
|
|
98
|
+
})
|
|
99
|
+
|
|
100
|
+
test("throws error when not in git repository", async () => {
|
|
101
|
+
const nonGitDir = join(testDir, "non-git")
|
|
102
|
+
await mkdir(nonGitDir, { recursive: true })
|
|
103
|
+
|
|
104
|
+
const originalCwd = process.cwd()
|
|
105
|
+
process.chdir(nonGitDir)
|
|
106
|
+
|
|
107
|
+
try {
|
|
108
|
+
await expect(
|
|
109
|
+
runTestEffect(templateList({ silent: true })),
|
|
110
|
+
).rejects.toThrow("Not in a git repository")
|
|
111
|
+
} finally {
|
|
112
|
+
process.chdir(originalCwd)
|
|
113
|
+
}
|
|
114
|
+
})
|
|
115
|
+
|
|
116
|
+
test("throws error when template not configured", async () => {
|
|
117
|
+
// Remove git config
|
|
118
|
+
await Bun.spawn(["git", "config", "--unset", "agency.template"], {
|
|
119
|
+
cwd: gitRoot,
|
|
120
|
+
stdout: "ignore",
|
|
121
|
+
stderr: "ignore",
|
|
122
|
+
}).exited
|
|
123
|
+
|
|
124
|
+
const originalCwd = process.cwd()
|
|
125
|
+
process.chdir(gitRoot)
|
|
126
|
+
|
|
127
|
+
try {
|
|
128
|
+
await expect(
|
|
129
|
+
runTestEffect(templateList({ silent: true })),
|
|
130
|
+
).rejects.toThrow("Repository not initialized")
|
|
131
|
+
} finally {
|
|
132
|
+
process.chdir(originalCwd)
|
|
133
|
+
}
|
|
134
|
+
})
|
|
135
|
+
})
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import { Effect } from "effect"
|
|
2
|
+
import type { BaseCommandOptions } from "../utils/command"
|
|
3
|
+
import { TemplateService } from "../services/TemplateService"
|
|
4
|
+
import { FileSystemService } from "../services/FileSystemService"
|
|
5
|
+
import { RepositoryNotInitializedError } from "../errors"
|
|
6
|
+
import highlight from "../utils/colors"
|
|
7
|
+
import { createLoggers, ensureGitRepo, getTemplateName } from "../utils/effect"
|
|
8
|
+
|
|
9
|
+
interface ListOptions extends BaseCommandOptions {}
|
|
10
|
+
|
|
11
|
+
export const templateList = (options: ListOptions = {}) =>
|
|
12
|
+
Effect.gen(function* () {
|
|
13
|
+
const { log, verboseLog } = createLoggers(options)
|
|
14
|
+
|
|
15
|
+
const templateService = yield* TemplateService
|
|
16
|
+
const fs = yield* FileSystemService
|
|
17
|
+
|
|
18
|
+
const gitRoot = yield* ensureGitRepo()
|
|
19
|
+
|
|
20
|
+
// Get template name from git config
|
|
21
|
+
const templateName = yield* getTemplateName(gitRoot)
|
|
22
|
+
if (!templateName) {
|
|
23
|
+
return yield* Effect.fail(new RepositoryNotInitializedError())
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
verboseLog(`Listing files in template: ${highlight.template(templateName)}`)
|
|
27
|
+
|
|
28
|
+
// Get template directory
|
|
29
|
+
const templateDir = yield* templateService.getTemplateDir(templateName)
|
|
30
|
+
|
|
31
|
+
// Check if template directory exists and is a directory
|
|
32
|
+
const isDirectory = yield* Effect.tryPromise({
|
|
33
|
+
try: async () => {
|
|
34
|
+
const file = Bun.file(templateDir)
|
|
35
|
+
const stat = await file.stat()
|
|
36
|
+
return stat?.isDirectory?.() ?? false
|
|
37
|
+
},
|
|
38
|
+
catch: (error) =>
|
|
39
|
+
new Error(`Failed to check template directory: ${error}`),
|
|
40
|
+
})
|
|
41
|
+
if (!isDirectory) {
|
|
42
|
+
return yield* Effect.fail(
|
|
43
|
+
new Error(
|
|
44
|
+
`Template directory does not exist: ${highlight.template(templateName)}`,
|
|
45
|
+
),
|
|
46
|
+
)
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// Collect all files recursively
|
|
50
|
+
const files = yield* fs.collectFiles(templateDir, {
|
|
51
|
+
exclude: [".gitkeep"],
|
|
52
|
+
sort: true,
|
|
53
|
+
})
|
|
54
|
+
|
|
55
|
+
if (files.length === 0) {
|
|
56
|
+
log(`Template ${highlight.template(templateName)} has no files`)
|
|
57
|
+
return
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
for (const file of files) {
|
|
61
|
+
log(highlight.file(file))
|
|
62
|
+
}
|
|
63
|
+
})
|
|
64
|
+
|
|
65
|
+
// Help text for reference (not exported as it's handled by template command)
|
|
66
|
+
const help = `
|
|
67
|
+
Usage: agency template list [options]
|
|
68
|
+
|
|
69
|
+
List all files in the configured template directory.
|
|
70
|
+
|
|
71
|
+
This command displays all files stored in the template configured for the
|
|
72
|
+
current git repository.
|
|
73
|
+
|
|
74
|
+
Options:
|
|
75
|
+
-h, --help Show this help message
|
|
76
|
+
-s, --silent Suppress output messages
|
|
77
|
+
-v, --verbose Show verbose output
|
|
78
|
+
|
|
79
|
+
Example:
|
|
80
|
+
agency template list # List files in current template
|
|
81
|
+
|
|
82
|
+
Notes:
|
|
83
|
+
- Requires agency.template to be set (run 'agency init' first)
|
|
84
|
+
- Shows files relative to template root directory
|
|
85
|
+
- Files are listed in alphabetical order
|
|
86
|
+
- Template directory must exist (created when you save files)
|
|
87
|
+
`
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
import { describe, expect, test, beforeEach, afterEach } from "bun:test"
|
|
2
|
+
import { templateView } from "./template-view"
|
|
3
|
+
import { mkdir, writeFile, rm } from "node:fs/promises"
|
|
4
|
+
import { join } from "node:path"
|
|
5
|
+
import { tmpdir } from "node:os"
|
|
6
|
+
import { runTestEffect } from "../test-utils"
|
|
7
|
+
|
|
8
|
+
describe("template view command", () => {
|
|
9
|
+
let testDir: string
|
|
10
|
+
let gitRoot: string
|
|
11
|
+
let templateDir: string
|
|
12
|
+
|
|
13
|
+
beforeEach(async () => {
|
|
14
|
+
// Create temporary test directory
|
|
15
|
+
testDir = join(tmpdir(), `agency-test-view-${Date.now()}`)
|
|
16
|
+
await mkdir(testDir, { recursive: true })
|
|
17
|
+
gitRoot = join(testDir, "repo")
|
|
18
|
+
await mkdir(gitRoot, { recursive: true })
|
|
19
|
+
|
|
20
|
+
// Initialize git repo
|
|
21
|
+
await Bun.spawn(["git", "init"], {
|
|
22
|
+
cwd: gitRoot,
|
|
23
|
+
stdout: "ignore",
|
|
24
|
+
stderr: "ignore",
|
|
25
|
+
}).exited
|
|
26
|
+
|
|
27
|
+
// Set up config dir
|
|
28
|
+
const configDir = join(testDir, "config")
|
|
29
|
+
templateDir = join(configDir, "templates", "test-template")
|
|
30
|
+
await mkdir(templateDir, { recursive: true })
|
|
31
|
+
|
|
32
|
+
// Set environment variable for config
|
|
33
|
+
process.env.AGENCY_CONFIG_DIR = configDir
|
|
34
|
+
|
|
35
|
+
// Set git config
|
|
36
|
+
await Bun.spawn(["git", "config", "agency.template", "test-template"], {
|
|
37
|
+
cwd: gitRoot,
|
|
38
|
+
stdout: "ignore",
|
|
39
|
+
stderr: "ignore",
|
|
40
|
+
}).exited
|
|
41
|
+
})
|
|
42
|
+
|
|
43
|
+
afterEach(async () => {
|
|
44
|
+
// Clean up
|
|
45
|
+
await rm(testDir, { recursive: true, force: true })
|
|
46
|
+
delete process.env.AGENCY_CONFIG_DIR
|
|
47
|
+
})
|
|
48
|
+
|
|
49
|
+
test("views file from template directory", async () => {
|
|
50
|
+
// Create test file in template
|
|
51
|
+
const content = "# Test File\n\nThis is a test."
|
|
52
|
+
const testFile = join(templateDir, "test.md")
|
|
53
|
+
await writeFile(testFile, content)
|
|
54
|
+
|
|
55
|
+
const originalCwd = process.cwd()
|
|
56
|
+
process.chdir(gitRoot)
|
|
57
|
+
|
|
58
|
+
try {
|
|
59
|
+
// Just verify the command doesn't throw - output goes to stdout
|
|
60
|
+
await runTestEffect(templateView({ file: "test.md", silent: true }))
|
|
61
|
+
|
|
62
|
+
// Verify the file exists and has expected content
|
|
63
|
+
const fileContent = await Bun.file(testFile).text()
|
|
64
|
+
expect(fileContent).toContain("# Test File")
|
|
65
|
+
expect(fileContent).toContain("This is a test.")
|
|
66
|
+
} finally {
|
|
67
|
+
process.chdir(originalCwd)
|
|
68
|
+
}
|
|
69
|
+
})
|
|
70
|
+
|
|
71
|
+
test("views file in subdirectory", async () => {
|
|
72
|
+
// Create test file in subdirectory
|
|
73
|
+
const docsDir = join(templateDir, "docs")
|
|
74
|
+
await mkdir(docsDir, { recursive: true })
|
|
75
|
+
const content = "# Documentation\n\nDocs content."
|
|
76
|
+
const testFile = join(docsDir, "README.md")
|
|
77
|
+
await writeFile(testFile, content)
|
|
78
|
+
|
|
79
|
+
const originalCwd = process.cwd()
|
|
80
|
+
process.chdir(gitRoot)
|
|
81
|
+
|
|
82
|
+
try {
|
|
83
|
+
// Just verify the command doesn't throw - output goes to stdout
|
|
84
|
+
await runTestEffect(
|
|
85
|
+
templateView({ file: "docs/README.md", silent: true }),
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
// Verify the file exists and has expected content
|
|
89
|
+
const fileContent = await Bun.file(testFile).text()
|
|
90
|
+
expect(fileContent).toContain("# Documentation")
|
|
91
|
+
expect(fileContent).toContain("Docs content.")
|
|
92
|
+
} finally {
|
|
93
|
+
process.chdir(originalCwd)
|
|
94
|
+
}
|
|
95
|
+
})
|
|
96
|
+
|
|
97
|
+
test("throws error when file does not exist", async () => {
|
|
98
|
+
const originalCwd = process.cwd()
|
|
99
|
+
process.chdir(gitRoot)
|
|
100
|
+
|
|
101
|
+
try {
|
|
102
|
+
await expect(
|
|
103
|
+
runTestEffect(templateView({ file: "nonexistent.md", silent: true })),
|
|
104
|
+
).rejects.toThrow("does not exist in template")
|
|
105
|
+
} finally {
|
|
106
|
+
process.chdir(originalCwd)
|
|
107
|
+
}
|
|
108
|
+
})
|
|
109
|
+
|
|
110
|
+
test("throws error when no file specified", async () => {
|
|
111
|
+
const originalCwd = process.cwd()
|
|
112
|
+
process.chdir(gitRoot)
|
|
113
|
+
|
|
114
|
+
try {
|
|
115
|
+
await expect(
|
|
116
|
+
runTestEffect(templateView({ silent: true })),
|
|
117
|
+
).rejects.toThrow("File path is required")
|
|
118
|
+
} finally {
|
|
119
|
+
process.chdir(originalCwd)
|
|
120
|
+
}
|
|
121
|
+
})
|
|
122
|
+
|
|
123
|
+
test("throws error when not in git repository", async () => {
|
|
124
|
+
const nonGitDir = join(testDir, "non-git")
|
|
125
|
+
await mkdir(nonGitDir, { recursive: true })
|
|
126
|
+
|
|
127
|
+
const originalCwd = process.cwd()
|
|
128
|
+
process.chdir(nonGitDir)
|
|
129
|
+
|
|
130
|
+
try {
|
|
131
|
+
await expect(
|
|
132
|
+
runTestEffect(templateView({ file: "test.md", silent: true })),
|
|
133
|
+
).rejects.toThrow("Not in a git repository")
|
|
134
|
+
} finally {
|
|
135
|
+
process.chdir(originalCwd)
|
|
136
|
+
}
|
|
137
|
+
})
|
|
138
|
+
|
|
139
|
+
test("throws error when template not configured", async () => {
|
|
140
|
+
// Remove git config
|
|
141
|
+
await Bun.spawn(["git", "config", "--unset", "agency.template"], {
|
|
142
|
+
cwd: gitRoot,
|
|
143
|
+
stdout: "ignore",
|
|
144
|
+
stderr: "ignore",
|
|
145
|
+
}).exited
|
|
146
|
+
|
|
147
|
+
const originalCwd = process.cwd()
|
|
148
|
+
process.chdir(gitRoot)
|
|
149
|
+
|
|
150
|
+
try {
|
|
151
|
+
await expect(
|
|
152
|
+
runTestEffect(templateView({ file: "test.md", silent: true })),
|
|
153
|
+
).rejects.toThrow("Repository not initialized")
|
|
154
|
+
} finally {
|
|
155
|
+
process.chdir(originalCwd)
|
|
156
|
+
}
|
|
157
|
+
})
|
|
158
|
+
})
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import { resolve } from "path"
|
|
2
|
+
import { Effect } from "effect"
|
|
3
|
+
import type { BaseCommandOptions } from "../utils/command"
|
|
4
|
+
import { TemplateService } from "../services/TemplateService"
|
|
5
|
+
import { FileSystemService } from "../services/FileSystemService"
|
|
6
|
+
import { RepositoryNotInitializedError } from "../errors"
|
|
7
|
+
import highlight from "../utils/colors"
|
|
8
|
+
import { createLoggers, ensureGitRepo, getTemplateName } from "../utils/effect"
|
|
9
|
+
|
|
10
|
+
interface ViewOptions extends BaseCommandOptions {
|
|
11
|
+
file?: string
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export const templateView = (options: ViewOptions = {}) =>
|
|
15
|
+
Effect.gen(function* () {
|
|
16
|
+
const { file: fileToView, silent = false } = options
|
|
17
|
+
const { verboseLog } = createLoggers(options)
|
|
18
|
+
|
|
19
|
+
const templateService = yield* TemplateService
|
|
20
|
+
const fs = yield* FileSystemService
|
|
21
|
+
|
|
22
|
+
const gitRoot = yield* ensureGitRepo()
|
|
23
|
+
|
|
24
|
+
// Get template name from git config
|
|
25
|
+
const templateName = yield* getTemplateName(gitRoot)
|
|
26
|
+
if (!templateName) {
|
|
27
|
+
return yield* Effect.fail(new RepositoryNotInitializedError())
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
if (!fileToView) {
|
|
31
|
+
return yield* Effect.fail(
|
|
32
|
+
new Error("File path is required. Usage: agency template view <file>"),
|
|
33
|
+
)
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
verboseLog(
|
|
37
|
+
`Viewing ${highlight.file(fileToView)} from ${highlight.template(templateName)} template`,
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
// Get template directory
|
|
41
|
+
const templateDir = yield* templateService.getTemplateDir(templateName)
|
|
42
|
+
const templateFilePath = resolve(templateDir, fileToView)
|
|
43
|
+
|
|
44
|
+
// Check if file exists
|
|
45
|
+
const exists = yield* fs.exists(templateFilePath)
|
|
46
|
+
if (!exists) {
|
|
47
|
+
return yield* Effect.fail(
|
|
48
|
+
new Error(
|
|
49
|
+
`File ${highlight.file(fileToView)} does not exist in template ${highlight.template(templateName)}`,
|
|
50
|
+
),
|
|
51
|
+
)
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// Read and display the file
|
|
55
|
+
const content = yield* fs.readFile(templateFilePath)
|
|
56
|
+
if (!silent) {
|
|
57
|
+
console.log(content)
|
|
58
|
+
}
|
|
59
|
+
})
|
|
60
|
+
|
|
61
|
+
// Help text for reference (not exported as it's handled by template command)
|
|
62
|
+
const help = `
|
|
63
|
+
Usage: agency template view <file> [options]
|
|
64
|
+
|
|
65
|
+
View the contents of a file in the configured template directory.
|
|
66
|
+
|
|
67
|
+
This command displays the contents of a file stored in the template
|
|
68
|
+
configured for the current git repository.
|
|
69
|
+
|
|
70
|
+
Arguments:
|
|
71
|
+
<file> File path to view (relative to template root)
|
|
72
|
+
|
|
73
|
+
Options:
|
|
74
|
+
-h, --help Show this help message
|
|
75
|
+
-s, --silent Suppress verbose messages (file content still shown)
|
|
76
|
+
-v, --verbose Show verbose output
|
|
77
|
+
|
|
78
|
+
Examples:
|
|
79
|
+
agency template view AGENTS.md # View AGENTS.md from template
|
|
80
|
+
agency template view docs/README.md # View file in subdirectory
|
|
81
|
+
|
|
82
|
+
Notes:
|
|
83
|
+
- Requires agency.template to be set (run 'agency init' first)
|
|
84
|
+
- File path is relative to template root directory
|
|
85
|
+
- File content is displayed directly to stdout
|
|
86
|
+
`
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test"
|
|
2
|
+
import { template } from "./template"
|
|
3
|
+
import { runTestEffect } from "../test-utils"
|
|
4
|
+
|
|
5
|
+
describe("template command", () => {
|
|
6
|
+
test("throws error when no subcommand provided", async () => {
|
|
7
|
+
await expect(
|
|
8
|
+
runTestEffect(
|
|
9
|
+
template({
|
|
10
|
+
args: [],
|
|
11
|
+
silent: true,
|
|
12
|
+
}),
|
|
13
|
+
),
|
|
14
|
+
).rejects.toThrow(
|
|
15
|
+
"Subcommand is required. Available subcommands: use, save, list, view, delete",
|
|
16
|
+
)
|
|
17
|
+
})
|
|
18
|
+
|
|
19
|
+
test("throws error for unknown subcommand", async () => {
|
|
20
|
+
await expect(
|
|
21
|
+
runTestEffect(
|
|
22
|
+
template({
|
|
23
|
+
subcommand: "invalid",
|
|
24
|
+
args: [],
|
|
25
|
+
silent: true,
|
|
26
|
+
}),
|
|
27
|
+
),
|
|
28
|
+
).rejects.toThrow(
|
|
29
|
+
"Unknown template subcommand 'invalid'. Available: use, save, list, view, delete",
|
|
30
|
+
)
|
|
31
|
+
})
|
|
32
|
+
})
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import { Effect } from "effect"
|
|
2
|
+
import { use } from "./use"
|
|
3
|
+
import { save } from "./save"
|
|
4
|
+
import { templateList } from "./template-list"
|
|
5
|
+
import { templateDelete } from "./template-delete"
|
|
6
|
+
import { templateView } from "./template-view"
|
|
7
|
+
|
|
8
|
+
export const help = `
|
|
9
|
+
Usage: agency template <subcommand> [options]
|
|
10
|
+
|
|
11
|
+
Subcommands:
|
|
12
|
+
use [template] Set template for this repository
|
|
13
|
+
save <file|dir> ... Save files/dirs to configured template
|
|
14
|
+
list List all files in configured template
|
|
15
|
+
view <file> View contents of a file in template
|
|
16
|
+
delete <file> ... Delete files from configured template
|
|
17
|
+
|
|
18
|
+
Examples:
|
|
19
|
+
agency template use # Interactively select template
|
|
20
|
+
agency template use work # Set template to 'work'
|
|
21
|
+
agency template save AGENTS.md # Save specific file to template
|
|
22
|
+
agency template list # List files in current template
|
|
23
|
+
agency template view AGENTS.md # View file from template
|
|
24
|
+
agency template delete AGENTS.md # Delete file from template
|
|
25
|
+
|
|
26
|
+
For more information about a subcommand, run:
|
|
27
|
+
agency template <subcommand> --help
|
|
28
|
+
`
|
|
29
|
+
|
|
30
|
+
export const template = (options: {
|
|
31
|
+
subcommand?: string
|
|
32
|
+
args: string[]
|
|
33
|
+
silent?: boolean
|
|
34
|
+
verbose?: boolean
|
|
35
|
+
template?: string
|
|
36
|
+
}) =>
|
|
37
|
+
Effect.gen(function* () {
|
|
38
|
+
const {
|
|
39
|
+
subcommand,
|
|
40
|
+
args,
|
|
41
|
+
silent,
|
|
42
|
+
verbose,
|
|
43
|
+
template: templateName,
|
|
44
|
+
} = options
|
|
45
|
+
|
|
46
|
+
if (!subcommand) {
|
|
47
|
+
return yield* Effect.fail(
|
|
48
|
+
new Error(
|
|
49
|
+
"Subcommand is required. Available subcommands: use, save, list, view, delete\n\nRun 'agency template --help' for usage information.",
|
|
50
|
+
),
|
|
51
|
+
)
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
switch (subcommand) {
|
|
55
|
+
case "use":
|
|
56
|
+
return yield* use({
|
|
57
|
+
template: args[0] || templateName,
|
|
58
|
+
silent,
|
|
59
|
+
verbose,
|
|
60
|
+
})
|
|
61
|
+
|
|
62
|
+
case "save":
|
|
63
|
+
return yield* save({
|
|
64
|
+
files: args,
|
|
65
|
+
silent,
|
|
66
|
+
verbose,
|
|
67
|
+
})
|
|
68
|
+
|
|
69
|
+
case "list":
|
|
70
|
+
return yield* templateList({
|
|
71
|
+
silent,
|
|
72
|
+
verbose,
|
|
73
|
+
})
|
|
74
|
+
|
|
75
|
+
case "view":
|
|
76
|
+
return yield* templateView({
|
|
77
|
+
file: args[0],
|
|
78
|
+
silent,
|
|
79
|
+
verbose,
|
|
80
|
+
})
|
|
81
|
+
|
|
82
|
+
case "delete":
|
|
83
|
+
return yield* templateDelete({
|
|
84
|
+
files: args,
|
|
85
|
+
silent,
|
|
86
|
+
verbose,
|
|
87
|
+
})
|
|
88
|
+
|
|
89
|
+
default:
|
|
90
|
+
return yield* Effect.fail(
|
|
91
|
+
new Error(
|
|
92
|
+
`Unknown template subcommand '${subcommand}'. Available: use, save, list, view, delete`,
|
|
93
|
+
),
|
|
94
|
+
)
|
|
95
|
+
}
|
|
96
|
+
})
|