outcrop 0.1.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 +398 -0
- package/assets/logo.svg +19 -0
- package/dist/cli.js +4107 -0
- package/docs/AGENTS.md +39 -0
- package/docs/API.md +176 -0
- package/docs/SPEC.md +101 -0
- package/package.json +64 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Amirul Hakim Azmi
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,398 @@
|
|
|
1
|
+
<p align="center"><img src="assets/logo.svg" alt="" width="96" height="96"></p>
|
|
2
|
+
|
|
3
|
+
<h1 align="center">outcrop</h1>
|
|
4
|
+
|
|
5
|
+
<p align="center">
|
|
6
|
+
Your markdown is the bedrock. <b>outcrop</b> exposes it: point it at a folder of markdown, get a live kanban board, a docs site, and a JSON API for AI agents.
|
|
7
|
+
</p>
|
|
8
|
+
|
|
9
|
+
<p align="center">
|
|
10
|
+
<a href="https://www.npmjs.com/package/outcrop"><img alt="npm" src="https://img.shields.io/npm/v/outcrop.svg"></a>
|
|
11
|
+
<a href="LICENSE"><img alt="license" src="https://img.shields.io/npm/l/outcrop.svg"></a>
|
|
12
|
+
<img alt="node" src="https://img.shields.io/node/v/outcrop.svg">
|
|
13
|
+
<img alt="dependencies" src="https://img.shields.io/badge/dependencies-0-brightgreen">
|
|
14
|
+
</p>
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
npx outcrop
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
That's it. No install, no config file, no database, no account. Your markdown files *are* the database.
|
|
21
|
+
|
|
22
|
+
---
|
|
23
|
+
|
|
24
|
+
## Table of contents
|
|
25
|
+
|
|
26
|
+
- [Why](#why)
|
|
27
|
+
- [Quick start](#quick-start)
|
|
28
|
+
- [Workspace format](#workspace-format)
|
|
29
|
+
- [Features](#features)
|
|
30
|
+
- [For AI agents](#for-ai-agents)
|
|
31
|
+
- [CLI reference](#cli-reference)
|
|
32
|
+
- [Documentation](#documentation)
|
|
33
|
+
- [Tech stack](#tech-stack)
|
|
34
|
+
- [Development](#development)
|
|
35
|
+
- [FAQ](#faq)
|
|
36
|
+
- [License](#license)
|
|
37
|
+
|
|
38
|
+
## Why
|
|
39
|
+
|
|
40
|
+
Task trackers put your work in someone else's database. Markdown files put it in your repo, but then you're reading a folder of `.md` files by hand: no board, no search, no way for an AI agent to safely pick up work.
|
|
41
|
+
|
|
42
|
+
outcrop sits in the middle. One command turns a plain `tasks/` folder into:
|
|
43
|
+
|
|
44
|
+
- a **kanban board** you can drag cards around in. Every drop writes back to the markdown
|
|
45
|
+
- a **docs site** with a foldable file tree, a per-page outline, full-text search, mermaid diagrams, and timelines
|
|
46
|
+
- a **graph view** of how every note links to every other, the way Obsidian draws one
|
|
47
|
+
- a **JSON API** with optimistic concurrency, so multiple AI agents can work the same workspace without clobbering each other
|
|
48
|
+
|
|
49
|
+
Humans and agents read and write the *same files*. Delete outcrop tomorrow and you still have your markdown, in git, exactly as it was.
|
|
50
|
+
|
|
51
|
+
## Quick start
|
|
52
|
+
|
|
53
|
+
Any directory is a workspace. A `tasks/` folder gets you a board; without one you get a docs site:
|
|
54
|
+
|
|
55
|
+
```bash
|
|
56
|
+
mkdir -p my-workspace/tasks
|
|
57
|
+
cat > my-workspace/tasks/ST-001-first-task.md <<'EOF'
|
|
58
|
+
---
|
|
59
|
+
id: ST-001
|
|
60
|
+
title: Ship the first thing
|
|
61
|
+
epic: E01
|
|
62
|
+
status: ready
|
|
63
|
+
priority: P1
|
|
64
|
+
depends_on: []
|
|
65
|
+
created: 2026-08-02
|
|
66
|
+
---
|
|
67
|
+
|
|
68
|
+
## Description
|
|
69
|
+
|
|
70
|
+
The first task in the workspace.
|
|
71
|
+
|
|
72
|
+
## Subtasks
|
|
73
|
+
|
|
74
|
+
- [ ] write it
|
|
75
|
+
- [ ] test it
|
|
76
|
+
EOF
|
|
77
|
+
|
|
78
|
+
npx outcrop my-workspace
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
Open http://localhost:4311.
|
|
82
|
+
|
|
83
|
+
| Route | What |
|
|
84
|
+
|---|---|
|
|
85
|
+
| `/` | Kanban board |
|
|
86
|
+
| `/docs` | Docs viewer |
|
|
87
|
+
| `/graph` | Link graph |
|
|
88
|
+
| `/api` | JSON API |
|
|
89
|
+
| `/sse` | Live change stream |
|
|
90
|
+
|
|
91
|
+
## Workspace format
|
|
92
|
+
|
|
93
|
+
A workspace is a directory. Nothing is generated, nothing is hidden.
|
|
94
|
+
|
|
95
|
+
```
|
|
96
|
+
my-workspace/
|
|
97
|
+
├── tasks/ # optional: one file per task (no tasks/ = docs-only site)
|
|
98
|
+
│ ├── ST-001-slug.md
|
|
99
|
+
│ └── ST-002-slug.md
|
|
100
|
+
├── epics/ # optional: one file per epic
|
|
101
|
+
│ └── E01-slug.md
|
|
102
|
+
├── decisions/ # optional: any folder of .md becomes a doc group
|
|
103
|
+
│ └── ADR-001.md
|
|
104
|
+
├── README.md # optional: root docs show in the viewer
|
|
105
|
+
└── TIMELINE.md # optional: renders as a Gantt chart
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
Task frontmatter is flat `key: value`. No nesting, no quoting rules, parseable in ten lines of any language:
|
|
109
|
+
|
|
110
|
+
```yaml
|
|
111
|
+
---
|
|
112
|
+
id: ST-001 # required, unique, ST-###
|
|
113
|
+
title: Short imperative title
|
|
114
|
+
epic: E01
|
|
115
|
+
status: ready # backlog | ready | in-progress | in-review | done | blocked
|
|
116
|
+
priority: P1 # P1 | P2 | P3
|
|
117
|
+
depends_on: [] # inline list: [ST-001, ST-002]
|
|
118
|
+
created: 2026-08-02
|
|
119
|
+
---
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
Body sections are optional and ordered: `## Description`, `## Subtasks`, `## Acceptance criteria`, `## Test steps`, `## Notes`. Checkboxes under `## Subtasks` become progress rings on the board.
|
|
123
|
+
|
|
124
|
+
Full normative spec: **[SPEC.md](docs/SPEC.md)**.
|
|
125
|
+
|
|
126
|
+
## Features
|
|
127
|
+
|
|
128
|
+
- **Kanban board** grouped by `status`. Drag and drop rewrites one frontmatter line on disk. Nothing else in the file changes.
|
|
129
|
+
- **Task detail sheet** with subtask checkboxes, dependency chips, and backlinks.
|
|
130
|
+
- **Docs viewer** with a foldable file tree, full-text search, mermaid diagrams, and timeline rendering (`render: timeline` in frontmatter, or a file named `TIMELINE.md`).
|
|
131
|
+
- **Outline pane** on the right of every document, built from its headings, with the current section highlighted as you scroll. Hidden when a document has nothing worth listing.
|
|
132
|
+
- **Foldable folders.** Each folder in the file tree collapses on its own, and one button in the tree header collapses or expands the lot. Everything starts open, and what you fold is remembered per browser.
|
|
133
|
+
- **Graph view**, a force-directed map of the workspace. Nodes are documents, edges are links (markdown links, bare `ST-001` mentions, `epic:` and `depends_on:`), and node size follows how well connected a note is. Scroll to zoom, drag to pan, drag a node to pull it around, click one to open it. Live like every other view.
|
|
134
|
+
- **Globe mode**, on by default. The graph wrapped onto a sphere: links follow great circles across the surface, the far side dims, and it turns slowly until you take hold of it. Drag to spin. `Flat` unwraps it onto a plane, which is worth doing when you need to read the whole graph at once, since a globe hides half its nodes behind itself. Either choice is remembered per browser.
|
|
135
|
+
- **Raw HTML, allowlisted.** The subset a README uses (centred headings, badge images, `<details>` blocks) renders as markup. Everything else stays escaped and shows as text. A workspace gets cloned and written into by agents, and the JSON API on this origin has no auth, so `<script>`, event handlers, `iframe`/`object`/`svg`, and the `class`/`id`/`style` attributes are all off the list. HTML inside a code span is left alone as the sample it is.
|
|
136
|
+
- **Hand entry.** The `+` in the header opens a form for a new user story, epic, or document, on any page. The server assigns ids (`ST-004`, `E02`) and filenames, the epic/status/priority dropdowns are filled from the workspace as it stands, and the result is an ordinary markdown file you could have typed yourself.
|
|
137
|
+
- **Clickable checklists**, in a task's subtasks and in any document. Ticking one rewrites that single `- [ ]` line on disk and nothing else. Checkboxes shown inside fenced code blocks are samples, so they are never counted or touched.
|
|
138
|
+
- **Auto-linking.** A bare `ST-001` or `E01` anywhere in any file becomes a link. Backlinks are derived, never stored.
|
|
139
|
+
- **Live reload** over SSE. Edit a file in your editor, the board updates. Agents can subscribe to the same stream instead of polling.
|
|
140
|
+
- **Git audit trail.** Set `BOARD_GIT_COMMIT=1` and every board mutation becomes a commit (`ST-001: ready -> in-progress via board`).
|
|
141
|
+
- **Keyboard shortcuts.** `⌘K` / `Ctrl+K` jumps to search from anywhere, including mid-typing in another field. `/` does the same when you're not in a text field, `Esc` clears and leaves it, and `Esc` also closes the detail pane.
|
|
142
|
+
- **Settings** in the gear menu, on every page. Workspace settings (title, views, columns, git auto-commit) are saved to `.outcrop.json` and travel with the repo. Appearance (theme, accent colour, typeface, UI scale) is per-browser and stays out of the workspace.
|
|
143
|
+
- **Docs-only workspaces.** No `tasks/` folder? Turn the board view off and it's a markdown site with search, backlinks, and diagrams.
|
|
144
|
+
- **Zero runtime dependencies.** A single bundled file on top of the Node standard library. Nothing is fetched from a CDN at page load: the stylesheet is compiled at build time and inlined into the binary.
|
|
145
|
+
- **Fast on big workspaces.** Parsed files, backlinks, and rendered pages are cached until something on disk changes. Detail panes load on click instead of shipping with the board. Responses are gzipped and revalidate with an `ETag`, so a live-reload for an unrelated file costs one `304` and no re-render.
|
|
146
|
+
|
|
147
|
+
## For AI agents
|
|
148
|
+
|
|
149
|
+
This is the part most task tools don't have. Agents are first-class users, not a scraping target.
|
|
150
|
+
|
|
151
|
+
```bash
|
|
152
|
+
# read
|
|
153
|
+
curl localhost:4311/api/tasks # all tasks, with id/status/rev/deps/subtask counts
|
|
154
|
+
curl localhost:4311/api/tasks/ST-001 # one item
|
|
155
|
+
curl 'localhost:4311/api/docs-search?q=auth'
|
|
156
|
+
|
|
157
|
+
# write
|
|
158
|
+
curl -X PATCH localhost:4311/api/items/ST-001 \
|
|
159
|
+
-H 'content-type: application/json' \
|
|
160
|
+
-d '{"status":"in-progress","rev":"<rev from the GET>"}'
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
Every item carries a **`rev`** (the file's mtime when you read it). Send it back with your mutation and outcrop rejects the write with `409` if anyone touched the file in between. Never a silent merge. That one rule is what makes several agents plus a human safe on one workspace:
|
|
164
|
+
|
|
165
|
+
1. `GET` the task, keep the `rev`.
|
|
166
|
+
2. `PATCH` with that `rev` to claim it (`status: in-progress`).
|
|
167
|
+
3. `409`? Someone else got there first. Re-read, reconcile, or pick a different task.
|
|
168
|
+
|
|
169
|
+
Agents can also skip HTTP and edit the markdown directly. The format is specified precisely so both paths produce identical files. Convention: **agents stop at `in-review`, a human moves things to `done`** after running the task's test steps.
|
|
170
|
+
|
|
171
|
+
Drop **[AGENTS.md](docs/AGENTS.md)** into your workspace to hand these rules to your agent, and **[llms.txt](llms.txt)** for the machine-readable index.
|
|
172
|
+
|
|
173
|
+
## CLI reference
|
|
174
|
+
|
|
175
|
+
```
|
|
176
|
+
Usage: outcrop [workspace-dir] [-p|--port N] [-v|--version]
|
|
177
|
+
|
|
178
|
+
workspace-dir workspace directory (default: cwd)
|
|
179
|
+
-p, --port N port to serve on (default: 4311, or $PORT)
|
|
180
|
+
-v, --version print version
|
|
181
|
+
-h, --help print this help
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
| Env var | Effect |
|
|
185
|
+
|---|---|
|
|
186
|
+
| `PORT` | Serving port (overridden by `-p`) |
|
|
187
|
+
| `BOARD_GIT_COMMIT=1` | Force git auto-commit on, whatever `.outcrop.json` says |
|
|
188
|
+
| `OUTCROP_QUIET=1` | Silence the request log |
|
|
189
|
+
|
|
190
|
+
If the port is already taken, usually by a second workspace you're already running, outcrop takes the next free one and says so instead of failing to start:
|
|
191
|
+
|
|
192
|
+
```
|
|
193
|
+
notes: http://localhost:4312
|
|
194
|
+
workspace /Users/me/notes
|
|
195
|
+
port 4311 was busy, using 4312
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
It scans up to 20 ports before giving up.
|
|
199
|
+
|
|
200
|
+
Every request is logged with what it actually touched, so you can watch a workspace while an agent works it:
|
|
201
|
+
|
|
202
|
+
```
|
|
203
|
+
21:23:41 PUT /api/settings 200 .outcrop.json: views: ["docs"] -> ["board","docs"]
|
|
204
|
+
21:23:41 GET /docs?file=SPEC.md 200 SPEC.md
|
|
205
|
+
21:23:53 PATCH /api/items/ST-001 200 tasks/ST-001.md: ST-001: ready -> in-progress via board
|
|
206
|
+
21:24:02 GET /sse 200 2 client(s) live
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
Opening a task card shows up as a `GET /api/items/:id/pane`, because the board fetches detail panes on click rather than shipping them all up front. Filtering and theme changes stay in the browser and are not logged.
|
|
210
|
+
|
|
211
|
+
## Settings
|
|
212
|
+
|
|
213
|
+
Two tiers, because two different things are being remembered.
|
|
214
|
+
|
|
215
|
+
**Workspace settings** live in `.outcrop.json` at the workspace root. They belong to the workspace, not to you, so commit them and your team gets the same board. Edit the file or use the gear menu; both take effect immediately, no restart. Missing file or a broken key falls back to the default, so an empty workspace still just works.
|
|
216
|
+
|
|
217
|
+
```json
|
|
218
|
+
{
|
|
219
|
+
"title": "Payments rewrite",
|
|
220
|
+
"views": ["board", "docs", "graph"],
|
|
221
|
+
"columns": ["backlog", "ready", "in-progress", "in-review", "done", "blocked"],
|
|
222
|
+
"gitCommit": false
|
|
223
|
+
}
|
|
224
|
+
```
|
|
225
|
+
|
|
226
|
+
- `title`: header and page title. Defaults to the workspace directory name.
|
|
227
|
+
- `views`: which of `board`, `docs` and `graph` are served. Drop `board` for a workspace with no `tasks/` folder and outcrop becomes a plain docs site. A disabled view redirects to the first one still enabled, and with one view the nav tabs disappear. At least one view must stay on: a `PUT` that turns all of them off is rejected, and the settings pane says so rather than quietly putting them back.
|
|
228
|
+
- `columns`: board columns, in order. Values are slugged to match the `status` frontmatter key. A status used by a task always gets a column even if it's listed nowhere here, so renaming columns can never hide a task.
|
|
229
|
+
- `gitCommit`: the persistent form of `BOARD_GIT_COMMIT=1`.
|
|
230
|
+
|
|
231
|
+
**Browser settings** live in `localStorage`. They're per-person, so they never touch the workspace and never show up in a teammate's diff.
|
|
232
|
+
|
|
233
|
+
| | |
|
|
234
|
+
|---|---|
|
|
235
|
+
| **Theme** | Light or dark, also on the header button. First visit follows your OS; after that your choice sticks. |
|
|
236
|
+
| **Accent** | Violet (default), blue, emerald, amber, rose. Drives links, refs, focus rings, the Save button, drag targets and search highlights. |
|
|
237
|
+
| **Typeface** | Sans (default), serif, or mono. OS font stacks only: no webfont is downloaded. |
|
|
238
|
+
| **Size** | Compact, default, or large, scaling the whole UI. |
|
|
239
|
+
|
|
240
|
+
Precedence: CLI flag > env var > `.outcrop.json` > default.
|
|
241
|
+
|
|
242
|
+
Install globally if you'd rather not type `npx`:
|
|
243
|
+
|
|
244
|
+
```bash
|
|
245
|
+
npm install -g outcrop
|
|
246
|
+
```
|
|
247
|
+
|
|
248
|
+
## Documentation
|
|
249
|
+
|
|
250
|
+
| Doc | What's in it |
|
|
251
|
+
|---|---|
|
|
252
|
+
| **[SPEC.md](docs/SPEC.md)** | Normative workspace format: frontmatter schema, IDs, links, revisions |
|
|
253
|
+
| **[API.md](docs/API.md)** | HTTP API contract: endpoints, request/response shapes, status codes |
|
|
254
|
+
| **[AGENTS.md](docs/AGENTS.md)** | Rules of engagement for AI agents working in a workspace |
|
|
255
|
+
| **[llms.txt](llms.txt)** | Machine-readable index for LLM tooling |
|
|
256
|
+
| **[RELEASING.md](docs/RELEASING.md)** | Maintainer notes: npm setup, versioning, publishing |
|
|
257
|
+
|
|
258
|
+
## Tech stack
|
|
259
|
+
|
|
260
|
+
No framework, on either side. The whole thing is TypeScript compiled to one Node script.
|
|
261
|
+
|
|
262
|
+
**Backend**: Node's standard library only, nothing else at runtime.
|
|
263
|
+
|
|
264
|
+
| Concern | What does it |
|
|
265
|
+
|---|---|
|
|
266
|
+
| HTTP | `node:http` `createServer`, with the handler written against the web `Request`/`Response` types |
|
|
267
|
+
| Storage | The filesystem. `node:fs` reads and writes markdown; there is no database and no index on disk |
|
|
268
|
+
| Live updates | Server-sent events over a `ReadableStream`, driven by a recursive `node:fs` `watch` |
|
|
269
|
+
| Concurrency | File mtime as an optimistic-concurrency `rev`, returning `409` on a stale write |
|
|
270
|
+
| Markdown | A ~100-line parser in `src/cli.ts`: frontmatter, headings, lists, tables, checkboxes, links |
|
|
271
|
+
| Compression, hashing | `node:zlib` gzip, `node:crypto` SHA-1 for `ETag`s |
|
|
272
|
+
| Git integration | `node:child_process` shelling out to `git` |
|
|
273
|
+
|
|
274
|
+
**Frontend**: no framework, no build step, no client bundle, no hydration.
|
|
275
|
+
|
|
276
|
+
| Concern | What does it |
|
|
277
|
+
|---|---|
|
|
278
|
+
| Rendering | Server-rendered HTML from template literals. The browser receives finished markup |
|
|
279
|
+
| Styling | Tailwind CSS, compiled at build time into one stylesheet and inlined into the binary. Accent, typeface and scale are CSS custom properties switched by a `data-` attribute on `<html>`, so a preference change repaints without refetching anything |
|
|
280
|
+
| Interactivity | ~300 lines of plain ES2022 in `<script>` tags: delegated listeners, `fetch`, `EventSource`, native drag-and-drop |
|
|
281
|
+
| Navigation | Clicking a doc swaps the content in place and pushes history. No page reload, so the SSE connection is opened once per tab and never churns |
|
|
282
|
+
| Updates | SSE tells the page something changed; it re-fetches and swaps `innerHTML`, or gets a `304` and does nothing. Every view closes its stream on `pagehide`: a browser allows six connections per origin and is slow to reclaim an abandoned page's, so leaving them to be collected starved later requests of a socket |
|
|
283
|
+
| Detail panes | Fetched as HTML fragments on click and cached in a `Map` |
|
|
284
|
+
| Diagrams | mermaid from a CDN, loaded lazily and only for docs that actually contain one |
|
|
285
|
+
| Graph | Canvas 2D and a cut-down `d3-force` (1/d repulsion, degree-weighted springs, clamped velocity), no dependency and no SVG nodes to lay out. The layout is settled off-screen and framed before the first paint, so it opens still rather than flying apart. The forces are 3D in both modes: the flat graph is the globe with the z axis pinned, which is why there is one simulation and not two |
|
|
286
|
+
| Persistence | `localStorage` for appearance, sidebar width and folded folders; everything shared goes to `.outcrop.json` |
|
|
287
|
+
|
|
288
|
+
**Build and tooling**: all devDependencies. The published package has zero runtime dependencies.
|
|
289
|
+
|
|
290
|
+
| | |
|
|
291
|
+
|---|---|
|
|
292
|
+
| Language | TypeScript (strict), typechecked by `tsc`, never emitted by it |
|
|
293
|
+
| Package manager | Bun (`bun.lock`); npm is only used to publish with `--provenance` |
|
|
294
|
+
| Bundler | Bun, producing one `dist/cli.js` targeting Node ≥ 20 |
|
|
295
|
+
| CSS | Tailwind CLI, purging against the template literals in `src/**/*.ts` |
|
|
296
|
+
| Lint / format | [oxlint](https://oxc.rs) and oxfmt |
|
|
297
|
+
| Tests | `bun test` |
|
|
298
|
+
| Hook | `.githooks/pre-commit`, wired by `bun install` |
|
|
299
|
+
|
|
300
|
+
## Size
|
|
301
|
+
|
|
302
|
+
One file, no runtime dependencies, nothing fetched at page load.
|
|
303
|
+
|
|
304
|
+
| | |
|
|
305
|
+
|---|---|
|
|
306
|
+
| npm download | **42 KB** (tarball) |
|
|
307
|
+
| Installed on disk | **147 KB**, of which `dist/cli.js` is 118 KB |
|
|
308
|
+
| Runtime dependencies | **0** |
|
|
309
|
+
| Stylesheet | 39 KB, inlined in the bundle, served as 6 KB gzipped and cached immutably |
|
|
310
|
+
|
|
311
|
+
What a browser actually pulls, measured on a 300-task / 371-file workspace:
|
|
312
|
+
|
|
313
|
+
| | gzipped |
|
|
314
|
+
|---|---|
|
|
315
|
+
| Board page | 46 KB |
|
|
316
|
+
| Docs page | 13 KB |
|
|
317
|
+
| Graph page | 9 KB, graph data inlined, so the view makes no follow-up request |
|
|
318
|
+
| Stylesheet | 6 KB (once, then cached) |
|
|
319
|
+
| One detail pane | 2 KB, fetched on click |
|
|
320
|
+
| Switching to another doc | ~11 KB, ~11 ms, no page reload |
|
|
321
|
+
| Live-reload with nothing to show | 0 bytes (`304`) |
|
|
322
|
+
|
|
323
|
+
The bundle is deliberately not minified: it saves ~3 KB gzipped and costs readable stack traces.
|
|
324
|
+
|
|
325
|
+
## Development
|
|
326
|
+
|
|
327
|
+
Requires [Bun](https://bun.sh) to build (the output is plain Node, with no runtime dependencies).
|
|
328
|
+
|
|
329
|
+
```bash
|
|
330
|
+
git clone https://github.com/amirulhakiim/outcrop
|
|
331
|
+
cd outcrop
|
|
332
|
+
bun install # dev tooling only; also installs the pre-commit hook
|
|
333
|
+
bun run build # styles.css -> styles.gen.css, then bundles src/cli.ts -> dist/cli.js
|
|
334
|
+
bun run check # format + lint + types + tests
|
|
335
|
+
node dist/cli.js path/to/workspace
|
|
336
|
+
```
|
|
337
|
+
|
|
338
|
+
Bun is the package manager and the runner. There is no `package-lock.json`, only `bun.lock`.
|
|
339
|
+
|
|
340
|
+
| Script | What it does |
|
|
341
|
+
|---|---|
|
|
342
|
+
| `bun run build` | Compile the stylesheet, then bundle to `dist/cli.js` |
|
|
343
|
+
| `bun run lint` | oxlint (`--deny-warnings`, so a warning fails) |
|
|
344
|
+
| `bun run fmt` | oxfmt, rewriting in place. `fmt:check` only reports |
|
|
345
|
+
| `bun run typecheck` | `tsc --noEmit` over both projects, server and client |
|
|
346
|
+
| `bun test` | Unit tests, plus end-to-end tests that spawn the built CLI and drive it over HTTP |
|
|
347
|
+
| `bun run check` | All four, in the order the hook uses |
|
|
348
|
+
|
|
349
|
+
`bun run` puts `node_modules/.bin` on `PATH`, so the scripts call `tailwindcss`, `oxlint`, and `tsc` directly, with no `npx` or `bunx` indirection.
|
|
350
|
+
|
|
351
|
+
`bun install` runs `prepare`, which points `core.hooksPath` at `.githooks/`. The pre-commit hook formats the staged `src/` files, re-stages them, then runs oxlint and `tsc`; a lint warning or type error stops the commit. `git commit --no-verify` skips it.
|
|
352
|
+
|
|
353
|
+
`bun run build` runs two steps: the Tailwind CLI compiles [`src/styles.css`](src/styles.css) into `src/styles.gen.css` (gitignored), then Bun inlines that as text so the shipped CLI stays a single file. Building `src/cli.ts` on its own will fail until `build:css` has run once.
|
|
354
|
+
|
|
355
|
+
```
|
|
356
|
+
src/
|
|
357
|
+
├── cli.ts server: routing, caching, markdown, HTML rendering
|
|
358
|
+
├── settings.ts .outcrop.json schema and normalisation
|
|
359
|
+
├── styles.css Tailwind source, compiled at build time
|
|
360
|
+
├── client/ browser code: real TypeScript, typechecked and linted
|
|
361
|
+
│ ├── prefs.ts appearance preferences (localStorage)
|
|
362
|
+
│ ├── chrome.ts resizer, theme, sheet, settings, search shortcut
|
|
363
|
+
│ ├── board.ts board entry point
|
|
364
|
+
│ ├── docs.ts docs entry point
|
|
365
|
+
│ └── tsconfig.json DOM types, no node types
|
|
366
|
+
├── settings.test.ts unit tests
|
|
367
|
+
└── server.test.ts end-to-end tests against the built CLI
|
|
368
|
+
```
|
|
369
|
+
|
|
370
|
+
The browser code is bundled separately (`bun run build:client` → `src/generated/*.js`, gitignored) and inlined into `dist/cli.js` as text, so the CLI is still a single shipped file with no runtime dependencies.
|
|
371
|
+
|
|
372
|
+
Each client bundle is built as an IIFE. Without that, every bundle's top-level names land on `window`, and the pre-paint head script shares eight of them with the page scripts.
|
|
373
|
+
|
|
374
|
+
### Performance notes
|
|
375
|
+
|
|
376
|
+
Class names are scanned out of the template literals in `src/**/*.ts`, so a class assembled by string concatenation gets purged. Write them out in full. The accent swatches list `"swatch swatch-violet"` and friends verbatim for exactly this reason.
|
|
377
|
+
|
|
378
|
+
Browser code belongs in `src/client/`, not in a template literal. Code inside a template string is data as far as `tsc` and oxlint are concerned: a call to an undefined function and an assignment to a `const` both used to pass lint, typecheck and build without a word.
|
|
379
|
+
|
|
380
|
+
Anything the board repeats per task (the card, its chips, the priority and progress marks) is a component class in `styles.css` rather than inline utilities. Inline utility strings on a few hundred cards were about a third of the page.
|
|
381
|
+
|
|
382
|
+
## FAQ
|
|
383
|
+
|
|
384
|
+
**Does it modify my files?**
|
|
385
|
+
Only the exact line you change. Moving a card rewrites one `status:` line; ticking a checkbox rewrites one `- [ ]`. Everything else in the file is byte-identical.
|
|
386
|
+
|
|
387
|
+
**Is it multiplayer / hosted?**
|
|
388
|
+
No. It binds locally and has no auth. Sharing state is git's job.
|
|
389
|
+
|
|
390
|
+
**What if two agents write at once?**
|
|
391
|
+
The second one gets a `409` and retries. See [For AI agents](#for-ai-agents).
|
|
392
|
+
|
|
393
|
+
**Can I use my own file naming or extra frontmatter keys?**
|
|
394
|
+
IDs and filenames follow the spec (`ST-###-slug.md`). Extra frontmatter keys are preserved on write but ignored by the board.
|
|
395
|
+
|
|
396
|
+
## License
|
|
397
|
+
|
|
398
|
+
[MIT](LICENSE)
|
package/assets/logo.svg
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="120 12 800 800">
|
|
2
|
+
<title>outcrop</title>
|
|
3
|
+
<desc>A rock outcrop: faceted strata breaking the surface.</desc>
|
|
4
|
+
<g id="outcrop-mark">
|
|
5
|
+
<path d="M140 640 L215 590 L275 500 L330 530 L385 400 L455 330 L515 170 L580 280 L650 245 L710 365 L780 430 L850 585 L900 640 Z" fill="#24282d"/>
|
|
6
|
+
<path d="M385 400 L455 330 L515 170 L525 410 L455 520 Z" fill="#e2e6ea"/>
|
|
7
|
+
<path d="M525 410 L515 170 L580 280 L650 245 L625 430 Z" fill="#59616a"/>
|
|
8
|
+
<path d="M625 430 L650 245 L710 365 L690 510 Z" fill="#30363d"/>
|
|
9
|
+
<path d="M455 520 L525 410 L625 430 L570 575 Z" fill="#414850"/>
|
|
10
|
+
<path d="M275 500 L330 530 L385 400 L410 525 L350 585 Z" fill="#7c858e"/>
|
|
11
|
+
<path d="M650 245 L710 365 L780 430 L720 465 L690 510 Z" fill="#747d86"/>
|
|
12
|
+
<path d="M140 640 L215 590 L275 600 L330 575 L350 585 L410 560 L455 590 L570 575 L630 610 L690 510 L720 465 L780 430 L850 585 L900 640 Z" fill="#343a41"/>
|
|
13
|
+
<path d="M215 590 L275 500 L275 600 Z" fill="#9aa1a8"/>
|
|
14
|
+
<path d="M350 585 L410 525 L455 590 Z" fill="#b9bec3"/>
|
|
15
|
+
<path d="M570 575 L625 430 L630 610 Z" fill="#8b939b"/>
|
|
16
|
+
<path d="M690 510 L720 465 L780 430 L760 550 Z" fill="#aeb4b9"/>
|
|
17
|
+
<path d="M140 640 L900 640 L850 655 L175 655 Z" fill="#15191d"/>
|
|
18
|
+
</g>
|
|
19
|
+
</svg>
|