Skip to content

Commit 9fdb523

Browse files
committed
* Fix up jxa script syntax errors
* Add jxa unittests, which are checked via Github Actions CI * Enable CI checks for all golang unit tests for consistency Co-authored by: Claude Code v2.0.24 + Claude Sonnet 4.5 with the prompts: ``` Debug why the scripts under @scripts/ crash with errors such as `Failed to list tasks: failed to execute list_tasks.jxa: exit status 1 - /opt/homebrew/share/mcp-omnifocus/list_tasks.jxa:13:14: script error: Expected end of line, etc. but found “(”. (-2741)` ``` ``` Add a github actions check to verify the syntax of all jxa scripts during CI ```
1 parent da6be33 commit 9fdb523

File tree

8 files changed

+104
-18
lines changed

8 files changed

+104
-18
lines changed

.github/workflows/build.yml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,3 +40,9 @@ jobs:
4040
exit 1
4141
fi
4242
done
43+
44+
- name: Validate JXA script syntax
45+
run: make validate-jxa
46+
47+
- name: Run Go tests
48+
run: go test ./... -v

CLAUDE.md

Lines changed: 20 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -66,14 +66,21 @@ scripts/ # JXA automation scripts
6666
- `./bin/mcp-omnifocus` - Run the compiled binary
6767
- `./bin/mcp-omnifocus -scripts /path/to/scripts` - Run with explicit scripts path
6868

69+
### Testing
70+
- `make test` - Run all tests (includes Go tests and JXA validation)
71+
- `make validate-jxa` - Validate JXA script syntax only
72+
- `go test ./... -v` - Run Go tests only
73+
6974
### Testing JXA Scripts
7075
Scripts can be tested independently:
7176
```bash
72-
osascript scripts/list_projects.jxa
73-
osascript scripts/list_tasks.jxa
74-
osascript scripts/create_task.jxa '{"name":"Test Task"}'
77+
osascript -l JavaScript scripts/list_projects.jxa
78+
osascript -l JavaScript scripts/list_tasks.jxa
79+
osascript -l JavaScript scripts/create_task.jxa '{"name":"Test Task"}'
7580
```
7681

82+
**Important:** Always use the `-l JavaScript` flag when testing JXA scripts with `osascript` to ensure proper syntax parsing.
83+
7784
## MCP Tools
7885

7986
All tools are registered in `cmd/mcp-omnifocus/main.go` in the `registerTools()` function.
@@ -194,6 +201,16 @@ To test the MCP server locally:
194201
3. Check that JXA scripts have execute permissions (`chmod +x scripts/*.jxa`)
195202
4. Ensure OmniFocus is running and has projects/tasks to query
196203

204+
## Continuous Integration
205+
206+
The project uses GitHub Actions for CI/CD. The build workflow (`.github/workflows/build.yml`) runs on every push and pull request to `main`:
207+
208+
1. **Build** - Compiles the Go binary
209+
2. **Validate JXA Syntax** - Uses `osacompile` to check all JXA scripts for syntax errors without executing them
210+
3. **Run Tests** - Executes all Go tests
211+
212+
The JXA syntax validation ensures that script syntax errors are caught early, before they cause runtime failures. This is particularly important because the `-l JavaScript` flag must be used when calling `osascript` for proper syntax parsing.
213+
197214
## Release Process
198215

199216
Releases are automated using GitHub Actions and GoReleaser v2.

Makefile

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
.PHONY: build clean install test run
1+
.PHONY: build clean install test run validate-jxa
22

33
# Build the MCP server
44
build:
@@ -24,3 +24,29 @@ run:
2424
# Build for release
2525
release:
2626
CGO_ENABLED=0 go build -ldflags="-s -w" -o bin/mcp-omnifocus ./cmd/mcp-omnifocus
27+
28+
# Validate JXA script syntax
29+
validate-jxa:
30+
@echo "Validating JXA script syntax..."
31+
@FAILED=0; \
32+
for script in scripts/*.jxa; do \
33+
echo "Checking syntax: $$script"; \
34+
if osacompile -l JavaScript -o /tmp/test.scpt "$$script" 2>&1; then \
35+
echo "$$script: syntax OK"; \
36+
rm -f /tmp/test.scpt; \
37+
else \
38+
echo "$$script: syntax error detected"; \
39+
FAILED=1; \
40+
fi; \
41+
done; \
42+
if [ $$FAILED -eq 1 ]; then \
43+
echo ""; \
44+
echo "JXA syntax validation failed"; \
45+
exit 1; \
46+
fi; \
47+
echo ""; \
48+
echo "All JXA scripts passed syntax validation"
49+
50+
# Run all tests
51+
test: validate-jxa
52+
go test ./...

internal/omnifocus/client.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -182,7 +182,8 @@ func isValidScriptsDir(dir string) bool {
182182
func (c *Client) executeJXA(scriptName string, args ...string) ([]byte, error) {
183183
scriptPath := filepath.Join(c.scriptsDir, scriptName)
184184

185-
cmdArgs := append([]string{scriptPath}, args...)
185+
// Build command arguments: -l JavaScript, script path, then script arguments
186+
cmdArgs := append([]string{"-l", "JavaScript", scriptPath}, args...)
186187
cmd := exec.Command("osascript", cmdArgs...)
187188

188189
output, err := cmd.CombinedOutput()

scripts/complete_task.jxa

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,12 +11,18 @@ function run(argv) {
1111
const doc = app.defaultDocument;
1212
const taskId = argv[0];
1313

14-
const tasks = doc.flattenedTasks.whose({id: taskId});
15-
if (tasks.length === 0) {
14+
// Find task by ID
15+
const allTasks = doc.flattenedTasks();
16+
let task = null;
17+
for (let i = 0; i < allTasks.length; i++) {
18+
if (allTasks[i].id() === taskId) {
19+
task = allTasks[i];
20+
break;
21+
}
22+
}
23+
if (!task) {
1624
return JSON.stringify({error: 'Task not found'});
1725
}
18-
19-
const task = tasks[0];
2026
task.markComplete();
2127

2228
return JSON.stringify({

scripts/create_task.jxa

Lines changed: 20 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,15 @@ function run(argv) {
1515

1616
if (taskData.projectId) {
1717
// Add to specific project
18-
const project = doc.flattenedProjects.whose({id: taskData.projectId})[0];
18+
// Find project by ID
19+
const allProjects = doc.flattenedProjects();
20+
let project = null;
21+
for (let i = 0; i < allProjects.length; i++) {
22+
if (allProjects[i].id() === taskData.projectId) {
23+
project = allProjects[i];
24+
break;
25+
}
26+
}
1927
if (!project) {
2028
return JSON.stringify({error: 'Project not found'});
2129
}
@@ -47,9 +55,17 @@ function run(argv) {
4755
// Add tags
4856
if (taskData.tags && taskData.tags.length > 0) {
4957
taskData.tags.forEach(tagName => {
50-
const tags = doc.flattenedTags.whose({name: tagName});
51-
if (tags.length > 0) {
52-
task.addTag(tags[0]);
58+
// Find tag by name
59+
const allTags = doc.flattenedTags();
60+
let foundTag = null;
61+
for (let i = 0; i < allTags.length; i++) {
62+
if (allTags[i].name() === tagName) {
63+
foundTag = allTags[i];
64+
break;
65+
}
66+
}
67+
if (foundTag) {
68+
task.addTag(foundTag);
5369
} else {
5470
// Create tag if it doesn't exist
5571
const newTag = doc.Tag({name: tagName});

scripts/list_tasks.jxa

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,15 @@ function run(argv) {
99

1010
let tasks;
1111
if (projectId) {
12-
const project = doc.flattenedProjects.whose({id: projectId})[0];
12+
// Find project by ID
13+
const allProjects = doc.flattenedProjects();
14+
let project = null;
15+
for (let i = 0; i < allProjects.length; i++) {
16+
if (allProjects[i].id() === projectId) {
17+
project = allProjects[i];
18+
break;
19+
}
20+
}
1321
if (!project) {
1422
return JSON.stringify({error: 'Project not found'});
1523
}

scripts/update_task.jxa

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -15,13 +15,19 @@ function run(argv) {
1515
return JSON.stringify({error: 'Task ID required'});
1616
}
1717

18-
const tasks = doc.flattenedTasks.whose({id: updateData.id});
19-
if (tasks.length === 0) {
18+
// Find task by ID
19+
const allTasks = doc.flattenedTasks();
20+
let task = null;
21+
for (let i = 0; i < allTasks.length; i++) {
22+
if (allTasks[i].id() === updateData.id) {
23+
task = allTasks[i];
24+
break;
25+
}
26+
}
27+
if (!task) {
2028
return JSON.stringify({error: 'Task not found'});
2129
}
2230

23-
const task = tasks[0];
24-
2531
// Update properties
2632
if (updateData.name !== undefined) {
2733
task.name = updateData.name;

0 commit comments

Comments
 (0)