@waishnav/devspace 1.0.2 → 1.0.4

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 (115) hide show
  1. package/README.md +43 -7
  2. package/dist/apply-patch.js +402 -0
  3. package/dist/cli.js +256 -4
  4. package/dist/config.js +17 -22
  5. package/dist/db/migrations.js +34 -0
  6. package/dist/db/schema.js +19 -0
  7. package/dist/local-agent-adapters.js +653 -0
  8. package/dist/local-agent-availability.js +119 -0
  9. package/dist/local-agent-path.js +24 -0
  10. package/dist/local-agent-profiles.js +135 -0
  11. package/dist/local-agent-runtime.js +48 -0
  12. package/dist/local-agent-store.js +145 -0
  13. package/dist/local-agent-targets.js +80 -0
  14. package/dist/process-platform.js +49 -0
  15. package/dist/process-sessions.js +337 -0
  16. package/dist/roots.js +5 -2
  17. package/dist/server.js +521 -243
  18. package/dist/skills.js +48 -4
  19. package/dist/ui/.vite/manifest.json +281 -281
  20. package/dist/ui/assets/{angular-html-Dj4LysSE.js → angular-html-CKeFzaPY.js} +1 -1
  21. package/dist/ui/assets/{angular-ts-CxuC_FFh.js → angular-ts-0wcM_Zxf.js} +1 -1
  22. package/dist/ui/assets/{apl-RwuvNFFo.js → apl-C7UdkNb2.js} +1 -1
  23. package/dist/ui/assets/{astro-ncoaUbsO.js → astro-DMqju4MJ.js} +1 -1
  24. package/dist/ui/assets/{blade-Dw8-Lzux.js → blade-DHanNgN5.js} +1 -1
  25. package/dist/ui/assets/{c-Dpbbo_f2.js → c-_iPKYPcx.js} +1 -1
  26. package/dist/ui/assets/{cobol-4ddX3THy.js → cobol-CagdLJsE.js} +1 -1
  27. package/dist/ui/assets/{coffee-Bn-a5KqK.js → coffee-CkNGriPi.js} +1 -1
  28. package/dist/ui/assets/{cpp-DoRrlM_T.js → cpp-CqmKOWIQ.js} +1 -1
  29. package/dist/ui/assets/{crystal-DiZfCxn-.js → crystal-DbAbDEO1.js} +1 -1
  30. package/dist/ui/assets/{css-DiQoSjDH.js → css-xwljL6Kw.js} +1 -1
  31. package/dist/ui/assets/{edge-wEhCUoEt.js → edge-CX3sYz15.js} +1 -1
  32. package/dist/ui/assets/{elixir-C3obhm0H.js → elixir-B3qL9hqd.js} +1 -1
  33. package/dist/ui/assets/{elm-DyTBfh1L.js → elm-CH4vrjBp.js} +1 -1
  34. package/dist/ui/assets/{erb-f4TnXRuY.js → erb-CupHqynN.js} +1 -1
  35. package/dist/ui/assets/{git-rebase-vhZbWNfp.js → git-rebase-CDxsU2LC.js} +1 -1
  36. package/dist/ui/assets/{glimmer-js-BG7puL6Y.js → glimmer-js-emkga3g1.js} +1 -1
  37. package/dist/ui/assets/{glimmer-ts-CNCfLLL1.js → glimmer-ts-D8kQDnUw.js} +1 -1
  38. package/dist/ui/assets/{glsl-DhT76AL5.js → glsl-BtfTaPhi.js} +1 -1
  39. package/dist/ui/assets/{graphql-CJX08w8y.js → graphql-DEus8iSO.js} +1 -1
  40. package/dist/ui/assets/{hack-DY4NGCCL.js → hack-DI8qpBqE.js} +1 -1
  41. package/dist/ui/assets/{haml-DwSkmVcG.js → haml-Br1i1gmi.js} +1 -1
  42. package/dist/ui/assets/{handlebars-CQ_1dEmi.js → handlebars-Ce23mu9x.js} +1 -1
  43. package/dist/ui/assets/{heavy-payload-2rGBiitA.js → heavy-payload-BcZ_nGfF.js} +1 -1
  44. package/dist/ui/assets/{html-B_e3Njif.js → html-RXVmfixf.js} +1 -1
  45. package/dist/ui/assets/{html-derivative-_nHZ_Bk-.js → html-derivative-D1aqeobb.js} +1 -1
  46. package/dist/ui/assets/{http-CfcvPIru.js → http-BebG00CE.js} +1 -1
  47. package/dist/ui/assets/{hurl-9vWDQIMO.js → hurl-kHMuvenL.js} +1 -1
  48. package/dist/ui/assets/{java-C4S5r6L5.js → java-DYc2V5RJ.js} +1 -1
  49. package/dist/ui/assets/{javascript-BRwqbMMC.js → javascript-BcfzTzr3.js} +1 -1
  50. package/dist/ui/assets/{jinja-DHxzMMc5.js → jinja-D2RhihUZ.js} +1 -1
  51. package/dist/ui/assets/{jison-B1DqeWXb.js → jison-BuoFEePN.js} +1 -1
  52. package/dist/ui/assets/{json-DcFTkEdd.js → json-BIDkCuJA.js} +1 -1
  53. package/dist/ui/assets/{jsx-DgfmYa9f.js → jsx-DHCL2HA0.js} +1 -1
  54. package/dist/ui/assets/{julia-3M8SJ8iF.js → julia-CvUQXscF.js} +1 -1
  55. package/dist/ui/assets/{just-UyH7_DNz.js → just-etc9R2qw.js} +1 -1
  56. package/dist/ui/assets/{latex-wUbha6EX.js → latex-BB3zRnxC.js} +1 -1
  57. package/dist/ui/assets/{liquid-DqNTB9kN.js → liquid-CIi5Mvpv.js} +1 -1
  58. package/dist/ui/assets/{lua-DrEQ1QTW.js → lua-Cox7XDiW.js} +1 -1
  59. package/dist/ui/assets/{marko-B3GJw9Md.js → marko-ruBIRV0n.js} +1 -1
  60. package/dist/ui/assets/{mdc-BnThk82m.js → mdc-CcbvB9U5.js} +1 -1
  61. package/dist/ui/assets/{nginx-B5IeSsOX.js → nginx-DtWz6ZYE.js} +1 -1
  62. package/dist/ui/assets/{nim-BJraatnS.js → nim-BV5BR4P7.js} +1 -1
  63. package/dist/ui/assets/{perl-BAhrR4gt.js → perl-BvzoSaJ4.js} +1 -1
  64. package/dist/ui/assets/{php-BKRCk_wX.js → php-C-clrAOy.js} +1 -1
  65. package/dist/ui/assets/{pug-B8gmSV7z.js → pug-E07_GXAr.js} +1 -1
  66. package/dist/ui/assets/{qml-DKrxIjHR.js → qml-dqt33dti.js} +1 -1
  67. package/dist/ui/assets/{r-YASBZvTH.js → r-BcVQxsAw.js} +1 -1
  68. package/dist/ui/assets/{razor-P-XFZqh6.js → razor-BW5T_UZ-.js} +1 -1
  69. package/dist/ui/assets/{regexp-CCpiH6nO.js → regexp-Ed_DEsuU.js} +1 -1
  70. package/dist/ui/assets/{review-payload-BLYwg6t8.js → review-payload-Dsmx33pY.js} +1 -1
  71. package/dist/ui/assets/{rst-CFAzckbn.js → rst-BkcGqLs8.js} +1 -1
  72. package/dist/ui/assets/{ruby-Dyp_Rx-S.js → ruby-Sf_X_YID.js} +1 -1
  73. package/dist/ui/assets/{sas-YkWZIXxT.js → sas-COsombsA.js} +1 -1
  74. package/dist/ui/assets/{scss-Cf89idGl.js → scss-K6Ylfa26.js} +1 -1
  75. package/dist/ui/assets/{shellscript-BFMb52q8.js → shellscript-D2EdqkgV.js} +1 -1
  76. package/dist/ui/assets/{shellsession-DuthW-5v.js → shellsession-jRXgXrnI.js} +1 -1
  77. package/dist/ui/assets/{soy-BohVVlJU.js → soy-BGspKQWW.js} +1 -1
  78. package/dist/ui/assets/{sql-CnK9R2Gt.js → sql-C4lAmYnM.js} +1 -1
  79. package/dist/ui/assets/{stata-D-sV4Vbr.js → stata-DQWvRqxp.js} +1 -1
  80. package/dist/ui/assets/{surrealql-ChAy0gHv.js → surrealql-OmKt9XJ_.js} +1 -1
  81. package/dist/ui/assets/{svelte-3HBVqG-H.js → svelte-D0rUmwDI.js} +1 -1
  82. package/dist/ui/assets/{templ-BixlV4Cp.js → templ-B-U0RMc0.js} +1 -1
  83. package/dist/ui/assets/{tex-DWoJKHvD.js → tex-BIHD-btS.js} +1 -1
  84. package/dist/ui/assets/{ts-tags-DLA8eH13.js → ts-tags-CGuDWfPC.js} +1 -1
  85. package/dist/ui/assets/{tsx-BYWa5JUz.js → tsx-B0-h_HlD.js} +1 -1
  86. package/dist/ui/assets/{twig-CnY65vAo.js → twig-BN2qDJWN.js} +1 -1
  87. package/dist/ui/assets/{typescript-ByInUsro.js → typescript-BRnwPzY3.js} +1 -1
  88. package/dist/ui/assets/{useFileDiffInstance-Dg0pWcJV.js → useFileDiffInstance-5ivdkUYO.js} +3 -3
  89. package/dist/ui/assets/{vue-Qngt2rkz.js → vue-DekNU1qt.js} +1 -1
  90. package/dist/ui/assets/{vue-html-DDue9upr.js → vue-html-Bjlv-uvH.js} +1 -1
  91. package/dist/ui/assets/{vue-vine-DBcAKsA6.js → vue-vine-C2Doytjq.js} +1 -1
  92. package/dist/ui/assets/{workspace-app-DGD9R89D.css → workspace-app-DASlX26z.css} +1 -1
  93. package/dist/ui/assets/{workspace-app-DuzpU4Hf.js → workspace-app-DYi7VJam.js} +5 -5
  94. package/dist/ui/assets/{xml-CRLR5jiL.js → xml-pXbxxgI3.js} +1 -1
  95. package/dist/ui/assets/{xsl-CmcwyXbm.js → xsl-BEAlgDI2.js} +1 -1
  96. package/dist/ui/assets/{yaml-Dpe6Eg-k.js → yaml-C1O-To71.js} +1 -1
  97. package/dist/ui/workspace-app.html +2 -2
  98. package/dist/user-config.js +21 -1
  99. package/dist/workspaces.js +3 -0
  100. package/docs/agent-profile-schema.md +165 -0
  101. package/docs/chatgpt-coding-workflow.md +46 -12
  102. package/docs/configuration.md +47 -12
  103. package/docs/gotchas.md +24 -5
  104. package/docs/setup.md +1 -1
  105. package/examples/agents/claude-implementer.md +30 -0
  106. package/examples/agents/codex-explorer.md +27 -0
  107. package/examples/agents/codex-qa-tester.md +29 -0
  108. package/examples/agents/codex-worker.md +25 -0
  109. package/examples/agents/copilot-reviewer.md +25 -0
  110. package/examples/agents/cursor-agent-worker.md +26 -0
  111. package/examples/agents/opencode-explorer.md +27 -0
  112. package/examples/agents/pi-reviewer.md +27 -0
  113. package/package.json +18 -5
  114. package/scripts/fix-node-pty-permissions.mjs +22 -0
  115. package/skills/subagent-delegation/SKILL.md +132 -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 align="center">
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 `>=20.12 <27`. Node 22 LTS is recommended.
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, the creator of [GitCMS](https://gitcms.dev/), a Git-backed CMS
196
- for markdown sites.
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,402 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import { constants } from "node:fs";
3
+ import { access, lstat, 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
+ import { createTwoFilesPatch, FILE_HEADERS_ONLY } from "diff";
7
+ function patchError(message) {
8
+ return new Error(`Invalid patch: ${message}`);
9
+ }
10
+ export function parsePatch(patch) {
11
+ const lines = patchLines(patch);
12
+ if (lines.shift()?.trim() !== "*** Begin Patch") {
13
+ throw patchError("missing *** Begin Patch marker");
14
+ }
15
+ if (lines.pop()?.trim() !== "*** End Patch") {
16
+ throw patchError("missing *** End Patch marker");
17
+ }
18
+ const actions = [];
19
+ let index = 0;
20
+ while (index < lines.length) {
21
+ const header = lines[index++].trim();
22
+ if (header === "")
23
+ continue;
24
+ if (header.startsWith("*** Environment ID: ")) {
25
+ if (!header.slice("*** Environment ID: ".length).trim()) {
26
+ throw patchError("environment id cannot be empty");
27
+ }
28
+ continue;
29
+ }
30
+ if (header.startsWith("*** Add File: ")) {
31
+ const path = header.slice("*** Add File: ".length);
32
+ const content = [];
33
+ while (index < lines.length && !isTopLevelHeader(lines[index])) {
34
+ const line = lines[index++];
35
+ if (!line.startsWith("+")) {
36
+ throw patchError(`added file line must start with +: ${line}`);
37
+ }
38
+ content.push(line.slice(1));
39
+ }
40
+ if (content.length === 0)
41
+ throw patchError(`add file for ${path} has no content`);
42
+ actions.push({
43
+ kind: "add",
44
+ path,
45
+ content: `${content.join("\n")}\n`,
46
+ });
47
+ continue;
48
+ }
49
+ if (header.startsWith("*** Delete File: ")) {
50
+ actions.push({ kind: "delete", path: header.slice("*** Delete File: ".length) });
51
+ continue;
52
+ }
53
+ if (header.startsWith("*** Update File: ")) {
54
+ const path = header.slice("*** Update File: ".length);
55
+ let moveTo;
56
+ const hunks = [];
57
+ if (lines[index]?.trim().startsWith("*** Move to: ")) {
58
+ moveTo = lines[index++].trim().slice("*** Move to: ".length);
59
+ }
60
+ let current;
61
+ const finishCurrent = () => {
62
+ if (!current)
63
+ return;
64
+ if (current.lines.length === 0)
65
+ throw patchError(`empty update hunk for ${path}`);
66
+ hunks.push(current);
67
+ current = undefined;
68
+ };
69
+ while (index < lines.length) {
70
+ const line = lines[index];
71
+ const trimmed = line.trim();
72
+ if (!current && trimmed === "") {
73
+ index++;
74
+ continue;
75
+ }
76
+ if (trimmed === "*** End of File") {
77
+ if (!current)
78
+ throw patchError(`end-of-file marker without update hunk for ${path}`);
79
+ current.endOfFile = true;
80
+ index++;
81
+ continue;
82
+ }
83
+ if ((!current || !line.startsWith(" ")) && isTopLevelHeader(line))
84
+ break;
85
+ if (trimmed.startsWith("@@") && !line.startsWith(" ")) {
86
+ finishCurrent();
87
+ const changeContext = trimmed.slice(2).trim();
88
+ current = { lines: [], changeContext: changeContext || undefined };
89
+ index++;
90
+ continue;
91
+ }
92
+ current ??= { lines: [] };
93
+ index++;
94
+ if (line.startsWith(" "))
95
+ current.lines.push({ kind: "context", text: line.slice(1) });
96
+ else if (line.startsWith("+"))
97
+ current.lines.push({ kind: "add", text: line.slice(1) });
98
+ else if (line.startsWith("-"))
99
+ current.lines.push({ kind: "remove", text: line.slice(1) });
100
+ else if (line === "\")
101
+ continue;
102
+ else
103
+ throw patchError(`hunk line must start with space, +, or -: ${line}`);
104
+ }
105
+ finishCurrent();
106
+ if (hunks.length === 0 && !moveTo) {
107
+ throw patchError(`update for ${path} has no hunks or move destination`);
108
+ }
109
+ actions.push({ kind: "update", path, moveTo, hunks });
110
+ continue;
111
+ }
112
+ throw patchError(`unknown action header: ${header}`);
113
+ }
114
+ if (actions.length === 0)
115
+ throw patchError("contains no file actions");
116
+ return actions;
117
+ }
118
+ function patchLines(patch) {
119
+ let lines = patch.replace(/\r\n/g, "\n").trim().split("\n");
120
+ const first = lines[0]?.trim();
121
+ const last = lines.at(-1)?.trim();
122
+ if ((first === "<<EOF" || first === "<<'EOF'" || first === '<<"EOF"') &&
123
+ last?.endsWith("EOF") &&
124
+ lines.length >= 4) {
125
+ lines = lines.slice(1, -1);
126
+ }
127
+ return lines;
128
+ }
129
+ function isTopLevelHeader(line) {
130
+ const trimmed = line.trim();
131
+ return (trimmed.startsWith("*** Add File: ") ||
132
+ trimmed.startsWith("*** Delete File: ") ||
133
+ trimmed.startsWith("*** Update File: ") ||
134
+ trimmed.startsWith("*** Environment ID: "));
135
+ }
136
+ function isInside(root, path) {
137
+ const rel = relative(root, path);
138
+ return rel === "" || (!rel.startsWith("..") && !isAbsolute(rel));
139
+ }
140
+ async function resolveConfinedPath(root, input) {
141
+ if (!input || input.includes("\0") || isAbsolute(input)) {
142
+ throw patchError(`path must be relative to the workspace: ${input}`);
143
+ }
144
+ const rootPath = await realpath(root);
145
+ const target = resolve(rootPath, input);
146
+ if (!isInside(rootPath, target)) {
147
+ throw patchError(`path escapes the workspace: ${input}`);
148
+ }
149
+ let existing = target;
150
+ while (true) {
151
+ try {
152
+ const resolved = await realpath(existing);
153
+ if (!isInside(rootPath, resolved)) {
154
+ throw patchError(`path resolves outside the workspace: ${input}`);
155
+ }
156
+ break;
157
+ }
158
+ catch (error) {
159
+ const code = error.code;
160
+ if (code !== "ENOENT")
161
+ throw error;
162
+ const parent = dirname(existing);
163
+ if (parent === existing)
164
+ throw error;
165
+ existing = parent;
166
+ }
167
+ }
168
+ return target;
169
+ }
170
+ function splitFile(content) {
171
+ const eol = content.includes("\r\n") ? "\r\n" : "\n";
172
+ const normalized = content.replace(/\r\n/g, "\n");
173
+ const finalNewline = normalized.endsWith("\n");
174
+ const lines = normalized.split("\n");
175
+ if (finalNewline)
176
+ lines.pop();
177
+ return { lines, eol, finalNewline };
178
+ }
179
+ function findSequence(haystack, needle, from, endOfFile = false) {
180
+ if (needle.length === 0)
181
+ return from;
182
+ const matchAt = (index, normalize) => needle.every((line, offset) => normalize(haystack[index + offset] ?? "") === normalize(line));
183
+ for (const normalize of [
184
+ (value) => value,
185
+ (value) => value.trimEnd(),
186
+ (value) => value.trim(),
187
+ ]) {
188
+ const start = endOfFile ? haystack.length - needle.length : from;
189
+ const end = haystack.length - needle.length;
190
+ for (let index = start; index <= end; index += 1) {
191
+ if (index >= from && matchAt(index, normalize))
192
+ return index;
193
+ }
194
+ }
195
+ return -1;
196
+ }
197
+ function applyHunks(path, content, hunks) {
198
+ const file = splitFile(content);
199
+ const lines = [...file.lines];
200
+ let cursor = 0;
201
+ for (const hunk of hunks) {
202
+ if (hunk.changeContext) {
203
+ const contextIndex = findSequence(lines, [hunk.changeContext], cursor);
204
+ if (contextIndex < 0) {
205
+ throw patchError(`could not find hunk context in ${path}: ${hunk.changeContext}`);
206
+ }
207
+ cursor = contextIndex + 1;
208
+ }
209
+ const oldLines = hunk.lines
210
+ .filter((line) => line.kind !== "add")
211
+ .map((line) => line.text);
212
+ const newLines = hunk.lines
213
+ .filter((line) => line.kind !== "remove")
214
+ .map((line) => line.text);
215
+ const index = hunk.endOfFile && oldLines.length === 0
216
+ ? lines.length
217
+ : findSequence(lines, oldLines, cursor, hunk.endOfFile);
218
+ if (index < 0) {
219
+ const preview = oldLines.slice(0, 3).join("\n");
220
+ throw patchError(`could not find hunk context in ${path}: ${preview}`);
221
+ }
222
+ lines.splice(index, oldLines.length, ...newLines);
223
+ cursor = index + newLines.length;
224
+ }
225
+ const normalized = `${lines.join("\n")}\n`;
226
+ return file.eol === "\r\n" ? normalized.replace(/\n/g, "\r\n") : normalized;
227
+ }
228
+ async function fileExists(path) {
229
+ try {
230
+ await access(path, constants.F_OK);
231
+ return true;
232
+ }
233
+ catch {
234
+ return false;
235
+ }
236
+ }
237
+ export async function replaceFile(temporary, destination, destinationExists, platform = process.platform) {
238
+ if (platform !== "win32" || !destinationExists) {
239
+ await rename(temporary, destination);
240
+ return;
241
+ }
242
+ const backup = `${temporary}.original`;
243
+ await rename(destination, backup);
244
+ try {
245
+ await rename(temporary, destination);
246
+ }
247
+ catch (error) {
248
+ await rename(backup, destination);
249
+ throw error;
250
+ }
251
+ await rm(backup, { force: true });
252
+ }
253
+ export async function isSamePatchFile(source, destination, readIdentity = lstat) {
254
+ if (source === destination)
255
+ return true;
256
+ if (source.toLowerCase() !== destination.toLowerCase())
257
+ return false;
258
+ try {
259
+ const [sourceIdentity, destinationIdentity] = await Promise.all([
260
+ readIdentity(source),
261
+ readIdentity(destination),
262
+ ]);
263
+ return sourceIdentity.dev === destinationIdentity.dev && sourceIdentity.ino === destinationIdentity.ino;
264
+ }
265
+ catch (error) {
266
+ const code = error.code;
267
+ if (code === "ENOENT" || code === "ENOTDIR")
268
+ return false;
269
+ throw error;
270
+ }
271
+ }
272
+ export async function applyPatch(root, patch) {
273
+ const actions = parsePatch(patch);
274
+ const results = [];
275
+ const patches = [];
276
+ const staged = new Map();
277
+ const readStagedOptional = async (absolute, displayPath) => {
278
+ if (staged.has(absolute))
279
+ return staged.get(absolute) ?? null;
280
+ const file = await readOptionalTextFile(absolute, displayPath);
281
+ staged.set(absolute, file);
282
+ return file;
283
+ };
284
+ const readStagedRequired = async (absolute, displayPath) => {
285
+ const file = await readStagedOptional(absolute, displayPath);
286
+ if (!file)
287
+ throw patchError(`file does not exist: ${displayPath}`);
288
+ return file;
289
+ };
290
+ for (const action of actions) {
291
+ if (action.kind === "add") {
292
+ const absolute = await resolveConfinedPath(root, action.path);
293
+ const original = await readStagedOptional(absolute, action.path);
294
+ staged.set(absolute, { content: action.content, mode: original?.mode });
295
+ patches.push(unifiedFilePatch(action.path, action.path, original?.content ?? null, action.content));
296
+ results.push({ path: action.path, operation: "add" });
297
+ continue;
298
+ }
299
+ const absolute = await resolveConfinedPath(root, action.path);
300
+ const file = await readStagedRequired(absolute, action.path);
301
+ if (action.kind === "delete") {
302
+ staged.set(absolute, null);
303
+ patches.push(unifiedFilePatch(action.path, action.path, file.content, null));
304
+ results.push({ path: action.path, operation: "delete" });
305
+ continue;
306
+ }
307
+ const updated = applyHunks(action.path, file.content, action.hunks);
308
+ if (action.moveTo) {
309
+ const destination = await resolveConfinedPath(root, action.moveTo);
310
+ const samePatchFile = await isSamePatchFile(absolute, destination);
311
+ if (!samePatchFile)
312
+ await readStagedOptional(destination, action.moveTo);
313
+ if (samePatchFile)
314
+ staged.delete(absolute);
315
+ staged.set(destination, { content: updated, mode: file.mode });
316
+ if (!samePatchFile)
317
+ staged.set(absolute, null);
318
+ patches.push(unifiedFilePatch(action.path, action.moveTo, file.content, updated));
319
+ results.push({ path: action.moveTo, previousPath: action.path, operation: "move" });
320
+ }
321
+ else {
322
+ staged.set(absolute, { content: updated, mode: file.mode });
323
+ patches.push(unifiedFilePatch(action.path, action.path, file.content, updated));
324
+ results.push({ path: action.path, operation: "update" });
325
+ }
326
+ }
327
+ for (const [absolute, file] of staged) {
328
+ if (file)
329
+ await writeTextFile(absolute, file.content, file.mode);
330
+ }
331
+ for (const [absolute, file] of staged) {
332
+ if (!file)
333
+ await rm(absolute, { force: true });
334
+ }
335
+ const unifiedPatch = patches.filter(Boolean).join("\n");
336
+ const stats = countPatchStats(unifiedPatch);
337
+ return { files: results, patch: unifiedPatch, ...stats };
338
+ }
339
+ async function readOptionalTextFile(absolute, displayPath) {
340
+ if (!(await fileExists(absolute)))
341
+ return null;
342
+ const metadata = await stat(absolute);
343
+ if (!metadata.isFile())
344
+ throw patchError(`path is not a regular file: ${displayPath}`);
345
+ return { content: await readUtf8Text(absolute, displayPath), mode: metadata.mode };
346
+ }
347
+ async function readUtf8Text(absolute, displayPath) {
348
+ const bytes = await readFile(absolute);
349
+ let content;
350
+ try {
351
+ content = new TextDecoder("utf-8", { fatal: true }).decode(bytes);
352
+ }
353
+ catch {
354
+ throw patchError(`file is not valid UTF-8 text: ${displayPath}`);
355
+ }
356
+ if (content.includes("\0"))
357
+ throw patchError(`file appears to be binary: ${displayPath}`);
358
+ return content;
359
+ }
360
+ async function writeTextFile(destination, content, mode) {
361
+ await mkdir(dirname(destination), { recursive: true });
362
+ const temporary = `${destination}.devspace-patch-${process.pid}-${randomUUID()}`;
363
+ try {
364
+ await writeFile(temporary, content, mode === undefined ? undefined : { mode });
365
+ await replaceFile(temporary, destination, await fileExists(destination));
366
+ }
367
+ catch (error) {
368
+ await rm(temporary, { force: true });
369
+ throw error;
370
+ }
371
+ }
372
+ function unifiedFilePatch(oldPath, newPath, oldContent, newContent) {
373
+ const oldFileName = oldContent === null ? "/dev/null" : `a/${oldPath}`;
374
+ const newFileName = newContent === null ? "/dev/null" : `b/${newPath}`;
375
+ const body = createTwoFilesPatch(oldFileName, newFileName, oldContent ?? "", newContent ?? "", "", "", { context: 3, headerOptions: FILE_HEADERS_ONLY });
376
+ return [
377
+ `diff --git a/${oldPath} b/${newPath}`,
378
+ oldContent === null ? "new file mode 100644" : undefined,
379
+ newContent === null ? "deleted file mode 100644" : undefined,
380
+ stripFinalNewline(body),
381
+ ]
382
+ .filter((line) => line !== undefined)
383
+ .join("\n");
384
+ }
385
+ function stripFinalNewline(value) {
386
+ if (value.endsWith("\r\n"))
387
+ return value.slice(0, -2);
388
+ if (value.endsWith("\n"))
389
+ return value.slice(0, -1);
390
+ return value;
391
+ }
392
+ function countPatchStats(patch) {
393
+ let additions = 0;
394
+ let removals = 0;
395
+ for (const line of patch.split("\n")) {
396
+ if (line.startsWith("+") && !line.startsWith("+++"))
397
+ additions += 1;
398
+ if (line.startsWith("-") && !line.startsWith("---"))
399
+ removals += 1;
400
+ }
401
+ return { additions, removals };
402
+ }