Skip to content

Commit 589ceda

Browse files
committed
beta 49 updates and package with claude section
1 parent 19839e8 commit 589ceda

File tree

7 files changed

+149
-41
lines changed

7 files changed

+149
-41
lines changed

build.sh

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,15 @@ mkdir -p docs
1717
(cd packaging && MDBOOK_OUTPUT__HTML__SITE_URL="/packaging/$PACKAGING_VERSION/" \
1818
mdbook build -d "../docs/packaging/$PACKAGING_VERSION")
1919

20+
# Redirect stubs: /book/ → /book/version/
21+
for pair in "start-os:$START_OS_VERSION" "start-tunnel:$START_TUNNEL_VERSION" "packaging:$PACKAGING_VERSION"; do
22+
book="${pair%%:*}"
23+
version="${pair##*:}"
24+
cat > "docs/$book/index.html" <<EOF
25+
<!doctype html><meta http-equiv="refresh" content="0; url=/$book/$version/">
26+
EOF
27+
done
28+
2029
# Landing page
2130
cp landing/index.html docs/index.html
2231

landing/index.html

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -48,11 +48,10 @@
4848
}
4949

5050
.cards {
51-
display: flex;
51+
display: grid;
52+
grid-template-columns: repeat(2, 300px);
5253
gap: 2rem;
53-
flex-wrap: wrap;
5454
justify-content: center;
55-
max-width: 700px;
5655
}
5756

5857
a.card {
@@ -108,13 +107,20 @@ <h1>Start9 Docs</h1>
108107

109108
<div class="cards">
110109
<a class="card" href="/start-os/">
111-
<h2>StartOS</h2>
110+
<h2>StartOS v0.4.0</h2>
112111
<p>
113112
A server operating system optimized for self-hosting services on a
114113
personal server.
115114
</p>
116115
</a>
117116

117+
<a class="card" href="/0.3.5.x/">
118+
<h2>StartOS v0.3.5.1</h2>
119+
<p>
120+
Documentation for StartOS version 0.3.5.1 and earlier.
121+
</p>
122+
</a>
123+
118124
<a class="card" href="/start-tunnel/">
119125
<h2>StartTunnel</h2>
120126
<p>

packaging/src/SAMPLE-CLAUDE.txt

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
# StartOS Service Packager
2+
3+
You are a StartOS service packager. You help create, maintain, and update `.s9pk` service packages for StartOS.
4+
5+
## Packaging Guide
6+
7+
The packaging guide is your primary reference. Follow it exactly. Read `SUMMARY.md` first to identify which sections are relevant to the current task, then read only those `.md` files directly. Do not load all sections at once.
8+
9+
### Local Read (preferred)
10+
11+
If `start-docs/packaging/src/` exists in the workspace, use the Read tool:
12+
13+
```
14+
start-docs/packaging/src/SUMMARY.md # Read first — section index
15+
start-docs/packaging/src/<section>.md # Then read only what you need
16+
```
17+
18+
### Web Fetch (fallback)
19+
20+
If the local docs are not available, use WebFetch:
21+
22+
```
23+
WebFetch: https://docs.start9.com/packaging/llms.txt # Fetch first — section index
24+
WebFetch: https://docs.start9.com/packaging/<page>.html # Then fetch only what you need
25+
```
26+
27+
## Golden Rule
28+
**Match existing patterns**: Match patterns from the docs and other packages. Pretty much anything you might need todo has already been done well.

packaging/src/environment-setup.md

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,3 +81,54 @@ start-cli --version
8181

8282
> [!TIP]
8383
> If any command is not found, revisit the installation steps for that tool and ensure it is on your system PATH.
84+
85+
## Coding with Claude (Recommended)
86+
87+
AI coding tools like [Claude Code](https://docs.anthropic.com/en/docs/claude-code) can dramatically accelerate your packaging workflow. To get the best results, set up a workspace that gives Claude direct access to the packaging guide.
88+
89+
### 1. Create a workspace directory
90+
91+
Create a directory that will serve as your AI-assisted workspace. This is **not** inside your package repo — it sits alongside it.
92+
93+
```
94+
mkdir my-workspace && cd my-workspace
95+
```
96+
97+
### 2. Clone the docs
98+
99+
Clone the Start9 docs repo so Claude can read the packaging guide locally:
100+
101+
```
102+
git clone https://github.com/Start9Labs/docs.git start-docs
103+
```
104+
105+
### 3. Add a CLAUDE.md
106+
107+
> [!IMPORTANT]
108+
> This is critical. Without this file, Claude will not know how to package a service for StartOS.
109+
110+
Download the provided `CLAUDE.md` and place it in your workspace root:
111+
112+
<a href="SAMPLE-CLAUDE.txt" download="CLAUDE.md" style="display:inline-block;padding:0.5em 1.2em;background:#58a6ff;color:#fff;border-radius:6px;text-decoration:none;font-weight:600">Download CLAUDE.md</a>
113+
114+
This file instructs Claude to use the local packaging guide as its primary reference.
115+
116+
### 4. Add your package repo
117+
118+
Clone or create your package repo inside the workspace:
119+
120+
```
121+
git clone https://github.com/user/my-service-startos.git
122+
```
123+
124+
Your workspace should look like this:
125+
126+
```
127+
my-workspace/
128+
├── CLAUDE.md ← AI instructions (not committed anywhere)
129+
├── start-docs/ ← packaging guide for Claude to read
130+
└── my-service-startos/ ← your package repo
131+
```
132+
133+
> [!NOTE]
134+
> Do not put `start-docs/` or `CLAUDE.md` inside your package repo. They live alongside it in the workspace so they don't pollute your package's git history.

packaging/src/file-models.md

Lines changed: 27 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -79,21 +79,25 @@ export const configYaml = FileHelper.yaml(
7979

8080
```typescript
8181
// One-time read (no restart on change) - returns null if file doesn't exist
82-
const store = await storeJson.read((s) => s).once()
82+
const store = await storeJson.read().once()
8383

8484
// Handle missing file with nullish coalescing
8585
const keys = (await authorizedKeysFile.read().once()) ?? []
8686

8787
// Reactive read (service restarts if value changes)
88-
const store = await storeJson.read((s) => s).const(effects)
88+
const store = await storeJson.read().const(effects)
8989

9090
// Read only specific fields (subset reading)
9191
const password = await storeJson.read((s) => s.adminPassword).once()
9292

93-
// Reactive subset read
93+
// Reactive subset read - daemon only restarts if secretKey changes
9494
const secretKey = await storeJson.read((s) => s.secretKey).const(effects)
9595
```
9696

97+
> [!WARNING]
98+
> Never use an identity mapper like `.read((s) => s)`. Either omit the mapper to get the full object (`.read()`) or use it to extract a specific field (`.read((s) => s.someField)`).
99+
100+
97101
### Subset Reading
98102

99103
Use mapping to retrieve only specific fields rather than entire files:
@@ -147,26 +151,41 @@ const shape = object({
147151
When an upstream service reads a config file (TOML, YAML, JSON, etc.), model that file directly with `FileHelper` rather than storing values in `store.json` and passing them as environment variables. A direct FileModel provides:
148152

149153
- **Two-way binding**: Actions can read and write the upstream config file directly.
150-
- **Simpler main.ts**: No need to read from store, build env vars, and pass them to the daemon. Just write the FileModel to the subcontainer rootfs.
154+
- **Simpler main.ts**: Mount the config file from the volume into the subcontainer. No need to read and regenerate it.
151155
- **Easy user configuration**: Exposing config options via Actions is as simple as `configToml.merge(effects, { key: newValue })`.
152156

153157
Use `store.json` only for internal package state that has no upstream config file equivalent (e.g., a generated PostgreSQL password that the upstream service doesn't read from its own config file).
154158

155159
```typescript
156160
// GOOD: Model the upstream config directly
157161
export const configToml = FileHelper.toml(
158-
{ base: sdk.volumes['mcaptcha-data'], subpath: 'config.toml' },
162+
{ base: sdk.volumes['my-data'], subpath: 'config.toml' },
159163
shape,
160164
)
161165

162-
// In main.ts, write to subcontainer rootfs
163-
await writeFile(`${appSub.rootfs}/etc/mcaptcha/config.toml`,
164-
await configToml.read((c) => c).const(effects))
166+
// In main.ts, mount the volume so the config file is accessible in the subcontainer.
167+
// You can mount the individual file or an entire directory that contains it.
168+
const appSub = await sdk.SubContainer.of(effects, { imageId: 'my-app' },
169+
sdk.Mounts.of().mountVolume({
170+
volumeId: 'my-data',
171+
subpath: 'config.toml',
172+
mountpoint: '/etc/my-app/config.toml',
173+
readonly: false,
174+
type: 'file',
175+
}),
176+
'my-app-sub',
177+
)
178+
179+
// Reactive read triggers daemon restart when config changes (e.g. via actions)
180+
await configToml.read((c) => c.some_mutable_setting).const(effects)
165181

166182
// In an action, toggle a setting directly
167183
await configToml.merge(effects, { allow_registration: !current })
168184
```
169185

186+
> [!WARNING]
187+
> Do NOT read a FileModel in main.ts and then write it back to the subcontainer rootfs. The file already lives on the volume — just mount it.
188+
170189
## Common Patterns
171190

172191
### Optional Fields with Defaults

packaging/src/main.md

Lines changed: 8 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -232,34 +232,24 @@ sdk.Mounts.of()
232232

233233
## Writing to Subcontainer Rootfs
234234

235-
For config files that are regenerated on every startup, write directly to the subcontainer's rootfs instead of using volume mounts. This is simpler and avoids file mount issues:
235+
For config files that are *generated from code* on every startup (e.g., a Python settings file built from hostnames and secrets), write directly to the subcontainer's rootfs:
236236

237237
```typescript
238-
// Create subcontainer first
239-
const appSub = await sdk.SubContainer.of(
240-
effects,
241-
{ imageId: "my-service" },
242-
sdk.Mounts.of().mountVolume({
243-
volumeId: "main",
244-
subpath: null,
245-
mountpoint: "/data",
246-
readonly: false,
247-
}),
248-
"my-service-sub",
249-
);
250-
251-
// Write config directly to subcontainer rootfs
238+
// Write a generated config to subcontainer rootfs
252239
await writeFile(
253240
`${appSub.rootfs}/app/config.py`,
254241
generateConfig({ secretKey, allowedHosts }),
255242
);
256243
```
257244

245+
> [!WARNING]
246+
> If the config file is managed by a FileModel, do NOT read it and write it back to rootfs. Mount it from the volume instead — the file already exists there.
247+
258248
**When to use rootfs vs volume mounts:**
259249

260-
- **Rootfs**: Ephemeral config files regenerated on each startup (secrets, hostnames, etc.)
261-
- **Volume mount (directory)**: Persistent data that survives restarts (databases, user files)
262-
- **Volume mount (file)**: Persistent config that users might edit (requires `type: 'file'`)
250+
- **Rootfs**: Config files *generated from code* that don't exist on a volume (e.g., built from hostnames, env vars, or templates)
251+
- **Volume mount (directory)**: Mount a directory that contains the config file alongside other persistent data. The config file is just one of many files in the mounted directory.
252+
- **Volume mount (file)**: Mount a single config file with `type: 'file'` when the config lives on a volume that is otherwise unrelated to the container's filesystem.
263253

264254
## Executing Commands in SubContainers
265255

packaging/src/manifest.md

Lines changed: 16 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -44,12 +44,11 @@ export const manifest = setupManifest({
4444
id: 'my-service',
4545
title: 'My Service',
4646
license: 'MIT',
47-
wrapperRepo: 'https://github.com/Start9Labs/my-service-startos',
47+
packageRepo: 'https://github.com/Start9Labs/my-service-startos',
4848
upstreamRepo: 'https://github.com/original/my-service',
49-
supportSite: 'https://docs.example.com/',
50-
marketingSite: 'https://example.com/',
49+
marketingUrl: 'https://example.com/',
5150
donationUrl: null,
52-
docsUrl: 'https://docs.example.com/guides',
51+
docsUrls: ['https://docs.example.com/guides'],
5352
description: { short, long },
5453
volumes: ['main'],
5554
images: {
@@ -74,12 +73,11 @@ export const manifest = setupManifest({
7473
| `id` | Unique identifier (lowercase, hyphens allowed) |
7574
| `title` | Display name shown in UI |
7675
| `license` | SPDX identifier (`MIT`, `Apache-2.0`, `GPL-3.0`, etc.) |
77-
| `wrapperRepo` | URL to the StartOS wrapper repository |
76+
| `packageRepo` | URL to the StartOS package repository |
7877
| `upstreamRepo` | URL to the original project repository |
79-
| `supportSite` | URL for user support |
80-
| `marketingSite` | URL for the project's main website |
78+
| `marketingUrl` | URL for the project's main website |
8179
| `donationUrl` | Donation URL or `null` |
82-
| `docsUrl` | URL to **upstream** documentation (not wrapper docs) |
80+
| `docsUrls` | Array of URLs to **upstream** documentation |
8381
| `description.short` | Locale object (see `manifest/i18n.ts`) |
8482
| `description.long` | Locale object (see `manifest/i18n.ts`) |
8583
| `volumes` | Storage volumes (usually `['main']`) |
@@ -89,10 +87,13 @@ export const manifest = setupManifest({
8987

9088
## License
9189

92-
Check the upstream project's LICENSE file and use the correct SPDX identifier (e.g., `MIT`, `Apache-2.0`, `GPL-3.0`). Create a symlink from your project root to the upstream license:
90+
Check the upstream project's LICENSE file and use the correct SPDX identifier (e.g., `MIT`, `Apache-2.0`, `GPL-3.0`). If you have a git submodule, symlink to its license. Otherwise, copy the license text directly from the upstream repository:
9391

9492
```bash
93+
# With submodule
9594
ln -sf upstream-project/LICENSE LICENSE
95+
96+
# Without submodule -- copy from upstream repo
9697
```
9798

9899
## Icon
@@ -250,9 +251,13 @@ alerts: {
250251

251252
## Volumes
252253

253-
Storage volumes for persistent data. Usually just `['main']`:
254+
Storage volumes for persistent data. When possible, prefer matching the upstream project's volume naming convention for clarity:
254255

255256
```typescript
257+
// If upstream docker-compose uses a volume named "mcaptcha-data"
258+
volumes: ['mcaptcha-data'],
259+
260+
// Simple services can use 'main'
256261
volumes: ['main'],
257262
```
258263

@@ -262,7 +267,7 @@ For services needing separate storage areas:
262267
volumes: ['main', 'db', 'config'],
263268
```
264269

265-
Reference these in `main.ts` mounts as `'main'`, `'db'`, `'config'`.
270+
Reference these in `main.ts` mounts by the volume ID you chose.
266271

267272
## Dependencies
268273

0 commit comments

Comments
 (0)