@bububuger/spanory 0.1.21 → 0.1.26
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/dist/index.js +140 -130
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
// dist/index.js
|
|
4
4
|
import { existsSync, readFileSync, realpathSync } from "node:fs";
|
|
5
|
-
import {
|
|
5
|
+
import { copyFile, mkdir as mkdir4, readdir as readdir5, readFile as readFile8, rm, stat as stat4, writeFile as writeFile4 } from "node:fs/promises";
|
|
6
6
|
import path8 from "node:path";
|
|
7
7
|
import { createHash as createHash4 } from "node:crypto";
|
|
8
8
|
import { spawnSync } from "node:child_process";
|
|
@@ -4473,6 +4473,14 @@ async function backupIfExists(filePath) {
|
|
|
4473
4473
|
await copyFile(filePath, backupPath);
|
|
4474
4474
|
return backupPath;
|
|
4475
4475
|
}
|
|
4476
|
+
function isSpanoryCodexNotifyConfigured(configText) {
|
|
4477
|
+
return /notify\s*=\s*\[[^\]]*spanory-codex-notify\.sh[^\]]*\]/m.test(String(configText ?? ""));
|
|
4478
|
+
}
|
|
4479
|
+
function stripSpanoryCodexNotifyConfig(configText) {
|
|
4480
|
+
const next = String(configText ?? "").replace(/^notify\s*=\s*\[[^\]]*spanory-codex-notify\.sh[^\]]*\]\s*$/gm, "").replace(/\n{3,}/g, "\n\n").trim();
|
|
4481
|
+
return next ? `${next}
|
|
4482
|
+
` : "";
|
|
4483
|
+
}
|
|
4476
4484
|
function isSpanoryHookCommand(command) {
|
|
4477
4485
|
const text = String(command ?? "");
|
|
4478
4486
|
return /\bspanory\b/.test(text) && /\bhook\b/.test(text);
|
|
@@ -4525,76 +4533,10 @@ async function applyClaudeSetup({ homeRoot, spanoryBin, dryRun }) {
|
|
|
4525
4533
|
backup
|
|
4526
4534
|
};
|
|
4527
4535
|
}
|
|
4528
|
-
function
|
|
4529
|
-
const notifyLine = `notify = ["${escapeTomlBasicString(notifyScriptRef)}"]`;
|
|
4530
|
-
if (/^notify\s*=.*$/m.test(configText)) {
|
|
4531
|
-
return configText.replace(/^notify\s*=.*$/m, notifyLine);
|
|
4532
|
-
}
|
|
4533
|
-
const withNewline = configText.trimEnd();
|
|
4534
|
-
if (!withNewline)
|
|
4535
|
-
return `${notifyLine}
|
|
4536
|
-
`;
|
|
4537
|
-
return `${withNewline}
|
|
4538
|
-
|
|
4539
|
-
${notifyLine}
|
|
4540
|
-
`;
|
|
4541
|
-
}
|
|
4542
|
-
function escapeTomlBasicString(value) {
|
|
4543
|
-
return String(value).replaceAll("\\", "\\\\").replaceAll('"', '\\"');
|
|
4544
|
-
}
|
|
4545
|
-
function isSpanoryCodexNotifyConfigured(configText) {
|
|
4546
|
-
return /notify\s*=\s*\[[^\]]*spanory-codex-notify\.sh[^\]]*\]/m.test(String(configText ?? ""));
|
|
4547
|
-
}
|
|
4548
|
-
function stripSpanoryCodexNotifyConfig(configText) {
|
|
4549
|
-
const next = String(configText ?? "").replace(/^notify\s*=\s*\[[^\]]*spanory-codex-notify\.sh[^\]]*\]\s*$/gm, "").replace(/\n{3,}/g, "\n\n").trim();
|
|
4550
|
-
return next ? `${next}
|
|
4551
|
-
` : "";
|
|
4552
|
-
}
|
|
4553
|
-
function codexNotifyScriptContent({ spanoryBin, codexHome, exportDir, logFile }) {
|
|
4554
|
-
return `#!/usr/bin/env bash
|
|
4555
|
-
set -euo pipefail
|
|
4556
|
-
payload="\${1:-}"
|
|
4557
|
-
if [[ -z "$payload" ]] && [[ ! -t 0 ]]; then
|
|
4558
|
-
IFS= read -r -t 0 payload || true
|
|
4559
|
-
fi
|
|
4560
|
-
if [[ -z "\${payload//[$'\\t\\r\\n ']/}" ]]; then
|
|
4561
|
-
echo "skip=empty-payload source=codex-notify args=$#" >> "${logFile}"
|
|
4562
|
-
exit 0
|
|
4563
|
-
fi
|
|
4564
|
-
payload_file="$(mktemp "\${TMPDIR:-/tmp}/spanory-codex-notify.XXXXXX")"
|
|
4565
|
-
printf '%s' "$payload" > "$payload_file"
|
|
4566
|
-
echo "$payload" | "${spanoryBin}" runtime codex hook \\
|
|
4567
|
-
--last-turn-only \\
|
|
4568
|
-
--runtime-home "${codexHome}" \\
|
|
4569
|
-
--export-json-dir "${exportDir}" \\
|
|
4570
|
-
>> "${logFile}" 2>&1 || true
|
|
4571
|
-
(
|
|
4572
|
-
sleep 2
|
|
4573
|
-
cat "$payload_file" | "${spanoryBin}" runtime codex hook \\
|
|
4574
|
-
--last-turn-only \\
|
|
4575
|
-
--force \\
|
|
4576
|
-
--runtime-home "${codexHome}" \\
|
|
4577
|
-
--export-json-dir "${exportDir}" \\
|
|
4578
|
-
>> "${logFile}" 2>&1 || true
|
|
4579
|
-
rm -f "$payload_file"
|
|
4580
|
-
) >/dev/null 2>&1 &
|
|
4581
|
-
`;
|
|
4582
|
-
}
|
|
4583
|
-
async function applyCodexSetup({ homeRoot, spanoryBin, dryRun }) {
|
|
4536
|
+
async function applyCodexWatchSetup({ homeRoot, dryRun }) {
|
|
4584
4537
|
const codexHome = path8.join(homeRoot, ".codex");
|
|
4585
|
-
const
|
|
4586
|
-
const binDir = path8.join(codexHome, "bin");
|
|
4587
|
-
const stateDir = path8.join(codexHome, "state", "spanory");
|
|
4588
|
-
const scriptPath = path8.join(binDir, "spanory-codex-notify.sh");
|
|
4589
|
-
const logFile = path8.join(spanoryHome, "logs", "codex-notify.log");
|
|
4538
|
+
const scriptPath = path8.join(codexHome, "bin", "spanory-codex-notify.sh");
|
|
4590
4539
|
const configPath = path8.join(codexHome, "config.toml");
|
|
4591
|
-
const notifyScriptRef = scriptPath;
|
|
4592
|
-
const scriptContent = codexNotifyScriptContent({
|
|
4593
|
-
spanoryBin,
|
|
4594
|
-
codexHome,
|
|
4595
|
-
exportDir: stateDir,
|
|
4596
|
-
logFile
|
|
4597
|
-
});
|
|
4598
4540
|
let currentConfig = "";
|
|
4599
4541
|
try {
|
|
4600
4542
|
currentConfig = await readFile8(configPath, "utf-8");
|
|
@@ -4602,29 +4544,24 @@ async function applyCodexSetup({ homeRoot, spanoryBin, dryRun }) {
|
|
|
4602
4544
|
if (error?.code !== "ENOENT")
|
|
4603
4545
|
throw error;
|
|
4604
4546
|
}
|
|
4605
|
-
const nextConfig =
|
|
4547
|
+
const nextConfig = stripSpanoryCodexNotifyConfig(currentConfig);
|
|
4606
4548
|
const configChanged = currentConfig !== nextConfig;
|
|
4607
|
-
let
|
|
4549
|
+
let scriptPresent = false;
|
|
4608
4550
|
try {
|
|
4609
|
-
|
|
4610
|
-
|
|
4611
|
-
|
|
4612
|
-
throw error;
|
|
4551
|
+
await stat4(scriptPath);
|
|
4552
|
+
scriptPresent = true;
|
|
4553
|
+
} catch {
|
|
4613
4554
|
}
|
|
4614
|
-
const
|
|
4615
|
-
const changed = configChanged || scriptChanged;
|
|
4555
|
+
const changed = configChanged || scriptPresent;
|
|
4616
4556
|
let configBackup = null;
|
|
4617
4557
|
if (changed && !dryRun) {
|
|
4618
|
-
await mkdir4(binDir, { recursive: true });
|
|
4619
|
-
await mkdir4(stateDir, { recursive: true });
|
|
4620
|
-
await mkdir4(path8.dirname(logFile), { recursive: true });
|
|
4621
4558
|
if (configChanged) {
|
|
4559
|
+
await mkdir4(path8.dirname(configPath), { recursive: true });
|
|
4622
4560
|
configBackup = await backupIfExists(configPath);
|
|
4623
4561
|
await writeFile4(configPath, nextConfig, "utf-8");
|
|
4624
4562
|
}
|
|
4625
|
-
if (
|
|
4626
|
-
await
|
|
4627
|
-
await chmod(scriptPath, 493);
|
|
4563
|
+
if (scriptPresent) {
|
|
4564
|
+
await rm(scriptPath, { force: true });
|
|
4628
4565
|
}
|
|
4629
4566
|
}
|
|
4630
4567
|
return {
|
|
@@ -4632,15 +4569,58 @@ async function applyCodexSetup({ homeRoot, spanoryBin, dryRun }) {
|
|
|
4632
4569
|
ok: true,
|
|
4633
4570
|
changed,
|
|
4634
4571
|
dryRun,
|
|
4572
|
+
mode: "watch",
|
|
4635
4573
|
configPath,
|
|
4636
4574
|
scriptPath,
|
|
4637
|
-
configBackup
|
|
4575
|
+
configBackup,
|
|
4576
|
+
notifyConfigured: isSpanoryCodexNotifyConfigured(nextConfig),
|
|
4577
|
+
scriptPresent: false
|
|
4638
4578
|
};
|
|
4639
4579
|
}
|
|
4640
|
-
|
|
4580
|
+
function removeClaudeHooks(settings) {
|
|
4581
|
+
if (!settings.hooks || typeof settings.hooks !== "object")
|
|
4582
|
+
return;
|
|
4583
|
+
for (const eventName of Object.keys(settings.hooks)) {
|
|
4584
|
+
const groups = settings.hooks[eventName];
|
|
4585
|
+
if (!Array.isArray(groups))
|
|
4586
|
+
continue;
|
|
4587
|
+
for (const group of groups) {
|
|
4588
|
+
if (!group || !Array.isArray(group.hooks))
|
|
4589
|
+
continue;
|
|
4590
|
+
group.hooks = group.hooks.filter((hook) => !(hook && typeof hook === "object" && isSpanoryHookCommand(hook.command)));
|
|
4591
|
+
}
|
|
4592
|
+
settings.hooks[eventName] = groups.filter((group) => !group || !Array.isArray(group.hooks) || group.hooks.length > 0);
|
|
4593
|
+
if (settings.hooks[eventName].length === 0)
|
|
4594
|
+
delete settings.hooks[eventName];
|
|
4595
|
+
}
|
|
4596
|
+
if (Object.keys(settings.hooks).length === 0)
|
|
4597
|
+
delete settings.hooks;
|
|
4598
|
+
}
|
|
4599
|
+
async function teardownClaudeSetup({ homeRoot, dryRun }) {
|
|
4600
|
+
const settingsPath = path8.join(homeRoot, ".claude", "settings.json");
|
|
4601
|
+
let settings = {};
|
|
4602
|
+
try {
|
|
4603
|
+
settings = JSON.parse(await readFile8(settingsPath, "utf-8"));
|
|
4604
|
+
} catch (error) {
|
|
4605
|
+
if (error?.code !== "ENOENT")
|
|
4606
|
+
throw new Error(`failed to parse Claude settings: ${error.message ?? String(error)}`);
|
|
4607
|
+
return { runtime: "claude-code", ok: true, changed: false, dryRun, settingsPath, backup: null };
|
|
4608
|
+
}
|
|
4609
|
+
const before = JSON.stringify(settings);
|
|
4610
|
+
removeClaudeHooks(settings);
|
|
4611
|
+
const after = JSON.stringify(settings);
|
|
4612
|
+
const changed = before !== after;
|
|
4613
|
+
let backup = null;
|
|
4614
|
+
if (changed && !dryRun) {
|
|
4615
|
+
backup = await backupIfExists(settingsPath);
|
|
4616
|
+
await writeFile4(settingsPath, JSON.stringify(settings, null, 2), "utf-8");
|
|
4617
|
+
}
|
|
4618
|
+
return { runtime: "claude-code", ok: true, changed, dryRun, settingsPath, backup };
|
|
4619
|
+
}
|
|
4620
|
+
async function teardownCodexSetup({ homeRoot, dryRun }) {
|
|
4641
4621
|
const codexHome = path8.join(homeRoot, ".codex");
|
|
4642
|
-
const scriptPath = path8.join(codexHome, "bin", "spanory-codex-notify.sh");
|
|
4643
4622
|
const configPath = path8.join(codexHome, "config.toml");
|
|
4623
|
+
const scriptPath = path8.join(codexHome, "bin", "spanory-codex-notify.sh");
|
|
4644
4624
|
let currentConfig = "";
|
|
4645
4625
|
try {
|
|
4646
4626
|
currentConfig = await readFile8(configPath, "utf-8");
|
|
@@ -4660,26 +4640,75 @@ async function applyCodexWatchSetup({ homeRoot, dryRun }) {
|
|
|
4660
4640
|
let configBackup = null;
|
|
4661
4641
|
if (changed && !dryRun) {
|
|
4662
4642
|
if (configChanged) {
|
|
4663
|
-
await mkdir4(path8.dirname(configPath), { recursive: true });
|
|
4664
4643
|
configBackup = await backupIfExists(configPath);
|
|
4665
4644
|
await writeFile4(configPath, nextConfig, "utf-8");
|
|
4666
4645
|
}
|
|
4667
|
-
if (scriptPresent)
|
|
4646
|
+
if (scriptPresent)
|
|
4668
4647
|
await rm(scriptPath, { force: true });
|
|
4648
|
+
}
|
|
4649
|
+
return { runtime: "codex", ok: true, changed, dryRun, configPath, scriptPath, configBackup, scriptRemoved: scriptPresent };
|
|
4650
|
+
}
|
|
4651
|
+
async function teardownOpenclawSetup({ homeRoot, openclawRuntimeHome, dryRun }) {
|
|
4652
|
+
if (!commandExists("openclaw")) {
|
|
4653
|
+
return { runtime: "openclaw", ok: true, skipped: true, detail: "openclaw command not found in PATH" };
|
|
4654
|
+
}
|
|
4655
|
+
const runtimeHome = openclawRuntimeHomeForSetup(homeRoot, openclawRuntimeHome);
|
|
4656
|
+
if (dryRun)
|
|
4657
|
+
return { runtime: "openclaw", ok: true, changed: true, dryRun };
|
|
4658
|
+
const result = runSystemCommand("openclaw", ["plugins", "uninstall", OPENCLAW_SPANORY_PLUGIN_ID], {
|
|
4659
|
+
env: { ...process.env, ...runtimeHome ? { OPENCLAW_STATE_DIR: resolveRuntimeHome3("openclaw", runtimeHome) } : {} }
|
|
4660
|
+
});
|
|
4661
|
+
if (result.code !== 0)
|
|
4662
|
+
throw new Error(result.stderr || result.error || "openclaw plugins uninstall failed");
|
|
4663
|
+
return { runtime: "openclaw", ok: true, changed: true, dryRun };
|
|
4664
|
+
}
|
|
4665
|
+
async function teardownOpencodeSetup({ homeRoot, opencodeRuntimeHome, dryRun }) {
|
|
4666
|
+
const runtimeHome = opencodeRuntimeHomeForSetup(homeRoot, opencodeRuntimeHome);
|
|
4667
|
+
const loaderFile = opencodePluginLoaderPath(runtimeHome);
|
|
4668
|
+
let present = false;
|
|
4669
|
+
try {
|
|
4670
|
+
await stat4(loaderFile);
|
|
4671
|
+
present = true;
|
|
4672
|
+
} catch {
|
|
4673
|
+
}
|
|
4674
|
+
if (present && !dryRun)
|
|
4675
|
+
await rm(loaderFile, { force: true });
|
|
4676
|
+
return { runtime: "opencode", ok: true, changed: present, dryRun, loaderFile };
|
|
4677
|
+
}
|
|
4678
|
+
async function runSetupTeardown(options) {
|
|
4679
|
+
const homeRoot = setupHomeRoot(options.home);
|
|
4680
|
+
const selected = parseSetupRuntimes(options.runtimes);
|
|
4681
|
+
const dryRun = Boolean(options.dryRun);
|
|
4682
|
+
const results = [];
|
|
4683
|
+
if (selected.includes("claude-code")) {
|
|
4684
|
+
try {
|
|
4685
|
+
results.push(await teardownClaudeSetup({ homeRoot, dryRun }));
|
|
4686
|
+
} catch (error) {
|
|
4687
|
+
results.push({ runtime: "claude-code", ok: false, error: String(error?.message ?? error) });
|
|
4669
4688
|
}
|
|
4670
4689
|
}
|
|
4671
|
-
|
|
4672
|
-
|
|
4673
|
-
|
|
4674
|
-
|
|
4675
|
-
|
|
4676
|
-
|
|
4677
|
-
|
|
4678
|
-
|
|
4679
|
-
|
|
4680
|
-
|
|
4681
|
-
|
|
4682
|
-
|
|
4690
|
+
if (selected.includes("codex")) {
|
|
4691
|
+
try {
|
|
4692
|
+
results.push(await teardownCodexSetup({ homeRoot, dryRun }));
|
|
4693
|
+
} catch (error) {
|
|
4694
|
+
results.push({ runtime: "codex", ok: false, error: String(error?.message ?? error) });
|
|
4695
|
+
}
|
|
4696
|
+
}
|
|
4697
|
+
if (selected.includes("openclaw")) {
|
|
4698
|
+
try {
|
|
4699
|
+
results.push(await teardownOpenclawSetup({ homeRoot, openclawRuntimeHome: options.openclawRuntimeHome, dryRun }));
|
|
4700
|
+
} catch (error) {
|
|
4701
|
+
results.push({ runtime: "openclaw", ok: false, error: String(error?.message ?? error) });
|
|
4702
|
+
}
|
|
4703
|
+
}
|
|
4704
|
+
if (selected.includes("opencode")) {
|
|
4705
|
+
try {
|
|
4706
|
+
results.push(await teardownOpencodeSetup({ homeRoot, opencodeRuntimeHome: options.opencodeRuntimeHome, dryRun }));
|
|
4707
|
+
} catch (error) {
|
|
4708
|
+
results.push({ runtime: "opencode", ok: false, error: String(error?.message ?? error) });
|
|
4709
|
+
}
|
|
4710
|
+
}
|
|
4711
|
+
return { ok: results.every((r) => r.ok), results };
|
|
4683
4712
|
}
|
|
4684
4713
|
function commandExists(command) {
|
|
4685
4714
|
const result = runSystemCommand("which", [command], { env: process.env });
|
|
@@ -5034,7 +5063,6 @@ async function runSetupApply(options) {
|
|
|
5034
5063
|
const selected = parseSetupRuntimes(options.runtimes);
|
|
5035
5064
|
const spanoryBin = options.spanoryBin ?? "spanory";
|
|
5036
5065
|
const dryRun = Boolean(options.dryRun);
|
|
5037
|
-
const codexMode = options.codexMode ?? process.env.SPANORY_CODEX_MODE ?? "watch";
|
|
5038
5066
|
const results = [];
|
|
5039
5067
|
if (selected.includes("claude-code")) {
|
|
5040
5068
|
try {
|
|
@@ -5049,35 +5077,11 @@ async function runSetupApply(options) {
|
|
|
5049
5077
|
}
|
|
5050
5078
|
}
|
|
5051
5079
|
if (selected.includes("codex")) {
|
|
5052
|
-
|
|
5053
|
-
|
|
5054
|
-
|
|
5055
|
-
|
|
5056
|
-
|
|
5057
|
-
results.push({
|
|
5058
|
-
runtime: "codex",
|
|
5059
|
-
ok: false,
|
|
5060
|
-
error: String(error?.message ?? error)
|
|
5061
|
-
});
|
|
5062
|
-
}
|
|
5063
|
-
} else if (codexMode === "watch") {
|
|
5064
|
-
try {
|
|
5065
|
-
const result = await applyCodexWatchSetup({ homeRoot, dryRun });
|
|
5066
|
-
results.push(result);
|
|
5067
|
-
} catch (error) {
|
|
5068
|
-
results.push({
|
|
5069
|
-
runtime: "codex",
|
|
5070
|
-
ok: false,
|
|
5071
|
-
error: String(error?.message ?? error)
|
|
5072
|
-
});
|
|
5073
|
-
}
|
|
5074
|
-
} else {
|
|
5075
|
-
results.push({
|
|
5076
|
-
runtime: "codex",
|
|
5077
|
-
ok: true,
|
|
5078
|
-
skipped: true,
|
|
5079
|
-
detail: `codex mode "${codexMode}" has no setup action`
|
|
5080
|
-
});
|
|
5080
|
+
try {
|
|
5081
|
+
const result = await applyCodexWatchSetup({ homeRoot, dryRun });
|
|
5082
|
+
results.push(result);
|
|
5083
|
+
} catch (error) {
|
|
5084
|
+
results.push({ runtime: "codex", ok: false, error: String(error?.message ?? error) });
|
|
5081
5085
|
}
|
|
5082
5086
|
}
|
|
5083
5087
|
if (selected.includes("openclaw")) {
|
|
@@ -5460,6 +5464,12 @@ setup.command("doctor").description("Run setup diagnostics for selected runtimes
|
|
|
5460
5464
|
if (!report2.ok)
|
|
5461
5465
|
process.exitCode = 2;
|
|
5462
5466
|
});
|
|
5467
|
+
setup.command("teardown").description("Remove all Spanory integration from local runtimes").option("--runtimes <csv>", `Comma-separated runtimes (default: ${DEFAULT_SETUP_RUNTIMES.join(",")})`, DEFAULT_SETUP_RUNTIMES.join(",")).option("--home <path>", "Home directory root override (default: $HOME)").option("--openclaw-runtime-home <path>", "Override OpenClaw runtime home (default: ~/.openclaw)").option("--opencode-runtime-home <path>", "Override OpenCode runtime home (default: ~/.config/opencode)").option("--dry-run", "Only print planned changes without writing files", false).action(async (options) => {
|
|
5468
|
+
const report2 = await runSetupTeardown(options);
|
|
5469
|
+
console.log(JSON.stringify(report2, null, 2));
|
|
5470
|
+
if (!report2.ok)
|
|
5471
|
+
process.exitCode = 2;
|
|
5472
|
+
});
|
|
5463
5473
|
program.command("upgrade").description("Upgrade spanory CLI from npm registry").option("--dry-run", "Print upgrade command without executing", false).option("--manager <name>", "Package manager override: npm|tnpm").option("--scope <scope>", "Install scope override: global|local").action(async (options) => {
|
|
5464
5474
|
const manager = options.manager === "tnpm" ? "tnpm" : detectUpgradePackageManager();
|
|
5465
5475
|
const scope = options.scope === "local" ? "local" : options.scope === "global" ? "global" : detectUpgradeScope();
|