@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
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Mise-like task runner for openmem (reads ~/.config/personal-memory/config.toml).
|
|
4
|
+
* ./pm configure | up | down | doctor
|
|
5
|
+
* Packaged installs reach it through `openmem <task>`.
|
|
6
|
+
*/
|
|
7
|
+
import { spawnSync } from "node:child_process";
|
|
8
|
+
import fs from "node:fs";
|
|
9
|
+
import path from "node:path";
|
|
10
|
+
import { ensureWizardDeps, resolveWizardRoots } from "../../cli/roots.mjs";
|
|
11
|
+
|
|
12
|
+
const { pkgRoot, repo: REPO, packaged } = resolveWizardRoots(import.meta.url);
|
|
13
|
+
// setup.mjs is code, so it always comes from the package root, never the stack root.
|
|
14
|
+
const SETUP = path.join(pkgRoot, "wizard/bin/setup.mjs");
|
|
15
|
+
|
|
16
|
+
const TASKS = {
|
|
17
|
+
configure: { args: ["--yes"], help: "Full setup and start" },
|
|
18
|
+
up: { args: ["--up", "--yes"], help: "Start stack" },
|
|
19
|
+
down: { args: ["--down", "--yes"], help: "Stop stack" },
|
|
20
|
+
doctor: { args: ["--yes", "--no-smoke"], help: "Validate config (no smoke)" },
|
|
21
|
+
"install-mcp": {
|
|
22
|
+
script: "cli/install-memory-mcp.mjs",
|
|
23
|
+
help: "Install memory MCP servers into ~/.cursor/mcp.json",
|
|
24
|
+
},
|
|
25
|
+
"install-coding-agent": {
|
|
26
|
+
script: "cli/install-coding-agent.mjs",
|
|
27
|
+
help: "Wire session memory for Claude Code, Codex, OpenCode, Cursor CLI",
|
|
28
|
+
},
|
|
29
|
+
"feed-sync": {
|
|
30
|
+
script: "cli/feed-sync.mjs",
|
|
31
|
+
help: "Fetch RSS/OpenAlex feeds into index.yml feeds.out_dir",
|
|
32
|
+
},
|
|
33
|
+
switch: {
|
|
34
|
+
script: "cli/switch-memory-provider.mjs",
|
|
35
|
+
help: "Switch MEMORY_PROVIDER (wizard.conf + optional stack/MCP)",
|
|
36
|
+
},
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
const task = process.argv[2];
|
|
40
|
+
if (!task || task === "-h" || task === "--help") {
|
|
41
|
+
ensureWizardDeps(pkgRoot, packaged);
|
|
42
|
+
const { loadPmConfig } = await import("../lib/pm-config.mjs");
|
|
43
|
+
const home = process.env.HOME || "";
|
|
44
|
+
const { merged } = loadPmConfig(home, REPO);
|
|
45
|
+
const tasks = /** @type {Record<string, {description?: string}>} */ (
|
|
46
|
+
merged.tasks || {}
|
|
47
|
+
);
|
|
48
|
+
console.log("Usage: pm <task>\n");
|
|
49
|
+
console.log("Tasks:");
|
|
50
|
+
for (const name of Object.keys(TASKS)) {
|
|
51
|
+
const desc = tasks[name]?.description || TASKS[name].help;
|
|
52
|
+
console.log(` ${name.padEnd(12)} ${desc}`);
|
|
53
|
+
}
|
|
54
|
+
process.exit(task ? 0 : 1);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const spec = TASKS[task];
|
|
58
|
+
if (!spec) {
|
|
59
|
+
console.error(`unknown task: ${task} (try: ${Object.keys(TASKS).join(", ")})`);
|
|
60
|
+
process.exit(1);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const extra = process.argv.slice(3);
|
|
64
|
+
|
|
65
|
+
if (spec.script) {
|
|
66
|
+
const script = path.join(pkgRoot, spec.script);
|
|
67
|
+
if (!fs.existsSync(script)) {
|
|
68
|
+
console.error(`${spec.script} not found; run from a openmem checkout or install`);
|
|
69
|
+
process.exit(1);
|
|
70
|
+
}
|
|
71
|
+
const r = spawnSync(process.execPath, [script, ...extra], {
|
|
72
|
+
cwd: REPO,
|
|
73
|
+
stdio: "inherit",
|
|
74
|
+
env: process.env,
|
|
75
|
+
});
|
|
76
|
+
process.exit(r.status ?? 1);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
if (!fs.existsSync(SETUP)) {
|
|
80
|
+
console.error("setup.mjs not found; run from a openmem checkout or install");
|
|
81
|
+
process.exit(1);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const r = spawnSync(process.execPath, [SETUP, ...spec.args, ...extra], {
|
|
85
|
+
cwd: REPO,
|
|
86
|
+
stdio: "inherit",
|
|
87
|
+
env: process.env,
|
|
88
|
+
});
|
|
89
|
+
if (task === "doctor" && (r.status ?? 1) === 0) {
|
|
90
|
+
const wiring = spawnSync(process.execPath, [path.join(pkgRoot, "cli/wiring-report.mjs")], {
|
|
91
|
+
cwd: REPO,
|
|
92
|
+
stdio: "inherit",
|
|
93
|
+
env: process.env,
|
|
94
|
+
});
|
|
95
|
+
process.exit(wiring.status ?? 0);
|
|
96
|
+
}
|
|
97
|
+
process.exit(r.status ?? 1);
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Setup wizard entry (./setup in a checkout; `openmem setup` when packaged).
|
|
4
|
+
*
|
|
5
|
+
* REPO is the mutable stack root (PM_STACK_ROOT, default: the checkout): compose project
|
|
6
|
+
* dir, .env, generated files and docker build context. PM_PACKAGE_ROOT is the read-only
|
|
7
|
+
* code root (providers/, scripts/setup.mjs, wizard/lib).
|
|
8
|
+
*/
|
|
9
|
+
import { ensureWizardDeps, resolveWizardRoots } from "../../cli/roots.mjs";
|
|
10
|
+
|
|
11
|
+
const { pkgRoot, repo: REPO, packaged } = resolveWizardRoots(import.meta.url);
|
|
12
|
+
ensureWizardDeps(pkgRoot, packaged);
|
|
13
|
+
|
|
14
|
+
const { runWizard } = await import("../lib/run-wizard.mjs");
|
|
15
|
+
|
|
16
|
+
const argv = process.argv.slice(2);
|
|
17
|
+
const wantsClack =
|
|
18
|
+
!argv.includes("--yes") &&
|
|
19
|
+
!argv.includes("-y") &&
|
|
20
|
+
!argv.includes("--dry-run") &&
|
|
21
|
+
process.stdin.isTTY;
|
|
22
|
+
|
|
23
|
+
/** @type {Record<string, string>} */
|
|
24
|
+
let handoff = {};
|
|
25
|
+
if (wantsClack) {
|
|
26
|
+
const { runClackSetup } = await import("../lib/run-clack.mjs");
|
|
27
|
+
handoff = await runClackSetup(REPO);
|
|
28
|
+
for (const [k, v] of Object.entries(handoff)) {
|
|
29
|
+
process.env[k] = v;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
await runWizard(REPO, argv);
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Docker Compose version gate for the wizard preflight.
|
|
3
|
+
*
|
|
4
|
+
* Provider compose fragments use `env_file: [{path, required: false}]`, which needs Compose 2.24;
|
|
5
|
+
* the Hindsight base file already needs 2.20 for `depends_on.required`. The wizard requires the
|
|
6
|
+
* higher bar everywhere so a stack never fails halfway through `docker compose up`.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
export const MIN_COMPOSE = { major: 2, minor: 24 };
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Parse `docker compose version --short` ("2.29.0", "v2.29.0") or the long form
|
|
13
|
+
* ("Docker Compose version v2.29.0"). Returns null when no version is found.
|
|
14
|
+
* @param {string} text
|
|
15
|
+
* @returns {{major:number, minor:number}|null}
|
|
16
|
+
*/
|
|
17
|
+
export function parseComposeVersion(text) {
|
|
18
|
+
const m = String(text || "").match(/v?(\d+)\.(\d+)/);
|
|
19
|
+
return m ? { major: Number(m[1]), minor: Number(m[2]) } : null;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* @param {{major:number, minor:number}|null} version
|
|
24
|
+
* @returns {boolean} true when the version meets MIN_COMPOSE
|
|
25
|
+
*/
|
|
26
|
+
export function composeVersionOk(version) {
|
|
27
|
+
if (!version) return false;
|
|
28
|
+
if (version.major !== MIN_COMPOSE.major) return version.major > MIN_COMPOSE.major;
|
|
29
|
+
return version.minor >= MIN_COMPOSE.minor;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export const COMPOSE_TOO_OLD_HINT = `Docker Compose ${MIN_COMPOSE.major}.${MIN_COMPOSE.minor} or newer is required (docker compose version --short); provider compose files use env_file required: false. Update the compose plugin`;
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Compose profiles for the active provider + optional extras (Ollama, NotebookLM).
|
|
6
|
+
*
|
|
7
|
+
* @param {{ memoryProvider?: { compose?: { profile?: string } }, extraComposeProfiles?: string[] }} ctx
|
|
8
|
+
* @returns {string[]}
|
|
9
|
+
*/
|
|
10
|
+
export function composeProfiles(ctx) {
|
|
11
|
+
const compose = ctx.memoryProvider?.compose || {};
|
|
12
|
+
const profiles = [];
|
|
13
|
+
if (compose.profile) profiles.push(compose.profile);
|
|
14
|
+
for (const p of ctx.extraComposeProfiles || []) {
|
|
15
|
+
if (typeof p === "string" && p && !profiles.includes(p)) profiles.push(p);
|
|
16
|
+
}
|
|
17
|
+
return profiles;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Arguments for `docker` that run a compose subcommand for the active provider.
|
|
22
|
+
*
|
|
23
|
+
* Every compose call in the wizard goes through here so the manifest's
|
|
24
|
+
* `compose.profile` and `compose.files` apply uniformly (up, build, run, down, ps).
|
|
25
|
+
* With no `compose.files`, no -f flag is added and compose auto-loads compose.yaml
|
|
26
|
+
* and compose.override.yaml from the stack root. With `compose.files`, the list is
|
|
27
|
+
* compose.yaml, compose.override.yaml (when present), then the extra files.
|
|
28
|
+
*
|
|
29
|
+
* Extra profiles (compose Ollama, NotebookLM) come from `ctx.extraComposeProfiles`.
|
|
30
|
+
* Docker Compose unions CLI `--profile` with `COMPOSE_PROFILES` from `.env`, so
|
|
31
|
+
* `composeSpawnOptions` overrides that env var to the same list — otherwise `.env`
|
|
32
|
+
* can start NotebookLM while the wizard intentionally omitted it (e.g. during smoke).
|
|
33
|
+
*
|
|
34
|
+
* @param {{ project: string, repoRoot: string, memoryProvider?: { compose?: { profile?: string, files?: string[] } }, extraComposeProfiles?: string[] }} ctx
|
|
35
|
+
* @param {...string} rest the compose subcommand and its arguments
|
|
36
|
+
* @returns {string[]}
|
|
37
|
+
*/
|
|
38
|
+
export function composeArgs(ctx, ...rest) {
|
|
39
|
+
const compose = ctx.memoryProvider?.compose || {};
|
|
40
|
+
const args = ["compose", "-p", ctx.project];
|
|
41
|
+
const extra = Array.isArray(compose.files) ? compose.files : [];
|
|
42
|
+
if (extra.length) {
|
|
43
|
+
args.push("-f", "compose.yaml");
|
|
44
|
+
if (fs.existsSync(path.join(ctx.repoRoot, "compose.override.yaml"))) {
|
|
45
|
+
args.push("-f", "compose.override.yaml");
|
|
46
|
+
}
|
|
47
|
+
for (const f of extra) args.push("-f", f);
|
|
48
|
+
}
|
|
49
|
+
for (const p of composeProfiles(ctx)) args.push("--profile", p);
|
|
50
|
+
return [...args, ...rest];
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** `docker ...` as the wizard prints it in descriptions. */
|
|
54
|
+
export function composeCommandText(ctx, ...rest) {
|
|
55
|
+
return `docker ${composeArgs(ctx, ...rest).join(" ")}`;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Spawn options for compose calls: cwd is the stack root; COMPOSE_PROFILES matches
|
|
60
|
+
* the `--profile` flags so `.env` cannot enable extra profiles behind the wizard's back.
|
|
61
|
+
*/
|
|
62
|
+
export function composeSpawnOptions(ctx) {
|
|
63
|
+
return {
|
|
64
|
+
cwd: ctx.repoRoot,
|
|
65
|
+
env: {
|
|
66
|
+
...process.env,
|
|
67
|
+
COMPOSE_PROFILES: composeProfiles(ctx).join(","),
|
|
68
|
+
},
|
|
69
|
+
};
|
|
70
|
+
}
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
import { spawnSync } from "node:child_process";
|
|
2
|
+
import {
|
|
3
|
+
CONTEXT_PROXY_SERVICE,
|
|
4
|
+
contextProxyRunning,
|
|
5
|
+
prepareContextProxyFiles,
|
|
6
|
+
proxyServiceNeeded,
|
|
7
|
+
} from "../../cli/context-management.mjs";
|
|
8
|
+
import { agentsPointedAtContextProxy } from "../../cli/coding-agent/install-context-proxy.mjs";
|
|
9
|
+
import { act } from "./exec.mjs";
|
|
10
|
+
import { composeArgs, composeSpawnOptions } from "./compose.mjs";
|
|
11
|
+
import { info, ok, step, warn } from "./log.mjs";
|
|
12
|
+
import { multiselect } from "./prompts.mjs";
|
|
13
|
+
import { CONTEXT_AGENT_KEYS, contextAgentsFromPmConfig, updatePmConfigContextAgents } from "./pm-config.mjs";
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Wizard step "Context management (smart compaction)": pick the installed agents to trim, save
|
|
17
|
+
* `[context_proxy.agents]` and `[context_proxy] enabled` (syncPmConfig keeps the table when it rewrites
|
|
18
|
+
* config.toml), and start or stop the compose `context-proxy` service. install-coding-agent reads the
|
|
19
|
+
* saved values when the next step wires agents.
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
export const CONTEXT_STEP_TITLE = "Context management (smart compaction)";
|
|
23
|
+
/** Agents the step offers. */
|
|
24
|
+
const SELECTABLE = ["claude-code", "opencode1", "codex", "opencode2"];
|
|
25
|
+
const LABELS = {
|
|
26
|
+
"claude-code": "Claude Code (context-proxy service)",
|
|
27
|
+
opencode1: "OpenCode 1 (context-proxy service)",
|
|
28
|
+
codex: "Codex (context-proxy service, ChatGPT login)",
|
|
29
|
+
opencode2: "OpenCode 2 (native plugin, no service)",
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Config keys for detected harnesses (`claude-code`, `codex`, `opencode`, `cursor-cli`).
|
|
34
|
+
* @param {string[]} harnesses
|
|
35
|
+
* @param {number} opencodeMajor
|
|
36
|
+
*/
|
|
37
|
+
export function contextAgentKeys(harnesses, opencodeMajor) {
|
|
38
|
+
const keys = [];
|
|
39
|
+
if (harnesses.includes("claude-code")) keys.push("claude-code");
|
|
40
|
+
if (harnesses.includes("codex")) keys.push("codex");
|
|
41
|
+
if (harnesses.includes("opencode")) keys.push(opencodeMajor >= 2 ? "opencode2" : "opencode1");
|
|
42
|
+
return keys;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Defaults: a saved per-agent key, else a saved `enabled`, else OpenCode 2 on and the proxy-routed agents off.
|
|
47
|
+
* @param {Record<string, unknown>} cfg
|
|
48
|
+
* @returns {Record<string, boolean>}
|
|
49
|
+
*/
|
|
50
|
+
export function contextAgentDefaults(cfg) {
|
|
51
|
+
return contextAgentsFromPmConfig(cfg).agents;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** Whether the proxy service is needed: the step's choice, else the saved config. */
|
|
55
|
+
export function contextServiceWanted(ctx) {
|
|
56
|
+
return proxyServiceNeeded(ctx.contextAgents || contextAgentsFromPmConfig(ctx.pmMerged || {}).agents);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** Context with the context-proxy profile added, for compose calls on that service. */
|
|
60
|
+
function withProxyProfile(ctx) {
|
|
61
|
+
return { ...ctx, extraComposeProfiles: [...(ctx.extraComposeProfiles || []), CONTEXT_PROXY_SERVICE] };
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Multi-select the installed agents (defaults: contextAgentDefaults) and save the values when they differ
|
|
66
|
+
* from config. Agents that are not installed keep their saved value, else their default.
|
|
67
|
+
* @param {import("./run-wizard.mjs").WizardContext} ctx
|
|
68
|
+
* @param {string} number step number shown in the title
|
|
69
|
+
* @param {string[]} installed config keys of the installed agents (contextAgentKeys)
|
|
70
|
+
* @returns {Promise<Record<string, boolean>>}
|
|
71
|
+
*/
|
|
72
|
+
export async function chooseContextManagement(ctx, number, installed) {
|
|
73
|
+
step(`${number}. ${CONTEXT_STEP_TITLE}`);
|
|
74
|
+
info("Stores every session message in a local SQLite store and trims long sessions; agents query the trimmed turns with smart_compact_search and smart_compact_sql.");
|
|
75
|
+
info("Claude Code, OpenCode 1 and Codex go through the local context-proxy compose service; OpenCode 2 uses its native plugin.");
|
|
76
|
+
const cfg = ctx.pmMerged || {};
|
|
77
|
+
const { agents: current, saved } = contextAgentsFromPmConfig(cfg);
|
|
78
|
+
const defaults = contextAgentDefaults(cfg);
|
|
79
|
+
const offered = SELECTABLE.filter((k) => installed.includes(k));
|
|
80
|
+
let values;
|
|
81
|
+
if (!offered.length) {
|
|
82
|
+
info("no agent that supports context management is installed; saved values kept");
|
|
83
|
+
values = current;
|
|
84
|
+
} else {
|
|
85
|
+
const picked = await multiselect(
|
|
86
|
+
ctx,
|
|
87
|
+
"Trim long sessions for which agents?",
|
|
88
|
+
offered.map((k) => ({ value: k, label: LABELS[k] })),
|
|
89
|
+
offered.filter((k) => defaults[k]),
|
|
90
|
+
);
|
|
91
|
+
values = Object.fromEntries(CONTEXT_AGENT_KEYS.map((k) => [k, offered.includes(k) ? picked.includes(k) : (saved.includes(k) ? current[k] : defaults[k])]));
|
|
92
|
+
const changed = CONTEXT_AGENT_KEYS.some((k) => !saved.includes(k) || current[k] !== values[k]);
|
|
93
|
+
if (changed) {
|
|
94
|
+
const r = updatePmConfigContextAgents(ctx.home, values, { dry: ctx.dry });
|
|
95
|
+
const summary = CONTEXT_AGENT_KEYS.map((k) => `${k} = ${values[k]}`).join(", ");
|
|
96
|
+
info(`${r.result === "dry-run" ? "[dry-run] would save" : "saved"} [context_proxy.agents] ${summary}`);
|
|
97
|
+
if (r.result !== "dry-run") {
|
|
98
|
+
const table = { ...(cfg.context_proxy || {}), enabled: r.enabled, agents: r.agents };
|
|
99
|
+
ctx.pmMerged = { ...cfg, context_proxy: table };
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
ctx.contextAgents = values;
|
|
104
|
+
ok(`context management: ${CONTEXT_AGENT_KEYS.map((k) => `${k} ${values[k] ? "on" : "off"}`).join(", ")}`);
|
|
105
|
+
return values;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Start the service when a proxy-routed agent is on (before agents are pointed at it). Writes the key file
|
|
110
|
+
* and store directories compose needs first.
|
|
111
|
+
* @param {import("./run-wizard.mjs").WizardContext} ctx
|
|
112
|
+
*/
|
|
113
|
+
export async function startContextProxy(ctx) {
|
|
114
|
+
if (!contextServiceWanted(ctx)) return;
|
|
115
|
+
if (!ctx.dry) prepareContextProxyFiles(ctx.home);
|
|
116
|
+
const c = withProxyProfile(ctx);
|
|
117
|
+
const up = composeArgs(c, "up", "-d", "--no-build", CONTEXT_PROXY_SERVICE);
|
|
118
|
+
await act(ctx, `docker ${up.join(" ")}`, "docker", up, composeSpawnOptions(c));
|
|
119
|
+
if (!ctx.dry) ok("context-proxy started");
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Stop the service when no proxy-routed agent is on and it runs, unless an agent still points at it
|
|
124
|
+
* (stopping would break that agent's every request).
|
|
125
|
+
* @param {import("./run-wizard.mjs").WizardContext} ctx
|
|
126
|
+
* @param {typeof spawnSync} [spawn]
|
|
127
|
+
*/
|
|
128
|
+
export async function stopContextProxy(ctx, spawn = spawnSync) {
|
|
129
|
+
if (contextServiceWanted(ctx)) return;
|
|
130
|
+
const c = withProxyProfile(ctx);
|
|
131
|
+
if (!contextProxyRunning(composeArgs(c), composeSpawnOptions(c), spawn)) return;
|
|
132
|
+
const pointed = ctx.dry ? [] : agentsPointedAtContextProxy(ctx.home);
|
|
133
|
+
if (pointed.length) {
|
|
134
|
+
warn(`context-proxy left running: ${pointed.join(", ")} still point at it. Run: npx @bcmyguest/openmem install-coding-agent --yes`);
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
const stop = composeArgs(c, "stop", CONTEXT_PROXY_SERVICE);
|
|
138
|
+
await act(ctx, `docker ${stop.join(" ")}`, "docker", stop, composeSpawnOptions(c));
|
|
139
|
+
if (!ctx.dry) ok("context-proxy stopped");
|
|
140
|
+
}
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import { spawn, spawnSync } from "node:child_process";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @param {string} url
|
|
5
|
+
* @param {number} [timeoutSec]
|
|
6
|
+
* @returns {string}
|
|
7
|
+
*/
|
|
8
|
+
export function curlGet(url, timeoutSec = 5) {
|
|
9
|
+
const r = spawnSync("curl", ["-fsS", "-m", String(timeoutSec), url], {
|
|
10
|
+
encoding: "utf8",
|
|
11
|
+
});
|
|
12
|
+
if (r.status !== 0) return "";
|
|
13
|
+
return r.stdout || "";
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* @param {string} url
|
|
18
|
+
* @returns {object|null}
|
|
19
|
+
*/
|
|
20
|
+
export function curlJson(url) {
|
|
21
|
+
const body = curlGet(url);
|
|
22
|
+
if (!body) return null;
|
|
23
|
+
try {
|
|
24
|
+
return JSON.parse(body);
|
|
25
|
+
} catch {
|
|
26
|
+
return null;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* @param {string} method
|
|
32
|
+
* @param {string} url
|
|
33
|
+
* @param {string} [jsonBody]
|
|
34
|
+
* @param {number} [timeoutSec]
|
|
35
|
+
* @returns {string}
|
|
36
|
+
*/
|
|
37
|
+
export function curlRequest(method, url, jsonBody, timeoutSec = 60) {
|
|
38
|
+
const args = curlArgs(method, url, jsonBody, timeoutSec);
|
|
39
|
+
const r = spawnSync("curl", args, { encoding: "utf8" });
|
|
40
|
+
if (r.status !== 0) return "";
|
|
41
|
+
return r.stdout || "";
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Build curl argv for a JSON (or empty-body) request.
|
|
46
|
+
* @param {string} method
|
|
47
|
+
* @param {string} url
|
|
48
|
+
* @param {string} [jsonBody]
|
|
49
|
+
* @param {number} timeoutSec
|
|
50
|
+
* @returns {string[]}
|
|
51
|
+
*/
|
|
52
|
+
export function curlArgs(method, url, jsonBody, timeoutSec) {
|
|
53
|
+
const args = ["-fsS", "-m", String(timeoutSec), "-X", method, url];
|
|
54
|
+
if (jsonBody !== undefined) {
|
|
55
|
+
args.push("-H", "Content-Type: application/json", "-d", jsonBody);
|
|
56
|
+
}
|
|
57
|
+
return args;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Like curlRequest, but async with an optional heartbeat so long retains do not look hung.
|
|
62
|
+
*
|
|
63
|
+
* @param {string} method
|
|
64
|
+
* @param {string} url
|
|
65
|
+
* @param {string} [jsonBody]
|
|
66
|
+
* @param {number} [timeoutSec]
|
|
67
|
+
* @param {{ tickSec?: number, onTick?: (info: { elapsed: number, timeoutSec: number }) => void }} [opts]
|
|
68
|
+
* @returns {Promise<string>} response body, or "" on failure / timeout
|
|
69
|
+
*/
|
|
70
|
+
export function curlRequestProgress(method, url, jsonBody, timeoutSec = 60, opts = {}) {
|
|
71
|
+
const tickSec = opts.tickSec ?? 15;
|
|
72
|
+
const onTick = opts.onTick;
|
|
73
|
+
const args = curlArgs(method, url, jsonBody, timeoutSec);
|
|
74
|
+
return new Promise((resolve) => {
|
|
75
|
+
const child = spawn("curl", args, { stdio: ["ignore", "pipe", "pipe"] });
|
|
76
|
+
let stdout = "";
|
|
77
|
+
let settled = false;
|
|
78
|
+
const started = Date.now();
|
|
79
|
+
const timer =
|
|
80
|
+
typeof onTick === "function"
|
|
81
|
+
? setInterval(() => {
|
|
82
|
+
onTick({
|
|
83
|
+
elapsed: Math.round((Date.now() - started) / 1000),
|
|
84
|
+
timeoutSec,
|
|
85
|
+
});
|
|
86
|
+
}, tickSec * 1000)
|
|
87
|
+
: null;
|
|
88
|
+
|
|
89
|
+
const finish = (body) => {
|
|
90
|
+
if (settled) return;
|
|
91
|
+
settled = true;
|
|
92
|
+
if (timer) clearInterval(timer);
|
|
93
|
+
resolve(body);
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
child.stdout.setEncoding("utf8");
|
|
97
|
+
child.stdout.on("data", (chunk) => {
|
|
98
|
+
stdout += chunk;
|
|
99
|
+
});
|
|
100
|
+
child.on("error", () => finish(""));
|
|
101
|
+
child.on("close", (code) => finish(code === 0 ? stdout : ""));
|
|
102
|
+
});
|
|
103
|
+
}
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
import crypto from "node:crypto";
|
|
2
|
+
import fs from "node:fs";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { spawnSync } from "node:child_process";
|
|
5
|
+
import { ok, die } from "./log.mjs";
|
|
6
|
+
import { act } from "./exec.mjs";
|
|
7
|
+
import { composeArgs, composeSpawnOptions } from "./compose.mjs";
|
|
8
|
+
|
|
9
|
+
function buildDockerfile(svc) {
|
|
10
|
+
if (svc === "indexer") return "indexer/Dockerfile";
|
|
11
|
+
if (svc === "notebooklm") return "sync/Dockerfile";
|
|
12
|
+
die(`the wizard does not build service ${svc}`);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function copySourcesFromDockerfile(repoRoot, dfRel) {
|
|
16
|
+
const dfPath = path.join(repoRoot, dfRel);
|
|
17
|
+
const text = fs.readFileSync(dfPath, "utf8");
|
|
18
|
+
const srcs = [];
|
|
19
|
+
for (const line of text.split("\n")) {
|
|
20
|
+
if (!line.startsWith("COPY")) continue;
|
|
21
|
+
const parts = line.trim().split(/\s+/).slice(1);
|
|
22
|
+
const filtered = parts.filter((p) => !p.startsWith("--"));
|
|
23
|
+
for (let i = 0; i < filtered.length - 1; i++) srcs.push(filtered[i]);
|
|
24
|
+
}
|
|
25
|
+
return srcs;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function expandInputs(repoRoot, rel) {
|
|
29
|
+
const abs = path.join(repoRoot, rel);
|
|
30
|
+
const out = [];
|
|
31
|
+
if (!fs.existsSync(abs)) {
|
|
32
|
+
out.push(rel);
|
|
33
|
+
return out;
|
|
34
|
+
}
|
|
35
|
+
if (fs.statSync(abs).isDirectory()) {
|
|
36
|
+
const walk = (dir, prefix) => {
|
|
37
|
+
for (const ent of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
38
|
+
if (ent.name === "__pycache__" || ent.name.endsWith(".pyc")) continue;
|
|
39
|
+
const p = path.join(dir, ent.name);
|
|
40
|
+
const relP = prefix ? `${prefix}/${ent.name}` : ent.name;
|
|
41
|
+
if (ent.isDirectory()) walk(p, relP);
|
|
42
|
+
else out.push(relP);
|
|
43
|
+
}
|
|
44
|
+
};
|
|
45
|
+
walk(abs, rel);
|
|
46
|
+
} else out.push(rel);
|
|
47
|
+
return out;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* @param {import("./run-wizard.mjs").WizardContext} ctx
|
|
52
|
+
* @param {string} svc
|
|
53
|
+
*/
|
|
54
|
+
export function buildHash(ctx, svc) {
|
|
55
|
+
const df = buildDockerfile(svc);
|
|
56
|
+
const files = new Set([df, "compose.yaml"]);
|
|
57
|
+
for (const f of ctx.memoryProvider?.compose?.files || []) files.add(f);
|
|
58
|
+
for (const src of copySourcesFromDockerfile(ctx.repoRoot, df)) {
|
|
59
|
+
for (const f of expandInputs(ctx.repoRoot, src)) files.add(f);
|
|
60
|
+
}
|
|
61
|
+
const sorted = [...files].sort();
|
|
62
|
+
const hash = crypto.createHash("sha256");
|
|
63
|
+
for (const f of sorted) {
|
|
64
|
+
const p = path.join(ctx.repoRoot, f);
|
|
65
|
+
if (!fs.existsSync(p)) die(`missing build input ${f}`);
|
|
66
|
+
hash.update(fs.readFileSync(p));
|
|
67
|
+
}
|
|
68
|
+
return hash.digest("hex");
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* @param {import("./run-wizard.mjs").WizardContext} ctx
|
|
73
|
+
* @param {string} svc
|
|
74
|
+
* @param {string} hash
|
|
75
|
+
*/
|
|
76
|
+
function recordBuildHash(ctx, svc, hash) {
|
|
77
|
+
let lines = [];
|
|
78
|
+
if (fs.existsSync(ctx.buildHashFile)) {
|
|
79
|
+
lines = fs
|
|
80
|
+
.readFileSync(ctx.buildHashFile, "utf8")
|
|
81
|
+
.split("\n")
|
|
82
|
+
.filter((l) => l && !l.startsWith(`${svc} `));
|
|
83
|
+
}
|
|
84
|
+
lines.push(`${svc} ${hash}`);
|
|
85
|
+
fs.writeFileSync(ctx.buildHashFile, `${lines.join("\n")}\n`);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* @param {import("./run-wizard.mjs").WizardContext} ctx
|
|
90
|
+
* @param {string} svc
|
|
91
|
+
* @param {string} image
|
|
92
|
+
*/
|
|
93
|
+
export async function buildService(ctx, svc, image) {
|
|
94
|
+
const hash = buildHash(ctx, svc);
|
|
95
|
+
let stored = "";
|
|
96
|
+
if (fs.existsSync(ctx.buildHashFile)) {
|
|
97
|
+
for (const line of fs.readFileSync(ctx.buildHashFile, "utf8").split("\n")) {
|
|
98
|
+
if (line.startsWith(`${svc} `)) stored = line.split(/\s+/)[1] || "";
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
const inspect = spawnSync("docker", ["image", "inspect", image], { stdio: "ignore" });
|
|
102
|
+
const imageOk = inspect.status === 0;
|
|
103
|
+
if (!ctx.rebuild && stored === hash && imageOk) {
|
|
104
|
+
ok(`${svc} image unchanged; build skipped`);
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
await act(
|
|
108
|
+
ctx,
|
|
109
|
+
`build the ${svc} image (compose project ${ctx.project})`,
|
|
110
|
+
"docker",
|
|
111
|
+
composeArgs(ctx, "build", svc),
|
|
112
|
+
composeSpawnOptions(ctx),
|
|
113
|
+
);
|
|
114
|
+
if (ctx.dry) return;
|
|
115
|
+
recordBuildHash(ctx, svc, hash);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* @param {import("./run-wizard.mjs").WizardContext} ctx
|
|
120
|
+
* @returns {Promise<string[]>}
|
|
121
|
+
*/
|
|
122
|
+
export async function snapshot(ctx) {
|
|
123
|
+
const lines = [];
|
|
124
|
+
const globHs = fs.existsSync(ctx.hsDir) ? fs.readdirSync(ctx.hsDir) : [];
|
|
125
|
+
for (const name of globHs) {
|
|
126
|
+
const f = path.join(ctx.hsDir, name);
|
|
127
|
+
if (fs.statSync(f).isFile()) {
|
|
128
|
+
const h = crypto.createHash("sha256").update(fs.readFileSync(f)).digest("hex");
|
|
129
|
+
lines.push(`${f} ${h}`);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
for (const f of [path.join(ctx.repoRoot, ".env"), path.join(ctx.repoRoot, "compose.override.yaml")]) {
|
|
133
|
+
if (fs.existsSync(f)) {
|
|
134
|
+
const h = crypto.createHash("sha256").update(fs.readFileSync(f)).digest("hex");
|
|
135
|
+
lines.push(`${f} ${h}`);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
const ps = spawnSync("docker", composeArgs(ctx, "ps", "-q"), {
|
|
139
|
+
encoding: "utf8",
|
|
140
|
+
...composeSpawnOptions(ctx),
|
|
141
|
+
});
|
|
142
|
+
if (ps.status === 0 && ps.stdout) {
|
|
143
|
+
for (const id of ps.stdout.split("\n").filter(Boolean)) {
|
|
144
|
+
lines.push(`container ${id.trim()}`);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
return lines.sort();
|
|
148
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { spawnSync } from "node:child_process";
|
|
2
|
+
import { info } from "./log.mjs";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Run a command, or print it when the context is a dry run. Exits the process on failure.
|
|
6
|
+
* @param {import("./run-wizard.mjs").WizardContext} ctx
|
|
7
|
+
* @param {string} description
|
|
8
|
+
* @param {string} cmd
|
|
9
|
+
* @param {string[]} args
|
|
10
|
+
* @param {{ env?: Record<string, string>, cwd?: string }} [opts] extra environment, working directory
|
|
11
|
+
*/
|
|
12
|
+
export async function act(ctx, description, cmd, args, opts = {}) {
|
|
13
|
+
if (ctx.dry) {
|
|
14
|
+
info(`[dry-run] ${description}`);
|
|
15
|
+
return;
|
|
16
|
+
}
|
|
17
|
+
info(description);
|
|
18
|
+
const r = spawnSync(cmd, args, {
|
|
19
|
+
stdio: "inherit",
|
|
20
|
+
env: { ...process.env, ...(opts.env || {}) },
|
|
21
|
+
...(opts.cwd ? { cwd: opts.cwd } : {}),
|
|
22
|
+
});
|
|
23
|
+
if (r.status !== 0) process.exit(r.status ?? 1);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Like act(), with extra environment variables for the child.
|
|
28
|
+
* @param {import("./run-wizard.mjs").WizardContext} ctx
|
|
29
|
+
* @param {string} description
|
|
30
|
+
* @param {string} cmd
|
|
31
|
+
* @param {string[]} args
|
|
32
|
+
* @param {Record<string, string>} extraEnv
|
|
33
|
+
*/
|
|
34
|
+
export async function actWithEnv(ctx, description, cmd, args, extraEnv) {
|
|
35
|
+
await act(ctx, description, cmd, args, { env: extraEnv });
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function commandExists(name) {
|
|
39
|
+
const r = spawnSync("command", ["-v", name], { shell: true, stdio: "ignore" });
|
|
40
|
+
return r.status === 0;
|
|
41
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The shared indexer's container entry point. The wizard runs it with --list to preview
|
|
3
|
+
* which sources an index.yml selects. PM_INDEXER_BIN overrides the name if the entry
|
|
4
|
+
* point is renamed.
|
|
5
|
+
*/
|
|
6
|
+
export const INDEXER_BIN = process.env.PM_INDEXER_BIN || "hindsight-project-index";
|