@bcmyguest/openmem 0.0.0-stage → 0.8.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/.agents/skills/openmem/SKILL.md +34 -0
- package/LICENSE +190 -0
- package/README.md +140 -2
- package/bin/hindsight-project-index +530 -0
- package/cli/coding-agent/access-policy.mjs +38 -0
- package/cli/coding-agent/agents.mjs +34 -0
- package/cli/coding-agent/bank-from-cwd.mjs +140 -0
- package/cli/coding-agent/capabilities.mjs +34 -0
- package/cli/coding-agent/context-store.mjs +81 -0
- package/cli/coding-agent/context-trim.mjs +897 -0
- package/cli/coding-agent/file-io.mjs +72 -0
- package/cli/coding-agent/hook-harnesses.mjs +88 -0
- package/cli/coding-agent/hook-runner.mjs +284 -0
- package/cli/coding-agent/install-access.mjs +119 -0
- package/cli/coding-agent/install-context-proxy.mjs +333 -0
- package/cli/coding-agent/install-rtk.mjs +60 -0
- package/cli/coding-agent/install-shim.mjs +201 -0
- package/cli/coding-agent/opencode-config.mjs +107 -0
- package/cli/coding-agent/opencode-plugin.mjs +132 -0
- package/cli/coding-agent/opencode2-plugin/index.js +299 -0
- package/cli/coding-agent/recall-frame.mjs +29 -0
- package/cli/coding-agent/remove-upstream-access.mjs +122 -0
- package/cli/coding-agent/runtime.mjs +40 -0
- package/cli/coding-agent/session-db.mjs +626 -0
- package/cli/coding-agent/session-facade.mjs +86 -0
- package/cli/coding-agent/toml-edit.mjs +161 -0
- package/cli/cognee-mcp.mjs +260 -0
- package/cli/context-management.mjs +254 -0
- package/cli/feed-sync.mjs +27 -0
- package/cli/install-coding-agent.mjs +298 -0
- package/cli/install-memory-mcp.mjs +426 -0
- package/cli/is-main.mjs +18 -0
- package/cli/mcp/context-sql-runner.mjs +58 -0
- package/cli/mcp/context-store.mjs +670 -0
- package/cli/mcp/context.mjs +61 -0
- package/cli/mcp/export/cognee-dataset-name.mjs +12 -0
- package/cli/mcp/export/cognee.mjs +119 -0
- package/cli/mcp/export/hindsight.mjs +48 -0
- package/cli/mcp/export/index.mjs +26 -0
- package/cli/mcp/export/mem0.mjs +31 -0
- package/cli/mcp/knowledge.mjs +333 -0
- package/cli/mcp/llm-synthesize.mjs +50 -0
- package/cli/mcp/mem0-scope-id.mjs +22 -0
- package/cli/mcp/memory-facade.mjs +521 -0
- package/cli/mcp/memory-reflect-consolidate.mjs +120 -0
- package/cli/mcp/notebooklm-banks.mjs +21 -0
- package/cli/mcp/notebooklm-hindsight-rest.mjs +45 -0
- package/cli/mcp/notebooklm-provider.mjs +225 -0
- package/cli/mcp/pmm-records.mjs +23 -0
- package/cli/mcp/portability.mjs +158 -0
- package/cli/mcp/stdio.mjs +65 -0
- package/cli/mcp/unified-mcp-install.mjs +65 -0
- package/cli/mem0-mcp.mjs +108 -0
- package/cli/notebooklm-mcp.mjs +236 -0
- package/cli/openmem-mcp.mjs +38 -0
- package/cli/openmem.mjs +222 -0
- package/cli/roots.mjs +85 -0
- package/cli/schedule.mjs +176 -0
- package/cli/stack.mjs +438 -0
- package/cli/switch-memory-provider.mjs +358 -0
- package/cli/wiring-report.mjs +125 -0
- package/compose.bench.yaml +12 -0
- package/compose.yaml +227 -0
- package/config/coding-agent.json +13 -0
- package/config/hindsight.env.example +26 -0
- package/config/index.yml +54 -0
- package/config/litellm.lemond.yaml +40 -0
- package/config/litellm.ollama.yaml +33 -0
- package/config/notebooklm.yml +33 -0
- package/config/openmem.toml.example +124 -0
- package/docs/OPENMEM.md +23 -0
- package/docs/architecture.md +245 -0
- package/docs/assets/README.md +10 -0
- package/docs/assets/setup-demo.gif +0 -0
- package/docs/coding-agent-hooks.md +75 -0
- package/docs/coding-agent-scopes.md +22 -0
- package/docs/coding-agents.md +71 -0
- package/docs/cognee.md +37 -0
- package/docs/compatibility.md +80 -0
- package/docs/concepts.md +135 -0
- package/docs/context-management.md +255 -0
- package/docs/context-proxy.md +115 -0
- package/docs/context-research.md +215 -0
- package/docs/context-store.md +277 -0
- package/docs/hindsight.md +77 -0
- package/docs/index-sources-roadmap.md +38 -0
- package/docs/index-sources.md +39 -0
- package/docs/indexer-sinks.md +122 -0
- package/docs/indexing.md +57 -0
- package/docs/manual-stack-setup.md +74 -0
- package/docs/mcp-install-guide.md +4 -0
- package/docs/mcp-setup.md +104 -0
- package/docs/mem0.md +141 -0
- package/docs/memory-portability.md +73 -0
- package/docs/memory-providers.md +246 -0
- package/docs/meridian-litellm.md +198 -0
- package/docs/model-configuration.md +106 -0
- package/docs/notebooklm.md +83 -0
- package/docs/npx-install.md +172 -0
- package/docs/openmem-config.md +194 -0
- package/docs/provider-wizard-hooks.md +222 -0
- package/docs/quickstart.md +107 -0
- package/docs/setup-wizard.md +117 -0
- package/docs/sources-feeds.md +39 -0
- package/docs/sources-readwise.md +23 -0
- package/docs/sources-zotero.md +22 -0
- package/glama.json +4 -0
- package/indexer/Dockerfile +38 -0
- package/indexer/code_outline.py +146 -0
- package/indexer/feed_sync.py +107 -0
- package/indexer/index_config.py +290 -0
- package/indexer/pytest.ini +5 -0
- package/indexer/sinks/__init__.py +148 -0
- package/indexer/sinks/_http.py +44 -0
- package/indexer/sinks/cognee.py +329 -0
- package/indexer/sinks/hindsight.py +107 -0
- package/indexer/sinks/mem0.py +247 -0
- package/indexer/watch.py +240 -0
- package/package.json +96 -4
- package/providers/_template/compose.yaml +52 -0
- package/providers/cognee/OPERATOR.md +82 -0
- package/providers/cognee/compose.yaml +89 -0
- package/providers/cognee/lib/clack.mjs +157 -0
- package/providers/cognee/lib/coding-agent.mjs +26 -0
- package/providers/cognee/lib/cognee-mcp-install.mjs +153 -0
- package/providers/cognee/lib/config.mjs +98 -0
- package/providers/cognee/lib/env-file.mjs +194 -0
- package/providers/cognee/lib/env-setup.mjs +83 -0
- package/providers/cognee/lib/llm-env.mjs +382 -0
- package/providers/cognee/lib/ports.mjs +25 -0
- package/providers/cognee/lib/smoke.mjs +246 -0
- package/providers/cognee/lib/stack-env.mjs +134 -0
- package/providers/cognee/lib/state.mjs +101 -0
- package/providers/cognee/lib/summary.mjs +56 -0
- package/providers/cognee/lib/validate.mjs +97 -0
- package/providers/cognee/manifest.json +56 -0
- package/providers/cognee/wizard.mjs +134 -0
- package/providers/graphiti/manifest.json +15 -0
- package/providers/hindsight/lib/clack.mjs +347 -0
- package/providers/hindsight/lib/coding-agent.mjs +204 -0
- package/providers/hindsight/lib/config.mjs +79 -0
- package/providers/hindsight/lib/env-setup.mjs +72 -0
- package/providers/hindsight/lib/litellm-routes.mjs +196 -0
- package/providers/hindsight/lib/meridian.mjs +307 -0
- package/providers/hindsight/lib/model-backend.mjs +243 -0
- package/providers/hindsight/lib/notebooklm-mcp-install.mjs +127 -0
- package/providers/hindsight/lib/ollama-compose.mjs +51 -0
- package/providers/hindsight/lib/ports.mjs +30 -0
- package/providers/hindsight/lib/probe.mjs +54 -0
- package/providers/hindsight/lib/stack-env.mjs +125 -0
- package/providers/hindsight/lib/state.mjs +80 -0
- package/providers/hindsight/lib/summary.mjs +57 -0
- package/providers/hindsight/lib/validate.mjs +81 -0
- package/providers/hindsight/manifest.json +58 -0
- package/providers/hindsight/wizard.mjs +150 -0
- package/providers/letta/manifest.json +29 -0
- package/providers/mem0/OPERATOR.md +143 -0
- package/providers/mem0/compose.yaml +79 -0
- package/providers/mem0/lib/coding-agent.mjs +58 -0
- package/providers/mem0/lib/config.mjs +45 -0
- package/providers/mem0/lib/configure.mjs +201 -0
- package/providers/mem0/lib/dotenv.mjs +71 -0
- package/providers/mem0/lib/env-file.mjs +110 -0
- package/providers/mem0/lib/http.mjs +69 -0
- package/providers/mem0/lib/litellm.mjs +457 -0
- package/providers/mem0/lib/mcp-install.mjs +165 -0
- package/providers/mem0/lib/secret.mjs +30 -0
- package/providers/mem0/lib/smoke.mjs +179 -0
- package/providers/mem0/lib/stack-env.mjs +255 -0
- package/providers/mem0/lib/state.mjs +67 -0
- package/providers/mem0/lib/summary.mjs +88 -0
- package/providers/mem0/lib/url.mjs +106 -0
- package/providers/mem0/lib/validate.mjs +158 -0
- package/providers/mem0/manifest.json +47 -0
- package/providers/mem0/wizard.mjs +146 -0
- package/providers/zep/manifest.json +29 -0
- package/proxy/config.yaml +33 -0
- package/proxy/openmem_context/__init__.py +6 -0
- package/proxy/openmem_context/adapters.py +504 -0
- package/proxy/openmem_context/compress.py +53 -0
- package/proxy/openmem_context/core.py +606 -0
- package/proxy/openmem_context/embed.py +212 -0
- package/proxy/openmem_context/recall.py +49 -0
- package/proxy/openmem_context/session.py +70 -0
- package/proxy/openmem_context/sessiondb.py +276 -0
- package/proxy/openmem_context/settings.py +134 -0
- package/proxy/openmem_context/store.py +129 -0
- package/proxy/openmem_context/tokens.py +123 -0
- package/proxy/openmem_context/ttl.py +78 -0
- package/proxy/openmem_context_hook.py +34 -0
- package/scripts/check-pack.mjs +219 -0
- package/scripts/gen-masks.mjs +83 -0
- package/scripts/gen-masks.sh +4 -0
- package/scripts/promotion-provider-smoke.mjs +34 -0
- package/scripts/run-setup.sh +30 -0
- package/scripts/setup.mjs +107 -0
- package/scripts/setup.sh +5 -0
- package/scripts/spin-up.sh +45 -0
- package/scripts/wizard.sh +5 -0
- package/server.json +34 -0
- package/sync/Dockerfile +27 -0
- package/sync/memory_backend.py +87 -0
- package/sync/notebooklm_sync.py +664 -0
- package/wizard/bin/pm.mjs +97 -0
- package/wizard/bin/setup.mjs +33 -0
- package/wizard/lib/compose-version.mjs +32 -0
- package/wizard/lib/compose.mjs +70 -0
- package/wizard/lib/context-step.mjs +140 -0
- package/wizard/lib/curl.mjs +103 -0
- package/wizard/lib/docker-build.mjs +148 -0
- package/wizard/lib/exec.mjs +41 -0
- package/wizard/lib/indexer.mjs +6 -0
- package/wizard/lib/litellm-env.mjs +55 -0
- package/wizard/lib/load-machine-env.mjs +4 -0
- package/wizard/lib/log.mjs +32 -0
- package/wizard/lib/notebooklm-agent-access.mjs +92 -0
- package/wizard/lib/notebooklm.mjs +214 -0
- package/wizard/lib/parse-args.mjs +87 -0
- package/wizard/lib/paths.mjs +39 -0
- package/wizard/lib/pm-config.mjs +371 -0
- package/wizard/lib/ports.mjs +94 -0
- package/wizard/lib/prompts.mjs +85 -0
- package/wizard/lib/provider-wizard/helpers.mjs +98 -0
- package/wizard/lib/provider-wizard/run-steps.mjs +129 -0
- package/wizard/lib/providers/registry.mjs +262 -0
- package/wizard/lib/run-clack.mjs +108 -0
- package/wizard/lib/run-wizard.mjs +750 -0
- package/wizard/lib/validate-setup.mjs +62 -0
- package/wizard/lib/wizard-action.mjs +34 -0
- package/wizard/lib/wizard-conf.mjs +71 -0
package/cli/schedule.mjs
ADDED
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/** Install a per-user, hourly feed-sync job using the host scheduler. */
|
|
3
|
+
import { spawnSync } from "node:child_process";
|
|
4
|
+
import fs from "node:fs";
|
|
5
|
+
import os from "node:os";
|
|
6
|
+
import path from "node:path";
|
|
7
|
+
import { parseArgs } from "node:util";
|
|
8
|
+
import { fileURLToPath } from "node:url";
|
|
9
|
+
import { pythonCandidates } from "./feed-sync.mjs";
|
|
10
|
+
import { isMainModule } from "./is-main.mjs";
|
|
11
|
+
|
|
12
|
+
const PACKAGE_ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
|
|
13
|
+
const PACKAGE_NAME = JSON.parse(fs.readFileSync(path.join(PACKAGE_ROOT, "package.json"), "utf8")).name;
|
|
14
|
+
const JOB_NAME = "openmem-feed-sync";
|
|
15
|
+
const LAUNCHD_LABEL = "io.github.bcmyguest.openmem.feed-sync";
|
|
16
|
+
const WINDOWS_TASK = "OpenMemFeedSync";
|
|
17
|
+
|
|
18
|
+
function escapeXml(value) {
|
|
19
|
+
return value.replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">").replaceAll('"', """);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function systemdArg(value) {
|
|
23
|
+
const escaped = value.replaceAll("\\", "\\\\").replaceAll('"', '\\"').replaceAll("%", "%%");
|
|
24
|
+
return `"${escaped}"`;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function schedulePaths(platform, home, env = process.env) {
|
|
28
|
+
if (platform === "linux") {
|
|
29
|
+
const config = path.isAbsolute(env.XDG_CONFIG_HOME || "") ? env.XDG_CONFIG_HOME : path.join(home, ".config");
|
|
30
|
+
return {
|
|
31
|
+
runner: path.join(config, "openmem", "feed-sync-runner.mjs"),
|
|
32
|
+
service: path.join(config, "systemd", "user", `${JOB_NAME}.service`),
|
|
33
|
+
timer: path.join(config, "systemd", "user", `${JOB_NAME}.timer`),
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
if (platform === "darwin") {
|
|
37
|
+
return {
|
|
38
|
+
runner: path.join(home, "Library", "Application Support", "OpenMem", "feed-sync-runner.mjs"),
|
|
39
|
+
plist: path.join(home, "Library", "LaunchAgents", `${LAUNCHD_LABEL}.plist`),
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
if (platform === "win32") {
|
|
43
|
+
const local = path.win32.isAbsolute(env.LOCALAPPDATA || "") ? env.LOCALAPPDATA : path.win32.join(home, "AppData", "Local");
|
|
44
|
+
return { runner: path.win32.join(local, "OpenMem", "feed-sync-runner.mjs") };
|
|
45
|
+
}
|
|
46
|
+
throw new Error(`native scheduling is unavailable on ${platform}; run ${PACKAGE_NAME} feed-sync with your system scheduler`);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function renderRunner(nodePath, npmCliPath, pythonPath) {
|
|
50
|
+
return `// Generated by OpenMem schedule install. Reinstall after changing Node.js installations.\n` +
|
|
51
|
+
`import { spawnSync } from "node:child_process";\n` +
|
|
52
|
+
`import path from "node:path";\n` +
|
|
53
|
+
`const node = ${JSON.stringify(nodePath)};\n` +
|
|
54
|
+
`const npm = ${JSON.stringify(npmCliPath)};\n` +
|
|
55
|
+
`const python = ${JSON.stringify(pythonPath)};\n` +
|
|
56
|
+
`const env = { ...process.env };\n` +
|
|
57
|
+
`const pathKey = Object.keys(env).find((key) => key.toLowerCase() === "path") || "PATH";\n` +
|
|
58
|
+
`env[pathKey] = [path.dirname(node), path.dirname(python), env[pathKey] || ""].join(path.delimiter);\n` +
|
|
59
|
+
`env.PYTHON = python;\n` +
|
|
60
|
+
`const result = spawnSync(node, [npm, "exec", "--yes", "--package=${PACKAGE_NAME}", "--", "openmem", "feed-sync"], { env, stdio: "inherit" });\n` +
|
|
61
|
+
`if (result.error) console.error(result.error.message);\n` +
|
|
62
|
+
`process.exit(result.status ?? 1);\n`;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export function renderSystemdUnits(nodePath, runnerPath) {
|
|
66
|
+
return {
|
|
67
|
+
service: `[Unit]\nDescription=Refresh OpenMem feed cache\n\n[Service]\nType=oneshot\nExecStart=${systemdArg(nodePath)} ${systemdArg(runnerPath)}\n`,
|
|
68
|
+
timer: `[Unit]\nDescription=Refresh OpenMem feed cache hourly\n\n[Timer]\nOnCalendar=hourly\nPersistent=true\n\n[Install]\nWantedBy=timers.target\n`,
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export function renderLaunchAgent(nodePath, runnerPath) {
|
|
73
|
+
return `<?xml version="1.0" encoding="UTF-8"?>\n` +
|
|
74
|
+
`<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">\n` +
|
|
75
|
+
`<plist version="1.0"><dict>\n` +
|
|
76
|
+
` <key>Label</key><string>${LAUNCHD_LABEL}</string>\n` +
|
|
77
|
+
` <key>ProgramArguments</key><array><string>${escapeXml(nodePath)}</string><string>${escapeXml(runnerPath)}</string></array>\n` +
|
|
78
|
+
` <key>StartInterval</key><integer>3600</integer>\n` +
|
|
79
|
+
`</dict></plist>\n`;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export function windowsTaskArgs(nodePath, runnerPath) {
|
|
83
|
+
return ["/create", "/sc", "hourly", "/mo", "1", "/tn", WINDOWS_TASK, "/tr", `"${nodePath}" "${runnerPath}"`, "/f"];
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function run(command, args, env, allowMissing = false) {
|
|
87
|
+
const result = spawnSync(command, args, { encoding: "utf8", env });
|
|
88
|
+
if (result.error) throw result.error;
|
|
89
|
+
if (result.status !== 0 && !allowMissing) throw new Error(`${command} ${args.join(" ")}: ${(result.stderr || result.stdout).trim()}`);
|
|
90
|
+
return result.status === 0;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function writeFile(file, content) {
|
|
94
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
95
|
+
fs.writeFileSync(file, content, { mode: 0o600 });
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function resolvePython(platform, env) {
|
|
99
|
+
for (const [command, prefix] of pythonCandidates(platform, env)) {
|
|
100
|
+
const result = spawnSync(command, [...prefix, "-c", "import sys, yaml; print(sys.executable)"], { encoding: "utf8", env });
|
|
101
|
+
if (result.status === 0) return result.stdout.trim();
|
|
102
|
+
}
|
|
103
|
+
throw new Error("Python 3 with PyYAML is required for feed sync; install PyYAML or set PYTHON to a working interpreter");
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function install(platform, home, env, dryRun) {
|
|
107
|
+
const files = schedulePaths(platform, home, env);
|
|
108
|
+
const npmCli = env.npm_execpath;
|
|
109
|
+
if (!npmCli || !fs.existsSync(npmCli)) throw new Error("run schedule install through npx so OpenMem can locate npm");
|
|
110
|
+
const nodePath = process.execPath;
|
|
111
|
+
if (dryRun) {
|
|
112
|
+
console.log(`Would write ${files.runner} and install an hourly ${JOB_NAME} job using ${nodePath} and ${npmCli}`);
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
const runner = renderRunner(nodePath, npmCli, resolvePython(platform, env));
|
|
116
|
+
writeFile(files.runner, runner);
|
|
117
|
+
if (platform === "linux") {
|
|
118
|
+
const units = renderSystemdUnits(nodePath, files.runner);
|
|
119
|
+
writeFile(files.service, units.service);
|
|
120
|
+
writeFile(files.timer, units.timer);
|
|
121
|
+
run("systemctl", ["--user", "daemon-reload"], env);
|
|
122
|
+
run("systemctl", ["--user", "enable", "--now", `${JOB_NAME}.timer`], env);
|
|
123
|
+
} else if (platform === "darwin") {
|
|
124
|
+
const target = `gui/${process.getuid()}`;
|
|
125
|
+
if (fs.existsSync(files.plist)) run("launchctl", ["bootout", target, files.plist], env, true);
|
|
126
|
+
writeFile(files.plist, renderLaunchAgent(nodePath, files.runner));
|
|
127
|
+
run("launchctl", ["bootstrap", target, files.plist], env);
|
|
128
|
+
} else {
|
|
129
|
+
run("schtasks", windowsTaskArgs(nodePath, files.runner), env);
|
|
130
|
+
}
|
|
131
|
+
console.log(`Installed hourly ${JOB_NAME} job`);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function remove(platform, home, env, dryRun) {
|
|
135
|
+
const files = schedulePaths(platform, home, env);
|
|
136
|
+
if (dryRun) {
|
|
137
|
+
console.log(`Would remove ${JOB_NAME} job and ${files.runner}`);
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
140
|
+
if (platform === "linux") {
|
|
141
|
+
run("systemctl", ["--user", "disable", "--now", `${JOB_NAME}.timer`], env, true);
|
|
142
|
+
fs.rmSync(files.service, { force: true });
|
|
143
|
+
fs.rmSync(files.timer, { force: true });
|
|
144
|
+
run("systemctl", ["--user", "daemon-reload"], env);
|
|
145
|
+
} else if (platform === "darwin") {
|
|
146
|
+
if (fs.existsSync(files.plist)) run("launchctl", ["bootout", `gui/${process.getuid()}`, files.plist], env, true);
|
|
147
|
+
fs.rmSync(files.plist, { force: true });
|
|
148
|
+
} else {
|
|
149
|
+
if (run("schtasks", ["/query", "/tn", WINDOWS_TASK], env, true)) {
|
|
150
|
+
run("schtasks", ["/delete", "/tn", WINDOWS_TASK, "/f"], env);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
fs.rmSync(files.runner, { force: true });
|
|
154
|
+
console.log(`Removed ${JOB_NAME} job`);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
export function main(argv = process.argv.slice(2), platform = process.platform, home = os.homedir(), env = process.env) {
|
|
158
|
+
const { values, positionals } = parseArgs({ args: argv, options: { "dry-run": { type: "boolean" }, help: { type: "boolean" } }, allowPositionals: true });
|
|
159
|
+
if (values.help || positionals.length === 0) {
|
|
160
|
+
console.log(`Usage: npx ${PACKAGE_NAME} schedule <install|remove> [--dry-run]\nInstalls an hourly per-user feed sync with the native OS scheduler.`);
|
|
161
|
+
return;
|
|
162
|
+
}
|
|
163
|
+
if (positionals.length !== 1) throw new Error("schedule accepts one action: install or remove");
|
|
164
|
+
if (positionals[0] === "install") install(platform, home, env, values["dry-run"]);
|
|
165
|
+
else if (positionals[0] === "remove") remove(platform, home, env, values["dry-run"]);
|
|
166
|
+
else throw new Error(`unknown schedule action: ${positionals[0]}`);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
if (isMainModule(import.meta.url)) {
|
|
170
|
+
try {
|
|
171
|
+
main();
|
|
172
|
+
} catch (error) {
|
|
173
|
+
console.error(`openmem schedule: ${error.message}`);
|
|
174
|
+
process.exitCode = 1;
|
|
175
|
+
}
|
|
176
|
+
}
|
package/cli/stack.mjs
ADDED
|
@@ -0,0 +1,438 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Stack root: the stable, writable directory the wizard and docker compose run from.
|
|
3
|
+
*
|
|
4
|
+
* Under npx the package directory is a read-only, garbage-collected cache, but the wizard
|
|
5
|
+
* writes .env, .build-hash and compose.override.yaml and uses compose files and the docker
|
|
6
|
+
* build context relative to its repo root. The mutable assets (compose files, indexer/,
|
|
7
|
+
* bin/, sync/, config templates, ...) are therefore copied from the package into the stack
|
|
8
|
+
* root. A manifest of the shipped-file hashes is kept in the stack root so an upgrade
|
|
9
|
+
* overwrites only shipped files the user did not modify, and never touches anything else.
|
|
10
|
+
*
|
|
11
|
+
* Everything here uses node builtins only.
|
|
12
|
+
*/
|
|
13
|
+
import crypto from "node:crypto";
|
|
14
|
+
import fs from "node:fs";
|
|
15
|
+
import os from "node:os";
|
|
16
|
+
import path from "node:path";
|
|
17
|
+
|
|
18
|
+
export const MANIFEST_NAME = ".pm-stack-manifest.json";
|
|
19
|
+
export const MANIFEST_SCHEMA = 1;
|
|
20
|
+
|
|
21
|
+
/** Path segments that are never shipped into the stack. */
|
|
22
|
+
const SKIP_SEGMENTS = new Set([
|
|
23
|
+
"__pycache__",
|
|
24
|
+
"tests",
|
|
25
|
+
"node_modules",
|
|
26
|
+
".pytest_cache",
|
|
27
|
+
".codegraph",
|
|
28
|
+
".git",
|
|
29
|
+
]);
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Which package files are materialized into the stack root. Paths are posix-style and
|
|
33
|
+
* relative to the package root. Keep package.json "files" a superset of this list
|
|
34
|
+
* (test_cli_package.mjs and scripts/check-pack.mjs enforce it).
|
|
35
|
+
*
|
|
36
|
+
* @typedef {{ files: string[] } | { tree: string, include?: RegExp }} AssetRule
|
|
37
|
+
* @type {AssetRule[]}
|
|
38
|
+
*/
|
|
39
|
+
export const STACK_ASSETS = [
|
|
40
|
+
{ files: ["compose.yaml", "compose.bench.yaml"] },
|
|
41
|
+
// Docker build contexts: the Dockerfiles COPY from indexer/, bin/, sync/ and config/.
|
|
42
|
+
{ tree: "indexer" },
|
|
43
|
+
{ tree: "bin" },
|
|
44
|
+
{ tree: "sync" },
|
|
45
|
+
{
|
|
46
|
+
tree: "config",
|
|
47
|
+
include:
|
|
48
|
+
/^config\/(?:[^/]+\.example|index\.yml|notebooklm\.yml|litellm\.[^/]+\.yaml|coding-agent\.json)$/,
|
|
49
|
+
},
|
|
50
|
+
// context-proxy mounts ./proxy (LiteLLM config and hook).
|
|
51
|
+
{ tree: "proxy" },
|
|
52
|
+
// Per-provider compose fragments (hooks and manifests stay in the read-only package).
|
|
53
|
+
{ tree: "providers", include: /^providers\/[^/]+\/compose[^/]*\.ya?ml$/ },
|
|
54
|
+
// Deliberately absent: scripts/ and wizard/. They are code, so they run from the
|
|
55
|
+
// read-only package (PM_PACKAGE_ROOT) and write into the stack (PM_STACK_ROOT).
|
|
56
|
+
];
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* @param {string} p
|
|
60
|
+
* @returns {string}
|
|
61
|
+
*/
|
|
62
|
+
function toPosix(p) {
|
|
63
|
+
return p.split(path.sep).join("/");
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* @param {string} root
|
|
68
|
+
* @param {string} rel posix relative dir
|
|
69
|
+
* @param {string[]} out
|
|
70
|
+
*/
|
|
71
|
+
function walk(root, rel, out) {
|
|
72
|
+
const abs = path.join(root, rel);
|
|
73
|
+
let entries;
|
|
74
|
+
try {
|
|
75
|
+
entries = fs.readdirSync(abs, { withFileTypes: true });
|
|
76
|
+
} catch {
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
for (const e of entries.sort((a, b) => (a.name < b.name ? -1 : 1))) {
|
|
80
|
+
if (SKIP_SEGMENTS.has(e.name)) continue;
|
|
81
|
+
const childRel = rel ? `${rel}/${e.name}` : e.name;
|
|
82
|
+
if (e.isDirectory()) walk(root, childRel, out);
|
|
83
|
+
else if (e.isFile() && !e.name.endsWith(".pyc")) out.push(childRel);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Sorted posix paths (relative to pkgRoot) of every file the stack should contain.
|
|
89
|
+
* Symlinks are never shipped (npm drops them too).
|
|
90
|
+
*
|
|
91
|
+
* @param {string} pkgRoot
|
|
92
|
+
* @param {AssetRule[]} [rules]
|
|
93
|
+
* @returns {string[]}
|
|
94
|
+
*/
|
|
95
|
+
export function listShippedFiles(pkgRoot, rules = STACK_ASSETS) {
|
|
96
|
+
const out = new Set();
|
|
97
|
+
for (const rule of rules) {
|
|
98
|
+
if ("files" in rule) {
|
|
99
|
+
for (const rel of rule.files) {
|
|
100
|
+
try {
|
|
101
|
+
if (fs.lstatSync(path.join(pkgRoot, rel)).isFile()) out.add(rel);
|
|
102
|
+
} catch {
|
|
103
|
+
// Optional asset absent from this checkout.
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
} else {
|
|
107
|
+
const found = [];
|
|
108
|
+
walk(pkgRoot, rule.tree, found);
|
|
109
|
+
for (const rel of found) {
|
|
110
|
+
if (!rule.include || rule.include.test(rel)) out.add(rel);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
return [...out].sort();
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* @param {Buffer|string} data
|
|
119
|
+
* @returns {string}
|
|
120
|
+
*/
|
|
121
|
+
export function hashBytes(data) {
|
|
122
|
+
return crypto.createHash("sha256").update(data).digest("hex");
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Is the package directory a git checkout (or forced to behave like one)?
|
|
127
|
+
*
|
|
128
|
+
* @param {string} pkgRoot
|
|
129
|
+
* @param {Record<string, string|undefined>} [env]
|
|
130
|
+
*/
|
|
131
|
+
export function isCheckout(pkgRoot, env = process.env) {
|
|
132
|
+
return env.PM_USE_CHECKOUT === "1" || fs.existsSync(path.join(pkgRoot, ".git"));
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Pick the stack root.
|
|
137
|
+
*
|
|
138
|
+
* Order: PM_USE_CHECKOUT=1 (the package root itself), PM_STACK_ROOT, a git checkout (the
|
|
139
|
+
* package root itself), then ${XDG_DATA_HOME:-~/.local/share}/personal-memory/stack.
|
|
140
|
+
* `copy` is false when the stack root is the package root: nothing is materialized.
|
|
141
|
+
*
|
|
142
|
+
* @param {{ pkgRoot: string, env?: Record<string, string|undefined>, home?: string }} opts
|
|
143
|
+
* @returns {{ root: string, mode: "checkout"|"stack", source: string, copy: boolean }}
|
|
144
|
+
*/
|
|
145
|
+
export function resolveStackRoot({ pkgRoot, env = process.env, home }) {
|
|
146
|
+
const homeDir = home || env.HOME || os.homedir();
|
|
147
|
+
const pkg = path.resolve(pkgRoot);
|
|
148
|
+
if (env.PM_USE_CHECKOUT === "1") {
|
|
149
|
+
return { root: pkg, mode: "checkout", source: "PM_USE_CHECKOUT", copy: false };
|
|
150
|
+
}
|
|
151
|
+
if (env.PM_STACK_ROOT) {
|
|
152
|
+
let p = env.PM_STACK_ROOT;
|
|
153
|
+
if (p === "~") p = homeDir;
|
|
154
|
+
else if (p.startsWith("~/")) p = path.join(homeDir, p.slice(2));
|
|
155
|
+
const root = path.resolve(p);
|
|
156
|
+
const same = root === pkg;
|
|
157
|
+
return {
|
|
158
|
+
root,
|
|
159
|
+
mode: same ? "checkout" : "stack",
|
|
160
|
+
source: "PM_STACK_ROOT",
|
|
161
|
+
copy: !same,
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
if (fs.existsSync(path.join(pkg, ".git"))) {
|
|
165
|
+
return { root: pkg, mode: "checkout", source: "git checkout", copy: false };
|
|
166
|
+
}
|
|
167
|
+
const xdg = env.XDG_DATA_HOME && path.isAbsolute(env.XDG_DATA_HOME);
|
|
168
|
+
const dataHome = xdg ? /** @type {string} */ (env.XDG_DATA_HOME) : path.join(homeDir, ".local/share");
|
|
169
|
+
return {
|
|
170
|
+
root: path.join(dataHome, "personal-memory", "stack"),
|
|
171
|
+
mode: "stack",
|
|
172
|
+
source: xdg ? "XDG_DATA_HOME" : "default",
|
|
173
|
+
copy: true,
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* @typedef {"create"|"restore"|"unchanged"|"update"|"conflict"|"conflict-untracked"} SyncAction
|
|
179
|
+
*/
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* Pure decision for one shipped file.
|
|
183
|
+
*
|
|
184
|
+
* @param {{
|
|
185
|
+
* shippedHash: string,
|
|
186
|
+
* disk: null | "symlink" | "not-file" | string, // null = missing, else sha256
|
|
187
|
+
* recordedHash?: string, // hash written by a previous sync
|
|
188
|
+
* }} s
|
|
189
|
+
* @returns {SyncAction}
|
|
190
|
+
*/
|
|
191
|
+
export function planEntry({ shippedHash, disk, recordedHash }) {
|
|
192
|
+
if (disk === null) return recordedHash ? "restore" : "create";
|
|
193
|
+
if (disk === "symlink" || disk === "not-file") return "conflict-untracked";
|
|
194
|
+
if (disk === shippedHash) return "unchanged";
|
|
195
|
+
if (recordedHash === undefined) return "conflict-untracked";
|
|
196
|
+
if (disk === recordedHash) return "update";
|
|
197
|
+
return "conflict";
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* @param {string} dest
|
|
202
|
+
* @param {Buffer} data
|
|
203
|
+
* @param {number} mode
|
|
204
|
+
*/
|
|
205
|
+
export function writeAtomic(dest, data, mode) {
|
|
206
|
+
fs.mkdirSync(path.dirname(dest), { recursive: true });
|
|
207
|
+
const tmp = `${dest}.tmp-${process.pid}-${crypto.randomBytes(4).toString("hex")}`;
|
|
208
|
+
try {
|
|
209
|
+
fs.writeFileSync(tmp, data, { mode });
|
|
210
|
+
fs.chmodSync(tmp, mode);
|
|
211
|
+
fs.renameSync(tmp, dest);
|
|
212
|
+
} catch (e) {
|
|
213
|
+
try {
|
|
214
|
+
fs.unlinkSync(tmp);
|
|
215
|
+
} catch {
|
|
216
|
+
// Nothing to clean up.
|
|
217
|
+
}
|
|
218
|
+
throw e;
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/**
|
|
223
|
+
* @param {string} stackRoot
|
|
224
|
+
* @returns {{ schema: number, name?: string, version?: string, files: Record<string, string> }}
|
|
225
|
+
*/
|
|
226
|
+
export function readManifest(stackRoot) {
|
|
227
|
+
const file = path.join(stackRoot, MANIFEST_NAME);
|
|
228
|
+
let raw;
|
|
229
|
+
try {
|
|
230
|
+
raw = fs.readFileSync(file, "utf8");
|
|
231
|
+
} catch {
|
|
232
|
+
return { schema: MANIFEST_SCHEMA, files: {} };
|
|
233
|
+
}
|
|
234
|
+
let m;
|
|
235
|
+
try {
|
|
236
|
+
m = JSON.parse(raw);
|
|
237
|
+
} catch {
|
|
238
|
+
throw new Error(
|
|
239
|
+
`${file} is not valid JSON; fix or delete it (shipped files will then be treated as untracked)`,
|
|
240
|
+
);
|
|
241
|
+
}
|
|
242
|
+
if (!m || typeof m !== "object" || typeof m.files !== "object" || m.files === null) {
|
|
243
|
+
throw new Error(`${file} has an unexpected shape; fix or delete it`);
|
|
244
|
+
}
|
|
245
|
+
if (typeof m.schema === "number" && m.schema > MANIFEST_SCHEMA) {
|
|
246
|
+
throw new Error(
|
|
247
|
+
`${file} was written by a newer openmem (schema ${m.schema}); upgrade the CLI`,
|
|
248
|
+
);
|
|
249
|
+
}
|
|
250
|
+
return m;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
/**
|
|
254
|
+
* @param {string} abs
|
|
255
|
+
* @returns {null | "symlink" | "not-file" | { hash: string, data: Buffer }}
|
|
256
|
+
*/
|
|
257
|
+
function inspectDisk(abs) {
|
|
258
|
+
let st;
|
|
259
|
+
try {
|
|
260
|
+
st = fs.lstatSync(abs);
|
|
261
|
+
} catch (e) {
|
|
262
|
+
if (/** @type {NodeJS.ErrnoException} */ (e).code === "ENOENT") return null;
|
|
263
|
+
throw e;
|
|
264
|
+
}
|
|
265
|
+
if (st.isSymbolicLink()) return "symlink";
|
|
266
|
+
if (!st.isFile()) return "not-file";
|
|
267
|
+
const data = fs.readFileSync(abs);
|
|
268
|
+
return { hash: hashBytes(data), data };
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
/**
|
|
272
|
+
* @typedef {object} SyncReport
|
|
273
|
+
* @property {string} stackRoot
|
|
274
|
+
* @property {string} version
|
|
275
|
+
* @property {string[]} created
|
|
276
|
+
* @property {string[]} restored
|
|
277
|
+
* @property {string[]} updated
|
|
278
|
+
* @property {number} unchanged
|
|
279
|
+
* @property {{ path: string, reason: string }[]} conflicts kept as-is; shipped version differs
|
|
280
|
+
* @property {string[]} forced overwritten under force (user copy saved next to it)
|
|
281
|
+
* @property {string[]} orphans recorded earlier, no longer shipped; left in place
|
|
282
|
+
* @property {boolean} dryRun
|
|
283
|
+
*/
|
|
284
|
+
|
|
285
|
+
/**
|
|
286
|
+
* Copy the shipped files from pkgRoot into stackRoot without clobbering user changes.
|
|
287
|
+
* Never deletes anything and never touches files that are not in the shipped set.
|
|
288
|
+
*
|
|
289
|
+
* @param {{
|
|
290
|
+
* pkgRoot: string,
|
|
291
|
+
* stackRoot: string,
|
|
292
|
+
* name?: string,
|
|
293
|
+
* version?: string,
|
|
294
|
+
* force?: boolean,
|
|
295
|
+
* dryRun?: boolean,
|
|
296
|
+
* rules?: AssetRule[],
|
|
297
|
+
* }} opts
|
|
298
|
+
* @returns {SyncReport}
|
|
299
|
+
*/
|
|
300
|
+
export function syncStack({
|
|
301
|
+
pkgRoot,
|
|
302
|
+
stackRoot,
|
|
303
|
+
name = "openmem",
|
|
304
|
+
version = "0.0.0",
|
|
305
|
+
force = false,
|
|
306
|
+
dryRun = false,
|
|
307
|
+
rules = STACK_ASSETS,
|
|
308
|
+
}) {
|
|
309
|
+
const pkg = path.resolve(pkgRoot);
|
|
310
|
+
const stack = path.resolve(stackRoot);
|
|
311
|
+
if (pkg === stack) throw new Error("stack root equals the package root; nothing to materialize");
|
|
312
|
+
|
|
313
|
+
if (!dryRun) fs.mkdirSync(stack, { recursive: true });
|
|
314
|
+
const manifest = readManifest(stack);
|
|
315
|
+
const recorded = manifest.files;
|
|
316
|
+
|
|
317
|
+
/** @type {SyncReport} */
|
|
318
|
+
const report = {
|
|
319
|
+
stackRoot: stack,
|
|
320
|
+
version,
|
|
321
|
+
created: [],
|
|
322
|
+
restored: [],
|
|
323
|
+
updated: [],
|
|
324
|
+
unchanged: 0,
|
|
325
|
+
conflicts: [],
|
|
326
|
+
forced: [],
|
|
327
|
+
orphans: [],
|
|
328
|
+
dryRun,
|
|
329
|
+
};
|
|
330
|
+
|
|
331
|
+
const shipped = listShippedFiles(pkg, rules);
|
|
332
|
+
/** @type {Record<string, string>} */
|
|
333
|
+
const nextFiles = {};
|
|
334
|
+
let manifestDirty = manifest.version !== version || manifest.name !== name;
|
|
335
|
+
|
|
336
|
+
for (const rel of shipped) {
|
|
337
|
+
const src = path.join(pkg, rel);
|
|
338
|
+
const data = fs.readFileSync(src);
|
|
339
|
+
const shippedHash = hashBytes(data);
|
|
340
|
+
const mode = fs.statSync(src).mode & 0o111 ? 0o755 : 0o644;
|
|
341
|
+
const dest = path.join(stack, ...rel.split("/"));
|
|
342
|
+
const diskInfo = inspectDisk(dest);
|
|
343
|
+
const disk = diskInfo === null || typeof diskInfo === "string" ? diskInfo : diskInfo.hash;
|
|
344
|
+
const action = planEntry({ shippedHash, disk, recordedHash: recorded[rel] });
|
|
345
|
+
|
|
346
|
+
switch (action) {
|
|
347
|
+
case "create":
|
|
348
|
+
case "restore":
|
|
349
|
+
case "update":
|
|
350
|
+
if (!dryRun) writeAtomic(dest, data, mode);
|
|
351
|
+
(action === "create" ? report.created : action === "restore" ? report.restored : report.updated).push(rel);
|
|
352
|
+
nextFiles[rel] = shippedHash;
|
|
353
|
+
break;
|
|
354
|
+
case "unchanged":
|
|
355
|
+
report.unchanged++;
|
|
356
|
+
// Adopt a byte-identical pre-existing file so later upgrades can update it.
|
|
357
|
+
nextFiles[rel] = shippedHash;
|
|
358
|
+
break;
|
|
359
|
+
case "conflict":
|
|
360
|
+
case "conflict-untracked": {
|
|
361
|
+
const reason =
|
|
362
|
+
disk === "symlink"
|
|
363
|
+
? "destination is a symlink"
|
|
364
|
+
: disk === "not-file"
|
|
365
|
+
? "destination is not a regular file"
|
|
366
|
+
: action === "conflict"
|
|
367
|
+
? "modified locally; the new shipped version was not applied"
|
|
368
|
+
: "exists but is not tracked as shipped; left untouched";
|
|
369
|
+
if (force && typeof diskInfo === "object" && diskInfo !== null) {
|
|
370
|
+
if (!dryRun) {
|
|
371
|
+
const backup = `${dest}.user-${Date.now()}.bak`;
|
|
372
|
+
writeAtomic(backup, diskInfo.data, 0o600);
|
|
373
|
+
writeAtomic(dest, data, mode);
|
|
374
|
+
}
|
|
375
|
+
report.forced.push(rel);
|
|
376
|
+
nextFiles[rel] = shippedHash;
|
|
377
|
+
} else {
|
|
378
|
+
report.conflicts.push({ path: rel, reason });
|
|
379
|
+
// Keep the old record so the file stays "modified" next time instead of "untracked".
|
|
380
|
+
if (recorded[rel] !== undefined) nextFiles[rel] = recorded[rel];
|
|
381
|
+
}
|
|
382
|
+
break;
|
|
383
|
+
}
|
|
384
|
+
default:
|
|
385
|
+
throw new Error(`unreachable sync action: ${action}`);
|
|
386
|
+
}
|
|
387
|
+
if (nextFiles[rel] !== recorded[rel]) manifestDirty = true;
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
for (const rel of Object.keys(recorded)) {
|
|
391
|
+
if (!shipped.includes(rel)) {
|
|
392
|
+
report.orphans.push(rel);
|
|
393
|
+
manifestDirty = true;
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
if (!dryRun && manifestDirty) {
|
|
398
|
+
const body = `${JSON.stringify(
|
|
399
|
+
{ schema: MANIFEST_SCHEMA, name, version, files: nextFiles },
|
|
400
|
+
null,
|
|
401
|
+
2,
|
|
402
|
+
)}\n`;
|
|
403
|
+
writeAtomic(path.join(stack, MANIFEST_NAME), Buffer.from(body), 0o644);
|
|
404
|
+
}
|
|
405
|
+
return report;
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
/**
|
|
409
|
+
* Number of files a sync changed on disk.
|
|
410
|
+
*
|
|
411
|
+
* @param {SyncReport} r
|
|
412
|
+
*/
|
|
413
|
+
export function changedCount(r) {
|
|
414
|
+
return r.created.length + r.restored.length + r.updated.length + r.forced.length;
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
/**
|
|
418
|
+
* Human-readable summary lines.
|
|
419
|
+
*
|
|
420
|
+
* @param {SyncReport} r
|
|
421
|
+
* @returns {string[]}
|
|
422
|
+
*/
|
|
423
|
+
export function formatReport(r) {
|
|
424
|
+
const lines = [];
|
|
425
|
+
const verb = r.dryRun ? "would be" : "were";
|
|
426
|
+
lines.push(`stack root: ${r.stackRoot} (openmem ${r.version})`);
|
|
427
|
+
if (r.created.length) lines.push(` ${r.created.length} file(s) ${verb} created`);
|
|
428
|
+
if (r.updated.length) lines.push(` ${r.updated.length} file(s) ${verb} updated: ${r.updated.join(", ")}`);
|
|
429
|
+
if (r.restored.length) lines.push(` ${r.restored.length} missing file(s) ${verb} restored: ${r.restored.join(", ")}`);
|
|
430
|
+
if (r.forced.length) lines.push(` ${r.forced.length} locally modified file(s) ${verb} overwritten (backup next to each): ${r.forced.join(", ")}`);
|
|
431
|
+
if (!changedCount(r)) lines.push(` ${r.unchanged} shipped file(s) up to date`);
|
|
432
|
+
for (const c of r.conflicts) lines.push(` CONFLICT ${c.path}: ${c.reason}`);
|
|
433
|
+
if (r.conflicts.length) {
|
|
434
|
+
lines.push(" Resolve by merging by hand, or run: openmem init --force (keeps a .bak copy)");
|
|
435
|
+
}
|
|
436
|
+
for (const o of r.orphans) lines.push(` note: ${o} is no longer shipped; left in place`);
|
|
437
|
+
return lines;
|
|
438
|
+
}
|