patchfeld 0.2.1__tar.gz → 0.2.2__tar.gz
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.
- {patchfeld-0.2.1 → patchfeld-0.2.2}/PKG-INFO +5 -3
- {patchfeld-0.2.1 → patchfeld-0.2.2}/README.md +4 -2
- {patchfeld-0.2.1 → patchfeld-0.2.2}/patchfeld/agents/session.py +31 -1
- {patchfeld-0.2.1 → patchfeld-0.2.2}/patchfeld/widgets/agent_transcript.py +35 -0
- {patchfeld-0.2.1 → patchfeld-0.2.2}/pyproject.toml +1 -1
- patchfeld-0.2.2/website/README.md +126 -0
- {patchfeld-0.2.1 → patchfeld-0.2.2}/.gitignore +0 -0
- {patchfeld-0.2.1 → patchfeld-0.2.2}/LICENSE +0 -0
- {patchfeld-0.2.1 → patchfeld-0.2.2}/patchfeld/__init__.py +0 -0
- {patchfeld-0.2.1 → patchfeld-0.2.2}/patchfeld/__main__.py +0 -0
- {patchfeld-0.2.1 → patchfeld-0.2.2}/patchfeld/actions.py +0 -0
- {patchfeld-0.2.1 → patchfeld-0.2.2}/patchfeld/activity/__init__.py +0 -0
- {patchfeld-0.2.1 → patchfeld-0.2.2}/patchfeld/activity/log.py +0 -0
- {patchfeld-0.2.1 → patchfeld-0.2.2}/patchfeld/agents/__init__.py +0 -0
- {patchfeld-0.2.1 → patchfeld-0.2.2}/patchfeld/agents/child_tools.py +0 -0
- {patchfeld-0.2.1 → patchfeld-0.2.2}/patchfeld/agents/fake_sdk_adapter.py +0 -0
- {patchfeld-0.2.1 → patchfeld-0.2.2}/patchfeld/agents/manager.py +0 -0
- {patchfeld-0.2.1 → patchfeld-0.2.2}/patchfeld/agents/permission_grants.py +0 -0
- {patchfeld-0.2.1 → patchfeld-0.2.2}/patchfeld/agents/permission_inbox.py +0 -0
- {patchfeld-0.2.1 → patchfeld-0.2.2}/patchfeld/agents/request_inbox.py +0 -0
- {patchfeld-0.2.1 → patchfeld-0.2.2}/patchfeld/agents/sdk_adapter.py +0 -0
- {patchfeld-0.2.1 → patchfeld-0.2.2}/patchfeld/agents/sort.py +0 -0
- {patchfeld-0.2.1 → patchfeld-0.2.2}/patchfeld/agents/state.py +0 -0
- {patchfeld-0.2.1 → patchfeld-0.2.2}/patchfeld/app.py +0 -0
- {patchfeld-0.2.1 → patchfeld-0.2.2}/patchfeld/config.py +0 -0
- {patchfeld-0.2.1 → patchfeld-0.2.2}/patchfeld/events.py +0 -0
- {patchfeld-0.2.1 → patchfeld-0.2.2}/patchfeld/layout/__init__.py +0 -0
- {patchfeld-0.2.1 → patchfeld-0.2.2}/patchfeld/layout/custom_widgets.py +0 -0
- {patchfeld-0.2.1 → patchfeld-0.2.2}/patchfeld/layout/defaults.py +0 -0
- {patchfeld-0.2.1 → patchfeld-0.2.2}/patchfeld/layout/engine.py +0 -0
- {patchfeld-0.2.1 → patchfeld-0.2.2}/patchfeld/layout/local_widgets.py +0 -0
- {patchfeld-0.2.1 → patchfeld-0.2.2}/patchfeld/layout/registry.py +0 -0
- {patchfeld-0.2.1 → patchfeld-0.2.2}/patchfeld/layout/spec.py +0 -0
- {patchfeld-0.2.1 → patchfeld-0.2.2}/patchfeld/layout/splitter.py +0 -0
- {patchfeld-0.2.1 → patchfeld-0.2.2}/patchfeld/layout/titles.py +0 -0
- {patchfeld-0.2.1 → patchfeld-0.2.2}/patchfeld/orchestrator/__init__.py +0 -0
- {patchfeld-0.2.1 → patchfeld-0.2.2}/patchfeld/orchestrator/formatting.py +0 -0
- {patchfeld-0.2.1 → patchfeld-0.2.2}/patchfeld/orchestrator/session.py +0 -0
- {patchfeld-0.2.1 → patchfeld-0.2.2}/patchfeld/orchestrator/tabs_tools.py +0 -0
- {patchfeld-0.2.1 → patchfeld-0.2.2}/patchfeld/orchestrator/tools.py +0 -0
- {patchfeld-0.2.1 → patchfeld-0.2.2}/patchfeld/persistence/__init__.py +0 -0
- {patchfeld-0.2.1 → patchfeld-0.2.2}/patchfeld/persistence/agents_index.py +0 -0
- {patchfeld-0.2.1 → patchfeld-0.2.2}/patchfeld/persistence/atomic.py +0 -0
- {patchfeld-0.2.1 → patchfeld-0.2.2}/patchfeld/persistence/layout_store.py +0 -0
- {patchfeld-0.2.1 → patchfeld-0.2.2}/patchfeld/persistence/layouts_store.py +0 -0
- {patchfeld-0.2.1 → patchfeld-0.2.2}/patchfeld/persistence/orchestrator_sessions.py +0 -0
- {patchfeld-0.2.1 → patchfeld-0.2.2}/patchfeld/persistence/paths.py +0 -0
- {patchfeld-0.2.1 → patchfeld-0.2.2}/patchfeld/persistence/themes_store.py +0 -0
- {patchfeld-0.2.1 → patchfeld-0.2.2}/patchfeld/persistence/transcript_store.py +0 -0
- {patchfeld-0.2.1 → patchfeld-0.2.2}/patchfeld/persistence/workspace_store.py +0 -0
- {patchfeld-0.2.1 → patchfeld-0.2.2}/patchfeld/theme/__init__.py +0 -0
- {patchfeld-0.2.1 → patchfeld-0.2.2}/patchfeld/theme/engine.py +0 -0
- {patchfeld-0.2.1 → patchfeld-0.2.2}/patchfeld/theme/spec.py +0 -0
- {patchfeld-0.2.1 → patchfeld-0.2.2}/patchfeld/widgets/__init__.py +0 -0
- {patchfeld-0.2.1 → patchfeld-0.2.2}/patchfeld/widgets/_file_lang.py +0 -0
- {patchfeld-0.2.1 → patchfeld-0.2.2}/patchfeld/widgets/_terminal_keys.py +0 -0
- {patchfeld-0.2.1 → patchfeld-0.2.2}/patchfeld/widgets/_terminal_render.py +0 -0
- {patchfeld-0.2.1 → patchfeld-0.2.2}/patchfeld/widgets/activity_feed.py +0 -0
- {patchfeld-0.2.1 → patchfeld-0.2.2}/patchfeld/widgets/agent_table.py +0 -0
- {patchfeld-0.2.1 → patchfeld-0.2.2}/patchfeld/widgets/change_cwd_screen.py +0 -0
- {patchfeld-0.2.1 → patchfeld-0.2.2}/patchfeld/widgets/chrome.py +0 -0
- {patchfeld-0.2.1 → patchfeld-0.2.2}/patchfeld/widgets/diff_viewer.py +0 -0
- {patchfeld-0.2.1 → patchfeld-0.2.2}/patchfeld/widgets/file_editor.py +0 -0
- {patchfeld-0.2.1 → patchfeld-0.2.2}/patchfeld/widgets/file_tree.py +0 -0
- {patchfeld-0.2.1 → patchfeld-0.2.2}/patchfeld/widgets/file_viewer.py +0 -0
- {patchfeld-0.2.1 → patchfeld-0.2.2}/patchfeld/widgets/history_screen.py +0 -0
- {patchfeld-0.2.1 → patchfeld-0.2.2}/patchfeld/widgets/layout_switcher.py +0 -0
- {patchfeld-0.2.1 → patchfeld-0.2.2}/patchfeld/widgets/log_tail.py +0 -0
- {patchfeld-0.2.1 → patchfeld-0.2.2}/patchfeld/widgets/markdown.py +0 -0
- {patchfeld-0.2.1 → patchfeld-0.2.2}/patchfeld/widgets/new_tab_screen.py +0 -0
- {patchfeld-0.2.1 → patchfeld-0.2.2}/patchfeld/widgets/notebook.py +0 -0
- {patchfeld-0.2.1 → patchfeld-0.2.2}/patchfeld/widgets/orchestrator_chat.py +0 -0
- {patchfeld-0.2.1 → patchfeld-0.2.2}/patchfeld/widgets/permission_modal.py +0 -0
- {patchfeld-0.2.1 → patchfeld-0.2.2}/patchfeld/widgets/permission_request_bar.py +0 -0
- {patchfeld-0.2.1 → patchfeld-0.2.2}/patchfeld/widgets/resume_screen.py +0 -0
- {patchfeld-0.2.1 → patchfeld-0.2.2}/patchfeld/widgets/rich_transcript.py +0 -0
- {patchfeld-0.2.1 → patchfeld-0.2.2}/patchfeld/widgets/system_usage.py +0 -0
- {patchfeld-0.2.1 → patchfeld-0.2.2}/patchfeld/widgets/terminal.py +0 -0
- {patchfeld-0.2.1 → patchfeld-0.2.2}/patchfeld/widgets/theme_switcher.py +0 -0
- {patchfeld-0.2.1 → patchfeld-0.2.2}/patchfeld/widgets/transcript_screen.py +0 -0
- {patchfeld-0.2.1 → patchfeld-0.2.2}/patchfeld/workspace/__init__.py +0 -0
- {patchfeld-0.2.1 → patchfeld-0.2.2}/patchfeld/workspace/spec.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: patchfeld
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.2
|
|
4
4
|
Summary: A Textual TUI for managing multiple Claude Code agent sessions
|
|
5
5
|
Project-URL: Homepage, https://github.com/jimmymills/patchfeld
|
|
6
6
|
Project-URL: Repository, https://github.com/jimmymills/patchfeld
|
|
@@ -39,6 +39,8 @@ Description-Content-Type: text/markdown
|
|
|
39
39
|
orchestrator-managed workspace — and lets the agent reshape the UI to fit
|
|
40
40
|
the work.**
|
|
41
41
|
|
|
42
|
+
**[patchfeld.com](https://patchfeld.com)** · [PyPI](https://pypi.org/project/patchfeld/) · [GitHub](https://github.com/jimmymills/patchfeld)
|
|
43
|
+
|
|
42
44
|

|
|
43
45
|
|
|
44
46
|
## The pitch
|
|
@@ -48,7 +50,7 @@ one's writing tests, one's doing a security pass. They live in three
|
|
|
48
50
|
terminal tabs with three scrollbacks, and you're the one mentally juggling
|
|
49
51
|
which is waiting on what.
|
|
50
52
|
|
|
51
|
-
**Patchfeld is
|
|
53
|
+
**Patchfeld is a studio for orchestrating agents.** One TUI. One top-level Claude —
|
|
52
54
|
the *orchestrator* — runs the show. You tell it what you want done in
|
|
53
55
|
plain English; it spawns the right children with the right tool
|
|
54
56
|
allowlists, watches their progress, and pulls them onscreen when they
|
|
@@ -118,7 +120,7 @@ the ideas.
|
|
|
118
120
|
the actual `claude` CLI, or your shell, in any panel. Mode-C custom
|
|
119
121
|
widgets let the orchestrator ship Python at runtime when the curated
|
|
120
122
|
widget library isn't enough.
|
|
121
|
-
- **Approve tool calls without leaving the
|
|
123
|
+
- **Approve tool calls without leaving the workspace.** When a child wants
|
|
122
124
|
to use a tool that isn't auto-approved, a modal pops in patchfeld with
|
|
123
125
|
the tool name and full arguments. Approve once, deny once, always
|
|
124
126
|
allow this tool for any agent named X (persisted to disk), or always
|
|
@@ -4,6 +4,8 @@
|
|
|
4
4
|
orchestrator-managed workspace — and lets the agent reshape the UI to fit
|
|
5
5
|
the work.**
|
|
6
6
|
|
|
7
|
+
**[patchfeld.com](https://patchfeld.com)** · [PyPI](https://pypi.org/project/patchfeld/) · [GitHub](https://github.com/jimmymills/patchfeld)
|
|
8
|
+
|
|
7
9
|

|
|
8
10
|
|
|
9
11
|
## The pitch
|
|
@@ -13,7 +15,7 @@ one's writing tests, one's doing a security pass. They live in three
|
|
|
13
15
|
terminal tabs with three scrollbacks, and you're the one mentally juggling
|
|
14
16
|
which is waiting on what.
|
|
15
17
|
|
|
16
|
-
**Patchfeld is
|
|
18
|
+
**Patchfeld is a studio for orchestrating agents.** One TUI. One top-level Claude —
|
|
17
19
|
the *orchestrator* — runs the show. You tell it what you want done in
|
|
18
20
|
plain English; it spawns the right children with the right tool
|
|
19
21
|
allowlists, watches their progress, and pulls them onscreen when they
|
|
@@ -83,7 +85,7 @@ the ideas.
|
|
|
83
85
|
the actual `claude` CLI, or your shell, in any panel. Mode-C custom
|
|
84
86
|
widgets let the orchestrator ship Python at runtime when the curated
|
|
85
87
|
widget library isn't enough.
|
|
86
|
-
- **Approve tool calls without leaving the
|
|
88
|
+
- **Approve tool calls without leaving the workspace.** When a child wants
|
|
87
89
|
to use a tool that isn't auto-approved, a modal pops in patchfeld with
|
|
88
90
|
the tool name and full arguments. Approve once, deny once, always
|
|
89
91
|
allow this tool for any agent named X (persisted to disk), or always
|
|
@@ -50,6 +50,13 @@ class AgentSession:
|
|
|
50
50
|
self._send_lock = asyncio.Lock()
|
|
51
51
|
self._pre_wait_state: AgentState | None = None
|
|
52
52
|
self._pre_perm_state: AgentState | None = None
|
|
53
|
+
# Tasks created by queue_send() that are still pending or in-flight.
|
|
54
|
+
# Tracked so interrupt() can cancel them before they call
|
|
55
|
+
# adapter.query — otherwise a DirectMessageToAgent queued behind
|
|
56
|
+
# the active stream (e.g. an orchestrator's send_to_agent payload)
|
|
57
|
+
# would wake up the moment the SDK signals end-of-stream and run
|
|
58
|
+
# the queued prompt against the now-interrupted session.
|
|
59
|
+
self._queued_send_tasks: list[asyncio.Task] = []
|
|
53
60
|
|
|
54
61
|
@property
|
|
55
62
|
def session_id(self) -> str | None:
|
|
@@ -79,14 +86,37 @@ class AgentSession:
|
|
|
79
86
|
in the same task will correctly block until the send completes —
|
|
80
87
|
without it, wait_idle could return before the send task acquires the
|
|
81
88
|
send lock.
|
|
89
|
+
|
|
90
|
+
The task is also tracked so `interrupt()` can cancel it if the user
|
|
91
|
+
interrupts before it has issued its query.
|
|
82
92
|
"""
|
|
83
93
|
self._idle_event.clear()
|
|
84
|
-
|
|
94
|
+
task = asyncio.create_task(self.send(prompt))
|
|
95
|
+
# Drop completed tasks before appending so the list doesn't grow.
|
|
96
|
+
self._queued_send_tasks = [
|
|
97
|
+
t for t in self._queued_send_tasks if not t.done()
|
|
98
|
+
]
|
|
99
|
+
self._queued_send_tasks.append(task)
|
|
100
|
+
task.add_done_callback(self._queued_send_tasks.remove)
|
|
101
|
+
return task
|
|
85
102
|
|
|
86
103
|
async def wait_idle(self) -> None:
|
|
87
104
|
await self._idle_event.wait()
|
|
88
105
|
|
|
89
106
|
async def interrupt(self) -> None:
|
|
107
|
+
# Cancel any send tasks that are still queued (blocked behind the
|
|
108
|
+
# active stream) BEFORE signalling the SDK. Otherwise the SDK's
|
|
109
|
+
# end-of-stream wakes them up and they post their prompt against
|
|
110
|
+
# the now-interrupted session — which is how an orchestrator's
|
|
111
|
+
# `send_to_agent` payload was landing on the child agent after
|
|
112
|
+
# the user pressed ctrl+c.
|
|
113
|
+
pending = [t for t in self._queued_send_tasks if not t.done()]
|
|
114
|
+
for task in pending:
|
|
115
|
+
task.cancel()
|
|
116
|
+
# Wait for cancellations to propagate so the lock is released
|
|
117
|
+
# before the SDK starts winding down the stream.
|
|
118
|
+
if pending:
|
|
119
|
+
await asyncio.gather(*pending, return_exceptions=True)
|
|
90
120
|
await self._adapter.interrupt()
|
|
91
121
|
|
|
92
122
|
async def stop(self) -> None:
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
from textual.app import ComposeResult
|
|
2
|
+
from textual.binding import Binding
|
|
2
3
|
from textual.containers import Vertical
|
|
3
4
|
from textual.widgets import Input
|
|
4
5
|
|
|
@@ -23,6 +24,14 @@ class AgentTranscript(Vertical):
|
|
|
23
24
|
}
|
|
24
25
|
"""
|
|
25
26
|
|
|
27
|
+
# priority=True so the binding fires even when the Input child has
|
|
28
|
+
# focus — without it, ctrl+c is consumed by Textual's default driver
|
|
29
|
+
# handling (which would quit the app). Mirrors OrchestratorChat so
|
|
30
|
+
# both chat panels respond to ctrl+c the same way.
|
|
31
|
+
BINDINGS = [
|
|
32
|
+
Binding("ctrl+c", "interrupt", "interrupt agent", priority=True),
|
|
33
|
+
]
|
|
34
|
+
|
|
26
35
|
def __init__(
|
|
27
36
|
self,
|
|
28
37
|
*,
|
|
@@ -73,6 +82,32 @@ class AgentTranscript(Vertical):
|
|
|
73
82
|
bus.publish(DirectMessageToAgent(agent_id=self._agent_id, text=text))
|
|
74
83
|
event.input.value = ""
|
|
75
84
|
|
|
85
|
+
async def action_interrupt(self) -> None:
|
|
86
|
+
manager = getattr(self.app, "manager", None)
|
|
87
|
+
if manager is None:
|
|
88
|
+
return
|
|
89
|
+
# Stale-panel guard: if no live session matches this panel's
|
|
90
|
+
# agent_id, manager.interrupt would silently no-op. Surface that
|
|
91
|
+
# to the user instead — otherwise ctrl+c looks broken when in
|
|
92
|
+
# fact the panel is bound to an agent that no longer exists
|
|
93
|
+
# (e.g. opened from a stale agents.json entry).
|
|
94
|
+
if manager.get_session(self._agent_id) is None:
|
|
95
|
+
try:
|
|
96
|
+
self.app.notify(
|
|
97
|
+
f"no active agent “{self._agent_id}” — panel may be stale",
|
|
98
|
+
severity="warning", timeout=5,
|
|
99
|
+
)
|
|
100
|
+
except Exception:
|
|
101
|
+
pass
|
|
102
|
+
return
|
|
103
|
+
await manager.interrupt(self._agent_id)
|
|
104
|
+
try:
|
|
105
|
+
self.app.notify(
|
|
106
|
+
f"interrupted {self._agent_id}", timeout=3,
|
|
107
|
+
)
|
|
108
|
+
except Exception:
|
|
109
|
+
pass
|
|
110
|
+
|
|
76
111
|
def rendered_text(self) -> str:
|
|
77
112
|
"""Test helper — delegates to the inner RichTranscript."""
|
|
78
113
|
return self.query_one(RichTranscript).rendered_text()
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
# patchfeld website
|
|
2
|
+
|
|
3
|
+
Marketing site for [patchfeld](../README.md), hosted at
|
|
4
|
+
[patchfeld.com](https://patchfeld.com).
|
|
5
|
+
|
|
6
|
+
Astro + Tailwind, static-first, single landing page. No backend.
|
|
7
|
+
|
|
8
|
+
## Stack
|
|
9
|
+
|
|
10
|
+
- [Astro 5](https://astro.build) — static site generator with component islands
|
|
11
|
+
- [Tailwind CSS 4](https://tailwindcss.com) — via the official Vite plugin
|
|
12
|
+
- TypeScript (strict)
|
|
13
|
+
- No animation library — plain CSS + a tiny `IntersectionObserver` for fade-in
|
|
14
|
+
|
|
15
|
+
Astro was chosen because the site is content-first and ships almost zero
|
|
16
|
+
JavaScript by default; the only client-side JS is the copy-to-clipboard
|
|
17
|
+
button and the install-tab switcher (a few dozen lines, inlined per
|
|
18
|
+
component). If we ever need MDX or a docs subsite, Astro accommodates
|
|
19
|
+
both without a rewrite.
|
|
20
|
+
|
|
21
|
+
## Prerequisites
|
|
22
|
+
|
|
23
|
+
- Node.js 20.x or 22.x (24.x works in dev — official Astro support is 22 LTS)
|
|
24
|
+
- npm 10+
|
|
25
|
+
|
|
26
|
+
## Local development
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
cd website
|
|
30
|
+
npm install
|
|
31
|
+
npm run dev
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
The dev server runs on <http://localhost:4321>.
|
|
35
|
+
|
|
36
|
+
## Build
|
|
37
|
+
|
|
38
|
+
```bash
|
|
39
|
+
cd website
|
|
40
|
+
npm run build # outputs to website/dist/
|
|
41
|
+
npm run preview # serves the built site
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
The build is fully static: `dist/` contains HTML, CSS, optimized images,
|
|
45
|
+
and a small bit of JS for the interactive bits.
|
|
46
|
+
|
|
47
|
+
## Project layout
|
|
48
|
+
|
|
49
|
+
```
|
|
50
|
+
website/
|
|
51
|
+
package.json
|
|
52
|
+
astro.config.mjs
|
|
53
|
+
tsconfig.json
|
|
54
|
+
src/
|
|
55
|
+
pages/
|
|
56
|
+
index.astro # the whole site is one page for v1
|
|
57
|
+
layouts/
|
|
58
|
+
Base.astro # html shell, fonts, meta, reveal observer
|
|
59
|
+
components/
|
|
60
|
+
Nav.astro
|
|
61
|
+
Hero.astro
|
|
62
|
+
Features.astro
|
|
63
|
+
Screenshot.astro
|
|
64
|
+
Examples.astro
|
|
65
|
+
Widgets.astro
|
|
66
|
+
Install.astro
|
|
67
|
+
Footer.astro
|
|
68
|
+
styles/
|
|
69
|
+
global.css # Tailwind import + design tokens + base
|
|
70
|
+
assets/
|
|
71
|
+
screenshot.png # copied from ../docs/images/screenshot.png
|
|
72
|
+
public/
|
|
73
|
+
favicon.svg
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
## Updating the screenshot
|
|
77
|
+
|
|
78
|
+
If the screenshot in the project root (`../docs/images/screenshot.png`)
|
|
79
|
+
is regenerated, copy it back over:
|
|
80
|
+
|
|
81
|
+
```bash
|
|
82
|
+
cp ../docs/images/screenshot.png src/assets/screenshot.png
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
`src/assets/` is processed by Astro's image pipeline (compression,
|
|
86
|
+
responsive sizing, lazy loading), which is why we import it via
|
|
87
|
+
`astro:assets` in `Screenshot.astro`.
|
|
88
|
+
|
|
89
|
+
## Deploying
|
|
90
|
+
|
|
91
|
+
The site is static — any host that serves `dist/` works.
|
|
92
|
+
|
|
93
|
+
### Cloudflare Pages (recommended)
|
|
94
|
+
|
|
95
|
+
1. Create a new Pages project pointed at this repo.
|
|
96
|
+
2. Set the **build command** to `npm run build`.
|
|
97
|
+
3. Set the **build output directory** to `dist`.
|
|
98
|
+
4. Set the **root directory** to `website`.
|
|
99
|
+
5. Add the custom domain `patchfeld.com` once DNS is set up.
|
|
100
|
+
|
|
101
|
+
Cloudflare Pages auto-deploys on push and handles HTTPS for free.
|
|
102
|
+
|
|
103
|
+
### Alternatives
|
|
104
|
+
|
|
105
|
+
- **Vercel:** import the repo, set the root directory to `website`, framework
|
|
106
|
+
preset "Astro". Vercel auto-detects build command + output.
|
|
107
|
+
- **Netlify:** set base directory `website`, build command `npm run build`,
|
|
108
|
+
publish directory `website/dist`.
|
|
109
|
+
- **GitHub Pages:** publish `dist/` from a workflow. There's no built-in
|
|
110
|
+
Astro action; use [`actions/deploy-pages`](https://github.com/actions/deploy-pages)
|
|
111
|
+
with `actions/upload-pages-artifact` pointed at `website/dist`.
|
|
112
|
+
|
|
113
|
+
## Updating copy
|
|
114
|
+
|
|
115
|
+
The site's copy is consistent with the project README's voice: direct,
|
|
116
|
+
technical, slightly playful. When you add or rewrite a section, read the
|
|
117
|
+
project root README first and match its register. Don't slip into
|
|
118
|
+
generic SaaS phrasing.
|
|
119
|
+
|
|
120
|
+
GitHub URL: the repo is `jimmymills/patchfeld`. The old `patchbai` URL
|
|
121
|
+
still redirects via GitHub's rename redirect, but everything in the site
|
|
122
|
+
points at the canonical name.
|
|
123
|
+
|
|
124
|
+
## License
|
|
125
|
+
|
|
126
|
+
MIT — same as the project itself.
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|