@waishnav/devspace 1.0.2 → 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 +43 -7
- package/dist/apply-patch.js +399 -0
- package/dist/cli.js +13 -0
- package/dist/config.js +11 -13
- package/dist/process-platform.js +49 -0
- package/dist/process-sessions.js +329 -0
- package/dist/roots.js +5 -2
- package/dist/server.js +439 -199
- package/dist/skills.js +23 -3
- package/dist/ui/.vite/manifest.json +280 -280
- package/dist/ui/assets/{angular-html-Dj4LysSE.js → angular-html-D6nc7PYw.js} +1 -1
- package/dist/ui/assets/{angular-ts-CxuC_FFh.js → angular-ts-BAIStLeI.js} +1 -1
- package/dist/ui/assets/{apl-RwuvNFFo.js → apl-CtEv5pop.js} +1 -1
- package/dist/ui/assets/{astro-ncoaUbsO.js → astro-CVFIMXc6.js} +1 -1
- package/dist/ui/assets/{blade-Dw8-Lzux.js → blade-BgSlZ6jY.js} +1 -1
- package/dist/ui/assets/{c-Dpbbo_f2.js → c-bxLlCV26.js} +1 -1
- package/dist/ui/assets/{cobol-4ddX3THy.js → cobol-BuZ-r51r.js} +1 -1
- package/dist/ui/assets/{coffee-Bn-a5KqK.js → coffee-_JP0Joa7.js} +1 -1
- package/dist/ui/assets/{cpp-DoRrlM_T.js → cpp-CP_eaz-H.js} +1 -1
- package/dist/ui/assets/{crystal-DiZfCxn-.js → crystal-DZ-sEZd0.js} +1 -1
- package/dist/ui/assets/{css-DiQoSjDH.js → css-CZnyti9s.js} +1 -1
- package/dist/ui/assets/{edge-wEhCUoEt.js → edge-DjsoDMTR.js} +1 -1
- package/dist/ui/assets/{elixir-C3obhm0H.js → elixir-C5b04AkY.js} +1 -1
- package/dist/ui/assets/{elm-DyTBfh1L.js → elm-Ddx-rgVY.js} +1 -1
- package/dist/ui/assets/{erb-f4TnXRuY.js → erb-cjF7oPcT.js} +1 -1
- package/dist/ui/assets/{git-rebase-vhZbWNfp.js → git-rebase-DN5exBTl.js} +1 -1
- package/dist/ui/assets/{glimmer-js-BG7puL6Y.js → glimmer-js-CEq0hNwa.js} +1 -1
- package/dist/ui/assets/{glimmer-ts-CNCfLLL1.js → glimmer-ts-4uYGFkon.js} +1 -1
- package/dist/ui/assets/{glsl-DhT76AL5.js → glsl-DEJ9tTWm.js} +1 -1
- package/dist/ui/assets/{graphql-CJX08w8y.js → graphql-Bim5WuQh.js} +1 -1
- package/dist/ui/assets/{hack-DY4NGCCL.js → hack-BcyVIXhE.js} +1 -1
- package/dist/ui/assets/{haml-DwSkmVcG.js → haml-D9YaukDg.js} +1 -1
- package/dist/ui/assets/{handlebars-CQ_1dEmi.js → handlebars-CFaMw9zH.js} +1 -1
- package/dist/ui/assets/{heavy-payload-2rGBiitA.js → heavy-payload-B9aTqgfs.js} +1 -1
- package/dist/ui/assets/{html-B_e3Njif.js → html-CgO_zx4D.js} +1 -1
- package/dist/ui/assets/{html-derivative-_nHZ_Bk-.js → html-derivative-BxqkysJT.js} +1 -1
- package/dist/ui/assets/{http-CfcvPIru.js → http-CO5dv0RA.js} +1 -1
- package/dist/ui/assets/{hurl-9vWDQIMO.js → hurl-DRHtrK78.js} +1 -1
- package/dist/ui/assets/{java-C4S5r6L5.js → java-BvHC_DgR.js} +1 -1
- package/dist/ui/assets/{javascript-BRwqbMMC.js → javascript-C-3payQx.js} +1 -1
- package/dist/ui/assets/{jinja-DHxzMMc5.js → jinja-CAOPvEOH.js} +1 -1
- package/dist/ui/assets/{jison-B1DqeWXb.js → jison-totCB1L-.js} +1 -1
- package/dist/ui/assets/{json-DcFTkEdd.js → json-D0J9rEe1.js} +1 -1
- package/dist/ui/assets/{jsx-DgfmYa9f.js → jsx-na-XQD_B.js} +1 -1
- package/dist/ui/assets/{julia-3M8SJ8iF.js → julia-CqrTtZ4-.js} +1 -1
- package/dist/ui/assets/{just-UyH7_DNz.js → just-pmrVSkMc.js} +1 -1
- package/dist/ui/assets/{latex-wUbha6EX.js → latex-Cd_geX3E.js} +1 -1
- package/dist/ui/assets/{liquid-DqNTB9kN.js → liquid-C6ky5sJ-.js} +1 -1
- package/dist/ui/assets/{lua-DrEQ1QTW.js → lua-dgyBbeof.js} +1 -1
- package/dist/ui/assets/{marko-B3GJw9Md.js → marko-Dy5mfXY9.js} +1 -1
- package/dist/ui/assets/{mdc-BnThk82m.js → mdc-7NUvxgI1.js} +1 -1
- package/dist/ui/assets/{nginx-B5IeSsOX.js → nginx-C-KiEXbI.js} +1 -1
- package/dist/ui/assets/{nim-BJraatnS.js → nim-BBw8kfl9.js} +1 -1
- package/dist/ui/assets/{perl-BAhrR4gt.js → perl-CyxhJgbO.js} +1 -1
- package/dist/ui/assets/{php-BKRCk_wX.js → php-m4LAOpam.js} +1 -1
- package/dist/ui/assets/{pug-B8gmSV7z.js → pug-Cpl0baYI.js} +1 -1
- package/dist/ui/assets/{qml-DKrxIjHR.js → qml-BoSGNgIb.js} +1 -1
- package/dist/ui/assets/{r-YASBZvTH.js → r-C0JhqQd5.js} +1 -1
- package/dist/ui/assets/{razor-P-XFZqh6.js → razor-CntsBNoh.js} +1 -1
- package/dist/ui/assets/{regexp-CCpiH6nO.js → regexp-BNZmOCRl.js} +1 -1
- package/dist/ui/assets/{review-payload-BLYwg6t8.js → review-payload-BgdWHWiv.js} +1 -1
- package/dist/ui/assets/{rst-CFAzckbn.js → rst-BZ2XHIxM.js} +1 -1
- package/dist/ui/assets/{ruby-Dyp_Rx-S.js → ruby-BIthfCTP.js} +1 -1
- package/dist/ui/assets/{sas-YkWZIXxT.js → sas-5aT3fOla.js} +1 -1
- package/dist/ui/assets/{scss-Cf89idGl.js → scss-DZPfdkDy.js} +1 -1
- package/dist/ui/assets/{shellscript-BFMb52q8.js → shellscript-DbhKc5Xj.js} +1 -1
- package/dist/ui/assets/{shellsession-DuthW-5v.js → shellsession-X9-bcyHh.js} +1 -1
- package/dist/ui/assets/{soy-BohVVlJU.js → soy-Bd4tecrW.js} +1 -1
- package/dist/ui/assets/{sql-CnK9R2Gt.js → sql-Chf5JrDn.js} +1 -1
- package/dist/ui/assets/{stata-D-sV4Vbr.js → stata-DvMMIc_T.js} +1 -1
- package/dist/ui/assets/{surrealql-ChAy0gHv.js → surrealql-D5FMzVF5.js} +1 -1
- package/dist/ui/assets/{svelte-3HBVqG-H.js → svelte-CTiVXZrv.js} +1 -1
- package/dist/ui/assets/{templ-BixlV4Cp.js → templ-ldnfq-7w.js} +1 -1
- package/dist/ui/assets/{tex-DWoJKHvD.js → tex-QW0_7T2r.js} +1 -1
- package/dist/ui/assets/{ts-tags-DLA8eH13.js → ts-tags-CAKiWIBx.js} +1 -1
- package/dist/ui/assets/{tsx-BYWa5JUz.js → tsx-Bu9xJJmq.js} +1 -1
- package/dist/ui/assets/{twig-CnY65vAo.js → twig-Dp1752_V.js} +1 -1
- package/dist/ui/assets/{typescript-ByInUsro.js → typescript-RfO03Kmm.js} +1 -1
- package/dist/ui/assets/{useFileDiffInstance-Dg0pWcJV.js → useFileDiffInstance-DjqU4y1a.js} +3 -3
- package/dist/ui/assets/{vue-Qngt2rkz.js → vue-Dlasv57G.js} +1 -1
- package/dist/ui/assets/{vue-html-DDue9upr.js → vue-html-BHuJ7anu.js} +1 -1
- package/dist/ui/assets/{vue-vine-DBcAKsA6.js → vue-vine-CavE0PSk.js} +1 -1
- package/dist/ui/assets/{workspace-app-DuzpU4Hf.js → workspace-app-DDV_qdjx.js} +4 -4
- package/dist/ui/assets/{xml-CRLR5jiL.js → xml-Bbv64l4W.js} +1 -1
- package/dist/ui/assets/{xsl-CmcwyXbm.js → xsl-Sro4gyQY.js} +1 -1
- package/dist/ui/assets/{yaml-Dpe6Eg-k.js → yaml-Du68c8sA.js} +1 -1
- package/dist/ui/workspace-app.html +1 -1
- 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/README.md
CHANGED
|
@@ -50,14 +50,14 @@ DevSpace is a self-hosted MCP server that lets ChatGPT read, edit, search, and r
|
|
|
50
50
|
</tbody>
|
|
51
51
|
</table>
|
|
52
52
|
|
|
53
|
-
<p
|
|
53
|
+
<p>
|
|
54
54
|
DevSpace is open to new sponsors.
|
|
55
55
|
<a href="https://x.com/wshxnv">Get in touch to become one.</a>
|
|
56
56
|
</p>
|
|
57
57
|
|
|
58
58
|
## Installation
|
|
59
59
|
|
|
60
|
-
DevSpace requires Node `>=
|
|
60
|
+
DevSpace requires Node `>=22.19 <27`.
|
|
61
61
|
|
|
62
62
|
Install the DevSpace CLI:
|
|
63
63
|
|
|
@@ -117,6 +117,14 @@ Most users should connect through a public HTTPS tunnel:
|
|
|
117
117
|
https://your-tunnel-host.example.com/mcp
|
|
118
118
|
```
|
|
119
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
|
+
|
|
120
128
|
## What ChatGPT Can Do
|
|
121
129
|
|
|
122
130
|
Once connected, ChatGPT can open one of your approved project folders as a
|
|
@@ -192,14 +200,42 @@ explicit, inspectable tools.
|
|
|
192
200
|
|
|
193
201
|
## Built by Waishnav
|
|
194
202
|
|
|
195
|
-
I'm Waishnav,
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
I like building opinionated products, and DevSpace is another example of that.
|
|
199
|
-
I'm on a journey to build a single-person company doing multiple millions in
|
|
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
|
|
200
205
|
revenue. If you want to watch the failures, wins, lessons, and everything in
|
|
201
206
|
between, come hang out with me on [X](https://x.com/wshxnv).
|
|
202
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>
|
|
238
|
+
|
|
203
239
|
## Local Development
|
|
204
240
|
|
|
205
241
|
For working on DevSpace itself:
|
|
@@ -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() {
|
|
@@ -226,11 +231,19 @@ function printHelp() {
|
|
|
226
231
|
" devspace doctor Show config, runtime, and native dependency status",
|
|
227
232
|
" devspace config get Print persisted config",
|
|
228
233
|
" devspace config set publicBaseUrl <url|null>",
|
|
234
|
+
" devspace -v, --version Print the installed version",
|
|
229
235
|
"",
|
|
230
236
|
"For temporary tunnels:",
|
|
231
237
|
" DEVSPACE_PUBLIC_BASE_URL=https://example.trycloudflare.com devspace serve",
|
|
232
238
|
].join("\n"));
|
|
233
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
|
+
}
|
|
234
247
|
function normalizeOptionalPublicBaseUrl(value) {
|
|
235
248
|
const trimmed = value.trim();
|
|
236
249
|
if (!trimmed || trimmed === "null" || trimmed === "none")
|
package/dist/config.js
CHANGED
|
@@ -44,17 +44,16 @@ function normalizeAllowedHosts(rawHosts, derivedHosts) {
|
|
|
44
44
|
function parseBoolean(value) {
|
|
45
45
|
return ["1", "true", "yes", "on"].includes(value?.toLowerCase() ?? "");
|
|
46
46
|
}
|
|
47
|
-
function
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
47
|
+
function parseToolMode(env) {
|
|
48
|
+
const mode = env.DEVSPACE_TOOL_MODE;
|
|
49
|
+
if (mode === "minimal" || mode === "full" || mode === "codex")
|
|
50
|
+
return mode;
|
|
51
|
+
if (mode)
|
|
52
|
+
throw new Error(`Invalid DEVSPACE_TOOL_MODE: ${mode}`);
|
|
53
|
+
if (env.DEVSPACE_MINIMAL_TOOLS !== undefined) {
|
|
54
|
+
return parseBoolean(env.DEVSPACE_MINIMAL_TOOLS) ? "minimal" : "full";
|
|
54
55
|
}
|
|
55
|
-
|
|
56
|
-
return parseBoolean(env.DEVSPACE_MINIMAL_TOOLS);
|
|
57
|
-
return true;
|
|
56
|
+
return "minimal";
|
|
58
57
|
}
|
|
59
58
|
function parseLogLevel(value) {
|
|
60
59
|
if (!value || value === "info")
|
|
@@ -74,8 +73,7 @@ function parsePathList(value) {
|
|
|
74
73
|
return (value
|
|
75
74
|
?.split(",")
|
|
76
75
|
.map((entry) => entry.trim())
|
|
77
|
-
.filter(Boolean)
|
|
78
|
-
.map((entry) => resolve(expandHomePath(entry))) ?? []);
|
|
76
|
+
.filter(Boolean) ?? []);
|
|
79
77
|
}
|
|
80
78
|
function parseStringList(value, fallback) {
|
|
81
79
|
const entries = value
|
|
@@ -170,7 +168,7 @@ export function loadConfig(env = process.env) {
|
|
|
170
168
|
allowedRoots: parseAllowedRoots(env.DEVSPACE_ALLOWED_ROOTS ?? files.config.allowedRoots),
|
|
171
169
|
allowedHosts: parseAllowedHosts(env.DEVSPACE_ALLOWED_HOSTS, derivedAllowedHosts),
|
|
172
170
|
publicBaseUrl,
|
|
173
|
-
|
|
171
|
+
toolMode: parseToolMode(env),
|
|
174
172
|
toolNaming: parseToolNaming(env.DEVSPACE_TOOL_NAMING),
|
|
175
173
|
widgets: parseWidgetMode(env.DEVSPACE_WIDGETS),
|
|
176
174
|
stateDir: resolve(expandHomePath(env.DEVSPACE_STATE_DIR ?? files.config.stateDir ?? defaultStateDir())),
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { basename } from "node:path";
|
|
2
|
+
import { spawnSync } from "node:child_process";
|
|
3
|
+
const defaultProcessTreeRuntime = {
|
|
4
|
+
platform: process.platform,
|
|
5
|
+
killGroup: (pid, signal) => process.kill(-pid, signal),
|
|
6
|
+
killWindowsTree: (pid) => {
|
|
7
|
+
const result = spawnSync("taskkill.exe", ["/pid", String(pid), "/T", "/F"], {
|
|
8
|
+
stdio: "ignore",
|
|
9
|
+
windowsHide: true,
|
|
10
|
+
});
|
|
11
|
+
return !result.error && result.status === 0;
|
|
12
|
+
},
|
|
13
|
+
};
|
|
14
|
+
const LOGIN_SHELLS = new Set(["bash", "ksh", "zsh"]);
|
|
15
|
+
const POSIX_SHELLS = new Set(["ash", "dash", "sh"]);
|
|
16
|
+
export function resolveShellCommand(command, platform = process.platform, environment = process.env) {
|
|
17
|
+
if (platform === "win32") {
|
|
18
|
+
return {
|
|
19
|
+
executable: environment.ComSpec ?? environment.COMSPEC ?? "cmd.exe",
|
|
20
|
+
args: ["/d", "/s", "/c", command],
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
const configuredShell = environment.SHELL;
|
|
24
|
+
const shellName = configuredShell ? basename(configuredShell) : "";
|
|
25
|
+
if (configuredShell && LOGIN_SHELLS.has(shellName)) {
|
|
26
|
+
return { executable: configuredShell, args: ["-lc", command] };
|
|
27
|
+
}
|
|
28
|
+
if (configuredShell && POSIX_SHELLS.has(shellName)) {
|
|
29
|
+
return { executable: configuredShell, args: ["-c", command] };
|
|
30
|
+
}
|
|
31
|
+
return { executable: "/bin/sh", args: ["-c", command] };
|
|
32
|
+
}
|
|
33
|
+
export function terminateProcessTree(child, signal, detached, runtime = defaultProcessTreeRuntime) {
|
|
34
|
+
if (runtime.platform === "win32" && child.pid) {
|
|
35
|
+
if (runtime.killWindowsTree(child.pid))
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
else if (detached && child.pid) {
|
|
39
|
+
try {
|
|
40
|
+
runtime.killGroup(child.pid, signal);
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
catch (error) {
|
|
44
|
+
if (error.code === "ESRCH")
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
child.kill(signal);
|
|
49
|
+
}
|