@bububuger/spanory 0.1.20 → 0.1.21
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 +182 -20
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -4542,6 +4542,14 @@ ${notifyLine}
|
|
|
4542
4542
|
function escapeTomlBasicString(value) {
|
|
4543
4543
|
return String(value).replaceAll("\\", "\\\\").replaceAll('"', '\\"');
|
|
4544
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
|
+
}
|
|
4545
4553
|
function codexNotifyScriptContent({ spanoryBin, codexHome, exportDir, logFile }) {
|
|
4546
4554
|
return `#!/usr/bin/env bash
|
|
4547
4555
|
set -euo pipefail
|
|
@@ -4629,6 +4637,50 @@ async function applyCodexSetup({ homeRoot, spanoryBin, dryRun }) {
|
|
|
4629
4637
|
configBackup
|
|
4630
4638
|
};
|
|
4631
4639
|
}
|
|
4640
|
+
async function applyCodexWatchSetup({ homeRoot, dryRun }) {
|
|
4641
|
+
const codexHome = path8.join(homeRoot, ".codex");
|
|
4642
|
+
const scriptPath = path8.join(codexHome, "bin", "spanory-codex-notify.sh");
|
|
4643
|
+
const configPath = path8.join(codexHome, "config.toml");
|
|
4644
|
+
let currentConfig = "";
|
|
4645
|
+
try {
|
|
4646
|
+
currentConfig = await readFile8(configPath, "utf-8");
|
|
4647
|
+
} catch (error) {
|
|
4648
|
+
if (error?.code !== "ENOENT")
|
|
4649
|
+
throw error;
|
|
4650
|
+
}
|
|
4651
|
+
const nextConfig = stripSpanoryCodexNotifyConfig(currentConfig);
|
|
4652
|
+
const configChanged = currentConfig !== nextConfig;
|
|
4653
|
+
let scriptPresent = false;
|
|
4654
|
+
try {
|
|
4655
|
+
await stat4(scriptPath);
|
|
4656
|
+
scriptPresent = true;
|
|
4657
|
+
} catch {
|
|
4658
|
+
}
|
|
4659
|
+
const changed = configChanged || scriptPresent;
|
|
4660
|
+
let configBackup = null;
|
|
4661
|
+
if (changed && !dryRun) {
|
|
4662
|
+
if (configChanged) {
|
|
4663
|
+
await mkdir4(path8.dirname(configPath), { recursive: true });
|
|
4664
|
+
configBackup = await backupIfExists(configPath);
|
|
4665
|
+
await writeFile4(configPath, nextConfig, "utf-8");
|
|
4666
|
+
}
|
|
4667
|
+
if (scriptPresent) {
|
|
4668
|
+
await rm(scriptPath, { force: true });
|
|
4669
|
+
}
|
|
4670
|
+
}
|
|
4671
|
+
return {
|
|
4672
|
+
runtime: "codex",
|
|
4673
|
+
ok: true,
|
|
4674
|
+
changed,
|
|
4675
|
+
dryRun,
|
|
4676
|
+
mode: "watch",
|
|
4677
|
+
configPath,
|
|
4678
|
+
scriptPath,
|
|
4679
|
+
configBackup,
|
|
4680
|
+
notifyConfigured: isSpanoryCodexNotifyConfigured(nextConfig),
|
|
4681
|
+
scriptPresent: false
|
|
4682
|
+
};
|
|
4683
|
+
}
|
|
4632
4684
|
function commandExists(command) {
|
|
4633
4685
|
const result = runSystemCommand("which", [command], { env: process.env });
|
|
4634
4686
|
return result.code === 0;
|
|
@@ -4671,7 +4723,102 @@ function openclawRuntimeHomeForSetup(homeRoot, explicitRuntimeHome) {
|
|
|
4671
4723
|
function opencodeRuntimeHomeForSetup(homeRoot, explicitRuntimeHome) {
|
|
4672
4724
|
return explicitRuntimeHome || path8.join(homeRoot, ".config", "opencode");
|
|
4673
4725
|
}
|
|
4674
|
-
function
|
|
4726
|
+
function resolveOpenclawConfigPath(runtimeHome) {
|
|
4727
|
+
return path8.join(runtimeHome, "openclaw.json");
|
|
4728
|
+
}
|
|
4729
|
+
function canonicalPath(input) {
|
|
4730
|
+
const resolved = path8.resolve(String(input ?? ""));
|
|
4731
|
+
try {
|
|
4732
|
+
return realpathSync(resolved);
|
|
4733
|
+
} catch {
|
|
4734
|
+
return resolved;
|
|
4735
|
+
}
|
|
4736
|
+
}
|
|
4737
|
+
function readOpenclawPluginNameFromDir(dirPath) {
|
|
4738
|
+
const pkgPath = path8.join(dirPath, "package.json");
|
|
4739
|
+
if (!existsSync(pkgPath))
|
|
4740
|
+
return null;
|
|
4741
|
+
try {
|
|
4742
|
+
const parsed = JSON.parse(readFileSync(pkgPath, "utf-8"));
|
|
4743
|
+
const name = String(parsed?.name ?? "").trim();
|
|
4744
|
+
return name || null;
|
|
4745
|
+
} catch {
|
|
4746
|
+
return null;
|
|
4747
|
+
}
|
|
4748
|
+
}
|
|
4749
|
+
function isSpanoryOpenclawPluginPath(candidatePath) {
|
|
4750
|
+
const text = String(candidatePath ?? "").trim();
|
|
4751
|
+
if (!text)
|
|
4752
|
+
return false;
|
|
4753
|
+
const normalized = text.replaceAll("\\", "/").toLowerCase();
|
|
4754
|
+
if (normalized.includes("spanory") && normalized.includes("openclaw-plugin")) {
|
|
4755
|
+
return true;
|
|
4756
|
+
}
|
|
4757
|
+
const canonical = canonicalPath(text);
|
|
4758
|
+
const pluginName = readOpenclawPluginNameFromDir(canonical);
|
|
4759
|
+
return pluginName === "@bububuger/spanory-openclaw-plugin" || pluginName === "@alipay/spanory-openclaw-plugin";
|
|
4760
|
+
}
|
|
4761
|
+
async function normalizeOpenclawPluginLoadPaths(runtimeHome, pluginDir, dryRun) {
|
|
4762
|
+
const configPath = resolveOpenclawConfigPath(runtimeHome);
|
|
4763
|
+
let configRaw = "";
|
|
4764
|
+
try {
|
|
4765
|
+
configRaw = await readFile8(configPath, "utf-8");
|
|
4766
|
+
} catch (error) {
|
|
4767
|
+
if (error?.code === "ENOENT")
|
|
4768
|
+
return { changed: false, configPath, backup: null };
|
|
4769
|
+
throw error;
|
|
4770
|
+
}
|
|
4771
|
+
let parsed;
|
|
4772
|
+
try {
|
|
4773
|
+
parsed = JSON.parse(configRaw);
|
|
4774
|
+
} catch (error) {
|
|
4775
|
+
throw new Error(`failed to parse openclaw config ${configPath}: ${error?.message ?? String(error)}`);
|
|
4776
|
+
}
|
|
4777
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
4778
|
+
parsed = {};
|
|
4779
|
+
}
|
|
4780
|
+
if (!parsed.plugins || typeof parsed.plugins !== "object" || Array.isArray(parsed.plugins)) {
|
|
4781
|
+
parsed.plugins = {};
|
|
4782
|
+
}
|
|
4783
|
+
if (!parsed.plugins.load || typeof parsed.plugins.load !== "object" || Array.isArray(parsed.plugins.load)) {
|
|
4784
|
+
parsed.plugins.load = {};
|
|
4785
|
+
}
|
|
4786
|
+
const target = path8.resolve(pluginDir);
|
|
4787
|
+
const targetCanonical = canonicalPath(target);
|
|
4788
|
+
const currentPaths = Array.isArray(parsed.plugins.load.paths) ? parsed.plugins.load.paths : [];
|
|
4789
|
+
const nonSpanoryPaths = [];
|
|
4790
|
+
const seen = /* @__PURE__ */ new Set();
|
|
4791
|
+
for (const item of currentPaths) {
|
|
4792
|
+
if (typeof item !== "string")
|
|
4793
|
+
continue;
|
|
4794
|
+
const text = item.trim();
|
|
4795
|
+
if (!text)
|
|
4796
|
+
continue;
|
|
4797
|
+
if (isSpanoryOpenclawPluginPath(text))
|
|
4798
|
+
continue;
|
|
4799
|
+
const key = canonicalPath(text);
|
|
4800
|
+
if (seen.has(key))
|
|
4801
|
+
continue;
|
|
4802
|
+
seen.add(key);
|
|
4803
|
+
nonSpanoryPaths.push(text);
|
|
4804
|
+
}
|
|
4805
|
+
if (!seen.has(targetCanonical)) {
|
|
4806
|
+
nonSpanoryPaths.push(target);
|
|
4807
|
+
}
|
|
4808
|
+
parsed.plugins.load.paths = nonSpanoryPaths;
|
|
4809
|
+
const nextRaw = `${JSON.stringify(parsed, null, 2)}
|
|
4810
|
+
`;
|
|
4811
|
+
if (nextRaw === configRaw) {
|
|
4812
|
+
return { changed: false, configPath, backup: null };
|
|
4813
|
+
}
|
|
4814
|
+
let backup = null;
|
|
4815
|
+
if (!dryRun) {
|
|
4816
|
+
backup = await backupIfExists(configPath);
|
|
4817
|
+
await writeFile4(configPath, nextRaw, "utf-8");
|
|
4818
|
+
}
|
|
4819
|
+
return { changed: true, configPath, backup };
|
|
4820
|
+
}
|
|
4821
|
+
async function installOpenclawPlugin(runtimeHome, dryRun) {
|
|
4675
4822
|
const pluginDir = path8.resolve(resolveOpenclawPluginDir());
|
|
4676
4823
|
const installResult = runSystemCommand("openclaw", ["plugins", "install", "-l", pluginDir], {
|
|
4677
4824
|
env: {
|
|
@@ -4691,9 +4838,12 @@ function installOpenclawPlugin(runtimeHome) {
|
|
|
4691
4838
|
if (enableResult.code !== 0) {
|
|
4692
4839
|
throw new Error(enableResult.stderr || enableResult.error || "openclaw plugins enable failed");
|
|
4693
4840
|
}
|
|
4841
|
+
const pathNormalizeResult = await normalizeOpenclawPluginLoadPaths(runtimeHome, pluginDir, dryRun);
|
|
4694
4842
|
return {
|
|
4843
|
+
pluginDir,
|
|
4695
4844
|
installStdout: installResult.stdout.trim(),
|
|
4696
|
-
enableStdout: enableResult.stdout.trim()
|
|
4845
|
+
enableStdout: enableResult.stdout.trim(),
|
|
4846
|
+
pathNormalizeResult
|
|
4697
4847
|
};
|
|
4698
4848
|
}
|
|
4699
4849
|
async function installOpencodePlugin(runtimeHome, pluginDirOverride) {
|
|
@@ -4749,7 +4899,7 @@ async function runSetupDetect(options) {
|
|
|
4749
4899
|
let codexNotifyConfigured = false;
|
|
4750
4900
|
try {
|
|
4751
4901
|
const config = await readFile8(codexConfigPath, "utf-8");
|
|
4752
|
-
codexNotifyConfigured =
|
|
4902
|
+
codexNotifyConfigured = isSpanoryCodexNotifyConfigured(config);
|
|
4753
4903
|
} catch {
|
|
4754
4904
|
}
|
|
4755
4905
|
let codexScriptPresent = true;
|
|
@@ -4761,10 +4911,11 @@ async function runSetupDetect(options) {
|
|
|
4761
4911
|
report2.runtimes.push({
|
|
4762
4912
|
runtime: "codex",
|
|
4763
4913
|
available: commandExists("codex"),
|
|
4764
|
-
configured: codexNotifyConfigured
|
|
4914
|
+
configured: !codexNotifyConfigured,
|
|
4765
4915
|
details: {
|
|
4766
4916
|
configPath: codexConfigPath,
|
|
4767
4917
|
scriptPath: codexScriptPath,
|
|
4918
|
+
mode: "watch",
|
|
4768
4919
|
notifyConfigured: codexNotifyConfigured,
|
|
4769
4920
|
scriptPresent: codexScriptPresent
|
|
4770
4921
|
}
|
|
@@ -4822,7 +4973,7 @@ async function runSetupDoctor(options) {
|
|
|
4822
4973
|
let notifyConfigured = false;
|
|
4823
4974
|
try {
|
|
4824
4975
|
const config = await readFile8(configPath, "utf-8");
|
|
4825
|
-
notifyConfigured =
|
|
4976
|
+
notifyConfigured = isSpanoryCodexNotifyConfigured(config);
|
|
4826
4977
|
} catch {
|
|
4827
4978
|
}
|
|
4828
4979
|
let scriptPresent = false;
|
|
@@ -4832,15 +4983,15 @@ async function runSetupDoctor(options) {
|
|
|
4832
4983
|
} catch {
|
|
4833
4984
|
}
|
|
4834
4985
|
checks.push({
|
|
4835
|
-
id: "
|
|
4986
|
+
id: "codex_watch_mode",
|
|
4836
4987
|
runtime: "codex",
|
|
4837
|
-
ok: notifyConfigured,
|
|
4988
|
+
ok: !notifyConfigured,
|
|
4838
4989
|
detail: configPath
|
|
4839
4990
|
});
|
|
4840
4991
|
checks.push({
|
|
4841
|
-
id: "
|
|
4992
|
+
id: "codex_notify_script_absent",
|
|
4842
4993
|
runtime: "codex",
|
|
4843
|
-
ok: scriptPresent,
|
|
4994
|
+
ok: !scriptPresent,
|
|
4844
4995
|
detail: scriptPath
|
|
4845
4996
|
});
|
|
4846
4997
|
}
|
|
@@ -4883,7 +5034,7 @@ async function runSetupApply(options) {
|
|
|
4883
5034
|
const selected = parseSetupRuntimes(options.runtimes);
|
|
4884
5035
|
const spanoryBin = options.spanoryBin ?? "spanory";
|
|
4885
5036
|
const dryRun = Boolean(options.dryRun);
|
|
4886
|
-
const codexMode = options.codexMode ?? "
|
|
5037
|
+
const codexMode = options.codexMode ?? process.env.SPANORY_CODEX_MODE ?? "watch";
|
|
4887
5038
|
const results = [];
|
|
4888
5039
|
if (selected.includes("claude-code")) {
|
|
4889
5040
|
try {
|
|
@@ -4898,14 +5049,7 @@ async function runSetupApply(options) {
|
|
|
4898
5049
|
}
|
|
4899
5050
|
}
|
|
4900
5051
|
if (selected.includes("codex")) {
|
|
4901
|
-
if (codexMode
|
|
4902
|
-
results.push({
|
|
4903
|
-
runtime: "codex",
|
|
4904
|
-
ok: true,
|
|
4905
|
-
skipped: true,
|
|
4906
|
-
detail: `codex mode "${codexMode}" skips notify setup`
|
|
4907
|
-
});
|
|
4908
|
-
} else {
|
|
5052
|
+
if (codexMode === "notify") {
|
|
4909
5053
|
try {
|
|
4910
5054
|
const result = await applyCodexSetup({ homeRoot, spanoryBin, dryRun });
|
|
4911
5055
|
results.push(result);
|
|
@@ -4916,6 +5060,24 @@ async function runSetupApply(options) {
|
|
|
4916
5060
|
error: String(error?.message ?? error)
|
|
4917
5061
|
});
|
|
4918
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
|
+
});
|
|
4919
5081
|
}
|
|
4920
5082
|
}
|
|
4921
5083
|
if (selected.includes("openclaw")) {
|
|
@@ -4930,7 +5092,7 @@ async function runSetupApply(options) {
|
|
|
4930
5092
|
const runtimeHome = openclawRuntimeHomeForSetup(homeRoot, options.openclawRuntimeHome);
|
|
4931
5093
|
try {
|
|
4932
5094
|
if (!dryRun)
|
|
4933
|
-
installOpenclawPlugin(runtimeHome);
|
|
5095
|
+
await installOpenclawPlugin(runtimeHome, dryRun);
|
|
4934
5096
|
const doctor = await runOpenclawPluginDoctor(runtimeHome);
|
|
4935
5097
|
results.push({
|
|
4936
5098
|
runtime: "openclaw",
|
|
@@ -5286,7 +5448,7 @@ setup.command("detect").description("Detect local runtime availability and setup
|
|
|
5286
5448
|
const report2 = await runSetupDetect(options);
|
|
5287
5449
|
console.log(JSON.stringify(report2, null, 2));
|
|
5288
5450
|
});
|
|
5289
|
-
setup.command("apply").description("Apply idempotent local setup for selected 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("--spanory-bin <path>", "Spanory binary/command to write into runtime configs", "spanory").option("--
|
|
5451
|
+
setup.command("apply").description("Apply idempotent local setup for selected 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("--spanory-bin <path>", "Spanory binary/command to write into runtime configs", "spanory").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) => {
|
|
5290
5452
|
const report2 = await runSetupApply(options);
|
|
5291
5453
|
console.log(JSON.stringify(report2, null, 2));
|
|
5292
5454
|
if (!report2.ok)
|