Skip to content

Commit e6caf43

Browse files
grichaclaude
andauthored
Add user scripts feature with multi-path and directory support (#41)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
1 parent 6384d95 commit e6caf43

File tree

17 files changed

+710
-568
lines changed

17 files changed

+710
-568
lines changed

docs/docs/configuration/ai-agents.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
---
2-
sidebar_position: 5
2+
sidebar_position: 6
33
---
44

55
# AI Agents

docs/docs/configuration/github.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
---
2-
sidebar_position: 4
2+
sidebar_position: 5
33
---
44

55
# GitHub Integration

docs/docs/configuration/overview.md

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,11 @@ Location: `~/.config/perry/config.json`
5050
"workspaces": {}
5151
},
5252
"scripts": {
53-
"post_start": "~/.config/perry/scripts/post-start.sh"
53+
"post_start": [
54+
"~/.perry/userscripts",
55+
"~/scripts/setup.sh"
56+
],
57+
"fail_on_error": false
5458
},
5559
"allowHostAccess": true
5660
}
@@ -103,6 +107,7 @@ perry config worker myserver.tail1234.ts.net
103107

104108
- [Environment Variables](./environment.md) - Inject env vars into workspaces
105109
- [Files](./files.md) - Copy files into workspaces
110+
- [Scripts](./scripts.md) - Run scripts after workspace starts
106111
- [GitHub](./github.md) - GitHub token and SSH key setup
107112
- [AI Agents](./ai-agents.md) - Claude Code, OpenCode, Codex CLI
108113
- [Tailscale](./tailscale.md) - Remote access via Tailscale

docs/docs/configuration/scripts.md

Lines changed: 214 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,214 @@
1+
---
2+
sidebar_position: 4
3+
---
4+
5+
# Scripts
6+
7+
Run custom scripts after workspace starts. Scripts execute after file sync, so they can reference synced resources.
8+
9+
## Configuration
10+
11+
### Via config.json
12+
13+
```json
14+
{
15+
"scripts": {
16+
"post_start": [
17+
"~/.perry/userscripts",
18+
"~/scripts/setup.sh"
19+
],
20+
"fail_on_error": false
21+
}
22+
}
23+
```
24+
25+
### Via Web UI
26+
27+
1. Open http://localhost:7391
28+
2. Go to Settings > Scripts
29+
3. Add script paths or directories
30+
4. Toggle "Stop on script error" if needed
31+
5. Save
32+
33+
## Default Configuration
34+
35+
New installations include:
36+
37+
```json
38+
{
39+
"scripts": {
40+
"post_start": ["~/.perry/userscripts"],
41+
"fail_on_error": false
42+
}
43+
}
44+
```
45+
46+
Create `~/.perry/userscripts/` directory on your host to add startup scripts.
47+
48+
## Script Types
49+
50+
### Single Scripts
51+
52+
Point to a shell script file:
53+
54+
```json
55+
{
56+
"scripts": {
57+
"post_start": ["~/scripts/setup.sh"]
58+
}
59+
}
60+
```
61+
62+
The script must be executable (`chmod +x`).
63+
64+
### Script Directories
65+
66+
Point to a directory containing `.sh` files:
67+
68+
```json
69+
{
70+
"scripts": {
71+
"post_start": ["~/.perry/userscripts"]
72+
}
73+
}
74+
```
75+
76+
All `.sh` files in the directory execute in **sorted order** (alphabetical). Use numeric prefixes to control order:
77+
78+
```
79+
~/.perry/userscripts/
80+
01-install-tools.sh
81+
02-configure-git.sh
82+
10-setup-project.sh
83+
```
84+
85+
Non-`.sh` files are ignored.
86+
87+
## Multiple Sources
88+
89+
Combine scripts and directories:
90+
91+
```json
92+
{
93+
"scripts": {
94+
"post_start": [
95+
"~/.perry/userscripts",
96+
"~/work/company-setup.sh",
97+
"~/projects/tools"
98+
]
99+
}
100+
}
101+
```
102+
103+
Scripts execute in array order.
104+
105+
## Error Handling
106+
107+
### Default: Continue on Error
108+
109+
```json
110+
{
111+
"scripts": {
112+
"post_start": ["~/scripts/setup.sh"],
113+
"fail_on_error": false
114+
}
115+
}
116+
```
117+
118+
If a script fails, Perry logs a warning and continues with remaining scripts. Workspace starts normally.
119+
120+
### Strict Mode
121+
122+
```json
123+
{
124+
"scripts": {
125+
"post_start": ["~/scripts/setup.sh"],
126+
"fail_on_error": true
127+
}
128+
}
129+
```
130+
131+
If any script exits with non-zero status, workspace startup fails.
132+
133+
## Execution Environment
134+
135+
Scripts run:
136+
- As the `workspace` user
137+
- In the container's home directory (`/home/workspace`)
138+
- After file sync completes (synced files are available)
139+
- With access to configured environment variables
140+
141+
## Common Use Cases
142+
143+
### Install Project Tools
144+
145+
```bash
146+
#!/bin/bash
147+
# ~/.perry/userscripts/01-install-tools.sh
148+
149+
# Install global npm packages
150+
npm install -g typescript tsx
151+
152+
# Install rust tools
153+
cargo install just
154+
```
155+
156+
### Configure Git
157+
158+
```bash
159+
#!/bin/bash
160+
# ~/.perry/userscripts/02-git-config.sh
161+
162+
# Set up git aliases not in .gitconfig
163+
git config --global alias.st status
164+
git config --global alias.co checkout
165+
```
166+
167+
### Create Symlinks
168+
169+
```bash
170+
#!/bin/bash
171+
# ~/.perry/userscripts/03-symlinks.sh
172+
173+
# Link synced config directories
174+
ln -sf ~/.synced-nvim ~/.config/nvim
175+
ln -sf ~/.synced-tmux/.tmux.conf ~/.tmux.conf
176+
```
177+
178+
### Start Background Services
179+
180+
```bash
181+
#!/bin/bash
182+
# ~/.perry/userscripts/99-services.sh
183+
184+
# Start any background services needed
185+
# (Note: prefer using Docker services when possible)
186+
```
187+
188+
## Path Expansion
189+
190+
- `~` expands to home directory on host
191+
- Scripts are copied to container and executed there
192+
- Absolute paths work as-is
193+
194+
## Apply Changes
195+
196+
Scripts run:
197+
- When creating new workspaces
198+
- When starting stopped workspaces
199+
200+
Scripts do **not** run when syncing (`perry sync`) - only file sync occurs.
201+
202+
## Debugging
203+
204+
Check script output in workspace logs:
205+
206+
```bash
207+
perry logs myworkspace
208+
```
209+
210+
Or connect to the workspace and check manually:
211+
212+
```bash
213+
perry ssh myworkspace
214+
```

docs/docs/configuration/tailscale.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
---
2-
sidebar_position: 6
2+
sidebar_position: 7
33
---
44

55
# Tailscale Integration

src/agent/router.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,8 @@ const CredentialsSchema = z.object({
6161
});
6262

6363
const ScriptsSchema = z.object({
64-
post_start: z.string().optional(),
64+
post_start: z.array(z.string()).optional(),
65+
fail_on_error: z.boolean().optional(),
6566
});
6667

6768
const CodingAgentsSchema = z.object({

src/config/loader.ts

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,10 @@ export function createDefaultAgentConfig(): AgentConfig {
1919
env: {},
2020
files: {},
2121
},
22-
scripts: {},
22+
scripts: {
23+
post_start: ['~/.perry/userscripts'],
24+
fail_on_error: false,
25+
},
2326
agents: {},
2427
allowHostAccess: true,
2528
ssh: {
@@ -33,6 +36,19 @@ export function createDefaultAgentConfig(): AgentConfig {
3336
};
3437
}
3538

39+
function migratePostStart(value: unknown): string[] {
40+
if (!value) {
41+
return ['~/.perry/userscripts'];
42+
}
43+
if (typeof value === 'string') {
44+
return [value, '~/.perry/userscripts'];
45+
}
46+
if (Array.isArray(value)) {
47+
return value.length > 0 ? value : ['~/.perry/userscripts'];
48+
}
49+
return ['~/.perry/userscripts'];
50+
}
51+
3652
export async function loadAgentConfig(configDir?: string): Promise<AgentConfig> {
3753
const dir = getConfigDir(configDir);
3854
const configPath = path.join(dir, CONFIG_FILE);
@@ -46,7 +62,10 @@ export async function loadAgentConfig(configDir?: string): Promise<AgentConfig>
4662
env: config.credentials?.env || {},
4763
files: config.credentials?.files || {},
4864
},
49-
scripts: config.scripts || {},
65+
scripts: {
66+
post_start: migratePostStart(config.scripts?.post_start),
67+
fail_on_error: config.scripts?.fail_on_error ?? false,
68+
},
5069
agents: config.agents || {},
5170
allowHostAccess: config.allowHostAccess ?? true,
5271
ssh: {

src/index.ts

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -437,8 +437,15 @@ configCmd
437437
for (const [dest, src] of Object.entries(config.credentials.files)) {
438438
console.log(` - ${dest} <- ${src}`);
439439
}
440-
if (config.scripts.post_start) {
441-
console.log(` Post-start Script: ${config.scripts.post_start}`);
440+
const scripts = config.scripts.post_start;
441+
if (scripts && scripts.length > 0) {
442+
console.log(` Post-start Scripts: ${scripts.length}`);
443+
for (const script of scripts) {
444+
console.log(` - ${script}`);
445+
}
446+
}
447+
if (config.scripts.fail_on_error) {
448+
console.log(` Scripts Fail on Error: enabled`);
442449
}
443450
});
444451

src/shared/client-types.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,8 @@ export interface Credentials {
3636
}
3737

3838
export interface Scripts {
39-
post_start?: string;
39+
post_start?: string[];
40+
fail_on_error?: boolean;
4041
}
4142

4243
export interface CodingAgents {

src/shared/types.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,8 @@ export interface WorkspaceCredentials {
1111
}
1212

1313
export interface WorkspaceScripts {
14-
post_start?: string;
14+
post_start?: string[];
15+
fail_on_error?: boolean;
1516
}
1617

1718
export interface CodingAgents {

0 commit comments

Comments
 (0)