Establishes version control for the personal Logseq DB fork before starting the server-client architecture migration and domain features described in docs/agent-guide and the approved implementation plan. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
451 lines
16 KiB
JavaScript
451 lines
16 KiB
JavaScript
import assert from "node:assert/strict";
|
|
import { execFileSync } from "node:child_process";
|
|
import { mkdtempSync } from "node:fs";
|
|
import os from "node:os";
|
|
import fs from "node:fs";
|
|
import path from "node:path";
|
|
import { fileURLToPath } from "node:url";
|
|
|
|
const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
|
|
|
|
const readText = (relativePath) =>
|
|
fs.readFileSync(path.join(repoRoot, relativePath), "utf8");
|
|
|
|
const readJson = (relativePath) =>
|
|
JSON.parse(readText(relativePath));
|
|
|
|
const rootPackage = readJson("package.json");
|
|
const desktopPackage = readJson("resources/package.json");
|
|
|
|
const zvecOptionalRuntimeDependencies = [
|
|
"@zvec/bindings-darwin-arm64",
|
|
"@zvec/bindings-linux-arm64",
|
|
"@zvec/bindings-linux-x64",
|
|
"@zvec/bindings-win32-x64",
|
|
"@zvec/zvec",
|
|
];
|
|
|
|
const assertNotContains = (text, needle, label) => {
|
|
assert.equal(
|
|
text.includes(needle),
|
|
false,
|
|
`${label} should not contain ${needle}`,
|
|
);
|
|
};
|
|
|
|
const assertNoShadowRuntime = (text, label) => {
|
|
for (const needle of ["SHADOW_IMPORT", ".shadow-cljs", "cljs-runtime"]) {
|
|
assertNotContains(text, needle, label);
|
|
}
|
|
};
|
|
|
|
const assertRootScriptDoesNotBuildShadowCli = (scriptName, command) => {
|
|
assert.ok(command, `package.json should define ${scriptName}`);
|
|
assert.doesNotMatch(
|
|
command,
|
|
/clojure -[MA]:cljs (?:watch|compile|release)[^"]*\blogseq-cli\b/,
|
|
`${scriptName} should not build the old Shadow CLI`,
|
|
);
|
|
};
|
|
|
|
const assertCliReleaseCommand = (command, label) => {
|
|
assert.match(command, /pnpm --dir cli bundle/, `${label} should bundle cli/`);
|
|
assert.match(
|
|
command,
|
|
/node \.\/scripts\/stage-cli-runtime\.mjs/,
|
|
`${label} should stage the cli/ bundle`,
|
|
);
|
|
};
|
|
|
|
const filesUnder = (relativePath) => {
|
|
const absolutePath = path.join(repoRoot, relativePath);
|
|
if (!fs.existsSync(absolutePath)) {
|
|
return [];
|
|
}
|
|
const stat = fs.statSync(absolutePath);
|
|
if (stat.isFile()) {
|
|
return [relativePath];
|
|
}
|
|
return fs
|
|
.readdirSync(absolutePath, { withFileTypes: true })
|
|
.flatMap((entry) => {
|
|
const entryPath = path.join(relativePath, entry.name);
|
|
return entry.isDirectory() ? filesUnder(entryPath) : [entryPath];
|
|
});
|
|
};
|
|
|
|
const assertFilesDoNotMatch = (relativePaths, pattern, label) => {
|
|
const matches = relativePaths.flatMap((relativePath) => {
|
|
const text = readText(relativePath);
|
|
return pattern.test(text) ? [relativePath] : [];
|
|
});
|
|
assert.deepEqual(matches, [], `${label} should not contain ${pattern}`);
|
|
};
|
|
|
|
const workflow = readText(".github/workflows/deps-cli.yml");
|
|
assertNotContains(workflow, "deps/cli", "deps-cli workflow");
|
|
assert.match(workflow, /cli\/\*\*/, "workflow should watch cli/ sources");
|
|
assert.match(workflow, /pnpm cli:release/, "workflow should build new CLI");
|
|
assertNotContains(workflow, "clojure -M:cljs release logseq-cli", "deps-cli workflow");
|
|
assert.match(workflow, /pnpm db-worker-node:release:bundle/, "workflow should build bundled db-worker-node runtime");
|
|
assert.match(workflow, /node scripts\/prepare-cli-package\.mjs/, "workflow should prepare publish package");
|
|
assert.match(workflow, /working-directory: dist\/cli-package/, "workflow should publish prepared package");
|
|
|
|
const buildWorkflow = readText(".github/workflows/build.yml");
|
|
assert.match(buildWorkflow, /pnpm cli:release/, "db graph workflow should build release CLI");
|
|
assertNotContains(buildWorkflow, "clojure -M:cljs release logseq-cli", "db graph workflow");
|
|
assert.match(buildWorkflow, /pnpm db-worker-node:release:bundle/, "db graph workflow should build release db-worker-node runtime");
|
|
assert.match(buildWorkflow, /pnpm --dir dist\/cli-package install --prod --ignore-workspace/, "db graph workflow should install prepared CLI package dependencies");
|
|
assert.match(buildWorkflow, /libsecret-1-0/, "db graph workflow should install keytar's Linux runtime dependency");
|
|
assert.match(buildWorkflow, /pnpm --dir dist\/cli-package pack --pack-destination \.\.\//, "db graph workflow should verify the prepared CLI package with pnpm pack");
|
|
assertNotContains(buildWorkflow, "create_graph_with_schema_org.cljs ./cli-root/graphs/schema-graph --subset", "db graph workflow");
|
|
assert.match(buildWorkflow, /node dist\/cli-package\/dist\/logseq\.js --root-dir scripts\/cli-root/, "db graph workflow should test packaged CLI");
|
|
assert.match(buildWorkflow, /node dist\/cli-package\/dist\/logseq\.js --root-dir scripts\/cli-root.+--timeout-ms 3000/, "db graph workflow should use a 3s CLI request timeout for packaged CLI graph commands");
|
|
assert.match(buildWorkflow, /--graph schema-graph --timeout-ms 120000/, "db graph workflow should allow the full schema graph validation to finish in CI");
|
|
assertNotContains(buildWorkflow, "clojure -M:cljs compile logseq-cli", "db graph workflow");
|
|
assertNotContains(buildWorkflow, "pnpm db-worker-node:compile:bundle", "db graph workflow");
|
|
|
|
const desktopReleaseWorkflow = readText(".github/workflows/build-desktop-release.yml");
|
|
assert.match(
|
|
desktopReleaseWorkflow,
|
|
/OCAML_VERSION: '5\.1\.1'/,
|
|
"desktop release workflow should define the OCaml version used by cli/",
|
|
);
|
|
assert.match(
|
|
desktopReleaseWorkflow,
|
|
/uses: ocaml\/setup-ocaml@v3/,
|
|
"desktop release workflow should set up OCaml before building cli/",
|
|
);
|
|
assert.match(
|
|
desktopReleaseWorkflow,
|
|
/working-directory: cli\s+run: opam install \. --deps-only --with-test --yes/,
|
|
"desktop release workflow should install cli/ OCaml deps",
|
|
);
|
|
assert.match(
|
|
desktopReleaseWorkflow,
|
|
/pnpm --dir cli install --frozen-lockfile --ignore-workspace/,
|
|
"desktop release workflow should install cli/ pnpm deps",
|
|
);
|
|
assert.match(
|
|
desktopReleaseWorkflow,
|
|
/opam exec -- pnpm cli:release/,
|
|
"desktop release workflow should build and stage the OCaml CLI",
|
|
);
|
|
assert.ok(
|
|
desktopReleaseWorkflow.indexOf("opam exec -- pnpm cli:release") <
|
|
desktopReleaseWorkflow.indexOf("pnpm desktop:prepare-runtime-js"),
|
|
"desktop release workflow should stage the CLI before preparing desktop runtime scripts",
|
|
);
|
|
assertNotContains(
|
|
desktopReleaseWorkflow,
|
|
"clojure -M:cljs release logseq-cli",
|
|
"desktop release workflow",
|
|
);
|
|
|
|
const shadowCljs = readText("shadow-cljs.edn");
|
|
assertNotContains(shadowCljs, ":logseq-cli", "shadow-cljs.edn");
|
|
|
|
assertCliReleaseCommand(rootPackage.scripts?.["cli:release"], "cli:release");
|
|
for (const [scriptName, command] of Object.entries(rootPackage.scripts ?? {})) {
|
|
assertRootScriptDoesNotBuildShadowCli(scriptName, command);
|
|
}
|
|
|
|
const gulpfile = readText("gulpfile.js");
|
|
assert.match(
|
|
gulpfile,
|
|
/pnpm cli:release/,
|
|
"Electron maker preparation should stage the CLI runtime",
|
|
);
|
|
assert.ok(
|
|
gulpfile.indexOf("pnpm cli:release") <
|
|
gulpfile.indexOf("pnpm desktop:prepare-runtime-js"),
|
|
"Electron maker preparation should stage the CLI before preparing desktop runtime scripts",
|
|
);
|
|
|
|
const stageScriptPath = path.join(repoRoot, "scripts", "stage-cli-runtime.mjs");
|
|
assert.equal(
|
|
fs.existsSync(stageScriptPath),
|
|
true,
|
|
"scripts/stage-cli-runtime.mjs should exist",
|
|
);
|
|
const stageScript = readText("scripts/stage-cli-runtime.mjs");
|
|
assert.match(
|
|
stageScript,
|
|
/cli[\\/]_build[\\/]default[\\/]dist[\\/]logseq-cli\.js/,
|
|
"stage script should read cli/_build/default/dist/logseq-cli.js",
|
|
);
|
|
assert.match(
|
|
stageScript,
|
|
/static[\\/]logseq-cli\.js/,
|
|
"stage script should write static/logseq-cli.js",
|
|
);
|
|
for (const needle of ["SHADOW_IMPORT", ".shadow-cljs", "cljs-runtime"]) {
|
|
assert.match(
|
|
stageScript,
|
|
new RegExp(needle.replace(".", "\\.")),
|
|
`stage script should reject ${needle}`,
|
|
);
|
|
}
|
|
|
|
const runStageCliRuntime = (fixtureRoot) =>
|
|
execFileSync(process.execPath, ["scripts/stage-cli-runtime.mjs"], {
|
|
cwd: repoRoot,
|
|
env: {
|
|
...process.env,
|
|
LOGSEQ_STAGE_CLI_RUNTIME_REPO_ROOT: fixtureRoot,
|
|
},
|
|
encoding: "utf8",
|
|
stdio: ["ignore", "pipe", "pipe"],
|
|
});
|
|
|
|
const makeStageFixture = () =>
|
|
mkdtempSync(path.join(os.tmpdir(), "logseq-stage-cli-"));
|
|
|
|
{
|
|
const fixtureRoot = makeStageFixture();
|
|
assert.throws(
|
|
() => runStageCliRuntime(fixtureRoot),
|
|
/cli\/_build\/default\/dist\/logseq-cli\.js/,
|
|
"stage script should fail when the cli/ bundle is missing",
|
|
);
|
|
}
|
|
|
|
{
|
|
const fixtureRoot = makeStageFixture();
|
|
const sourcePath = path.join(
|
|
fixtureRoot,
|
|
"cli",
|
|
"_build",
|
|
"default",
|
|
"dist",
|
|
"logseq-cli.js",
|
|
);
|
|
fs.mkdirSync(path.dirname(sourcePath), { recursive: true });
|
|
fs.writeFileSync(sourcePath, "console.log('SHADOW_IMPORT');\n");
|
|
assert.throws(
|
|
() => runStageCliRuntime(fixtureRoot),
|
|
/SHADOW_IMPORT/,
|
|
"stage script should reject Shadow runtime markers",
|
|
);
|
|
}
|
|
|
|
{
|
|
const fixtureRoot = makeStageFixture();
|
|
const sourcePath = path.join(
|
|
fixtureRoot,
|
|
"cli",
|
|
"_build",
|
|
"default",
|
|
"dist",
|
|
"logseq-cli.js",
|
|
);
|
|
const stagedPath = path.join(fixtureRoot, "static", "logseq-cli.js");
|
|
const staleMapPath = path.join(fixtureRoot, "static", "logseq-cli.js.map");
|
|
fs.mkdirSync(path.dirname(sourcePath), { recursive: true });
|
|
fs.mkdirSync(path.dirname(staleMapPath), { recursive: true });
|
|
fs.writeFileSync(sourcePath, "console.log('new cli');\n");
|
|
fs.writeFileSync(staleMapPath, "{}\n");
|
|
const output = runStageCliRuntime(fixtureRoot);
|
|
assert.equal(
|
|
fs.readFileSync(stagedPath, "utf8"),
|
|
"console.log('new cli');\n",
|
|
);
|
|
assert.equal(
|
|
fs.existsSync(staleMapPath),
|
|
false,
|
|
"stage script should remove stale root sourcemap",
|
|
);
|
|
assert.match(output, /cli\/_build\/default\/dist\/logseq-cli\.js/);
|
|
assert.match(output, /static\/logseq-cli\.js/);
|
|
}
|
|
|
|
{
|
|
const fixtureRoot = makeStageFixture();
|
|
const sourcePath = path.join(
|
|
fixtureRoot,
|
|
"cli",
|
|
"_build",
|
|
"default",
|
|
"dist",
|
|
"logseq-cli.js",
|
|
);
|
|
const stagedPath = path.join(fixtureRoot, "static", "logseq-cli.js");
|
|
fs.mkdirSync(path.dirname(sourcePath), { recursive: true });
|
|
fs.mkdirSync(path.dirname(stagedPath), { recursive: true });
|
|
fs.writeFileSync(sourcePath, "console.log('first');\n");
|
|
fs.writeFileSync(stagedPath, "console.log('old');\n");
|
|
fs.chmodSync(stagedPath, 0o444);
|
|
runStageCliRuntime(fixtureRoot);
|
|
assert.equal(
|
|
fs.readFileSync(stagedPath, "utf8"),
|
|
"console.log('first');\n",
|
|
"stage script should overwrite an existing read-only staged file",
|
|
);
|
|
}
|
|
|
|
assert.equal(
|
|
fs.existsSync(path.join(repoRoot, "deps/cli/package.json")),
|
|
false,
|
|
"old deps/cli package should be removed",
|
|
);
|
|
|
|
const depsEdn = readText("deps.edn");
|
|
assertNotContains(depsEdn, 'logseq/cli', "deps.edn");
|
|
assertNotContains(depsEdn, '"deps/cli"', "deps.edn");
|
|
|
|
const bbEdn = readText("bb.edn");
|
|
assertNotContains(bbEdn, "legacy cli", "bb.edn");
|
|
assertNotContains(bbEdn, "../cli/src", "bb.edn");
|
|
|
|
const lintTask = readText("scripts/src/logseq/tasks/dev/lint.clj");
|
|
assertNotContains(lintTask, '"deps/cli"', "lint task");
|
|
|
|
const lintDepsTask = readText("scripts/src/logseq/tasks/dev/lint_test_deps.clj");
|
|
assertNotContains(lintDepsTask, '"deps/cli"', "lint/test deps task");
|
|
|
|
for (const dependencyName of zvecOptionalRuntimeDependencies) {
|
|
assert.equal(
|
|
rootPackage.dependencies?.[dependencyName],
|
|
undefined,
|
|
`${dependencyName} should not be a hard root runtime dependency`,
|
|
);
|
|
assert.ok(
|
|
rootPackage.optionalDependencies?.[dependencyName],
|
|
`${dependencyName} should be an optional root runtime dependency`,
|
|
);
|
|
assert.equal(
|
|
desktopPackage.dependencies?.[dependencyName],
|
|
undefined,
|
|
`${dependencyName} should not be a hard desktop runtime dependency`,
|
|
);
|
|
assert.ok(
|
|
desktopPackage.optionalDependencies?.[dependencyName],
|
|
`${dependencyName} should be an optional desktop runtime dependency`,
|
|
);
|
|
}
|
|
|
|
assert.equal(
|
|
fs.existsSync(path.join(repoRoot, "src/main/logseq/cli/common/mcp/server.cljs")),
|
|
false,
|
|
"CLI sources should not include an MCP server",
|
|
);
|
|
|
|
assertFilesDoNotMatch(
|
|
[
|
|
...filesUnder("src/main/logseq/cli"),
|
|
...filesUnder("src/test/logseq/cli"),
|
|
"scripts/prepare-cli-package.mjs",
|
|
],
|
|
/logseq\.cli\.common\.mcp|modelcontextprotocol|zod\/v3|mcp-server/,
|
|
"CLI sources and package preparation",
|
|
);
|
|
|
|
const prepareCliPackageScript = readText("scripts/prepare-cli-package.mjs");
|
|
assert.match(
|
|
prepareCliPackageScript,
|
|
/static[\\/]logseq-cli\.js/,
|
|
"package preparation should package the staged CLI",
|
|
);
|
|
assert.match(
|
|
prepareCliPackageScript,
|
|
/cli[\\/]_build[\\/]default[\\/]dist[\\/]logseq-cli\.js/,
|
|
"package preparation should compare the staged CLI against the cli/ bundle",
|
|
);
|
|
for (const needle of ["SHADOW_IMPORT", ".shadow-cljs", "cljs-runtime"]) {
|
|
assert.match(
|
|
prepareCliPackageScript,
|
|
new RegExp(needle.replace(".", "\\.")),
|
|
`package preparation should reject ${needle}`,
|
|
);
|
|
}
|
|
|
|
const prepareDesktopRuntimeScript = readText(
|
|
"scripts/prepare-desktop-runtime-js.mjs",
|
|
);
|
|
assert.match(
|
|
prepareDesktopRuntimeScript,
|
|
/static[\\/]logseq-cli\.js/,
|
|
"desktop runtime preparation should read the staged CLI",
|
|
);
|
|
assert.match(
|
|
prepareDesktopRuntimeScript,
|
|
/static[\\/]js[\\/]logseq-cli\.js/,
|
|
"desktop runtime preparation should write static/js/logseq-cli.js",
|
|
);
|
|
assert.match(
|
|
prepareDesktopRuntimeScript,
|
|
/cli[\\/]_build[\\/]default[\\/]dist[\\/]logseq-cli\.js/,
|
|
"desktop runtime preparation should compare the staged CLI against the cli/ bundle",
|
|
);
|
|
assert.match(
|
|
prepareDesktopRuntimeScript,
|
|
/fs\.chmod\(to, stats\.mode \| 0o200\)/,
|
|
"desktop runtime preparation should overwrite read-only staged runtime files",
|
|
);
|
|
assertNotContains(
|
|
prepareDesktopRuntimeScript,
|
|
'fs.rm(path.join(staticDir, "logseq-cli.js")',
|
|
"desktop runtime preparation should keep root staged CLI available",
|
|
);
|
|
assertNotContains(
|
|
prepareDesktopRuntimeScript,
|
|
'fs.rm(path.join(staticDir, "db-worker-node.js")',
|
|
"desktop runtime preparation should keep root staged db-worker-node available",
|
|
);
|
|
|
|
execFileSync(process.execPath, ["scripts/prepare-cli-package.mjs"], {
|
|
cwd: repoRoot,
|
|
stdio: "pipe",
|
|
});
|
|
|
|
const packageRoot = path.join(repoRoot, "dist/cli-package");
|
|
const packageJson = JSON.parse(
|
|
fs.readFileSync(path.join(packageRoot, "package.json"), "utf8"),
|
|
);
|
|
|
|
assert.equal(packageJson.name, "@logseq/cli");
|
|
assert.equal(packageJson.bin.logseq, "dist/logseq.js");
|
|
assert.equal(packageJson.private, undefined);
|
|
assert.equal(packageJson.dependencies?.["@modelcontextprotocol/sdk"], undefined);
|
|
assert.equal(packageJson.dependencies?.zod, undefined);
|
|
assert.ok(packageJson.dependencies?.["@js-joda/core"], "publish package should include @js-joda/core for release artifacts");
|
|
assert.ok(packageJson.dependencies?.keytar, "publish package should include keytar for db-worker-node");
|
|
assert.ok(packageJson.dependencies?.["string-width"], "publish package should include string-width for CLI rendering");
|
|
for (const dependencyName of zvecOptionalRuntimeDependencies) {
|
|
assert.equal(
|
|
packageJson.dependencies?.[dependencyName],
|
|
undefined,
|
|
`${dependencyName} should not be a hard publish package dependency`,
|
|
);
|
|
assert.ok(
|
|
packageJson.optionalDependencies?.[dependencyName],
|
|
`${dependencyName} should be an optional publish package dependency`,
|
|
);
|
|
}
|
|
assert.deepEqual(packageJson.pnpm?.onlyBuiltDependencies, [
|
|
"@zvec/zvec",
|
|
"better-sqlite3",
|
|
"keytar",
|
|
]);
|
|
assert.deepEqual(packageJson.files, [
|
|
"dist/logseq.js",
|
|
"static/logseq-cli.js",
|
|
"static/js/db-worker-node.js",
|
|
"static/js/db-worker-node-assets.json",
|
|
".agents/skills/logseq-cli/SKILL.md",
|
|
]);
|
|
|
|
for (const relativePath of packageJson.files) {
|
|
assert.equal(
|
|
fs.existsSync(path.join(packageRoot, relativePath)),
|
|
true,
|
|
`publish package should include ${relativePath}`,
|
|
);
|
|
}
|
|
|
|
assertNoShadowRuntime(readText("static/logseq-cli.js"), "root CLI release artifact");
|
|
assertNoShadowRuntime(
|
|
fs.readFileSync(path.join(packageRoot, "static/logseq-cli.js"), "utf8"),
|
|
"publish package CLI release artifact",
|
|
);
|