@dench.com/cli 2.1.0 → 2.1.2
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/browser.ts +507 -0
- package/crm.ts +291 -37
- package/dench.ts +124 -41
- package/fs-daemon.ts +201 -2
- package/lib/api-schemas.ts +1100 -0
- package/lib/command-registry.ts +1664 -0
- package/lib/enrichment-gateway.ts +22 -0
- package/lib/organizationSelector.ts +58 -0
- package/linkedin.ts +210 -0
- package/package.json +6 -2
package/fs-daemon.ts
CHANGED
|
@@ -50,7 +50,14 @@
|
|
|
50
50
|
*/
|
|
51
51
|
import { createHash } from "node:crypto";
|
|
52
52
|
import { mkdirSync } from "node:fs";
|
|
53
|
-
import {
|
|
53
|
+
import {
|
|
54
|
+
mkdir,
|
|
55
|
+
readdir,
|
|
56
|
+
readFile,
|
|
57
|
+
stat,
|
|
58
|
+
unlink,
|
|
59
|
+
writeFile,
|
|
60
|
+
} from "node:fs/promises";
|
|
54
61
|
import { dirname, relative } from "node:path";
|
|
55
62
|
import { ConvexClient, ConvexHttpClient } from "convex/browser";
|
|
56
63
|
import { makeFunctionReference } from "convex/server";
|
|
@@ -142,6 +149,33 @@ function sha256(bytes: Uint8Array): string {
|
|
|
142
149
|
return createHash("sha256").update(bytes).digest("hex");
|
|
143
150
|
}
|
|
144
151
|
|
|
152
|
+
function normalizePosixWorkspacePath(input: string): string {
|
|
153
|
+
let cleaned = input.replace(/\\/g, "/").replace(/\/+/g, "/");
|
|
154
|
+
if (cleaned.startsWith("/workspace/")) {
|
|
155
|
+
cleaned = cleaned.slice("/workspace".length);
|
|
156
|
+
} else if (cleaned === "/workspace") {
|
|
157
|
+
cleaned = "/";
|
|
158
|
+
}
|
|
159
|
+
if (!cleaned.startsWith("/")) cleaned = `/${cleaned}`;
|
|
160
|
+
return cleaned.length > 1 && cleaned.endsWith("/")
|
|
161
|
+
? cleaned.slice(0, -1)
|
|
162
|
+
: cleaned;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
type PullRequest = {
|
|
166
|
+
id: string;
|
|
167
|
+
paths: string[];
|
|
168
|
+
requestedAt: number;
|
|
169
|
+
};
|
|
170
|
+
|
|
171
|
+
type PullResponse = {
|
|
172
|
+
errors: Array<{ error: string; path: string }>;
|
|
173
|
+
id: string;
|
|
174
|
+
ok: boolean;
|
|
175
|
+
pulled: Array<{ contentHash: string; path: string }>;
|
|
176
|
+
respondedAt: number;
|
|
177
|
+
};
|
|
178
|
+
|
|
145
179
|
const SYNC_DEBOUNCE_MS = 300;
|
|
146
180
|
const MAX_INLINE_TEXT_BYTES = 100_000;
|
|
147
181
|
const CIRCUIT_BREAKER_LIMIT = 5;
|
|
@@ -155,6 +189,7 @@ const RECONCILE_INTERVAL_DEFAULT_MS = 30_000;
|
|
|
155
189
|
// Status / breaker state visible to `dench fs status` over a tiny
|
|
156
190
|
// JSON file the daemon refreshes after each tick.
|
|
157
191
|
const DAEMON_STATUS_PATH = "/tmp/dench-fs-daemon.status.json";
|
|
192
|
+
const PULL_REQUEST_DIR = "/tmp/dench-fs-daemon.pull";
|
|
158
193
|
|
|
159
194
|
type BreakerState = {
|
|
160
195
|
path: string;
|
|
@@ -421,6 +456,13 @@ class DenchFileClient {
|
|
|
421
456
|
return Array.isArray(rows) ? rows : [];
|
|
422
457
|
}
|
|
423
458
|
|
|
459
|
+
async getFileTreeRow(path: string): Promise<FileTreeSyncRow | null> {
|
|
460
|
+
const normalized = normalizePosixWorkspacePath(path);
|
|
461
|
+
const parent = dirname(normalized) === "." ? "/" : dirname(normalized);
|
|
462
|
+
const rows = await this.listTree(parent);
|
|
463
|
+
return rows.find((row) => row.path === normalized) ?? null;
|
|
464
|
+
}
|
|
465
|
+
|
|
424
466
|
async getSignedDownloadUrl(path: string): Promise<string | null> {
|
|
425
467
|
const url = (await this.http.action(api.files.getFileSignedDownloadUrl, {
|
|
426
468
|
path,
|
|
@@ -526,6 +568,68 @@ async function pullDownRemoteFile(
|
|
|
526
568
|
);
|
|
527
569
|
}
|
|
528
570
|
|
|
571
|
+
async function processPullRequests(args: {
|
|
572
|
+
client: DenchFileClient;
|
|
573
|
+
tracker: HashTracker;
|
|
574
|
+
workspace: string;
|
|
575
|
+
}): Promise<void> {
|
|
576
|
+
await mkdir(PULL_REQUEST_DIR, { recursive: true });
|
|
577
|
+
const entries = await readdir(PULL_REQUEST_DIR).catch(() => []);
|
|
578
|
+
const requestFiles = entries.filter(
|
|
579
|
+
(entry) => entry.startsWith("request-") && entry.endsWith(".json"),
|
|
580
|
+
);
|
|
581
|
+
for (const entry of requestFiles) {
|
|
582
|
+
const requestPath = `${PULL_REQUEST_DIR}/${entry}`;
|
|
583
|
+
let request: PullRequest | null = null;
|
|
584
|
+
try {
|
|
585
|
+
request = JSON.parse(await readFile(requestPath, "utf-8")) as PullRequest;
|
|
586
|
+
} catch (error) {
|
|
587
|
+
console.error(
|
|
588
|
+
`[dench-fs-daemon] invalid pull request ${entry}: ${
|
|
589
|
+
error instanceof Error ? error.message : String(error)
|
|
590
|
+
}`,
|
|
591
|
+
);
|
|
592
|
+
await unlink(requestPath).catch(() => undefined);
|
|
593
|
+
continue;
|
|
594
|
+
}
|
|
595
|
+
const response: PullResponse = {
|
|
596
|
+
errors: [],
|
|
597
|
+
id: request.id,
|
|
598
|
+
ok: true,
|
|
599
|
+
pulled: [],
|
|
600
|
+
respondedAt: Date.now(),
|
|
601
|
+
};
|
|
602
|
+
for (const rawPath of request.paths) {
|
|
603
|
+
const path = normalizePosixWorkspacePath(rawPath);
|
|
604
|
+
try {
|
|
605
|
+
const row = await args.client.getFileTreeRow(path);
|
|
606
|
+
if (!row || row.isDir || !row.contentHash) {
|
|
607
|
+
throw new Error("File is not present in Convex fileTree.");
|
|
608
|
+
}
|
|
609
|
+
await pullDownRemoteFile(args.client, {
|
|
610
|
+
expectedHash: row.contentHash,
|
|
611
|
+
posixPath: path,
|
|
612
|
+
tracker: args.tracker,
|
|
613
|
+
workspace: args.workspace,
|
|
614
|
+
});
|
|
615
|
+
response.pulled.push({ contentHash: row.contentHash, path });
|
|
616
|
+
} catch (error) {
|
|
617
|
+
response.ok = false;
|
|
618
|
+
response.errors.push({
|
|
619
|
+
error: error instanceof Error ? error.message : String(error),
|
|
620
|
+
path,
|
|
621
|
+
});
|
|
622
|
+
}
|
|
623
|
+
}
|
|
624
|
+
await writeFile(
|
|
625
|
+
`${PULL_REQUEST_DIR}/response-${request.id}.json`,
|
|
626
|
+
JSON.stringify(response),
|
|
627
|
+
"utf-8",
|
|
628
|
+
);
|
|
629
|
+
await unlink(requestPath).catch(() => undefined);
|
|
630
|
+
}
|
|
631
|
+
}
|
|
632
|
+
|
|
529
633
|
/**
|
|
530
634
|
* sysexits.h-style exit codes the supervisor (in
|
|
531
635
|
* src/lib/daytona/sandbox-bootstrap.ts) recognizes as **permanent** —
|
|
@@ -785,6 +889,21 @@ async function runDaemon(daemonArgs: Args): Promise<void> {
|
|
|
785
889
|
.catch(() => undefined);
|
|
786
890
|
});
|
|
787
891
|
|
|
892
|
+
process.on("SIGUSR2", () => {
|
|
893
|
+
console.log(`[dench-fs-daemon] SIGUSR2 received — processing pull queue`);
|
|
894
|
+
void processPullRequests({
|
|
895
|
+
client,
|
|
896
|
+
tracker,
|
|
897
|
+
workspace: daemonArgs.workspace,
|
|
898
|
+
}).catch((error) =>
|
|
899
|
+
console.error(
|
|
900
|
+
`[dench-fs-daemon] pull queue failed: ${
|
|
901
|
+
error instanceof Error ? error.message : String(error)
|
|
902
|
+
}`,
|
|
903
|
+
),
|
|
904
|
+
);
|
|
905
|
+
});
|
|
906
|
+
|
|
788
907
|
process.on("SIGTERM", () => {
|
|
789
908
|
reconciler.stop();
|
|
790
909
|
unsubscribe();
|
|
@@ -1303,6 +1422,82 @@ async function runFlush(argv: string[]): Promise<void> {
|
|
|
1303
1422
|
outcome.reason === "no_daemon" ? 1 : outcome.reason === "timeout" ? 2 : 3;
|
|
1304
1423
|
}
|
|
1305
1424
|
|
|
1425
|
+
async function runPull(argv: string[]): Promise<void> {
|
|
1426
|
+
const json = argv.includes("--json");
|
|
1427
|
+
const wait = argv.includes("--wait");
|
|
1428
|
+
const timeoutIndex = argv.indexOf("--timeout-ms");
|
|
1429
|
+
const timeoutMs =
|
|
1430
|
+
timeoutIndex === -1
|
|
1431
|
+
? 10_000
|
|
1432
|
+
: Number.parseInt(argv[timeoutIndex + 1] ?? "10000", 10);
|
|
1433
|
+
const paths: string[] = [];
|
|
1434
|
+
for (let i = 0; i < argv.length; i += 1) {
|
|
1435
|
+
const arg = argv[i];
|
|
1436
|
+
if (arg === "--json" || arg === "--wait") continue;
|
|
1437
|
+
if (arg === "--timeout-ms") {
|
|
1438
|
+
i += 1;
|
|
1439
|
+
continue;
|
|
1440
|
+
}
|
|
1441
|
+
paths.push(normalizePosixWorkspacePath(arg));
|
|
1442
|
+
}
|
|
1443
|
+
if (paths.length === 0) {
|
|
1444
|
+
throw new Error("dench-fs-daemon pull requires at least one path");
|
|
1445
|
+
}
|
|
1446
|
+
const status = (await readStatusFile()).state;
|
|
1447
|
+
if (!status || !isPidAlive(status.pid)) {
|
|
1448
|
+
const response = { ok: false, reason: "no_daemon" };
|
|
1449
|
+
if (json) process.stdout.write(`${JSON.stringify(response)}\n`);
|
|
1450
|
+
else process.stderr.write("DENCH_FS_PULL=no_daemon\n");
|
|
1451
|
+
process.exitCode = 1;
|
|
1452
|
+
return;
|
|
1453
|
+
}
|
|
1454
|
+
await mkdir(PULL_REQUEST_DIR, { recursive: true });
|
|
1455
|
+
const id = `${Date.now()}-${Math.random().toString(36).slice(2)}`;
|
|
1456
|
+
const request: PullRequest = { id, paths, requestedAt: Date.now() };
|
|
1457
|
+
await writeFile(
|
|
1458
|
+
`${PULL_REQUEST_DIR}/request-${id}.json`,
|
|
1459
|
+
JSON.stringify(request),
|
|
1460
|
+
"utf-8",
|
|
1461
|
+
);
|
|
1462
|
+
process.kill(status.pid, "SIGUSR2");
|
|
1463
|
+
if (!wait) {
|
|
1464
|
+
const response = { ok: true, id, queued: paths };
|
|
1465
|
+
if (json) process.stdout.write(`${JSON.stringify(response)}\n`);
|
|
1466
|
+
else process.stdout.write(`DENCH_FS_PULL=queued id=${id}\n`);
|
|
1467
|
+
return;
|
|
1468
|
+
}
|
|
1469
|
+
|
|
1470
|
+
const responsePath = `${PULL_REQUEST_DIR}/response-${id}.json`;
|
|
1471
|
+
const deadline =
|
|
1472
|
+
Date.now() + (Number.isFinite(timeoutMs) ? timeoutMs : 10_000);
|
|
1473
|
+
while (Date.now() < deadline) {
|
|
1474
|
+
try {
|
|
1475
|
+
const response = JSON.parse(
|
|
1476
|
+
await readFile(responsePath, "utf-8"),
|
|
1477
|
+
) as PullResponse;
|
|
1478
|
+
await unlink(responsePath).catch(() => undefined);
|
|
1479
|
+
if (json) process.stdout.write(`${JSON.stringify(response)}\n`);
|
|
1480
|
+
else if (response.ok) {
|
|
1481
|
+
process.stdout.write(
|
|
1482
|
+
`DENCH_FS_PULL=ok pulled=${response.pulled.length}\n`,
|
|
1483
|
+
);
|
|
1484
|
+
} else {
|
|
1485
|
+
process.stderr.write(
|
|
1486
|
+
`DENCH_FS_PULL=failed errors=${response.errors.length}\n`,
|
|
1487
|
+
);
|
|
1488
|
+
}
|
|
1489
|
+
if (!response.ok) process.exitCode = 3;
|
|
1490
|
+
return;
|
|
1491
|
+
} catch {
|
|
1492
|
+
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
1493
|
+
}
|
|
1494
|
+
}
|
|
1495
|
+
const timeoutResponse = { ok: false, id, paths, reason: "timeout" };
|
|
1496
|
+
if (json) process.stdout.write(`${JSON.stringify(timeoutResponse)}\n`);
|
|
1497
|
+
else process.stderr.write(`DENCH_FS_PULL=timeout id=${id}\n`);
|
|
1498
|
+
process.exitCode = 2;
|
|
1499
|
+
}
|
|
1500
|
+
|
|
1306
1501
|
async function main(): Promise<void> {
|
|
1307
1502
|
const argv = process.argv.slice(2);
|
|
1308
1503
|
const subcommand = argv[0];
|
|
@@ -1327,6 +1522,10 @@ async function main(): Promise<void> {
|
|
|
1327
1522
|
await runFlush(argv.slice(1));
|
|
1328
1523
|
return;
|
|
1329
1524
|
}
|
|
1525
|
+
if (subcommand === "pull") {
|
|
1526
|
+
await runPull(argv.slice(1));
|
|
1527
|
+
return;
|
|
1528
|
+
}
|
|
1330
1529
|
if (subcommand === "status") {
|
|
1331
1530
|
await runStatus(argv.slice(1));
|
|
1332
1531
|
return;
|
|
@@ -1425,7 +1624,7 @@ async function collectStatus(args: {
|
|
|
1425
1624
|
|
|
1426
1625
|
const pidAlive = statusFile.state ? isPidAlive(statusFile.state.pid) : false;
|
|
1427
1626
|
|
|
1428
|
-
|
|
1627
|
+
const onDiskList: Array<{ path: string; size: number; mtimeMs: number }> = [];
|
|
1429
1628
|
let convexRows: Array<{
|
|
1430
1629
|
path: string;
|
|
1431
1630
|
contentHash?: string;
|