cwresdev 0.1.8 → 0.2.0
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/bin/cwgit.js +99 -0
- package/package.json +3 -2
- package/src/cwgit.js +362 -0
- package/src/proxy.js +12 -2
- package/src/server.js +12 -1
package/bin/cwgit.js
ADDED
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
"use strict";
|
|
4
|
+
|
|
5
|
+
const path = require("node:path");
|
|
6
|
+
const { detectChanges, exportCsv, findWorkspaceRoot, initBase, resolveDocRoot } = require("../src/cwgit");
|
|
7
|
+
|
|
8
|
+
async function main() {
|
|
9
|
+
if (process.platform !== "win32") {
|
|
10
|
+
throw new Error("cwgit currently supports Windows only.");
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const args = process.argv.slice(2);
|
|
14
|
+
const command = args[0];
|
|
15
|
+
|
|
16
|
+
if (!command || command === "--help" || command === "-h") {
|
|
17
|
+
process.stdout.write([
|
|
18
|
+
"cwgit <command>",
|
|
19
|
+
"",
|
|
20
|
+
"Commands:",
|
|
21
|
+
" init Snapshot the current project as the base version",
|
|
22
|
+
" changes Detect and export changed files as CSV",
|
|
23
|
+
"",
|
|
24
|
+
"Usage:",
|
|
25
|
+
" Run from inside a project folder (under the docRoot).",
|
|
26
|
+
" The tool locates the workspace root (package.json with cwrespro config)",
|
|
27
|
+
" and resolves docRoot from that config.",
|
|
28
|
+
"",
|
|
29
|
+
"Examples:",
|
|
30
|
+
" cd virtual_env/IC && cwgit init",
|
|
31
|
+
" cd virtual_env/IC && cwgit changes",
|
|
32
|
+
"",
|
|
33
|
+
].join("\n"));
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const cwd = process.cwd();
|
|
38
|
+
|
|
39
|
+
// Find the workspace root by walking up
|
|
40
|
+
const workspaceRoot = findWorkspaceRoot(cwd);
|
|
41
|
+
|
|
42
|
+
if (!workspaceRoot) {
|
|
43
|
+
throw new Error(
|
|
44
|
+
"Could not find workspace root.\n" +
|
|
45
|
+
"Ensure a package.json with a \"cwrespro\" key or a cwrespro.config.json exists in an ancestor directory."
|
|
46
|
+
);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// Resolve docRoot from config
|
|
50
|
+
const docRoot = resolveDocRoot(workspaceRoot);
|
|
51
|
+
|
|
52
|
+
if (command === "init") {
|
|
53
|
+
const { fileCount, basePath } = await initBase(cwd, docRoot);
|
|
54
|
+
process.stdout.write(`[cwgit] Base snapshot created: ${fileCount} files tracked.\n`);
|
|
55
|
+
process.stdout.write(`[cwgit] Stored at: ${basePath}\n`);
|
|
56
|
+
process.stdout.write(`[cwgit] Tip: add .cwgit/ to .gitignore if using Git.\n`);
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
if (command === "changes") {
|
|
61
|
+
const changes = await detectChanges(cwd, docRoot);
|
|
62
|
+
|
|
63
|
+
if (changes.length === 0) {
|
|
64
|
+
process.stdout.write("[cwgit] No changes detected.\n");
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const csvPath = await exportCsv(changes, cwd);
|
|
69
|
+
|
|
70
|
+
// Print summary
|
|
71
|
+
const deleted = changes.filter((c) => c.status === "D").length;
|
|
72
|
+
const modified = changes.filter((c) => c.status === "M").length;
|
|
73
|
+
const untracked = changes.filter((c) => c.status === "U").length;
|
|
74
|
+
|
|
75
|
+
process.stdout.write(`[cwgit] ${changes.length} change(s) detected:\n`);
|
|
76
|
+
|
|
77
|
+
if (modified > 0) {
|
|
78
|
+
process.stdout.write(` M (Modified): ${modified}\n`);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
if (untracked > 0) {
|
|
82
|
+
process.stdout.write(` U (Untracked): ${untracked}\n`);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
if (deleted > 0) {
|
|
86
|
+
process.stdout.write(` D (Deleted): ${deleted}\n`);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
process.stdout.write(`[cwgit] CSV exported: ${csvPath}\n`);
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
throw new Error(`Unknown command: "${command}". Use "cwgit --help" for usage.`);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
main().catch((error) => {
|
|
97
|
+
process.stderr.write(`[cwgit] Error: ${error.message}\n`);
|
|
98
|
+
process.exit(1);
|
|
99
|
+
});
|
package/package.json
CHANGED
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "cwresdev",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"private": false,
|
|
7
7
|
"type": "commonjs",
|
|
8
8
|
"main": "./src/index.js",
|
|
9
9
|
"bin": {
|
|
10
|
-
"cwrespro": "bin/cwrespro.js"
|
|
10
|
+
"cwrespro": "bin/cwrespro.js",
|
|
11
|
+
"cwgit": "bin/cwgit.js"
|
|
11
12
|
},
|
|
12
13
|
"files": [
|
|
13
14
|
"bin",
|
package/src/cwgit.js
ADDED
|
@@ -0,0 +1,362 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
const crypto = require("node:crypto");
|
|
4
|
+
const fs = require("node:fs");
|
|
5
|
+
const path = require("node:path");
|
|
6
|
+
|
|
7
|
+
const CWGIT_DIR = ".cwgit";
|
|
8
|
+
const IGNORED_DIRS = new Set([".cwgit", "node_modules", ".git"]);
|
|
9
|
+
const HASH_STREAM_THRESHOLD = 5 * 1024 * 1024; // 5 MB
|
|
10
|
+
|
|
11
|
+
// CSV-injection-dangerous prefixes (OWASP)
|
|
12
|
+
const CSV_DANGEROUS_CHARS = new Set(["=", "+", "-", "@", "\t", "\r"]);
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Walk up from startDir to find the nearest directory containing a package.json
|
|
16
|
+
* with a "cwrespro" key or a cwrespro.config.json file.
|
|
17
|
+
*/
|
|
18
|
+
function findWorkspaceRoot(startDir) {
|
|
19
|
+
let current = path.resolve(startDir);
|
|
20
|
+
const { root } = path.parse(current);
|
|
21
|
+
|
|
22
|
+
while (true) {
|
|
23
|
+
const pkgPath = path.join(current, "package.json");
|
|
24
|
+
|
|
25
|
+
if (fs.existsSync(pkgPath)) {
|
|
26
|
+
try {
|
|
27
|
+
const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf8"));
|
|
28
|
+
|
|
29
|
+
if (pkg.cwrespro && typeof pkg.cwrespro === "object") {
|
|
30
|
+
return current;
|
|
31
|
+
}
|
|
32
|
+
} catch (_) {
|
|
33
|
+
// malformed package.json — skip
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const configPath = path.join(current, "cwrespro.config.json");
|
|
38
|
+
|
|
39
|
+
if (fs.existsSync(configPath)) {
|
|
40
|
+
return current;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
if (current === root) {
|
|
44
|
+
return null;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
current = path.dirname(current);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Resolve the docRoot from the workspace root's config.
|
|
53
|
+
* Returns absolute docRoot path.
|
|
54
|
+
*/
|
|
55
|
+
function resolveDocRoot(workspaceRoot) {
|
|
56
|
+
const configPath = path.join(workspaceRoot, "cwrespro.config.json");
|
|
57
|
+
|
|
58
|
+
if (fs.existsSync(configPath)) {
|
|
59
|
+
try {
|
|
60
|
+
const cfg = JSON.parse(fs.readFileSync(configPath, "utf8"));
|
|
61
|
+
|
|
62
|
+
if (cfg.docRoot) {
|
|
63
|
+
return path.resolve(workspaceRoot, cfg.docRoot);
|
|
64
|
+
}
|
|
65
|
+
} catch (_) {
|
|
66
|
+
// fall through to package.json
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const pkgPath = path.join(workspaceRoot, "package.json");
|
|
71
|
+
|
|
72
|
+
if (fs.existsSync(pkgPath)) {
|
|
73
|
+
try {
|
|
74
|
+
const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf8"));
|
|
75
|
+
|
|
76
|
+
if (pkg.cwrespro && pkg.cwrespro.docRoot) {
|
|
77
|
+
return path.resolve(workspaceRoot, pkg.cwrespro.docRoot);
|
|
78
|
+
}
|
|
79
|
+
} catch (_) {
|
|
80
|
+
// fall through to default
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
return path.resolve(workspaceRoot, "virtual_env");
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Compute SHA-256 hash of a file.
|
|
89
|
+
* Uses one-shot API for small files, streaming for large files.
|
|
90
|
+
*/
|
|
91
|
+
async function hashFile(filePath) {
|
|
92
|
+
const stat = await fs.promises.stat(filePath);
|
|
93
|
+
|
|
94
|
+
if (stat.size <= HASH_STREAM_THRESHOLD) {
|
|
95
|
+
const data = await fs.promises.readFile(filePath);
|
|
96
|
+
return crypto.createHash("sha256").update(data).digest("hex");
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// Stream for large files (videos, images, etc.)
|
|
100
|
+
return new Promise((resolve, reject) => {
|
|
101
|
+
const hash = crypto.createHash("sha256");
|
|
102
|
+
const stream = fs.createReadStream(filePath);
|
|
103
|
+
|
|
104
|
+
stream.on("data", (chunk) => hash.update(chunk));
|
|
105
|
+
stream.on("end", () => resolve(hash.digest("hex")));
|
|
106
|
+
stream.on("error", reject);
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Recursively collect all files in a directory.
|
|
112
|
+
* Skips symlinks, IGNORED_DIRS, and validates paths stay within boundary.
|
|
113
|
+
*/
|
|
114
|
+
async function collectFiles(dir, boundary) {
|
|
115
|
+
const results = [];
|
|
116
|
+
const resolvedBoundary = path.resolve(boundary);
|
|
117
|
+
|
|
118
|
+
async function walk(current) {
|
|
119
|
+
let entries;
|
|
120
|
+
|
|
121
|
+
try {
|
|
122
|
+
entries = await fs.promises.readdir(current, { withFileTypes: true });
|
|
123
|
+
} catch (err) {
|
|
124
|
+
if (err.code === "EACCES" || err.code === "EPERM") {
|
|
125
|
+
return; // skip inaccessible directories
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
throw err;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
for (const entry of entries) {
|
|
132
|
+
if (IGNORED_DIRS.has(entry.name)) {
|
|
133
|
+
continue;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
const fullPath = path.join(current, entry.name);
|
|
137
|
+
const resolved = path.resolve(fullPath);
|
|
138
|
+
|
|
139
|
+
// Path traversal guard
|
|
140
|
+
if (!resolved.startsWith(resolvedBoundary + path.sep) && resolved !== resolvedBoundary) {
|
|
141
|
+
continue;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// Use lstat to detect symlinks (don't follow them)
|
|
145
|
+
let stat;
|
|
146
|
+
|
|
147
|
+
try {
|
|
148
|
+
stat = await fs.promises.lstat(fullPath);
|
|
149
|
+
} catch (err) {
|
|
150
|
+
if (err.code === "EBUSY" || err.code === "EPERM" || err.code === "ENOENT") {
|
|
151
|
+
continue; // skip locked/deleted files
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
throw err;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// Skip symlinks for security
|
|
158
|
+
if (stat.isSymbolicLink()) {
|
|
159
|
+
continue;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
if (stat.isDirectory()) {
|
|
163
|
+
await walk(fullPath);
|
|
164
|
+
} else if (stat.isFile()) {
|
|
165
|
+
results.push(fullPath);
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
await walk(dir);
|
|
171
|
+
return results;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Initialize (or re-initialize) the base snapshot for a project.
|
|
176
|
+
*/
|
|
177
|
+
async function initBase(projectDir, docRoot) {
|
|
178
|
+
const resolvedProject = path.resolve(projectDir);
|
|
179
|
+
const resolvedDocRoot = path.resolve(docRoot);
|
|
180
|
+
|
|
181
|
+
// Validate projectDir is inside or equal to docRoot
|
|
182
|
+
if (resolvedProject !== resolvedDocRoot && !resolvedProject.startsWith(resolvedDocRoot + path.sep)) {
|
|
183
|
+
throw new Error(`Project directory must be inside docRoot.\n Project: ${resolvedProject}\n DocRoot: ${resolvedDocRoot}`);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
const relPath = resolvedProject === resolvedDocRoot
|
|
187
|
+
? "."
|
|
188
|
+
: path.relative(resolvedDocRoot, resolvedProject);
|
|
189
|
+
|
|
190
|
+
const cwgitDir = path.join(resolvedDocRoot, CWGIT_DIR, relPath);
|
|
191
|
+
const basePath = path.join(cwgitDir, "base.json");
|
|
192
|
+
const tmpPath = basePath + ".tmp";
|
|
193
|
+
|
|
194
|
+
// Collect and hash files
|
|
195
|
+
const filePaths = await collectFiles(resolvedProject, resolvedProject);
|
|
196
|
+
const fileCount = filePaths.length;
|
|
197
|
+
|
|
198
|
+
process.stderr.write(`[cwgit] Hashing ${fileCount} files...\n`);
|
|
199
|
+
|
|
200
|
+
const files = {};
|
|
201
|
+
let processed = 0;
|
|
202
|
+
|
|
203
|
+
for (const filePath of filePaths) {
|
|
204
|
+
const relFilePath = path.relative(resolvedProject, filePath).replace(/\\/g, "/");
|
|
205
|
+
|
|
206
|
+
try {
|
|
207
|
+
files[relFilePath] = await hashFile(filePath);
|
|
208
|
+
} catch (err) {
|
|
209
|
+
if (err.code === "EBUSY" || err.code === "EPERM" || err.code === "ENOENT") {
|
|
210
|
+
process.stderr.write(`[cwgit] Warning: skipped inaccessible file: ${relFilePath}\n`);
|
|
211
|
+
continue;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
throw err;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
processed += 1;
|
|
218
|
+
|
|
219
|
+
if (processed % 100 === 0) {
|
|
220
|
+
process.stderr.write(`[cwgit] ${processed}/${fileCount} files hashed\n`);
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
const baseData = {
|
|
225
|
+
version: 1,
|
|
226
|
+
createdAt: new Date().toISOString(),
|
|
227
|
+
project: relPath,
|
|
228
|
+
files,
|
|
229
|
+
};
|
|
230
|
+
|
|
231
|
+
// Atomic write: write to tmp then rename
|
|
232
|
+
await fs.promises.mkdir(cwgitDir, { recursive: true });
|
|
233
|
+
await fs.promises.writeFile(tmpPath, JSON.stringify(baseData, null, 2), "utf8");
|
|
234
|
+
await fs.promises.rename(tmpPath, basePath);
|
|
235
|
+
|
|
236
|
+
return { fileCount: Object.keys(files).length, basePath };
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/**
|
|
240
|
+
* Detect changes between current state and base snapshot.
|
|
241
|
+
* Returns array of { file, status } where status is M, U, or D.
|
|
242
|
+
*/
|
|
243
|
+
async function detectChanges(projectDir, docRoot) {
|
|
244
|
+
const resolvedProject = path.resolve(projectDir);
|
|
245
|
+
const resolvedDocRoot = path.resolve(docRoot);
|
|
246
|
+
|
|
247
|
+
if (resolvedProject !== resolvedDocRoot && !resolvedProject.startsWith(resolvedDocRoot + path.sep)) {
|
|
248
|
+
throw new Error(`Project directory must be inside docRoot.\n Project: ${resolvedProject}\n DocRoot: ${resolvedDocRoot}`);
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
const relPath = resolvedProject === resolvedDocRoot
|
|
252
|
+
? "."
|
|
253
|
+
: path.relative(resolvedDocRoot, resolvedProject);
|
|
254
|
+
|
|
255
|
+
const basePath = path.join(resolvedDocRoot, CWGIT_DIR, relPath, "base.json");
|
|
256
|
+
|
|
257
|
+
if (!fs.existsSync(basePath)) {
|
|
258
|
+
throw new Error(`No base snapshot found. Run "cwgit init" first.\n Expected: ${basePath}`);
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
const baseData = JSON.parse(await fs.promises.readFile(basePath, "utf8"));
|
|
262
|
+
const baseFiles = baseData.files || {};
|
|
263
|
+
|
|
264
|
+
// Collect current files
|
|
265
|
+
const currentPaths = await collectFiles(resolvedProject, resolvedProject);
|
|
266
|
+
const currentFiles = {};
|
|
267
|
+
|
|
268
|
+
process.stderr.write(`[cwgit] Scanning ${currentPaths.length} files...\n`);
|
|
269
|
+
|
|
270
|
+
for (const filePath of currentPaths) {
|
|
271
|
+
const relFilePath = path.relative(resolvedProject, filePath).replace(/\\/g, "/");
|
|
272
|
+
|
|
273
|
+
try {
|
|
274
|
+
currentFiles[relFilePath] = await hashFile(filePath);
|
|
275
|
+
} catch (err) {
|
|
276
|
+
if (err.code === "EBUSY" || err.code === "EPERM" || err.code === "ENOENT") {
|
|
277
|
+
process.stderr.write(`[cwgit] Warning: skipped inaccessible file: ${relFilePath}\n`);
|
|
278
|
+
continue;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
throw err;
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
const changes = [];
|
|
286
|
+
|
|
287
|
+
// Check for modified and deleted files
|
|
288
|
+
for (const [file, hash] of Object.entries(baseFiles)) {
|
|
289
|
+
if (!(file in currentFiles)) {
|
|
290
|
+
changes.push({ file, status: "D" });
|
|
291
|
+
} else if (currentFiles[file] !== hash) {
|
|
292
|
+
changes.push({ file, status: "M" });
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
// Check for new/untracked files
|
|
297
|
+
for (const file of Object.keys(currentFiles)) {
|
|
298
|
+
if (!(file in baseFiles)) {
|
|
299
|
+
changes.push({ file, status: "U" });
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
// Sort: D first, then M, then U; alphabetical within each group
|
|
304
|
+
changes.sort((a, b) => {
|
|
305
|
+
const order = { D: 0, M: 1, U: 2 };
|
|
306
|
+
|
|
307
|
+
if (order[a.status] !== order[b.status]) {
|
|
308
|
+
return order[a.status] - order[b.status];
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
return a.file.localeCompare(b.file);
|
|
312
|
+
});
|
|
313
|
+
|
|
314
|
+
return changes;
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
/**
|
|
318
|
+
* Sanitize a CSV cell value to prevent formula injection.
|
|
319
|
+
*/
|
|
320
|
+
function sanitizeCsvCell(value) {
|
|
321
|
+
if (typeof value !== "string" || value.length === 0) {
|
|
322
|
+
return value;
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
if (CSV_DANGEROUS_CHARS.has(value[0])) {
|
|
326
|
+
return "'" + value;
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
return value;
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
/**
|
|
333
|
+
* Export changes to a CSV file.
|
|
334
|
+
*/
|
|
335
|
+
async function exportCsv(changes, outputDir) {
|
|
336
|
+
const csvPath = path.join(outputDir, "cwgit-changes.csv");
|
|
337
|
+
const lines = ["file,status"];
|
|
338
|
+
|
|
339
|
+
for (const { file, status } of changes) {
|
|
340
|
+
const safeFile = sanitizeCsvCell(file);
|
|
341
|
+
// Escape double quotes in file path and wrap in quotes if contains comma
|
|
342
|
+
const escaped = safeFile.includes(",") || safeFile.includes('"')
|
|
343
|
+
? `"${safeFile.replace(/"/g, '""')}"`
|
|
344
|
+
: safeFile;
|
|
345
|
+
lines.push(`${escaped},${status}`);
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
await fs.promises.writeFile(csvPath, lines.join("\n") + "\n", "utf8");
|
|
349
|
+
return csvPath;
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
module.exports = {
|
|
353
|
+
CWGIT_DIR,
|
|
354
|
+
collectFiles,
|
|
355
|
+
detectChanges,
|
|
356
|
+
exportCsv,
|
|
357
|
+
findWorkspaceRoot,
|
|
358
|
+
hashFile,
|
|
359
|
+
initBase,
|
|
360
|
+
resolveDocRoot,
|
|
361
|
+
sanitizeCsvCell,
|
|
362
|
+
};
|
package/src/proxy.js
CHANGED
|
@@ -91,12 +91,22 @@ async function proxyRequest({ req, res, target, injectHtml = false, injectCookie
|
|
|
91
91
|
outHeaders.cookie = _stagingCookie;
|
|
92
92
|
}
|
|
93
93
|
|
|
94
|
-
const
|
|
94
|
+
const fetchOpts = {
|
|
95
95
|
method: req.method,
|
|
96
96
|
headers: outHeaders,
|
|
97
97
|
body,
|
|
98
98
|
redirect: "manual",
|
|
99
|
-
}
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
let upstream;
|
|
102
|
+
|
|
103
|
+
try {
|
|
104
|
+
upstream = await fetch(target, fetchOpts);
|
|
105
|
+
} catch (err) {
|
|
106
|
+
// Retry once – works around a transient Node.js v24 undici parser bug
|
|
107
|
+
// (ERR_ASSERTION in Parser.finish) that can reject the fetch promise.
|
|
108
|
+
upstream = await fetch(target, fetchOpts);
|
|
109
|
+
}
|
|
100
110
|
|
|
101
111
|
res.status(upstream.status);
|
|
102
112
|
|
package/src/server.js
CHANGED
|
@@ -231,6 +231,11 @@ async function startDevServer(config) {
|
|
|
231
231
|
});
|
|
232
232
|
});
|
|
233
233
|
|
|
234
|
+
// Block HTTP access to .cwgit tracking data
|
|
235
|
+
app.use("/.cwgit", (req, res) => {
|
|
236
|
+
res.status(404).type("text/plain").send("Not Found");
|
|
237
|
+
});
|
|
238
|
+
|
|
234
239
|
app.get(CLIENT_SCRIPT_ROUTE, (req, res) => {
|
|
235
240
|
res.type("application/javascript");
|
|
236
241
|
res.send(clientScript);
|
|
@@ -340,9 +345,15 @@ async function startDevServer(config) {
|
|
|
340
345
|
return (filePath) => filePath.replace(/\\/g, "/") === normalized;
|
|
341
346
|
});
|
|
342
347
|
|
|
348
|
+
// Always ignore .cwgit tracking folder (prevents reload loops)
|
|
349
|
+
const cwgitIgnore = (filePath) => {
|
|
350
|
+
const normalized = filePath.replace(/\\/g, "/");
|
|
351
|
+
return normalized.includes("/.cwgit/") || normalized.endsWith("/.cwgit");
|
|
352
|
+
};
|
|
353
|
+
|
|
343
354
|
const watcher = chokidar.watch(config.docRoot, {
|
|
344
355
|
ignoreInitial: true,
|
|
345
|
-
ignored,
|
|
356
|
+
ignored: [cwgitIgnore, ...ignored],
|
|
346
357
|
});
|
|
347
358
|
|
|
348
359
|
watcher.on("all", (eventName, filePath) => {
|