@n1creator/openacp-cli 2026.712.11 → 2026.713.1
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/README.md +126 -329
- package/dist/cli.js +3041 -1626
- package/dist/cli.js.map +1 -1
- package/dist/data/plugin-catalog.json +7 -0
- package/dist/index.d.ts +16 -1
- package/dist/index.js +1579 -517
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -291,12 +291,12 @@ function createSessionLogger(sessionId, parentLogger) {
|
|
|
291
291
|
async function closeSessionLogger(logger) {
|
|
292
292
|
const dest = logger.__sessionDest;
|
|
293
293
|
if (!dest) return;
|
|
294
|
-
await new Promise((
|
|
294
|
+
await new Promise((resolve8) => {
|
|
295
295
|
let settled = false;
|
|
296
296
|
const done = () => {
|
|
297
297
|
if (!settled) {
|
|
298
298
|
settled = true;
|
|
299
|
-
|
|
299
|
+
resolve8();
|
|
300
300
|
}
|
|
301
301
|
};
|
|
302
302
|
const timeout = setTimeout(done, 3e3);
|
|
@@ -321,11 +321,11 @@ async function shutdownLogger() {
|
|
|
321
321
|
logDir = void 0;
|
|
322
322
|
initialized = false;
|
|
323
323
|
if (transport) {
|
|
324
|
-
await new Promise((
|
|
325
|
-
const timeout = setTimeout(
|
|
324
|
+
await new Promise((resolve8) => {
|
|
325
|
+
const timeout = setTimeout(resolve8, 3e3);
|
|
326
326
|
transport.on("close", () => {
|
|
327
327
|
clearTimeout(timeout);
|
|
328
|
-
|
|
328
|
+
resolve8();
|
|
329
329
|
});
|
|
330
330
|
transport.end();
|
|
331
331
|
});
|
|
@@ -458,22 +458,22 @@ __export(config_registry_exports, {
|
|
|
458
458
|
resolveOptions: () => resolveOptions,
|
|
459
459
|
setFieldValueAsync: () => setFieldValueAsync
|
|
460
460
|
});
|
|
461
|
-
function getFieldDef(
|
|
462
|
-
return CONFIG_REGISTRY.find((f) => f.path ===
|
|
461
|
+
function getFieldDef(path39) {
|
|
462
|
+
return CONFIG_REGISTRY.find((f) => f.path === path39);
|
|
463
463
|
}
|
|
464
464
|
function getSafeFields() {
|
|
465
465
|
return CONFIG_REGISTRY.filter((f) => f.scope === "safe");
|
|
466
466
|
}
|
|
467
|
-
function isHotReloadable(
|
|
468
|
-
const def = getFieldDef(
|
|
467
|
+
function isHotReloadable(path39) {
|
|
468
|
+
const def = getFieldDef(path39);
|
|
469
469
|
return def?.hotReload ?? false;
|
|
470
470
|
}
|
|
471
471
|
function resolveOptions(def, config) {
|
|
472
472
|
if (!def.options) return void 0;
|
|
473
473
|
return typeof def.options === "function" ? def.options(config) : def.options;
|
|
474
474
|
}
|
|
475
|
-
function getConfigValue(config,
|
|
476
|
-
const parts =
|
|
475
|
+
function getConfigValue(config, path39) {
|
|
476
|
+
const parts = path39.split(".");
|
|
477
477
|
let current = config;
|
|
478
478
|
for (const part of parts) {
|
|
479
479
|
if (current && typeof current === "object" && part in current) {
|
|
@@ -1682,6 +1682,996 @@ var init_agent_installer = __esm({
|
|
|
1682
1682
|
}
|
|
1683
1683
|
});
|
|
1684
1684
|
|
|
1685
|
+
// src/core/plugin/plugin-installer.ts
|
|
1686
|
+
var plugin_installer_exports = {};
|
|
1687
|
+
__export(plugin_installer_exports, {
|
|
1688
|
+
PluginInstallCrashSimulation: () => PluginInstallCrashSimulation,
|
|
1689
|
+
PluginInstallError: () => PluginInstallError,
|
|
1690
|
+
PluginInstallJournalController: () => PluginInstallJournalController,
|
|
1691
|
+
acquirePluginInstallLock: () => acquirePluginInstallLock,
|
|
1692
|
+
importFromDir: () => importFromDir,
|
|
1693
|
+
installNpmPlugin: () => installNpmPlugin,
|
|
1694
|
+
parseNpmPackageSpec: () => parseNpmPackageSpec,
|
|
1695
|
+
pluginMutationLockHeld: () => pluginMutationLockHeld,
|
|
1696
|
+
recoverPluginInstallTransaction: () => recoverPluginInstallTransaction,
|
|
1697
|
+
stageNpmPlugin: () => stageNpmPlugin,
|
|
1698
|
+
validateOpenACPPluginModule: () => validateOpenACPPluginModule,
|
|
1699
|
+
withHeldPluginMutationLock: () => withHeldPluginMutationLock,
|
|
1700
|
+
withPluginMutationLock: () => withPluginMutationLock
|
|
1701
|
+
});
|
|
1702
|
+
import { execFile } from "child_process";
|
|
1703
|
+
import { promisify } from "util";
|
|
1704
|
+
import * as fs15 from "fs/promises";
|
|
1705
|
+
import fsSync from "fs";
|
|
1706
|
+
import * as path14 from "path";
|
|
1707
|
+
import { pathToFileURL } from "url";
|
|
1708
|
+
import { createHash as createHash2, randomUUID as randomUUID2 } from "crypto";
|
|
1709
|
+
import { AsyncLocalStorage } from "async_hooks";
|
|
1710
|
+
async function importFromDir(packageName, dir) {
|
|
1711
|
+
const pkgDir = path14.join(dir, "node_modules", ...packageName.split("/"));
|
|
1712
|
+
const pkgJsonPath = path14.join(pkgDir, "package.json");
|
|
1713
|
+
let pkgJson;
|
|
1714
|
+
try {
|
|
1715
|
+
pkgJson = JSON.parse(await fs15.readFile(pkgJsonPath, "utf-8"));
|
|
1716
|
+
} catch (err) {
|
|
1717
|
+
throw new Error(`Cannot read package.json for "${packageName}" at ${pkgJsonPath}: ${err.message}`);
|
|
1718
|
+
}
|
|
1719
|
+
let entry;
|
|
1720
|
+
const exportsMain = pkgJson.exports?.["."];
|
|
1721
|
+
if (typeof exportsMain === "string") {
|
|
1722
|
+
entry = exportsMain;
|
|
1723
|
+
} else if (exportsMain?.import) {
|
|
1724
|
+
entry = exportsMain.import;
|
|
1725
|
+
} else {
|
|
1726
|
+
entry = pkgJson.main ?? "index.js";
|
|
1727
|
+
}
|
|
1728
|
+
const entryPath = path14.join(pkgDir, entry);
|
|
1729
|
+
try {
|
|
1730
|
+
await fs15.access(entryPath);
|
|
1731
|
+
} catch {
|
|
1732
|
+
throw new Error(`Entry point "${entry}" not found for "${packageName}" at ${entryPath}`);
|
|
1733
|
+
}
|
|
1734
|
+
return import(pathToFileURL(entryPath).href);
|
|
1735
|
+
}
|
|
1736
|
+
function parseNpmPackageSpec(spec) {
|
|
1737
|
+
if (!VALID_NPM_NAME.test(spec)) throw new PluginInstallError("PLUGIN_PACKAGE_SPEC_INVALID", "Plugin package spec is invalid.");
|
|
1738
|
+
if (spec.startsWith("@")) {
|
|
1739
|
+
const slash = spec.indexOf("/");
|
|
1740
|
+
const versionAt2 = spec.indexOf("@", slash + 1);
|
|
1741
|
+
return { packageName: versionAt2 === -1 ? spec : spec.slice(0, versionAt2), spec };
|
|
1742
|
+
}
|
|
1743
|
+
const versionAt = spec.lastIndexOf("@");
|
|
1744
|
+
return { packageName: versionAt > 0 ? spec.slice(0, versionAt) : spec, spec };
|
|
1745
|
+
}
|
|
1746
|
+
function validateOpenACPPluginModule(module, manifest, packageName) {
|
|
1747
|
+
const plugin = module?.default;
|
|
1748
|
+
if (!plugin || typeof plugin !== "object" || plugin.name !== packageName || manifest.name !== packageName || typeof manifest.version !== "string" || plugin.version !== manifest.version || typeof plugin.setup !== "function" || typeof plugin.install !== "function") {
|
|
1749
|
+
throw new PluginInstallError(
|
|
1750
|
+
"PLUGIN_CONTRACT_INVALID",
|
|
1751
|
+
"Package is not an installable OpenACP plugin: default export, matching name/version, setup(), and explicit install() are required."
|
|
1752
|
+
);
|
|
1753
|
+
}
|
|
1754
|
+
return plugin;
|
|
1755
|
+
}
|
|
1756
|
+
function assertDirectoryBoundary(target, label) {
|
|
1757
|
+
const stat = fsSync.lstatSync(target);
|
|
1758
|
+
if (stat.isSymbolicLink() || !stat.isDirectory() || fsSync.realpathSync(target) !== path14.resolve(target)) {
|
|
1759
|
+
throw new PluginInstallError("PLUGIN_RECOVERY_FAILED", `${label} must be a real directory without symbolic-link ancestors.`);
|
|
1760
|
+
}
|
|
1761
|
+
}
|
|
1762
|
+
function assertInstanceBoundaries(instanceRoot) {
|
|
1763
|
+
assertDirectoryBoundary(instanceRoot, "Plugin instance root");
|
|
1764
|
+
const pluginsDir = path14.join(instanceRoot, "plugins");
|
|
1765
|
+
if (fsSync.existsSync(pluginsDir)) assertDirectoryBoundary(pluginsDir, "Plugin package root");
|
|
1766
|
+
}
|
|
1767
|
+
function assertContainedPath(instanceRoot, target, label) {
|
|
1768
|
+
const root = path14.resolve(instanceRoot);
|
|
1769
|
+
const resolved = path14.resolve(target);
|
|
1770
|
+
const relative2 = path14.relative(root, resolved);
|
|
1771
|
+
if (relative2.startsWith("..") || path14.isAbsolute(relative2)) throw new PluginInstallError("PLUGIN_RECOVERY_FAILED", `${label} escapes the trusted instance root.`);
|
|
1772
|
+
let current = root;
|
|
1773
|
+
for (const segment of relative2 ? relative2.split(path14.sep) : []) {
|
|
1774
|
+
current = path14.join(current, segment);
|
|
1775
|
+
if (!fsSync.existsSync(current)) break;
|
|
1776
|
+
const stat = fsSync.lstatSync(current);
|
|
1777
|
+
if (stat.isSymbolicLink()) throw new PluginInstallError("PLUGIN_RECOVERY_FAILED", `${label} contains a symbolic-link boundary.`);
|
|
1778
|
+
const real = fsSync.realpathSync(current);
|
|
1779
|
+
if (real !== root && !real.startsWith(`${root}${path14.sep}`)) throw new PluginInstallError("PLUGIN_RECOVERY_FAILED", `${label} resolves outside the trusted instance root.`);
|
|
1780
|
+
}
|
|
1781
|
+
}
|
|
1782
|
+
function atomicJson(file, value) {
|
|
1783
|
+
const directory = path14.dirname(file);
|
|
1784
|
+
fsSync.mkdirSync(directory, { recursive: true, mode: 448 });
|
|
1785
|
+
const temporary = `${file}.${process.pid}.${randomUUID2()}.tmp`;
|
|
1786
|
+
try {
|
|
1787
|
+
fsSync.writeFileSync(temporary, `${JSON.stringify(value, null, 2)}
|
|
1788
|
+
`, { flag: "wx", mode: 384 });
|
|
1789
|
+
fsSync.chmodSync(temporary, 384);
|
|
1790
|
+
const fd = fsSync.openSync(temporary, "r");
|
|
1791
|
+
fsSync.fsyncSync(fd);
|
|
1792
|
+
fsSync.closeSync(fd);
|
|
1793
|
+
fsSync.renameSync(temporary, file);
|
|
1794
|
+
fsSync.chmodSync(file, 384);
|
|
1795
|
+
try {
|
|
1796
|
+
const dir = fsSync.openSync(directory, "r");
|
|
1797
|
+
fsSync.fsyncSync(dir);
|
|
1798
|
+
fsSync.closeSync(dir);
|
|
1799
|
+
} catch {
|
|
1800
|
+
}
|
|
1801
|
+
} finally {
|
|
1802
|
+
try {
|
|
1803
|
+
if (fsSync.existsSync(temporary)) fsSync.unlinkSync(temporary);
|
|
1804
|
+
} catch {
|
|
1805
|
+
}
|
|
1806
|
+
}
|
|
1807
|
+
}
|
|
1808
|
+
function processAlive(pid) {
|
|
1809
|
+
try {
|
|
1810
|
+
process.kill(pid, 0);
|
|
1811
|
+
return true;
|
|
1812
|
+
} catch (error) {
|
|
1813
|
+
return error.code !== "ESRCH";
|
|
1814
|
+
}
|
|
1815
|
+
}
|
|
1816
|
+
async function acquirePluginInstallLock(instanceRoot, options = {}) {
|
|
1817
|
+
const lockPath = path14.join(instanceRoot, LOCK_FILE);
|
|
1818
|
+
const reclaimPrefix = `${LOCK_FILE}.reclaim-`;
|
|
1819
|
+
const timeoutMs = options.timeoutMs ?? 5e3;
|
|
1820
|
+
const staleMs = options.staleMs ?? 3e4;
|
|
1821
|
+
const transactionId = options.transactionId ?? randomUUID2();
|
|
1822
|
+
fsSync.mkdirSync(instanceRoot, { recursive: true, mode: 448 });
|
|
1823
|
+
const createOwnedLock = () => {
|
|
1824
|
+
const fd = fsSync.openSync(lockPath, "wx", 384);
|
|
1825
|
+
try {
|
|
1826
|
+
fsSync.writeFileSync(fd, JSON.stringify({ version: 1, transactionId, pid: process.pid, createdAt: Date.now() }));
|
|
1827
|
+
fsSync.fsyncSync(fd);
|
|
1828
|
+
} finally {
|
|
1829
|
+
fsSync.closeSync(fd);
|
|
1830
|
+
}
|
|
1831
|
+
fsSync.chmodSync(lockPath, 384);
|
|
1832
|
+
let released = false;
|
|
1833
|
+
return { transactionId, release: () => {
|
|
1834
|
+
if (released) return;
|
|
1835
|
+
released = true;
|
|
1836
|
+
try {
|
|
1837
|
+
const owner = JSON.parse(fsSync.readFileSync(lockPath, "utf8"));
|
|
1838
|
+
if (owner.transactionId === transactionId) fsSync.unlinkSync(lockPath);
|
|
1839
|
+
} catch {
|
|
1840
|
+
}
|
|
1841
|
+
} };
|
|
1842
|
+
};
|
|
1843
|
+
const started = Date.now();
|
|
1844
|
+
while (Date.now() - started <= timeoutMs) {
|
|
1845
|
+
try {
|
|
1846
|
+
const reclaims = fsSync.readdirSync(instanceRoot).filter((name) => name.startsWith(reclaimPrefix));
|
|
1847
|
+
for (const name of reclaims) {
|
|
1848
|
+
const ownerId = name.slice(reclaimPrefix.length);
|
|
1849
|
+
const reclaimPath = path14.join(instanceRoot, name);
|
|
1850
|
+
const stat = fsSync.lstatSync(reclaimPath);
|
|
1851
|
+
if (!TRANSACTION_ID.test(ownerId) || stat.isSymbolicLink() || !stat.isFile() || stat.size > 4096) {
|
|
1852
|
+
throw new PluginInstallError("PLUGIN_INSTALL_BUSY", "Malformed stale-lock reclaim evidence requires operator inspection.");
|
|
1853
|
+
}
|
|
1854
|
+
const owner = JSON.parse(fsSync.readFileSync(reclaimPath, "utf8"));
|
|
1855
|
+
const reclaimable = owner.transactionId === ownerId && Number.isInteger(owner.pid) && Number.isFinite(owner.createdAt) && Date.now() - Number(owner.createdAt) > staleMs && !processAlive(Number(owner.pid));
|
|
1856
|
+
if (!reclaimable) throw new PluginInstallError("PLUGIN_INSTALL_BUSY", "A plugin lock reclaim is active or cannot be proven stale.");
|
|
1857
|
+
if (!fsSync.existsSync(lockPath)) {
|
|
1858
|
+
const lock = createOwnedLock();
|
|
1859
|
+
fsSync.unlinkSync(reclaimPath);
|
|
1860
|
+
return lock;
|
|
1861
|
+
}
|
|
1862
|
+
fsSync.unlinkSync(reclaimPath);
|
|
1863
|
+
}
|
|
1864
|
+
return createOwnedLock();
|
|
1865
|
+
} catch (error) {
|
|
1866
|
+
if (error instanceof PluginInstallError) throw error;
|
|
1867
|
+
if (error.code !== "EEXIST") throw error;
|
|
1868
|
+
try {
|
|
1869
|
+
const owner = JSON.parse(fsSync.readFileSync(lockPath, "utf8"));
|
|
1870
|
+
const oldEnough = TRANSACTION_ID.test(owner.transactionId ?? "") && Number.isFinite(owner.createdAt) && Date.now() - Number(owner.createdAt) > staleMs;
|
|
1871
|
+
const definitelyDead = Number.isInteger(owner.pid) && !processAlive(Number(owner.pid));
|
|
1872
|
+
if (oldEnough && definitelyDead) {
|
|
1873
|
+
const reclaimPath = path14.join(instanceRoot, `${reclaimPrefix}${owner.transactionId}`);
|
|
1874
|
+
try {
|
|
1875
|
+
if (fsSync.existsSync(reclaimPath)) continue;
|
|
1876
|
+
fsSync.renameSync(lockPath, reclaimPath);
|
|
1877
|
+
const renamed = JSON.parse(fsSync.readFileSync(reclaimPath, "utf8"));
|
|
1878
|
+
if (renamed.transactionId !== owner.transactionId || renamed.pid !== owner.pid || renamed.createdAt !== owner.createdAt) {
|
|
1879
|
+
if (!fsSync.existsSync(lockPath)) fsSync.renameSync(reclaimPath, lockPath);
|
|
1880
|
+
continue;
|
|
1881
|
+
}
|
|
1882
|
+
} catch (reclaimError) {
|
|
1883
|
+
if (reclaimError.code !== "ENOENT") throw reclaimError;
|
|
1884
|
+
}
|
|
1885
|
+
continue;
|
|
1886
|
+
}
|
|
1887
|
+
} catch {
|
|
1888
|
+
}
|
|
1889
|
+
await new Promise((resolve8) => setTimeout(resolve8, 20));
|
|
1890
|
+
}
|
|
1891
|
+
}
|
|
1892
|
+
throw new PluginInstallError("PLUGIN_INSTALL_BUSY", "Another plugin package transaction owns the install lock; retry after it finishes.");
|
|
1893
|
+
}
|
|
1894
|
+
function treeRecords(root, excludeMarker = true) {
|
|
1895
|
+
if (!fsSync.existsSync(root)) return [];
|
|
1896
|
+
const rootStat = fsSync.lstatSync(root);
|
|
1897
|
+
if (rootStat.isSymbolicLink() || !rootStat.isDirectory()) throw new PluginInstallError("PLUGIN_SNAPSHOT_FAILED", "Plugin data snapshot root must be a real directory.");
|
|
1898
|
+
const records = [];
|
|
1899
|
+
const walk = (directory, relative2 = "") => {
|
|
1900
|
+
for (const entry of fsSync.readdirSync(directory, { withFileTypes: true }).sort((a, b) => a.name.localeCompare(b.name))) {
|
|
1901
|
+
if (excludeMarker && !relative2 && entry.name === ".snapshot-complete.json") continue;
|
|
1902
|
+
const childRelative = path14.join(relative2, entry.name);
|
|
1903
|
+
const child = path14.join(directory, entry.name);
|
|
1904
|
+
const stat = fsSync.lstatSync(child);
|
|
1905
|
+
if (entry.isSymbolicLink()) throw new PluginInstallError("PLUGIN_SNAPSHOT_FAILED", "Plugin data snapshot refuses symbolic links.");
|
|
1906
|
+
if (entry.isDirectory()) {
|
|
1907
|
+
records.push({ path: childRelative, mode: stat.mode & 511, type: "directory" });
|
|
1908
|
+
walk(child, childRelative);
|
|
1909
|
+
} else if (entry.isFile()) records.push({ path: childRelative, mode: stat.mode & 511, type: "file", sha256: createHash2("sha256").update(fsSync.readFileSync(child)).digest("hex") });
|
|
1910
|
+
else throw new PluginInstallError("PLUGIN_SNAPSHOT_FAILED", "Plugin data snapshot contains an unsupported file type.");
|
|
1911
|
+
}
|
|
1912
|
+
};
|
|
1913
|
+
walk(root);
|
|
1914
|
+
return records;
|
|
1915
|
+
}
|
|
1916
|
+
function treeDigest(root) {
|
|
1917
|
+
return createHash2("sha256").update(JSON.stringify({
|
|
1918
|
+
records: treeRecords(root).map(({ path: recordPath, type, sha256 }) => ({ path: recordPath, type, sha256 }))
|
|
1919
|
+
})).digest("hex");
|
|
1920
|
+
}
|
|
1921
|
+
function treeModeDigest(root, excludeMarker = false) {
|
|
1922
|
+
const records = treeRecords(root, excludeMarker);
|
|
1923
|
+
return createHash2("sha256").update(JSON.stringify({ rootMode: fsSync.lstatSync(root).mode & 511, records })).digest("hex");
|
|
1924
|
+
}
|
|
1925
|
+
function boundaryDigest(target) {
|
|
1926
|
+
const records = [];
|
|
1927
|
+
const walk = (current, relative2) => {
|
|
1928
|
+
const stat = fsSync.lstatSync(current);
|
|
1929
|
+
if (stat.isSymbolicLink()) records.push({ path: relative2, type: "symlink", mode: stat.mode & 511, link: fsSync.readlinkSync(current) });
|
|
1930
|
+
else if (stat.isFile()) records.push({ path: relative2, type: "file", mode: stat.mode & 511, sha256: createHash2("sha256").update(fsSync.readFileSync(current)).digest("hex") });
|
|
1931
|
+
else if (stat.isDirectory()) {
|
|
1932
|
+
records.push({ path: relative2, type: "directory", mode: stat.mode & 511 });
|
|
1933
|
+
for (const name of fsSync.readdirSync(current).sort()) walk(path14.join(current, name), relative2 ? path14.join(relative2, name) : name);
|
|
1934
|
+
} else throw new PluginInstallError("PLUGIN_RECOVERY_FAILED", "Plugin package evidence contains an unsupported boundary type.");
|
|
1935
|
+
};
|
|
1936
|
+
walk(target, "");
|
|
1937
|
+
return createHash2("sha256").update(JSON.stringify(records)).digest("hex");
|
|
1938
|
+
}
|
|
1939
|
+
function fsyncBoundary(target) {
|
|
1940
|
+
const stat = fsSync.lstatSync(target);
|
|
1941
|
+
if (stat.isFile()) {
|
|
1942
|
+
const fd = fsSync.openSync(target, "r");
|
|
1943
|
+
fsSync.fsyncSync(fd);
|
|
1944
|
+
fsSync.closeSync(fd);
|
|
1945
|
+
return;
|
|
1946
|
+
}
|
|
1947
|
+
if (stat.isSymbolicLink()) return;
|
|
1948
|
+
for (const name of fsSync.readdirSync(target)) fsyncBoundary(path14.join(target, name));
|
|
1949
|
+
try {
|
|
1950
|
+
const fd = fsSync.openSync(target, "r");
|
|
1951
|
+
fsSync.fsyncSync(fd);
|
|
1952
|
+
fsSync.closeSync(fd);
|
|
1953
|
+
} catch {
|
|
1954
|
+
}
|
|
1955
|
+
}
|
|
1956
|
+
function fsyncDirectory(directory) {
|
|
1957
|
+
try {
|
|
1958
|
+
const fd = fsSync.openSync(directory, "r");
|
|
1959
|
+
fsSync.fsyncSync(fd);
|
|
1960
|
+
fsSync.closeSync(fd);
|
|
1961
|
+
} catch {
|
|
1962
|
+
}
|
|
1963
|
+
}
|
|
1964
|
+
function secureSnapshotTree(root) {
|
|
1965
|
+
fsSync.chmodSync(root, 448);
|
|
1966
|
+
for (const record of treeRecords(root)) fsSync.chmodSync(path14.join(root, record.path), record.type === "directory" ? 448 : 384);
|
|
1967
|
+
}
|
|
1968
|
+
function fsyncTree(root) {
|
|
1969
|
+
for (const record of treeRecords(root)) {
|
|
1970
|
+
const target = path14.join(root, record.path);
|
|
1971
|
+
if (record.type === "file") {
|
|
1972
|
+
const fd = fsSync.openSync(target, "r");
|
|
1973
|
+
fsSync.fsyncSync(fd);
|
|
1974
|
+
fsSync.closeSync(fd);
|
|
1975
|
+
}
|
|
1976
|
+
}
|
|
1977
|
+
const directories = [root, ...treeRecords(root).filter((item) => item.type === "directory").map((item) => path14.join(root, item.path))].reverse();
|
|
1978
|
+
for (const directory of directories) {
|
|
1979
|
+
try {
|
|
1980
|
+
const fd = fsSync.openSync(directory, "r");
|
|
1981
|
+
fsSync.fsyncSync(fd);
|
|
1982
|
+
fsSync.closeSync(fd);
|
|
1983
|
+
} catch {
|
|
1984
|
+
}
|
|
1985
|
+
}
|
|
1986
|
+
}
|
|
1987
|
+
function copySnapshotContents(snapshot, destination) {
|
|
1988
|
+
fsSync.mkdirSync(destination, { recursive: true });
|
|
1989
|
+
for (const entry of fsSync.readdirSync(snapshot, { withFileTypes: true })) {
|
|
1990
|
+
if (entry.name === ".snapshot-complete.json") continue;
|
|
1991
|
+
fsSync.cpSync(path14.join(snapshot, entry.name), path14.join(destination, entry.name), { recursive: true, preserveTimestamps: true });
|
|
1992
|
+
}
|
|
1993
|
+
fsSync.chmodSync(destination, fsSync.lstatSync(snapshot).mode & 511);
|
|
1994
|
+
for (const record of treeRecords(snapshot)) {
|
|
1995
|
+
fsSync.chmodSync(path14.join(destination, record.path), record.mode);
|
|
1996
|
+
}
|
|
1997
|
+
}
|
|
1998
|
+
function describeTreeMismatch(expectedRoot, actualRoot) {
|
|
1999
|
+
try {
|
|
2000
|
+
const expectedRootMode = fsSync.lstatSync(expectedRoot).mode & 511;
|
|
2001
|
+
const actualRootMode = fsSync.lstatSync(actualRoot).mode & 511;
|
|
2002
|
+
if (expectedRootMode !== actualRootMode) return `root mode expected ${expectedRootMode.toString(8)}, actual ${actualRootMode.toString(8)}`;
|
|
2003
|
+
const expected = treeRecords(expectedRoot);
|
|
2004
|
+
const actual = new Map(treeRecords(actualRoot).map((record) => [record.path, record]));
|
|
2005
|
+
for (const record of expected) {
|
|
2006
|
+
const copy = actual.get(record.path);
|
|
2007
|
+
if (!copy) return `missing relative record ${JSON.stringify(record.path)}`;
|
|
2008
|
+
if (copy.type !== record.type) return `relative record ${JSON.stringify(record.path)} type mismatch`;
|
|
2009
|
+
if (copy.mode !== record.mode) return `relative record ${JSON.stringify(record.path)} mode expected ${record.mode.toString(8)}, actual ${copy.mode.toString(8)}`;
|
|
2010
|
+
if (copy.sha256 !== record.sha256) return `relative record ${JSON.stringify(record.path)} content digest mismatch`;
|
|
2011
|
+
actual.delete(record.path);
|
|
2012
|
+
}
|
|
2013
|
+
const extra = actual.keys().next().value;
|
|
2014
|
+
if (extra !== void 0) return `unexpected relative record ${JSON.stringify(extra)}`;
|
|
2015
|
+
} catch {
|
|
2016
|
+
return "tree comparison unavailable";
|
|
2017
|
+
}
|
|
2018
|
+
return "aggregate digest mismatch without a record-level difference";
|
|
2019
|
+
}
|
|
2020
|
+
function validateJournal(instanceRoot, journal) {
|
|
2021
|
+
assertInstanceBoundaries(instanceRoot);
|
|
2022
|
+
if (!journal || typeof journal !== "object" || !journal.data || typeof journal.data !== "object" || !journal.registry || typeof journal.registry !== "object" || !Array.isArray(journal.items) || typeof journal.pluginName !== "string" || typeof journal.transactionId !== "string" || typeof journal.phase !== "string") {
|
|
2023
|
+
throw new PluginInstallError("PLUGIN_RECOVERY_FAILED", "Plugin transaction journal structure is invalid.");
|
|
2024
|
+
}
|
|
2025
|
+
const exactKeys = (value, allowed) => Object.keys(value).every((key) => allowed.includes(key)) && allowed.filter((key) => !["contentBase64", "commitEvidence", "existed", "snapshotDir", "digest", "modeDigest", "hookStarted", "originalDigest"].includes(key)).every((key) => key in value);
|
|
2026
|
+
if (!exactKeys(journal, ["version", "transactionId", "pluginName", "phase", "stageDir", "rollbackDir", "items", "data", "registry"]) || !exactKeys(journal.data, ["directory", "existed", "snapshotDir", "digest", "modeDigest", "hookStarted"]) || !exactKeys(journal.registry, ["path", "existed", "contentBase64", "mode", "commitEvidence"]) || journal.items.some((item) => !item || typeof item !== "object" || !exactKeys(item, ["name", "hadLive", "state", "originalDigest"]))) {
|
|
2027
|
+
throw new PluginInstallError("PLUGIN_RECOVERY_FAILED", "Plugin transaction journal contains unknown or missing fields.");
|
|
2028
|
+
}
|
|
2029
|
+
const pluginsDir = path14.join(instanceRoot, "plugins");
|
|
2030
|
+
const validPhases = /* @__PURE__ */ new Set([
|
|
2031
|
+
"initialized",
|
|
2032
|
+
"staged",
|
|
2033
|
+
"snapshot-pending",
|
|
2034
|
+
"hook-pending",
|
|
2035
|
+
"hook-running",
|
|
2036
|
+
"hook-complete",
|
|
2037
|
+
"packages-activated",
|
|
2038
|
+
"registry-committing",
|
|
2039
|
+
"registry-committed",
|
|
2040
|
+
"committed",
|
|
2041
|
+
...PACKAGE_ITEMS.flatMap((name) => [`backup:${name}`, `activate:${name}`])
|
|
2042
|
+
]);
|
|
2043
|
+
let parsedName;
|
|
2044
|
+
try {
|
|
2045
|
+
parsedName = parseNpmPackageSpec(journal.pluginName);
|
|
2046
|
+
} catch {
|
|
2047
|
+
throw new PluginInstallError("PLUGIN_RECOVERY_FAILED", "Plugin transaction journal package name is invalid.");
|
|
2048
|
+
}
|
|
2049
|
+
const expectedData = path14.join(pluginsDir, "data", ...journal.pluginName.split("/"));
|
|
2050
|
+
const itemNames = Array.isArray(journal.items) ? journal.items.map((item) => item?.name) : [];
|
|
2051
|
+
const exactItems = itemNames.length === PACKAGE_ITEMS.length && new Set(itemNames).size === PACKAGE_ITEMS.length && PACKAGE_ITEMS.every((name) => itemNames.includes(name));
|
|
2052
|
+
if (journal.version !== 3 || !TRANSACTION_ID.test(journal.transactionId) || parsedName.packageName !== journal.pluginName || parsedName.spec !== journal.pluginName || !validPhases.has(journal.phase) || journal.registry.path !== path14.join(instanceRoot, "plugins.json") || journal.stageDir !== path14.join(pluginsDir, `.stage-${journal.transactionId}`) || journal.rollbackDir !== path14.join(pluginsDir, `.rollback-${journal.transactionId}`) || journal.data.directory !== expectedData || !exactItems) {
|
|
2053
|
+
throw new PluginInstallError("PLUGIN_RECOVERY_FAILED", "Plugin transaction journal ownership validation failed.");
|
|
2054
|
+
}
|
|
2055
|
+
if (journal.items.some((item) => typeof item.hadLive !== "boolean" || !["untouched", "live-backed-up-complete", "new-activated"].includes(item.state))) {
|
|
2056
|
+
throw new PluginInstallError("PLUGIN_RECOVERY_FAILED", "Plugin transaction journal item state is invalid.");
|
|
2057
|
+
}
|
|
2058
|
+
if (journal.items.some((item) => item.hadLive ? !/^[0-9a-f]{64}$/.test(item.originalDigest ?? "") : item.originalDigest !== void 0)) throw new PluginInstallError("PLUGIN_RECOVERY_FAILED", "Plugin transaction original package evidence is invalid.");
|
|
2059
|
+
if (typeof journal.registry.existed !== "boolean" || !Number.isInteger(journal.registry.mode) || journal.registry.mode < 0 || journal.registry.mode > 511 || journal.registry.existed && typeof journal.registry.contentBase64 !== "string" || !journal.registry.existed && journal.registry.contentBase64 !== void 0 || journal.registry.contentBase64 && Buffer.byteLength(journal.registry.contentBase64, "base64") > 4 * 1024 * 1024) {
|
|
2060
|
+
throw new PluginInstallError("PLUGIN_RECOVERY_FAILED", "Plugin transaction journal registry snapshot is invalid.");
|
|
2061
|
+
}
|
|
2062
|
+
if (journal.registry.contentBase64 !== void 0 && (!/^[A-Za-z0-9+/]*={0,2}$/.test(journal.registry.contentBase64) || Buffer.from(journal.registry.contentBase64, "base64").toString("base64") !== journal.registry.contentBase64)) throw new PluginInstallError("PLUGIN_RECOVERY_FAILED", "Plugin registry snapshot encoding is invalid.");
|
|
2063
|
+
const committedPhase = journal.phase === "registry-committed" || journal.phase === "committed";
|
|
2064
|
+
const evidence = journal.registry.commitEvidence;
|
|
2065
|
+
if (committedPhase !== Boolean(evidence)) throw new PluginInstallError("PLUGIN_RECOVERY_FAILED", "Plugin registry commit evidence is missing or present before its committed phase.");
|
|
2066
|
+
if (evidence) {
|
|
2067
|
+
if (!exactKeys(evidence, ["contentBase64", "digest", "mode"]) || !/^[A-Za-z0-9+/]*={0,2}$/.test(evidence.contentBase64) || Buffer.from(evidence.contentBase64, "base64").toString("base64") !== evidence.contentBase64 || Buffer.byteLength(evidence.contentBase64, "base64") > 4 * 1024 * 1024 || !/^[0-9a-f]{64}$/.test(evidence.digest) || createHash2("sha256").update(Buffer.from(evidence.contentBase64, "base64")).digest("hex") !== evidence.digest || !Number.isInteger(evidence.mode) || evidence.mode < 0 || evidence.mode > 511) throw new PluginInstallError("PLUGIN_RECOVERY_FAILED", "Plugin registry commit evidence is invalid.");
|
|
2068
|
+
if (!fsSync.existsSync(journal.registry.path) || !fsSync.readFileSync(journal.registry.path).equals(Buffer.from(evidence.contentBase64, "base64")) || (fsSync.statSync(journal.registry.path).mode & 511) !== evidence.mode) throw new PluginInstallError("PLUGIN_RECOVERY_FAILED", "Persisted plugin registry does not match committed evidence; cleanup is quarantined.");
|
|
2069
|
+
}
|
|
2070
|
+
const expectedSnapshot = path14.join(pluginsDir, `.data-snapshot-${journal.transactionId}.complete`);
|
|
2071
|
+
if (journal.data.snapshotDir && journal.data.snapshotDir !== expectedSnapshot) {
|
|
2072
|
+
throw new PluginInstallError("PLUGIN_RECOVERY_FAILED", "Plugin data snapshot provenance validation failed.");
|
|
2073
|
+
}
|
|
2074
|
+
if (journal.data.existed !== void 0 && typeof journal.data.existed !== "boolean") throw new PluginInstallError("PLUGIN_RECOVERY_FAILED", "Plugin data snapshot state is invalid.");
|
|
2075
|
+
if (journal.data.existed === true && (journal.data.snapshotDir !== expectedSnapshot || !/^[0-9a-f]{64}$/.test(journal.data.digest ?? "") || !/^[0-9a-f]{64}$/.test(journal.data.modeDigest ?? ""))) {
|
|
2076
|
+
throw new PluginInstallError("PLUGIN_RECOVERY_FAILED", "Plugin data snapshot completion state is invalid.");
|
|
2077
|
+
}
|
|
2078
|
+
if (journal.data.existed === false && (journal.data.snapshotDir !== void 0 || journal.data.digest !== void 0 || journal.data.modeDigest !== void 0)) throw new PluginInstallError("PLUGIN_RECOVERY_FAILED", "Plugin data absence state is invalid.");
|
|
2079
|
+
for (const [target, label] of [
|
|
2080
|
+
[pluginsDir, "Plugin package root"],
|
|
2081
|
+
[path14.join(pluginsDir, "data"), "Plugin data root"],
|
|
2082
|
+
[expectedData, "Plugin-owned data path"],
|
|
2083
|
+
[journal.stageDir, "Plugin stage path"],
|
|
2084
|
+
[journal.rollbackDir, "Plugin rollback path"],
|
|
2085
|
+
[path14.join(pluginsDir, `.data-snapshot-${journal.transactionId}.pending`), "Plugin pending snapshot"],
|
|
2086
|
+
[path14.join(pluginsDir, `.data-snapshot-${journal.transactionId}.complete`), "Plugin complete snapshot"],
|
|
2087
|
+
[journal.registry.path, "Plugin registry path"]
|
|
2088
|
+
]) assertContainedPath(instanceRoot, target, label);
|
|
2089
|
+
for (const name of PACKAGE_ITEMS) {
|
|
2090
|
+
assertContainedPath(instanceRoot, path14.join(pluginsDir, name), `Live package boundary ${name}`);
|
|
2091
|
+
assertContainedPath(instanceRoot, path14.join(journal.stageDir, name), `Staged package boundary ${name}`);
|
|
2092
|
+
assertContainedPath(instanceRoot, path14.join(journal.rollbackDir, name), `Backup package boundary ${name}`);
|
|
2093
|
+
}
|
|
2094
|
+
validateJournalStateMachine(journal);
|
|
2095
|
+
}
|
|
2096
|
+
function validateJournalStateMachine(journal) {
|
|
2097
|
+
const byName = new Map(journal.items.map((item) => [item.name, item]));
|
|
2098
|
+
const states = PACKAGE_ITEMS.map((name) => byName.get(name));
|
|
2099
|
+
const allUntouched = states.every((item) => item.state === "untouched");
|
|
2100
|
+
const allActivated = states.every((item) => item.state === "new-activated");
|
|
2101
|
+
const prePackage = /* @__PURE__ */ new Set(["initialized", "staged", "snapshot-pending", "hook-pending", "hook-running", "hook-complete"]);
|
|
2102
|
+
let packageStateValid = true;
|
|
2103
|
+
if (prePackage.has(journal.phase)) packageStateValid = allUntouched;
|
|
2104
|
+
else if (journal.phase.startsWith("backup:") || journal.phase.startsWith("activate:")) {
|
|
2105
|
+
const [kind, name] = journal.phase.split(":");
|
|
2106
|
+
const index = PACKAGE_ITEMS.indexOf(name);
|
|
2107
|
+
packageStateValid = index >= 0 && states.slice(0, index).every((item) => item.state === "new-activated") && states.slice(index + 1).every((item) => item.state === "untouched");
|
|
2108
|
+
const current = states[index];
|
|
2109
|
+
if (packageStateValid && kind === "backup") {
|
|
2110
|
+
packageStateValid = current.state === "untouched" || current.hadLive && current.state === "live-backed-up-complete";
|
|
2111
|
+
} else if (packageStateValid) {
|
|
2112
|
+
packageStateValid = current.state === "new-activated" || (current.hadLive ? current.state === "live-backed-up-complete" : current.state === "untouched");
|
|
2113
|
+
}
|
|
2114
|
+
} else if (["packages-activated", "registry-committing", "registry-committed", "committed"].includes(journal.phase)) packageStateValid = allActivated;
|
|
2115
|
+
if (!packageStateValid) throw new PluginInstallError("PLUGIN_RECOVERY_FAILED", "Plugin transaction phase and package item states are inconsistent.");
|
|
2116
|
+
const noSnapshotYet = journal.data.existed === void 0 && journal.data.snapshotDir === void 0 && journal.data.digest === void 0 && journal.data.modeDigest === void 0;
|
|
2117
|
+
const snapshotComplete = typeof journal.data.existed === "boolean" && (journal.data.existed === false || typeof journal.data.snapshotDir === "string" && typeof journal.data.digest === "string" && typeof journal.data.modeDigest === "string");
|
|
2118
|
+
const beforeSnapshot = ["initialized", "staged", "snapshot-pending"].includes(journal.phase);
|
|
2119
|
+
const beforeHook = journal.phase === "hook-pending";
|
|
2120
|
+
const afterHookStart = !beforeSnapshot && !beforeHook;
|
|
2121
|
+
const dataStateValid = beforeSnapshot && noSnapshotYet && journal.data.hookStarted === void 0 || beforeHook && snapshotComplete && journal.data.hookStarted !== true || afterHookStart && snapshotComplete && journal.data.hookStarted === true;
|
|
2122
|
+
if (!dataStateValid) throw new PluginInstallError("PLUGIN_RECOVERY_FAILED", "Plugin transaction phase and data snapshot state are inconsistent.");
|
|
2123
|
+
}
|
|
2124
|
+
function validateCompleteSnapshot(journal) {
|
|
2125
|
+
if (journal.data.existed !== true) return void 0;
|
|
2126
|
+
const snapshot = journal.data.snapshotDir;
|
|
2127
|
+
if (!snapshot || !journal.data.digest || !journal.data.modeDigest || !fsSync.existsSync(snapshot)) return void 0;
|
|
2128
|
+
let marker;
|
|
2129
|
+
try {
|
|
2130
|
+
marker = JSON.parse(fsSync.readFileSync(path14.join(snapshot, ".snapshot-complete.json"), "utf8"));
|
|
2131
|
+
} catch {
|
|
2132
|
+
throw new PluginInstallError("PLUGIN_RECOVERY_FAILED", "Plugin data snapshot completion marker is unavailable; original data was not deleted.");
|
|
2133
|
+
}
|
|
2134
|
+
if (marker.transactionId !== journal.transactionId || marker.digest !== journal.data.digest || marker.modeDigest !== journal.data.modeDigest || treeDigest(snapshot) !== journal.data.digest || treeModeDigest(snapshot, true) !== journal.data.modeDigest) {
|
|
2135
|
+
throw new PluginInstallError("PLUGIN_RECOVERY_FAILED", "Plugin data snapshot integrity validation failed; original data was not deleted.");
|
|
2136
|
+
}
|
|
2137
|
+
return snapshot;
|
|
2138
|
+
}
|
|
2139
|
+
function restoreJournal(instanceRoot, journal, boundary) {
|
|
2140
|
+
validateJournal(instanceRoot, journal);
|
|
2141
|
+
const completeSnapshot = validateCompleteSnapshot(journal);
|
|
2142
|
+
const pluginsDir = path14.join(instanceRoot, "plugins");
|
|
2143
|
+
for (const item of journal.items) {
|
|
2144
|
+
const live = path14.join(pluginsDir, item.name);
|
|
2145
|
+
const backup = path14.join(journal.rollbackDir, item.name);
|
|
2146
|
+
const staged = path14.join(journal.stageDir, item.name);
|
|
2147
|
+
const activated = item.state === "new-activated" || (journal.phase === `activate:${item.name}` || journal.phase === "packages-activated" || journal.phase === "registry-committing") && fsSync.existsSync(live) && !fsSync.existsSync(staged) && (fsSync.existsSync(backup) || !item.hadLive);
|
|
2148
|
+
if (item.hadLive) {
|
|
2149
|
+
const expected = item.originalDigest;
|
|
2150
|
+
if (!fsSync.existsSync(live) || boundaryDigest(live) !== expected) {
|
|
2151
|
+
if (!fsSync.existsSync(backup) || boundaryDigest(backup) !== expected) throw new PluginInstallError("PLUGIN_RECOVERY_FAILED", `Original package evidence is unavailable for ${item.name}; live state was not removed.`);
|
|
2152
|
+
const restoreTemp = path14.join(pluginsDir, `.restore-${journal.transactionId}-${item.name}`);
|
|
2153
|
+
fsSync.rmSync(restoreTemp, { recursive: true, force: true });
|
|
2154
|
+
fsSync.cpSync(backup, restoreTemp, { recursive: true, preserveTimestamps: true, dereference: false });
|
|
2155
|
+
if (boundaryDigest(restoreTemp) !== expected) throw new PluginInstallError("PLUGIN_RECOVERY_FAILED", `Prepared package restore did not verify for ${item.name}.`);
|
|
2156
|
+
fsyncBoundary(restoreTemp);
|
|
2157
|
+
boundary?.(`package:${item.name}:prepared`);
|
|
2158
|
+
fsSync.rmSync(live, { recursive: true, force: true });
|
|
2159
|
+
boundary?.(`package:${item.name}:live-removed`);
|
|
2160
|
+
fsSync.renameSync(restoreTemp, live);
|
|
2161
|
+
try {
|
|
2162
|
+
const fd = fsSync.openSync(pluginsDir, "r");
|
|
2163
|
+
fsSync.fsyncSync(fd);
|
|
2164
|
+
fsSync.closeSync(fd);
|
|
2165
|
+
} catch {
|
|
2166
|
+
}
|
|
2167
|
+
boundary?.(`package:${item.name}:restored`);
|
|
2168
|
+
}
|
|
2169
|
+
if (!fsSync.existsSync(live) || boundaryDigest(live) !== expected) throw new PluginInstallError("PLUGIN_RECOVERY_FAILED", `Restored package verification failed for ${item.name}.`);
|
|
2170
|
+
boundary?.(`package:${item.name}:verified`);
|
|
2171
|
+
} else if (activated && fsSync.existsSync(live)) {
|
|
2172
|
+
fsSync.rmSync(live, { recursive: true, force: true });
|
|
2173
|
+
fsyncDirectory(pluginsDir);
|
|
2174
|
+
boundary?.(`package:${item.name}:absent`);
|
|
2175
|
+
}
|
|
2176
|
+
}
|
|
2177
|
+
if (journal.data.existed === true) {
|
|
2178
|
+
const dataVerified = fsSync.existsSync(journal.data.directory) && treeDigest(journal.data.directory) === journal.data.digest && treeModeDigest(journal.data.directory) === journal.data.modeDigest;
|
|
2179
|
+
if (!dataVerified) {
|
|
2180
|
+
if (!completeSnapshot) throw new PluginInstallError("PLUGIN_RECOVERY_FAILED", "Complete plugin data evidence is unavailable; live data was not removed.");
|
|
2181
|
+
const restoreTemp = path14.join(pluginsDir, `.data-restore-${journal.transactionId}.pending`);
|
|
2182
|
+
fsSync.rmSync(restoreTemp, { recursive: true, force: true });
|
|
2183
|
+
copySnapshotContents(completeSnapshot, restoreTemp);
|
|
2184
|
+
if (treeDigest(restoreTemp) !== journal.data.digest || treeModeDigest(restoreTemp) !== journal.data.modeDigest) {
|
|
2185
|
+
throw new PluginInstallError("PLUGIN_RECOVERY_FAILED", `Prepared plugin data rollback verification failed: ${describeTreeMismatch(completeSnapshot, restoreTemp)}.`);
|
|
2186
|
+
}
|
|
2187
|
+
fsyncTree(restoreTemp);
|
|
2188
|
+
boundary?.("data:prepared");
|
|
2189
|
+
fsSync.rmSync(journal.data.directory, { recursive: true, force: true });
|
|
2190
|
+
boundary?.("data:live-removed");
|
|
2191
|
+
fsSync.renameSync(restoreTemp, journal.data.directory);
|
|
2192
|
+
try {
|
|
2193
|
+
const fd = fsSync.openSync(path14.dirname(journal.data.directory), "r");
|
|
2194
|
+
fsSync.fsyncSync(fd);
|
|
2195
|
+
fsSync.closeSync(fd);
|
|
2196
|
+
} catch {
|
|
2197
|
+
}
|
|
2198
|
+
boundary?.("data:restored");
|
|
2199
|
+
}
|
|
2200
|
+
if (!fsSync.existsSync(journal.data.directory) || treeDigest(journal.data.directory) !== journal.data.digest || treeModeDigest(journal.data.directory) !== journal.data.modeDigest) {
|
|
2201
|
+
const detail = completeSnapshot ? describeTreeMismatch(completeSnapshot, journal.data.directory) : "durable snapshot unavailable";
|
|
2202
|
+
throw new PluginInstallError("PLUGIN_RECOVERY_FAILED", `Plugin data rollback verification failed: ${detail}.`);
|
|
2203
|
+
}
|
|
2204
|
+
boundary?.("data:verified");
|
|
2205
|
+
} else if (journal.data.existed === false && journal.data.hookStarted === true) {
|
|
2206
|
+
fsSync.rmSync(journal.data.directory, { recursive: true, force: true });
|
|
2207
|
+
fsyncDirectory(path14.dirname(journal.data.directory));
|
|
2208
|
+
boundary?.("data:absent");
|
|
2209
|
+
}
|
|
2210
|
+
if (journal.registry.existed && journal.registry.contentBase64 !== void 0) {
|
|
2211
|
+
const expected = Buffer.from(journal.registry.contentBase64, "base64");
|
|
2212
|
+
const alreadyVerified = fsSync.existsSync(journal.registry.path) && fsSync.readFileSync(journal.registry.path).equals(expected) && (fsSync.statSync(journal.registry.path).mode & 511) === journal.registry.mode;
|
|
2213
|
+
if (!alreadyVerified) {
|
|
2214
|
+
const tmp = `${journal.registry.path}.${journal.transactionId}.recovery`;
|
|
2215
|
+
fsSync.rmSync(tmp, { force: true });
|
|
2216
|
+
try {
|
|
2217
|
+
fsSync.writeFileSync(tmp, expected, { flag: "wx", mode: journal.registry.mode });
|
|
2218
|
+
const fd = fsSync.openSync(tmp, "r");
|
|
2219
|
+
fsSync.fsyncSync(fd);
|
|
2220
|
+
fsSync.closeSync(fd);
|
|
2221
|
+
fsSync.renameSync(tmp, journal.registry.path);
|
|
2222
|
+
fsSync.chmodSync(journal.registry.path, journal.registry.mode);
|
|
2223
|
+
try {
|
|
2224
|
+
const dir = fsSync.openSync(path14.dirname(journal.registry.path), "r");
|
|
2225
|
+
fsSync.fsyncSync(dir);
|
|
2226
|
+
fsSync.closeSync(dir);
|
|
2227
|
+
} catch {
|
|
2228
|
+
}
|
|
2229
|
+
} finally {
|
|
2230
|
+
fsSync.rmSync(tmp, { force: true });
|
|
2231
|
+
}
|
|
2232
|
+
boundary?.("registry:restored");
|
|
2233
|
+
}
|
|
2234
|
+
if (!fsSync.readFileSync(journal.registry.path).equals(expected) || (fsSync.statSync(journal.registry.path).mode & 511) !== journal.registry.mode) throw new PluginInstallError("PLUGIN_RECOVERY_FAILED", "Plugin registry rollback verification failed.");
|
|
2235
|
+
} else if (journal.phase === "registry-committing") {
|
|
2236
|
+
fsSync.rmSync(journal.registry.path, { force: true });
|
|
2237
|
+
fsyncDirectory(path14.dirname(journal.registry.path));
|
|
2238
|
+
} else if (fsSync.existsSync(journal.registry.path)) throw new PluginInstallError("PLUGIN_RECOVERY_FAILED", "Unexpected registry appeared before its mutation boundary; it was not removed.");
|
|
2239
|
+
boundary?.("all:verified");
|
|
2240
|
+
}
|
|
2241
|
+
function cleanJournalArtifacts(instanceRoot, journal, removePath = (target) => fsSync.rmSync(target, { recursive: true, force: true })) {
|
|
2242
|
+
validateJournal(instanceRoot, journal);
|
|
2243
|
+
const pluginsDir = path14.join(instanceRoot, "plugins");
|
|
2244
|
+
const pendingSnapshot = path14.join(pluginsDir, `.data-snapshot-${journal.transactionId}.pending`);
|
|
2245
|
+
const completeSnapshot = path14.join(pluginsDir, `.data-snapshot-${journal.transactionId}.complete`);
|
|
2246
|
+
const restorePaths = PACKAGE_ITEMS.map((name) => path14.join(pluginsDir, `.restore-${journal.transactionId}-${name}`));
|
|
2247
|
+
const dataRestore = path14.join(pluginsDir, `.data-restore-${journal.transactionId}.pending`);
|
|
2248
|
+
for (const owned of [journal.stageDir, journal.rollbackDir, journal.data.snapshotDir, pendingSnapshot, completeSnapshot, ...restorePaths, dataRestore]) if (owned) removePath(owned);
|
|
2249
|
+
removePath(`${journal.registry.path}.${journal.transactionId}.recovery`);
|
|
2250
|
+
removePath(path14.join(instanceRoot, JOURNAL_FILE));
|
|
2251
|
+
fsyncDirectory(pluginsDir);
|
|
2252
|
+
fsyncDirectory(instanceRoot);
|
|
2253
|
+
}
|
|
2254
|
+
function recoverLocked(instanceRoot) {
|
|
2255
|
+
assertInstanceBoundaries(instanceRoot);
|
|
2256
|
+
const journalPath = path14.join(instanceRoot, JOURNAL_FILE);
|
|
2257
|
+
if (!fsSync.existsSync(journalPath)) return;
|
|
2258
|
+
let journal;
|
|
2259
|
+
try {
|
|
2260
|
+
if (fsSync.lstatSync(journalPath).isSymbolicLink() || fsSync.statSync(journalPath).size > MAX_JOURNAL_BYTES) throw new Error("invalid journal file");
|
|
2261
|
+
journal = JSON.parse(fsSync.readFileSync(journalPath, "utf8"));
|
|
2262
|
+
} catch {
|
|
2263
|
+
throw new PluginInstallError("PLUGIN_RECOVERY_FAILED", "Plugin transaction journal is unreadable; community plugins remain quarantined.");
|
|
2264
|
+
}
|
|
2265
|
+
validateJournal(instanceRoot, journal);
|
|
2266
|
+
if (journal.phase !== "registry-committed" && journal.phase !== "committed") restoreJournal(instanceRoot, journal);
|
|
2267
|
+
cleanJournalArtifacts(instanceRoot, journal);
|
|
2268
|
+
}
|
|
2269
|
+
async function recoverPluginInstallTransaction(instanceRoot, options = {}) {
|
|
2270
|
+
const lock = await acquirePluginInstallLock(instanceRoot, options);
|
|
2271
|
+
try {
|
|
2272
|
+
recoverLocked(instanceRoot);
|
|
2273
|
+
} finally {
|
|
2274
|
+
lock.release();
|
|
2275
|
+
}
|
|
2276
|
+
}
|
|
2277
|
+
function pluginMutationLockHeld(instanceRoot) {
|
|
2278
|
+
return mutationContext.getStore()?.instanceRoot === instanceRoot;
|
|
2279
|
+
}
|
|
2280
|
+
async function withHeldPluginMutationLock(instanceRoot, lock, callback) {
|
|
2281
|
+
return mutationContext.run({ instanceRoot, transactionId: lock.transactionId }, callback);
|
|
2282
|
+
}
|
|
2283
|
+
async function withPluginMutationLock(instanceRoot, callback) {
|
|
2284
|
+
if (pluginMutationLockHeld(instanceRoot)) return callback();
|
|
2285
|
+
const lock = await acquirePluginInstallLock(instanceRoot);
|
|
2286
|
+
try {
|
|
2287
|
+
recoverLocked(instanceRoot);
|
|
2288
|
+
return await withHeldPluginMutationLock(instanceRoot, lock, callback);
|
|
2289
|
+
} finally {
|
|
2290
|
+
lock.release();
|
|
2291
|
+
}
|
|
2292
|
+
}
|
|
2293
|
+
async function pathExists(file) {
|
|
2294
|
+
try {
|
|
2295
|
+
await fs15.access(file);
|
|
2296
|
+
return true;
|
|
2297
|
+
} catch {
|
|
2298
|
+
return false;
|
|
2299
|
+
}
|
|
2300
|
+
}
|
|
2301
|
+
async function stageNpmPlugin(packageSpec, pluginsDir, childEnv, options = {}) {
|
|
2302
|
+
const { packageName } = parseNpmPackageSpec(packageSpec);
|
|
2303
|
+
await fs15.mkdir(pluginsDir, { recursive: true });
|
|
2304
|
+
const liveManifest = path14.join(pluginsDir, "package.json");
|
|
2305
|
+
const liveModules = path14.join(pluginsDir, "node_modules");
|
|
2306
|
+
if (await pathExists(liveModules) && !await pathExists(liveManifest)) {
|
|
2307
|
+
throw new PluginInstallError("PLUGIN_INSTALL_STATE_INVALID", "Plugin directory has node_modules without package.json; no changes were made.");
|
|
2308
|
+
}
|
|
2309
|
+
const transactionId = options.transactionId ?? randomUUID2();
|
|
2310
|
+
const stageDir = path14.join(pluginsDir, `.stage-${transactionId}`);
|
|
2311
|
+
const rollbackDir = path14.join(pluginsDir, `.rollback-${transactionId}`);
|
|
2312
|
+
await fs15.mkdir(stageDir, { recursive: true });
|
|
2313
|
+
try {
|
|
2314
|
+
for (const name of ["package.json", "package-lock.json"]) {
|
|
2315
|
+
const source = path14.join(pluginsDir, name);
|
|
2316
|
+
if (await pathExists(source)) await fs15.copyFile(source, path14.join(stageDir, name));
|
|
2317
|
+
}
|
|
2318
|
+
if (!await pathExists(path14.join(stageDir, "package.json"))) {
|
|
2319
|
+
await fs15.writeFile(path14.join(stageDir, "package.json"), '{"private":true}\n', { mode: 384 });
|
|
2320
|
+
}
|
|
2321
|
+
try {
|
|
2322
|
+
if (options.runNpm) await options.runNpm(stageDir, packageSpec);
|
|
2323
|
+
else await execFileAsync("npm", ["install", packageSpec, "--prefix", stageDir, "--save", "--ignore-scripts"], {
|
|
2324
|
+
timeout: 6e4,
|
|
2325
|
+
env: childEnv
|
|
2326
|
+
});
|
|
2327
|
+
} catch {
|
|
2328
|
+
throw new PluginInstallError("PLUGIN_STAGE_FAILED", "npm could not stage the plugin package; live plugins were not changed.");
|
|
2329
|
+
}
|
|
2330
|
+
const packagePath = path14.join(stageDir, "node_modules", ...packageName.split("/"), "package.json");
|
|
2331
|
+
let manifest;
|
|
2332
|
+
try {
|
|
2333
|
+
manifest = JSON.parse(await fs15.readFile(packagePath, "utf8"));
|
|
2334
|
+
} catch {
|
|
2335
|
+
throw new PluginInstallError("PLUGIN_CONTRACT_INVALID", "Staged plugin package metadata is unavailable or invalid.");
|
|
2336
|
+
}
|
|
2337
|
+
let module;
|
|
2338
|
+
try {
|
|
2339
|
+
module = await importFromDir(packageName, stageDir);
|
|
2340
|
+
} catch {
|
|
2341
|
+
throw new PluginInstallError("PLUGIN_CONTRACT_INVALID", "Staged plugin entry point could not be loaded.");
|
|
2342
|
+
}
|
|
2343
|
+
const plugin = validateOpenACPPluginModule(module, manifest, packageName);
|
|
2344
|
+
let activated = false;
|
|
2345
|
+
let finished = false;
|
|
2346
|
+
const rollback = async () => {
|
|
2347
|
+
if (finished) return;
|
|
2348
|
+
for (const name of ["node_modules", "package.json", "package-lock.json"]) {
|
|
2349
|
+
const live = path14.join(pluginsDir, name);
|
|
2350
|
+
const backup = path14.join(rollbackDir, name);
|
|
2351
|
+
if (await pathExists(live)) await fs15.rm(live, { recursive: true, force: true });
|
|
2352
|
+
if (await pathExists(backup)) await fs15.rename(backup, live);
|
|
2353
|
+
}
|
|
2354
|
+
finished = true;
|
|
2355
|
+
await fs15.rm(rollbackDir, { recursive: true, force: true });
|
|
2356
|
+
await fs15.rm(stageDir, { recursive: true, force: true });
|
|
2357
|
+
};
|
|
2358
|
+
return {
|
|
2359
|
+
packageName,
|
|
2360
|
+
manifest,
|
|
2361
|
+
plugin,
|
|
2362
|
+
stageDir,
|
|
2363
|
+
rollbackDir,
|
|
2364
|
+
activationItems: async () => Promise.all(PACKAGE_ITEMS.map(async (name) => ({ name, hadLive: await pathExists(path14.join(pluginsDir, name)), state: "untouched" }))),
|
|
2365
|
+
discard: async () => {
|
|
2366
|
+
if (activated) await rollback();
|
|
2367
|
+
else await fs15.rm(stageDir, { recursive: true, force: true });
|
|
2368
|
+
},
|
|
2369
|
+
activate: async (hooks) => {
|
|
2370
|
+
if (activated) throw new PluginInstallError("PLUGIN_ACTIVATION_FAILED", "Staged plugin was already activated.");
|
|
2371
|
+
await fs15.mkdir(rollbackDir, { recursive: true });
|
|
2372
|
+
try {
|
|
2373
|
+
const roots = [pluginsDir, stageDir, rollbackDir];
|
|
2374
|
+
for (const root of roots) assertDirectoryBoundary(root, "Plugin activation boundary");
|
|
2375
|
+
const devices = roots.map((root) => fsSync.statSync(root).dev);
|
|
2376
|
+
if (new Set(devices).size !== 1) throw new PluginInstallError("PLUGIN_ACTIVATION_FAILED", "Plugin activation boundaries must share one filesystem.");
|
|
2377
|
+
for (const name of PACKAGE_ITEMS) {
|
|
2378
|
+
const live = path14.join(pluginsDir, name);
|
|
2379
|
+
const stagedItem = path14.join(stageDir, name);
|
|
2380
|
+
const backup = path14.join(rollbackDir, name);
|
|
2381
|
+
if (fsSync.existsSync(backup)) throw new PluginInstallError("PLUGIN_ACTIVATION_FAILED", "Plugin rollback boundary must be empty before activation.");
|
|
2382
|
+
for (const boundary of [live, stagedItem]) {
|
|
2383
|
+
if (!fsSync.existsSync(boundary)) continue;
|
|
2384
|
+
const stat = fsSync.lstatSync(boundary);
|
|
2385
|
+
if (stat.isSymbolicLink() || stat.dev !== devices[0]) throw new PluginInstallError("PLUGIN_ACTIVATION_FAILED", "Plugin activation boundary is a symlink or crosses filesystems.");
|
|
2386
|
+
if (fsSync.realpathSync(path14.dirname(boundary)) !== path14.resolve(path14.dirname(boundary))) throw new PluginInstallError("PLUGIN_ACTIVATION_FAILED", "Plugin activation boundary has a symbolic-link ancestor.");
|
|
2387
|
+
}
|
|
2388
|
+
}
|
|
2389
|
+
await hooks?.beforeActivation();
|
|
2390
|
+
for (const name of ["node_modules", "package.json", "package-lock.json"]) {
|
|
2391
|
+
const live = path14.join(pluginsDir, name);
|
|
2392
|
+
const staged = path14.join(stageDir, name);
|
|
2393
|
+
const backup = path14.join(rollbackDir, name);
|
|
2394
|
+
await hooks?.beforeBackup(name);
|
|
2395
|
+
if (await pathExists(live)) {
|
|
2396
|
+
if (options.renamePath) options.renamePath(live, backup);
|
|
2397
|
+
else await fs15.rename(live, backup);
|
|
2398
|
+
}
|
|
2399
|
+
await hooks?.afterBackup(name);
|
|
2400
|
+
await hooks?.beforeActivate(name);
|
|
2401
|
+
if (await pathExists(staged)) {
|
|
2402
|
+
if (options.renamePath) options.renamePath(staged, live);
|
|
2403
|
+
else await fs15.rename(staged, live);
|
|
2404
|
+
}
|
|
2405
|
+
await hooks?.afterActivate(name);
|
|
2406
|
+
}
|
|
2407
|
+
activated = true;
|
|
2408
|
+
await hooks?.afterPackagesActivated();
|
|
2409
|
+
} catch (error) {
|
|
2410
|
+
if (error instanceof PluginInstallCrashSimulation || hooks) throw error;
|
|
2411
|
+
await rollback();
|
|
2412
|
+
throw new PluginInstallError("PLUGIN_ACTIVATION_FAILED", "Plugin activation failed; previous packages were restored.");
|
|
2413
|
+
}
|
|
2414
|
+
return {
|
|
2415
|
+
rollback,
|
|
2416
|
+
commit: async () => {
|
|
2417
|
+
if (finished) return;
|
|
2418
|
+
finished = true;
|
|
2419
|
+
await Promise.allSettled([
|
|
2420
|
+
fs15.rm(rollbackDir, { recursive: true, force: true }),
|
|
2421
|
+
fs15.rm(stageDir, { recursive: true, force: true })
|
|
2422
|
+
]);
|
|
2423
|
+
}
|
|
2424
|
+
};
|
|
2425
|
+
}
|
|
2426
|
+
};
|
|
2427
|
+
} catch (error) {
|
|
2428
|
+
await fs15.rm(stageDir, { recursive: true, force: true });
|
|
2429
|
+
throw error;
|
|
2430
|
+
}
|
|
2431
|
+
}
|
|
2432
|
+
async function installNpmPlugin(packageName, pluginsDir, childEnv) {
|
|
2433
|
+
if (!VALID_NPM_NAME.test(packageName)) {
|
|
2434
|
+
throw new Error(`Invalid package name: "${packageName}". Must be a valid npm package name.`);
|
|
2435
|
+
}
|
|
2436
|
+
const dir = pluginsDir;
|
|
2437
|
+
try {
|
|
2438
|
+
return await importFromDir(packageName, dir);
|
|
2439
|
+
} catch {
|
|
2440
|
+
}
|
|
2441
|
+
void childEnv;
|
|
2442
|
+
throw new PluginInstallError(
|
|
2443
|
+
"PLUGIN_INSTALL_STATE_INVALID",
|
|
2444
|
+
`Runtime package installation is not safe while OpenACP may be loaded. Run "openacp plugin install ${packageName}" and restart OpenACP.`
|
|
2445
|
+
);
|
|
2446
|
+
}
|
|
2447
|
+
var execFileAsync, VALID_NPM_NAME, PluginInstallError, PluginInstallCrashSimulation, JOURNAL_FILE, LOCK_FILE, PACKAGE_ITEMS, MAX_JOURNAL_BYTES, TRANSACTION_ID, mutationContext, PluginInstallJournalController;
|
|
2448
|
+
var init_plugin_installer = __esm({
|
|
2449
|
+
"src/core/plugin/plugin-installer.ts"() {
|
|
2450
|
+
"use strict";
|
|
2451
|
+
execFileAsync = promisify(execFile);
|
|
2452
|
+
VALID_NPM_NAME = /^(@[a-z0-9][\w.-]*\/)?[a-z0-9][\w.-]*(@[\w.^~>=<|-]+)?$/i;
|
|
2453
|
+
PluginInstallError = class extends Error {
|
|
2454
|
+
constructor(code, message) {
|
|
2455
|
+
super(message);
|
|
2456
|
+
this.code = code;
|
|
2457
|
+
this.name = "PluginInstallError";
|
|
2458
|
+
}
|
|
2459
|
+
code;
|
|
2460
|
+
};
|
|
2461
|
+
PluginInstallCrashSimulation = class extends Error {
|
|
2462
|
+
constructor(phase) {
|
|
2463
|
+
super(`Simulated process crash at ${phase}`);
|
|
2464
|
+
this.phase = phase;
|
|
2465
|
+
this.name = "PluginInstallCrashSimulation";
|
|
2466
|
+
}
|
|
2467
|
+
phase;
|
|
2468
|
+
};
|
|
2469
|
+
JOURNAL_FILE = "plugin-install.journal.json";
|
|
2470
|
+
LOCK_FILE = "plugin-install.lock";
|
|
2471
|
+
PACKAGE_ITEMS = ["node_modules", "package.json", "package-lock.json"];
|
|
2472
|
+
MAX_JOURNAL_BYTES = 8 * 1024 * 1024;
|
|
2473
|
+
TRANSACTION_ID = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
|
|
2474
|
+
mutationContext = new AsyncLocalStorage();
|
|
2475
|
+
PluginInstallJournalController = class {
|
|
2476
|
+
constructor(instanceRoot, lock, hooks = {}) {
|
|
2477
|
+
this.instanceRoot = instanceRoot;
|
|
2478
|
+
this.lock = lock;
|
|
2479
|
+
this.hooks = hooks;
|
|
2480
|
+
this.transactionId = lock.transactionId;
|
|
2481
|
+
this.journalPath = path14.join(instanceRoot, JOURNAL_FILE);
|
|
2482
|
+
}
|
|
2483
|
+
instanceRoot;
|
|
2484
|
+
lock;
|
|
2485
|
+
hooks;
|
|
2486
|
+
journal;
|
|
2487
|
+
journalPath;
|
|
2488
|
+
transactionId;
|
|
2489
|
+
recoverExisting() {
|
|
2490
|
+
recoverLocked(this.instanceRoot);
|
|
2491
|
+
}
|
|
2492
|
+
initialize(pluginName, dataDirectory, registryPath) {
|
|
2493
|
+
if (this.journal) throw new PluginInstallError("PLUGIN_RECOVERY_FAILED", "Plugin transaction journal is already initialized.");
|
|
2494
|
+
const pluginsDir = path14.join(this.instanceRoot, "plugins");
|
|
2495
|
+
assertInstanceBoundaries(this.instanceRoot);
|
|
2496
|
+
const expectedData = path14.join(pluginsDir, "data", ...pluginName.split("/"));
|
|
2497
|
+
if (dataDirectory !== expectedData || registryPath !== path14.join(this.instanceRoot, "plugins.json")) throw new PluginInstallError("PLUGIN_RECOVERY_FAILED", "Plugin transaction intent paths are invalid.");
|
|
2498
|
+
const existed = fsSync.existsSync(registryPath);
|
|
2499
|
+
this.journal = {
|
|
2500
|
+
version: 3,
|
|
2501
|
+
transactionId: this.transactionId,
|
|
2502
|
+
pluginName,
|
|
2503
|
+
phase: "initialized",
|
|
2504
|
+
stageDir: path14.join(pluginsDir, `.stage-${this.transactionId}`),
|
|
2505
|
+
rollbackDir: path14.join(pluginsDir, `.rollback-${this.transactionId}`),
|
|
2506
|
+
items: PACKAGE_ITEMS.map((name) => {
|
|
2507
|
+
const live = path14.join(pluginsDir, name);
|
|
2508
|
+
const hadLive = fsSync.existsSync(live);
|
|
2509
|
+
return { name, hadLive, state: "untouched", originalDigest: hadLive ? boundaryDigest(live) : void 0 };
|
|
2510
|
+
}),
|
|
2511
|
+
data: { directory: dataDirectory },
|
|
2512
|
+
registry: { path: registryPath, existed, contentBase64: existed ? fsSync.readFileSync(registryPath).toString("base64") : void 0, mode: existed ? fsSync.statSync(registryPath).mode & 511 : 384 }
|
|
2513
|
+
};
|
|
2514
|
+
validateJournal(this.instanceRoot, this.journal);
|
|
2515
|
+
this.persist();
|
|
2516
|
+
this.hooks.crashAt?.("initialized");
|
|
2517
|
+
}
|
|
2518
|
+
snapshotData(directory) {
|
|
2519
|
+
if (!this.journal || this.journal.data.directory !== directory) throw new PluginInstallError("PLUGIN_RECOVERY_FAILED", "Plugin transaction was not prepared for this data directory.");
|
|
2520
|
+
validateJournal(this.instanceRoot, this.journal);
|
|
2521
|
+
if (!fsSync.existsSync(directory)) {
|
|
2522
|
+
const result2 = { directory, existed: false };
|
|
2523
|
+
this.journal.data = result2;
|
|
2524
|
+
this.transition("hook-pending");
|
|
2525
|
+
return result2;
|
|
2526
|
+
}
|
|
2527
|
+
const pluginsDir = path14.join(this.instanceRoot, "plugins");
|
|
2528
|
+
const pending = path14.join(pluginsDir, `.data-snapshot-${this.transactionId}.pending`);
|
|
2529
|
+
const complete = path14.join(pluginsDir, `.data-snapshot-${this.transactionId}.complete`);
|
|
2530
|
+
let result;
|
|
2531
|
+
try {
|
|
2532
|
+
fsSync.rmSync(pending, { recursive: true, force: true });
|
|
2533
|
+
fsSync.rmSync(complete, { recursive: true, force: true });
|
|
2534
|
+
(this.hooks.copyTree ?? ((source, destination) => fsSync.cpSync(source, destination, { recursive: true, preserveTimestamps: true })))(directory, pending);
|
|
2535
|
+
const digest = treeDigest(directory);
|
|
2536
|
+
if (treeDigest(pending) !== digest) throw new PluginInstallError("PLUGIN_SNAPSHOT_FAILED", "Plugin data snapshot verification failed.");
|
|
2537
|
+
secureSnapshotTree(pending);
|
|
2538
|
+
const modeDigest = treeModeDigest(pending, true);
|
|
2539
|
+
atomicJson(path14.join(pending, ".snapshot-complete.json"), { version: 1, transactionId: this.transactionId, digest, modeDigest });
|
|
2540
|
+
fsyncTree(pending);
|
|
2541
|
+
const deviceForPath = this.hooks.deviceForPath ?? ((target) => fsSync.statSync(target).dev);
|
|
2542
|
+
if (deviceForPath(pluginsDir) !== deviceForPath(pending)) throw new PluginInstallError("PLUGIN_SNAPSHOT_FAILED", "Plugin transaction paths must share one filesystem.");
|
|
2543
|
+
fsSync.renameSync(pending, complete);
|
|
2544
|
+
try {
|
|
2545
|
+
const fd = fsSync.openSync(pluginsDir, "r");
|
|
2546
|
+
fsSync.fsyncSync(fd);
|
|
2547
|
+
fsSync.closeSync(fd);
|
|
2548
|
+
} catch {
|
|
2549
|
+
}
|
|
2550
|
+
this.hooks.crashAfterSnapshotRename?.();
|
|
2551
|
+
result = { directory, existed: true, snapshotDir: complete, digest, modeDigest };
|
|
2552
|
+
} catch (error) {
|
|
2553
|
+
if (error instanceof PluginInstallCrashSimulation) throw error;
|
|
2554
|
+
fsSync.rmSync(pending, { recursive: true, force: true });
|
|
2555
|
+
fsSync.rmSync(complete, { recursive: true, force: true });
|
|
2556
|
+
if (error instanceof PluginInstallError) throw error;
|
|
2557
|
+
throw new PluginInstallError("PLUGIN_SNAPSHOT_FAILED", "Plugin data snapshot could not be completed; original data was not changed.");
|
|
2558
|
+
}
|
|
2559
|
+
this.journal.data = result;
|
|
2560
|
+
try {
|
|
2561
|
+
this.transition("hook-pending");
|
|
2562
|
+
} catch (error) {
|
|
2563
|
+
if (error instanceof PluginInstallCrashSimulation) throw error;
|
|
2564
|
+
fsSync.rmSync(complete, { recursive: true, force: true });
|
|
2565
|
+
throw error;
|
|
2566
|
+
}
|
|
2567
|
+
return result;
|
|
2568
|
+
}
|
|
2569
|
+
async prepare(pluginName, staged, dataDirectory, registryPath) {
|
|
2570
|
+
const pluginsDir = path14.join(this.instanceRoot, "plugins");
|
|
2571
|
+
const deviceForPath = this.hooks.deviceForPath ?? ((target) => fsSync.statSync(target).dev);
|
|
2572
|
+
if (deviceForPath(pluginsDir) !== deviceForPath(staged.stageDir)) throw new PluginInstallError("PLUGIN_STAGE_FAILED", "Plugin transaction paths must share one filesystem.");
|
|
2573
|
+
if (!this.journal) this.initialize(pluginName, dataDirectory, registryPath);
|
|
2574
|
+
const journal = this.journal;
|
|
2575
|
+
if (journal.pluginName !== pluginName || journal.stageDir !== staged.stageDir || journal.rollbackDir !== staged.rollbackDir) throw new PluginInstallError("PLUGIN_RECOVERY_FAILED", "Staged plugin does not match durable transaction intent.");
|
|
2576
|
+
this.transition("staged");
|
|
2577
|
+
this.transition("snapshot-pending");
|
|
2578
|
+
return {
|
|
2579
|
+
beforeActivation: async () => {
|
|
2580
|
+
},
|
|
2581
|
+
beforeBackup: async (name) => this.transition(`backup:${name}`),
|
|
2582
|
+
afterBackup: async (name) => {
|
|
2583
|
+
const item = this.journal.items.find((entry) => entry.name === name);
|
|
2584
|
+
if (item.hadLive) item.state = "live-backed-up-complete";
|
|
2585
|
+
this.persist();
|
|
2586
|
+
},
|
|
2587
|
+
beforeActivate: async (name) => this.transition(`activate:${name}`),
|
|
2588
|
+
afterActivate: async (name) => {
|
|
2589
|
+
const item = this.journal.items.find((entry) => entry.name === name);
|
|
2590
|
+
item.state = "new-activated";
|
|
2591
|
+
this.persist();
|
|
2592
|
+
},
|
|
2593
|
+
afterPackagesActivated: async () => this.transition("packages-activated")
|
|
2594
|
+
};
|
|
2595
|
+
}
|
|
2596
|
+
transition(phase) {
|
|
2597
|
+
if (!this.journal) throw new PluginInstallError("PLUGIN_RECOVERY_FAILED", "Plugin transaction journal was not prepared.");
|
|
2598
|
+
const previous = this.journal.phase;
|
|
2599
|
+
const previousHookStarted = this.journal.data.hookStarted;
|
|
2600
|
+
this.journal.phase = phase;
|
|
2601
|
+
if (phase === "hook-running") this.journal.data.hookStarted = true;
|
|
2602
|
+
try {
|
|
2603
|
+
this.hooks.failPersistAt?.(phase);
|
|
2604
|
+
this.persist();
|
|
2605
|
+
} catch (error) {
|
|
2606
|
+
this.journal.phase = previous;
|
|
2607
|
+
this.journal.data.hookStarted = previousHookStarted;
|
|
2608
|
+
throw error;
|
|
2609
|
+
}
|
|
2610
|
+
this.hooks.crashAt?.(phase);
|
|
2611
|
+
}
|
|
2612
|
+
markRegistryCommitted(expectedContent, expectedMode) {
|
|
2613
|
+
if (!this.journal || this.journal.phase !== "registry-committing") throw new PluginInstallError("PLUGIN_RECOVERY_FAILED", "Registry commit marker requires the registry-committing phase.");
|
|
2614
|
+
const actual = fsSync.readFileSync(this.journal.registry.path);
|
|
2615
|
+
const actualMode = fsSync.statSync(this.journal.registry.path).mode & 511;
|
|
2616
|
+
if (!actual.equals(expectedContent) || actualMode !== expectedMode) throw new PluginInstallError("PLUGIN_RECOVERY_FAILED", "Persisted registry does not match the intended committed state.");
|
|
2617
|
+
const previousPhase = this.journal.phase;
|
|
2618
|
+
this.journal.registry.commitEvidence = {
|
|
2619
|
+
contentBase64: expectedContent.toString("base64"),
|
|
2620
|
+
digest: createHash2("sha256").update(expectedContent).digest("hex"),
|
|
2621
|
+
mode: expectedMode
|
|
2622
|
+
};
|
|
2623
|
+
this.journal.phase = "registry-committed";
|
|
2624
|
+
try {
|
|
2625
|
+
this.hooks.failPersistAt?.("registry-committed");
|
|
2626
|
+
this.persist();
|
|
2627
|
+
} catch (error) {
|
|
2628
|
+
this.journal.phase = previousPhase;
|
|
2629
|
+
delete this.journal.registry.commitEvidence;
|
|
2630
|
+
throw error;
|
|
2631
|
+
}
|
|
2632
|
+
this.hooks.crashAt?.("registry-committed");
|
|
2633
|
+
}
|
|
2634
|
+
rollback() {
|
|
2635
|
+
if (!this.journal) return;
|
|
2636
|
+
if (this.journal.phase === "registry-committed" || this.journal.phase === "committed") {
|
|
2637
|
+
cleanJournalArtifacts(this.instanceRoot, this.journal, this.hooks.removePath);
|
|
2638
|
+
this.journal = void 0;
|
|
2639
|
+
return;
|
|
2640
|
+
}
|
|
2641
|
+
restoreJournal(this.instanceRoot, this.journal, this.hooks.rollbackBoundary);
|
|
2642
|
+
cleanJournalArtifacts(this.instanceRoot, this.journal, this.hooks.removePath);
|
|
2643
|
+
this.journal = void 0;
|
|
2644
|
+
}
|
|
2645
|
+
commit() {
|
|
2646
|
+
try {
|
|
2647
|
+
this.transition("committed");
|
|
2648
|
+
} catch (error) {
|
|
2649
|
+
if (error instanceof PluginInstallCrashSimulation) throw error;
|
|
2650
|
+
return false;
|
|
2651
|
+
}
|
|
2652
|
+
try {
|
|
2653
|
+
cleanJournalArtifacts(this.instanceRoot, this.journal, this.hooks.removePath);
|
|
2654
|
+
this.journal = void 0;
|
|
2655
|
+
return true;
|
|
2656
|
+
} catch (error) {
|
|
2657
|
+
if (error instanceof PluginInstallCrashSimulation) throw error;
|
|
2658
|
+
return false;
|
|
2659
|
+
}
|
|
2660
|
+
}
|
|
2661
|
+
get isCommitted() {
|
|
2662
|
+
return this.journal?.phase === "registry-committed" || this.journal?.phase === "committed";
|
|
2663
|
+
}
|
|
2664
|
+
release() {
|
|
2665
|
+
this.lock.release();
|
|
2666
|
+
}
|
|
2667
|
+
persist() {
|
|
2668
|
+
validateJournal(this.instanceRoot, this.journal);
|
|
2669
|
+
atomicJson(this.journalPath, this.journal);
|
|
2670
|
+
}
|
|
2671
|
+
};
|
|
2672
|
+
}
|
|
2673
|
+
});
|
|
2674
|
+
|
|
1685
2675
|
// src/plugins/speech/providers/groq.ts
|
|
1686
2676
|
function mimeToExt(mimeType) {
|
|
1687
2677
|
const map = {
|
|
@@ -1700,16 +2690,18 @@ var init_groq = __esm({
|
|
|
1700
2690
|
"use strict";
|
|
1701
2691
|
GROQ_API_URL = "https://api.groq.com/openai/v1/audio/transcriptions";
|
|
1702
2692
|
GroqSTT = class {
|
|
1703
|
-
constructor(apiKey, defaultModel = "whisper-large-v3-turbo", scopedFetch = globalThis.fetch, getScopedFetch) {
|
|
2693
|
+
constructor(apiKey, defaultModel = "whisper-large-v3-turbo", scopedFetch = globalThis.fetch, getScopedFetch, endpoint = GROQ_API_URL) {
|
|
1704
2694
|
this.apiKey = apiKey;
|
|
1705
2695
|
this.defaultModel = defaultModel;
|
|
1706
2696
|
this.scopedFetch = scopedFetch;
|
|
1707
2697
|
this.getScopedFetch = getScopedFetch;
|
|
2698
|
+
this.endpoint = endpoint;
|
|
1708
2699
|
}
|
|
1709
2700
|
apiKey;
|
|
1710
2701
|
defaultModel;
|
|
1711
2702
|
scopedFetch;
|
|
1712
2703
|
getScopedFetch;
|
|
2704
|
+
endpoint;
|
|
1713
2705
|
name = "groq";
|
|
1714
2706
|
/**
|
|
1715
2707
|
* Transcribes audio using the Groq Whisper API.
|
|
@@ -1726,7 +2718,7 @@ var init_groq = __esm({
|
|
|
1726
2718
|
if (options?.language) {
|
|
1727
2719
|
form.append("language", options.language);
|
|
1728
2720
|
}
|
|
1729
|
-
const resp = await (this.getScopedFetch?.() ?? this.scopedFetch)(
|
|
2721
|
+
const resp = await (this.getScopedFetch?.() ?? this.scopedFetch)(this.endpoint, {
|
|
1730
2722
|
method: "POST",
|
|
1731
2723
|
headers: { Authorization: `Bearer ${this.apiKey}` },
|
|
1732
2724
|
body: form
|
|
@@ -1756,26 +2748,26 @@ var init_groq = __esm({
|
|
|
1756
2748
|
});
|
|
1757
2749
|
|
|
1758
2750
|
// src/plugins/speech/providers/local-whisper.ts
|
|
1759
|
-
import { execFile } from "child_process";
|
|
2751
|
+
import { execFile as execFile2 } from "child_process";
|
|
1760
2752
|
import { existsSync as existsSync6 } from "fs";
|
|
1761
|
-
import { mkdtemp, rm, writeFile } from "fs/promises";
|
|
2753
|
+
import { mkdtemp, rm as rm2, writeFile as writeFile2 } from "fs/promises";
|
|
1762
2754
|
import { tmpdir } from "os";
|
|
1763
|
-
import
|
|
2755
|
+
import path16 from "path";
|
|
1764
2756
|
import { fileURLToPath } from "url";
|
|
1765
|
-
import { promisify } from "util";
|
|
2757
|
+
import { promisify as promisify2 } from "util";
|
|
1766
2758
|
function resolveLocalWhisperScriptPath() {
|
|
1767
|
-
const moduleDir =
|
|
2759
|
+
const moduleDir = path16.dirname(fileURLToPath(import.meta.url));
|
|
1768
2760
|
const candidates = [
|
|
1769
|
-
|
|
1770
|
-
|
|
2761
|
+
path16.resolve(moduleDir, "../scripts/transcribe_audio.sh"),
|
|
2762
|
+
path16.resolve(moduleDir, "speech/transcribe_audio.sh")
|
|
1771
2763
|
];
|
|
1772
2764
|
return candidates.find(existsSync6) ?? candidates[0];
|
|
1773
2765
|
}
|
|
1774
|
-
var
|
|
2766
|
+
var execFileAsync2, LOCAL_WHISPER_PROVIDER, LOCAL_WHISPER_DEFAULTS;
|
|
1775
2767
|
var init_local_whisper = __esm({
|
|
1776
2768
|
"src/plugins/speech/providers/local-whisper.ts"() {
|
|
1777
2769
|
"use strict";
|
|
1778
|
-
|
|
2770
|
+
execFileAsync2 = promisify2(execFile2);
|
|
1779
2771
|
LOCAL_WHISPER_PROVIDER = "local-whisper";
|
|
1780
2772
|
LOCAL_WHISPER_DEFAULTS = {
|
|
1781
2773
|
language: "ru",
|
|
@@ -1805,7 +2797,7 @@ function readLocalWhisperSettings(raw) {
|
|
|
1805
2797
|
function buildSpeechServiceConfig(raw) {
|
|
1806
2798
|
const groqApiKey = readOptionalString(raw.groqApiKey);
|
|
1807
2799
|
const requestedProvider = readOptionalString(raw.sttProvider);
|
|
1808
|
-
const provider = requestedProvider === LOCAL_WHISPER_PROVIDER ? LOCAL_WHISPER_PROVIDER : groqApiKey ? "groq" : null;
|
|
2800
|
+
const provider = requestedProvider === LOCAL_WHISPER_PROVIDER ? LOCAL_WHISPER_PROVIDER : requestedProvider === "groq" && groqApiKey ? "groq" : raw.sttProvider === void 0 && groqApiKey ? "groq" : null;
|
|
1809
2801
|
const providers = {};
|
|
1810
2802
|
if (groqApiKey) providers.groq = { apiKey: groqApiKey };
|
|
1811
2803
|
if (provider === LOCAL_WHISPER_PROVIDER) {
|
|
@@ -1856,23 +2848,23 @@ var init_proxy_types = __esm({
|
|
|
1856
2848
|
});
|
|
1857
2849
|
|
|
1858
2850
|
// src/core/network/proxy-store.ts
|
|
1859
|
-
import
|
|
1860
|
-
import
|
|
2851
|
+
import fs17 from "fs";
|
|
2852
|
+
import path17 from "path";
|
|
1861
2853
|
import net from "net";
|
|
1862
2854
|
function atomicWrite(file, value, mode) {
|
|
1863
|
-
|
|
2855
|
+
fs17.mkdirSync(path17.dirname(file), { recursive: true, mode: 448 });
|
|
1864
2856
|
const tmp = `${file}.${process.pid}.${Date.now()}.tmp`;
|
|
1865
|
-
|
|
2857
|
+
fs17.writeFileSync(tmp, `${JSON.stringify(value, null, 2)}
|
|
1866
2858
|
`, { mode, flag: "wx" });
|
|
1867
|
-
|
|
1868
|
-
const fd =
|
|
1869
|
-
|
|
1870
|
-
|
|
1871
|
-
|
|
2859
|
+
fs17.chmodSync(tmp, mode);
|
|
2860
|
+
const fd = fs17.openSync(tmp, "r");
|
|
2861
|
+
fs17.fsyncSync(fd);
|
|
2862
|
+
fs17.closeSync(fd);
|
|
2863
|
+
fs17.renameSync(tmp, file);
|
|
1872
2864
|
try {
|
|
1873
|
-
const dir =
|
|
1874
|
-
|
|
1875
|
-
|
|
2865
|
+
const dir = fs17.openSync(path17.dirname(file), "r");
|
|
2866
|
+
fs17.fsyncSync(dir);
|
|
2867
|
+
fs17.closeSync(dir);
|
|
1876
2868
|
} catch {
|
|
1877
2869
|
}
|
|
1878
2870
|
}
|
|
@@ -1964,7 +2956,7 @@ var init_proxy_store = __esm({
|
|
|
1964
2956
|
};
|
|
1965
2957
|
ProxyStoreCorruptError = class extends ProxyStoreError {
|
|
1966
2958
|
constructor(file, reason, quarantine, lkgAvailable = false) {
|
|
1967
|
-
super("PROXY_STORE_CORRUPT", `Proxy policy store is invalid (${
|
|
2959
|
+
super("PROXY_STORE_CORRUPT", `Proxy policy store is invalid (${path17.basename(file)}): ${reason}`, { file, quarantine, lkgAvailable });
|
|
1968
2960
|
this.name = "ProxyStoreCorruptError";
|
|
1969
2961
|
}
|
|
1970
2962
|
};
|
|
@@ -1981,11 +2973,11 @@ var init_proxy_store = __esm({
|
|
|
1981
2973
|
lkgPath;
|
|
1982
2974
|
lockPath;
|
|
1983
2975
|
constructor(instanceRoot) {
|
|
1984
|
-
this.configPath =
|
|
1985
|
-
this.secretsPath =
|
|
1986
|
-
this.journalPath =
|
|
1987
|
-
this.lkgPath =
|
|
1988
|
-
this.lockPath =
|
|
2976
|
+
this.configPath = path17.join(instanceRoot, "proxy.json");
|
|
2977
|
+
this.secretsPath = path17.join(instanceRoot, "proxy-secrets.json");
|
|
2978
|
+
this.journalPath = path17.join(instanceRoot, "proxy-transaction.json");
|
|
2979
|
+
this.lkgPath = path17.join(instanceRoot, "proxy-lkg.json");
|
|
2980
|
+
this.lockPath = path17.join(instanceRoot, "proxy.lock");
|
|
1989
2981
|
const lock = this.acquireLock();
|
|
1990
2982
|
try {
|
|
1991
2983
|
this.cleanupOrphans(instanceRoot);
|
|
@@ -1996,18 +2988,18 @@ var init_proxy_store = __esm({
|
|
|
1996
2988
|
}
|
|
1997
2989
|
cleanupOrphans(root) {
|
|
1998
2990
|
try {
|
|
1999
|
-
const names =
|
|
2000
|
-
for (const name of names) if (/^proxy.*\.tmp$/.test(name))
|
|
2991
|
+
const names = fs17.readdirSync(root);
|
|
2992
|
+
for (const name of names) if (/^proxy.*\.tmp$/.test(name)) fs17.rmSync(path17.join(root, name), { force: true });
|
|
2001
2993
|
const quarantines = names.filter((n) => n.includes(".corrupt.")).sort().reverse();
|
|
2002
|
-
for (const name of quarantines.slice(3))
|
|
2994
|
+
for (const name of quarantines.slice(3)) fs17.rmSync(path17.join(root, name), { force: true });
|
|
2003
2995
|
} catch {
|
|
2004
2996
|
}
|
|
2005
2997
|
}
|
|
2006
2998
|
quarantine(file) {
|
|
2007
2999
|
try {
|
|
2008
3000
|
const target = `${file}.corrupt.${Date.now()}`;
|
|
2009
|
-
|
|
2010
|
-
|
|
3001
|
+
fs17.copyFileSync(file, target, fs17.constants.COPYFILE_EXCL);
|
|
3002
|
+
fs17.chmodSync(target, 384);
|
|
2011
3003
|
return target;
|
|
2012
3004
|
} catch {
|
|
2013
3005
|
return void 0;
|
|
@@ -2015,28 +3007,28 @@ var init_proxy_store = __esm({
|
|
|
2015
3007
|
}
|
|
2016
3008
|
readConfigFile(file = this.configPath) {
|
|
2017
3009
|
try {
|
|
2018
|
-
return validateConfig(JSON.parse(
|
|
3010
|
+
return validateConfig(JSON.parse(fs17.readFileSync(file, "utf8")));
|
|
2019
3011
|
} catch (error) {
|
|
2020
3012
|
if (error.code === "ENOENT" && file === this.configPath) return structuredClone(DEFAULT_CONFIG2);
|
|
2021
3013
|
const q = file === this.configPath ? this.quarantine(file) : void 0;
|
|
2022
|
-
throw new ProxyStoreCorruptError(file, error.message, q,
|
|
3014
|
+
throw new ProxyStoreCorruptError(file, error.message, q, fs17.existsSync(this.lkgPath));
|
|
2023
3015
|
}
|
|
2024
3016
|
}
|
|
2025
3017
|
readSecretsFile() {
|
|
2026
3018
|
try {
|
|
2027
|
-
const stat =
|
|
3019
|
+
const stat = fs17.statSync(this.secretsPath);
|
|
2028
3020
|
if ((stat.mode & 63) !== 0) throw new Error("secret file mode must be 0600");
|
|
2029
|
-
return validateSecrets(JSON.parse(
|
|
3021
|
+
return validateSecrets(JSON.parse(fs17.readFileSync(this.secretsPath, "utf8")));
|
|
2030
3022
|
} catch (error) {
|
|
2031
3023
|
if (error.code === "ENOENT") return {};
|
|
2032
3024
|
const q = this.quarantine(this.secretsPath);
|
|
2033
|
-
throw new ProxyStoreCorruptError(this.secretsPath, error.message, q,
|
|
3025
|
+
throw new ProxyStoreCorruptError(this.secretsPath, error.message, q, fs17.existsSync(this.lkgPath));
|
|
2034
3026
|
}
|
|
2035
3027
|
}
|
|
2036
3028
|
recoverJournal() {
|
|
2037
|
-
if (!
|
|
3029
|
+
if (!fs17.existsSync(this.journalPath)) return;
|
|
2038
3030
|
try {
|
|
2039
|
-
const tx = JSON.parse(
|
|
3031
|
+
const tx = JSON.parse(fs17.readFileSync(this.journalPath, "utf8"));
|
|
2040
3032
|
if (tx.version !== 1) throw new Error("unsupported transaction version");
|
|
2041
3033
|
const config = validateConfig(tx.config);
|
|
2042
3034
|
const secrets = validateSecrets(tx.secrets);
|
|
@@ -2044,9 +3036,9 @@ var init_proxy_store = __esm({
|
|
|
2044
3036
|
atomicWrite(this.configPath, config, 384);
|
|
2045
3037
|
atomicWrite(this.secretsPath, secrets, 384);
|
|
2046
3038
|
atomicWrite(this.lkgPath, tx, 384);
|
|
2047
|
-
|
|
3039
|
+
fs17.unlinkSync(this.journalPath);
|
|
2048
3040
|
} catch (error) {
|
|
2049
|
-
throw new ProxyStoreCorruptError(this.journalPath, error.message, this.quarantine(this.journalPath),
|
|
3041
|
+
throw new ProxyStoreCorruptError(this.journalPath, error.message, this.quarantine(this.journalPath), fs17.existsSync(this.lkgPath));
|
|
2050
3042
|
}
|
|
2051
3043
|
}
|
|
2052
3044
|
load() {
|
|
@@ -2055,7 +3047,7 @@ var init_proxy_store = __esm({
|
|
|
2055
3047
|
validateCredentialConsistency(config, this.readSecretsFile());
|
|
2056
3048
|
} catch (error) {
|
|
2057
3049
|
if (error instanceof ProxyStoreCorruptError) throw error;
|
|
2058
|
-
throw new ProxyStoreCorruptError(this.secretsPath, error.message, this.quarantine(this.secretsPath),
|
|
3050
|
+
throw new ProxyStoreCorruptError(this.secretsPath, error.message, this.quarantine(this.secretsPath), fs17.existsSync(this.lkgPath));
|
|
2059
3051
|
}
|
|
2060
3052
|
return config;
|
|
2061
3053
|
}
|
|
@@ -2065,7 +3057,7 @@ var init_proxy_store = __esm({
|
|
|
2065
3057
|
try {
|
|
2066
3058
|
validateCredentialConsistency(config, secrets);
|
|
2067
3059
|
} catch (error) {
|
|
2068
|
-
throw new ProxyStoreCorruptError(this.secretsPath, error.message, this.quarantine(this.secretsPath),
|
|
3060
|
+
throw new ProxyStoreCorruptError(this.secretsPath, error.message, this.quarantine(this.secretsPath), fs17.existsSync(this.lkgPath));
|
|
2069
3061
|
}
|
|
2070
3062
|
return secrets;
|
|
2071
3063
|
}
|
|
@@ -2086,7 +3078,7 @@ var init_proxy_store = __esm({
|
|
|
2086
3078
|
atomicWrite(this.configPath, next, 384);
|
|
2087
3079
|
atomicWrite(this.secretsPath, cleanSecrets, 384);
|
|
2088
3080
|
atomicWrite(this.lkgPath, tx, 384);
|
|
2089
|
-
|
|
3081
|
+
fs17.unlinkSync(this.journalPath);
|
|
2090
3082
|
return next;
|
|
2091
3083
|
} finally {
|
|
2092
3084
|
this.releaseLock(lock);
|
|
@@ -2094,21 +3086,21 @@ var init_proxy_store = __esm({
|
|
|
2094
3086
|
}
|
|
2095
3087
|
releaseLock(fd) {
|
|
2096
3088
|
try {
|
|
2097
|
-
|
|
3089
|
+
fs17.closeSync(fd);
|
|
2098
3090
|
} catch {
|
|
2099
3091
|
}
|
|
2100
3092
|
try {
|
|
2101
|
-
|
|
3093
|
+
fs17.unlinkSync(this.lockPath);
|
|
2102
3094
|
} catch {
|
|
2103
3095
|
}
|
|
2104
3096
|
}
|
|
2105
3097
|
acquireLock() {
|
|
2106
|
-
|
|
3098
|
+
fs17.mkdirSync(path17.dirname(this.lockPath), { recursive: true, mode: 448 });
|
|
2107
3099
|
for (let attempt = 0; attempt < 200; attempt++) {
|
|
2108
3100
|
try {
|
|
2109
|
-
const fd =
|
|
2110
|
-
|
|
2111
|
-
|
|
3101
|
+
const fd = fs17.openSync(this.lockPath, "wx", 384);
|
|
3102
|
+
fs17.writeFileSync(fd, JSON.stringify({ pid: process.pid, createdAt: Date.now() }));
|
|
3103
|
+
fs17.fsyncSync(fd);
|
|
2112
3104
|
return fd;
|
|
2113
3105
|
} catch (error) {
|
|
2114
3106
|
if (error.code !== "EEXIST") throw error;
|
|
@@ -2120,7 +3112,7 @@ var init_proxy_store = __esm({
|
|
|
2120
3112
|
}
|
|
2121
3113
|
removeStaleLock() {
|
|
2122
3114
|
try {
|
|
2123
|
-
const lock = JSON.parse(
|
|
3115
|
+
const lock = JSON.parse(fs17.readFileSync(this.lockPath, "utf8"));
|
|
2124
3116
|
const old = !Number.isFinite(lock.createdAt) || Date.now() - Number(lock.createdAt) > 3e4;
|
|
2125
3117
|
let dead = false;
|
|
2126
3118
|
if (Number.isInteger(lock.pid)) {
|
|
@@ -2131,13 +3123,13 @@ var init_proxy_store = __esm({
|
|
|
2131
3123
|
}
|
|
2132
3124
|
}
|
|
2133
3125
|
if (dead || old && !Number.isInteger(lock.pid)) {
|
|
2134
|
-
|
|
3126
|
+
fs17.unlinkSync(this.lockPath);
|
|
2135
3127
|
return true;
|
|
2136
3128
|
}
|
|
2137
3129
|
} catch {
|
|
2138
3130
|
try {
|
|
2139
|
-
if (Date.now() -
|
|
2140
|
-
|
|
3131
|
+
if (Date.now() - fs17.statSync(this.lockPath).mtimeMs > 3e4) {
|
|
3132
|
+
fs17.unlinkSync(this.lockPath);
|
|
2141
3133
|
return true;
|
|
2142
3134
|
}
|
|
2143
3135
|
} catch {
|
|
@@ -2147,7 +3139,7 @@ var init_proxy_store = __esm({
|
|
|
2147
3139
|
}
|
|
2148
3140
|
loadLastKnownGood() {
|
|
2149
3141
|
try {
|
|
2150
|
-
return validateConfig(JSON.parse(
|
|
3142
|
+
return validateConfig(JSON.parse(fs17.readFileSync(this.lkgPath, "utf8")).config);
|
|
2151
3143
|
} catch {
|
|
2152
3144
|
return void 0;
|
|
2153
3145
|
}
|
|
@@ -2159,7 +3151,7 @@ var init_proxy_store = __esm({
|
|
|
2159
3151
|
// src/core/network/proxy-service.ts
|
|
2160
3152
|
import { ProxyAgent } from "proxy-agent";
|
|
2161
3153
|
import nodeFetch from "node-fetch";
|
|
2162
|
-
import
|
|
3154
|
+
import fs18 from "fs";
|
|
2163
3155
|
import net2 from "net";
|
|
2164
3156
|
import { Readable } from "stream";
|
|
2165
3157
|
import debug from "debug";
|
|
@@ -2331,6 +3323,7 @@ var init_proxy_service = __esm({
|
|
|
2331
3323
|
"services.npmUpdate",
|
|
2332
3324
|
"services.agentRegistry",
|
|
2333
3325
|
"services.pluginInstaller",
|
|
3326
|
+
"services.speech",
|
|
2334
3327
|
"services.speechDownloads",
|
|
2335
3328
|
"plugins.default"
|
|
2336
3329
|
]);
|
|
@@ -2534,11 +3527,11 @@ var init_proxy_service = __esm({
|
|
|
2534
3527
|
return this.saveProfile(this.parseEnvFile(id, envFile, name));
|
|
2535
3528
|
}
|
|
2536
3529
|
parseEnvFile(id, envFile, name) {
|
|
2537
|
-
const stat =
|
|
3530
|
+
const stat = fs18.statSync(envFile);
|
|
2538
3531
|
if (!stat.isFile()) throw new Error("Proxy env path is not a file");
|
|
2539
3532
|
if ((stat.mode & 63) !== 0) throw new Error("Proxy env file must have mode 0600");
|
|
2540
3533
|
const values = {};
|
|
2541
|
-
for (const rawLine of
|
|
3534
|
+
for (const rawLine of fs18.readFileSync(envFile, "utf8").split(/\r?\n/)) {
|
|
2542
3535
|
const line = rawLine.trim();
|
|
2543
3536
|
if (!line || line.startsWith("#")) continue;
|
|
2544
3537
|
const match = /^(?:export\s+)?([A-Za-z_][A-Za-z0-9_]*)=(.*)$/.exec(line);
|
|
@@ -3040,7 +4033,7 @@ var init_proxy_service = __esm({
|
|
|
3040
4033
|
});
|
|
3041
4034
|
|
|
3042
4035
|
// src/core/doctor/checks/config.ts
|
|
3043
|
-
import * as
|
|
4036
|
+
import * as fs19 from "fs";
|
|
3044
4037
|
var configCheck;
|
|
3045
4038
|
var init_config2 = __esm({
|
|
3046
4039
|
"src/core/doctor/checks/config.ts"() {
|
|
@@ -3052,14 +4045,14 @@ var init_config2 = __esm({
|
|
|
3052
4045
|
order: 1,
|
|
3053
4046
|
async run(ctx) {
|
|
3054
4047
|
const results = [];
|
|
3055
|
-
if (!
|
|
4048
|
+
if (!fs19.existsSync(ctx.configPath)) {
|
|
3056
4049
|
results.push({ status: "fail", message: "Config file not found" });
|
|
3057
4050
|
return results;
|
|
3058
4051
|
}
|
|
3059
4052
|
results.push({ status: "pass", message: "Config file exists" });
|
|
3060
4053
|
let raw;
|
|
3061
4054
|
try {
|
|
3062
|
-
raw = JSON.parse(
|
|
4055
|
+
raw = JSON.parse(fs19.readFileSync(ctx.configPath, "utf-8"));
|
|
3063
4056
|
} catch (err) {
|
|
3064
4057
|
results.push({
|
|
3065
4058
|
status: "fail",
|
|
@@ -3078,7 +4071,7 @@ var init_config2 = __esm({
|
|
|
3078
4071
|
fixRisk: "safe",
|
|
3079
4072
|
fix: async () => {
|
|
3080
4073
|
applyMigrations(raw);
|
|
3081
|
-
|
|
4074
|
+
fs19.writeFileSync(ctx.configPath, JSON.stringify(raw, null, 2));
|
|
3082
4075
|
return { success: true, message: "applied migrations" };
|
|
3083
4076
|
}
|
|
3084
4077
|
});
|
|
@@ -3102,8 +4095,8 @@ var init_config2 = __esm({
|
|
|
3102
4095
|
|
|
3103
4096
|
// src/core/doctor/checks/agents.ts
|
|
3104
4097
|
import { execFileSync as execFileSync3 } from "child_process";
|
|
3105
|
-
import * as
|
|
3106
|
-
import * as
|
|
4098
|
+
import * as fs20 from "fs";
|
|
4099
|
+
import * as path19 from "path";
|
|
3107
4100
|
function commandExists2(cmd) {
|
|
3108
4101
|
try {
|
|
3109
4102
|
execFileSync3("which", [cmd], { stdio: "pipe" });
|
|
@@ -3112,9 +4105,9 @@ function commandExists2(cmd) {
|
|
|
3112
4105
|
}
|
|
3113
4106
|
let dir = process.cwd();
|
|
3114
4107
|
while (true) {
|
|
3115
|
-
const binPath =
|
|
3116
|
-
if (
|
|
3117
|
-
const parent =
|
|
4108
|
+
const binPath = path19.join(dir, "node_modules", ".bin", cmd);
|
|
4109
|
+
if (fs20.existsSync(binPath)) return true;
|
|
4110
|
+
const parent = path19.dirname(dir);
|
|
3118
4111
|
if (parent === dir) break;
|
|
3119
4112
|
dir = parent;
|
|
3120
4113
|
}
|
|
@@ -3136,9 +4129,9 @@ var init_agents = __esm({
|
|
|
3136
4129
|
const defaultAgent = ctx.config.defaultAgent;
|
|
3137
4130
|
let agents = {};
|
|
3138
4131
|
try {
|
|
3139
|
-
const agentsPath =
|
|
3140
|
-
if (
|
|
3141
|
-
const data = JSON.parse(
|
|
4132
|
+
const agentsPath = path19.join(ctx.dataDir, "agents.json");
|
|
4133
|
+
if (fs20.existsSync(agentsPath)) {
|
|
4134
|
+
const data = JSON.parse(fs20.readFileSync(agentsPath, "utf-8"));
|
|
3142
4135
|
agents = data.installed ?? {};
|
|
3143
4136
|
}
|
|
3144
4137
|
} catch {
|
|
@@ -3189,8 +4182,61 @@ var settings_manager_exports = {};
|
|
|
3189
4182
|
__export(settings_manager_exports, {
|
|
3190
4183
|
SettingsManager: () => SettingsManager
|
|
3191
4184
|
});
|
|
3192
|
-
import
|
|
3193
|
-
import
|
|
4185
|
+
import fs21 from "fs";
|
|
4186
|
+
import path20 from "path";
|
|
4187
|
+
import { randomUUID as randomUUID4 } from "crypto";
|
|
4188
|
+
function secureSettingsDirectory(directory) {
|
|
4189
|
+
fs21.mkdirSync(directory, { recursive: true, mode: 448 });
|
|
4190
|
+
fs21.chmodSync(directory, 448);
|
|
4191
|
+
}
|
|
4192
|
+
function secureSettingsTree(basePath, settingsPath, create) {
|
|
4193
|
+
const target = path20.dirname(settingsPath);
|
|
4194
|
+
const relative2 = path20.relative(basePath, target);
|
|
4195
|
+
if (relative2.startsWith("..") || path20.isAbsolute(relative2)) throw new Error("Settings path escapes its base directory");
|
|
4196
|
+
secureSettingsDirectory(basePath);
|
|
4197
|
+
let current = basePath;
|
|
4198
|
+
for (const segment of relative2 ? relative2.split(path20.sep) : []) {
|
|
4199
|
+
current = path20.join(current, segment);
|
|
4200
|
+
if (create || fs21.existsSync(current)) secureSettingsDirectory(current);
|
|
4201
|
+
}
|
|
4202
|
+
}
|
|
4203
|
+
function repairSettingsPermissions(settingsPath, basePath) {
|
|
4204
|
+
if (!fs21.existsSync(settingsPath)) return;
|
|
4205
|
+
secureSettingsTree(basePath, settingsPath, false);
|
|
4206
|
+
fs21.chmodSync(settingsPath, 384);
|
|
4207
|
+
}
|
|
4208
|
+
function atomicSettingsWrite(settingsPath, basePath, data) {
|
|
4209
|
+
const directory = path20.dirname(settingsPath);
|
|
4210
|
+
secureSettingsTree(basePath, settingsPath, true);
|
|
4211
|
+
const temporary = `${settingsPath}.${process.pid}.${randomUUID4()}.tmp`;
|
|
4212
|
+
try {
|
|
4213
|
+
fs21.writeFileSync(temporary, `${JSON.stringify(data, null, 2)}
|
|
4214
|
+
`, { mode: 384, flag: "wx" });
|
|
4215
|
+
fs21.chmodSync(temporary, 384);
|
|
4216
|
+
const fd = fs21.openSync(temporary, "r");
|
|
4217
|
+
try {
|
|
4218
|
+
fs21.fsyncSync(fd);
|
|
4219
|
+
} finally {
|
|
4220
|
+
fs21.closeSync(fd);
|
|
4221
|
+
}
|
|
4222
|
+
fs21.renameSync(temporary, settingsPath);
|
|
4223
|
+
fs21.chmodSync(settingsPath, 384);
|
|
4224
|
+
try {
|
|
4225
|
+
const directoryFd = fs21.openSync(directory, "r");
|
|
4226
|
+
try {
|
|
4227
|
+
fs21.fsyncSync(directoryFd);
|
|
4228
|
+
} finally {
|
|
4229
|
+
fs21.closeSync(directoryFd);
|
|
4230
|
+
}
|
|
4231
|
+
} catch {
|
|
4232
|
+
}
|
|
4233
|
+
} finally {
|
|
4234
|
+
try {
|
|
4235
|
+
if (fs21.existsSync(temporary)) fs21.unlinkSync(temporary);
|
|
4236
|
+
} catch {
|
|
4237
|
+
}
|
|
4238
|
+
}
|
|
4239
|
+
}
|
|
3194
4240
|
var SettingsManager, SettingsAPIImpl;
|
|
3195
4241
|
var init_settings_manager = __esm({
|
|
3196
4242
|
"src/core/plugin/settings-manager.ts"() {
|
|
@@ -3198,6 +4244,7 @@ var init_settings_manager = __esm({
|
|
|
3198
4244
|
SettingsManager = class {
|
|
3199
4245
|
constructor(basePath) {
|
|
3200
4246
|
this.basePath = basePath;
|
|
4247
|
+
secureSettingsDirectory(basePath);
|
|
3201
4248
|
}
|
|
3202
4249
|
basePath;
|
|
3203
4250
|
/** Returns the base path for all plugin settings directories. */
|
|
@@ -3207,13 +4254,16 @@ var init_settings_manager = __esm({
|
|
|
3207
4254
|
/** Create a SettingsAPI instance scoped to a specific plugin. */
|
|
3208
4255
|
createAPI(pluginName) {
|
|
3209
4256
|
const settingsPath = this.getSettingsPath(pluginName);
|
|
3210
|
-
return new SettingsAPIImpl(settingsPath);
|
|
4257
|
+
return new SettingsAPIImpl(settingsPath, this.basePath);
|
|
3211
4258
|
}
|
|
3212
4259
|
/** Load a plugin's settings from disk. Returns empty object if file doesn't exist. */
|
|
3213
4260
|
async loadSettings(pluginName) {
|
|
3214
4261
|
const settingsPath = this.getSettingsPath(pluginName);
|
|
4262
|
+
secureSettingsDirectory(this.basePath);
|
|
4263
|
+
if (!fs21.existsSync(settingsPath)) return {};
|
|
4264
|
+
repairSettingsPermissions(settingsPath, this.basePath);
|
|
3215
4265
|
try {
|
|
3216
|
-
const content =
|
|
4266
|
+
const content = fs21.readFileSync(settingsPath, "utf-8");
|
|
3217
4267
|
return JSON.parse(content);
|
|
3218
4268
|
} catch {
|
|
3219
4269
|
return {};
|
|
@@ -3233,7 +4283,7 @@ var init_settings_manager = __esm({
|
|
|
3233
4283
|
}
|
|
3234
4284
|
/** Resolve the absolute path to a plugin's settings.json file. */
|
|
3235
4285
|
getSettingsPath(pluginName) {
|
|
3236
|
-
return
|
|
4286
|
+
return path20.join(this.basePath, pluginName, "settings.json");
|
|
3237
4287
|
}
|
|
3238
4288
|
async getPluginSettings(pluginName) {
|
|
3239
4289
|
return this.loadSettings(pluginName);
|
|
@@ -3246,15 +4296,19 @@ var init_settings_manager = __esm({
|
|
|
3246
4296
|
}
|
|
3247
4297
|
};
|
|
3248
4298
|
SettingsAPIImpl = class {
|
|
3249
|
-
constructor(settingsPath) {
|
|
4299
|
+
constructor(settingsPath, basePath) {
|
|
3250
4300
|
this.settingsPath = settingsPath;
|
|
4301
|
+
this.basePath = basePath;
|
|
3251
4302
|
}
|
|
3252
4303
|
settingsPath;
|
|
4304
|
+
basePath;
|
|
3253
4305
|
cache = null;
|
|
3254
4306
|
readFile() {
|
|
3255
4307
|
if (this.cache !== null) return this.cache;
|
|
4308
|
+
secureSettingsDirectory(this.basePath);
|
|
4309
|
+
if (fs21.existsSync(this.settingsPath)) repairSettingsPermissions(this.settingsPath, this.basePath);
|
|
3256
4310
|
try {
|
|
3257
|
-
const content =
|
|
4311
|
+
const content = fs21.readFileSync(this.settingsPath, "utf-8");
|
|
3258
4312
|
this.cache = JSON.parse(content);
|
|
3259
4313
|
return this.cache;
|
|
3260
4314
|
} catch {
|
|
@@ -3263,9 +4317,7 @@ var init_settings_manager = __esm({
|
|
|
3263
4317
|
}
|
|
3264
4318
|
}
|
|
3265
4319
|
writeFile(data) {
|
|
3266
|
-
|
|
3267
|
-
fs19.mkdirSync(dir, { recursive: true });
|
|
3268
|
-
fs19.writeFileSync(this.settingsPath, JSON.stringify(data, null, 2));
|
|
4320
|
+
atomicSettingsWrite(this.settingsPath, this.basePath, data);
|
|
3269
4321
|
this.cache = data;
|
|
3270
4322
|
}
|
|
3271
4323
|
async get(key) {
|
|
@@ -3307,20 +4359,20 @@ __export(command_ownership_store_exports, {
|
|
|
3307
4359
|
telegramCommandHostId: () => telegramCommandHostId,
|
|
3308
4360
|
telegramCommandInstanceKey: () => telegramCommandInstanceKey
|
|
3309
4361
|
});
|
|
3310
|
-
import
|
|
3311
|
-
import
|
|
4362
|
+
import fs22 from "fs";
|
|
4363
|
+
import path21 from "path";
|
|
3312
4364
|
import os5 from "os";
|
|
3313
|
-
import { createHash as
|
|
4365
|
+
import { createHash as createHash4 } from "crypto";
|
|
3314
4366
|
function telegramCommandHostId() {
|
|
3315
4367
|
let source = `${os5.hostname()}|${typeof process.getuid === "function" ? process.getuid() : "unknown"}`;
|
|
3316
4368
|
try {
|
|
3317
|
-
source =
|
|
4369
|
+
source = fs22.readFileSync("/etc/machine-id", "utf8").trim() || source;
|
|
3318
4370
|
} catch {
|
|
3319
4371
|
}
|
|
3320
|
-
return
|
|
4372
|
+
return createHash4("sha256").update(source).digest("hex");
|
|
3321
4373
|
}
|
|
3322
4374
|
function telegramCommandInstanceKey(instanceRoot) {
|
|
3323
|
-
return
|
|
4375
|
+
return createHash4("sha256").update(path21.resolve(instanceRoot)).digest("hex");
|
|
3324
4376
|
}
|
|
3325
4377
|
function emptyLedger() {
|
|
3326
4378
|
return { version: STORE_VERSION, bots: {} };
|
|
@@ -3368,21 +4420,21 @@ var init_command_ownership_store = __esm({
|
|
|
3368
4420
|
file;
|
|
3369
4421
|
lockFile;
|
|
3370
4422
|
constructor(instanceRoot) {
|
|
3371
|
-
this.file =
|
|
4423
|
+
this.file = path21.join(instanceRoot, "telegram-command-ownership.json");
|
|
3372
4424
|
this.lockFile = `${this.file}.lock`;
|
|
3373
4425
|
}
|
|
3374
4426
|
async withLock(operation) {
|
|
3375
|
-
|
|
4427
|
+
fs22.mkdirSync(path21.dirname(this.file), { recursive: true, mode: 448 });
|
|
3376
4428
|
const deadline = Date.now() + 1e4;
|
|
3377
4429
|
let fd;
|
|
3378
4430
|
while (fd === void 0) {
|
|
3379
4431
|
try {
|
|
3380
|
-
fd =
|
|
3381
|
-
|
|
4432
|
+
fd = fs22.openSync(this.lockFile, "wx", 384);
|
|
4433
|
+
fs22.writeFileSync(fd, String(process.pid));
|
|
3382
4434
|
} catch (error) {
|
|
3383
4435
|
if (error.code !== "EEXIST") throw error;
|
|
3384
4436
|
try {
|
|
3385
|
-
const owner = Number(
|
|
4437
|
+
const owner = Number(fs22.readFileSync(this.lockFile, "utf8"));
|
|
3386
4438
|
let alive = Number.isSafeInteger(owner) && owner > 0;
|
|
3387
4439
|
if (alive) {
|
|
3388
4440
|
try {
|
|
@@ -3391,34 +4443,34 @@ var init_command_ownership_store = __esm({
|
|
|
3391
4443
|
alive = false;
|
|
3392
4444
|
}
|
|
3393
4445
|
}
|
|
3394
|
-
if (!alive)
|
|
4446
|
+
if (!alive) fs22.unlinkSync(this.lockFile);
|
|
3395
4447
|
} catch {
|
|
3396
4448
|
}
|
|
3397
4449
|
if (Date.now() >= deadline) throw new Error("Timed out waiting for Telegram command ownership lock");
|
|
3398
|
-
await new Promise((
|
|
4450
|
+
await new Promise((resolve8) => setTimeout(resolve8, 50));
|
|
3399
4451
|
}
|
|
3400
4452
|
}
|
|
3401
4453
|
try {
|
|
3402
4454
|
return await operation(this.load());
|
|
3403
4455
|
} finally {
|
|
3404
4456
|
try {
|
|
3405
|
-
|
|
4457
|
+
fs22.closeSync(fd);
|
|
3406
4458
|
} catch {
|
|
3407
4459
|
}
|
|
3408
4460
|
try {
|
|
3409
|
-
|
|
4461
|
+
fs22.unlinkSync(this.lockFile);
|
|
3410
4462
|
} catch {
|
|
3411
4463
|
}
|
|
3412
4464
|
}
|
|
3413
4465
|
}
|
|
3414
4466
|
load() {
|
|
3415
|
-
if (!
|
|
4467
|
+
if (!fs22.existsSync(this.file)) return { ledger: emptyLedger(), conservative: false };
|
|
3416
4468
|
try {
|
|
3417
|
-
return { ledger: validateLedger(JSON.parse(
|
|
4469
|
+
return { ledger: validateLedger(JSON.parse(fs22.readFileSync(this.file, "utf8"))), conservative: false };
|
|
3418
4470
|
} catch {
|
|
3419
4471
|
const quarantine = `${this.file}.corrupt.${Date.now()}`;
|
|
3420
4472
|
try {
|
|
3421
|
-
|
|
4473
|
+
fs22.renameSync(this.file, quarantine);
|
|
3422
4474
|
} catch {
|
|
3423
4475
|
}
|
|
3424
4476
|
return { ledger: emptyLedger(), conservative: true };
|
|
@@ -3499,19 +4551,19 @@ var init_command_ownership_store = __esm({
|
|
|
3499
4551
|
}
|
|
3500
4552
|
save(ledger) {
|
|
3501
4553
|
const clean = validateLedger(ledger);
|
|
3502
|
-
|
|
4554
|
+
fs22.mkdirSync(path21.dirname(this.file), { recursive: true, mode: 448 });
|
|
3503
4555
|
const tmp = `${this.file}.${process.pid}.${Date.now()}.tmp`;
|
|
3504
|
-
|
|
4556
|
+
fs22.writeFileSync(tmp, `${JSON.stringify(clean, null, 2)}
|
|
3505
4557
|
`, { mode: 384, flag: "wx" });
|
|
3506
|
-
|
|
3507
|
-
const fd =
|
|
3508
|
-
|
|
3509
|
-
|
|
3510
|
-
|
|
4558
|
+
fs22.chmodSync(tmp, 384);
|
|
4559
|
+
const fd = fs22.openSync(tmp, "r");
|
|
4560
|
+
fs22.fsyncSync(fd);
|
|
4561
|
+
fs22.closeSync(fd);
|
|
4562
|
+
fs22.renameSync(tmp, this.file);
|
|
3511
4563
|
try {
|
|
3512
|
-
const dir =
|
|
3513
|
-
|
|
3514
|
-
|
|
4564
|
+
const dir = fs22.openSync(path21.dirname(this.file), "r");
|
|
4565
|
+
fs22.fsyncSync(dir);
|
|
4566
|
+
fs22.closeSync(dir);
|
|
3515
4567
|
} catch {
|
|
3516
4568
|
}
|
|
3517
4569
|
}
|
|
@@ -3528,8 +4580,8 @@ __export(instance_context_exports, {
|
|
|
3528
4580
|
resolveInstanceRoot: () => resolveInstanceRoot,
|
|
3529
4581
|
resolveRunningInstance: () => resolveRunningInstance
|
|
3530
4582
|
});
|
|
3531
|
-
import
|
|
3532
|
-
import
|
|
4583
|
+
import path22 from "path";
|
|
4584
|
+
import fs23 from "fs";
|
|
3533
4585
|
import os6 from "os";
|
|
3534
4586
|
function createInstanceContext(opts) {
|
|
3535
4587
|
const { id, root } = opts;
|
|
@@ -3538,22 +4590,22 @@ function createInstanceContext(opts) {
|
|
|
3538
4590
|
id,
|
|
3539
4591
|
root,
|
|
3540
4592
|
paths: {
|
|
3541
|
-
config:
|
|
3542
|
-
sessions:
|
|
3543
|
-
agents:
|
|
3544
|
-
registryCache:
|
|
3545
|
-
plugins:
|
|
3546
|
-
pluginsData:
|
|
3547
|
-
pluginRegistry:
|
|
3548
|
-
logs:
|
|
3549
|
-
pid:
|
|
3550
|
-
running:
|
|
3551
|
-
apiPort:
|
|
3552
|
-
apiSecret:
|
|
3553
|
-
bin:
|
|
3554
|
-
cache:
|
|
3555
|
-
tunnels:
|
|
3556
|
-
agentsDir:
|
|
4593
|
+
config: path22.join(root, "config.json"),
|
|
4594
|
+
sessions: path22.join(root, "sessions.json"),
|
|
4595
|
+
agents: path22.join(root, "agents.json"),
|
|
4596
|
+
registryCache: path22.join(globalRoot, "cache", "registry-cache.json"),
|
|
4597
|
+
plugins: path22.join(root, "plugins"),
|
|
4598
|
+
pluginsData: path22.join(root, "plugins", "data"),
|
|
4599
|
+
pluginRegistry: path22.join(root, "plugins.json"),
|
|
4600
|
+
logs: path22.join(root, "logs"),
|
|
4601
|
+
pid: path22.join(root, "openacp.pid"),
|
|
4602
|
+
running: path22.join(root, "running"),
|
|
4603
|
+
apiPort: path22.join(root, "api.port"),
|
|
4604
|
+
apiSecret: path22.join(root, "api-secret"),
|
|
4605
|
+
bin: path22.join(globalRoot, "bin"),
|
|
4606
|
+
cache: path22.join(root, "cache"),
|
|
4607
|
+
tunnels: path22.join(root, "tunnels.json"),
|
|
4608
|
+
agentsDir: path22.join(globalRoot, "agents")
|
|
3557
4609
|
}
|
|
3558
4610
|
};
|
|
3559
4611
|
}
|
|
@@ -3562,50 +4614,50 @@ function generateSlug(name) {
|
|
|
3562
4614
|
return slug || "openacp";
|
|
3563
4615
|
}
|
|
3564
4616
|
function expandHome3(p) {
|
|
3565
|
-
if (p.startsWith("~")) return
|
|
4617
|
+
if (p.startsWith("~")) return path22.join(os6.homedir(), p.slice(1));
|
|
3566
4618
|
return p;
|
|
3567
4619
|
}
|
|
3568
4620
|
function resolveInstanceRoot(opts) {
|
|
3569
4621
|
const cwd = opts.cwd ?? process.cwd();
|
|
3570
4622
|
const home = os6.homedir();
|
|
3571
4623
|
const globalRoot = getGlobalRoot();
|
|
3572
|
-
if (opts.dir) return
|
|
3573
|
-
if (opts.local) return
|
|
3574
|
-
const cwdRoot =
|
|
3575
|
-
if (
|
|
3576
|
-
let dir =
|
|
4624
|
+
if (opts.dir) return path22.join(expandHome3(opts.dir), ".openacp");
|
|
4625
|
+
if (opts.local) return path22.join(cwd, ".openacp");
|
|
4626
|
+
const cwdRoot = path22.join(cwd, ".openacp");
|
|
4627
|
+
if (fs23.existsSync(path22.join(cwdRoot, "config.json"))) return cwdRoot;
|
|
4628
|
+
let dir = path22.resolve(cwd);
|
|
3577
4629
|
while (true) {
|
|
3578
|
-
const parent =
|
|
4630
|
+
const parent = path22.dirname(dir);
|
|
3579
4631
|
if (parent === dir) break;
|
|
3580
4632
|
dir = parent;
|
|
3581
|
-
const candidate =
|
|
4633
|
+
const candidate = path22.join(dir, ".openacp");
|
|
3582
4634
|
if (candidate === globalRoot) {
|
|
3583
4635
|
if (dir === home) break;
|
|
3584
4636
|
continue;
|
|
3585
4637
|
}
|
|
3586
|
-
if (
|
|
4638
|
+
if (fs23.existsSync(path22.join(candidate, "config.json"))) return candidate;
|
|
3587
4639
|
if (dir === home) break;
|
|
3588
4640
|
}
|
|
3589
|
-
if (
|
|
3590
|
-
const defaultWs =
|
|
3591
|
-
if (
|
|
4641
|
+
if (path22.resolve(cwd) === path22.resolve(home)) {
|
|
4642
|
+
const defaultWs = path22.join(home, "openacp-workspace", ".openacp");
|
|
4643
|
+
if (fs23.existsSync(path22.join(defaultWs, "config.json"))) return defaultWs;
|
|
3592
4644
|
}
|
|
3593
4645
|
if (process.env.OPENACP_INSTANCE_ROOT) return process.env.OPENACP_INSTANCE_ROOT;
|
|
3594
4646
|
return null;
|
|
3595
4647
|
}
|
|
3596
4648
|
function getGlobalRoot() {
|
|
3597
|
-
return
|
|
4649
|
+
return path22.join(os6.homedir(), ".openacp");
|
|
3598
4650
|
}
|
|
3599
4651
|
async function resolveRunningInstance(cwd) {
|
|
3600
4652
|
const globalRoot = getGlobalRoot();
|
|
3601
4653
|
const home = os6.homedir();
|
|
3602
|
-
let dir =
|
|
4654
|
+
let dir = path22.resolve(cwd);
|
|
3603
4655
|
while (true) {
|
|
3604
|
-
const candidate =
|
|
3605
|
-
if (candidate !== globalRoot &&
|
|
4656
|
+
const candidate = path22.join(dir, ".openacp");
|
|
4657
|
+
if (candidate !== globalRoot && fs23.existsSync(candidate)) {
|
|
3606
4658
|
if (await isInstanceRunning(candidate)) return candidate;
|
|
3607
4659
|
}
|
|
3608
|
-
const parent =
|
|
4660
|
+
const parent = path22.dirname(dir);
|
|
3609
4661
|
if (parent === dir) break;
|
|
3610
4662
|
if (dir === home) break;
|
|
3611
4663
|
dir = parent;
|
|
@@ -3613,9 +4665,9 @@ async function resolveRunningInstance(cwd) {
|
|
|
3613
4665
|
return null;
|
|
3614
4666
|
}
|
|
3615
4667
|
async function isInstanceRunning(instanceRoot) {
|
|
3616
|
-
const portFile =
|
|
4668
|
+
const portFile = path22.join(instanceRoot, "api.port");
|
|
3617
4669
|
try {
|
|
3618
|
-
const content =
|
|
4670
|
+
const content = fs23.readFileSync(portFile, "utf-8").trim();
|
|
3619
4671
|
const port = parseInt(content, 10);
|
|
3620
4672
|
if (isNaN(port)) return false;
|
|
3621
4673
|
const controller = new AbortController();
|
|
@@ -3636,7 +4688,7 @@ var init_instance_context = __esm({
|
|
|
3636
4688
|
});
|
|
3637
4689
|
|
|
3638
4690
|
// src/core/doctor/checks/telegram.ts
|
|
3639
|
-
import * as
|
|
4691
|
+
import * as path23 from "path";
|
|
3640
4692
|
var BOT_TOKEN_REGEX, telegramCheck;
|
|
3641
4693
|
var init_telegram = __esm({
|
|
3642
4694
|
"src/core/doctor/checks/telegram.ts"() {
|
|
@@ -3654,7 +4706,7 @@ var init_telegram = __esm({
|
|
|
3654
4706
|
return results;
|
|
3655
4707
|
}
|
|
3656
4708
|
const { SettingsManager: SettingsManager2 } = await Promise.resolve().then(() => (init_settings_manager(), settings_manager_exports));
|
|
3657
|
-
const sm = new SettingsManager2(
|
|
4709
|
+
const sm = new SettingsManager2(path23.join(ctx.pluginsDir, "data"));
|
|
3658
4710
|
const ps = await sm.loadSettings("@openacp/telegram");
|
|
3659
4711
|
const botToken = ps.botToken;
|
|
3660
4712
|
const chatId = ps.chatId;
|
|
@@ -3814,7 +4866,7 @@ var init_telegram = __esm({
|
|
|
3814
4866
|
});
|
|
3815
4867
|
|
|
3816
4868
|
// src/core/doctor/checks/storage.ts
|
|
3817
|
-
import * as
|
|
4869
|
+
import * as fs24 from "fs";
|
|
3818
4870
|
var storageCheck;
|
|
3819
4871
|
var init_storage = __esm({
|
|
3820
4872
|
"src/core/doctor/checks/storage.ts"() {
|
|
@@ -3824,28 +4876,28 @@ var init_storage = __esm({
|
|
|
3824
4876
|
order: 4,
|
|
3825
4877
|
async run(ctx) {
|
|
3826
4878
|
const results = [];
|
|
3827
|
-
if (!
|
|
4879
|
+
if (!fs24.existsSync(ctx.dataDir)) {
|
|
3828
4880
|
results.push({
|
|
3829
4881
|
status: "fail",
|
|
3830
4882
|
message: "Data directory ~/.openacp does not exist",
|
|
3831
4883
|
fixable: true,
|
|
3832
4884
|
fixRisk: "safe",
|
|
3833
4885
|
fix: async () => {
|
|
3834
|
-
|
|
4886
|
+
fs24.mkdirSync(ctx.dataDir, { recursive: true });
|
|
3835
4887
|
return { success: true, message: "created directory" };
|
|
3836
4888
|
}
|
|
3837
4889
|
});
|
|
3838
4890
|
} else {
|
|
3839
4891
|
try {
|
|
3840
|
-
|
|
4892
|
+
fs24.accessSync(ctx.dataDir, fs24.constants.W_OK);
|
|
3841
4893
|
results.push({ status: "pass", message: "Data directory exists and writable" });
|
|
3842
4894
|
} catch {
|
|
3843
4895
|
results.push({ status: "fail", message: "Data directory not writable" });
|
|
3844
4896
|
}
|
|
3845
4897
|
}
|
|
3846
|
-
if (
|
|
4898
|
+
if (fs24.existsSync(ctx.sessionsPath)) {
|
|
3847
4899
|
try {
|
|
3848
|
-
const content =
|
|
4900
|
+
const content = fs24.readFileSync(ctx.sessionsPath, "utf-8");
|
|
3849
4901
|
const data = JSON.parse(content);
|
|
3850
4902
|
if (typeof data === "object" && data !== null && "sessions" in data) {
|
|
3851
4903
|
results.push({ status: "pass", message: "Sessions file valid" });
|
|
@@ -3856,7 +4908,7 @@ var init_storage = __esm({
|
|
|
3856
4908
|
fixable: true,
|
|
3857
4909
|
fixRisk: "risky",
|
|
3858
4910
|
fix: async () => {
|
|
3859
|
-
|
|
4911
|
+
fs24.writeFileSync(ctx.sessionsPath, JSON.stringify({ version: 1, sessions: {} }, null, 2));
|
|
3860
4912
|
return { success: true, message: "reset sessions file" };
|
|
3861
4913
|
}
|
|
3862
4914
|
});
|
|
@@ -3868,7 +4920,7 @@ var init_storage = __esm({
|
|
|
3868
4920
|
fixable: true,
|
|
3869
4921
|
fixRisk: "risky",
|
|
3870
4922
|
fix: async () => {
|
|
3871
|
-
|
|
4923
|
+
fs24.writeFileSync(ctx.sessionsPath, JSON.stringify({ version: 1, sessions: {} }, null, 2));
|
|
3872
4924
|
return { success: true, message: "reset sessions file" };
|
|
3873
4925
|
}
|
|
3874
4926
|
});
|
|
@@ -3876,20 +4928,20 @@ var init_storage = __esm({
|
|
|
3876
4928
|
} else {
|
|
3877
4929
|
results.push({ status: "pass", message: "Sessions file not present yet (created on first session)" });
|
|
3878
4930
|
}
|
|
3879
|
-
if (!
|
|
4931
|
+
if (!fs24.existsSync(ctx.logsDir)) {
|
|
3880
4932
|
results.push({
|
|
3881
4933
|
status: "warn",
|
|
3882
4934
|
message: "Log directory does not exist",
|
|
3883
4935
|
fixable: true,
|
|
3884
4936
|
fixRisk: "safe",
|
|
3885
4937
|
fix: async () => {
|
|
3886
|
-
|
|
4938
|
+
fs24.mkdirSync(ctx.logsDir, { recursive: true });
|
|
3887
4939
|
return { success: true, message: "created log directory" };
|
|
3888
4940
|
}
|
|
3889
4941
|
});
|
|
3890
4942
|
} else {
|
|
3891
4943
|
try {
|
|
3892
|
-
|
|
4944
|
+
fs24.accessSync(ctx.logsDir, fs24.constants.W_OK);
|
|
3893
4945
|
results.push({ status: "pass", message: "Log directory exists and writable" });
|
|
3894
4946
|
} catch {
|
|
3895
4947
|
results.push({ status: "fail", message: "Log directory not writable" });
|
|
@@ -3902,8 +4954,8 @@ var init_storage = __esm({
|
|
|
3902
4954
|
});
|
|
3903
4955
|
|
|
3904
4956
|
// src/core/doctor/checks/workspace.ts
|
|
3905
|
-
import * as
|
|
3906
|
-
import * as
|
|
4957
|
+
import * as fs25 from "fs";
|
|
4958
|
+
import * as path24 from "path";
|
|
3907
4959
|
var workspaceCheck;
|
|
3908
4960
|
var init_workspace = __esm({
|
|
3909
4961
|
"src/core/doctor/checks/workspace.ts"() {
|
|
@@ -3913,21 +4965,21 @@ var init_workspace = __esm({
|
|
|
3913
4965
|
order: 5,
|
|
3914
4966
|
async run(ctx) {
|
|
3915
4967
|
const results = [];
|
|
3916
|
-
const workspace =
|
|
3917
|
-
if (!
|
|
4968
|
+
const workspace = path24.dirname(ctx.dataDir);
|
|
4969
|
+
if (!fs25.existsSync(workspace)) {
|
|
3918
4970
|
results.push({
|
|
3919
4971
|
status: "warn",
|
|
3920
4972
|
message: `Workspace directory does not exist: ${workspace}`,
|
|
3921
4973
|
fixable: true,
|
|
3922
4974
|
fixRisk: "safe",
|
|
3923
4975
|
fix: async () => {
|
|
3924
|
-
|
|
4976
|
+
fs25.mkdirSync(workspace, { recursive: true });
|
|
3925
4977
|
return { success: true, message: "created directory" };
|
|
3926
4978
|
}
|
|
3927
4979
|
});
|
|
3928
4980
|
} else {
|
|
3929
4981
|
try {
|
|
3930
|
-
|
|
4982
|
+
fs25.accessSync(workspace, fs25.constants.W_OK);
|
|
3931
4983
|
results.push({ status: "pass", message: `Workspace directory exists: ${workspace}` });
|
|
3932
4984
|
} catch {
|
|
3933
4985
|
results.push({ status: "fail", message: `Workspace directory not writable: ${workspace}` });
|
|
@@ -3940,8 +4992,8 @@ var init_workspace = __esm({
|
|
|
3940
4992
|
});
|
|
3941
4993
|
|
|
3942
4994
|
// src/core/doctor/checks/plugins.ts
|
|
3943
|
-
import * as
|
|
3944
|
-
import * as
|
|
4995
|
+
import * as fs26 from "fs";
|
|
4996
|
+
import * as path25 from "path";
|
|
3945
4997
|
var pluginsCheck;
|
|
3946
4998
|
var init_plugins = __esm({
|
|
3947
4999
|
"src/core/doctor/checks/plugins.ts"() {
|
|
@@ -3951,16 +5003,16 @@ var init_plugins = __esm({
|
|
|
3951
5003
|
order: 6,
|
|
3952
5004
|
async run(ctx) {
|
|
3953
5005
|
const results = [];
|
|
3954
|
-
if (!
|
|
5006
|
+
if (!fs26.existsSync(ctx.pluginsDir)) {
|
|
3955
5007
|
results.push({
|
|
3956
5008
|
status: "warn",
|
|
3957
5009
|
message: "Plugins directory does not exist",
|
|
3958
5010
|
fixable: true,
|
|
3959
5011
|
fixRisk: "safe",
|
|
3960
5012
|
fix: async () => {
|
|
3961
|
-
|
|
3962
|
-
|
|
3963
|
-
|
|
5013
|
+
fs26.mkdirSync(ctx.pluginsDir, { recursive: true });
|
|
5014
|
+
fs26.writeFileSync(
|
|
5015
|
+
path25.join(ctx.pluginsDir, "package.json"),
|
|
3964
5016
|
JSON.stringify({ name: "openacp-plugins", private: true, dependencies: {} }, null, 2)
|
|
3965
5017
|
);
|
|
3966
5018
|
return { success: true, message: "initialized plugins directory" };
|
|
@@ -3969,15 +5021,15 @@ var init_plugins = __esm({
|
|
|
3969
5021
|
return results;
|
|
3970
5022
|
}
|
|
3971
5023
|
results.push({ status: "pass", message: "Plugins directory exists" });
|
|
3972
|
-
const pkgPath =
|
|
3973
|
-
if (!
|
|
5024
|
+
const pkgPath = path25.join(ctx.pluginsDir, "package.json");
|
|
5025
|
+
if (!fs26.existsSync(pkgPath)) {
|
|
3974
5026
|
results.push({
|
|
3975
5027
|
status: "warn",
|
|
3976
5028
|
message: "Plugins package.json missing",
|
|
3977
5029
|
fixable: true,
|
|
3978
5030
|
fixRisk: "safe",
|
|
3979
5031
|
fix: async () => {
|
|
3980
|
-
|
|
5032
|
+
fs26.writeFileSync(
|
|
3981
5033
|
pkgPath,
|
|
3982
5034
|
JSON.stringify({ name: "openacp-plugins", private: true, dependencies: {} }, null, 2)
|
|
3983
5035
|
);
|
|
@@ -3987,7 +5039,7 @@ var init_plugins = __esm({
|
|
|
3987
5039
|
return results;
|
|
3988
5040
|
}
|
|
3989
5041
|
try {
|
|
3990
|
-
const pkg = JSON.parse(
|
|
5042
|
+
const pkg = JSON.parse(fs26.readFileSync(pkgPath, "utf-8"));
|
|
3991
5043
|
const deps = pkg.dependencies || {};
|
|
3992
5044
|
const count = Object.keys(deps).length;
|
|
3993
5045
|
results.push({ status: "pass", message: `Plugins package.json valid (${count} plugins)` });
|
|
@@ -3998,7 +5050,7 @@ var init_plugins = __esm({
|
|
|
3998
5050
|
fixable: true,
|
|
3999
5051
|
fixRisk: "risky",
|
|
4000
5052
|
fix: async () => {
|
|
4001
|
-
|
|
5053
|
+
fs26.writeFileSync(
|
|
4002
5054
|
pkgPath,
|
|
4003
5055
|
JSON.stringify({ name: "openacp-plugins", private: true, dependencies: {} }, null, 2)
|
|
4004
5056
|
);
|
|
@@ -4013,7 +5065,7 @@ var init_plugins = __esm({
|
|
|
4013
5065
|
});
|
|
4014
5066
|
|
|
4015
5067
|
// src/core/doctor/checks/daemon.ts
|
|
4016
|
-
import * as
|
|
5068
|
+
import * as fs27 from "fs";
|
|
4017
5069
|
import * as net3 from "net";
|
|
4018
5070
|
function isProcessAlive(pid) {
|
|
4019
5071
|
try {
|
|
@@ -4024,12 +5076,12 @@ function isProcessAlive(pid) {
|
|
|
4024
5076
|
}
|
|
4025
5077
|
}
|
|
4026
5078
|
function checkPortInUse(port) {
|
|
4027
|
-
return new Promise((
|
|
5079
|
+
return new Promise((resolve8) => {
|
|
4028
5080
|
const server = net3.createServer();
|
|
4029
|
-
server.once("error", () =>
|
|
5081
|
+
server.once("error", () => resolve8(true));
|
|
4030
5082
|
server.once("listening", () => {
|
|
4031
5083
|
server.close();
|
|
4032
|
-
|
|
5084
|
+
resolve8(false);
|
|
4033
5085
|
});
|
|
4034
5086
|
server.listen(port, "127.0.0.1");
|
|
4035
5087
|
});
|
|
@@ -4043,8 +5095,8 @@ var init_daemon = __esm({
|
|
|
4043
5095
|
order: 7,
|
|
4044
5096
|
async run(ctx) {
|
|
4045
5097
|
const results = [];
|
|
4046
|
-
if (
|
|
4047
|
-
const content =
|
|
5098
|
+
if (fs27.existsSync(ctx.pidPath)) {
|
|
5099
|
+
const content = fs27.readFileSync(ctx.pidPath, "utf-8").trim();
|
|
4048
5100
|
const pid = parseInt(content, 10);
|
|
4049
5101
|
if (isNaN(pid)) {
|
|
4050
5102
|
results.push({
|
|
@@ -4053,7 +5105,7 @@ var init_daemon = __esm({
|
|
|
4053
5105
|
fixable: true,
|
|
4054
5106
|
fixRisk: "safe",
|
|
4055
5107
|
fix: async () => {
|
|
4056
|
-
|
|
5108
|
+
fs27.unlinkSync(ctx.pidPath);
|
|
4057
5109
|
return { success: true, message: "removed invalid PID file" };
|
|
4058
5110
|
}
|
|
4059
5111
|
});
|
|
@@ -4064,7 +5116,7 @@ var init_daemon = __esm({
|
|
|
4064
5116
|
fixable: true,
|
|
4065
5117
|
fixRisk: "safe",
|
|
4066
5118
|
fix: async () => {
|
|
4067
|
-
|
|
5119
|
+
fs27.unlinkSync(ctx.pidPath);
|
|
4068
5120
|
return { success: true, message: "removed stale PID file" };
|
|
4069
5121
|
}
|
|
4070
5122
|
});
|
|
@@ -4072,8 +5124,8 @@ var init_daemon = __esm({
|
|
|
4072
5124
|
results.push({ status: "pass", message: `Daemon running (PID ${pid})` });
|
|
4073
5125
|
}
|
|
4074
5126
|
}
|
|
4075
|
-
if (
|
|
4076
|
-
const content =
|
|
5127
|
+
if (fs27.existsSync(ctx.portFilePath)) {
|
|
5128
|
+
const content = fs27.readFileSync(ctx.portFilePath, "utf-8").trim();
|
|
4077
5129
|
const port = parseInt(content, 10);
|
|
4078
5130
|
if (isNaN(port)) {
|
|
4079
5131
|
results.push({
|
|
@@ -4082,7 +5134,7 @@ var init_daemon = __esm({
|
|
|
4082
5134
|
fixable: true,
|
|
4083
5135
|
fixRisk: "safe",
|
|
4084
5136
|
fix: async () => {
|
|
4085
|
-
|
|
5137
|
+
fs27.unlinkSync(ctx.portFilePath);
|
|
4086
5138
|
return { success: true, message: "removed invalid port file" };
|
|
4087
5139
|
}
|
|
4088
5140
|
});
|
|
@@ -4094,8 +5146,8 @@ var init_daemon = __esm({
|
|
|
4094
5146
|
const apiPort = 21420;
|
|
4095
5147
|
const inUse = await checkPortInUse(apiPort);
|
|
4096
5148
|
if (inUse) {
|
|
4097
|
-
if (
|
|
4098
|
-
const pid = parseInt(
|
|
5149
|
+
if (fs27.existsSync(ctx.pidPath)) {
|
|
5150
|
+
const pid = parseInt(fs27.readFileSync(ctx.pidPath, "utf-8").trim(), 10);
|
|
4099
5151
|
if (!isNaN(pid) && isProcessAlive(pid)) {
|
|
4100
5152
|
results.push({ status: "pass", message: `API port ${apiPort} in use by OpenACP daemon` });
|
|
4101
5153
|
} else {
|
|
@@ -4115,17 +5167,17 @@ var init_daemon = __esm({
|
|
|
4115
5167
|
});
|
|
4116
5168
|
|
|
4117
5169
|
// src/core/utils/install-binary.ts
|
|
4118
|
-
import
|
|
4119
|
-
import
|
|
5170
|
+
import fs28 from "fs";
|
|
5171
|
+
import path26 from "path";
|
|
4120
5172
|
import https from "https";
|
|
4121
5173
|
import os7 from "os";
|
|
4122
5174
|
import { execFileSync as execFileSync4 } from "child_process";
|
|
4123
5175
|
function downloadFile(url, dest, maxRedirects = 10) {
|
|
4124
|
-
return new Promise((
|
|
4125
|
-
const file =
|
|
5176
|
+
return new Promise((resolve8, reject) => {
|
|
5177
|
+
const file = fs28.createWriteStream(dest);
|
|
4126
5178
|
const cleanup = () => {
|
|
4127
5179
|
try {
|
|
4128
|
-
if (
|
|
5180
|
+
if (fs28.existsSync(dest)) fs28.unlinkSync(dest);
|
|
4129
5181
|
} catch {
|
|
4130
5182
|
}
|
|
4131
5183
|
};
|
|
@@ -4140,7 +5192,7 @@ function downloadFile(url, dest, maxRedirects = 10) {
|
|
|
4140
5192
|
}
|
|
4141
5193
|
file.close(() => {
|
|
4142
5194
|
cleanup();
|
|
4143
|
-
downloadFile(response.headers.location, dest, maxRedirects - 1).then(
|
|
5195
|
+
downloadFile(response.headers.location, dest, maxRedirects - 1).then(resolve8).catch(reject);
|
|
4144
5196
|
});
|
|
4145
5197
|
return;
|
|
4146
5198
|
}
|
|
@@ -4164,7 +5216,7 @@ function downloadFile(url, dest, maxRedirects = 10) {
|
|
|
4164
5216
|
}
|
|
4165
5217
|
});
|
|
4166
5218
|
response.pipe(file);
|
|
4167
|
-
file.on("finish", () => file.close(() =>
|
|
5219
|
+
file.on("finish", () => file.close(() => resolve8(dest)));
|
|
4168
5220
|
file.on("error", (err) => {
|
|
4169
5221
|
file.close(() => {
|
|
4170
5222
|
cleanup();
|
|
@@ -4191,36 +5243,36 @@ function getDownloadUrl(spec) {
|
|
|
4191
5243
|
async function ensureBinary(spec, binDir) {
|
|
4192
5244
|
const resolvedBinDir = binDir ?? DEFAULT_BIN_DIR;
|
|
4193
5245
|
const binName = IS_WINDOWS ? `${spec.name}.exe` : spec.name;
|
|
4194
|
-
const binPath =
|
|
5246
|
+
const binPath = path26.join(resolvedBinDir, binName);
|
|
4195
5247
|
if (commandExists(spec.name)) {
|
|
4196
5248
|
log19.debug({ name: spec.name }, "Found in PATH");
|
|
4197
5249
|
return spec.name;
|
|
4198
5250
|
}
|
|
4199
|
-
if (
|
|
4200
|
-
if (!IS_WINDOWS)
|
|
5251
|
+
if (fs28.existsSync(binPath)) {
|
|
5252
|
+
if (!IS_WINDOWS) fs28.chmodSync(binPath, "755");
|
|
4201
5253
|
log19.debug({ name: spec.name, path: binPath }, "Found in ~/.openacp/bin");
|
|
4202
5254
|
return binPath;
|
|
4203
5255
|
}
|
|
4204
5256
|
log19.info({ name: spec.name }, "Not found, downloading from GitHub...");
|
|
4205
|
-
|
|
5257
|
+
fs28.mkdirSync(resolvedBinDir, { recursive: true });
|
|
4206
5258
|
const url = getDownloadUrl(spec);
|
|
4207
5259
|
const isArchive = spec.isArchive?.(url) ?? false;
|
|
4208
|
-
const downloadDest = isArchive ?
|
|
5260
|
+
const downloadDest = isArchive ? path26.join(resolvedBinDir, `${spec.name}.tgz`) : binPath;
|
|
4209
5261
|
await downloadFile(url, downloadDest);
|
|
4210
5262
|
if (isArchive) {
|
|
4211
5263
|
const listing = execFileSync4("tar", ["-tf", downloadDest], { stdio: "pipe" }).toString().trim().split("\n").filter(Boolean);
|
|
4212
5264
|
validateTarContents(listing, resolvedBinDir);
|
|
4213
5265
|
execFileSync4("tar", ["-xzf", downloadDest, "-C", resolvedBinDir], { stdio: "pipe" });
|
|
4214
5266
|
try {
|
|
4215
|
-
|
|
5267
|
+
fs28.unlinkSync(downloadDest);
|
|
4216
5268
|
} catch {
|
|
4217
5269
|
}
|
|
4218
5270
|
}
|
|
4219
|
-
if (!
|
|
5271
|
+
if (!fs28.existsSync(binPath)) {
|
|
4220
5272
|
throw new Error(`${spec.name}: binary not found at ${binPath} after download/extraction. The archive structure may have changed.`);
|
|
4221
5273
|
}
|
|
4222
5274
|
if (!IS_WINDOWS) {
|
|
4223
|
-
|
|
5275
|
+
fs28.chmodSync(binPath, "755");
|
|
4224
5276
|
}
|
|
4225
5277
|
log19.info({ name: spec.name, path: binPath }, "Installed successfully");
|
|
4226
5278
|
return binPath;
|
|
@@ -4233,7 +5285,7 @@ var init_install_binary = __esm({
|
|
|
4233
5285
|
init_agent_dependencies();
|
|
4234
5286
|
init_agent_installer();
|
|
4235
5287
|
log19 = createChildLogger({ module: "binary-installer" });
|
|
4236
|
-
DEFAULT_BIN_DIR =
|
|
5288
|
+
DEFAULT_BIN_DIR = path26.join(os7.homedir(), ".openacp", "bin");
|
|
4237
5289
|
IS_WINDOWS = os7.platform() === "win32";
|
|
4238
5290
|
}
|
|
4239
5291
|
});
|
|
@@ -4274,8 +5326,8 @@ var init_install_cloudflared = __esm({
|
|
|
4274
5326
|
});
|
|
4275
5327
|
|
|
4276
5328
|
// src/core/doctor/checks/tunnel.ts
|
|
4277
|
-
import * as
|
|
4278
|
-
import * as
|
|
5329
|
+
import * as fs29 from "fs";
|
|
5330
|
+
import * as path27 from "path";
|
|
4279
5331
|
import * as os8 from "os";
|
|
4280
5332
|
import { execFileSync as execFileSync5 } from "child_process";
|
|
4281
5333
|
var tunnelCheck;
|
|
@@ -4292,7 +5344,7 @@ var init_tunnel = __esm({
|
|
|
4292
5344
|
return results;
|
|
4293
5345
|
}
|
|
4294
5346
|
const { SettingsManager: SettingsManager2 } = await Promise.resolve().then(() => (init_settings_manager(), settings_manager_exports));
|
|
4295
|
-
const sm = new SettingsManager2(
|
|
5347
|
+
const sm = new SettingsManager2(path27.join(ctx.pluginsDir, "data"));
|
|
4296
5348
|
const tunnelSettings = await sm.loadSettings("@openacp/tunnel");
|
|
4297
5349
|
const tunnelEnabled = tunnelSettings.enabled ?? false;
|
|
4298
5350
|
const provider = tunnelSettings.provider ?? "cloudflare";
|
|
@@ -4304,9 +5356,9 @@ var init_tunnel = __esm({
|
|
|
4304
5356
|
results.push({ status: "pass", message: `Tunnel provider: ${provider}` });
|
|
4305
5357
|
if (provider === "cloudflare") {
|
|
4306
5358
|
const binName = os8.platform() === "win32" ? "cloudflared.exe" : "cloudflared";
|
|
4307
|
-
const binPath =
|
|
5359
|
+
const binPath = path27.join(ctx.dataDir, "bin", binName);
|
|
4308
5360
|
let found = false;
|
|
4309
|
-
if (
|
|
5361
|
+
if (fs29.existsSync(binPath)) {
|
|
4310
5362
|
found = true;
|
|
4311
5363
|
} else {
|
|
4312
5364
|
try {
|
|
@@ -4380,8 +5432,8 @@ var init_proxy = __esm({
|
|
|
4380
5432
|
});
|
|
4381
5433
|
|
|
4382
5434
|
// src/core/doctor/index.ts
|
|
4383
|
-
import * as
|
|
4384
|
-
import * as
|
|
5435
|
+
import * as fs30 from "fs";
|
|
5436
|
+
import * as path28 from "path";
|
|
4385
5437
|
var ALL_CHECKS, CHECK_TIMEOUT_MS, DoctorEngine;
|
|
4386
5438
|
var init_doctor = __esm({
|
|
4387
5439
|
"src/core/doctor/index.ts"() {
|
|
@@ -4474,27 +5526,27 @@ var init_doctor = __esm({
|
|
|
4474
5526
|
/** Constructs the shared context used by all checks — loads config if available. */
|
|
4475
5527
|
async buildContext() {
|
|
4476
5528
|
const dataDir = this.dataDir;
|
|
4477
|
-
const configPath = process.env.OPENACP_CONFIG_PATH ||
|
|
5529
|
+
const configPath = process.env.OPENACP_CONFIG_PATH || path28.join(dataDir, "config.json");
|
|
4478
5530
|
let config = null;
|
|
4479
5531
|
let rawConfig = null;
|
|
4480
5532
|
try {
|
|
4481
|
-
const content =
|
|
5533
|
+
const content = fs30.readFileSync(configPath, "utf-8");
|
|
4482
5534
|
rawConfig = JSON.parse(content);
|
|
4483
5535
|
const cm = new ConfigManager(configPath);
|
|
4484
5536
|
await cm.load();
|
|
4485
5537
|
config = cm.get();
|
|
4486
5538
|
} catch {
|
|
4487
5539
|
}
|
|
4488
|
-
const logsDir = config ? expandHome2(config.logging.logDir) :
|
|
5540
|
+
const logsDir = config ? expandHome2(config.logging.logDir) : path28.join(dataDir, "logs");
|
|
4489
5541
|
return {
|
|
4490
5542
|
config,
|
|
4491
5543
|
rawConfig,
|
|
4492
5544
|
configPath,
|
|
4493
5545
|
dataDir,
|
|
4494
|
-
sessionsPath:
|
|
4495
|
-
pidPath:
|
|
4496
|
-
portFilePath:
|
|
4497
|
-
pluginsDir:
|
|
5546
|
+
sessionsPath: path28.join(dataDir, "sessions.json"),
|
|
5547
|
+
pidPath: path28.join(dataDir, "openacp.pid"),
|
|
5548
|
+
portFilePath: path28.join(dataDir, "api.port"),
|
|
5549
|
+
pluginsDir: path28.join(dataDir, "plugins"),
|
|
4498
5550
|
logsDir,
|
|
4499
5551
|
fetchForScope: (scope) => this.proxyService.createFetch(scope)
|
|
4500
5552
|
};
|
|
@@ -4504,12 +5556,12 @@ var init_doctor = __esm({
|
|
|
4504
5556
|
});
|
|
4505
5557
|
|
|
4506
5558
|
// src/core/instance/instance-registry.ts
|
|
4507
|
-
import
|
|
4508
|
-
import
|
|
4509
|
-
import { randomUUID as
|
|
5559
|
+
import fs32 from "fs";
|
|
5560
|
+
import path30 from "path";
|
|
5561
|
+
import { randomUUID as randomUUID5 } from "crypto";
|
|
4510
5562
|
function readIdFromConfig(instanceRoot) {
|
|
4511
5563
|
try {
|
|
4512
|
-
const raw = JSON.parse(
|
|
5564
|
+
const raw = JSON.parse(fs32.readFileSync(path30.join(instanceRoot, "config.json"), "utf-8"));
|
|
4513
5565
|
return typeof raw.id === "string" && raw.id ? raw.id : null;
|
|
4514
5566
|
} catch {
|
|
4515
5567
|
return null;
|
|
@@ -4528,7 +5580,7 @@ var init_instance_registry = __esm({
|
|
|
4528
5580
|
/** Load the registry from disk. If the file is missing or corrupt, starts fresh. */
|
|
4529
5581
|
load() {
|
|
4530
5582
|
try {
|
|
4531
|
-
const raw =
|
|
5583
|
+
const raw = fs32.readFileSync(this.registryPath, "utf-8");
|
|
4532
5584
|
const parsed = JSON.parse(raw);
|
|
4533
5585
|
if (parsed.version === 1 && parsed.instances) {
|
|
4534
5586
|
this.data = parsed;
|
|
@@ -4555,9 +5607,9 @@ var init_instance_registry = __esm({
|
|
|
4555
5607
|
}
|
|
4556
5608
|
/** Persist the registry to disk, creating parent directories if needed. */
|
|
4557
5609
|
save() {
|
|
4558
|
-
const dir =
|
|
4559
|
-
|
|
4560
|
-
|
|
5610
|
+
const dir = path30.dirname(this.registryPath);
|
|
5611
|
+
fs32.mkdirSync(dir, { recursive: true });
|
|
5612
|
+
fs32.writeFileSync(this.registryPath, JSON.stringify(this.data, null, 2));
|
|
4561
5613
|
}
|
|
4562
5614
|
/** Add or update an instance entry in the registry. Does not persist — call save() after. */
|
|
4563
5615
|
register(id, root) {
|
|
@@ -4605,7 +5657,7 @@ var init_instance_registry = __esm({
|
|
|
4605
5657
|
this.register(configId, instanceRoot);
|
|
4606
5658
|
return { id: configId, registryUpdated: true };
|
|
4607
5659
|
}
|
|
4608
|
-
const id = configId ?? entry?.id ??
|
|
5660
|
+
const id = configId ?? entry?.id ?? randomUUID5();
|
|
4609
5661
|
if (!this.getByRoot(instanceRoot)) {
|
|
4610
5662
|
this.register(id, instanceRoot);
|
|
4611
5663
|
return { id, registryUpdated: true };
|
|
@@ -4616,67 +5668,6 @@ var init_instance_registry = __esm({
|
|
|
4616
5668
|
}
|
|
4617
5669
|
});
|
|
4618
5670
|
|
|
4619
|
-
// src/core/plugin/plugin-installer.ts
|
|
4620
|
-
var plugin_installer_exports = {};
|
|
4621
|
-
__export(plugin_installer_exports, {
|
|
4622
|
-
importFromDir: () => importFromDir,
|
|
4623
|
-
installNpmPlugin: () => installNpmPlugin
|
|
4624
|
-
});
|
|
4625
|
-
import { execFile as execFile2 } from "child_process";
|
|
4626
|
-
import { promisify as promisify2 } from "util";
|
|
4627
|
-
import * as fs32 from "fs/promises";
|
|
4628
|
-
import * as path30 from "path";
|
|
4629
|
-
import { pathToFileURL } from "url";
|
|
4630
|
-
async function importFromDir(packageName, dir) {
|
|
4631
|
-
const pkgDir = path30.join(dir, "node_modules", ...packageName.split("/"));
|
|
4632
|
-
const pkgJsonPath = path30.join(pkgDir, "package.json");
|
|
4633
|
-
let pkgJson;
|
|
4634
|
-
try {
|
|
4635
|
-
pkgJson = JSON.parse(await fs32.readFile(pkgJsonPath, "utf-8"));
|
|
4636
|
-
} catch (err) {
|
|
4637
|
-
throw new Error(`Cannot read package.json for "${packageName}" at ${pkgJsonPath}: ${err.message}`);
|
|
4638
|
-
}
|
|
4639
|
-
let entry;
|
|
4640
|
-
const exportsMain = pkgJson.exports?.["."];
|
|
4641
|
-
if (typeof exportsMain === "string") {
|
|
4642
|
-
entry = exportsMain;
|
|
4643
|
-
} else if (exportsMain?.import) {
|
|
4644
|
-
entry = exportsMain.import;
|
|
4645
|
-
} else {
|
|
4646
|
-
entry = pkgJson.main ?? "index.js";
|
|
4647
|
-
}
|
|
4648
|
-
const entryPath = path30.join(pkgDir, entry);
|
|
4649
|
-
try {
|
|
4650
|
-
await fs32.access(entryPath);
|
|
4651
|
-
} catch {
|
|
4652
|
-
throw new Error(`Entry point "${entry}" not found for "${packageName}" at ${entryPath}`);
|
|
4653
|
-
}
|
|
4654
|
-
return import(pathToFileURL(entryPath).href);
|
|
4655
|
-
}
|
|
4656
|
-
async function installNpmPlugin(packageName, pluginsDir, childEnv) {
|
|
4657
|
-
if (!VALID_NPM_NAME.test(packageName)) {
|
|
4658
|
-
throw new Error(`Invalid package name: "${packageName}". Must be a valid npm package name.`);
|
|
4659
|
-
}
|
|
4660
|
-
const dir = pluginsDir;
|
|
4661
|
-
try {
|
|
4662
|
-
return await importFromDir(packageName, dir);
|
|
4663
|
-
} catch {
|
|
4664
|
-
}
|
|
4665
|
-
await execFileAsync2("npm", ["install", packageName, "--prefix", dir, "--save", "--ignore-scripts"], {
|
|
4666
|
-
timeout: 6e4,
|
|
4667
|
-
env: childEnv
|
|
4668
|
-
});
|
|
4669
|
-
return await importFromDir(packageName, dir);
|
|
4670
|
-
}
|
|
4671
|
-
var execFileAsync2, VALID_NPM_NAME;
|
|
4672
|
-
var init_plugin_installer = __esm({
|
|
4673
|
-
"src/core/plugin/plugin-installer.ts"() {
|
|
4674
|
-
"use strict";
|
|
4675
|
-
execFileAsync2 = promisify2(execFile2);
|
|
4676
|
-
VALID_NPM_NAME = /^(@[a-z0-9][\w.-]*\/)?[a-z0-9][\w.-]*(@[\w.^~>=<|-]+)?$/i;
|
|
4677
|
-
}
|
|
4678
|
-
});
|
|
4679
|
-
|
|
4680
5671
|
// src/core/plugin/terminal-io.ts
|
|
4681
5672
|
import * as clack from "@clack/prompts";
|
|
4682
5673
|
function isCancel(value) {
|
|
@@ -4740,10 +5731,10 @@ var install_context_exports = {};
|
|
|
4740
5731
|
__export(install_context_exports, {
|
|
4741
5732
|
createInstallContext: () => createInstallContext
|
|
4742
5733
|
});
|
|
4743
|
-
import
|
|
5734
|
+
import path32 from "path";
|
|
4744
5735
|
function createInstallContext(opts) {
|
|
4745
5736
|
const { pluginName, settingsManager, basePath, instanceRoot } = opts;
|
|
4746
|
-
const dataDir =
|
|
5737
|
+
const dataDir = path32.join(basePath, pluginName, "data");
|
|
4747
5738
|
return {
|
|
4748
5739
|
pluginName,
|
|
4749
5740
|
terminal: createTerminalIO(),
|
|
@@ -4771,14 +5762,14 @@ __export(api_client_exports, {
|
|
|
4771
5762
|
waitForApiReady: () => waitForApiReady,
|
|
4772
5763
|
waitForPortFile: () => waitForPortFile
|
|
4773
5764
|
});
|
|
4774
|
-
import * as
|
|
4775
|
-
import * as
|
|
5765
|
+
import * as fs34 from "fs";
|
|
5766
|
+
import * as path33 from "path";
|
|
4776
5767
|
import { setTimeout as sleep } from "timers/promises";
|
|
4777
5768
|
function defaultPortFile(root) {
|
|
4778
|
-
return
|
|
5769
|
+
return path33.join(root, "api.port");
|
|
4779
5770
|
}
|
|
4780
5771
|
function defaultSecretFile(root) {
|
|
4781
|
-
return
|
|
5772
|
+
return path33.join(root, "api-secret");
|
|
4782
5773
|
}
|
|
4783
5774
|
async function waitForPortFile(portFilePath, timeoutMs = 5e3, intervalMs = 100) {
|
|
4784
5775
|
const deadline = Date.now() + timeoutMs;
|
|
@@ -4812,7 +5803,7 @@ async function waitForApiReady(instanceRoot, expectedInstanceId, timeoutMs = 1e4
|
|
|
4812
5803
|
function readApiPort(portFilePath, instanceRoot) {
|
|
4813
5804
|
const filePath = portFilePath ?? defaultPortFile(instanceRoot);
|
|
4814
5805
|
try {
|
|
4815
|
-
const content =
|
|
5806
|
+
const content = fs34.readFileSync(filePath, "utf-8").trim();
|
|
4816
5807
|
const port = parseInt(content, 10);
|
|
4817
5808
|
return isNaN(port) ? null : port;
|
|
4818
5809
|
} catch {
|
|
@@ -4822,7 +5813,7 @@ function readApiPort(portFilePath, instanceRoot) {
|
|
|
4822
5813
|
function readApiSecret(secretFilePath, instanceRoot) {
|
|
4823
5814
|
const filePath = secretFilePath ?? defaultSecretFile(instanceRoot);
|
|
4824
5815
|
try {
|
|
4825
|
-
const content =
|
|
5816
|
+
const content = fs34.readFileSync(filePath, "utf-8").trim();
|
|
4826
5817
|
return content || null;
|
|
4827
5818
|
} catch {
|
|
4828
5819
|
return null;
|
|
@@ -4831,7 +5822,7 @@ function readApiSecret(secretFilePath, instanceRoot) {
|
|
|
4831
5822
|
function removeStalePortFile(portFilePath, instanceRoot) {
|
|
4832
5823
|
const filePath = portFilePath ?? defaultPortFile(instanceRoot);
|
|
4833
5824
|
try {
|
|
4834
|
-
|
|
5825
|
+
fs34.unlinkSync(filePath);
|
|
4835
5826
|
} catch {
|
|
4836
5827
|
}
|
|
4837
5828
|
}
|
|
@@ -4968,8 +5959,8 @@ var init_notification = __esm({
|
|
|
4968
5959
|
});
|
|
4969
5960
|
|
|
4970
5961
|
// src/plugins/file-service/file-service.ts
|
|
4971
|
-
import
|
|
4972
|
-
import
|
|
5962
|
+
import fs36 from "fs";
|
|
5963
|
+
import path36 from "path";
|
|
4973
5964
|
import { OggOpusDecoder } from "ogg-opus-decoder";
|
|
4974
5965
|
import wav from "node-wav";
|
|
4975
5966
|
function classifyMime(mimeType) {
|
|
@@ -5026,14 +6017,14 @@ var init_file_service = __esm({
|
|
|
5026
6017
|
const cutoff = Date.now() - maxAgeDays * 24 * 60 * 60 * 1e3;
|
|
5027
6018
|
let removed = 0;
|
|
5028
6019
|
try {
|
|
5029
|
-
const entries = await
|
|
6020
|
+
const entries = await fs36.promises.readdir(this.baseDir, { withFileTypes: true });
|
|
5030
6021
|
for (const entry of entries) {
|
|
5031
6022
|
if (!entry.isDirectory()) continue;
|
|
5032
|
-
const dirPath =
|
|
6023
|
+
const dirPath = path36.join(this.baseDir, entry.name);
|
|
5033
6024
|
try {
|
|
5034
|
-
const stat = await
|
|
6025
|
+
const stat = await fs36.promises.stat(dirPath);
|
|
5035
6026
|
if (stat.mtimeMs < cutoff) {
|
|
5036
|
-
await
|
|
6027
|
+
await fs36.promises.rm(dirPath, { recursive: true, force: true });
|
|
5037
6028
|
removed++;
|
|
5038
6029
|
}
|
|
5039
6030
|
} catch {
|
|
@@ -5052,11 +6043,11 @@ var init_file_service = __esm({
|
|
|
5052
6043
|
* so the user-facing name is not lost.
|
|
5053
6044
|
*/
|
|
5054
6045
|
async saveFile(sessionId, fileName, data, mimeType) {
|
|
5055
|
-
const sessionDir =
|
|
5056
|
-
await
|
|
6046
|
+
const sessionDir = path36.join(this.baseDir, sessionId);
|
|
6047
|
+
await fs36.promises.mkdir(sessionDir, { recursive: true });
|
|
5057
6048
|
const safeName = `${Date.now()}-${fileName.replace(/[^a-zA-Z0-9._-]/g, "_")}`;
|
|
5058
|
-
const filePath =
|
|
5059
|
-
await
|
|
6049
|
+
const filePath = path36.join(sessionDir, safeName);
|
|
6050
|
+
await fs36.promises.writeFile(filePath, data);
|
|
5060
6051
|
return {
|
|
5061
6052
|
type: classifyMime(mimeType),
|
|
5062
6053
|
filePath,
|
|
@@ -5071,14 +6062,14 @@ var init_file_service = __esm({
|
|
|
5071
6062
|
*/
|
|
5072
6063
|
async resolveFile(filePath) {
|
|
5073
6064
|
try {
|
|
5074
|
-
const stat = await
|
|
6065
|
+
const stat = await fs36.promises.stat(filePath);
|
|
5075
6066
|
if (!stat.isFile()) return null;
|
|
5076
|
-
const ext =
|
|
6067
|
+
const ext = path36.extname(filePath).toLowerCase();
|
|
5077
6068
|
const mimeType = EXT_TO_MIME[ext] || "application/octet-stream";
|
|
5078
6069
|
return {
|
|
5079
6070
|
type: classifyMime(mimeType),
|
|
5080
6071
|
filePath,
|
|
5081
|
-
fileName:
|
|
6072
|
+
fileName: path36.basename(filePath),
|
|
5082
6073
|
mimeType,
|
|
5083
6074
|
size: stat.size
|
|
5084
6075
|
};
|
|
@@ -5721,8 +6712,8 @@ data: ${JSON.stringify(data)}
|
|
|
5721
6712
|
});
|
|
5722
6713
|
|
|
5723
6714
|
// src/plugins/api-server/static-server.ts
|
|
5724
|
-
import * as
|
|
5725
|
-
import * as
|
|
6715
|
+
import * as fs37 from "fs";
|
|
6716
|
+
import * as path37 from "path";
|
|
5726
6717
|
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
5727
6718
|
var MIME_TYPES, StaticServer;
|
|
5728
6719
|
var init_static_server = __esm({
|
|
@@ -5746,16 +6737,16 @@ var init_static_server = __esm({
|
|
|
5746
6737
|
this.uiDir = uiDir;
|
|
5747
6738
|
if (!this.uiDir) {
|
|
5748
6739
|
const __filename = fileURLToPath2(import.meta.url);
|
|
5749
|
-
const candidate =
|
|
5750
|
-
if (
|
|
6740
|
+
const candidate = path37.resolve(path37.dirname(__filename), "../../ui/dist");
|
|
6741
|
+
if (fs37.existsSync(path37.join(candidate, "index.html"))) {
|
|
5751
6742
|
this.uiDir = candidate;
|
|
5752
6743
|
}
|
|
5753
6744
|
if (!this.uiDir) {
|
|
5754
|
-
const publishCandidate =
|
|
5755
|
-
|
|
6745
|
+
const publishCandidate = path37.resolve(
|
|
6746
|
+
path37.dirname(__filename),
|
|
5756
6747
|
"../ui"
|
|
5757
6748
|
);
|
|
5758
|
-
if (
|
|
6749
|
+
if (fs37.existsSync(path37.join(publishCandidate, "index.html"))) {
|
|
5759
6750
|
this.uiDir = publishCandidate;
|
|
5760
6751
|
}
|
|
5761
6752
|
}
|
|
@@ -5773,23 +6764,23 @@ var init_static_server = __esm({
|
|
|
5773
6764
|
serve(req, res) {
|
|
5774
6765
|
if (!this.uiDir) return false;
|
|
5775
6766
|
const urlPath = (req.url || "/").split("?")[0];
|
|
5776
|
-
const safePath =
|
|
5777
|
-
const filePath =
|
|
5778
|
-
if (!filePath.startsWith(this.uiDir +
|
|
6767
|
+
const safePath = path37.normalize(urlPath);
|
|
6768
|
+
const filePath = path37.join(this.uiDir, safePath);
|
|
6769
|
+
if (!filePath.startsWith(this.uiDir + path37.sep) && filePath !== this.uiDir)
|
|
5779
6770
|
return false;
|
|
5780
6771
|
let realFilePath;
|
|
5781
6772
|
try {
|
|
5782
|
-
realFilePath =
|
|
6773
|
+
realFilePath = fs37.realpathSync(filePath);
|
|
5783
6774
|
} catch {
|
|
5784
6775
|
realFilePath = null;
|
|
5785
6776
|
}
|
|
5786
6777
|
if (realFilePath !== null) {
|
|
5787
|
-
const realUiDir =
|
|
5788
|
-
if (!realFilePath.startsWith(realUiDir +
|
|
6778
|
+
const realUiDir = fs37.realpathSync(this.uiDir);
|
|
6779
|
+
if (!realFilePath.startsWith(realUiDir + path37.sep) && realFilePath !== realUiDir)
|
|
5789
6780
|
return false;
|
|
5790
6781
|
}
|
|
5791
|
-
if (realFilePath !== null &&
|
|
5792
|
-
const ext =
|
|
6782
|
+
if (realFilePath !== null && fs37.existsSync(realFilePath) && fs37.statSync(realFilePath).isFile()) {
|
|
6783
|
+
const ext = path37.extname(filePath);
|
|
5793
6784
|
const contentType = MIME_TYPES[ext] ?? "application/octet-stream";
|
|
5794
6785
|
const isHashed = /\.[a-zA-Z0-9]{8,}\.(js|css)$/.test(filePath);
|
|
5795
6786
|
const cacheControl = isHashed ? "public, max-age=31536000, immutable" : "no-cache";
|
|
@@ -5797,16 +6788,16 @@ var init_static_server = __esm({
|
|
|
5797
6788
|
"Content-Type": contentType,
|
|
5798
6789
|
"Cache-Control": cacheControl
|
|
5799
6790
|
});
|
|
5800
|
-
|
|
6791
|
+
fs37.createReadStream(realFilePath).pipe(res);
|
|
5801
6792
|
return true;
|
|
5802
6793
|
}
|
|
5803
|
-
const indexPath =
|
|
5804
|
-
if (
|
|
6794
|
+
const indexPath = path37.join(this.uiDir, "index.html");
|
|
6795
|
+
if (fs37.existsSync(indexPath)) {
|
|
5805
6796
|
res.writeHead(200, {
|
|
5806
6797
|
"Content-Type": "text/html; charset=utf-8",
|
|
5807
6798
|
"Cache-Control": "no-cache"
|
|
5808
6799
|
});
|
|
5809
|
-
|
|
6800
|
+
fs37.createReadStream(indexPath).pipe(res);
|
|
5810
6801
|
return true;
|
|
5811
6802
|
}
|
|
5812
6803
|
return false;
|
|
@@ -5828,6 +6819,8 @@ var init_speech_service = __esm({
|
|
|
5828
6819
|
sttProviders = /* @__PURE__ */ new Map();
|
|
5829
6820
|
ttsProviders = /* @__PURE__ */ new Map();
|
|
5830
6821
|
providerFactory;
|
|
6822
|
+
factoryOwnedSTT = /* @__PURE__ */ new Set();
|
|
6823
|
+
factoryOwnedTTS = /* @__PURE__ */ new Set();
|
|
5831
6824
|
/** Set a factory function that can recreate providers from config (for hot-reload) */
|
|
5832
6825
|
setProviderFactory(factory) {
|
|
5833
6826
|
this.providerFactory = factory;
|
|
@@ -5835,10 +6828,12 @@ var init_speech_service = __esm({
|
|
|
5835
6828
|
/** Register an STT provider by name. Overwrites any existing provider with the same name. */
|
|
5836
6829
|
registerSTTProvider(name, provider) {
|
|
5837
6830
|
this.sttProviders.set(name, provider);
|
|
6831
|
+
this.factoryOwnedSTT.delete(name);
|
|
5838
6832
|
}
|
|
5839
6833
|
/** Register a TTS provider by name. Called by external TTS plugins (e.g. msedge-tts-plugin). */
|
|
5840
6834
|
registerTTSProvider(name, provider) {
|
|
5841
6835
|
this.ttsProviders.set(name, provider);
|
|
6836
|
+
this.factoryOwnedTTS.delete(name);
|
|
5842
6837
|
}
|
|
5843
6838
|
/** Remove a TTS provider — called by external plugins on teardown. */
|
|
5844
6839
|
unregisterTTSProvider(name) {
|
|
@@ -5903,16 +6898,22 @@ var init_speech_service = __esm({
|
|
|
5903
6898
|
* (e.g. from `@openacp/msedge-tts-plugin`) are preserved rather than discarded.
|
|
5904
6899
|
*/
|
|
5905
6900
|
refreshProviders(newConfig) {
|
|
5906
|
-
this.
|
|
5907
|
-
|
|
5908
|
-
|
|
5909
|
-
for (const [name, provider] of stt) {
|
|
5910
|
-
this.sttProviders.set(name, provider);
|
|
5911
|
-
}
|
|
5912
|
-
for (const [name, provider] of tts) {
|
|
5913
|
-
this.ttsProviders.set(name, provider);
|
|
5914
|
-
}
|
|
6901
|
+
if (!this.providerFactory) {
|
|
6902
|
+
this.config = newConfig;
|
|
6903
|
+
return;
|
|
5915
6904
|
}
|
|
6905
|
+
const built = this.providerFactory(newConfig);
|
|
6906
|
+
const nextSTT = new Map(this.sttProviders);
|
|
6907
|
+
const nextTTS = new Map(this.ttsProviders);
|
|
6908
|
+
for (const name of this.factoryOwnedSTT) nextSTT.delete(name);
|
|
6909
|
+
for (const name of this.factoryOwnedTTS) nextTTS.delete(name);
|
|
6910
|
+
for (const [name, provider] of built.stt) nextSTT.set(name, provider);
|
|
6911
|
+
for (const [name, provider] of built.tts) nextTTS.set(name, provider);
|
|
6912
|
+
this.config = newConfig;
|
|
6913
|
+
this.sttProviders = nextSTT;
|
|
6914
|
+
this.ttsProviders = nextTTS;
|
|
6915
|
+
this.factoryOwnedSTT = new Set(built.stt.keys());
|
|
6916
|
+
this.factoryOwnedTTS = new Set(built.tts.keys());
|
|
5916
6917
|
}
|
|
5917
6918
|
};
|
|
5918
6919
|
}
|
|
@@ -5929,8 +6930,8 @@ var init_exports = __esm({
|
|
|
5929
6930
|
});
|
|
5930
6931
|
|
|
5931
6932
|
// src/plugins/context/context-cache.ts
|
|
5932
|
-
import * as
|
|
5933
|
-
import * as
|
|
6933
|
+
import * as fs38 from "fs";
|
|
6934
|
+
import * as path38 from "path";
|
|
5934
6935
|
import * as crypto2 from "crypto";
|
|
5935
6936
|
var DEFAULT_TTL_MS, ContextCache;
|
|
5936
6937
|
var init_context_cache = __esm({
|
|
@@ -5941,7 +6942,7 @@ var init_context_cache = __esm({
|
|
|
5941
6942
|
constructor(cacheDir, ttlMs = DEFAULT_TTL_MS) {
|
|
5942
6943
|
this.cacheDir = cacheDir;
|
|
5943
6944
|
this.ttlMs = ttlMs;
|
|
5944
|
-
|
|
6945
|
+
fs38.mkdirSync(cacheDir, { recursive: true });
|
|
5945
6946
|
}
|
|
5946
6947
|
cacheDir;
|
|
5947
6948
|
ttlMs;
|
|
@@ -5949,23 +6950,23 @@ var init_context_cache = __esm({
|
|
|
5949
6950
|
return crypto2.createHash("sha256").update(`${repoPath}:${queryKey}`).digest("hex").slice(0, 16);
|
|
5950
6951
|
}
|
|
5951
6952
|
filePath(repoPath, queryKey) {
|
|
5952
|
-
return
|
|
6953
|
+
return path38.join(this.cacheDir, `${this.keyHash(repoPath, queryKey)}.json`);
|
|
5953
6954
|
}
|
|
5954
6955
|
get(repoPath, queryKey) {
|
|
5955
6956
|
const fp = this.filePath(repoPath, queryKey);
|
|
5956
6957
|
try {
|
|
5957
|
-
const stat =
|
|
6958
|
+
const stat = fs38.statSync(fp);
|
|
5958
6959
|
if (Date.now() - stat.mtimeMs > this.ttlMs) {
|
|
5959
|
-
|
|
6960
|
+
fs38.unlinkSync(fp);
|
|
5960
6961
|
return null;
|
|
5961
6962
|
}
|
|
5962
|
-
return JSON.parse(
|
|
6963
|
+
return JSON.parse(fs38.readFileSync(fp, "utf-8"));
|
|
5963
6964
|
} catch {
|
|
5964
6965
|
return null;
|
|
5965
6966
|
}
|
|
5966
6967
|
}
|
|
5967
6968
|
set(repoPath, queryKey, result) {
|
|
5968
|
-
|
|
6969
|
+
fs38.writeFileSync(this.filePath(repoPath, queryKey), JSON.stringify(result));
|
|
5969
6970
|
}
|
|
5970
6971
|
};
|
|
5971
6972
|
}
|
|
@@ -6939,8 +7940,8 @@ function formatToolSummary(name, rawInput, displaySummary) {
|
|
|
6939
7940
|
}
|
|
6940
7941
|
if (lowerName === "grep") {
|
|
6941
7942
|
const pattern = args.pattern ?? "";
|
|
6942
|
-
const
|
|
6943
|
-
return pattern ? `\u{1F50D} Grep "${pattern}"${
|
|
7943
|
+
const path39 = args.path ?? "";
|
|
7944
|
+
return pattern ? `\u{1F50D} Grep "${pattern}"${path39 ? ` in ${path39}` : ""}` : `\u{1F527} ${name}`;
|
|
6944
7945
|
}
|
|
6945
7946
|
if (lowerName === "glob") {
|
|
6946
7947
|
const pattern = args.pattern ?? "";
|
|
@@ -6976,8 +7977,8 @@ function formatToolTitle(name, rawInput, displayTitle) {
|
|
|
6976
7977
|
}
|
|
6977
7978
|
if (lowerName === "grep") {
|
|
6978
7979
|
const pattern = args.pattern ?? "";
|
|
6979
|
-
const
|
|
6980
|
-
return pattern ? `"${pattern}"${
|
|
7980
|
+
const path39 = args.path ?? "";
|
|
7981
|
+
return pattern ? `"${pattern}"${path39 ? ` in ${path39}` : ""}` : name;
|
|
6981
7982
|
}
|
|
6982
7983
|
if (lowerName === "glob") {
|
|
6983
7984
|
return String(args.pattern ?? name);
|
|
@@ -7398,10 +8399,10 @@ var init_send_queue = __esm({
|
|
|
7398
8399
|
const type = opts?.type ?? "other";
|
|
7399
8400
|
const key = opts?.key;
|
|
7400
8401
|
const category = opts?.category;
|
|
7401
|
-
let
|
|
8402
|
+
let resolve8;
|
|
7402
8403
|
let reject;
|
|
7403
8404
|
const promise = new Promise((res, rej) => {
|
|
7404
|
-
|
|
8405
|
+
resolve8 = res;
|
|
7405
8406
|
reject = rej;
|
|
7406
8407
|
});
|
|
7407
8408
|
promise.catch(() => {
|
|
@@ -7412,12 +8413,12 @@ var init_send_queue = __esm({
|
|
|
7412
8413
|
);
|
|
7413
8414
|
if (idx !== -1) {
|
|
7414
8415
|
this.items[idx].resolve(void 0);
|
|
7415
|
-
this.items[idx] = { fn, type, key, category, resolve:
|
|
8416
|
+
this.items[idx] = { fn, type, key, category, resolve: resolve8, reject, promise };
|
|
7416
8417
|
this.scheduleProcess();
|
|
7417
8418
|
return promise;
|
|
7418
8419
|
}
|
|
7419
8420
|
}
|
|
7420
|
-
this.items.push({ fn, type, key, category, resolve:
|
|
8421
|
+
this.items.push({ fn, type, key, category, resolve: resolve8, reject, promise });
|
|
7421
8422
|
this.scheduleProcess();
|
|
7422
8423
|
return promise;
|
|
7423
8424
|
}
|
|
@@ -8153,10 +9154,10 @@ function shortenTitle(title, kind, workingDirectory) {
|
|
|
8153
9154
|
const relativized = normalizedPathPart.split(", ").map((segment) => segment.startsWith(prefix) ? segment.slice(prefix.length) : segment).join(", ");
|
|
8154
9155
|
if (relativized !== normalizedPathPart) return relativized + rangePart;
|
|
8155
9156
|
}
|
|
8156
|
-
if (FILE_KINDS.has(kind)) return
|
|
9157
|
+
if (FILE_KINDS.has(kind)) return basename2(pathPart) + rangePart;
|
|
8157
9158
|
return title;
|
|
8158
9159
|
}
|
|
8159
|
-
function
|
|
9160
|
+
function basename2(pathLike) {
|
|
8160
9161
|
return pathLike.replace(/\\/g, "/").split("/").pop() || pathLike;
|
|
8161
9162
|
}
|
|
8162
9163
|
function renderSpecSection(spec) {
|
|
@@ -8193,7 +9194,7 @@ function renderSpecSection(spec) {
|
|
|
8193
9194
|
}
|
|
8194
9195
|
if (spec.viewerLinks?.file || spec.viewerLinks?.diff || spec.outputViewerLink) {
|
|
8195
9196
|
const linkParts = [];
|
|
8196
|
-
const linkName =
|
|
9197
|
+
const linkName = basename2(displayTitle || kindLabel || spec.kind);
|
|
8197
9198
|
if (spec.viewerLinks?.file)
|
|
8198
9199
|
linkParts.push(`<a href="${escapeHtml(spec.viewerLinks.file)}">View ${escapeHtml(linkName)}</a>`);
|
|
8199
9200
|
if (spec.viewerLinks?.diff)
|
|
@@ -8292,7 +9293,7 @@ __export(version_exports, {
|
|
|
8292
9293
|
runUpdate: () => runUpdate
|
|
8293
9294
|
});
|
|
8294
9295
|
import { fileURLToPath as fileURLToPath3 } from "url";
|
|
8295
|
-
import { dirname as
|
|
9296
|
+
import { dirname as dirname13, join as join16, resolve as resolve7 } from "path";
|
|
8296
9297
|
import { existsSync as existsSync17, readFileSync as readFileSync15 } from "fs";
|
|
8297
9298
|
function getUpdateNetwork(instanceRoot) {
|
|
8298
9299
|
if (!instanceRoot) return {};
|
|
@@ -8306,11 +9307,11 @@ function getUpdateNetwork(instanceRoot) {
|
|
|
8306
9307
|
};
|
|
8307
9308
|
}
|
|
8308
9309
|
function findPackageJson() {
|
|
8309
|
-
let dir =
|
|
9310
|
+
let dir = dirname13(fileURLToPath3(import.meta.url));
|
|
8310
9311
|
for (let i = 0; i < 5; i++) {
|
|
8311
9312
|
const candidate = join16(dir, "package.json");
|
|
8312
9313
|
if (existsSync17(candidate)) return candidate;
|
|
8313
|
-
const parent =
|
|
9314
|
+
const parent = resolve7(dir, "..");
|
|
8314
9315
|
if (parent === dir) break;
|
|
8315
9316
|
dir = parent;
|
|
8316
9317
|
}
|
|
@@ -8349,7 +9350,7 @@ function compareVersions(current, latest) {
|
|
|
8349
9350
|
}
|
|
8350
9351
|
async function runUpdate(environment) {
|
|
8351
9352
|
const { spawn: spawn4 } = await import("child_process");
|
|
8352
|
-
return new Promise((
|
|
9353
|
+
return new Promise((resolve8) => {
|
|
8353
9354
|
const child = spawn4("npm", ["install", "-g", `${NPM_PACKAGE}@latest`], {
|
|
8354
9355
|
stdio: "inherit",
|
|
8355
9356
|
shell: false,
|
|
@@ -8357,14 +9358,14 @@ async function runUpdate(environment) {
|
|
|
8357
9358
|
});
|
|
8358
9359
|
const onSignal = () => {
|
|
8359
9360
|
child.kill("SIGTERM");
|
|
8360
|
-
|
|
9361
|
+
resolve8(false);
|
|
8361
9362
|
};
|
|
8362
9363
|
process.on("SIGINT", onSignal);
|
|
8363
9364
|
process.on("SIGTERM", onSignal);
|
|
8364
9365
|
child.on("close", (code) => {
|
|
8365
9366
|
process.off("SIGINT", onSignal);
|
|
8366
9367
|
process.off("SIGTERM", onSignal);
|
|
8367
|
-
|
|
9368
|
+
resolve8(code === 0);
|
|
8368
9369
|
});
|
|
8369
9370
|
});
|
|
8370
9371
|
}
|
|
@@ -9196,7 +10197,7 @@ __export(integrate_exports, {
|
|
|
9196
10197
|
uninstallIntegration: () => uninstallIntegration
|
|
9197
10198
|
});
|
|
9198
10199
|
import { existsSync as existsSync18, mkdirSync as mkdirSync11, readFileSync as readFileSync16, writeFileSync as writeFileSync11, unlinkSync as unlinkSync7, chmodSync, rmdirSync } from "fs";
|
|
9199
|
-
import { join as join17, dirname as
|
|
10200
|
+
import { join as join17, dirname as dirname14 } from "path";
|
|
9200
10201
|
import { homedir as homedir4 } from "os";
|
|
9201
10202
|
function isHooksIntegrationSpec(spec) {
|
|
9202
10203
|
return spec.strategy === "hooks";
|
|
@@ -9374,7 +10375,7 @@ function mergeSettingsJson(settingsPath, hookEvent, hookScriptPath) {
|
|
|
9374
10375
|
hooks: [{ type: "command", command: hookScriptPath }]
|
|
9375
10376
|
});
|
|
9376
10377
|
}
|
|
9377
|
-
mkdirSync11(
|
|
10378
|
+
mkdirSync11(dirname14(fullPath), { recursive: true });
|
|
9378
10379
|
writeFileSync11(fullPath, JSON.stringify(settings, null, 2) + "\n");
|
|
9379
10380
|
}
|
|
9380
10381
|
function mergeHooksJson(settingsPath, hookEvent, hookScriptPath) {
|
|
@@ -9393,7 +10394,7 @@ function mergeHooksJson(settingsPath, hookEvent, hookScriptPath) {
|
|
|
9393
10394
|
if (!alreadyInstalled) {
|
|
9394
10395
|
eventHooks.push({ command: hookScriptPath });
|
|
9395
10396
|
}
|
|
9396
|
-
mkdirSync11(
|
|
10397
|
+
mkdirSync11(dirname14(fullPath), { recursive: true });
|
|
9397
10398
|
writeFileSync11(fullPath, JSON.stringify(config, null, 2) + "\n");
|
|
9398
10399
|
}
|
|
9399
10400
|
function removeFromSettingsJson(settingsPath, hookEvent) {
|
|
@@ -9619,7 +10620,7 @@ function buildTunnelItem(spec) {
|
|
|
9619
10620
|
const logs = [];
|
|
9620
10621
|
try {
|
|
9621
10622
|
const skillPath = getTunnelPath();
|
|
9622
|
-
mkdirSync11(
|
|
10623
|
+
mkdirSync11(dirname14(skillPath), { recursive: true });
|
|
9623
10624
|
writeFileSync11(skillPath, generateTunnelCommand());
|
|
9624
10625
|
logs.push(`Created ${skillPath}`);
|
|
9625
10626
|
return { success: true, logs };
|
|
@@ -9635,7 +10636,7 @@ function buildTunnelItem(spec) {
|
|
|
9635
10636
|
if (existsSync18(skillPath)) {
|
|
9636
10637
|
unlinkSync7(skillPath);
|
|
9637
10638
|
try {
|
|
9638
|
-
rmdirSync(
|
|
10639
|
+
rmdirSync(dirname14(skillPath));
|
|
9639
10640
|
} catch {
|
|
9640
10641
|
}
|
|
9641
10642
|
logs.push(`Removed ${skillPath}`);
|
|
@@ -10168,6 +11169,7 @@ async function buildSettingsKeyboard(core) {
|
|
|
10168
11169
|
}
|
|
10169
11170
|
}
|
|
10170
11171
|
kb.text("\u{1F310} Proxy Management", settingsCommandCallback("/proxy")).row();
|
|
11172
|
+
kb.text("\u{1F399} Speech-to-Text", settingsCommandCallback("/speech")).row();
|
|
10171
11173
|
kb.text("\u25C0\uFE0F Back to Menu", "s:back");
|
|
10172
11174
|
return kb;
|
|
10173
11175
|
}
|
|
@@ -10255,33 +11257,6 @@ Select a value:`, {
|
|
|
10255
11257
|
const fieldDef = getSafeFields().find((f) => f.path === fieldPath);
|
|
10256
11258
|
if (!fieldDef) return;
|
|
10257
11259
|
try {
|
|
10258
|
-
if (fieldPath === "speech.stt.provider") {
|
|
10259
|
-
const sm = core.settingsManager;
|
|
10260
|
-
let hasApiKey = false;
|
|
10261
|
-
if (sm) {
|
|
10262
|
-
const speechSettings = await sm.loadSettings("@openacp/speech");
|
|
10263
|
-
hasApiKey = !!speechSettings.groqApiKey;
|
|
10264
|
-
} else {
|
|
10265
|
-
hasApiKey = false;
|
|
10266
|
-
}
|
|
10267
|
-
if (!hasApiKey) {
|
|
10268
|
-
const assistant = getAssistantSession();
|
|
10269
|
-
if (assistant) {
|
|
10270
|
-
try {
|
|
10271
|
-
await ctx.answerCallbackQuery({ text: `\u{1F511} API key needed \u2014 check Assistant topic` });
|
|
10272
|
-
} catch {
|
|
10273
|
-
}
|
|
10274
|
-
const prompt = `User wants to enable ${newValue} as Speech-to-Text provider, but no API key is configured yet. Guide them to get a ${newValue} API key and set it up. After they provide the key, run both commands: \`openacp config set speech.stt.providers.${newValue}.apiKey <key>\` and \`openacp config set speech.stt.provider ${newValue}\``;
|
|
10275
|
-
await assistant.enqueuePrompt(prompt);
|
|
10276
|
-
return;
|
|
10277
|
-
}
|
|
10278
|
-
try {
|
|
10279
|
-
await ctx.answerCallbackQuery({ text: `\u26A0\uFE0F Set API key first: openacp config set speech.stt.providers.${newValue}.apiKey <key>` });
|
|
10280
|
-
} catch {
|
|
10281
|
-
}
|
|
10282
|
-
return;
|
|
10283
|
-
}
|
|
10284
|
-
}
|
|
10285
11260
|
await setFieldValueAsync(fieldDef, newValue, core.configManager);
|
|
10286
11261
|
try {
|
|
10287
11262
|
await ctx.answerCallbackQuery({ text: `\u2705 ${fieldPath} = ${newValue}` });
|
|
@@ -11978,7 +12953,7 @@ var init_types = __esm({
|
|
|
11978
12953
|
});
|
|
11979
12954
|
|
|
11980
12955
|
// src/core/commands/proxy.ts
|
|
11981
|
-
import { randomUUID as
|
|
12956
|
+
import { randomUUID as randomUUID6 } from "crypto";
|
|
11982
12957
|
function clearProxyDraftsForChannel(channelId) {
|
|
11983
12958
|
const prefix = `${channelId}:`;
|
|
11984
12959
|
for (const [id, draft] of drafts) if (draft.owner.startsWith(prefix)) drafts.delete(id);
|
|
@@ -11997,7 +12972,7 @@ var init_proxy2 = __esm({
|
|
|
11997
12972
|
});
|
|
11998
12973
|
|
|
11999
12974
|
// src/plugins/telegram/command-sync.ts
|
|
12000
|
-
import { createHash as
|
|
12975
|
+
import { createHash as createHash6 } from "crypto";
|
|
12001
12976
|
function validDescription(description) {
|
|
12002
12977
|
if (typeof description !== "string") return false;
|
|
12003
12978
|
const trimmed = description.trim();
|
|
@@ -12066,7 +13041,7 @@ function localeLabel(locale) {
|
|
|
12066
13041
|
async function synchronizeTelegramCommands(api, chatId, desired, options) {
|
|
12067
13042
|
if (!/^\d{1,20}$/.test(options.botId)) throw new Error("Telegram bot identity is invalid");
|
|
12068
13043
|
assertTelegramCommandBoundary(desired);
|
|
12069
|
-
const chatKey =
|
|
13044
|
+
const chatKey = createHash6("sha256").update(String(chatId)).digest("hex").slice(0, 16);
|
|
12070
13045
|
const scopes = [
|
|
12071
13046
|
{ name: "default", scope: { type: "default" }, ledgerName: "default" },
|
|
12072
13047
|
{ name: "chat", scope: { type: "chat", chat_id: chatId }, ledgerName: `chat:${chatKey}` },
|
|
@@ -13063,11 +14038,11 @@ ${p}` : p;
|
|
|
13063
14038
|
{ error: safeError2, attempt, maxAttempts, delayMs, operation: "telegram-command-sync" },
|
|
13064
14039
|
"Telegram command menu sync failed, retrying"
|
|
13065
14040
|
);
|
|
13066
|
-
await new Promise((
|
|
13067
|
-
const timer = setTimeout(
|
|
14041
|
+
await new Promise((resolve8) => {
|
|
14042
|
+
const timer = setTimeout(resolve8, delayMs);
|
|
13068
14043
|
signal?.addEventListener("abort", () => {
|
|
13069
14044
|
clearTimeout(timer);
|
|
13070
|
-
|
|
14045
|
+
resolve8();
|
|
13071
14046
|
}, { once: true });
|
|
13072
14047
|
});
|
|
13073
14048
|
}
|
|
@@ -14361,15 +15336,15 @@ var ChannelAdapter = class {
|
|
|
14361
15336
|
function nodeToWebWritable(nodeStream) {
|
|
14362
15337
|
return new WritableStream({
|
|
14363
15338
|
write(chunk) {
|
|
14364
|
-
return new Promise((
|
|
15339
|
+
return new Promise((resolve8, reject) => {
|
|
14365
15340
|
const ok2 = nodeStream.write(chunk);
|
|
14366
15341
|
if (ok2) {
|
|
14367
|
-
|
|
15342
|
+
resolve8();
|
|
14368
15343
|
return;
|
|
14369
15344
|
}
|
|
14370
15345
|
const onDrain = () => {
|
|
14371
15346
|
nodeStream.removeListener("error", onError);
|
|
14372
|
-
|
|
15347
|
+
resolve8();
|
|
14373
15348
|
};
|
|
14374
15349
|
const onError = (err) => {
|
|
14375
15350
|
nodeStream.removeListener("drain", onDrain);
|
|
@@ -14861,12 +15836,12 @@ var TerminalManager = class {
|
|
|
14861
15836
|
signal: state.exitStatus.signal
|
|
14862
15837
|
};
|
|
14863
15838
|
}
|
|
14864
|
-
return new Promise((
|
|
15839
|
+
return new Promise((resolve8) => {
|
|
14865
15840
|
state.process.on("exit", (code, signal) => {
|
|
14866
|
-
|
|
15841
|
+
resolve8({ exitCode: code, signal });
|
|
14867
15842
|
});
|
|
14868
15843
|
if (state.exitStatus !== null) {
|
|
14869
|
-
|
|
15844
|
+
resolve8({
|
|
14870
15845
|
exitCode: state.exitStatus.exitCode,
|
|
14871
15846
|
signal: state.exitStatus.signal
|
|
14872
15847
|
});
|
|
@@ -15148,7 +16123,7 @@ var AgentInstance = class _AgentInstance extends TypedEmitter {
|
|
|
15148
16123
|
env: environment ?? filterEnv(process.env, agentDef.env)
|
|
15149
16124
|
}
|
|
15150
16125
|
);
|
|
15151
|
-
await new Promise((
|
|
16126
|
+
await new Promise((resolve8, reject) => {
|
|
15152
16127
|
instance.child.on("error", (err) => {
|
|
15153
16128
|
reject(
|
|
15154
16129
|
new Error(
|
|
@@ -15156,7 +16131,7 @@ var AgentInstance = class _AgentInstance extends TypedEmitter {
|
|
|
15156
16131
|
)
|
|
15157
16132
|
);
|
|
15158
16133
|
});
|
|
15159
|
-
instance.child.on("spawn", () =>
|
|
16134
|
+
instance.child.on("spawn", () => resolve8());
|
|
15160
16135
|
});
|
|
15161
16136
|
instance.stderrCapture = new StderrCapture(50);
|
|
15162
16137
|
instance.child.stderr.on("data", (chunk) => {
|
|
@@ -15783,15 +16758,15 @@ ${skipNote}`;
|
|
|
15783
16758
|
this._destroying = true;
|
|
15784
16759
|
this.terminalManager.destroyAll();
|
|
15785
16760
|
if (this.child.exitCode !== null) return;
|
|
15786
|
-
await new Promise((
|
|
16761
|
+
await new Promise((resolve8) => {
|
|
15787
16762
|
this.child.on("exit", () => {
|
|
15788
16763
|
clearTimeout(forceKillTimer);
|
|
15789
|
-
|
|
16764
|
+
resolve8();
|
|
15790
16765
|
});
|
|
15791
16766
|
this.child.kill("SIGTERM");
|
|
15792
16767
|
const forceKillTimer = setTimeout(() => {
|
|
15793
16768
|
if (this.child.exitCode === null) this.child.kill("SIGKILL");
|
|
15794
|
-
|
|
16769
|
+
resolve8();
|
|
15795
16770
|
}, 1e4);
|
|
15796
16771
|
if (typeof forceKillTimer === "object" && forceKillTimer !== null && "unref" in forceKillTimer) {
|
|
15797
16772
|
forceKillTimer.unref();
|
|
@@ -15802,7 +16777,7 @@ ${skipNote}`;
|
|
|
15802
16777
|
var AGENT_INIT_TIMEOUT_MS = 3e4;
|
|
15803
16778
|
function withAgentTimeout(promise, agentName, op, timeoutMs) {
|
|
15804
16779
|
const timeout = timeoutMs ?? AGENT_INIT_TIMEOUT_MS;
|
|
15805
|
-
return new Promise((
|
|
16780
|
+
return new Promise((resolve8, reject) => {
|
|
15806
16781
|
const timer = setTimeout(() => {
|
|
15807
16782
|
reject(new Error(
|
|
15808
16783
|
`Agent "${agentName}" did not respond to ${op} within ${timeout / 1e3}s. The agent process may have hung during initialization.`
|
|
@@ -15811,7 +16786,7 @@ function withAgentTimeout(promise, agentName, op, timeoutMs) {
|
|
|
15811
16786
|
promise.then(
|
|
15812
16787
|
(val) => {
|
|
15813
16788
|
clearTimeout(timer);
|
|
15814
|
-
|
|
16789
|
+
resolve8(val);
|
|
15815
16790
|
},
|
|
15816
16791
|
(err) => {
|
|
15817
16792
|
clearTimeout(timer);
|
|
@@ -16075,8 +17050,8 @@ var PromptQueue = class {
|
|
|
16075
17050
|
if (this.processing) {
|
|
16076
17051
|
const position = this.queue.length + 1;
|
|
16077
17052
|
this.onActuallyQueued?.(turnId, position, routing);
|
|
16078
|
-
return new Promise((
|
|
16079
|
-
this.queue.push({ text: text3, userPrompt, attachments, routing, turnId, meta, resolve:
|
|
17053
|
+
return new Promise((resolve8) => {
|
|
17054
|
+
this.queue.push({ text: text3, userPrompt, attachments, routing, turnId, meta, resolve: resolve8 });
|
|
16080
17055
|
});
|
|
16081
17056
|
}
|
|
16082
17057
|
await this.process(text3, userPrompt, attachments, routing, turnId, meta);
|
|
@@ -16206,8 +17181,8 @@ var PermissionGate = class {
|
|
|
16206
17181
|
this.request = request;
|
|
16207
17182
|
this.settled = false;
|
|
16208
17183
|
this.clearTimeout();
|
|
16209
|
-
return new Promise((
|
|
16210
|
-
this.resolveFn =
|
|
17184
|
+
return new Promise((resolve8, reject) => {
|
|
17185
|
+
this.resolveFn = resolve8;
|
|
16211
17186
|
this.rejectFn = reject;
|
|
16212
17187
|
this.timeoutTimer = setTimeout(() => {
|
|
16213
17188
|
this.reject("Permission request timed out (no response received)");
|
|
@@ -18123,12 +19098,12 @@ var SessionBridge = class {
|
|
|
18123
19098
|
break;
|
|
18124
19099
|
case "image_content": {
|
|
18125
19100
|
if (this.deps.fileService) {
|
|
18126
|
-
const
|
|
19101
|
+
const fs39 = this.deps.fileService;
|
|
18127
19102
|
const sid = this.session.id;
|
|
18128
19103
|
const { data, mimeType } = event;
|
|
18129
19104
|
const buffer = Buffer.from(data, "base64");
|
|
18130
|
-
const ext =
|
|
18131
|
-
|
|
19105
|
+
const ext = fs39.extensionFromMime(mimeType);
|
|
19106
|
+
fs39.saveFile(sid, `agent-image${ext}`, buffer, mimeType).then((att) => {
|
|
18132
19107
|
this.sendMessage(sid, {
|
|
18133
19108
|
type: "attachment",
|
|
18134
19109
|
text: "",
|
|
@@ -18140,12 +19115,12 @@ var SessionBridge = class {
|
|
|
18140
19115
|
}
|
|
18141
19116
|
case "audio_content": {
|
|
18142
19117
|
if (this.deps.fileService) {
|
|
18143
|
-
const
|
|
19118
|
+
const fs39 = this.deps.fileService;
|
|
18144
19119
|
const sid = this.session.id;
|
|
18145
19120
|
const { data, mimeType } = event;
|
|
18146
19121
|
const buffer = Buffer.from(data, "base64");
|
|
18147
|
-
const ext =
|
|
18148
|
-
|
|
19122
|
+
const ext = fs39.extensionFromMime(mimeType);
|
|
19123
|
+
fs39.saveFile(sid, `agent-audio${ext}`, buffer, mimeType).then((att) => {
|
|
18149
19124
|
this.sendMessage(sid, {
|
|
18150
19125
|
type: "attachment",
|
|
18151
19126
|
text: "",
|
|
@@ -18757,7 +19732,7 @@ var SessionFactory = class {
|
|
|
18757
19732
|
};
|
|
18758
19733
|
|
|
18759
19734
|
// src/core/core.ts
|
|
18760
|
-
import
|
|
19735
|
+
import path18 from "path";
|
|
18761
19736
|
import { nanoid as nanoid3 } from "nanoid";
|
|
18762
19737
|
|
|
18763
19738
|
// src/core/sessions/session-store.ts
|
|
@@ -20192,16 +21167,51 @@ function createPluginContext(opts) {
|
|
|
20192
21167
|
|
|
20193
21168
|
// src/core/plugin/lifecycle-manager.ts
|
|
20194
21169
|
init_events();
|
|
21170
|
+
init_plugin_installer();
|
|
21171
|
+
import fs16 from "fs";
|
|
21172
|
+
import path15 from "path";
|
|
21173
|
+
import { createHash as createHash3, randomUUID as randomUUID3 } from "crypto";
|
|
20195
21174
|
var SETUP_TIMEOUT_MS = 3e4;
|
|
20196
21175
|
var TEARDOWN_TIMEOUT_MS = 1e4;
|
|
21176
|
+
function migrationGuardPath(instanceRoot, pluginName) {
|
|
21177
|
+
return path15.join(instanceRoot, "plugin-migration-quarantine", `${createHash3("sha256").update(pluginName).digest("hex")}.json`);
|
|
21178
|
+
}
|
|
21179
|
+
function writeMigrationGuard(instanceRoot, pluginName, fromVersion, toVersion) {
|
|
21180
|
+
const guardPath = migrationGuardPath(instanceRoot, pluginName);
|
|
21181
|
+
const directory = path15.dirname(guardPath);
|
|
21182
|
+
fs16.mkdirSync(directory, { recursive: true, mode: 448 });
|
|
21183
|
+
fs16.chmodSync(directory, 448);
|
|
21184
|
+
const temporary = `${guardPath}.${process.pid}.${randomUUID3()}.tmp`;
|
|
21185
|
+
try {
|
|
21186
|
+
fs16.writeFileSync(temporary, `${JSON.stringify({ version: 1, pluginName, fromVersion, toVersion, createdAt: (/* @__PURE__ */ new Date()).toISOString() }, null, 2)}
|
|
21187
|
+
`, { flag: "wx", mode: 384 });
|
|
21188
|
+
const fd = fs16.openSync(temporary, "r");
|
|
21189
|
+
fs16.fsyncSync(fd);
|
|
21190
|
+
fs16.closeSync(fd);
|
|
21191
|
+
fs16.renameSync(temporary, guardPath);
|
|
21192
|
+
fs16.chmodSync(guardPath, 384);
|
|
21193
|
+
try {
|
|
21194
|
+
const dir = fs16.openSync(directory, "r");
|
|
21195
|
+
fs16.fsyncSync(dir);
|
|
21196
|
+
fs16.closeSync(dir);
|
|
21197
|
+
} catch {
|
|
21198
|
+
}
|
|
21199
|
+
} finally {
|
|
21200
|
+
try {
|
|
21201
|
+
if (fs16.existsSync(temporary)) fs16.unlinkSync(temporary);
|
|
21202
|
+
} catch {
|
|
21203
|
+
}
|
|
21204
|
+
}
|
|
21205
|
+
return guardPath;
|
|
21206
|
+
}
|
|
20197
21207
|
function withTimeout(promise, ms, label) {
|
|
20198
|
-
return new Promise((
|
|
21208
|
+
return new Promise((resolve8, reject) => {
|
|
20199
21209
|
const timer = setTimeout(() => reject(new Error(`Timeout: ${label} exceeded ${ms}ms`)), ms);
|
|
20200
21210
|
if (typeof timer === "object" && timer !== null && "unref" in timer) {
|
|
20201
21211
|
;
|
|
20202
21212
|
timer.unref();
|
|
20203
21213
|
}
|
|
20204
|
-
promise.then(
|
|
21214
|
+
promise.then(resolve8, reject).finally(() => clearTimeout(timer));
|
|
20205
21215
|
});
|
|
20206
21216
|
}
|
|
20207
21217
|
function resolvePluginConfig(pluginName, configManager) {
|
|
@@ -20246,6 +21256,8 @@ var LifecycleManager = class {
|
|
|
20246
21256
|
settingsManager;
|
|
20247
21257
|
pluginRegistry;
|
|
20248
21258
|
_instanceRoot;
|
|
21259
|
+
installRecoveryChecked = false;
|
|
21260
|
+
communityRecoveryError;
|
|
20249
21261
|
contexts = /* @__PURE__ */ new Map();
|
|
20250
21262
|
loadOrder = [];
|
|
20251
21263
|
_loaded = /* @__PURE__ */ new Set();
|
|
@@ -20312,6 +21324,17 @@ var LifecycleManager = class {
|
|
|
20312
21324
|
* Already-loaded plugins are included in dependency resolution but not re-booted.
|
|
20313
21325
|
*/
|
|
20314
21326
|
async boot(plugins) {
|
|
21327
|
+
if (!this.installRecoveryChecked && this._instanceRoot && this.pluginRegistry) {
|
|
21328
|
+
this.installRecoveryChecked = true;
|
|
21329
|
+
try {
|
|
21330
|
+
await recoverPluginInstallTransaction(this._instanceRoot);
|
|
21331
|
+
await this.pluginRegistry.load();
|
|
21332
|
+
} catch (error) {
|
|
21333
|
+
const code = error instanceof PluginInstallError ? error.code : "PLUGIN_RECOVERY_FAILED";
|
|
21334
|
+
this.communityRecoveryError = `${code}: Community plugin transaction recovery did not complete; community plugins are quarantined until recovery succeeds.`;
|
|
21335
|
+
this.log?.error(this.communityRecoveryError);
|
|
21336
|
+
}
|
|
21337
|
+
}
|
|
20315
21338
|
const newNames = new Set(plugins.map((p) => p.name));
|
|
20316
21339
|
const allForResolution = [...this.loadOrder.filter((p) => !newNames.has(p.name)), ...plugins];
|
|
20317
21340
|
let sorted;
|
|
@@ -20345,13 +21368,27 @@ var LifecycleManager = class {
|
|
|
20345
21368
|
}
|
|
20346
21369
|
}
|
|
20347
21370
|
const registryEntry = this.pluginRegistry?.get(plugin.name);
|
|
21371
|
+
if (registryEntry && registryEntry.source !== "builtin" && this.communityRecoveryError) {
|
|
21372
|
+
this._failed.add(plugin.name);
|
|
21373
|
+
this.eventBus?.emit(BusEvent.PLUGIN_FAILED, { name: plugin.name, code: "PLUGIN_RECOVERY_FAILED", error: this.communityRecoveryError });
|
|
21374
|
+
continue;
|
|
21375
|
+
}
|
|
21376
|
+
if (this._instanceRoot && fs16.existsSync(migrationGuardPath(this._instanceRoot, plugin.name))) {
|
|
21377
|
+
const status = `PLUGIN_MIGRATION_ROLLBACK_FAILED: ${plugin.name} has a durable migration quarantine marker. Repair or restore its settings and registry entry, then remove the marker before re-enabling it.`;
|
|
21378
|
+
this._failed.add(plugin.name);
|
|
21379
|
+
this.getPluginLogger(plugin.name).error(status);
|
|
21380
|
+
this.eventBus?.emit(BusEvent.PLUGIN_FAILED, { name: plugin.name, code: "PLUGIN_MIGRATION_ROLLBACK_FAILED", error: status });
|
|
21381
|
+
continue;
|
|
21382
|
+
}
|
|
20348
21383
|
if (registryEntry && registryEntry.enabled === false) {
|
|
20349
21384
|
this.eventBus?.emit(BusEvent.PLUGIN_DISABLED, { name: plugin.name });
|
|
20350
21385
|
continue;
|
|
20351
21386
|
}
|
|
20352
21387
|
if (registryEntry && plugin.migrate && registryEntry.version !== plugin.version && this.settingsManager) {
|
|
21388
|
+
const oldSettings = structuredClone(await this.settingsManager.loadSettings(plugin.name));
|
|
21389
|
+
const oldRegistryEntry = structuredClone(registryEntry);
|
|
21390
|
+
const migrationGuard = this._instanceRoot ? writeMigrationGuard(this._instanceRoot, plugin.name, registryEntry.version, plugin.version) : void 0;
|
|
20353
21391
|
try {
|
|
20354
|
-
const oldSettings = await this.settingsManager.loadSettings(plugin.name);
|
|
20355
21392
|
const pluginLog = this.getPluginLogger(plugin.name);
|
|
20356
21393
|
const migrateCtx = {
|
|
20357
21394
|
pluginName: plugin.name,
|
|
@@ -20363,13 +21400,38 @@ var LifecycleManager = class {
|
|
|
20363
21400
|
SETUP_TIMEOUT_MS,
|
|
20364
21401
|
`${plugin.name}.migrate()`
|
|
20365
21402
|
);
|
|
20366
|
-
|
|
20367
|
-
|
|
21403
|
+
const migratedSettings = newSettings && typeof newSettings === "object" ? newSettings : await migrateCtx.settings.getAll();
|
|
21404
|
+
if (plugin.settingsSchema) {
|
|
21405
|
+
const validation = this.settingsManager.validateSettings(plugin.name, migratedSettings, plugin.settingsSchema);
|
|
21406
|
+
if (!validation.valid) throw new Error(`migrated settings invalid: ${validation.errors?.join("; ")}`);
|
|
20368
21407
|
}
|
|
21408
|
+
await migrateCtx.settings.setAll(migratedSettings);
|
|
20369
21409
|
this.pluginRegistry.updateVersion(plugin.name, plugin.version);
|
|
20370
21410
|
await this.pluginRegistry.save();
|
|
20371
|
-
|
|
20372
|
-
|
|
21411
|
+
if (migrationGuard) fs16.rmSync(migrationGuard, { force: true });
|
|
21412
|
+
} catch {
|
|
21413
|
+
let settingsRestored = true;
|
|
21414
|
+
try {
|
|
21415
|
+
await this.settingsManager.createAPI(plugin.name).setAll(oldSettings);
|
|
21416
|
+
} catch {
|
|
21417
|
+
settingsRestored = false;
|
|
21418
|
+
}
|
|
21419
|
+
this.pluginRegistry.restore(plugin.name, oldRegistryEntry);
|
|
21420
|
+
let registryRestored = true;
|
|
21421
|
+
try {
|
|
21422
|
+
await this.pluginRegistry.save();
|
|
21423
|
+
} catch {
|
|
21424
|
+
registryRestored = false;
|
|
21425
|
+
}
|
|
21426
|
+
if (settingsRestored && registryRestored && migrationGuard) fs16.rmSync(migrationGuard, { force: true });
|
|
21427
|
+
this._failed.add(plugin.name);
|
|
21428
|
+
const rollbackComplete = settingsRestored && registryRestored;
|
|
21429
|
+
const code = rollbackComplete ? "PLUGIN_MIGRATION_FAILED" : "PLUGIN_MIGRATION_ROLLBACK_FAILED";
|
|
21430
|
+
const recovery = rollbackComplete ? "the exact previous registry entry and settings were restored" : "rollback persistence was incomplete and the durable migration quarantine remains in place";
|
|
21431
|
+
const status = `${code}: ${plugin.name} migration ${oldRegistryEntry.version} \u2192 ${plugin.version} failed; ${recovery}. Fix the migration/settings before retrying.`;
|
|
21432
|
+
this.getPluginLogger(plugin.name).error(status);
|
|
21433
|
+
this.eventBus?.emit(BusEvent.PLUGIN_FAILED, { name: plugin.name, code, error: status });
|
|
21434
|
+
continue;
|
|
20373
21435
|
}
|
|
20374
21436
|
}
|
|
20375
21437
|
let pluginConfig;
|
|
@@ -21049,7 +22111,7 @@ var OpenACPCore = class {
|
|
|
21049
22111
|
}
|
|
21050
22112
|
);
|
|
21051
22113
|
registerCoreMenuItems(this.menuRegistry);
|
|
21052
|
-
this.assistantRegistry.setInstanceRoot(
|
|
22114
|
+
this.assistantRegistry.setInstanceRoot(path18.dirname(ctx.root));
|
|
21053
22115
|
this.assistantRegistry.register(createSessionsSection(this));
|
|
21054
22116
|
this.assistantRegistry.register(createAgentsSection(this));
|
|
21055
22117
|
this.assistantRegistry.register(createConfigSection(this));
|
|
@@ -21869,33 +22931,33 @@ init_doctor();
|
|
|
21869
22931
|
init_config_registry();
|
|
21870
22932
|
|
|
21871
22933
|
// src/core/config/config-editor.ts
|
|
21872
|
-
import * as
|
|
22934
|
+
import * as path34 from "path";
|
|
21873
22935
|
import * as clack2 from "@clack/prompts";
|
|
21874
22936
|
|
|
21875
22937
|
// src/cli/autostart.ts
|
|
21876
22938
|
init_log();
|
|
21877
22939
|
import { execFileSync as execFileSync6 } from "child_process";
|
|
21878
|
-
import * as
|
|
21879
|
-
import * as
|
|
22940
|
+
import * as fs31 from "fs";
|
|
22941
|
+
import * as path29 from "path";
|
|
21880
22942
|
import * as os9 from "os";
|
|
21881
22943
|
var log20 = createChildLogger({ module: "autostart" });
|
|
21882
|
-
var LEGACY_LAUNCHD_PLIST_PATH =
|
|
21883
|
-
var LEGACY_SYSTEMD_SERVICE_PATH =
|
|
22944
|
+
var LEGACY_LAUNCHD_PLIST_PATH = path29.join(os9.homedir(), "Library", "LaunchAgents", "com.openacp.daemon.plist");
|
|
22945
|
+
var LEGACY_SYSTEMD_SERVICE_PATH = path29.join(os9.homedir(), ".config", "systemd", "user", "openacp.service");
|
|
21884
22946
|
function getLaunchdLabel(instanceId) {
|
|
21885
22947
|
return `com.openacp.daemon.${instanceId}`;
|
|
21886
22948
|
}
|
|
21887
22949
|
function getLaunchdPlistPath(instanceId) {
|
|
21888
|
-
return
|
|
22950
|
+
return path29.join(os9.homedir(), "Library", "LaunchAgents", `${getLaunchdLabel(instanceId)}.plist`);
|
|
21889
22951
|
}
|
|
21890
22952
|
function getSystemdServiceName(instanceId) {
|
|
21891
22953
|
return `openacp-${instanceId}`;
|
|
21892
22954
|
}
|
|
21893
22955
|
function getSystemdServicePath(instanceId) {
|
|
21894
|
-
return
|
|
22956
|
+
return path29.join(os9.homedir(), ".config", "systemd", "user", `${getSystemdServiceName(instanceId)}.service`);
|
|
21895
22957
|
}
|
|
21896
22958
|
function getAutoStartState(instanceId) {
|
|
21897
22959
|
if (process.platform === "linux") {
|
|
21898
|
-
const installed =
|
|
22960
|
+
const installed = fs31.existsSync(getSystemdServicePath(instanceId));
|
|
21899
22961
|
if (!installed) return { installed: false, manager: null, active: false };
|
|
21900
22962
|
try {
|
|
21901
22963
|
const output = execFileSync6("systemctl", [
|
|
@@ -21917,7 +22979,7 @@ function getAutoStartState(instanceId) {
|
|
|
21917
22979
|
}
|
|
21918
22980
|
}
|
|
21919
22981
|
if (process.platform === "darwin") {
|
|
21920
|
-
const installed =
|
|
22982
|
+
const installed = fs31.existsSync(getLaunchdPlistPath(instanceId));
|
|
21921
22983
|
if (!installed) return { installed: false, manager: null, active: false };
|
|
21922
22984
|
try {
|
|
21923
22985
|
const uid = process.getuid();
|
|
@@ -21947,7 +23009,7 @@ function escapeSystemdValue(str) {
|
|
|
21947
23009
|
}
|
|
21948
23010
|
function generateLaunchdPlist(nodePath, cliPath, logDir2, instanceRoot, instanceId) {
|
|
21949
23011
|
const label = getLaunchdLabel(instanceId);
|
|
21950
|
-
const logFile =
|
|
23012
|
+
const logFile = path29.join(logDir2, "openacp.log");
|
|
21951
23013
|
return `<?xml version="1.0" encoding="UTF-8"?>
|
|
21952
23014
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
21953
23015
|
<plist version="1.0">
|
|
@@ -21999,25 +23061,25 @@ WantedBy=default.target
|
|
|
21999
23061
|
`;
|
|
22000
23062
|
}
|
|
22001
23063
|
function migrateLegacy() {
|
|
22002
|
-
if (process.platform === "darwin" &&
|
|
23064
|
+
if (process.platform === "darwin" && fs31.existsSync(LEGACY_LAUNCHD_PLIST_PATH)) {
|
|
22003
23065
|
try {
|
|
22004
23066
|
const uid = process.getuid();
|
|
22005
23067
|
execFileSync6("launchctl", ["bootout", `gui/${uid}`, "com.openacp.daemon"], { stdio: "pipe" });
|
|
22006
23068
|
} catch {
|
|
22007
23069
|
}
|
|
22008
23070
|
try {
|
|
22009
|
-
|
|
23071
|
+
fs31.unlinkSync(LEGACY_LAUNCHD_PLIST_PATH);
|
|
22010
23072
|
} catch {
|
|
22011
23073
|
}
|
|
22012
23074
|
log20.info("Removed legacy single-instance LaunchAgent");
|
|
22013
23075
|
}
|
|
22014
|
-
if (process.platform === "linux" &&
|
|
23076
|
+
if (process.platform === "linux" && fs31.existsSync(LEGACY_SYSTEMD_SERVICE_PATH)) {
|
|
22015
23077
|
try {
|
|
22016
23078
|
execFileSync6("systemctl", ["--user", "disable", "openacp"], { stdio: "pipe" });
|
|
22017
23079
|
} catch {
|
|
22018
23080
|
}
|
|
22019
23081
|
try {
|
|
22020
|
-
|
|
23082
|
+
fs31.unlinkSync(LEGACY_SYSTEMD_SERVICE_PATH);
|
|
22021
23083
|
} catch {
|
|
22022
23084
|
}
|
|
22023
23085
|
try {
|
|
@@ -22032,26 +23094,26 @@ function installAutoStart(logDir2, instanceRoot, instanceId) {
|
|
|
22032
23094
|
return { success: false, error: "Auto-start not supported on this platform" };
|
|
22033
23095
|
}
|
|
22034
23096
|
const nodePath = process.execPath;
|
|
22035
|
-
const cliPath =
|
|
22036
|
-
const resolvedLogDir = logDir2.startsWith("~") ?
|
|
23097
|
+
const cliPath = path29.resolve(process.argv[1]);
|
|
23098
|
+
const resolvedLogDir = logDir2.startsWith("~") ? path29.join(os9.homedir(), logDir2.slice(1)) : logDir2;
|
|
22037
23099
|
try {
|
|
22038
23100
|
migrateLegacy();
|
|
22039
23101
|
if (process.platform === "darwin") {
|
|
22040
23102
|
const plistPath = getLaunchdPlistPath(instanceId);
|
|
22041
23103
|
const plist = generateLaunchdPlist(nodePath, cliPath, resolvedLogDir, instanceRoot, instanceId);
|
|
22042
|
-
const dir =
|
|
22043
|
-
|
|
23104
|
+
const dir = path29.dirname(plistPath);
|
|
23105
|
+
fs31.mkdirSync(dir, { recursive: true });
|
|
22044
23106
|
const uid = process.getuid();
|
|
22045
23107
|
const domain = `gui/${uid}`;
|
|
22046
|
-
const previous =
|
|
23108
|
+
const previous = fs31.existsSync(plistPath) ? fs31.readFileSync(plistPath, "utf8") : void 0;
|
|
22047
23109
|
const previousState = getAutoStartState(instanceId);
|
|
22048
23110
|
const tmp = `${plistPath}.${process.pid}.tmp`;
|
|
22049
23111
|
let replaced = false;
|
|
22050
23112
|
let bootedOut = false;
|
|
22051
23113
|
try {
|
|
22052
|
-
|
|
23114
|
+
fs31.writeFileSync(tmp, plist, { mode: 384 });
|
|
22053
23115
|
execFileSync6("plutil", ["-lint", tmp], { stdio: "pipe" });
|
|
22054
|
-
|
|
23116
|
+
fs31.renameSync(tmp, plistPath);
|
|
22055
23117
|
replaced = true;
|
|
22056
23118
|
if (previousState.active) {
|
|
22057
23119
|
execFileSync6("launchctl", ["bootout", domain, plistPath], { stdio: "pipe" });
|
|
@@ -22060,7 +23122,7 @@ function installAutoStart(logDir2, instanceRoot, instanceId) {
|
|
|
22060
23122
|
execFileSync6("launchctl", ["bootstrap", domain, plistPath], { stdio: "pipe" });
|
|
22061
23123
|
} catch (error) {
|
|
22062
23124
|
try {
|
|
22063
|
-
|
|
23125
|
+
fs31.rmSync(tmp, { force: true });
|
|
22064
23126
|
} catch {
|
|
22065
23127
|
}
|
|
22066
23128
|
if (replaced) {
|
|
@@ -22070,11 +23132,11 @@ function installAutoStart(logDir2, instanceRoot, instanceId) {
|
|
|
22070
23132
|
rollbackBootedOut = true;
|
|
22071
23133
|
} catch {
|
|
22072
23134
|
}
|
|
22073
|
-
if (previous === void 0)
|
|
23135
|
+
if (previous === void 0) fs31.rmSync(plistPath, { force: true });
|
|
22074
23136
|
else {
|
|
22075
23137
|
const rollbackTmp = `${plistPath}.${process.pid}.rollback.tmp`;
|
|
22076
|
-
|
|
22077
|
-
|
|
23138
|
+
fs31.writeFileSync(rollbackTmp, previous, { mode: 384 });
|
|
23139
|
+
fs31.renameSync(rollbackTmp, plistPath);
|
|
22078
23140
|
if (previousState.active || bootedOut || rollbackBootedOut) execFileSync6("launchctl", ["bootstrap", domain, plistPath], { stdio: "pipe" });
|
|
22079
23141
|
}
|
|
22080
23142
|
}
|
|
@@ -22087,18 +23149,18 @@ function installAutoStart(logDir2, instanceRoot, instanceId) {
|
|
|
22087
23149
|
const servicePath = getSystemdServicePath(instanceId);
|
|
22088
23150
|
const serviceName = getSystemdServiceName(instanceId);
|
|
22089
23151
|
const unit = generateSystemdUnit(nodePath, cliPath, instanceRoot, instanceId);
|
|
22090
|
-
const dir =
|
|
22091
|
-
|
|
22092
|
-
const previous =
|
|
23152
|
+
const dir = path29.dirname(servicePath);
|
|
23153
|
+
fs31.mkdirSync(dir, { recursive: true });
|
|
23154
|
+
const previous = fs31.existsSync(servicePath) ? fs31.readFileSync(servicePath, "utf8") : void 0;
|
|
22093
23155
|
const tmp = `${servicePath}.${process.pid}.tmp`;
|
|
22094
|
-
|
|
22095
|
-
|
|
23156
|
+
fs31.writeFileSync(tmp, unit, { mode: 384 });
|
|
23157
|
+
fs31.renameSync(tmp, servicePath);
|
|
22096
23158
|
try {
|
|
22097
23159
|
execFileSync6("systemctl", ["--user", "daemon-reload"], { stdio: "pipe" });
|
|
22098
23160
|
execFileSync6("systemctl", ["--user", "enable", serviceName], { stdio: "pipe" });
|
|
22099
23161
|
} catch (error) {
|
|
22100
|
-
if (previous === void 0)
|
|
22101
|
-
else
|
|
23162
|
+
if (previous === void 0) fs31.unlinkSync(servicePath);
|
|
23163
|
+
else fs31.writeFileSync(servicePath, previous);
|
|
22102
23164
|
try {
|
|
22103
23165
|
execFileSync6("systemctl", ["--user", "daemon-reload"], { stdio: "pipe" });
|
|
22104
23166
|
} catch {
|
|
@@ -22122,13 +23184,13 @@ function uninstallAutoStart(instanceId) {
|
|
|
22122
23184
|
try {
|
|
22123
23185
|
if (process.platform === "darwin") {
|
|
22124
23186
|
const plistPath = getLaunchdPlistPath(instanceId);
|
|
22125
|
-
if (
|
|
23187
|
+
if (fs31.existsSync(plistPath)) {
|
|
22126
23188
|
const uid = process.getuid();
|
|
22127
23189
|
try {
|
|
22128
23190
|
execFileSync6("launchctl", ["bootout", `gui/${uid}`, plistPath], { stdio: "pipe" });
|
|
22129
23191
|
} catch {
|
|
22130
23192
|
}
|
|
22131
|
-
|
|
23193
|
+
fs31.unlinkSync(plistPath);
|
|
22132
23194
|
log20.info({ instanceId }, "LaunchAgent removed");
|
|
22133
23195
|
}
|
|
22134
23196
|
return { success: true };
|
|
@@ -22136,12 +23198,12 @@ function uninstallAutoStart(instanceId) {
|
|
|
22136
23198
|
if (process.platform === "linux") {
|
|
22137
23199
|
const servicePath = getSystemdServicePath(instanceId);
|
|
22138
23200
|
const serviceName = getSystemdServiceName(instanceId);
|
|
22139
|
-
if (
|
|
23201
|
+
if (fs31.existsSync(servicePath)) {
|
|
22140
23202
|
try {
|
|
22141
23203
|
execFileSync6("systemctl", ["--user", "disable", serviceName], { stdio: "pipe" });
|
|
22142
23204
|
} catch {
|
|
22143
23205
|
}
|
|
22144
|
-
|
|
23206
|
+
fs31.unlinkSync(servicePath);
|
|
22145
23207
|
execFileSync6("systemctl", ["--user", "daemon-reload"], { stdio: "pipe" });
|
|
22146
23208
|
log20.info({ instanceId }, "systemd user service removed");
|
|
22147
23209
|
}
|
|
@@ -22156,10 +23218,10 @@ function uninstallAutoStart(instanceId) {
|
|
|
22156
23218
|
}
|
|
22157
23219
|
function isAutoStartInstalled(instanceId) {
|
|
22158
23220
|
if (process.platform === "darwin") {
|
|
22159
|
-
return
|
|
23221
|
+
return fs31.existsSync(getLaunchdPlistPath(instanceId));
|
|
22160
23222
|
}
|
|
22161
23223
|
if (process.platform === "linux") {
|
|
22162
|
-
return
|
|
23224
|
+
return fs31.existsSync(getSystemdServicePath(instanceId));
|
|
22163
23225
|
}
|
|
22164
23226
|
return false;
|
|
22165
23227
|
}
|
|
@@ -22168,25 +23230,25 @@ function isAutoStartInstalled(instanceId) {
|
|
|
22168
23230
|
init_instance_context();
|
|
22169
23231
|
init_instance_registry();
|
|
22170
23232
|
init_log();
|
|
22171
|
-
import
|
|
22172
|
-
import
|
|
23233
|
+
import fs33 from "fs";
|
|
23234
|
+
import path31 from "path";
|
|
22173
23235
|
var log21 = createChildLogger({ module: "resolve-instance-id" });
|
|
22174
23236
|
function resolveInstanceId(instanceRoot) {
|
|
22175
23237
|
try {
|
|
22176
|
-
const configPath =
|
|
22177
|
-
const raw = JSON.parse(
|
|
23238
|
+
const configPath = path31.join(instanceRoot, "config.json");
|
|
23239
|
+
const raw = JSON.parse(fs33.readFileSync(configPath, "utf-8"));
|
|
22178
23240
|
if (raw.id && typeof raw.id === "string") return raw.id;
|
|
22179
23241
|
} catch {
|
|
22180
23242
|
}
|
|
22181
23243
|
try {
|
|
22182
|
-
const reg = new InstanceRegistry(
|
|
23244
|
+
const reg = new InstanceRegistry(path31.join(getGlobalRoot(), "instances.json"));
|
|
22183
23245
|
reg.load();
|
|
22184
23246
|
const entry = reg.getByRoot(instanceRoot);
|
|
22185
23247
|
if (entry?.id) return entry.id;
|
|
22186
23248
|
} catch (err) {
|
|
22187
23249
|
log21.debug({ err: err.message, instanceRoot }, "Could not read instance registry, using fallback id");
|
|
22188
23250
|
}
|
|
22189
|
-
return
|
|
23251
|
+
return path31.basename(path31.dirname(instanceRoot)).replace(/[^a-zA-Z0-9-]/g, "-") || "default";
|
|
22190
23252
|
}
|
|
22191
23253
|
|
|
22192
23254
|
// src/core/config/config-editor.ts
|
|
@@ -22758,7 +23820,7 @@ async function runConfigEditor(configManager, mode = "file", apiPort, settingsMa
|
|
|
22758
23820
|
await configManager.load();
|
|
22759
23821
|
const config = configManager.get();
|
|
22760
23822
|
const updates = {};
|
|
22761
|
-
const instanceRoot =
|
|
23823
|
+
const instanceRoot = path34.dirname(configManager.getConfigPath());
|
|
22762
23824
|
console.log(`
|
|
22763
23825
|
${c.cyan}${c.bold}OpenACP Config Editor${c.reset}`);
|
|
22764
23826
|
console.log(dim(`Config: ${configManager.getConfigPath()}`));
|
|
@@ -22815,17 +23877,17 @@ ${c.cyan}${c.bold}OpenACP Config Editor${c.reset}`);
|
|
|
22815
23877
|
async function sendConfigViaApi(port, updates) {
|
|
22816
23878
|
const { apiCall: call } = await Promise.resolve().then(() => (init_api_client(), api_client_exports));
|
|
22817
23879
|
const paths = flattenToPaths(updates);
|
|
22818
|
-
for (const { path:
|
|
23880
|
+
for (const { path: path39, value } of paths) {
|
|
22819
23881
|
const res = await call(port, "/api/config", {
|
|
22820
23882
|
method: "PATCH",
|
|
22821
23883
|
headers: { "Content-Type": "application/json" },
|
|
22822
|
-
body: JSON.stringify({ path:
|
|
23884
|
+
body: JSON.stringify({ path: path39, value })
|
|
22823
23885
|
});
|
|
22824
23886
|
const data = await res.json();
|
|
22825
23887
|
if (!res.ok) {
|
|
22826
|
-
console.log(warn(`Failed to update ${
|
|
23888
|
+
console.log(warn(`Failed to update ${path39}: ${data.error}`));
|
|
22827
23889
|
} else if (data.needsRestart) {
|
|
22828
|
-
console.log(warn(`${
|
|
23890
|
+
console.log(warn(`${path39} updated \u2014 restart required`));
|
|
22829
23891
|
}
|
|
22830
23892
|
}
|
|
22831
23893
|
}
|
|
@@ -22845,25 +23907,25 @@ function flattenToPaths(obj, prefix = "") {
|
|
|
22845
23907
|
// src/cli/daemon.ts
|
|
22846
23908
|
init_config();
|
|
22847
23909
|
import { spawn as spawn3 } from "child_process";
|
|
22848
|
-
import * as
|
|
22849
|
-
import * as
|
|
23910
|
+
import * as fs35 from "fs";
|
|
23911
|
+
import * as path35 from "path";
|
|
22850
23912
|
function getPidPath(root) {
|
|
22851
|
-
return
|
|
23913
|
+
return path35.join(root, "openacp.pid");
|
|
22852
23914
|
}
|
|
22853
23915
|
function getLogDir(root) {
|
|
22854
|
-
return
|
|
23916
|
+
return path35.join(root, "logs");
|
|
22855
23917
|
}
|
|
22856
23918
|
function getRunningMarker(root) {
|
|
22857
|
-
return
|
|
23919
|
+
return path35.join(root, "running");
|
|
22858
23920
|
}
|
|
22859
23921
|
function writePidFile(pidPath, pid) {
|
|
22860
|
-
const dir =
|
|
22861
|
-
|
|
22862
|
-
|
|
23922
|
+
const dir = path35.dirname(pidPath);
|
|
23923
|
+
fs35.mkdirSync(dir, { recursive: true });
|
|
23924
|
+
fs35.writeFileSync(pidPath, String(pid));
|
|
22863
23925
|
}
|
|
22864
23926
|
function readPidFile(pidPath) {
|
|
22865
23927
|
try {
|
|
22866
|
-
const content =
|
|
23928
|
+
const content = fs35.readFileSync(pidPath, "utf-8").trim();
|
|
22867
23929
|
const pid = parseInt(content, 10);
|
|
22868
23930
|
return isNaN(pid) ? null : pid;
|
|
22869
23931
|
} catch {
|
|
@@ -22872,7 +23934,7 @@ function readPidFile(pidPath) {
|
|
|
22872
23934
|
}
|
|
22873
23935
|
function removePidFile(pidPath) {
|
|
22874
23936
|
try {
|
|
22875
|
-
|
|
23937
|
+
fs35.unlinkSync(pidPath);
|
|
22876
23938
|
} catch {
|
|
22877
23939
|
}
|
|
22878
23940
|
}
|
|
@@ -22905,12 +23967,12 @@ function startDaemon(pidPath, logDir2, instanceRoot) {
|
|
|
22905
23967
|
return { error: `Already running (PID ${pid})` };
|
|
22906
23968
|
}
|
|
22907
23969
|
const resolvedLogDir = logDir2 ? expandHome2(logDir2) : getLogDir(instanceRoot);
|
|
22908
|
-
|
|
22909
|
-
const logFile =
|
|
22910
|
-
const cliPath =
|
|
23970
|
+
fs35.mkdirSync(resolvedLogDir, { recursive: true });
|
|
23971
|
+
const logFile = path35.join(resolvedLogDir, "openacp.log");
|
|
23972
|
+
const cliPath = path35.resolve(process.argv[1]);
|
|
22911
23973
|
const nodePath = process.execPath;
|
|
22912
|
-
const out =
|
|
22913
|
-
const err =
|
|
23974
|
+
const out = fs35.openSync(logFile, "a");
|
|
23975
|
+
const err = fs35.openSync(logFile, "a");
|
|
22914
23976
|
const child = spawn3(nodePath, [cliPath, "--daemon-child"], {
|
|
22915
23977
|
detached: true,
|
|
22916
23978
|
stdio: ["ignore", out, err],
|
|
@@ -22919,8 +23981,8 @@ function startDaemon(pidPath, logDir2, instanceRoot) {
|
|
|
22919
23981
|
...instanceRoot ? { OPENACP_INSTANCE_ROOT: instanceRoot } : {}
|
|
22920
23982
|
}
|
|
22921
23983
|
});
|
|
22922
|
-
|
|
22923
|
-
|
|
23984
|
+
fs35.closeSync(out);
|
|
23985
|
+
fs35.closeSync(err);
|
|
22924
23986
|
if (!child.pid) {
|
|
22925
23987
|
return { error: "Failed to spawn daemon process" };
|
|
22926
23988
|
}
|
|
@@ -22929,7 +23991,7 @@ function startDaemon(pidPath, logDir2, instanceRoot) {
|
|
|
22929
23991
|
return { pid: child.pid };
|
|
22930
23992
|
}
|
|
22931
23993
|
function sleep2(ms) {
|
|
22932
|
-
return new Promise((
|
|
23994
|
+
return new Promise((resolve8) => setTimeout(resolve8, ms));
|
|
22933
23995
|
}
|
|
22934
23996
|
function isProcessAlive2(pid) {
|
|
22935
23997
|
try {
|
|
@@ -22991,12 +24053,12 @@ async function stopDaemon(pidPath, instanceRoot) {
|
|
|
22991
24053
|
}
|
|
22992
24054
|
function markRunning(root) {
|
|
22993
24055
|
const marker = getRunningMarker(root);
|
|
22994
|
-
|
|
22995
|
-
|
|
24056
|
+
fs35.mkdirSync(path35.dirname(marker), { recursive: true });
|
|
24057
|
+
fs35.writeFileSync(marker, "");
|
|
22996
24058
|
}
|
|
22997
24059
|
function clearRunning(root) {
|
|
22998
24060
|
try {
|
|
22999
|
-
|
|
24061
|
+
fs35.unlinkSync(getRunningMarker(root));
|
|
23000
24062
|
} catch {
|
|
23001
24063
|
}
|
|
23002
24064
|
}
|