@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.
Files changed (102) hide show
  1. package/README.md +140 -30
  2. package/dist/apply-patch.js +399 -0
  3. package/dist/cli.js +18 -2
  4. package/dist/config.js +11 -13
  5. package/dist/db/client.js +10 -3
  6. package/dist/db/migrations.js +123 -0
  7. package/dist/db/schema.js +24 -1
  8. package/dist/oauth-provider.js +35 -64
  9. package/dist/oauth-store.js +138 -0
  10. package/dist/process-platform.js +49 -0
  11. package/dist/process-sessions.js +329 -0
  12. package/dist/roots.js +5 -2
  13. package/dist/server.js +462 -203
  14. package/dist/skills.js +23 -3
  15. package/dist/ui/.vite/manifest.json +281 -281
  16. package/dist/ui/assets/{angular-html-B_77pPm_.js → angular-html-D6nc7PYw.js} +1 -1
  17. package/dist/ui/assets/{angular-ts-tGBi7sOr.js → angular-ts-BAIStLeI.js} +1 -1
  18. package/dist/ui/assets/{apl-CtVPpGgP.js → apl-CtEv5pop.js} +1 -1
  19. package/dist/ui/assets/{astro-CALPYvnG.js → astro-CVFIMXc6.js} +1 -1
  20. package/dist/ui/assets/{blade-Bh4eBXdk.js → blade-BgSlZ6jY.js} +1 -1
  21. package/dist/ui/assets/{c-BGkebjQE.js → c-bxLlCV26.js} +1 -1
  22. package/dist/ui/assets/{cobol-f7rP2PjR.js → cobol-BuZ-r51r.js} +1 -1
  23. package/dist/ui/assets/{coffee-E4OhMc7W.js → coffee-_JP0Joa7.js} +1 -1
  24. package/dist/ui/assets/{cpp-Cr_J6lB2.js → cpp-CP_eaz-H.js} +1 -1
  25. package/dist/ui/assets/{crystal-B_TFXhon.js → crystal-DZ-sEZd0.js} +1 -1
  26. package/dist/ui/assets/{css-DDMxq-4J.js → css-CZnyti9s.js} +1 -1
  27. package/dist/ui/assets/{edge-Bxqhgg5I.js → edge-DjsoDMTR.js} +1 -1
  28. package/dist/ui/assets/{elixir-WrSbgJJ1.js → elixir-C5b04AkY.js} +1 -1
  29. package/dist/ui/assets/{elm-BXBWJ4Kz.js → elm-Ddx-rgVY.js} +1 -1
  30. package/dist/ui/assets/{erb-D0iMw9rO.js → erb-cjF7oPcT.js} +1 -1
  31. package/dist/ui/assets/{git-rebase-BOrZ-_iR.js → git-rebase-DN5exBTl.js} +1 -1
  32. package/dist/ui/assets/{glimmer-js-PCWk96O8.js → glimmer-js-CEq0hNwa.js} +1 -1
  33. package/dist/ui/assets/{glimmer-ts-4pYwX8pC.js → glimmer-ts-4uYGFkon.js} +1 -1
  34. package/dist/ui/assets/{glsl-CDyKZvZX.js → glsl-DEJ9tTWm.js} +1 -1
  35. package/dist/ui/assets/{graphql-8sIw92YT.js → graphql-Bim5WuQh.js} +1 -1
  36. package/dist/ui/assets/{hack-Ci96Zcew.js → hack-BcyVIXhE.js} +1 -1
  37. package/dist/ui/assets/{haml-4uzh6End.js → haml-D9YaukDg.js} +1 -1
  38. package/dist/ui/assets/{handlebars-B6Eq7QV8.js → handlebars-CFaMw9zH.js} +1 -1
  39. package/dist/ui/assets/{heavy-payload--bwRtRpo.js → heavy-payload-B9aTqgfs.js} +1 -1
  40. package/dist/ui/assets/{html-C29wL7bm.js → html-CgO_zx4D.js} +1 -1
  41. package/dist/ui/assets/{html-derivative-Cmd90ilz.js → html-derivative-BxqkysJT.js} +1 -1
  42. package/dist/ui/assets/{http-Bdr_R0Kw.js → http-CO5dv0RA.js} +1 -1
  43. package/dist/ui/assets/{hurl-CiTPn-zH.js → hurl-DRHtrK78.js} +1 -1
  44. package/dist/ui/assets/{java-C03kBuJH.js → java-BvHC_DgR.js} +1 -1
  45. package/dist/ui/assets/{javascript-135g9Wwx.js → javascript-C-3payQx.js} +1 -1
  46. package/dist/ui/assets/{jinja-CPpiEHqV.js → jinja-CAOPvEOH.js} +1 -1
  47. package/dist/ui/assets/{jison-qabCQzwp.js → jison-totCB1L-.js} +1 -1
  48. package/dist/ui/assets/{json-CtRj9Trp.js → json-D0J9rEe1.js} +1 -1
  49. package/dist/ui/assets/{jsx-Ccuy5Wm3.js → jsx-na-XQD_B.js} +1 -1
  50. package/dist/ui/assets/{julia-Fwya-MwM.js → julia-CqrTtZ4-.js} +1 -1
  51. package/dist/ui/assets/{just-c9YEH7Gf.js → just-pmrVSkMc.js} +1 -1
  52. package/dist/ui/assets/{latex-D9xoD6UX.js → latex-Cd_geX3E.js} +1 -1
  53. package/dist/ui/assets/{liquid-BKZLZSHN.js → liquid-C6ky5sJ-.js} +1 -1
  54. package/dist/ui/assets/{lua-CBD2zNyC.js → lua-dgyBbeof.js} +1 -1
  55. package/dist/ui/assets/{marko-Dkgf9bj3.js → marko-Dy5mfXY9.js} +1 -1
  56. package/dist/ui/assets/{mdc-BUgUKiX3.js → mdc-7NUvxgI1.js} +1 -1
  57. package/dist/ui/assets/{nginx-CAaav4Zh.js → nginx-C-KiEXbI.js} +1 -1
  58. package/dist/ui/assets/{nim-CTjWWztL.js → nim-BBw8kfl9.js} +1 -1
  59. package/dist/ui/assets/{perl-DTM5zgH4.js → perl-CyxhJgbO.js} +1 -1
  60. package/dist/ui/assets/{php-dqfFDbWm.js → php-m4LAOpam.js} +1 -1
  61. package/dist/ui/assets/{pug-4f_clTm2.js → pug-Cpl0baYI.js} +1 -1
  62. package/dist/ui/assets/{qml-BFeWjFRL.js → qml-BoSGNgIb.js} +1 -1
  63. package/dist/ui/assets/{r-ei1iNvHm.js → r-C0JhqQd5.js} +1 -1
  64. package/dist/ui/assets/{razor-jj95jncJ.js → razor-CntsBNoh.js} +1 -1
  65. package/dist/ui/assets/{regexp-0joUf3Cn.js → regexp-BNZmOCRl.js} +1 -1
  66. package/dist/ui/assets/{review-payload-DxI2fTnP.js → review-payload-BgdWHWiv.js} +1 -1
  67. package/dist/ui/assets/{rst-BMbcN8--.js → rst-BZ2XHIxM.js} +1 -1
  68. package/dist/ui/assets/{ruby-BQx0xtOB.js → ruby-BIthfCTP.js} +1 -1
  69. package/dist/ui/assets/{sas-C_Fl1EyP.js → sas-5aT3fOla.js} +1 -1
  70. package/dist/ui/assets/{scss-5vaLJPJl.js → scss-DZPfdkDy.js} +1 -1
  71. package/dist/ui/assets/{shellscript-p_y7YCYP.js → shellscript-DbhKc5Xj.js} +1 -1
  72. package/dist/ui/assets/{shellsession-ChZkMp2K.js → shellsession-X9-bcyHh.js} +1 -1
  73. package/dist/ui/assets/{soy-im25cA8U.js → soy-Bd4tecrW.js} +1 -1
  74. package/dist/ui/assets/{sql-BzvG6ssm.js → sql-Chf5JrDn.js} +1 -1
  75. package/dist/ui/assets/{stata-CwSzNM20.js → stata-DvMMIc_T.js} +1 -1
  76. package/dist/ui/assets/{surrealql-BWzKhI6S.js → surrealql-D5FMzVF5.js} +1 -1
  77. package/dist/ui/assets/{svelte-NMlQHjaa.js → svelte-CTiVXZrv.js} +1 -1
  78. package/dist/ui/assets/{templ-1W11KGSL.js → templ-ldnfq-7w.js} +1 -1
  79. package/dist/ui/assets/{tex-CTxN8q2c.js → tex-QW0_7T2r.js} +1 -1
  80. package/dist/ui/assets/{ts-tags-B6prphu-.js → ts-tags-CAKiWIBx.js} +1 -1
  81. package/dist/ui/assets/{tsx-Daks24y9.js → tsx-Bu9xJJmq.js} +1 -1
  82. package/dist/ui/assets/{twig-C0KeCxch.js → twig-Dp1752_V.js} +1 -1
  83. package/dist/ui/assets/{typescript-37-dkCwp.js → typescript-RfO03Kmm.js} +1 -1
  84. package/dist/ui/assets/{useFileDiffInstance--R0_ywL_.js → useFileDiffInstance-DjqU4y1a.js} +3 -3
  85. package/dist/ui/assets/{vue-Ba-v02vP.js → vue-Dlasv57G.js} +1 -1
  86. package/dist/ui/assets/{vue-html-D2UmbuN7.js → vue-html-BHuJ7anu.js} +1 -1
  87. package/dist/ui/assets/{vue-vine-ZftUW_ID.js → vue-vine-CavE0PSk.js} +1 -1
  88. package/dist/ui/assets/{workspace-app-CsMiGtz9.js → workspace-app-DDV_qdjx.js} +6 -6
  89. package/dist/ui/assets/workspace-app-DGD9R89D.css +1 -0
  90. package/dist/ui/assets/{xml-DrhRYxTS.js → xml-Bbv64l4W.js} +1 -1
  91. package/dist/ui/assets/{xsl-DvbIvBcQ.js → xsl-Sro4gyQY.js} +1 -1
  92. package/dist/ui/assets/{yaml-oktUYAzQ.js → yaml-Du68c8sA.js} +1 -1
  93. package/dist/ui/workspace-app.html +2 -2
  94. package/dist/workspace-store.js +0 -50
  95. package/docs/chatgpt-coding-workflow.md +24 -4
  96. package/docs/codex-tool-mode-qa.md +73 -0
  97. package/docs/configuration.md +28 -5
  98. package/docs/gotchas.md +11 -5
  99. package/docs/setup.md +1 -1
  100. package/package.json +10 -5
  101. package/scripts/fix-node-pty-permissions.mjs +22 -0
  102. 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
- [![DevSpace connected to ChatGPT](docs/assets/devspace-screenshot.png)](docs/assets/devspace-screenshot.png)
17
+ [![DevSpace connected to ChatGPT](https://raw.githubusercontent.com/Waishnav/devspace/main/docs/assets/devspace-screenshot.png)](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
- ## Quick Start
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
- DevSpace requires Node `>=20.12 <27`. Node 22 LTS is recommended.
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
- Then configure your MCP client with:
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
- ## What ChatGPT Gets
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 `npx @waishnav/devspace serve`.
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
- npx @waishnav/devspace doctor
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(() => process.exit(0));
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")