@maplab/hyperdoc 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/dist/index.js ADDED
@@ -0,0 +1,962 @@
1
+ #!/usr/bin/env node
2
+ #!/usr/bin/env node
3
+ "use strict";
4
+ var __create = Object.create;
5
+ var __defProp = Object.defineProperty;
6
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
7
+ var __getOwnPropNames = Object.getOwnPropertyNames;
8
+ var __getProtoOf = Object.getPrototypeOf;
9
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
10
+ var __commonJS = (cb, mod) => function __require() {
11
+ return mod || (0, cb[__getOwnPropNames(cb)[0]])((mod = { exports: {} }).exports, mod), mod.exports;
12
+ };
13
+ var __copyProps = (to, from, except, desc) => {
14
+ if (from && typeof from === "object" || typeof from === "function") {
15
+ for (let key of __getOwnPropNames(from))
16
+ if (!__hasOwnProp.call(to, key) && key !== except)
17
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
18
+ }
19
+ return to;
20
+ };
21
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
22
+ // If the importer is in node compatibility mode or this is not an ESM
23
+ // file that has been converted to a CommonJS file using a Babel-
24
+ // compatible transform (i.e. "__esModule" has not been set), then set
25
+ // "default" to the CommonJS "module.exports" for node compatibility.
26
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
27
+ mod
28
+ ));
29
+
30
+ // ../../packages/core/dist/errors.js
31
+ var require_errors = __commonJS({
32
+ "../../packages/core/dist/errors.js"(exports2) {
33
+ "use strict";
34
+ Object.defineProperty(exports2, "__esModule", { value: true });
35
+ exports2.HyperdocError = void 0;
36
+ exports2.formatError = formatError2;
37
+ var HyperdocError = class extends Error {
38
+ constructor(message) {
39
+ super(message);
40
+ this.name = "HyperdocError";
41
+ }
42
+ };
43
+ exports2.HyperdocError = HyperdocError;
44
+ function formatError2(error, includeStack) {
45
+ if (error instanceof Error) {
46
+ if (includeStack && error.stack) {
47
+ return error.stack;
48
+ }
49
+ return error.message;
50
+ }
51
+ if (typeof error === "string") {
52
+ return error;
53
+ }
54
+ return "Unknown error";
55
+ }
56
+ }
57
+ });
58
+
59
+ // ../../packages/core/dist/utils/logger.js
60
+ var require_logger = __commonJS({
61
+ "../../packages/core/dist/utils/logger.js"(exports2) {
62
+ "use strict";
63
+ Object.defineProperty(exports2, "__esModule", { value: true });
64
+ exports2.logger = exports2.LogLevel = void 0;
65
+ var LogLevel3;
66
+ (function(LogLevel4) {
67
+ LogLevel4[LogLevel4["DEBUG"] = 10] = "DEBUG";
68
+ LogLevel4[LogLevel4["INFO"] = 20] = "INFO";
69
+ LogLevel4[LogLevel4["WARN"] = 30] = "WARN";
70
+ LogLevel4[LogLevel4["ERROR"] = 40] = "ERROR";
71
+ })(LogLevel3 || (exports2.LogLevel = LogLevel3 = {}));
72
+ var Logger = class {
73
+ constructor() {
74
+ this.level = LogLevel3.INFO;
75
+ }
76
+ setLevel(level) {
77
+ this.level = level;
78
+ }
79
+ getLevel() {
80
+ return this.level;
81
+ }
82
+ logIf(level, fn, message) {
83
+ if (this.level <= level) {
84
+ fn(message);
85
+ }
86
+ }
87
+ debug(message) {
88
+ this.logIf(LogLevel3.DEBUG, console.log, message);
89
+ }
90
+ info(message) {
91
+ this.logIf(LogLevel3.INFO, console.log, message);
92
+ }
93
+ warn(message) {
94
+ this.logIf(LogLevel3.WARN, console.warn, message);
95
+ }
96
+ error(message) {
97
+ this.logIf(LogLevel3.ERROR, console.error, message);
98
+ }
99
+ };
100
+ exports2.logger = new Logger();
101
+ }
102
+ });
103
+
104
+ // ../../packages/core/dist/schemas/project.js
105
+ var require_project = __commonJS({
106
+ "../../packages/core/dist/schemas/project.js"(exports2) {
107
+ "use strict";
108
+ Object.defineProperty(exports2, "__esModule", { value: true });
109
+ exports2.projectConfigSchema = void 0;
110
+ var zod_1 = require("zod");
111
+ exports2.projectConfigSchema = zod_1.z.object({
112
+ name: zod_1.z.string().trim().min(1).max(200).optional()
113
+ }).strict();
114
+ }
115
+ });
116
+
117
+ // ../../packages/core/dist/index.js
118
+ var require_dist = __commonJS({
119
+ "../../packages/core/dist/index.js"(exports2) {
120
+ "use strict";
121
+ var __createBinding = exports2 && exports2.__createBinding || (Object.create ? (function(o, m, k, k2) {
122
+ if (k2 === void 0) k2 = k;
123
+ var desc = Object.getOwnPropertyDescriptor(m, k);
124
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
125
+ desc = { enumerable: true, get: function() {
126
+ return m[k];
127
+ } };
128
+ }
129
+ Object.defineProperty(o, k2, desc);
130
+ }) : (function(o, m, k, k2) {
131
+ if (k2 === void 0) k2 = k;
132
+ o[k2] = m[k];
133
+ }));
134
+ var __exportStar = exports2 && exports2.__exportStar || function(m, exports3) {
135
+ for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports3, p)) __createBinding(exports3, m, p);
136
+ };
137
+ Object.defineProperty(exports2, "__esModule", { value: true });
138
+ __exportStar(require_errors(), exports2);
139
+ __exportStar(require_logger(), exports2);
140
+ __exportStar(require_project(), exports2);
141
+ }
142
+ });
143
+
144
+ // ../../packages/adapter-markdown/dist/index.js
145
+ var require_dist2 = __commonJS({
146
+ "../../packages/adapter-markdown/dist/index.js"(exports2) {
147
+ "use strict";
148
+ Object.defineProperty(exports2, "__esModule", { value: true });
149
+ exports2.extractMarkdownTitle = exports2.stripMarkdownMetadataHeader = exports2.getMarkdownMetadata = void 0;
150
+ var getMarkdownMetadata = (content) => {
151
+ const match = content.match(/^\uFEFF?\s*<!--\s*hyperdoc:([A-Za-z0-9+/=]+)\s*-->\s*(?:\r?\n|$)/i);
152
+ if (!match)
153
+ return null;
154
+ try {
155
+ const json = Buffer.from(match[1], "base64").toString("utf-8");
156
+ const parsed = JSON.parse(json);
157
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
158
+ return null;
159
+ }
160
+ const tagsRaw = parsed.tags;
161
+ const tags = Array.isArray(tagsRaw) ? tagsRaw.filter((tag) => typeof tag === "string") : [];
162
+ const pinned = parsed.pinned === true;
163
+ const title = typeof parsed.title === "string" ? parsed.title ?? null : null;
164
+ return { tags, pinned, title };
165
+ } catch {
166
+ return null;
167
+ }
168
+ };
169
+ exports2.getMarkdownMetadata = getMarkdownMetadata;
170
+ var metadataHeaderStrip = /^\uFEFF?\s*<!--\s*hyperdoc:[A-Za-z0-9+/=]+\s*-->\s*(?:\r?\n|$)/i;
171
+ var stripMetadataSpacer = (value) => {
172
+ if (!value)
173
+ return value;
174
+ const match = value.match(/^\s*(?:\r?\n)/);
175
+ if (match) {
176
+ return value.slice(match[0].length);
177
+ }
178
+ return value;
179
+ };
180
+ var stripMarkdownMetadataHeader = (content) => {
181
+ let output = content.replace(/^\uFEFF/, "");
182
+ let previous = "";
183
+ while (output !== previous) {
184
+ previous = output;
185
+ output = output.replace(metadataHeaderStrip, "");
186
+ }
187
+ return stripMetadataSpacer(output);
188
+ };
189
+ exports2.stripMarkdownMetadataHeader = stripMarkdownMetadataHeader;
190
+ var parseLegacyTitle = (content) => {
191
+ const match = content.match(/^\s*<!--\s*title:\s*(.+?)\s*-->\s*(?:\r?\n|$)/i);
192
+ return match ? match[1].trim() : null;
193
+ };
194
+ var extractMarkdownTitle = (content) => parseLegacyTitle(content);
195
+ exports2.extractMarkdownTitle = extractMarkdownTitle;
196
+ }
197
+ });
198
+
199
+ // ../../packages/server/dist/createApp.js
200
+ var require_createApp = __commonJS({
201
+ "../../packages/server/dist/createApp.js"(exports2) {
202
+ "use strict";
203
+ var __importDefault = exports2 && exports2.__importDefault || function(mod) {
204
+ return mod && mod.__esModule ? mod : { "default": mod };
205
+ };
206
+ Object.defineProperty(exports2, "__esModule", { value: true });
207
+ exports2.createApp = createApp2;
208
+ var express_1 = __importDefault(require("express"));
209
+ var chokidar_1 = __importDefault(require("chokidar"));
210
+ var fs_1 = __importDefault(require("fs"));
211
+ var path_1 = __importDefault(require("path"));
212
+ var adapter_markdown_1 = require_dist2();
213
+ var core_1 = require_dist();
214
+ function createApp2(options) {
215
+ const app = (0, express_1.default)();
216
+ const rootDir = path_1.default.resolve(options.rootDir);
217
+ const rootDirPrefix = rootDir.endsWith(path_1.default.sep) ? rootDir : `${rootDir}${path_1.default.sep}`;
218
+ const metadataFileName = "metadata.json";
219
+ const hyperdocDirName = ".hyperdoc";
220
+ const archiveDirName = "archive";
221
+ const trashDirName = "trash";
222
+ const projectFileName = "project.json";
223
+ const resolveDocPath = (name) => path_1.default.resolve(rootDir, name);
224
+ const isDocPathSafe = (filePath) => filePath.startsWith(rootDirPrefix);
225
+ const isMetadataName = (name) => name === metadataFileName;
226
+ const hyperdocDir = path_1.default.join(rootDir, hyperdocDirName);
227
+ const projectFilePath = path_1.default.join(hyperdocDir, projectFileName);
228
+ const ensureHyperdocDir = () => {
229
+ if (!fs_1.default.existsSync(hyperdocDir)) {
230
+ fs_1.default.mkdirSync(hyperdocDir, { recursive: true });
231
+ }
232
+ };
233
+ const readProjectConfig = () => {
234
+ if (!fs_1.default.existsSync(projectFilePath)) {
235
+ return { config: null, error: null };
236
+ }
237
+ try {
238
+ const raw = fs_1.default.readFileSync(projectFilePath, "utf-8");
239
+ const parsed = JSON.parse(raw);
240
+ const result = core_1.projectConfigSchema.safeParse(parsed);
241
+ if (!result.success) {
242
+ return { config: null, error: result.error };
243
+ }
244
+ return { config: result.data, error: null };
245
+ } catch (error) {
246
+ return { config: null, error };
247
+ }
248
+ };
249
+ const writeProjectConfig = (config) => {
250
+ ensureHyperdocDir();
251
+ const payload = JSON.stringify(config, null, 2);
252
+ fs_1.default.writeFileSync(projectFilePath, `${payload}
253
+ `, "utf-8");
254
+ };
255
+ const decodeJsonMetadata = (content) => {
256
+ try {
257
+ const parsed = JSON.parse(content);
258
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
259
+ return null;
260
+ }
261
+ const metadataRaw = parsed.metadata;
262
+ if (!metadataRaw || typeof metadataRaw !== "object" || Array.isArray(metadataRaw)) {
263
+ return { tags: [] };
264
+ }
265
+ const metadata = metadataRaw;
266
+ const tags = Array.isArray(metadata.tags) ? metadata.tags.filter((tag) => typeof tag === "string") : [];
267
+ const pinned = metadata.pinned === true;
268
+ return { tags, pinned };
269
+ } catch {
270
+ return null;
271
+ }
272
+ };
273
+ const normalizeTags = (value) => {
274
+ if (!Array.isArray(value))
275
+ return [];
276
+ const cleaned = value.map((tag) => typeof tag === "string" ? tag.trim() : "").filter((tag) => tag.length > 0 && /^[a-z]+$/.test(tag));
277
+ return Array.from(new Set(cleaned)).sort();
278
+ };
279
+ const tagsFromContent = (name, content) => {
280
+ const ext = path_1.default.extname(name).toLowerCase();
281
+ if (ext === ".md") {
282
+ return (0, adapter_markdown_1.getMarkdownMetadata)(content)?.tags ?? [];
283
+ }
284
+ if (ext === ".json") {
285
+ return decodeJsonMetadata(content)?.tags ?? [];
286
+ }
287
+ return [];
288
+ };
289
+ const fileTags = /* @__PURE__ */ new Map();
290
+ const tagIndex = /* @__PURE__ */ new Set();
291
+ const indexFileTags = (name) => {
292
+ const filePath = resolveDocPath(name);
293
+ if (!fs_1.default.existsSync(filePath))
294
+ return;
295
+ try {
296
+ const content = fs_1.default.readFileSync(filePath, "utf-8");
297
+ const tags = normalizeTags(tagsFromContent(name, content));
298
+ fileTags.set(name, tags);
299
+ } catch {
300
+ fileTags.set(name, []);
301
+ }
302
+ };
303
+ const rebuildTagIndex = () => {
304
+ tagIndex.clear();
305
+ for (const tags of fileTags.values()) {
306
+ for (const tag of tags)
307
+ tagIndex.add(tag);
308
+ }
309
+ };
310
+ const refreshTagsFromDisk = () => {
311
+ fileTags.clear();
312
+ const entries = fs_1.default.readdirSync(rootDir, { withFileTypes: true }).filter((entry) => entry.isFile()).filter((entry) => entry.name.endsWith(".md") || entry.name.endsWith(".json")).filter((entry) => !isMetadataName(entry.name));
313
+ for (const entry of entries) {
314
+ indexFileTags(entry.name);
315
+ }
316
+ rebuildTagIndex();
317
+ };
318
+ refreshTagsFromDisk();
319
+ const validateDocName = (name) => {
320
+ if (!name)
321
+ return "Missing name";
322
+ if (name.includes("/") || name.includes("\\") || name.includes("..")) {
323
+ return "Invalid name";
324
+ }
325
+ return null;
326
+ };
327
+ const displayNameFromFileName = (name) => {
328
+ const base = name.replace(/\.(md|json)$/i, "");
329
+ const spaced = base.replace(/-+/g, " ").trim();
330
+ if (!spaced)
331
+ return base;
332
+ return `${spaced.charAt(0).toUpperCase()}${spaced.slice(1)}`;
333
+ };
334
+ app.use(express_1.default.text({ type: "*/*", limit: "5mb" }));
335
+ if (options.staticDir && fs_1.default.existsSync(options.staticDir)) {
336
+ app.use(express_1.default.static(options.staticDir));
337
+ }
338
+ app.get("/api/project", (req, res) => {
339
+ const { config, error } = readProjectConfig();
340
+ if (error) {
341
+ res.status(400).json({ error: "Invalid project config" });
342
+ return;
343
+ }
344
+ res.json({
345
+ name: config?.name ?? null,
346
+ exists: config !== null
347
+ });
348
+ });
349
+ app.put("/api/project", (req, res) => {
350
+ const raw = typeof req.body === "string" ? req.body : "";
351
+ if (!raw.trim()) {
352
+ res.status(400).send("Missing body");
353
+ return;
354
+ }
355
+ let parsed;
356
+ try {
357
+ parsed = JSON.parse(raw);
358
+ } catch {
359
+ res.status(400).send("Invalid JSON");
360
+ return;
361
+ }
362
+ const result = core_1.projectConfigSchema.safeParse(parsed);
363
+ if (!result.success) {
364
+ res.status(400).json({ error: "Invalid project config" });
365
+ return;
366
+ }
367
+ writeProjectConfig(result.data);
368
+ res.json({ name: result.data.name ?? null });
369
+ });
370
+ app.get("/api/docs", (req, res) => {
371
+ const entries = fs_1.default.readdirSync(rootDir, { withFileTypes: true }).filter((entry) => entry.isFile()).filter((entry) => entry.name.endsWith(".md") || entry.name.endsWith(".json")).filter((entry) => !isMetadataName(entry.name)).map((entry) => ({
372
+ name: entry.name,
373
+ type: entry.name.endsWith(".md") ? "markdown" : "json"
374
+ })).map((entry) => {
375
+ const filePath = resolveDocPath(entry.name);
376
+ if (entry.type === "markdown") {
377
+ let title = null;
378
+ let tags2 = [];
379
+ let pinned2 = false;
380
+ try {
381
+ const content = fs_1.default.readFileSync(filePath, "utf-8");
382
+ title = (0, adapter_markdown_1.extractMarkdownTitle)(content);
383
+ const metadata = (0, adapter_markdown_1.getMarkdownMetadata)(content);
384
+ tags2 = normalizeTags(metadata?.tags ?? []);
385
+ pinned2 = metadata?.pinned ?? false;
386
+ } catch {
387
+ title = null;
388
+ tags2 = [];
389
+ pinned2 = false;
390
+ }
391
+ return {
392
+ ...entry,
393
+ title,
394
+ tags: tags2,
395
+ pinned: pinned2,
396
+ displayName: title && title.length > 0 ? title : displayNameFromFileName(entry.name)
397
+ };
398
+ }
399
+ let tags = [];
400
+ let pinned = false;
401
+ try {
402
+ const content = fs_1.default.readFileSync(filePath, "utf-8");
403
+ const metadata = decodeJsonMetadata(content);
404
+ tags = normalizeTags(metadata?.tags ?? []);
405
+ pinned = metadata?.pinned ?? false;
406
+ } catch {
407
+ tags = [];
408
+ pinned = false;
409
+ }
410
+ return {
411
+ ...entry,
412
+ title: null,
413
+ tags,
414
+ pinned,
415
+ displayName: displayNameFromFileName(entry.name)
416
+ };
417
+ });
418
+ res.json(entries);
419
+ });
420
+ app.get("/api/docs/exists", (req, res) => {
421
+ const name = String(req.query.name ?? "");
422
+ if (!name) {
423
+ res.status(400).send("Missing name");
424
+ return;
425
+ }
426
+ if (name.includes("/") || name.includes("\\") || name.includes("..")) {
427
+ res.status(400).send("Invalid name");
428
+ return;
429
+ }
430
+ const filePath = resolveDocPath(name);
431
+ if (!isDocPathSafe(filePath)) {
432
+ res.status(400).send("Invalid path");
433
+ return;
434
+ }
435
+ res.json({ exists: fs_1.default.existsSync(filePath) });
436
+ });
437
+ app.post("/api/docs", (req, res) => {
438
+ let payload = null;
439
+ if (typeof req.body === "string") {
440
+ try {
441
+ payload = JSON.parse(req.body);
442
+ } catch {
443
+ payload = null;
444
+ }
445
+ } else if (req.body && typeof req.body === "object") {
446
+ payload = req.body;
447
+ }
448
+ const rawName = String(payload?.name ?? "").trim();
449
+ const rawType = payload?.type ?? null;
450
+ if (!rawType) {
451
+ res.status(400).send("Missing type");
452
+ return;
453
+ }
454
+ const name = rawName;
455
+ const nameError = validateDocName(name);
456
+ if (nameError) {
457
+ res.status(400).send(nameError);
458
+ return;
459
+ }
460
+ const hasMarkdownExt = name.endsWith(".md");
461
+ const hasJsonExt = name.endsWith(".json");
462
+ if (hasMarkdownExt && rawType !== "markdown") {
463
+ res.status(400).send("Name extension does not match type");
464
+ return;
465
+ }
466
+ if (hasJsonExt && rawType !== "json") {
467
+ res.status(400).send("Name extension does not match type");
468
+ return;
469
+ }
470
+ const fileName = hasMarkdownExt || hasJsonExt ? name : `${name}.${rawType === "json" ? "json" : "md"}`;
471
+ const filePath = resolveDocPath(fileName);
472
+ if (!isDocPathSafe(filePath)) {
473
+ res.status(400).send("Invalid path");
474
+ return;
475
+ }
476
+ if (fs_1.default.existsSync(filePath)) {
477
+ res.status(409).send("Name already exists");
478
+ return;
479
+ }
480
+ const content = payload?.content ?? (rawType === "json" ? JSON.stringify({ data: [] }, null, 2) : "");
481
+ fs_1.default.writeFileSync(filePath, content, "utf-8");
482
+ indexFileTags(fileName);
483
+ rebuildTagIndex();
484
+ const createdTags = normalizeTags(tagsFromContent(fileName, content));
485
+ const createdTitle = rawType === "markdown" ? (0, adapter_markdown_1.extractMarkdownTitle)(content) : null;
486
+ res.status(201).json({
487
+ name: fileName,
488
+ type: rawType,
489
+ title: createdTitle,
490
+ tags: createdTags,
491
+ displayName: createdTitle && createdTitle.length > 0 ? createdTitle : displayNameFromFileName(fileName)
492
+ });
493
+ });
494
+ app.post("/api/doc/rename", (req, res) => {
495
+ let payload = null;
496
+ if (typeof req.body === "string") {
497
+ try {
498
+ payload = JSON.parse(req.body);
499
+ } catch {
500
+ payload = null;
501
+ }
502
+ } else if (req.body && typeof req.body === "object") {
503
+ payload = req.body;
504
+ }
505
+ const from = String(payload?.from ?? "").trim();
506
+ const to = String(payload?.to ?? "").trim();
507
+ if (!from || !to) {
508
+ res.status(400).send("Missing rename target");
509
+ return;
510
+ }
511
+ const fromError = validateDocName(from);
512
+ if (fromError) {
513
+ res.status(400).send(fromError);
514
+ return;
515
+ }
516
+ const toError = validateDocName(to);
517
+ if (toError) {
518
+ res.status(400).send(toError);
519
+ return;
520
+ }
521
+ if (isMetadataName(from) || isMetadataName(to)) {
522
+ res.status(400).send("Reserved name");
523
+ return;
524
+ }
525
+ const fromPath = resolveDocPath(from);
526
+ const toPath = resolveDocPath(to);
527
+ if (!isDocPathSafe(fromPath) || !isDocPathSafe(toPath)) {
528
+ res.status(400).send("Invalid path");
529
+ return;
530
+ }
531
+ if (!fs_1.default.existsSync(fromPath)) {
532
+ res.status(404).send("Not found");
533
+ return;
534
+ }
535
+ if (fs_1.default.existsSync(toPath)) {
536
+ res.status(409).send("Name already exists");
537
+ return;
538
+ }
539
+ const fromExt = path_1.default.extname(from).toLowerCase();
540
+ const toExt = path_1.default.extname(to).toLowerCase();
541
+ if (fromExt !== toExt || fromExt !== ".md" && fromExt !== ".json") {
542
+ res.status(400).send("Name extension does not match type");
543
+ return;
544
+ }
545
+ fs_1.default.renameSync(fromPath, toPath);
546
+ if (fromExt === ".md" || fromExt === ".json") {
547
+ fileTags.delete(from);
548
+ indexFileTags(to);
549
+ rebuildTagIndex();
550
+ }
551
+ res.json({
552
+ name: to,
553
+ type: fromExt === ".json" ? "json" : "markdown",
554
+ title: null,
555
+ displayName: displayNameFromFileName(to)
556
+ });
557
+ });
558
+ app.post("/api/doc/:name/archive", (req, res) => {
559
+ const name = String(req.params.name);
560
+ if (isMetadataName(name)) {
561
+ res.status(400).send("Reserved name");
562
+ return;
563
+ }
564
+ if (name.includes("/") || name.includes("\\") || name.includes("..")) {
565
+ res.status(400).send("Invalid name");
566
+ return;
567
+ }
568
+ const filePath = resolveDocPath(name);
569
+ if (!isDocPathSafe(filePath)) {
570
+ res.status(400).send("Invalid path");
571
+ return;
572
+ }
573
+ if (!fs_1.default.existsSync(filePath)) {
574
+ res.status(404).send("Not found");
575
+ return;
576
+ }
577
+ const archiveDir = path_1.default.join(hyperdocDir, archiveDirName);
578
+ ensureHyperdocDir();
579
+ if (!fs_1.default.existsSync(archiveDir)) {
580
+ fs_1.default.mkdirSync(archiveDir, { recursive: true });
581
+ }
582
+ const archivePath = path_1.default.join(archiveDir, name);
583
+ if (fs_1.default.existsSync(archivePath)) {
584
+ res.status(409).send("Archived file already exists with that name");
585
+ return;
586
+ }
587
+ fs_1.default.renameSync(filePath, archivePath);
588
+ fileTags.delete(name);
589
+ rebuildTagIndex();
590
+ res.json({ archived: true, name });
591
+ });
592
+ app.get("/api/doc/:name", (req, res) => {
593
+ const name = String(req.params.name);
594
+ if (isMetadataName(name)) {
595
+ res.status(400).send("Reserved name");
596
+ return;
597
+ }
598
+ const filePath = resolveDocPath(name);
599
+ if (!isDocPathSafe(filePath)) {
600
+ res.status(400).send("Invalid path");
601
+ return;
602
+ }
603
+ if (!fs_1.default.existsSync(filePath)) {
604
+ res.status(404).send("Not found");
605
+ return;
606
+ }
607
+ const content = fs_1.default.readFileSync(filePath, "utf-8");
608
+ const stat = fs_1.default.statSync(filePath);
609
+ const ext = path_1.default.extname(name).toLowerCase();
610
+ if (ext === ".md") {
611
+ const metadata = (0, adapter_markdown_1.getMarkdownMetadata)(content);
612
+ res.json({
613
+ content: (0, adapter_markdown_1.stripMarkdownMetadataHeader)(content),
614
+ mtimeMs: stat.mtimeMs,
615
+ metadata
616
+ });
617
+ return;
618
+ }
619
+ res.json({ content, mtimeMs: stat.mtimeMs });
620
+ });
621
+ app.put("/api/doc/:name", (req, res) => {
622
+ const name = String(req.params.name);
623
+ if (isMetadataName(name)) {
624
+ res.status(400).send("Reserved name");
625
+ return;
626
+ }
627
+ const filePath = resolveDocPath(name);
628
+ if (!isDocPathSafe(filePath)) {
629
+ res.status(400).send("Invalid path");
630
+ return;
631
+ }
632
+ if (!fs_1.default.existsSync(filePath)) {
633
+ res.status(404).send("Not found");
634
+ return;
635
+ }
636
+ const baseHeader = req.header("x-hyperdoc-base-version");
637
+ if (baseHeader !== void 0) {
638
+ const baseVersion = Number(baseHeader);
639
+ if (!Number.isFinite(baseVersion)) {
640
+ res.status(400).send("Invalid base version");
641
+ return;
642
+ }
643
+ const currentStat = fs_1.default.statSync(filePath);
644
+ if (currentStat.mtimeMs !== baseVersion) {
645
+ res.status(409).json({ mtimeMs: currentStat.mtimeMs });
646
+ return;
647
+ }
648
+ }
649
+ fs_1.default.writeFileSync(filePath, req.body ?? "", "utf-8");
650
+ const stat = fs_1.default.statSync(filePath);
651
+ const clientId = req.header("x-hyperdoc-client-id");
652
+ if (clientId) {
653
+ recentWrites.set(name, {
654
+ clientId,
655
+ ts: Date.now(),
656
+ mtimeMs: stat.mtimeMs
657
+ });
658
+ }
659
+ res.json({ mtimeMs: stat.mtimeMs });
660
+ });
661
+ app.delete("/api/doc/:name", (req, res) => {
662
+ const name = String(req.params.name);
663
+ if (isMetadataName(name)) {
664
+ res.status(400).send("Reserved name");
665
+ return;
666
+ }
667
+ if (name.includes("/") || name.includes("\\") || name.includes("..")) {
668
+ res.status(400).send("Invalid name");
669
+ return;
670
+ }
671
+ const filePath = resolveDocPath(name);
672
+ if (!isDocPathSafe(filePath)) {
673
+ res.status(400).send("Invalid path");
674
+ return;
675
+ }
676
+ if (!fs_1.default.existsSync(filePath)) {
677
+ res.status(404).send("Not found");
678
+ return;
679
+ }
680
+ const trashDir = path_1.default.join(hyperdocDir, trashDirName);
681
+ ensureHyperdocDir();
682
+ if (!fs_1.default.existsSync(trashDir)) {
683
+ fs_1.default.mkdirSync(trashDir, { recursive: true });
684
+ fs_1.default.writeFileSync(path_1.default.join(trashDir, ".gitignore"), "*\n!.gitignore\n");
685
+ }
686
+ const trashPath = path_1.default.join(trashDir, name);
687
+ if (fs_1.default.existsSync(trashPath)) {
688
+ const ext = path_1.default.extname(name);
689
+ const base = name.slice(0, -ext.length);
690
+ const timestamp = Date.now();
691
+ const uniqueName = `${base}-${timestamp}${ext}`;
692
+ fs_1.default.renameSync(filePath, path_1.default.join(trashDir, uniqueName));
693
+ } else {
694
+ fs_1.default.renameSync(filePath, trashPath);
695
+ }
696
+ fileTags.delete(name);
697
+ rebuildTagIndex();
698
+ res.json({ deleted: true });
699
+ });
700
+ app.get("/api/tags", (req, res) => {
701
+ res.json({ tags: Array.from(tagIndex).sort() });
702
+ });
703
+ const clients = /* @__PURE__ */ new Set();
704
+ const recentWrites = /* @__PURE__ */ new Map();
705
+ const watcher = chokidar_1.default.watch(rootDir, {
706
+ ignoreInitial: true,
707
+ ignored: (target) => {
708
+ const rel = path_1.default.relative(rootDir, target);
709
+ if (!rel)
710
+ return false;
711
+ if (rel.startsWith(".git"))
712
+ return true;
713
+ if (rel.startsWith(hyperdocDirName))
714
+ return true;
715
+ if (rel.startsWith(".trash"))
716
+ return true;
717
+ if (rel.startsWith(".archive"))
718
+ return true;
719
+ if (rel.startsWith("node_modules"))
720
+ return true;
721
+ if (rel.startsWith("dist"))
722
+ return true;
723
+ return false;
724
+ }
725
+ });
726
+ watcher.on("all", (event, filePath) => {
727
+ if (!filePath.endsWith(".md") && !filePath.endsWith(".json"))
728
+ return;
729
+ if (path_1.default.basename(filePath) === metadataFileName)
730
+ return;
731
+ const name = path_1.default.basename(filePath);
732
+ if (name.endsWith(".md") || name.endsWith(".json")) {
733
+ if (event === "unlink") {
734
+ fileTags.delete(name);
735
+ recentWrites.delete(name);
736
+ } else {
737
+ indexFileTags(name);
738
+ }
739
+ rebuildTagIndex();
740
+ }
741
+ let mtimeMs;
742
+ if (event !== "unlink") {
743
+ try {
744
+ const stat = fs_1.default.statSync(filePath);
745
+ mtimeMs = stat.mtimeMs;
746
+ } catch {
747
+ mtimeMs = void 0;
748
+ }
749
+ }
750
+ const recent = recentWrites.get(name);
751
+ const sourceId = recent && Date.now() - recent.ts < 2e3 ? recent.clientId : void 0;
752
+ const payload = JSON.stringify({ type: event, name, mtimeMs, sourceId });
753
+ for (const client of clients) {
754
+ client.write(`data: ${payload}
755
+
756
+ `);
757
+ }
758
+ });
759
+ app.get("/api/events", (req, res) => {
760
+ res.setHeader("Content-Type", "text/event-stream");
761
+ res.setHeader("Cache-Control", "no-cache");
762
+ res.setHeader("Connection", "keep-alive");
763
+ res.flushHeaders();
764
+ clients.add(res);
765
+ req.on("close", () => {
766
+ clients.delete(res);
767
+ });
768
+ });
769
+ return app;
770
+ }
771
+ }
772
+ });
773
+
774
+ // ../../packages/server/dist/index.js
775
+ var require_dist3 = __commonJS({
776
+ "../../packages/server/dist/index.js"(exports2) {
777
+ "use strict";
778
+ var __createBinding = exports2 && exports2.__createBinding || (Object.create ? (function(o, m, k, k2) {
779
+ if (k2 === void 0) k2 = k;
780
+ var desc = Object.getOwnPropertyDescriptor(m, k);
781
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
782
+ desc = { enumerable: true, get: function() {
783
+ return m[k];
784
+ } };
785
+ }
786
+ Object.defineProperty(o, k2, desc);
787
+ }) : (function(o, m, k, k2) {
788
+ if (k2 === void 0) k2 = k;
789
+ o[k2] = m[k];
790
+ }));
791
+ var __exportStar = exports2 && exports2.__exportStar || function(m, exports3) {
792
+ for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports3, p)) __createBinding(exports3, m, p);
793
+ };
794
+ Object.defineProperty(exports2, "__esModule", { value: true });
795
+ __exportStar(require_createApp(), exports2);
796
+ }
797
+ });
798
+
799
+ // src/cli/CommanderCli.ts
800
+ var import_commander = require("commander");
801
+
802
+ // src/commands/PromptCommand.ts
803
+ var import_fs = __toESM(require("fs"));
804
+ var import_path = __toESM(require("path"));
805
+ var PromptCommand = class {
806
+ async execute(context) {
807
+ void context;
808
+ const promptPath = import_path.default.join(__dirname, "..", "prompts", "agent.txt");
809
+ const prompt = import_fs.default.readFileSync(promptPath, "utf-8");
810
+ process.stdout.write(prompt);
811
+ if (!prompt.endsWith("\n")) {
812
+ process.stdout.write("\n");
813
+ }
814
+ }
815
+ };
816
+
817
+ // src/commands/ServerCommand.ts
818
+ var import_fs2 = __toESM(require("fs"));
819
+ var import_path2 = __toESM(require("path"));
820
+ var import_open = __toESM(require("open"));
821
+ var import_core = __toESM(require_dist());
822
+ var import_server = __toESM(require_dist3());
823
+ var DEFAULT_PORT = 4321;
824
+ var ServerCommand = class {
825
+ async execute(context) {
826
+ const rootDir = import_path2.default.resolve(context.options.root ?? process.cwd());
827
+ const port = Number(
828
+ context.options.port ?? process.env.HYPERDOC_PORT ?? DEFAULT_PORT
829
+ );
830
+ const webDir = import_path2.default.join(__dirname, "..", "web");
831
+ if (!import_fs2.default.existsSync(webDir)) {
832
+ import_core.logger.warn(
833
+ `Web bundle not found at ${webDir}. Run pnpm --filter hyperdoc-web build.`
834
+ );
835
+ }
836
+ const app = (0, import_server.createApp)({ rootDir, staticDir: webDir });
837
+ app.get("*", (req, res) => {
838
+ const indexPath = import_path2.default.join(webDir, "index.html");
839
+ if (import_fs2.default.existsSync(indexPath)) {
840
+ res.sendFile(indexPath);
841
+ } else {
842
+ res.status(500).send("Web bundle missing. Run pnpm build:web.");
843
+ }
844
+ });
845
+ app.listen(port, () => {
846
+ const url = `http://localhost:${port}`;
847
+ import_core.logger.info(`Hyperdoc server running at ${url}`);
848
+ if (context.options.open) {
849
+ (0, import_open.default)(url).catch(() => void 0);
850
+ }
851
+ });
852
+ }
853
+ };
854
+
855
+ // src/cli/CommanderCli.ts
856
+ var import_core2 = __toESM(require_dist());
857
+
858
+ // package.json
859
+ var package_default = {
860
+ name: "@maplab/hyperdoc",
861
+ version: "0.2.0",
862
+ description: "Hyperdoc CLI and local server",
863
+ type: "commonjs",
864
+ main: "dist/index.js",
865
+ types: "dist/index.d.ts",
866
+ bin: {
867
+ hyper: "dist/index.js"
868
+ },
869
+ files: [
870
+ "dist"
871
+ ],
872
+ publishConfig: {
873
+ access: "public"
874
+ },
875
+ scripts: {
876
+ build: "tsup && node scripts/copy-prompts.js && node scripts/copy-web.js",
877
+ dev: "tsx src/index.ts",
878
+ start: "node dist/index.js",
879
+ typecheck: "tsc -p tsconfig.json --noEmit"
880
+ },
881
+ dependencies: {
882
+ chokidar: "^5.0.0",
883
+ commander: "^14.0.0",
884
+ "markdown-it": "^14.1.0",
885
+ open: "^9.1.0",
886
+ express: "^4.19.2",
887
+ zod: "^4.1.5"
888
+ },
889
+ devDependencies: {
890
+ "@hyperdoc/core": "workspace:*",
891
+ "@hyperdoc/server": "workspace:*",
892
+ tsup: "^8.5.0"
893
+ }
894
+ };
895
+
896
+ // src/cli/CommanderCli.ts
897
+ var CommanderCli = class {
898
+ constructor() {
899
+ this.commandHandlers = /* @__PURE__ */ new Map();
900
+ this.program = new import_commander.Command();
901
+ this.setupCommandHandlers();
902
+ this.setupProgram();
903
+ }
904
+ setupCommandHandlers() {
905
+ this.commandHandlers.set("prompt", new PromptCommand());
906
+ this.commandHandlers.set("server", new ServerCommand());
907
+ }
908
+ setupProgram() {
909
+ this.program.name("hyper").description("Local Hyperdoc server and tooling").version(package_default.version);
910
+ this.program.option("--root <dir>", "Project root (defaults to cwd)").option("-v, --verbose", "Increase logging verbosity").option("-q, --quiet", "Reduce logging output").option("-d, --debug", "Enable debug logging");
911
+ this.program.command("prompt").description("Print the agent prompt/spec to stdout").action(async (options, command) => {
912
+ await this.executeCommand("prompt", command);
913
+ });
914
+ this.program.command("server").description("Start the local Hyperdoc server").option(
915
+ "--port <number>",
916
+ "Port to run the server on",
917
+ (value) => Number(value)
918
+ ).option("-o, --open", "Open the UI in the browser").action(async (options, command) => {
919
+ await this.executeCommand("server", command);
920
+ });
921
+ }
922
+ async executeCommand(command, commandInstance) {
923
+ const parent = commandInstance.parent;
924
+ const globalOpts = parent.opts();
925
+ const commandOpts = commandInstance.opts();
926
+ const allOptions = { ...globalOpts, ...commandOpts };
927
+ if (allOptions.debug) {
928
+ import_core2.logger.setLevel(import_core2.LogLevel.DEBUG);
929
+ } else if (allOptions.verbose) {
930
+ import_core2.logger.setLevel(import_core2.LogLevel.INFO);
931
+ } else if (allOptions.quiet) {
932
+ import_core2.logger.setLevel(import_core2.LogLevel.WARN);
933
+ }
934
+ const handler = this.commandHandlers.get(command);
935
+ if (!handler) {
936
+ throw new Error(`No handler found for command: ${command}`);
937
+ }
938
+ await handler.execute({ options: allOptions });
939
+ }
940
+ async parse(args) {
941
+ await this.program.parseAsync(args);
942
+ }
943
+ getHelp() {
944
+ return this.program.helpInformation();
945
+ }
946
+ };
947
+
948
+ // src/index.ts
949
+ var import_core3 = __toESM(require_dist());
950
+ var cli = new CommanderCli();
951
+ async function main() {
952
+ try {
953
+ await cli.parse(process.argv);
954
+ } catch (error) {
955
+ const showStackTrace = import_core3.logger.getLevel() === import_core3.LogLevel.DEBUG;
956
+ const formattedError = (0, import_core3.formatError)(error, showStackTrace);
957
+ console.error(formattedError);
958
+ process.exit(1);
959
+ }
960
+ }
961
+ main();
962
+ //# sourceMappingURL=index.js.map