@profullstack/readm3 0.3.0 → 0.5.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.
@@ -0,0 +1,166 @@
1
+ import { existsSync, lstatSync, readFileSync, statSync } from "node:fs";
2
+ import { basename, dirname, join, relative, resolve, sep } from "node:path";
3
+ import { applyFiles, planApply } from "@profullstack/synconfig";
4
+ import { configDir, credentials, writeConfig } from "./account.js";
5
+ import { loadSettings, localSettings, saveSettings, syncContext, syncStatus } from "./settings-sync.js";
6
+ import { validateWorkspace } from "./sync-schema.js";
7
+ import { files, scan } from "./tree.js";
8
+ import { isFlavor } from "./flavors.js";
9
+ import { themeList } from "@profullstack/hqtui";
10
+ export const ACCOUNT_USAGE = `Account and sync commands
11
+ readm3 whoami Show the verified account
12
+ readm3 settings [--theme NAME] [--flavor NAME]
13
+ readm3 save [file-or-directory] [--force]
14
+ readm3 load [output-directory] [--dry-run] [--force]
15
+ readm3 sync [status | save | load | revisions]
16
+
17
+ Save includes settings.json and the last imported workspace.json only.
18
+ Giving save a path imports its Markdown files. Load optionally writes them to
19
+ an output directory, backing up replaced files. --force explicitly resolves a
20
+ conflict. Credentials are stored separately and never synced.
21
+ READM3_API_URL, READM3_TOKEN and READM3_CONFIG_DIR override local defaults.
22
+ `;
23
+ function importWorkspace(target) {
24
+ const path = resolve(target);
25
+ const directory = statSync(path).isDirectory();
26
+ const root = directory ? path : dirname(path);
27
+ const paths = directory ? files(scan(root)).map((entry) => entry.path) : [path];
28
+ let bytes = 0;
29
+ const documents = paths.map((file) => {
30
+ const size = statSync(file).size;
31
+ bytes += size;
32
+ if (size > 4 * 1024 * 1024 || bytes > 20 * 1024 * 1024)
33
+ throw new Error("Workspace limit: 4 MB per file and 20 MB total.");
34
+ return { path: relative(root, file).split(sep).join("/"), source: readFileSync(file, "utf8") };
35
+ });
36
+ return validateWorkspace({ name: basename(root), documents, active: documents[0]?.path });
37
+ }
38
+ function exportWorkspace(ws, output, force, dryRun) {
39
+ const root = resolve(output);
40
+ const data = Object.fromEntries(ws.documents.map((doc) => [doc.path, { content: doc.source }]));
41
+ // The library writes relative paths; reject symlink parents before handing it
42
+ // any workspace paths, so a remote document cannot escape the chosen folder.
43
+ for (const path of Object.keys(data)) {
44
+ let current = join(root, path);
45
+ while (true) {
46
+ try {
47
+ if (lstatSync(current).isSymbolicLink())
48
+ throw new Error(`Refusing a symlink in output path: ${current}`);
49
+ }
50
+ catch (error) {
51
+ if (error.code !== "ENOENT")
52
+ throw error;
53
+ }
54
+ if (current === root)
55
+ break;
56
+ const parent = dirname(current);
57
+ if (parent === current)
58
+ break;
59
+ current = parent;
60
+ }
61
+ }
62
+ const plan = planApply(root, data);
63
+ if (dryRun)
64
+ return { plan };
65
+ if (!force && plan.some((entry) => entry.status === "changed"))
66
+ throw new Error("Output files differ. Use a new directory, or --force to replace them with numbered backups.");
67
+ const backups = [];
68
+ const written = applyFiles(root, data, plan, { onBackup: (path, backup) => backups.push({ path, backup }) });
69
+ return { written, backups };
70
+ }
71
+ export async function accountCommand(argv) {
72
+ const commands = ["whoami", "settings", "save", "load", "sync"];
73
+ if (!commands.includes(argv[0] || ""))
74
+ return false;
75
+ let [command, ...args] = argv;
76
+ if (args.includes("--help") || args.includes("-h")) {
77
+ process.stdout.write(ACCOUNT_USAGE);
78
+ return true;
79
+ }
80
+ if (command === "sync")
81
+ command = args[0] && !args[0].startsWith("-") ? args.shift() : "status";
82
+ const options = {};
83
+ const positional = [];
84
+ for (let i = 0; i < args.length; i++) {
85
+ if (["--force", "--dry-run"].includes(args[i]))
86
+ options[args[i].slice(2)] = true;
87
+ else if (["--theme", "--flavor"].includes(args[i])) {
88
+ if (!args[i + 1] || args[i + 1].startsWith("--"))
89
+ throw new Error(`${args[i]} needs a value.`);
90
+ options[args[i].slice(2)] = args[++i];
91
+ }
92
+ else if (args[i].startsWith("-"))
93
+ throw new Error(`Unknown option: ${args[i]}`);
94
+ else
95
+ positional.push(args[i]);
96
+ }
97
+ if (positional.length > 1 || (positional.length && !["save", "load"].includes(command)))
98
+ throw new Error("Unexpected positional argument. Run `readm3 sync --help`.");
99
+ const allowed = { settings: ["theme", "flavor"], save: ["force"], load: ["force", "dry-run"] };
100
+ for (const option of Object.keys(options))
101
+ if (!(allowed[command] || []).includes(option))
102
+ throw new Error(`--${option} is not supported by ${command}.`);
103
+ let result;
104
+ switch (command) {
105
+ case "whoami": {
106
+ const account = await credentials();
107
+ result = { user: account.user, api: account.api };
108
+ break;
109
+ }
110
+ case "settings": {
111
+ const settings = localSettings();
112
+ if (typeof options.theme === "string") {
113
+ if (!themeList.some((theme) => theme.name === options.theme))
114
+ throw new Error("Unknown theme.");
115
+ settings.theme = options.theme;
116
+ }
117
+ if (typeof options.flavor === "string") {
118
+ if (!isFlavor(options.flavor))
119
+ throw new Error("Unknown flavor.");
120
+ settings.flavor = options.flavor;
121
+ }
122
+ if (options.theme || options.flavor)
123
+ writeConfig("settings.json", settings);
124
+ result = settings;
125
+ break;
126
+ }
127
+ case "save": {
128
+ const ctx = await syncContext();
129
+ if (positional[0])
130
+ writeConfig("workspace.json", importWorkspace(positional[0]));
131
+ if (!existsSync(join(configDir(), "settings.json")))
132
+ writeConfig("settings.json", {});
133
+ result = await saveSettings(ctx, { force: Boolean(options.force) });
134
+ break;
135
+ }
136
+ case "load": {
137
+ const ctx = await syncContext();
138
+ const latest = await ctx.client.get();
139
+ const fixed = { ...ctx, client: { ...ctx.client, get: async () => latest } };
140
+ const ws = latest?.snapshot.files["workspace.json"];
141
+ // Check output paths before updating the local marker or config files.
142
+ if (positional[0] && ws) {
143
+ const plan = exportWorkspace(validateWorkspace(JSON.parse(ws.content)), positional[0], Boolean(options.force), true);
144
+ if (!options.force && !options["dry-run"] && plan.plan.some((entry) => entry.status === "changed"))
145
+ throw new Error("Output files differ. Use a new directory or --force (with backups).");
146
+ }
147
+ const loaded = await loadSettings(fixed, { force: Boolean(options.force), dryRun: Boolean(options["dry-run"]) });
148
+ result = loaded;
149
+ if (positional[0] && ws && ["loaded", "same", "planned"].includes(loaded.status))
150
+ result = { ...loaded, output: exportWorkspace(validateWorkspace(JSON.parse(ws.content)), positional[0], Boolean(options.force), Boolean(options["dry-run"])) };
151
+ break;
152
+ }
153
+ case "status":
154
+ result = await syncStatus(await syncContext());
155
+ break;
156
+ case "revisions":
157
+ result = await (await syncContext()).client.revisions();
158
+ break;
159
+ default: throw new Error(`Unknown sync command: ${command}`);
160
+ }
161
+ process.stdout.write(JSON.stringify(result, null, 2) + "\n");
162
+ if (result && typeof result === "object" && "status" in result && ["conflict", "local_changes", "newer"].includes(String(result.status)))
163
+ process.exitCode = 1;
164
+ return true;
165
+ }
166
+ //# sourceMappingURL=sync-cli.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sync-cli.js","sourceRoot":"","sources":["../src/sync-cli.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,37 @@
1
+ /** Browser-safe account data contract. Only these two files may be synced. */
2
+ import { type Flavor } from "./flavors.ts";
3
+ export interface WorkspaceDocument {
4
+ path: string;
5
+ source: string;
6
+ }
7
+ export interface SyncedWorkspace {
8
+ documents: WorkspaceDocument[];
9
+ active: string;
10
+ name: string;
11
+ }
12
+ export interface ReaderSettings {
13
+ theme?: string;
14
+ flavor?: Flavor;
15
+ }
16
+ export declare const SYNC_LIMITS: {
17
+ maxFileBytes: number;
18
+ maxTotalBytes: number;
19
+ maxFiles: number;
20
+ };
21
+ export declare const SYNC_FILES: readonly ["settings.json", "workspace.json"];
22
+ export declare const SYNC_POLICY: {
23
+ maxFileBytes: number;
24
+ maxTotalBytes: number;
25
+ maxFiles: number;
26
+ files: {
27
+ path: "settings.json" | "workspace.json";
28
+ json: boolean;
29
+ }[];
30
+ never: string[];
31
+ };
32
+ export declare function validateSettings(value: unknown): ReaderSettings;
33
+ export declare function validateWorkspace(value: unknown): SyncedWorkspace;
34
+ export declare function validateSyncFiles(files: Record<string, {
35
+ content: string;
36
+ }>): void;
37
+ //# sourceMappingURL=sync-schema.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sync-schema.d.ts","sourceRoot":"","sources":["../src/sync-schema.ts"],"names":[],"mappings":"AAAA,8EAA8E;AAC9E,OAAO,EAAY,KAAK,MAAM,EAAE,MAAM,cAAc,CAAC;AAErD,MAAM,WAAW,iBAAiB;IAAG,IAAI,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE;AACnE,MAAM,WAAW,eAAe;IAAG,SAAS,EAAE,iBAAiB,EAAE,CAAC;IAAC,MAAM,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE;AACjG,MAAM,WAAW,cAAc;IAAG,KAAK,CAAC,EAAE,MAAM,CAAC;IAAC,MAAM,CAAC,EAAE,MAAM,CAAA;CAAE;AACnE,eAAO,MAAM,WAAW;;;;CAAmF,CAAC;AAC5G,eAAO,MAAM,UAAU,8CAA+C,CAAC;AACvE,eAAO,MAAM,WAAW;;;;;;;;;CAIvB,CAAC;AAEF,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,OAAO,GAAG,cAAc,CAO/D;AAED,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,OAAO,GAAG,eAAe,CAmBjE;AAED,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE;IAAE,OAAO,EAAE,MAAM,CAAA;CAAE,CAAC,GAAG,IAAI,CAOlF"}
@@ -0,0 +1,63 @@
1
+ /** Browser-safe account data contract. Only these two files may be synced. */
2
+ import { isFlavor } from "./flavors.js";
3
+ export const SYNC_LIMITS = { maxFileBytes: 32 * 1024 * 1024, maxTotalBytes: 33 * 1024 * 1024, maxFiles: 2 };
4
+ export const SYNC_FILES = ["settings.json", "workspace.json"];
5
+ export const SYNC_POLICY = {
6
+ files: SYNC_FILES.map((path) => ({ path, json: true })),
7
+ never: ["account.json", "cloud.json", "sync.json"],
8
+ ...SYNC_LIMITS,
9
+ };
10
+ export function validateSettings(value) {
11
+ if (!value || typeof value !== "object" || Array.isArray(value))
12
+ throw new Error("Settings must be an object.");
13
+ const settings = value;
14
+ if (Object.keys(settings).some((key) => !["theme", "flavor"].includes(key)))
15
+ throw new Error("Only theme and flavor can be synced as settings.");
16
+ if (settings.theme !== undefined && (typeof settings.theme !== "string" || !/^[a-z0-9-]{1,40}$/.test(settings.theme)))
17
+ throw new Error("Invalid theme.");
18
+ if (settings.flavor !== undefined && !isFlavor(settings.flavor))
19
+ throw new Error("Invalid Markdown flavor.");
20
+ return settings;
21
+ }
22
+ export function validateWorkspace(value) {
23
+ if (!value || typeof value !== "object")
24
+ throw new Error("Workspace must be an object.");
25
+ const ws = value;
26
+ if (Object.keys(ws).some((key) => !["name", "active", "documents"].includes(key)))
27
+ throw new Error("Workspace can only contain name, active and documents.");
28
+ if (typeof ws.name !== "string" || ws.name.length > 200 || !Array.isArray(ws.documents) || !ws.documents.length || ws.documents.length > 1000)
29
+ throw new Error("Workspace needs a name and 1–1,000 Markdown files.");
30
+ const paths = new Set();
31
+ let total = 0;
32
+ const encoder = new TextEncoder();
33
+ for (const doc of ws.documents) {
34
+ if (!doc || typeof doc.path !== "string" || doc.path.length > 1024 || /[\\\x00-\x1f:]/.test(doc.path) || doc.path.split("/").some((p) => !p || p.startsWith(".")) || !/\.(md|markdown|mdown|mkd|mdx)$/i.test(doc.path) || paths.has(doc.path))
35
+ throw new Error("Workspace contains an invalid or duplicate Markdown path.");
36
+ if (typeof doc.source !== "string")
37
+ throw new Error("Markdown source must be text.");
38
+ if (Object.keys(doc).some((key) => !["path", "source"].includes(key)))
39
+ throw new Error("Documents can only contain path and source.");
40
+ const bytes = encoder.encode(doc.source).length;
41
+ total += bytes;
42
+ if (bytes > 4 * 1024 * 1024 || total > 20 * 1024 * 1024)
43
+ throw new Error("Workspace limit: 4 MB per file and 20 MB total.");
44
+ paths.add(doc.path);
45
+ }
46
+ if (!paths.has(ws.active))
47
+ throw new Error("The active document is missing.");
48
+ return ws;
49
+ }
50
+ export function validateSyncFiles(files) {
51
+ for (const [path, file] of Object.entries(files)) {
52
+ if (!SYNC_FILES.includes(path))
53
+ throw new Error(`File is not syncable: ${path}`);
54
+ if (!file || typeof file.content !== "string")
55
+ throw new Error(`${path}: content must be text.`);
56
+ const value = JSON.parse(file.content);
57
+ if (path === "settings.json")
58
+ validateSettings(value);
59
+ else
60
+ validateWorkspace(value);
61
+ }
62
+ }
63
+ //# sourceMappingURL=sync-schema.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sync-schema.js","sourceRoot":"","sources":["../src/sync-schema.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@profullstack/readm3",
3
- "version": "0.3.0",
4
- "description": "A terminal markdown reader and editor: file browser on the left, rendered markdown on the right. CommonMark, GitHub and Reddit flavors. Bun and Node, built on HQTUI.",
3
+ "version": "0.5.0",
4
+ "description": "Markdown editing and sharing for people and teams. Web/PWA, CLI, API and MCP with version history, view/edit links, and a terminal reader.",
5
5
  "type": "module",
6
6
  "license": "MIT",
7
7
  "author": "Profullstack, Inc.",
@@ -33,16 +33,23 @@
33
33
  "site:build": "bun site/build.ts",
34
34
  "site:start": "bun site/server.ts",
35
35
  "build": "tsc -p tsconfig.build.json",
36
- "typecheck": "tsc -p tsconfig.json --noEmit",
36
+ "typecheck": "tsc -p tsconfig.json --noEmit && tsc -p site/tsconfig.json --noEmit",
37
37
  "test": "node --experimental-strip-types --test test/*.test.ts",
38
- "test:bun": "bun test test/",
39
- "prepublishOnly": "npm run build"
38
+ "test:bun": "bun test ./test",
39
+ "prepublishOnly": "npm run build",
40
+ "test:site": "playwright test -c site/playwright.config.ts",
41
+ "test:server": "bun test ./server/test",
42
+ "test:accounts": "bun test ./server/test && playwright test -c site/accounts.playwright.config.ts"
40
43
  },
41
44
  "dependencies": {
42
- "@profullstack/hqtui": "^0.1.9",
45
+ "@modelcontextprotocol/sdk": "^1.30.0",
46
+ "@profullstack/hqtui": "^0.5.0",
47
+ "@profullstack/synconfig": "^0.1.3",
43
48
  "marked": "^18.0.11"
44
49
  },
45
50
  "devDependencies": {
51
+ "@playwright/test": "^1.63.0",
52
+ "@types/bun": "^1.4.2",
46
53
  "@types/node": "^22",
47
54
  "typescript": "^5.7.2"
48
55
  },