@hiai-gg/docsmint 0.3.4 → 0.3.5
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/README.md +18 -0
- package/dist/backend/index.js +214020 -0
- package/dist/backend-launcher.d.ts +40 -0
- package/dist/backend-launcher.js +114 -0
- package/dist/storage-quota.d.ts +56 -0
- package/dist/storage-quota.js +91 -0
- package/package.json +13 -1
- package/packages/cli/src/index.ts +1 -1
- package/packages/mcp-server/src/index.ts +1 -1
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/** Server-only launcher for the bundled DocsMint backend runtime. */
|
|
2
|
+
export type DocsmintBackendEnvironment = Readonly<Record<string, string | undefined>>;
|
|
3
|
+
export type LaunchDocsmintBackendOptions = Readonly<{
|
|
4
|
+
cwd?: string;
|
|
5
|
+
env?: DocsmintBackendEnvironment;
|
|
6
|
+
healthUrl?: string;
|
|
7
|
+
startupTimeoutMs?: number;
|
|
8
|
+
pollIntervalMs?: number;
|
|
9
|
+
signal?: AbortSignal;
|
|
10
|
+
}>;
|
|
11
|
+
export type DocsmintBackendProcess = Readonly<{
|
|
12
|
+
pid: number;
|
|
13
|
+
exited: Promise<number>;
|
|
14
|
+
kill(signal: "SIGTERM" | "SIGKILL"): void;
|
|
15
|
+
}>;
|
|
16
|
+
export type DocsmintBackendSpawnSpec = Readonly<{
|
|
17
|
+
command: readonly string[];
|
|
18
|
+
cwd?: string;
|
|
19
|
+
env: Readonly<Record<string, string>>;
|
|
20
|
+
}>;
|
|
21
|
+
export type DocsmintBackendLauncherRuntime = Readonly<{
|
|
22
|
+
executable: string;
|
|
23
|
+
launcherModuleUrl?: string | URL;
|
|
24
|
+
spawn(spec: DocsmintBackendSpawnSpec): DocsmintBackendProcess;
|
|
25
|
+
fetch(input: string, init?: RequestInit): Promise<Response>;
|
|
26
|
+
now?: () => number;
|
|
27
|
+
sleep?: (milliseconds: number) => Promise<void>;
|
|
28
|
+
}>;
|
|
29
|
+
export type DocsmintBackendHandle = Readonly<{
|
|
30
|
+
pid: number;
|
|
31
|
+
ready: Promise<void>;
|
|
32
|
+
exited: Promise<number>;
|
|
33
|
+
stop(): Promise<void>;
|
|
34
|
+
}>;
|
|
35
|
+
export type DocsmintBackendLauncher = Readonly<{
|
|
36
|
+
launch(options?: LaunchDocsmintBackendOptions): DocsmintBackendHandle;
|
|
37
|
+
}>;
|
|
38
|
+
export declare function resolveDocsmintBackendEntrypoint(launcherModuleUrl?: string | URL): URL;
|
|
39
|
+
export declare function createDocsmintBackendLauncher(runtime: DocsmintBackendLauncherRuntime): DocsmintBackendLauncher;
|
|
40
|
+
export declare function launchDocsmintBackend(options?: LaunchDocsmintBackendOptions): DocsmintBackendHandle;
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
/** Server-only launcher for the bundled DocsMint backend runtime. */
|
|
2
|
+
const DEFAULT_STARTUP_TIMEOUT_MS = 30_000;
|
|
3
|
+
const DEFAULT_POLL_INTERVAL_MS = 200;
|
|
4
|
+
const DEFAULT_API_PORT = "50700";
|
|
5
|
+
function assertPositiveMilliseconds(value, name) {
|
|
6
|
+
if (!Number.isSafeInteger(value) || value <= 0) {
|
|
7
|
+
throw new TypeError(`${name} must be a positive safe integer`);
|
|
8
|
+
}
|
|
9
|
+
}
|
|
10
|
+
function immutableEnvironment(overrides = {}) {
|
|
11
|
+
const merged = {};
|
|
12
|
+
for (const [key, value] of Object.entries({ ...process.env, ...overrides })) {
|
|
13
|
+
if (typeof value === "string")
|
|
14
|
+
merged[key] = value;
|
|
15
|
+
}
|
|
16
|
+
return Object.freeze(merged);
|
|
17
|
+
}
|
|
18
|
+
export function resolveDocsmintBackendEntrypoint(launcherModuleUrl = import.meta.url) {
|
|
19
|
+
return new URL("./backend/index.js", launcherModuleUrl);
|
|
20
|
+
}
|
|
21
|
+
function defaultRuntime() {
|
|
22
|
+
if (typeof Bun === "undefined") {
|
|
23
|
+
throw new Error("DocsMint backend launcher requires the Bun runtime");
|
|
24
|
+
}
|
|
25
|
+
return {
|
|
26
|
+
executable: Bun.argv[0] ?? "bun",
|
|
27
|
+
spawn(spec) {
|
|
28
|
+
const child = Bun.spawn({
|
|
29
|
+
cmd: [...spec.command],
|
|
30
|
+
...(spec.cwd ? { cwd: spec.cwd } : {}),
|
|
31
|
+
env: { ...spec.env },
|
|
32
|
+
stdout: "inherit",
|
|
33
|
+
stderr: "inherit",
|
|
34
|
+
});
|
|
35
|
+
return {
|
|
36
|
+
pid: child.pid,
|
|
37
|
+
exited: child.exited,
|
|
38
|
+
kill(signal) {
|
|
39
|
+
child.kill(signal);
|
|
40
|
+
},
|
|
41
|
+
};
|
|
42
|
+
},
|
|
43
|
+
fetch: (input, init) => fetch(input, init),
|
|
44
|
+
now: Date.now,
|
|
45
|
+
sleep: (milliseconds) => Bun.sleep(milliseconds),
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
export function createDocsmintBackendLauncher(runtime) {
|
|
49
|
+
const now = runtime.now ?? Date.now;
|
|
50
|
+
const sleep = runtime.sleep ?? ((milliseconds) => Bun.sleep(milliseconds));
|
|
51
|
+
return Object.freeze({
|
|
52
|
+
launch(options = {}) {
|
|
53
|
+
const startupTimeoutMs = options.startupTimeoutMs ?? DEFAULT_STARTUP_TIMEOUT_MS;
|
|
54
|
+
const pollIntervalMs = options.pollIntervalMs ?? DEFAULT_POLL_INTERVAL_MS;
|
|
55
|
+
assertPositiveMilliseconds(startupTimeoutMs, "startupTimeoutMs");
|
|
56
|
+
assertPositiveMilliseconds(pollIntervalMs, "pollIntervalMs");
|
|
57
|
+
if (options.signal?.aborted) {
|
|
58
|
+
throw new DOMException("Backend launch aborted", "AbortError");
|
|
59
|
+
}
|
|
60
|
+
const env = immutableEnvironment(options.env);
|
|
61
|
+
const port = env.API_PORT ?? DEFAULT_API_PORT;
|
|
62
|
+
const healthUrl = options.healthUrl ?? `http://127.0.0.1:${port}/api/health`;
|
|
63
|
+
const entrypoint = resolveDocsmintBackendEntrypoint(runtime.launcherModuleUrl ?? import.meta.url);
|
|
64
|
+
const child = runtime.spawn(Object.freeze({
|
|
65
|
+
command: Object.freeze([runtime.executable, entrypoint.pathname]),
|
|
66
|
+
...(options.cwd ? { cwd: options.cwd } : {}),
|
|
67
|
+
env,
|
|
68
|
+
}));
|
|
69
|
+
let stopped = false;
|
|
70
|
+
const stop = async () => {
|
|
71
|
+
if (stopped)
|
|
72
|
+
return;
|
|
73
|
+
stopped = true;
|
|
74
|
+
child.kill("SIGTERM");
|
|
75
|
+
};
|
|
76
|
+
const ready = (async () => {
|
|
77
|
+
const deadline = now() + startupTimeoutMs;
|
|
78
|
+
try {
|
|
79
|
+
while (now() <= deadline) {
|
|
80
|
+
if (options.signal?.aborted) {
|
|
81
|
+
throw new DOMException("Backend launch aborted", "AbortError");
|
|
82
|
+
}
|
|
83
|
+
try {
|
|
84
|
+
const response = await runtime.fetch(healthUrl, {
|
|
85
|
+
signal: options.signal,
|
|
86
|
+
});
|
|
87
|
+
if (response.ok)
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
catch (error) {
|
|
91
|
+
if (options.signal?.aborted)
|
|
92
|
+
throw error;
|
|
93
|
+
}
|
|
94
|
+
await sleep(pollIntervalMs);
|
|
95
|
+
}
|
|
96
|
+
throw new Error(`DocsMint backend did not become ready within ${startupTimeoutMs}ms`);
|
|
97
|
+
}
|
|
98
|
+
catch (error) {
|
|
99
|
+
await stop();
|
|
100
|
+
throw error;
|
|
101
|
+
}
|
|
102
|
+
})();
|
|
103
|
+
return Object.freeze({
|
|
104
|
+
pid: child.pid,
|
|
105
|
+
ready,
|
|
106
|
+
exited: child.exited,
|
|
107
|
+
stop,
|
|
108
|
+
});
|
|
109
|
+
},
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
export function launchDocsmintBackend(options = {}) {
|
|
113
|
+
return createDocsmintBackendLauncher(defaultRuntime()).launch(options);
|
|
114
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/** Server-only, persistence-agnostic storage quota contract. */
|
|
2
|
+
export type StorageQuotaContext = Readonly<{
|
|
3
|
+
actorUserId: string;
|
|
4
|
+
requestId: string;
|
|
5
|
+
idempotencyKey: string;
|
|
6
|
+
signal?: AbortSignal;
|
|
7
|
+
}>;
|
|
8
|
+
export type StorageQuotaReservationRequest = StorageQuotaContext & Readonly<{
|
|
9
|
+
bytes: number;
|
|
10
|
+
}>;
|
|
11
|
+
export type StorageQuotaReservation = Readonly<{
|
|
12
|
+
status: "reserved" | "already_reserved";
|
|
13
|
+
reservationId: string;
|
|
14
|
+
reservedBytes: number;
|
|
15
|
+
usageBytes: number;
|
|
16
|
+
limitBytes: number;
|
|
17
|
+
expiresAt: string;
|
|
18
|
+
}>;
|
|
19
|
+
export type StorageQuotaRejection = Readonly<{
|
|
20
|
+
status: "rejected";
|
|
21
|
+
usageBytes: number;
|
|
22
|
+
limitBytes: number;
|
|
23
|
+
requestedBytes: number;
|
|
24
|
+
}>;
|
|
25
|
+
export type StorageQuotaCommitRequest = StorageQuotaContext & Readonly<{
|
|
26
|
+
reservationId: string;
|
|
27
|
+
actualBytes: number;
|
|
28
|
+
}>;
|
|
29
|
+
export type StorageQuotaReleaseRequest = StorageQuotaContext & Readonly<{
|
|
30
|
+
reservationId: string;
|
|
31
|
+
}>;
|
|
32
|
+
export type StorageQuotaCommitResult = Readonly<{
|
|
33
|
+
status: "committed" | "already_committed";
|
|
34
|
+
}>;
|
|
35
|
+
export type StorageQuotaReleaseResult = Readonly<{
|
|
36
|
+
status: "released" | "already_released" | "not_found";
|
|
37
|
+
}>;
|
|
38
|
+
export type StorageQuotaAdapter = Readonly<{
|
|
39
|
+
/** Must atomically check usage and create an idempotent reservation. */
|
|
40
|
+
reserve(request: StorageQuotaReservationRequest): Promise<StorageQuotaReservation | StorageQuotaRejection>;
|
|
41
|
+
commit(request: StorageQuotaCommitRequest): Promise<StorageQuotaCommitResult>;
|
|
42
|
+
release(request: StorageQuotaReleaseRequest): Promise<StorageQuotaReleaseResult>;
|
|
43
|
+
}>;
|
|
44
|
+
export type StorageQuotaService = Readonly<{
|
|
45
|
+
reserve(request: StorageQuotaReservationRequest): Promise<StorageQuotaReservation>;
|
|
46
|
+
commit(request: StorageQuotaCommitRequest): Promise<StorageQuotaCommitResult>;
|
|
47
|
+
release(request: StorageQuotaReleaseRequest): Promise<StorageQuotaReleaseResult>;
|
|
48
|
+
}>;
|
|
49
|
+
export declare class StorageQuotaExceededError extends Error {
|
|
50
|
+
readonly code: "STORAGE_QUOTA_EXCEEDED";
|
|
51
|
+
readonly usageBytes: number;
|
|
52
|
+
readonly limitBytes: number;
|
|
53
|
+
readonly requestedBytes: number;
|
|
54
|
+
constructor(rejection: StorageQuotaRejection);
|
|
55
|
+
}
|
|
56
|
+
export declare function createStorageQuotaService(adapter: StorageQuotaAdapter): StorageQuotaService;
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
/** Server-only, persistence-agnostic storage quota contract. */
|
|
2
|
+
export class StorageQuotaExceededError extends Error {
|
|
3
|
+
code = "STORAGE_QUOTA_EXCEEDED";
|
|
4
|
+
usageBytes;
|
|
5
|
+
limitBytes;
|
|
6
|
+
requestedBytes;
|
|
7
|
+
constructor(rejection) {
|
|
8
|
+
super("Storage quota exceeded");
|
|
9
|
+
this.name = "StorageQuotaExceededError";
|
|
10
|
+
this.usageBytes = rejection.usageBytes;
|
|
11
|
+
this.limitBytes = rejection.limitBytes;
|
|
12
|
+
this.requestedBytes = rejection.requestedBytes;
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
function assertContext(context) {
|
|
16
|
+
for (const [name, value] of [
|
|
17
|
+
["actorUserId", context.actorUserId],
|
|
18
|
+
["requestId", context.requestId],
|
|
19
|
+
["idempotencyKey", context.idempotencyKey],
|
|
20
|
+
]) {
|
|
21
|
+
if (!value.trim())
|
|
22
|
+
throw new TypeError(`${name} must not be empty`);
|
|
23
|
+
}
|
|
24
|
+
if (context.signal?.aborted) {
|
|
25
|
+
throw new DOMException("Storage quota operation aborted", "AbortError");
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
function assertPositiveBytes(bytes, name) {
|
|
29
|
+
if (!Number.isSafeInteger(bytes) || bytes <= 0) {
|
|
30
|
+
throw new TypeError(`${name} must be a positive safe integer`);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
function assertNonnegativeBytes(bytes, name) {
|
|
34
|
+
if (!Number.isSafeInteger(bytes) || bytes < 0) {
|
|
35
|
+
throw new TypeError(`${name} must be a non-negative safe integer`);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
function assertReservation(result) {
|
|
39
|
+
assertReservationId(result.reservationId);
|
|
40
|
+
assertPositiveBytes(result.reservedBytes, "reservedBytes");
|
|
41
|
+
assertNonnegativeBytes(result.usageBytes, "usageBytes");
|
|
42
|
+
assertNonnegativeBytes(result.limitBytes, "limitBytes");
|
|
43
|
+
if (Number.isNaN(Date.parse(result.expiresAt))) {
|
|
44
|
+
throw new TypeError("expiresAt must be an ISO-compatible timestamp");
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
function assertRejection(result) {
|
|
48
|
+
assertNonnegativeBytes(result.usageBytes, "usageBytes");
|
|
49
|
+
assertNonnegativeBytes(result.limitBytes, "limitBytes");
|
|
50
|
+
assertPositiveBytes(result.requestedBytes, "requestedBytes");
|
|
51
|
+
}
|
|
52
|
+
function assertReservationId(value) {
|
|
53
|
+
if (!value.trim())
|
|
54
|
+
throw new TypeError("reservationId must not be empty");
|
|
55
|
+
}
|
|
56
|
+
export function createStorageQuotaService(adapter) {
|
|
57
|
+
return Object.freeze({
|
|
58
|
+
async reserve(request) {
|
|
59
|
+
assertContext(request);
|
|
60
|
+
assertPositiveBytes(request.bytes, "bytes");
|
|
61
|
+
const result = await adapter.reserve(Object.freeze({ ...request }));
|
|
62
|
+
if (result.status === "rejected") {
|
|
63
|
+
assertRejection(result);
|
|
64
|
+
throw new StorageQuotaExceededError(result);
|
|
65
|
+
}
|
|
66
|
+
assertReservation(result);
|
|
67
|
+
return Object.freeze({ ...result });
|
|
68
|
+
},
|
|
69
|
+
async commit(request) {
|
|
70
|
+
assertContext(request);
|
|
71
|
+
assertReservationId(request.reservationId);
|
|
72
|
+
assertPositiveBytes(request.actualBytes, "actualBytes");
|
|
73
|
+
const result = await adapter.commit(Object.freeze({ ...request }));
|
|
74
|
+
if (result.status !== "committed" && result.status !== "already_committed") {
|
|
75
|
+
throw new TypeError("Storage quota adapter returned an invalid commit status");
|
|
76
|
+
}
|
|
77
|
+
return Object.freeze({ ...result });
|
|
78
|
+
},
|
|
79
|
+
async release(request) {
|
|
80
|
+
assertContext(request);
|
|
81
|
+
assertReservationId(request.reservationId);
|
|
82
|
+
const result = await adapter.release(Object.freeze({ ...request }));
|
|
83
|
+
if (result.status !== "released" &&
|
|
84
|
+
result.status !== "already_released" &&
|
|
85
|
+
result.status !== "not_found") {
|
|
86
|
+
throw new TypeError("Storage quota adapter returned an invalid release status");
|
|
87
|
+
}
|
|
88
|
+
return Object.freeze({ ...result });
|
|
89
|
+
},
|
|
90
|
+
});
|
|
91
|
+
}
|
package/package.json
CHANGED
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hiai-gg/docsmint",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.5",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"browser": {
|
|
6
|
+
"./dist/backend-launcher.js": false,
|
|
6
7
|
"./dist/lifecycle.js": false,
|
|
8
|
+
"./dist/storage-quota.js": false,
|
|
7
9
|
"./dist/workspace.js": false
|
|
8
10
|
},
|
|
9
11
|
"license": "Apache-2.0",
|
|
@@ -61,6 +63,16 @@
|
|
|
61
63
|
"import": "./dist/workspace.js",
|
|
62
64
|
"types": "./dist/workspace.d.ts"
|
|
63
65
|
},
|
|
66
|
+
"./backend/launcher": {
|
|
67
|
+
"browser": "./dist/server-only-browser-entry.js",
|
|
68
|
+
"import": "./dist/backend-launcher.js",
|
|
69
|
+
"types": "./dist/backend-launcher.d.ts"
|
|
70
|
+
},
|
|
71
|
+
"./storage-quota": {
|
|
72
|
+
"browser": "./dist/server-only-browser-entry.js",
|
|
73
|
+
"import": "./dist/storage-quota.js",
|
|
74
|
+
"types": "./dist/storage-quota.d.ts"
|
|
75
|
+
},
|
|
64
76
|
"./frontend/dashboard": {
|
|
65
77
|
"import": "./dist/frontend/dashboard.js",
|
|
66
78
|
"types": "./dist/frontend/dashboard.d.ts"
|
|
@@ -24,7 +24,7 @@ import { registerSearch } from "./commands/search.js";
|
|
|
24
24
|
import { registerSnapshot } from "./commands/snapshot.js";
|
|
25
25
|
import { registerUpdate } from "./commands/update.js";
|
|
26
26
|
|
|
27
|
-
const VERSION = "0.3.
|
|
27
|
+
const VERSION = "0.3.5";
|
|
28
28
|
|
|
29
29
|
const program = new Command();
|
|
30
30
|
program
|