docugate 0.2.0 → 0.2.1
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/dist/tour-server.js +114 -5
- package/package.json +1 -1
- package/pill/dist/pill.js +926 -422
package/dist/tour-server.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
|
+
import { spawn } from 'node:child_process';
|
|
1
2
|
import { createServer } from 'node:http';
|
|
2
3
|
import { existsSync, mkdirSync, readFileSync, realpathSync, watch, writeFileSync } from 'node:fs';
|
|
3
|
-
import { join } from 'node:path';
|
|
4
|
+
import { join, relative, resolve, isAbsolute } from 'node:path';
|
|
4
5
|
import { fileURLToPath } from 'node:url';
|
|
5
6
|
import { loadTours, matchRoute, parseTourFile, routeToFilename, serializeTourFile } from './tour.js';
|
|
6
7
|
// ---------------------------------------------------------------------------
|
|
@@ -94,7 +95,7 @@ function handleGetTour(req, res, root) {
|
|
|
94
95
|
sendJson(res, 404, { error: 'not found' });
|
|
95
96
|
return;
|
|
96
97
|
}
|
|
97
|
-
sendJson(res, 200, { route: match.route, title: match.title, stops: match.stops });
|
|
98
|
+
sendJson(res, 200, { route: match.route, title: match.title, file: match.file, stops: match.stops });
|
|
98
99
|
}
|
|
99
100
|
function handleGetEvents(req, res, root) {
|
|
100
101
|
const tourDir = join(root, '.docugate', 'tour');
|
|
@@ -134,7 +135,7 @@ async function handlePutStop(req, res, root) {
|
|
|
134
135
|
sendJson(res, 400, { error: 'invalid JSON body' });
|
|
135
136
|
return;
|
|
136
137
|
}
|
|
137
|
-
const { route, title: bodyTitle, stop } = body;
|
|
138
|
+
const { route, title: bodyTitle, stop, previousTarget } = body;
|
|
138
139
|
if (!route || !stop) {
|
|
139
140
|
sendJson(res, 400, { error: 'body must have route and stop' });
|
|
140
141
|
return;
|
|
@@ -161,8 +162,9 @@ async function handlePutStop(req, res, root) {
|
|
|
161
162
|
stops: [],
|
|
162
163
|
};
|
|
163
164
|
}
|
|
164
|
-
// Upsert: find by target
|
|
165
|
-
|
|
165
|
+
// Upsert: find the stop being edited (by its old target when the edit
|
|
166
|
+
// changed it) and update it in place, or append a new one.
|
|
167
|
+
const idx = tour.stops.findIndex((s) => s.target === (previousTarget ?? stop.target));
|
|
166
168
|
if (idx >= 0) {
|
|
167
169
|
tour.stops[idx] = stop;
|
|
168
170
|
}
|
|
@@ -172,6 +174,91 @@ async function handlePutStop(req, res, root) {
|
|
|
172
174
|
writeFileSync(filepath, serializeTourFile(tour));
|
|
173
175
|
sendJson(res, 200, { ok: true });
|
|
174
176
|
}
|
|
177
|
+
/** DELETE /stop { route, target }: removes one stop, leaving the rest of the file as it was. */
|
|
178
|
+
async function handleDeleteStop(req, res, root) {
|
|
179
|
+
let body;
|
|
180
|
+
try {
|
|
181
|
+
body = JSON.parse(await readBody(req));
|
|
182
|
+
}
|
|
183
|
+
catch {
|
|
184
|
+
sendJson(res, 400, { error: 'invalid JSON body' });
|
|
185
|
+
return;
|
|
186
|
+
}
|
|
187
|
+
const existing = loadTours(root).find((t) => t.route === body.route);
|
|
188
|
+
if (!existing?.file || !body.target) {
|
|
189
|
+
sendJson(res, 404, { error: 'no such stop' });
|
|
190
|
+
return;
|
|
191
|
+
}
|
|
192
|
+
const filepath = join(root, existing.file);
|
|
193
|
+
const tour = parseTourFile(readFileSync(filepath, 'utf8'), existing.file);
|
|
194
|
+
const before = tour.stops.length;
|
|
195
|
+
tour.stops = tour.stops.filter((s) => s.target !== body.target);
|
|
196
|
+
if (tour.stops.length === before) {
|
|
197
|
+
sendJson(res, 404, { error: 'no such stop' });
|
|
198
|
+
return;
|
|
199
|
+
}
|
|
200
|
+
writeFileSync(filepath, serializeTourFile(tour));
|
|
201
|
+
sendJson(res, 200, { ok: true });
|
|
202
|
+
}
|
|
203
|
+
/**
|
|
204
|
+
* The editor `docugate tour serve` was started from, as the command that opens
|
|
205
|
+
* a file at a line. DOCUGATE_EDITOR wins; otherwise the integrated terminal's
|
|
206
|
+
* environment says which VS Code family editor it is.
|
|
207
|
+
*/
|
|
208
|
+
export function editorCommand(env = process.env) {
|
|
209
|
+
if (env.DOCUGATE_EDITOR)
|
|
210
|
+
return env.DOCUGATE_EDITOR;
|
|
211
|
+
const hint = `${env.VSCODE_GIT_ASKPASS_NODE ?? ''} ${env.VSCODE_CWD ?? ''} ${env.CURSOR_TRACE_ID ? 'cursor' : ''}`.toLowerCase();
|
|
212
|
+
if (hint.includes('cursor'))
|
|
213
|
+
return 'cursor';
|
|
214
|
+
if (hint.includes('windsurf'))
|
|
215
|
+
return 'windsurf';
|
|
216
|
+
if (hint.includes('insiders'))
|
|
217
|
+
return 'code-insiders';
|
|
218
|
+
return 'code';
|
|
219
|
+
}
|
|
220
|
+
/** The 1-based line of the first occurrence of `find` in `text`, or 1. */
|
|
221
|
+
export function lineOf(text, find) {
|
|
222
|
+
if (!find)
|
|
223
|
+
return 1;
|
|
224
|
+
const index = text.split(/\r?\n/).findIndex((line) => line.includes(find));
|
|
225
|
+
return index >= 0 ? index + 1 : 1;
|
|
226
|
+
}
|
|
227
|
+
/**
|
|
228
|
+
* POST /open { path, find? }: opens a file of this repository in the editor,
|
|
229
|
+
* at the line that contains `find` (the element's data-tour value), so "Code"
|
|
230
|
+
* in the pill jumps straight to the element. Only files inside the repository.
|
|
231
|
+
*/
|
|
232
|
+
async function handleOpen(req, res, root) {
|
|
233
|
+
let body;
|
|
234
|
+
try {
|
|
235
|
+
body = JSON.parse(await readBody(req));
|
|
236
|
+
}
|
|
237
|
+
catch {
|
|
238
|
+
sendJson(res, 400, { error: 'invalid JSON body' });
|
|
239
|
+
return;
|
|
240
|
+
}
|
|
241
|
+
const abs = resolve(root, body.path ?? '');
|
|
242
|
+
const rel = relative(root, abs);
|
|
243
|
+
if (!body.path || rel.startsWith('..') || isAbsolute(rel) || !existsSync(abs) || /["%^&|<>`$]/.test(abs)) {
|
|
244
|
+
sendJson(res, 404, { error: 'no such file in this repository' });
|
|
245
|
+
return;
|
|
246
|
+
}
|
|
247
|
+
const line = lineOf(readFileSync(abs, 'utf8'), body.find);
|
|
248
|
+
const command = editorCommand();
|
|
249
|
+
// One command string, the path quoted: editor launchers are .cmd files on
|
|
250
|
+
// Windows, which only run through a shell.
|
|
251
|
+
const child = spawn(`${command} -g "${abs}:${line}"`, { shell: true, stdio: 'ignore' });
|
|
252
|
+
child.on('error', () => sendJson(res, 501, { error: `could not run ${command}` }));
|
|
253
|
+
child.on('exit', (code) => {
|
|
254
|
+
if (res.headersSent)
|
|
255
|
+
return;
|
|
256
|
+
if (code === 0)
|
|
257
|
+
sendJson(res, 200, { ok: true, line });
|
|
258
|
+
else
|
|
259
|
+
sendJson(res, 501, { error: `could not run ${command}`, line });
|
|
260
|
+
});
|
|
261
|
+
}
|
|
175
262
|
function handleGetPill(res) {
|
|
176
263
|
// pill/dist/pill.js is resolved relative to this package's root directory.
|
|
177
264
|
// import.meta.url points at dist/tour-server.js at runtime, so go up one level.
|
|
@@ -194,6 +281,18 @@ export function startTourServer(root, port) {
|
|
|
194
281
|
}
|
|
195
282
|
const url = new URL(req.url ?? '/', 'http://localhost');
|
|
196
283
|
const path = url.pathname;
|
|
284
|
+
// The pill runs on the app's own port, so a JSON PUT from it is a
|
|
285
|
+
// cross-origin request: the browser asks first, and must get a yes.
|
|
286
|
+
if (req.method === 'OPTIONS') {
|
|
287
|
+
res.writeHead(204, {
|
|
288
|
+
'Access-Control-Allow-Origin': '*',
|
|
289
|
+
'Access-Control-Allow-Methods': 'GET, PUT, POST, DELETE',
|
|
290
|
+
'Access-Control-Allow-Headers': 'Content-Type',
|
|
291
|
+
'Access-Control-Max-Age': '600',
|
|
292
|
+
});
|
|
293
|
+
res.end();
|
|
294
|
+
return;
|
|
295
|
+
}
|
|
197
296
|
if (req.method === 'GET' && path === '/tour') {
|
|
198
297
|
handleGetTour(req, res, root);
|
|
199
298
|
}
|
|
@@ -205,6 +304,16 @@ export function startTourServer(root, port) {
|
|
|
205
304
|
sendJson(res, 500, { error: err.message });
|
|
206
305
|
});
|
|
207
306
|
}
|
|
307
|
+
else if (req.method === 'DELETE' && path === '/stop') {
|
|
308
|
+
handleDeleteStop(req, res, root).catch((err) => {
|
|
309
|
+
sendJson(res, 500, { error: err.message });
|
|
310
|
+
});
|
|
311
|
+
}
|
|
312
|
+
else if (req.method === 'POST' && path === '/open') {
|
|
313
|
+
handleOpen(req, res, root).catch((err) => {
|
|
314
|
+
sendJson(res, 500, { error: err.message });
|
|
315
|
+
});
|
|
316
|
+
}
|
|
208
317
|
else if (req.method === 'GET' && path === '/pill.js') {
|
|
209
318
|
handleGetPill(res);
|
|
210
319
|
}
|
package/package.json
CHANGED