@waishnav/devspace 1.0.1 → 1.0.3
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/README.md +140 -30
- package/dist/apply-patch.js +399 -0
- package/dist/cli.js +18 -2
- package/dist/config.js +11 -13
- package/dist/db/client.js +10 -3
- package/dist/db/migrations.js +123 -0
- package/dist/db/schema.js +24 -1
- package/dist/oauth-provider.js +35 -64
- package/dist/oauth-store.js +138 -0
- package/dist/process-platform.js +49 -0
- package/dist/process-sessions.js +329 -0
- package/dist/roots.js +5 -2
- package/dist/server.js +462 -203
- package/dist/skills.js +23 -3
- package/dist/ui/.vite/manifest.json +281 -281
- package/dist/ui/assets/{angular-html-B_77pPm_.js → angular-html-D6nc7PYw.js} +1 -1
- package/dist/ui/assets/{angular-ts-tGBi7sOr.js → angular-ts-BAIStLeI.js} +1 -1
- package/dist/ui/assets/{apl-CtVPpGgP.js → apl-CtEv5pop.js} +1 -1
- package/dist/ui/assets/{astro-CALPYvnG.js → astro-CVFIMXc6.js} +1 -1
- package/dist/ui/assets/{blade-Bh4eBXdk.js → blade-BgSlZ6jY.js} +1 -1
- package/dist/ui/assets/{c-BGkebjQE.js → c-bxLlCV26.js} +1 -1
- package/dist/ui/assets/{cobol-f7rP2PjR.js → cobol-BuZ-r51r.js} +1 -1
- package/dist/ui/assets/{coffee-E4OhMc7W.js → coffee-_JP0Joa7.js} +1 -1
- package/dist/ui/assets/{cpp-Cr_J6lB2.js → cpp-CP_eaz-H.js} +1 -1
- package/dist/ui/assets/{crystal-B_TFXhon.js → crystal-DZ-sEZd0.js} +1 -1
- package/dist/ui/assets/{css-DDMxq-4J.js → css-CZnyti9s.js} +1 -1
- package/dist/ui/assets/{edge-Bxqhgg5I.js → edge-DjsoDMTR.js} +1 -1
- package/dist/ui/assets/{elixir-WrSbgJJ1.js → elixir-C5b04AkY.js} +1 -1
- package/dist/ui/assets/{elm-BXBWJ4Kz.js → elm-Ddx-rgVY.js} +1 -1
- package/dist/ui/assets/{erb-D0iMw9rO.js → erb-cjF7oPcT.js} +1 -1
- package/dist/ui/assets/{git-rebase-BOrZ-_iR.js → git-rebase-DN5exBTl.js} +1 -1
- package/dist/ui/assets/{glimmer-js-PCWk96O8.js → glimmer-js-CEq0hNwa.js} +1 -1
- package/dist/ui/assets/{glimmer-ts-4pYwX8pC.js → glimmer-ts-4uYGFkon.js} +1 -1
- package/dist/ui/assets/{glsl-CDyKZvZX.js → glsl-DEJ9tTWm.js} +1 -1
- package/dist/ui/assets/{graphql-8sIw92YT.js → graphql-Bim5WuQh.js} +1 -1
- package/dist/ui/assets/{hack-Ci96Zcew.js → hack-BcyVIXhE.js} +1 -1
- package/dist/ui/assets/{haml-4uzh6End.js → haml-D9YaukDg.js} +1 -1
- package/dist/ui/assets/{handlebars-B6Eq7QV8.js → handlebars-CFaMw9zH.js} +1 -1
- package/dist/ui/assets/{heavy-payload--bwRtRpo.js → heavy-payload-B9aTqgfs.js} +1 -1
- package/dist/ui/assets/{html-C29wL7bm.js → html-CgO_zx4D.js} +1 -1
- package/dist/ui/assets/{html-derivative-Cmd90ilz.js → html-derivative-BxqkysJT.js} +1 -1
- package/dist/ui/assets/{http-Bdr_R0Kw.js → http-CO5dv0RA.js} +1 -1
- package/dist/ui/assets/{hurl-CiTPn-zH.js → hurl-DRHtrK78.js} +1 -1
- package/dist/ui/assets/{java-C03kBuJH.js → java-BvHC_DgR.js} +1 -1
- package/dist/ui/assets/{javascript-135g9Wwx.js → javascript-C-3payQx.js} +1 -1
- package/dist/ui/assets/{jinja-CPpiEHqV.js → jinja-CAOPvEOH.js} +1 -1
- package/dist/ui/assets/{jison-qabCQzwp.js → jison-totCB1L-.js} +1 -1
- package/dist/ui/assets/{json-CtRj9Trp.js → json-D0J9rEe1.js} +1 -1
- package/dist/ui/assets/{jsx-Ccuy5Wm3.js → jsx-na-XQD_B.js} +1 -1
- package/dist/ui/assets/{julia-Fwya-MwM.js → julia-CqrTtZ4-.js} +1 -1
- package/dist/ui/assets/{just-c9YEH7Gf.js → just-pmrVSkMc.js} +1 -1
- package/dist/ui/assets/{latex-D9xoD6UX.js → latex-Cd_geX3E.js} +1 -1
- package/dist/ui/assets/{liquid-BKZLZSHN.js → liquid-C6ky5sJ-.js} +1 -1
- package/dist/ui/assets/{lua-CBD2zNyC.js → lua-dgyBbeof.js} +1 -1
- package/dist/ui/assets/{marko-Dkgf9bj3.js → marko-Dy5mfXY9.js} +1 -1
- package/dist/ui/assets/{mdc-BUgUKiX3.js → mdc-7NUvxgI1.js} +1 -1
- package/dist/ui/assets/{nginx-CAaav4Zh.js → nginx-C-KiEXbI.js} +1 -1
- package/dist/ui/assets/{nim-CTjWWztL.js → nim-BBw8kfl9.js} +1 -1
- package/dist/ui/assets/{perl-DTM5zgH4.js → perl-CyxhJgbO.js} +1 -1
- package/dist/ui/assets/{php-dqfFDbWm.js → php-m4LAOpam.js} +1 -1
- package/dist/ui/assets/{pug-4f_clTm2.js → pug-Cpl0baYI.js} +1 -1
- package/dist/ui/assets/{qml-BFeWjFRL.js → qml-BoSGNgIb.js} +1 -1
- package/dist/ui/assets/{r-ei1iNvHm.js → r-C0JhqQd5.js} +1 -1
- package/dist/ui/assets/{razor-jj95jncJ.js → razor-CntsBNoh.js} +1 -1
- package/dist/ui/assets/{regexp-0joUf3Cn.js → regexp-BNZmOCRl.js} +1 -1
- package/dist/ui/assets/{review-payload-DxI2fTnP.js → review-payload-BgdWHWiv.js} +1 -1
- package/dist/ui/assets/{rst-BMbcN8--.js → rst-BZ2XHIxM.js} +1 -1
- package/dist/ui/assets/{ruby-BQx0xtOB.js → ruby-BIthfCTP.js} +1 -1
- package/dist/ui/assets/{sas-C_Fl1EyP.js → sas-5aT3fOla.js} +1 -1
- package/dist/ui/assets/{scss-5vaLJPJl.js → scss-DZPfdkDy.js} +1 -1
- package/dist/ui/assets/{shellscript-p_y7YCYP.js → shellscript-DbhKc5Xj.js} +1 -1
- package/dist/ui/assets/{shellsession-ChZkMp2K.js → shellsession-X9-bcyHh.js} +1 -1
- package/dist/ui/assets/{soy-im25cA8U.js → soy-Bd4tecrW.js} +1 -1
- package/dist/ui/assets/{sql-BzvG6ssm.js → sql-Chf5JrDn.js} +1 -1
- package/dist/ui/assets/{stata-CwSzNM20.js → stata-DvMMIc_T.js} +1 -1
- package/dist/ui/assets/{surrealql-BWzKhI6S.js → surrealql-D5FMzVF5.js} +1 -1
- package/dist/ui/assets/{svelte-NMlQHjaa.js → svelte-CTiVXZrv.js} +1 -1
- package/dist/ui/assets/{templ-1W11KGSL.js → templ-ldnfq-7w.js} +1 -1
- package/dist/ui/assets/{tex-CTxN8q2c.js → tex-QW0_7T2r.js} +1 -1
- package/dist/ui/assets/{ts-tags-B6prphu-.js → ts-tags-CAKiWIBx.js} +1 -1
- package/dist/ui/assets/{tsx-Daks24y9.js → tsx-Bu9xJJmq.js} +1 -1
- package/dist/ui/assets/{twig-C0KeCxch.js → twig-Dp1752_V.js} +1 -1
- package/dist/ui/assets/{typescript-37-dkCwp.js → typescript-RfO03Kmm.js} +1 -1
- package/dist/ui/assets/{useFileDiffInstance--R0_ywL_.js → useFileDiffInstance-DjqU4y1a.js} +3 -3
- package/dist/ui/assets/{vue-Ba-v02vP.js → vue-Dlasv57G.js} +1 -1
- package/dist/ui/assets/{vue-html-D2UmbuN7.js → vue-html-BHuJ7anu.js} +1 -1
- package/dist/ui/assets/{vue-vine-ZftUW_ID.js → vue-vine-CavE0PSk.js} +1 -1
- package/dist/ui/assets/{workspace-app-CsMiGtz9.js → workspace-app-DDV_qdjx.js} +6 -6
- package/dist/ui/assets/workspace-app-DGD9R89D.css +1 -0
- package/dist/ui/assets/{xml-DrhRYxTS.js → xml-Bbv64l4W.js} +1 -1
- package/dist/ui/assets/{xsl-DvbIvBcQ.js → xsl-Sro4gyQY.js} +1 -1
- package/dist/ui/assets/{yaml-oktUYAzQ.js → yaml-Du68c8sA.js} +1 -1
- package/dist/ui/workspace-app.html +2 -2
- package/dist/workspace-store.js +0 -50
- package/docs/chatgpt-coding-workflow.md +24 -4
- package/docs/codex-tool-mode-qa.md +73 -0
- package/docs/configuration.md +28 -5
- package/docs/gotchas.md +11 -5
- package/docs/setup.md +1 -1
- package/package.json +10 -5
- package/scripts/fix-node-pty-permissions.mjs +22 -0
- package/dist/ui/assets/workspace-app-8--oTbCd.css +0 -1
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
<p align="center">
|
|
2
2
|
<picture>
|
|
3
|
-
<img src="docs/assets/devspace-logo-light.png" alt="DevSpace logo" width="140">
|
|
3
|
+
<img src="https://raw.githubusercontent.com/Waishnav/devspace/main/docs/assets/devspace-logo-light.png" alt="DevSpace logo" width="140">
|
|
4
4
|
</picture>
|
|
5
5
|
</p>
|
|
6
6
|
|
|
@@ -14,15 +14,65 @@
|
|
|
14
14
|
<a href="https://github.com/Waishnav/devspace/blob/main/LICENSE"><img alt="License" src="https://img.shields.io/npm/l/%40waishnav%2Fdevspace?style=flat-square" /></a>
|
|
15
15
|
</p>
|
|
16
16
|
|
|
17
|
-
[](docs/assets/devspace-screenshot.png)
|
|
17
|
+
[](https://raw.githubusercontent.com/Waishnav/devspace/main/docs/assets/devspace-screenshot.png)
|
|
18
18
|
|
|
19
19
|
**Give ChatGPT a secure connection to your own machine and Turn ChatGPT into Codex**
|
|
20
20
|
|
|
21
21
|
DevSpace is a self-hosted MCP server that lets ChatGPT read, edit, search, and run code in your real local projects — your files, your tools, your terminal — without uploading anything to a third party. You run it on your machine, expose it through a tunnel you control, and approve the connection with a password only you have.
|
|
22
22
|
|
|
23
|
-
##
|
|
23
|
+
## Sponsors and Special Thanks
|
|
24
|
+
|
|
25
|
+
<table>
|
|
26
|
+
<thead>
|
|
27
|
+
<tr>
|
|
28
|
+
<th>Sponsor</th>
|
|
29
|
+
<th>About</th>
|
|
30
|
+
</tr>
|
|
31
|
+
</thead>
|
|
32
|
+
<tbody>
|
|
33
|
+
<tr>
|
|
34
|
+
<td align="center" width="220">
|
|
35
|
+
<a href="https://rebates.ai/">
|
|
36
|
+
<img
|
|
37
|
+
src="https://app.rebates.ai/brand/rebates-lockup.svg"
|
|
38
|
+
alt="Rebates"
|
|
39
|
+
width="170"
|
|
40
|
+
>
|
|
41
|
+
</a>
|
|
42
|
+
</td>
|
|
43
|
+
<td>
|
|
44
|
+
<strong>The ads in your terminal pay you.</strong><br><br>
|
|
45
|
+
<a href="https://rebates.ai/">Rebates</a> adds one optional
|
|
46
|
+
sponsored footer to your coding agent and pays you cash back for every
|
|
47
|
+
session in which it is shown. Turn it off at any time.
|
|
48
|
+
</td>
|
|
49
|
+
</tr>
|
|
50
|
+
</tbody>
|
|
51
|
+
</table>
|
|
52
|
+
|
|
53
|
+
<p>
|
|
54
|
+
DevSpace is open to new sponsors.
|
|
55
|
+
<a href="https://x.com/wshxnv">Get in touch to become one.</a>
|
|
56
|
+
</p>
|
|
57
|
+
|
|
58
|
+
## Installation
|
|
59
|
+
|
|
60
|
+
DevSpace requires Node `>=22.19 <27`.
|
|
61
|
+
|
|
62
|
+
Install the DevSpace CLI:
|
|
63
|
+
|
|
64
|
+
```bash
|
|
65
|
+
npm install -g @waishnav/devspace
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
Then initialize and start the server:
|
|
69
|
+
|
|
70
|
+
```bash
|
|
71
|
+
devspace init
|
|
72
|
+
devspace serve
|
|
73
|
+
```
|
|
24
74
|
|
|
25
|
-
|
|
75
|
+
Or run it without a global install:
|
|
26
76
|
|
|
27
77
|
```bash
|
|
28
78
|
npx @waishnav/devspace init
|
|
@@ -42,11 +92,7 @@ Use the public origin without `/mcp` during setup:
|
|
|
42
92
|
https://your-tunnel-host.example.com
|
|
43
93
|
```
|
|
44
94
|
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
```text
|
|
48
|
-
https://your-tunnel-host.example.com/mcp
|
|
49
|
-
```
|
|
95
|
+
You will configure your MCP client with the public `/mcp` URL after setup.
|
|
50
96
|
|
|
51
97
|
When the client connects, DevSpace opens an Owner password approval page. Enter
|
|
52
98
|
the Owner password printed by `devspace init`. It is also stored in:
|
|
@@ -57,20 +103,7 @@ the Owner password printed by `devspace init`. It is also stored in:
|
|
|
57
103
|
|
|
58
104
|
Keep that password private.
|
|
59
105
|
|
|
60
|
-
##
|
|
61
|
-
|
|
62
|
-
After the MCP client connects, ChatGPT can open a project with
|
|
63
|
-
`open_workspace` and then reuse the returned `workspaceId` for later calls.
|
|
64
|
-
|
|
65
|
-
DevSpace provides tools for:
|
|
66
|
-
|
|
67
|
-
- reading, writing, and editing files inside the opened workspace
|
|
68
|
-
- running shell-backed search and directory inspection in the default minimal tool mode
|
|
69
|
-
- running shell commands for tests, builds, git, and package scripts
|
|
70
|
-
- opening isolated Git worktrees when you want parallel work
|
|
71
|
-
- loading `AGENTS.md` and `CLAUDE.md` instructions
|
|
72
|
-
- exposing local agent skills from your skill folders
|
|
73
|
-
- showing ChatGPT Apps tool cards, with an opt-in aggregate `show_changes` card
|
|
106
|
+
## Connect Your MCP Client
|
|
74
107
|
|
|
75
108
|
The default local endpoint is:
|
|
76
109
|
|
|
@@ -84,6 +117,30 @@ Most users should connect through a public HTTPS tunnel:
|
|
|
84
117
|
https://your-tunnel-host.example.com/mcp
|
|
85
118
|
```
|
|
86
119
|
|
|
120
|
+
> [!NOTE]
|
|
121
|
+
> Using DevSpace as an MCP connector isn't against OpenAI's Usage Policies — it's
|
|
122
|
+
> a standard custom App/connector setup, and writing or running code isn't a
|
|
123
|
+
> restricted use case. But your account is governed by your usage, not by
|
|
124
|
+
> DevSpace. Don't point it at anything that would violate your provider's terms.
|
|
125
|
+
> Used normally, you're fine. (Based on OpenAI's Usage Policies and Service Terms
|
|
126
|
+
> as of June 2026.)
|
|
127
|
+
|
|
128
|
+
## What ChatGPT Can Do
|
|
129
|
+
|
|
130
|
+
Once connected, ChatGPT can open one of your approved project folders as a
|
|
131
|
+
workspace. From there, it can inspect the repo, make scoped edits, run commands,
|
|
132
|
+
and show you what changed.
|
|
133
|
+
|
|
134
|
+
DevSpace gives ChatGPT tools to:
|
|
135
|
+
|
|
136
|
+
- read, write, and edit files inside the opened workspace
|
|
137
|
+
- search code and inspect directories
|
|
138
|
+
- run shell commands for tests, builds, git, and package scripts
|
|
139
|
+
- use isolated Git worktrees for parallel coding sessions
|
|
140
|
+
- follow project instructions from `AGENTS.md` and `CLAUDE.md`
|
|
141
|
+
- discover local agent skills from your skill folders
|
|
142
|
+
- show tool cards and optional change summaries in ChatGPT Apps-compatible hosts
|
|
143
|
+
|
|
87
144
|
## Mental Model
|
|
88
145
|
|
|
89
146
|
DevSpace is remote access to selected local folders.
|
|
@@ -95,7 +152,7 @@ connected client like a trusted coding partner with access to your machine.
|
|
|
95
152
|
For a normal ChatGPT coding session:
|
|
96
153
|
|
|
97
154
|
1. Start your tunnel.
|
|
98
|
-
2. Run `
|
|
155
|
+
2. Run `devspace serve`.
|
|
99
156
|
3. Connect the MCP client to your public `/mcp` URL.
|
|
100
157
|
4. Approve the connection with the Owner password.
|
|
101
158
|
5. Ask ChatGPT to open a project inside one of your allowed roots.
|
|
@@ -115,16 +172,69 @@ shell.
|
|
|
115
172
|
Run this to inspect your local setup:
|
|
116
173
|
|
|
117
174
|
```bash
|
|
118
|
-
|
|
175
|
+
devspace doctor
|
|
119
176
|
```
|
|
120
177
|
|
|
121
178
|
## Documentation
|
|
122
179
|
|
|
123
|
-
- [Setup Guide](docs/setup.md)
|
|
124
|
-
- [ChatGPT Coding Workflow](docs/chatgpt-coding-workflow.md)
|
|
125
|
-
- [Configuration Reference](docs/configuration.md)
|
|
126
|
-
- [Security Model](docs/security.md)
|
|
127
|
-
- [Troubleshooting Gotchas](docs/gotchas.md)
|
|
180
|
+
- [Setup Guide](https://github.com/Waishnav/devspace/blob/main/docs/setup.md)
|
|
181
|
+
- [ChatGPT Coding Workflow](https://github.com/Waishnav/devspace/blob/main/docs/chatgpt-coding-workflow.md)
|
|
182
|
+
- [Configuration Reference](https://github.com/Waishnav/devspace/blob/main/docs/configuration.md)
|
|
183
|
+
- [Security Model](https://github.com/Waishnav/devspace/blob/main/docs/security.md)
|
|
184
|
+
- [Troubleshooting Gotchas](https://github.com/Waishnav/devspace/blob/main/docs/gotchas.md)
|
|
185
|
+
|
|
186
|
+
## Philosophy
|
|
187
|
+
|
|
188
|
+
Every piece of software is becoming conversational. Natural language is
|
|
189
|
+
redefining how we interact with tools, workflows, and systems.
|
|
190
|
+
|
|
191
|
+
My bet is that ChatGPT becomes the operating system for everything. Once we
|
|
192
|
+
reach AGI, we will simply talk to ChatGPT, and it will prompt, coordinate, and
|
|
193
|
+
orchestrate sub-agents that set up the right loops for us.
|
|
194
|
+
|
|
195
|
+
We are not there yet.
|
|
196
|
+
|
|
197
|
+
DevSpace is one attempt to fast-forward that future: a way for MCP-capable
|
|
198
|
+
hosts like ChatGPT and Claude to work directly with local project files through
|
|
199
|
+
explicit, inspectable tools.
|
|
200
|
+
|
|
201
|
+
## Built by Waishnav
|
|
202
|
+
|
|
203
|
+
I'm Waishnav, I like building opinionated products and tools, and DevSpace is one example of that.
|
|
204
|
+
This year, I started my journey to build a single-person and multiple-agents company doing multiple millions in
|
|
205
|
+
revenue. If you want to watch the failures, wins, lessons, and everything in
|
|
206
|
+
between, come hang out with me on [X](https://x.com/wshxnv).
|
|
207
|
+
|
|
208
|
+
## More from me
|
|
209
|
+
|
|
210
|
+
<table>
|
|
211
|
+
<thead>
|
|
212
|
+
<tr>
|
|
213
|
+
<th>Project</th>
|
|
214
|
+
<th>About</th>
|
|
215
|
+
</tr>
|
|
216
|
+
</thead>
|
|
217
|
+
<tbody>
|
|
218
|
+
<tr>
|
|
219
|
+
<td align="center" width="220">
|
|
220
|
+
<a href="https://gitcms.dev/">
|
|
221
|
+
<img
|
|
222
|
+
src="https://gitcms.dev/brand/gitcms-logo.svg"
|
|
223
|
+
alt="GitCMS"
|
|
224
|
+
width="48"
|
|
225
|
+
/><br />
|
|
226
|
+
<strong>GitCMS</strong>
|
|
227
|
+
</a>
|
|
228
|
+
</td>
|
|
229
|
+
<td>
|
|
230
|
+
<strong>Modern CMS and tooling for markdown based content sites — built for agents and humans.</strong><br><br>
|
|
231
|
+
Visual editing, editorial workflow, and ChatGPT/Claude content agents, with
|
|
232
|
+
every post and page stored as files in your repo.
|
|
233
|
+
<a href="https://gitcms.dev/">Learn more</a>.
|
|
234
|
+
</td>
|
|
235
|
+
</tr>
|
|
236
|
+
</tbody>
|
|
237
|
+
</table>
|
|
128
238
|
|
|
129
239
|
## Local Development
|
|
130
240
|
|
|
@@ -0,0 +1,399 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import { constants } from "node:fs";
|
|
3
|
+
import { access, mkdir, readFile, realpath, rename, rm, stat, writeFile } from "node:fs/promises";
|
|
4
|
+
import { dirname, isAbsolute, relative, resolve } from "node:path";
|
|
5
|
+
import { TextDecoder } from "node:util";
|
|
6
|
+
function patchError(message) {
|
|
7
|
+
return new Error(`Invalid patch: ${message}`);
|
|
8
|
+
}
|
|
9
|
+
export function parsePatch(patch) {
|
|
10
|
+
const lines = patchLines(patch);
|
|
11
|
+
if (lines.shift()?.trim() !== "*** Begin Patch") {
|
|
12
|
+
throw patchError("missing *** Begin Patch marker");
|
|
13
|
+
}
|
|
14
|
+
if (lines.pop()?.trim() !== "*** End Patch") {
|
|
15
|
+
throw patchError("missing *** End Patch marker");
|
|
16
|
+
}
|
|
17
|
+
const actions = [];
|
|
18
|
+
let index = 0;
|
|
19
|
+
while (index < lines.length) {
|
|
20
|
+
const header = lines[index++].trim();
|
|
21
|
+
if (header === "")
|
|
22
|
+
continue;
|
|
23
|
+
if (header.startsWith("*** Environment ID: ")) {
|
|
24
|
+
if (!header.slice("*** Environment ID: ".length).trim()) {
|
|
25
|
+
throw patchError("environment id cannot be empty");
|
|
26
|
+
}
|
|
27
|
+
continue;
|
|
28
|
+
}
|
|
29
|
+
if (header.startsWith("*** Add File: ")) {
|
|
30
|
+
const path = header.slice("*** Add File: ".length);
|
|
31
|
+
const content = [];
|
|
32
|
+
while (index < lines.length && !isTopLevelHeader(lines[index])) {
|
|
33
|
+
const line = lines[index++];
|
|
34
|
+
if (!line.startsWith("+")) {
|
|
35
|
+
throw patchError(`added file line must start with +: ${line}`);
|
|
36
|
+
}
|
|
37
|
+
content.push(line.slice(1));
|
|
38
|
+
}
|
|
39
|
+
if (content.length === 0)
|
|
40
|
+
throw patchError(`add file for ${path} has no content`);
|
|
41
|
+
actions.push({
|
|
42
|
+
kind: "add",
|
|
43
|
+
path,
|
|
44
|
+
content: `${content.join("\n")}\n`,
|
|
45
|
+
});
|
|
46
|
+
continue;
|
|
47
|
+
}
|
|
48
|
+
if (header.startsWith("*** Delete File: ")) {
|
|
49
|
+
actions.push({ kind: "delete", path: header.slice("*** Delete File: ".length) });
|
|
50
|
+
continue;
|
|
51
|
+
}
|
|
52
|
+
if (header.startsWith("*** Update File: ")) {
|
|
53
|
+
const path = header.slice("*** Update File: ".length);
|
|
54
|
+
let moveTo;
|
|
55
|
+
const hunks = [];
|
|
56
|
+
if (lines[index]?.trim().startsWith("*** Move to: ")) {
|
|
57
|
+
moveTo = lines[index++].trim().slice("*** Move to: ".length);
|
|
58
|
+
}
|
|
59
|
+
let current;
|
|
60
|
+
const finishCurrent = () => {
|
|
61
|
+
if (!current)
|
|
62
|
+
return;
|
|
63
|
+
if (current.lines.length === 0)
|
|
64
|
+
throw patchError(`empty update hunk for ${path}`);
|
|
65
|
+
hunks.push(current);
|
|
66
|
+
current = undefined;
|
|
67
|
+
};
|
|
68
|
+
while (index < lines.length) {
|
|
69
|
+
const line = lines[index];
|
|
70
|
+
const trimmed = line.trim();
|
|
71
|
+
if (!current && trimmed === "") {
|
|
72
|
+
index++;
|
|
73
|
+
continue;
|
|
74
|
+
}
|
|
75
|
+
if (trimmed === "*** End of File") {
|
|
76
|
+
if (!current)
|
|
77
|
+
throw patchError(`end-of-file marker without update hunk for ${path}`);
|
|
78
|
+
current.endOfFile = true;
|
|
79
|
+
index++;
|
|
80
|
+
continue;
|
|
81
|
+
}
|
|
82
|
+
if ((!current || !line.startsWith(" ")) && isTopLevelHeader(line))
|
|
83
|
+
break;
|
|
84
|
+
if (trimmed.startsWith("@@") && !line.startsWith(" ")) {
|
|
85
|
+
finishCurrent();
|
|
86
|
+
const changeContext = trimmed.slice(2).trim();
|
|
87
|
+
current = { lines: [], changeContext: changeContext || undefined };
|
|
88
|
+
index++;
|
|
89
|
+
continue;
|
|
90
|
+
}
|
|
91
|
+
current ??= { lines: [] };
|
|
92
|
+
index++;
|
|
93
|
+
if (line.startsWith(" "))
|
|
94
|
+
current.lines.push({ kind: "context", text: line.slice(1) });
|
|
95
|
+
else if (line.startsWith("+"))
|
|
96
|
+
current.lines.push({ kind: "add", text: line.slice(1) });
|
|
97
|
+
else if (line.startsWith("-"))
|
|
98
|
+
current.lines.push({ kind: "remove", text: line.slice(1) });
|
|
99
|
+
else if (line === "\")
|
|
100
|
+
continue;
|
|
101
|
+
else
|
|
102
|
+
throw patchError(`hunk line must start with space, +, or -: ${line}`);
|
|
103
|
+
}
|
|
104
|
+
finishCurrent();
|
|
105
|
+
if (hunks.length === 0 && !moveTo) {
|
|
106
|
+
throw patchError(`update for ${path} has no hunks or move destination`);
|
|
107
|
+
}
|
|
108
|
+
actions.push({ kind: "update", path, moveTo, hunks });
|
|
109
|
+
continue;
|
|
110
|
+
}
|
|
111
|
+
throw patchError(`unknown action header: ${header}`);
|
|
112
|
+
}
|
|
113
|
+
if (actions.length === 0)
|
|
114
|
+
throw patchError("contains no file actions");
|
|
115
|
+
return actions;
|
|
116
|
+
}
|
|
117
|
+
function patchLines(patch) {
|
|
118
|
+
let lines = patch.replace(/\r\n/g, "\n").trim().split("\n");
|
|
119
|
+
const first = lines[0]?.trim();
|
|
120
|
+
const last = lines.at(-1)?.trim();
|
|
121
|
+
if ((first === "<<EOF" || first === "<<'EOF'" || first === '<<"EOF"') &&
|
|
122
|
+
last?.endsWith("EOF") &&
|
|
123
|
+
lines.length >= 4) {
|
|
124
|
+
lines = lines.slice(1, -1);
|
|
125
|
+
}
|
|
126
|
+
return lines;
|
|
127
|
+
}
|
|
128
|
+
function isTopLevelHeader(line) {
|
|
129
|
+
const trimmed = line.trim();
|
|
130
|
+
return (trimmed.startsWith("*** Add File: ") ||
|
|
131
|
+
trimmed.startsWith("*** Delete File: ") ||
|
|
132
|
+
trimmed.startsWith("*** Update File: ") ||
|
|
133
|
+
trimmed.startsWith("*** Environment ID: "));
|
|
134
|
+
}
|
|
135
|
+
function isInside(root, path) {
|
|
136
|
+
const rel = relative(root, path);
|
|
137
|
+
return rel === "" || (!rel.startsWith("..") && !isAbsolute(rel));
|
|
138
|
+
}
|
|
139
|
+
async function resolveConfinedPath(root, input) {
|
|
140
|
+
if (!input || input.includes("\0") || isAbsolute(input)) {
|
|
141
|
+
throw patchError(`path must be relative to the workspace: ${input}`);
|
|
142
|
+
}
|
|
143
|
+
const rootPath = await realpath(root);
|
|
144
|
+
const target = resolve(rootPath, input);
|
|
145
|
+
if (!isInside(rootPath, target)) {
|
|
146
|
+
throw patchError(`path escapes the workspace: ${input}`);
|
|
147
|
+
}
|
|
148
|
+
let existing = target;
|
|
149
|
+
while (true) {
|
|
150
|
+
try {
|
|
151
|
+
const resolved = await realpath(existing);
|
|
152
|
+
if (!isInside(rootPath, resolved)) {
|
|
153
|
+
throw patchError(`path resolves outside the workspace: ${input}`);
|
|
154
|
+
}
|
|
155
|
+
break;
|
|
156
|
+
}
|
|
157
|
+
catch (error) {
|
|
158
|
+
const code = error.code;
|
|
159
|
+
if (code !== "ENOENT")
|
|
160
|
+
throw error;
|
|
161
|
+
const parent = dirname(existing);
|
|
162
|
+
if (parent === existing)
|
|
163
|
+
throw error;
|
|
164
|
+
existing = parent;
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
return target;
|
|
168
|
+
}
|
|
169
|
+
function splitFile(content) {
|
|
170
|
+
const eol = content.includes("\r\n") ? "\r\n" : "\n";
|
|
171
|
+
const normalized = content.replace(/\r\n/g, "\n");
|
|
172
|
+
const finalNewline = normalized.endsWith("\n");
|
|
173
|
+
const lines = normalized.split("\n");
|
|
174
|
+
if (finalNewline)
|
|
175
|
+
lines.pop();
|
|
176
|
+
return { lines, eol, finalNewline };
|
|
177
|
+
}
|
|
178
|
+
function findSequence(haystack, needle, from, endOfFile = false) {
|
|
179
|
+
if (needle.length === 0)
|
|
180
|
+
return from;
|
|
181
|
+
const matchAt = (index, normalize) => needle.every((line, offset) => normalize(haystack[index + offset] ?? "") === normalize(line));
|
|
182
|
+
for (const normalize of [
|
|
183
|
+
(value) => value,
|
|
184
|
+
(value) => value.trimEnd(),
|
|
185
|
+
(value) => value.trim(),
|
|
186
|
+
]) {
|
|
187
|
+
const start = endOfFile ? haystack.length - needle.length : from;
|
|
188
|
+
const end = haystack.length - needle.length;
|
|
189
|
+
for (let index = start; index <= end; index += 1) {
|
|
190
|
+
if (index >= from && matchAt(index, normalize))
|
|
191
|
+
return index;
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
return -1;
|
|
195
|
+
}
|
|
196
|
+
function applyHunks(path, content, hunks) {
|
|
197
|
+
const file = splitFile(content);
|
|
198
|
+
const lines = [...file.lines];
|
|
199
|
+
let cursor = 0;
|
|
200
|
+
for (const hunk of hunks) {
|
|
201
|
+
if (hunk.changeContext) {
|
|
202
|
+
const contextIndex = findSequence(lines, [hunk.changeContext], cursor);
|
|
203
|
+
if (contextIndex < 0) {
|
|
204
|
+
throw patchError(`could not find hunk context in ${path}: ${hunk.changeContext}`);
|
|
205
|
+
}
|
|
206
|
+
cursor = contextIndex + 1;
|
|
207
|
+
}
|
|
208
|
+
const oldLines = hunk.lines
|
|
209
|
+
.filter((line) => line.kind !== "add")
|
|
210
|
+
.map((line) => line.text);
|
|
211
|
+
const newLines = hunk.lines
|
|
212
|
+
.filter((line) => line.kind !== "remove")
|
|
213
|
+
.map((line) => line.text);
|
|
214
|
+
const index = hunk.endOfFile && oldLines.length === 0
|
|
215
|
+
? lines.length
|
|
216
|
+
: findSequence(lines, oldLines, cursor, hunk.endOfFile);
|
|
217
|
+
if (index < 0) {
|
|
218
|
+
const preview = oldLines.slice(0, 3).join("\n");
|
|
219
|
+
throw patchError(`could not find hunk context in ${path}: ${preview}`);
|
|
220
|
+
}
|
|
221
|
+
lines.splice(index, oldLines.length, ...newLines);
|
|
222
|
+
cursor = index + newLines.length;
|
|
223
|
+
}
|
|
224
|
+
const normalized = `${lines.join("\n")}\n`;
|
|
225
|
+
return file.eol === "\r\n" ? normalized.replace(/\n/g, "\r\n") : normalized;
|
|
226
|
+
}
|
|
227
|
+
async function fileExists(path) {
|
|
228
|
+
try {
|
|
229
|
+
await access(path, constants.F_OK);
|
|
230
|
+
return true;
|
|
231
|
+
}
|
|
232
|
+
catch {
|
|
233
|
+
return false;
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
export async function replaceFile(temporary, destination, destinationExists, platform = process.platform) {
|
|
237
|
+
if (platform !== "win32" || !destinationExists) {
|
|
238
|
+
await rename(temporary, destination);
|
|
239
|
+
return;
|
|
240
|
+
}
|
|
241
|
+
const backup = `${temporary}.original`;
|
|
242
|
+
await rename(destination, backup);
|
|
243
|
+
try {
|
|
244
|
+
await rename(temporary, destination);
|
|
245
|
+
}
|
|
246
|
+
catch (error) {
|
|
247
|
+
await rename(backup, destination);
|
|
248
|
+
throw error;
|
|
249
|
+
}
|
|
250
|
+
await rm(backup, { force: true });
|
|
251
|
+
}
|
|
252
|
+
export async function applyPatch(root, patch) {
|
|
253
|
+
const actions = parsePatch(patch);
|
|
254
|
+
const results = [];
|
|
255
|
+
const patches = [];
|
|
256
|
+
for (const action of actions) {
|
|
257
|
+
if (action.kind === "add") {
|
|
258
|
+
const absolute = await resolveConfinedPath(root, action.path);
|
|
259
|
+
const original = await readOptionalTextFile(absolute, action.path);
|
|
260
|
+
await writeTextFile(absolute, action.content, original?.mode);
|
|
261
|
+
patches.push(unifiedFilePatch(action.path, action.path, original?.content ?? null, action.content));
|
|
262
|
+
results.push({ path: action.path, operation: "add" });
|
|
263
|
+
continue;
|
|
264
|
+
}
|
|
265
|
+
const absolute = await resolveConfinedPath(root, action.path);
|
|
266
|
+
const file = await readRequiredTextFile(absolute, action.path);
|
|
267
|
+
if (action.kind === "delete") {
|
|
268
|
+
await rm(absolute);
|
|
269
|
+
patches.push(unifiedFilePatch(action.path, action.path, file.content, null));
|
|
270
|
+
results.push({ path: action.path, operation: "delete" });
|
|
271
|
+
continue;
|
|
272
|
+
}
|
|
273
|
+
const updated = applyHunks(action.path, file.content, action.hunks);
|
|
274
|
+
if (action.moveTo) {
|
|
275
|
+
const destination = await resolveConfinedPath(root, action.moveTo);
|
|
276
|
+
if (destination !== absolute)
|
|
277
|
+
await readOptionalTextFile(destination, action.moveTo);
|
|
278
|
+
await writeTextFile(destination, updated, file.mode);
|
|
279
|
+
if (destination !== absolute)
|
|
280
|
+
await rm(absolute);
|
|
281
|
+
patches.push(unifiedFilePatch(action.path, action.moveTo, file.content, updated));
|
|
282
|
+
results.push({ path: action.moveTo, previousPath: action.path, operation: "move" });
|
|
283
|
+
}
|
|
284
|
+
else {
|
|
285
|
+
await writeTextFile(absolute, updated, file.mode);
|
|
286
|
+
patches.push(unifiedFilePatch(action.path, action.path, file.content, updated));
|
|
287
|
+
results.push({ path: action.path, operation: "update" });
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
const unifiedPatch = patches.filter(Boolean).join("\n");
|
|
291
|
+
const stats = countPatchStats(unifiedPatch);
|
|
292
|
+
return { files: results, patch: unifiedPatch, ...stats };
|
|
293
|
+
}
|
|
294
|
+
async function readRequiredTextFile(absolute, displayPath) {
|
|
295
|
+
if (!(await fileExists(absolute)))
|
|
296
|
+
throw patchError(`file does not exist: ${displayPath}`);
|
|
297
|
+
const metadata = await stat(absolute);
|
|
298
|
+
if (!metadata.isFile())
|
|
299
|
+
throw patchError(`path is not a regular file: ${displayPath}`);
|
|
300
|
+
return { content: await readUtf8Text(absolute, displayPath), mode: metadata.mode };
|
|
301
|
+
}
|
|
302
|
+
async function readOptionalTextFile(absolute, displayPath) {
|
|
303
|
+
if (!(await fileExists(absolute)))
|
|
304
|
+
return null;
|
|
305
|
+
const metadata = await stat(absolute);
|
|
306
|
+
if (!metadata.isFile())
|
|
307
|
+
throw patchError(`path is not a regular file: ${displayPath}`);
|
|
308
|
+
return { content: await readUtf8Text(absolute, displayPath), mode: metadata.mode };
|
|
309
|
+
}
|
|
310
|
+
async function readUtf8Text(absolute, displayPath) {
|
|
311
|
+
const bytes = await readFile(absolute);
|
|
312
|
+
let content;
|
|
313
|
+
try {
|
|
314
|
+
content = new TextDecoder("utf-8", { fatal: true }).decode(bytes);
|
|
315
|
+
}
|
|
316
|
+
catch {
|
|
317
|
+
throw patchError(`file is not valid UTF-8 text: ${displayPath}`);
|
|
318
|
+
}
|
|
319
|
+
if (content.includes("\0"))
|
|
320
|
+
throw patchError(`file appears to be binary: ${displayPath}`);
|
|
321
|
+
return content;
|
|
322
|
+
}
|
|
323
|
+
async function writeTextFile(destination, content, mode) {
|
|
324
|
+
await mkdir(dirname(destination), { recursive: true });
|
|
325
|
+
const temporary = `${destination}.devspace-patch-${process.pid}-${randomUUID()}`;
|
|
326
|
+
try {
|
|
327
|
+
await writeFile(temporary, content, mode === undefined ? undefined : { mode });
|
|
328
|
+
await replaceFile(temporary, destination, await fileExists(destination));
|
|
329
|
+
}
|
|
330
|
+
catch (error) {
|
|
331
|
+
await rm(temporary, { force: true });
|
|
332
|
+
throw error;
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
function fileLines(content) {
|
|
336
|
+
if (content.length === 0)
|
|
337
|
+
return [];
|
|
338
|
+
const normalized = content.replace(/\r\n/g, "\n");
|
|
339
|
+
const lines = normalized.split("\n");
|
|
340
|
+
if (normalized.endsWith("\n"))
|
|
341
|
+
lines.pop();
|
|
342
|
+
return lines;
|
|
343
|
+
}
|
|
344
|
+
function hunkRange(start, count) {
|
|
345
|
+
return count === 0 ? "0,0" : `${start},${count}`;
|
|
346
|
+
}
|
|
347
|
+
function unifiedFilePatch(oldPath, newPath, oldContent, newContent) {
|
|
348
|
+
const oldLines = fileLines(oldContent ?? "");
|
|
349
|
+
const newLines = fileLines(newContent ?? "");
|
|
350
|
+
let prefix = 0;
|
|
351
|
+
while (prefix < oldLines.length &&
|
|
352
|
+
prefix < newLines.length &&
|
|
353
|
+
oldLines[prefix] === newLines[prefix]) {
|
|
354
|
+
prefix += 1;
|
|
355
|
+
}
|
|
356
|
+
let suffix = 0;
|
|
357
|
+
while (suffix < oldLines.length - prefix &&
|
|
358
|
+
suffix < newLines.length - prefix &&
|
|
359
|
+
oldLines[oldLines.length - 1 - suffix] === newLines[newLines.length - 1 - suffix]) {
|
|
360
|
+
suffix += 1;
|
|
361
|
+
}
|
|
362
|
+
const contextBefore = Math.min(3, prefix);
|
|
363
|
+
const contextAfter = Math.min(3, suffix);
|
|
364
|
+
const oldChanged = oldLines.slice(prefix, oldLines.length - suffix);
|
|
365
|
+
const newChanged = newLines.slice(prefix, newLines.length - suffix);
|
|
366
|
+
const before = oldLines.slice(prefix - contextBefore, prefix);
|
|
367
|
+
const after = oldLines.slice(oldLines.length - suffix, oldLines.length - suffix + contextAfter);
|
|
368
|
+
const oldCount = contextBefore + oldChanged.length + contextAfter;
|
|
369
|
+
const newCount = contextBefore + newChanged.length + contextAfter;
|
|
370
|
+
const oldStart = oldContent === null ? 0 : prefix - contextBefore + 1;
|
|
371
|
+
const newStart = newContent === null ? 0 : prefix - contextBefore + 1;
|
|
372
|
+
const displayOld = oldContent === null ? "/dev/null" : `a/${oldPath}`;
|
|
373
|
+
const displayNew = newContent === null ? "/dev/null" : `b/${newPath}`;
|
|
374
|
+
return [
|
|
375
|
+
`diff --git a/${oldPath} b/${newPath}`,
|
|
376
|
+
oldContent === null ? "new file mode 100644" : undefined,
|
|
377
|
+
newContent === null ? "deleted file mode 100644" : undefined,
|
|
378
|
+
`--- ${displayOld}`,
|
|
379
|
+
`+++ ${displayNew}`,
|
|
380
|
+
`@@ -${hunkRange(oldStart, oldCount)} +${hunkRange(newStart, newCount)} @@`,
|
|
381
|
+
...before.map((line) => ` ${line}`),
|
|
382
|
+
...oldChanged.map((line) => `-${line}`),
|
|
383
|
+
...newChanged.map((line) => `+${line}`),
|
|
384
|
+
...after.map((line) => ` ${line}`),
|
|
385
|
+
]
|
|
386
|
+
.filter((line) => line !== undefined)
|
|
387
|
+
.join("\n");
|
|
388
|
+
}
|
|
389
|
+
function countPatchStats(patch) {
|
|
390
|
+
let additions = 0;
|
|
391
|
+
let removals = 0;
|
|
392
|
+
for (const line of patch.split("\n")) {
|
|
393
|
+
if (line.startsWith("+") && !line.startsWith("+++"))
|
|
394
|
+
additions += 1;
|
|
395
|
+
if (line.startsWith("-") && !line.startsWith("---"))
|
|
396
|
+
removals += 1;
|
|
397
|
+
}
|
|
398
|
+
return { additions, removals };
|
|
399
|
+
}
|
package/dist/cli.js
CHANGED
|
@@ -31,6 +31,9 @@ async function main(argv) {
|
|
|
31
31
|
case "help":
|
|
32
32
|
printHelp();
|
|
33
33
|
return;
|
|
34
|
+
case "version":
|
|
35
|
+
printVersion();
|
|
36
|
+
return;
|
|
34
37
|
}
|
|
35
38
|
}
|
|
36
39
|
function normalizeCommand(command) {
|
|
@@ -40,6 +43,8 @@ function normalizeCommand(command) {
|
|
|
40
43
|
return command;
|
|
41
44
|
if (command === "help" || command === "--help" || command === "-h")
|
|
42
45
|
return "help";
|
|
46
|
+
if (command === "version" || command === "--version" || command === "-v")
|
|
47
|
+
return "version";
|
|
43
48
|
throw new Error(`Unknown command: ${command}`);
|
|
44
49
|
}
|
|
45
50
|
async function ensureConfigured() {
|
|
@@ -149,7 +154,7 @@ async function serve() {
|
|
|
149
154
|
}
|
|
150
155
|
const { createServer } = await import("./server.js");
|
|
151
156
|
const config = loadConfig();
|
|
152
|
-
const { app } = createServer(config);
|
|
157
|
+
const { app, close } = createServer(config);
|
|
153
158
|
const httpServer = app.listen(config.port, config.host, () => {
|
|
154
159
|
console.log(`devspace listening on http://${config.host}:${config.port}/mcp`);
|
|
155
160
|
console.log(`public base url: ${config.publicBaseUrl}`);
|
|
@@ -162,7 +167,10 @@ async function serve() {
|
|
|
162
167
|
console.log(`logging: ${config.logging.level} ${config.logging.format}`);
|
|
163
168
|
});
|
|
164
169
|
const shutdown = () => {
|
|
165
|
-
httpServer.close(() =>
|
|
170
|
+
httpServer.close(() => {
|
|
171
|
+
close();
|
|
172
|
+
process.exit(0);
|
|
173
|
+
});
|
|
166
174
|
};
|
|
167
175
|
process.once("SIGINT", shutdown);
|
|
168
176
|
process.once("SIGTERM", shutdown);
|
|
@@ -223,11 +231,19 @@ function printHelp() {
|
|
|
223
231
|
" devspace doctor Show config, runtime, and native dependency status",
|
|
224
232
|
" devspace config get Print persisted config",
|
|
225
233
|
" devspace config set publicBaseUrl <url|null>",
|
|
234
|
+
" devspace -v, --version Print the installed version",
|
|
226
235
|
"",
|
|
227
236
|
"For temporary tunnels:",
|
|
228
237
|
" DEVSPACE_PUBLIC_BASE_URL=https://example.trycloudflare.com devspace serve",
|
|
229
238
|
].join("\n"));
|
|
230
239
|
}
|
|
240
|
+
function printVersion() {
|
|
241
|
+
const packageJson = require("../package.json");
|
|
242
|
+
if (typeof packageJson.version !== "string") {
|
|
243
|
+
throw new Error("Unable to read DevSpace package version.");
|
|
244
|
+
}
|
|
245
|
+
console.log(packageJson.version);
|
|
246
|
+
}
|
|
231
247
|
function normalizeOptionalPublicBaseUrl(value) {
|
|
232
248
|
const trimmed = value.trim();
|
|
233
249
|
if (!trimmed || trimmed === "null" || trimmed === "none")
|