create-egregore 0.21.2 → 0.21.4
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/assets/egregore-launcher.js +152 -56
- package/package.json +1 -1
- package/runtime/claude/.claude/context/telemetry.md +27 -48
- package/runtime/claude/.claude/skills/issue/SKILL.md +13 -0
- package/runtime/claude/bin/settings.sh +69 -11
- package/runtime/claude/egregore_runtime/issue_reporting.py +38 -7
- package/runtime/codex/bin/settings.sh +69 -11
- package/runtime/codex/egregore_runtime/issue_reporting.py +38 -7
- package/runtime/pi/bin/settings.sh +69 -11
- package/runtime/pi/egregore_runtime/issue_reporting.py +38 -7
- package/runtime/prime/bin/settings.sh +69 -11
- package/runtime/prime/egregore_runtime/issue_reporting.py +38 -7
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
// egregore-launcher-version: 0.21.
|
|
2
|
+
// egregore-launcher-version: 0.21.4
|
|
3
3
|
//
|
|
4
4
|
// egregore — launch and browse Egregore instances from your terminal.
|
|
5
5
|
// Installed by create-egregore. Reads ~/.egregore/instances.json.
|
|
@@ -35,7 +35,7 @@ const crypto = require("node:crypto");
|
|
|
35
35
|
const { spawnSync, spawn } = require("node:child_process");
|
|
36
36
|
const readline = require("node:readline");
|
|
37
37
|
|
|
38
|
-
const VERSION = "0.21.
|
|
38
|
+
const VERSION = "0.21.4";
|
|
39
39
|
const REGISTRY = path.join(os.homedir(), ".egregore", "instances.json");
|
|
40
40
|
const DEFAULT_FILE = path.join(os.homedir(), ".egregore", "default");
|
|
41
41
|
const UPDATE_CACHE_FILE = path.join(os.homedir(), ".egregore", "update-check.json");
|
|
@@ -1707,6 +1707,18 @@ function connectedOrganizationId(config, response) {
|
|
|
1707
1707
|
return returned || current;
|
|
1708
1708
|
}
|
|
1709
1709
|
|
|
1710
|
+
function validatedConnectedClaim(config, response, expectedOrgIds = []) {
|
|
1711
|
+
const orgId = connectedOrganizationId(config, response);
|
|
1712
|
+
for (const expectedOrgId of expectedOrgIds) {
|
|
1713
|
+
if (expectedOrgId) connectedOrganizationId({ org_id: expectedOrgId }, response);
|
|
1714
|
+
}
|
|
1715
|
+
if (typeof response?.api_url !== "string" || !response.api_url.trim() ||
|
|
1716
|
+
typeof response?.api_key !== "string" || !response.api_key.trim()) {
|
|
1717
|
+
die("Connected did not return usable access settings. Local configuration is unchanged; run the same command to retry.");
|
|
1718
|
+
}
|
|
1719
|
+
return { orgId, apiUrl: response.api_url, apiKey: response.api_key };
|
|
1720
|
+
}
|
|
1721
|
+
|
|
1710
1722
|
function hostedMemorySyncEnabled(config) {
|
|
1711
1723
|
const override = process.env.EGREGORE_GRAPH_PROJECTION;
|
|
1712
1724
|
if (["1", "true", "enabled"].includes(override)) return true;
|
|
@@ -1714,6 +1726,25 @@ function hostedMemorySyncEnabled(config) {
|
|
|
1714
1726
|
return config.features?.graph_projection === true;
|
|
1715
1727
|
}
|
|
1716
1728
|
|
|
1729
|
+
function connectionHasSubscription(status) {
|
|
1730
|
+
if (typeof status?.entitled === "boolean") return status.entitled;
|
|
1731
|
+
return ["entitled", "provisioning", "backfilling", "connected"].includes(status?.status);
|
|
1732
|
+
}
|
|
1733
|
+
|
|
1734
|
+
function refreshConnectedRuntime(inst) {
|
|
1735
|
+
// Connect is an instance-scoped overlay; never refresh other runtimes here.
|
|
1736
|
+
const result = spawnSync(
|
|
1737
|
+
"npx", ["-y", `${PUBLIC_NPX_PACKAGE}@${VERSION}`, "--refresh-connect"],
|
|
1738
|
+
{ cwd: inst.path, encoding: "utf-8", stdio: "pipe", timeout: 3 * 60 * 1000 },
|
|
1739
|
+
);
|
|
1740
|
+
if (result.status === 0) {
|
|
1741
|
+
console.log(` ${GREEN}✓${RESET} Connect features installed`);
|
|
1742
|
+
} else {
|
|
1743
|
+
console.log(` ${YELLOW}!${RESET} Connect features will retry on the next update`);
|
|
1744
|
+
console.log(` ${DIM}npx -y ${PUBLIC_NPX_PACKAGE}@${VERSION} --refresh-connect${RESET}`);
|
|
1745
|
+
}
|
|
1746
|
+
}
|
|
1747
|
+
|
|
1717
1748
|
async function cmdConnect(args) {
|
|
1718
1749
|
const fresh = args.includes("--fresh");
|
|
1719
1750
|
const positional = args.filter((a) => !String(a).startsWith("--"));
|
|
@@ -1733,7 +1764,7 @@ async function cmdConnect(args) {
|
|
|
1733
1764
|
const activation = state.connected_activation || {};
|
|
1734
1765
|
let expectedOrgId = cfg.org_id && !String(cfg.org_id).startsWith("legacy-org:")
|
|
1735
1766
|
? cfg.org_id : activation.org_id || "";
|
|
1736
|
-
if (cfg.mode === "connected" && activation.status === "connected") {
|
|
1767
|
+
if (cfg.mode === "connected" && activation.status === "connected" && activation.intent_id) {
|
|
1737
1768
|
if (activation.intent_id) {
|
|
1738
1769
|
const repaired = await connectedApiRequest(
|
|
1739
1770
|
githubToken,
|
|
@@ -1746,16 +1777,14 @@ async function cmdConnect(args) {
|
|
|
1746
1777
|
`Connected is active, but its shared configuration could not be published: ${connectError(repaired, `HTTP ${repaired.status}`)}`,
|
|
1747
1778
|
);
|
|
1748
1779
|
}
|
|
1749
|
-
const
|
|
1780
|
+
const access = validatedConnectedClaim(cfg, repaired.data, [activation.org_id]);
|
|
1750
1781
|
writeJsonFileAtomic(path.join(inst.path, "egregore.json"), {
|
|
1751
1782
|
...cfg,
|
|
1752
|
-
|
|
1783
|
+
org_id: access.orgId,
|
|
1753
1784
|
mode: "connected",
|
|
1754
|
-
api_url:
|
|
1785
|
+
api_url: access.apiUrl,
|
|
1755
1786
|
});
|
|
1756
|
-
|
|
1757
|
-
writeEnvValueAtomic(path.join(inst.path, ".env"), "EGREGORE_API_KEY", repaired.data.api_key);
|
|
1758
|
-
}
|
|
1787
|
+
writeEnvValueAtomic(path.join(inst.path, ".env"), "EGREGORE_API_KEY", access.apiKey);
|
|
1759
1788
|
}
|
|
1760
1789
|
normalizeClaudeDenyRules(inst.path);
|
|
1761
1790
|
ensureClaudeModeStatusLine(inst.path);
|
|
@@ -1766,18 +1795,80 @@ async function cmdConnect(args) {
|
|
|
1766
1795
|
|
|
1767
1796
|
console.log("");
|
|
1768
1797
|
console.log(` ${BOLD}CONNECT${RESET} ${DIM}${inst.slug}${RESET}`);
|
|
1769
|
-
console.log(` ${DIM}
|
|
1798
|
+
console.log(` ${DIM}Connect this installation · existing Markdown and Git stay in place${RESET}`);
|
|
1770
1799
|
console.log("");
|
|
1771
1800
|
|
|
1772
1801
|
let intentId = activation.intent_id || "";
|
|
1773
1802
|
let connectUrl = activation.connect_url || "";
|
|
1774
1803
|
let ticketExpiresAt = Number(activation.connect_ticket_expires_at || 0) * 1000;
|
|
1775
|
-
|
|
1804
|
+
let checkoutRequired = true;
|
|
1805
|
+
const resumable = ["created", "checkout_pending", "entitled", "provisioning", "backfilling", "connected"].includes(
|
|
1776
1806
|
activation.status,
|
|
1777
1807
|
);
|
|
1808
|
+
const connectRequest = {
|
|
1809
|
+
org_slug: cfg.slug || inst.slug,
|
|
1810
|
+
plan: "connect",
|
|
1811
|
+
instance_metadata: {
|
|
1812
|
+
launcher_version: VERSION,
|
|
1813
|
+
mode: cfg.mode || "local",
|
|
1814
|
+
repo_name: cfg.repo_name || "",
|
|
1815
|
+
managed_repo_count: Array.isArray(cfg.repos) ? cfg.repos.length : 0,
|
|
1816
|
+
hosted_memory_sync: hostedMemorySyncEnabled(cfg),
|
|
1817
|
+
fresh,
|
|
1818
|
+
},
|
|
1819
|
+
};
|
|
1820
|
+
if (intentId && resumable) {
|
|
1821
|
+
const current = await connectedApiRequest(
|
|
1822
|
+
githubToken, "GET", `/api/billing/connect/${encodeURIComponent(intentId)}`,
|
|
1823
|
+
);
|
|
1824
|
+
if (current.status !== 200) {
|
|
1825
|
+
die(`could not check the existing connection: ${connectError(current, `HTTP ${current.status}`)}. Run the same command to retry.`);
|
|
1826
|
+
}
|
|
1827
|
+
checkoutRequired = !connectionHasSubscription(current.data);
|
|
1828
|
+
if (checkoutRequired && ["entitled", "provisioning", "backfilling", "connected"].includes(current.data?.status)) {
|
|
1829
|
+
die("Your subscription is not active. Check its billing status before reconnecting this installation.");
|
|
1830
|
+
}
|
|
1831
|
+
if (current.data?.entitled === true &&
|
|
1832
|
+
[activation.status, current.data.status].some(value => ["created", "checkout_pending"].includes(value))) {
|
|
1833
|
+
if (activation.org_id && !String(activation.org_id).startsWith("legacy-org:")) {
|
|
1834
|
+
connectedOrganizationId(cfg, { org_id: activation.org_id });
|
|
1835
|
+
}
|
|
1836
|
+
if (current.data.org_slug && current.data.org_slug !== connectRequest.org_slug) {
|
|
1837
|
+
die("The saved connection belongs to another Egregore. Local configuration is unchanged.");
|
|
1838
|
+
}
|
|
1839
|
+
// Retry this saved attempt with the current target and explicit optional
|
|
1840
|
+
// sync choice. No local writes until the API confirms a claimable intent.
|
|
1841
|
+
const retryKey = crypto.createHash("sha256").update(JSON.stringify([
|
|
1842
|
+
intentId, connectRequest.org_slug, connectRequest.instance_metadata.repo_name,
|
|
1843
|
+
connectRequest.instance_metadata.hosted_memory_sync,
|
|
1844
|
+
])).digest("hex");
|
|
1845
|
+
const recovered = await connectedApiRequest(
|
|
1846
|
+
githubToken, "POST", "/api/billing/connect-intents", connectRequest,
|
|
1847
|
+
{ "X-Idempotency-Key": `cli:recover:${retryKey}`, "X-Egregore-Connect-Recovery": "1" },
|
|
1848
|
+
);
|
|
1849
|
+
if (recovered.status !== 200) {
|
|
1850
|
+
die(`could not recover the saved connection: ${connectError(recovered, `HTTP ${recovered.status}`)}. Run the same command to retry.`);
|
|
1851
|
+
}
|
|
1852
|
+
const result = recovered.data;
|
|
1853
|
+
if (typeof result?.connection_intent_id !== "string" || !result.connection_intent_id.trim() ||
|
|
1854
|
+
result.entitled !== true || result.checkout_required !== false ||
|
|
1855
|
+
!["entitled", "provisioning", "backfilling", "connected"].includes(result.status) ||
|
|
1856
|
+
(result.org_slug && result.org_slug !== connectRequest.org_slug)) {
|
|
1857
|
+
die("The saved connection could not be recovered without a new purchase. Check its billing status, then retry; your saved activation is unchanged.");
|
|
1858
|
+
}
|
|
1859
|
+
intentId = result.connection_intent_id;
|
|
1860
|
+
checkoutRequired = false;
|
|
1861
|
+
connectUrl = "";
|
|
1862
|
+
persistConnectState(inst, {
|
|
1863
|
+
intent_id: intentId, status: result.status, checkout_required: false,
|
|
1864
|
+
connect_url: "", connect_ticket: undefined, connect_ticket_expires_at: undefined,
|
|
1865
|
+
});
|
|
1866
|
+
}
|
|
1867
|
+
}
|
|
1778
1868
|
if (
|
|
1779
1869
|
intentId &&
|
|
1780
1870
|
resumable &&
|
|
1871
|
+
checkoutRequired &&
|
|
1781
1872
|
(!activation.connect_ticket || !ticketExpiresAt || ticketExpiresAt <= Date.now() + 60_000)
|
|
1782
1873
|
) {
|
|
1783
1874
|
const refreshed = await connectedApiRequest(
|
|
@@ -1822,31 +1913,22 @@ async function cmdConnect(args) {
|
|
|
1822
1913
|
githubToken,
|
|
1823
1914
|
"POST",
|
|
1824
1915
|
"/api/billing/connect-intents",
|
|
1825
|
-
|
|
1826
|
-
|
|
1827
|
-
plan: "connect",
|
|
1828
|
-
instance_metadata: {
|
|
1829
|
-
launcher_version: VERSION,
|
|
1830
|
-
mode: cfg.mode || "local",
|
|
1831
|
-
repo_name: cfg.repo_name || "",
|
|
1832
|
-
managed_repo_count: Array.isArray(cfg.repos) ? cfg.repos.length : 0,
|
|
1833
|
-
hosted_memory_sync: hostedMemorySyncEnabled(cfg),
|
|
1834
|
-
fresh,
|
|
1835
|
-
},
|
|
1836
|
-
},
|
|
1837
|
-
{ "X-Idempotency-Key": `cli:${inst.slug}:${crypto.randomUUID()}` },
|
|
1916
|
+
connectRequest,
|
|
1917
|
+
{ "X-Idempotency-Key": `cli:${inst.slug}:${crypto.randomUUID()}`, "X-Egregore-Connect-Recovery": "1" },
|
|
1838
1918
|
);
|
|
1839
1919
|
if (checkout.status !== 200) {
|
|
1840
|
-
die(`could not
|
|
1920
|
+
die(`could not connect this installation: ${connectError(checkout, `HTTP ${checkout.status}`)}`);
|
|
1841
1921
|
}
|
|
1842
1922
|
intentId = checkout.data.connection_intent_id;
|
|
1843
|
-
connectUrl = checkout.data.connect_url;
|
|
1923
|
+
connectUrl = checkout.data.connect_url || "";
|
|
1924
|
+
checkoutRequired = checkout.data.checkout_required !== false && !connectionHasSubscription(checkout.data);
|
|
1844
1925
|
persistConnectState(inst, {
|
|
1845
1926
|
intent_id: intentId,
|
|
1846
1927
|
org_id: expectedOrgId,
|
|
1847
1928
|
connect_ticket: checkout.data.connect_ticket,
|
|
1848
1929
|
connect_ticket_expires_at: checkout.data.connect_ticket_expires_at,
|
|
1849
1930
|
connect_url: connectUrl,
|
|
1931
|
+
checkout_required: checkoutRequired,
|
|
1850
1932
|
status: checkout.data.status || "created",
|
|
1851
1933
|
});
|
|
1852
1934
|
console.log(` [1/5] ${GREEN}✓${RESET} Connection intent`);
|
|
@@ -1865,16 +1947,16 @@ async function cmdConnect(args) {
|
|
|
1865
1947
|
console.log(` [1/5] ${GREEN}✓${RESET} Resuming connection intent`);
|
|
1866
1948
|
}
|
|
1867
1949
|
|
|
1868
|
-
if (
|
|
1950
|
+
if (checkoutRequired) {
|
|
1869
1951
|
if (connectUrl) {
|
|
1870
1952
|
const opened = openExternalUrl(connectUrl);
|
|
1871
1953
|
const mark = opened ? `${GREEN}✓${RESET}` : `${YELLOW}!${RESET}`;
|
|
1872
1954
|
console.log(` [2/5] ${mark} ${opened ? "Opened" : "Open"} Egregore Connect`);
|
|
1873
1955
|
console.log(` ${CYAN}${connectUrl}${RESET}`);
|
|
1874
1956
|
}
|
|
1875
|
-
console.log(` ${DIM}Complete checkout in the browser
|
|
1957
|
+
console.log(` ${DIM}New subscription: €500/month. Complete checkout in the browser; promotion codes are entered in Stripe.${RESET}`);
|
|
1876
1958
|
} else {
|
|
1877
|
-
console.log(` [2/5] ${GREEN}✓${RESET}
|
|
1959
|
+
console.log(` [2/5] ${GREEN}✓${RESET} Existing subscription found · no new purchase needed`);
|
|
1878
1960
|
}
|
|
1879
1961
|
|
|
1880
1962
|
const pollMs = Math.max(100, Number(process.env.EGREGORE_CONNECT_POLL_INTERVAL_MS || 2000));
|
|
@@ -1889,10 +1971,10 @@ async function cmdConnect(args) {
|
|
|
1889
1971
|
);
|
|
1890
1972
|
if (result.status === 200) {
|
|
1891
1973
|
status = result.data;
|
|
1892
|
-
if (
|
|
1893
|
-
|
|
1894
|
-
|
|
1895
|
-
|
|
1974
|
+
if (connectionHasSubscription(status)) break;
|
|
1975
|
+
if (["entitled", "provisioning", "backfilling", "connected"].includes(status.status)) {
|
|
1976
|
+
die("Your subscription is not active. Check its billing status before reconnecting this installation.");
|
|
1977
|
+
}
|
|
1896
1978
|
if (["canceled", "failed"].includes(status.status)) {
|
|
1897
1979
|
persistConnectState(inst, { status: status.status });
|
|
1898
1980
|
die(`Connected checkout ${status.status}. Your local Egregore was not changed.`);
|
|
@@ -1902,7 +1984,7 @@ async function cmdConnect(args) {
|
|
|
1902
1984
|
}
|
|
1903
1985
|
await sleep(pollMs);
|
|
1904
1986
|
}
|
|
1905
|
-
if (!
|
|
1987
|
+
if (!connectionHasSubscription(status)) {
|
|
1906
1988
|
die("timed out waiting for checkout. Run the same command to resume.");
|
|
1907
1989
|
}
|
|
1908
1990
|
console.log(` [3/5] ${GREEN}✓${RESET} Subscription confirmed`);
|
|
@@ -1921,17 +2003,25 @@ async function cmdConnect(args) {
|
|
|
1921
2003
|
const configFile = path.join(inst.path, "egregore.json");
|
|
1922
2004
|
const envFile = path.join(inst.path, ".env");
|
|
1923
2005
|
const currentConfig = readJsonFile(configFile);
|
|
1924
|
-
const
|
|
2006
|
+
const access = validatedConnectedClaim(currentConfig, claim.data, [expectedOrgId, activation.org_id]);
|
|
1925
2007
|
const syncEnabled = hostedMemorySyncEnabled(currentConfig);
|
|
1926
2008
|
writeJsonFileAtomic(configFile, {
|
|
1927
2009
|
...currentConfig,
|
|
1928
|
-
|
|
2010
|
+
org_id: access.orgId,
|
|
1929
2011
|
mode: "connected",
|
|
1930
|
-
api_url:
|
|
2012
|
+
api_url: access.apiUrl,
|
|
1931
2013
|
});
|
|
1932
|
-
writeEnvValueAtomic(envFile, "EGREGORE_API_KEY",
|
|
2014
|
+
writeEnvValueAtomic(envFile, "EGREGORE_API_KEY", access.apiKey);
|
|
1933
2015
|
normalizeClaudeDenyRules(inst.path);
|
|
1934
2016
|
ensureClaudeModeStatusLine(inst.path);
|
|
2017
|
+
if (claim.data.status === "connected") {
|
|
2018
|
+
persistConnectState(inst, { status: "connected", activation_protocol: claim.data.activation_protocol });
|
|
2019
|
+
console.log(` [4/5] ${GREEN}✓${RESET} Access restored for this installation`);
|
|
2020
|
+
console.log(` [5/5] ${GREEN}✓${RESET} Existing connection restored · memory sync was not restarted`);
|
|
2021
|
+
refreshConnectedRuntime(inst);
|
|
2022
|
+
console.log(`\n ${GREEN}✓${RESET} ${BOLD}${inst.slug}${RESET} is connected. Local files remain the source of truth.\n`);
|
|
2023
|
+
return;
|
|
2024
|
+
}
|
|
1935
2025
|
persistConnectState(inst, {
|
|
1936
2026
|
status: "backfilling",
|
|
1937
2027
|
activation_protocol: claim.data.activation_protocol,
|
|
@@ -1991,23 +2081,7 @@ async function cmdConnect(args) {
|
|
|
1991
2081
|
console.log(` ${DIM}${line}${RESET}`);
|
|
1992
2082
|
}
|
|
1993
2083
|
}
|
|
1994
|
-
|
|
1995
|
-
const runtimeRefresh = spawnSync(
|
|
1996
|
-
"npx",
|
|
1997
|
-
["-y", `${PUBLIC_NPX_PACKAGE}@${VERSION}`, "--refresh-connect"],
|
|
1998
|
-
{
|
|
1999
|
-
cwd: inst.path,
|
|
2000
|
-
encoding: "utf-8",
|
|
2001
|
-
stdio: "pipe",
|
|
2002
|
-
timeout: 3 * 60 * 1000,
|
|
2003
|
-
},
|
|
2004
|
-
);
|
|
2005
|
-
if (runtimeRefresh.status === 0) {
|
|
2006
|
-
console.log(` ${GREEN}✓${RESET} Connect features installed`);
|
|
2007
|
-
} else {
|
|
2008
|
-
console.log(` ${YELLOW}!${RESET} Connect features will retry on the next update`);
|
|
2009
|
-
console.log(` ${DIM}npx -y ${PUBLIC_NPX_PACKAGE}@${VERSION} --refresh-connect${RESET}`);
|
|
2010
|
-
}
|
|
2084
|
+
refreshConnectedRuntime(inst);
|
|
2011
2085
|
console.log("");
|
|
2012
2086
|
console.log(` ${GREEN}✓${RESET} ${BOLD}${inst.slug}${RESET} is connected. Local files remain the source of truth.`);
|
|
2013
2087
|
console.log("");
|
|
@@ -3562,6 +3636,13 @@ function renderSettingsPanel({ inst, data, cursor, view, domain, items, listCurs
|
|
|
3562
3636
|
}
|
|
3563
3637
|
} else {
|
|
3564
3638
|
const upgradeValue = runtimeUpdateRowValue(upgrade, candidate);
|
|
3639
|
+
const hostingReadiness = d.hosting === false ? "disabled" : d.hosting_status?.readiness;
|
|
3640
|
+
const hostingLabels = {
|
|
3641
|
+
disabled: `${YELLOW}OFF${RESET}`,
|
|
3642
|
+
connected_configured: `${GREEN}ACCESS CONFIGURED${RESET}`,
|
|
3643
|
+
public_relay: `${YELLOW}PUBLIC RELAY${RESET}`,
|
|
3644
|
+
setup_incomplete: `${YELLOW}SETUP INCOMPLETE${RESET}`,
|
|
3645
|
+
};
|
|
3565
3646
|
const rows = [
|
|
3566
3647
|
{
|
|
3567
3648
|
name: "Runtime update",
|
|
@@ -3571,7 +3652,7 @@ function renderSettingsPanel({ inst, data, cursor, view, domain, items, listCurs
|
|
|
3571
3652
|
},
|
|
3572
3653
|
{ name: "Privacy & access", value: `${DIM}identity · boundary · consent${RESET}` },
|
|
3573
3654
|
{ name: "Telemetry", value: `${DIM}personal · opt-out${RESET}` },
|
|
3574
|
-
{ name: "Hosted Artifacts", value:
|
|
3655
|
+
{ name: "Hosted Artifacts", value: hostingLabels[hostingReadiness] || `${YELLOW}STATUS UNKNOWN${RESET}` },
|
|
3575
3656
|
{
|
|
3576
3657
|
name: "Git workflow",
|
|
3577
3658
|
value: d.workflow === "simple"
|
|
@@ -3591,6 +3672,21 @@ function renderSettingsPanel({ inst, data, cursor, view, domain, items, listCurs
|
|
|
3591
3672
|
lines.push(boxLine(`${marker} ${pad(name, 20)} ${row.value}`, innerWidth, margin));
|
|
3592
3673
|
});
|
|
3593
3674
|
lines.push(emptyBoxLine(innerWidth, margin));
|
|
3675
|
+
if (cursor === 3) {
|
|
3676
|
+
const hostingHints = {
|
|
3677
|
+
disabled: ["Publishing is turned off."],
|
|
3678
|
+
connected_configured: ["Access is configured. Connection not checked."],
|
|
3679
|
+
public_relay: ["Pages are public without sign-in and expire after 7 days."],
|
|
3680
|
+
setup_incomplete: [
|
|
3681
|
+
"Publishing is enabled; connection setup is incomplete.",
|
|
3682
|
+
`Run ${d.hosting_status?.recovery_command || `egregore connect ${inst.slug}`}`,
|
|
3683
|
+
],
|
|
3684
|
+
};
|
|
3685
|
+
for (const hint of hostingHints[hostingReadiness] || ["Update this Egregore to check publishing readiness."]) {
|
|
3686
|
+
lines.push(boxLine(`${DIM}${hint}${RESET}`, innerWidth, margin));
|
|
3687
|
+
}
|
|
3688
|
+
lines.push(emptyBoxLine(innerWidth, margin));
|
|
3689
|
+
}
|
|
3594
3690
|
if (notice) lines.push(boxLine(notice, innerWidth, margin));
|
|
3595
3691
|
lines.push(boxLine(`${DIM}↑↓ nav · ${RESET}${BOLD}enter${RESET}${DIM} ${cursor === 3 ? "toggle" : "open"} · ← back · q quit${RESET}`, innerWidth, margin));
|
|
3596
3692
|
}
|
package/package.json
CHANGED
|
@@ -153,51 +153,30 @@ On the first session where telemetry events are emitted, if `telemetry_noticed`
|
|
|
153
153
|
|
|
154
154
|
Then set `telemetry_noticed: true` in the state file. Never repeat this notice.
|
|
155
155
|
|
|
156
|
-
##
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
-
|
|
183
|
-
- Conversation transcript or user prompts
|
|
184
|
-
- Environment variables or secrets
|
|
185
|
-
- Org-specific data (sanitized before sending — org names, person names, tokens replaced)
|
|
186
|
-
|
|
187
|
-
### Opt-out
|
|
188
|
-
|
|
189
|
-
- `EGREGORE_NO_REPORTS=1` in `.env`
|
|
190
|
-
- `.egregore-state.json` → `"session_reports": false`
|
|
191
|
-
- Simply answer "No thanks" when prompted during `/wrap`
|
|
192
|
-
- `DO_NOT_TRACK=1` also disables reports
|
|
193
|
-
|
|
194
|
-
### Transport
|
|
195
|
-
|
|
196
|
-
- Direct to Supabase via anon key (INSERT-only RLS policy)
|
|
197
|
-
- If network fails: saved locally to `~/.egregore/reports/` as fallback
|
|
198
|
-
- Telegram notification to maintainers fires server-side via Supabase DB webhook
|
|
199
|
-
- Optional: GitHub issue creation on `egregore-labs/egregore` (user chooses each time)
|
|
200
|
-
|
|
201
|
-
### Configuration
|
|
202
|
-
|
|
203
|
-
Reports require `report_url` and `report_key` in `egregore.json`. New OSS installations get these automatically via `create-egregore`. If missing, the report prompt is skipped entirely.
|
|
156
|
+
## Framework support and legacy session feedback
|
|
157
|
+
|
|
158
|
+
For an Egregore bug, including the historical `/issue egregore:` wording, use
|
|
159
|
+
the [issue workflow](../skills/issue/SKILL.md). Connected instances prepare an
|
|
160
|
+
exact private support preview and submit only after confirmation. Local users
|
|
161
|
+
can explicitly choose a public upstream report. An internal issue stays within
|
|
162
|
+
its organization's memory or configured tracker; it does not automatically
|
|
163
|
+
reach Egregore support. `/wrap` does not collect or submit session feedback.
|
|
164
|
+
|
|
165
|
+
The older `bin/session-report.sh` is a feedback-storage compatibility helper,
|
|
166
|
+
not a support-reporting route. It inserts selected feedback into `session_reports`
|
|
167
|
+
when `report_url` and `report_key` are configured. A `submitted` receipt means
|
|
168
|
+
only that storage accepted the feedback: no GitHub issue was created and no
|
|
169
|
+
Telegram message was sent. The authenticated database callback acknowledges
|
|
170
|
+
storage without planning or dispatching a notification to a named teammate.
|
|
171
|
+
Transport failure saves feedback locally under `~/.egregore/reports/`; that
|
|
172
|
+
does not establish remote storage or delivery. Missing configuration skips
|
|
173
|
+
submission.
|
|
174
|
+
|
|
175
|
+
Never invoke legacy feedback automatically, generate it from a transcript, or
|
|
176
|
+
use it as a fallback when private support fails. Only explicitly selected and
|
|
177
|
+
reviewed content may be shared. The helper adds legacy user and organization
|
|
178
|
+
labels; it does not guarantee anonymization or scrub arbitrary input.
|
|
179
|
+
|
|
180
|
+
Legacy feedback can be disabled with `EGREGORE_NO_REPORTS=1` or
|
|
181
|
+
`DO_NOT_TRACK=1` in the process environment, or `"session_reports": false` in
|
|
182
|
+
`.egregore-state.json`. These switches do not grant consent to submit content.
|
|
@@ -67,6 +67,12 @@ its default recipient is **Egregore support, private**. Local/OSS users may
|
|
|
67
67
|
explicitly choose `upstream` for a **public** report. Connected users can also
|
|
68
68
|
choose public upstream explicitly. Never fall back from private to public.
|
|
69
69
|
|
|
70
|
+
Treat the historical `egregore:` prefix as framework-support intent and use
|
|
71
|
+
this reviewed reporting flow. Never route a framework bug through the
|
|
72
|
+
legacy session-feedback helper: it only stores feedback and does not
|
|
73
|
+
file a support issue or send a Telegram notification. Being Connected does not
|
|
74
|
+
turn an internal issue in the customer's memory into a report to Egregore.
|
|
75
|
+
|
|
70
76
|
Use `'{title}'` for the concise title derived from the user's report and
|
|
71
77
|
`'{report}'` for the report text the user supplied or reviewed.
|
|
72
78
|
Write an embedded single quote as `'\''` in each substituted value.
|
|
@@ -119,6 +125,13 @@ review the private issue in its Project/Archive. Keep a failed/uncertain report'
|
|
|
119
125
|
request ID; no backend failure authorizes public publication or a fresh blind
|
|
120
126
|
submission.
|
|
121
127
|
|
|
128
|
+
Report the confirmed outcome precisely: a preview is not submitted; a local
|
|
129
|
+
issue is saved within this organization; only a matching private receipt with
|
|
130
|
+
`submission_state: filed` means filed with Egregore support. Other states need
|
|
131
|
+
recovery using the same request ID. A filed issue does not prove Telegram
|
|
132
|
+
delivery: `notification_state: unverified` must never be described as sent or
|
|
133
|
+
received. Only a separate delivery receipt can establish that.
|
|
134
|
+
|
|
122
135
|
Public reports use the reporter's GitHub authentication and return a public
|
|
123
136
|
issue URL. Destination comes only from HTTPS GitHub `upstream_url`; absent means
|
|
124
137
|
`egregore-labs/egregore`, `none` means no upstream. Do not infer it from remotes.
|
|
@@ -56,7 +56,7 @@ _control_plane_push() {
|
|
|
56
56
|
local api_url api_key payload actor response revision
|
|
57
57
|
api_url=$(jq -r '.api_url // empty' "$CONFIG" 2>/dev/null)
|
|
58
58
|
[ -n "$api_url" ] || return 0
|
|
59
|
-
api_key=$(grep '^EGREGORE_API_KEY=' "$SCRIPT_DIR/.env" 2>/dev/null | cut -d'=' -f2-)
|
|
59
|
+
api_key=$(grep '^EGREGORE_API_KEY=' "$SCRIPT_DIR/.env" 2>/dev/null | cut -d'=' -f2- || true)
|
|
60
60
|
[ -n "$api_key" ] || return 0
|
|
61
61
|
actor=$(jq -r '.github_username // empty' "$SCRIPT_DIR/.egregore-state.json" 2>/dev/null)
|
|
62
62
|
payload=$(jq -c --arg by "$actor" '{
|
|
@@ -104,22 +104,78 @@ EOF
|
|
|
104
104
|
|
|
105
105
|
# ── verbs ────────────────────────────────────────────────────────────────
|
|
106
106
|
|
|
107
|
+
_hosting_status() {
|
|
108
|
+
# Read local configuration only. A saved key is not proof that the hosted
|
|
109
|
+
# service can be reached or that the credential is still accepted.
|
|
110
|
+
local api_key="" has_key=false slug recovery="egregore connect"
|
|
111
|
+
if [ -r "$SCRIPT_DIR/.env" ]; then
|
|
112
|
+
api_key=$(sed -n 's/^EGREGORE_API_KEY=//p' "$SCRIPT_DIR/.env" | tail -n 1)
|
|
113
|
+
api_key="${api_key#"${api_key%%[![:space:]]*}"}"
|
|
114
|
+
api_key="${api_key%"${api_key##*[![:space:]]}"}"
|
|
115
|
+
case "$api_key" in
|
|
116
|
+
\"*\"|\'*\') api_key="${api_key:1:${#api_key}-2}" ;;
|
|
117
|
+
esac
|
|
118
|
+
[ -n "${api_key//[[:space:]]/}" ] && has_key=true
|
|
119
|
+
fi
|
|
120
|
+
# Match publish-artifact.sh's route selection separately from the saved
|
|
121
|
+
# configuration check. Raw nonempty overrides (even malformed ones) select
|
|
122
|
+
# organisation upload, so they must never imply the public relay's expiry.
|
|
123
|
+
local effective_url="${EGREGORE_API_URL:-}" effective_key="${EGREGORE_API_KEY:-}" org_route=false
|
|
124
|
+
if [ -f "$SCRIPT_DIR/.env" ]; then
|
|
125
|
+
effective_url="${effective_url:-$(grep '^EGREGORE_API_URL=' "$SCRIPT_DIR/.env" 2>/dev/null | cut -d'=' -f2- || true)}"
|
|
126
|
+
effective_key="${effective_key:-$(grep '^EGREGORE_API_KEY=' "$SCRIPT_DIR/.env" 2>/dev/null | cut -d'=' -f2- || true)}"
|
|
127
|
+
fi
|
|
128
|
+
effective_url="${effective_url:-$(jq -r '.api_url // empty' "$CONFIG" 2>/dev/null)}"
|
|
129
|
+
if [ -n "$effective_url" ] && [ -n "$effective_key" ]; then org_route=true; fi
|
|
130
|
+
slug=$(jq -r '.slug // empty' "$CONFIG")
|
|
131
|
+
if _valid_token "$slug"; then recovery="egregore connect $slug"; fi
|
|
132
|
+
jq --argjson has_key "$has_key" --argjson org_route "$org_route" --arg recovery "$recovery" '
|
|
133
|
+
def nonblank: if type == "string" then test("\\S") else false end;
|
|
134
|
+
(.features.publishing != false) as $publishing |
|
|
135
|
+
(.features.public_relay == true) as $relay |
|
|
136
|
+
(((if .mode == null then "connected" else .mode end) == "connected") and
|
|
137
|
+
(.api_url | nonblank) and $has_key) as $configured |
|
|
138
|
+
(if $publishing == false then "disabled"
|
|
139
|
+
elif $configured then "connected_configured"
|
|
140
|
+
elif $org_route then "setup_incomplete"
|
|
141
|
+
elif $relay then "public_relay"
|
|
142
|
+
else "setup_incomplete" end) as $readiness |
|
|
143
|
+
{
|
|
144
|
+
domain: "hosting",
|
|
145
|
+
publishing: $publishing,
|
|
146
|
+
readiness: $readiness,
|
|
147
|
+
connection_configured: $configured,
|
|
148
|
+
public_relay: $relay,
|
|
149
|
+
connectivity: "not_checked",
|
|
150
|
+
recovery_command: (if $readiness == "setup_incomplete" then $recovery else null end)
|
|
151
|
+
}' "$CONFIG"
|
|
152
|
+
}
|
|
153
|
+
|
|
107
154
|
cmd_hosting() {
|
|
108
155
|
local action="${1:-status}"
|
|
109
|
-
# NEVER use jq's `// "true"` here: `//` treats an explicit `false` as empty and
|
|
110
|
-
# would return the default, silently defeating the off-switch. Test `!= false`
|
|
111
|
-
# so absent/true => enabled, explicit false => disabled. `enabled` is the
|
|
112
|
-
# resolved boolean string ("true"/"false").
|
|
113
|
-
local enabled; enabled="$(jq -r '.features.publishing != false' "$CONFIG" 2>/dev/null || echo "true")"
|
|
114
156
|
case "$action" in
|
|
115
157
|
status)
|
|
158
|
+
local status; status="$(_hosting_status)"
|
|
116
159
|
if [ "$JSON" = 1 ]; then
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
160
|
+
printf '%s\n' "$status"
|
|
161
|
+
else
|
|
162
|
+
case "$(printf '%s' "$status" | jq -r '.readiness')" in
|
|
163
|
+
disabled) echo "Hosting (egregore.xyz): OFF" ;;
|
|
164
|
+
connected_configured)
|
|
165
|
+
echo "Hosting (egregore.xyz): ACCESS CONFIGURED"
|
|
166
|
+
echo " Connection not checked." ;;
|
|
167
|
+
public_relay)
|
|
168
|
+
echo "Hosting (egregore.xyz): PUBLIC RELAY"
|
|
169
|
+
echo " Anyone with the link can read published artifacts for 7 days. Connection not checked." ;;
|
|
170
|
+
setup_incomplete)
|
|
171
|
+
echo "Hosting (egregore.xyz): SETUP INCOMPLETE"
|
|
172
|
+
echo " Publishing is enabled; connection setup is incomplete."
|
|
173
|
+
echo " Run: $(printf '%s' "$status" | jq -r '.recovery_command')" ;;
|
|
174
|
+
esac
|
|
175
|
+
fi ;;
|
|
120
176
|
on)
|
|
121
177
|
_save '.features.publishing = true'
|
|
122
|
-
|
|
178
|
+
JSON=0 cmd_hosting status ;;
|
|
123
179
|
off)
|
|
124
180
|
_save '.features.publishing = false'
|
|
125
181
|
echo "Hosting (egregore.xyz): OFF — artifacts will no longer upload to egregore.xyz" ;;
|
|
@@ -378,10 +434,12 @@ cmd_people() {
|
|
|
378
434
|
cmd_dump() {
|
|
379
435
|
# Always JSON — the launcher calls this once to render the whole settings screen.
|
|
380
436
|
local people; people="$(_people_json)"
|
|
381
|
-
|
|
437
|
+
local hosting_status; hosting_status="$(_hosting_status)"
|
|
438
|
+
jq --argjson people "$people" --argjson hosting_status "$hosting_status" '{
|
|
382
439
|
org: .org_name,
|
|
383
440
|
slug: .slug,
|
|
384
441
|
hosting: (.features.publishing != false),
|
|
442
|
+
hosting_status: $hosting_status,
|
|
385
443
|
public_relay: (.features.public_relay == true),
|
|
386
444
|
workflow: (
|
|
387
445
|
if ((if has("base_branch") then .base_branch else "develop" end) == "main") then "simple"
|
|
@@ -21,6 +21,26 @@ from .policy import scope_allows_path
|
|
|
21
21
|
KINDS = {"bug", "improvement", "question"}
|
|
22
22
|
AREAS = {"runtime", "onboarding", "connectors", "site", "other"}
|
|
23
23
|
SEVERITIES = {"critical", "high", "normal"}
|
|
24
|
+
SUPPORT_STATES = {"filed", "failed", "submitting", "uncertain"}
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def support_receipt(result, identifier):
|
|
28
|
+
"""Accept only a receipt for this request; filing never proves notification."""
|
|
29
|
+
if (not isinstance(result, dict) or result.get('request_id') != identifier
|
|
30
|
+
or not isinstance(result.get('submission_state'), str)
|
|
31
|
+
or result['submission_state'] not in SUPPORT_STATES):
|
|
32
|
+
raise ValueError(f"private support returned an invalid receipt; keep request {identifier} and use report-status")
|
|
33
|
+
state = result['submission_state']
|
|
34
|
+
notices = {
|
|
35
|
+
'filed': 'Filed with Egregore support. Telegram delivery has not been verified.',
|
|
36
|
+
'failed': 'Report preserved; support filing failed. Retry this same request.',
|
|
37
|
+
'submitting': 'Report preserved; support filing is not confirmed. Keep this request ID and check report-status.',
|
|
38
|
+
'uncertain': 'Report preserved; support filing needs reconciliation. Keep this request ID and check report-status.',
|
|
39
|
+
}
|
|
40
|
+
return {'request_id': identifier, 'submission_state': state,
|
|
41
|
+
'retryable': state == 'failed',
|
|
42
|
+
'reconciliation_needed': state in {'submitting', 'uncertain'},
|
|
43
|
+
'notification_state': 'unverified', 'notice': notices[state]}
|
|
24
44
|
|
|
25
45
|
|
|
26
46
|
def _scrub(value, *, public=False):
|
|
@@ -207,18 +227,29 @@ class FrameworkReporting:
|
|
|
207
227
|
return {'request_id':identifier,'status':result['status'],'github_url':result['github_url']}
|
|
208
228
|
credential, _ = self._verified_credential(state['destination'],state['authenticated_org'])
|
|
209
229
|
state['status'] = 'submitting'; GitHubIssues._save(path,state)
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
230
|
+
try:
|
|
231
|
+
code, result = self.api(self.root,state['destination'],'/api/v1/support-reports',
|
|
232
|
+
{'request_id':identifier, **state['payload']},credential=credential)
|
|
233
|
+
except OSError:
|
|
234
|
+
state['status'] = 'reconciliation_needed'; GitHubIssues._save(path,state)
|
|
235
|
+
raise OSError(f"private support response unavailable; keep request {identifier} and use report-status or confirm this same preview") from None
|
|
236
|
+
if not 200 <= code < 300:
|
|
213
237
|
state['status'] = 'reconciliation_needed'; GitHubIssues._save(path,state)
|
|
214
238
|
raise ValueError(f"private support request {identifier} needs recovery (HTTP {code}); use report-status or confirm this same preview")
|
|
215
|
-
|
|
216
|
-
|
|
239
|
+
try:
|
|
240
|
+
receipt = support_receipt(result, identifier)
|
|
241
|
+
except ValueError:
|
|
242
|
+
state['status'] = 'reconciliation_needed'; GitHubIssues._save(path,state)
|
|
243
|
+
raise
|
|
244
|
+
state['status'] = receipt['submission_state']; GitHubIssues._save(path,state)
|
|
245
|
+
if state['status'] != 'filed':
|
|
246
|
+
raise ValueError(f"private support request {identifier} needs recovery ({state['status']}); use report-status or confirm this same preview")
|
|
247
|
+
return receipt
|
|
217
248
|
|
|
218
249
|
def status(self, identifier):
|
|
219
250
|
path, state = self._load('report',identifier,write=False)
|
|
220
251
|
credential, _ = self._verified_credential(state['destination'],state['authenticated_org'])
|
|
221
252
|
code, result = self.api(self.root,state['destination'],f'/api/v1/support-reports/{identifier}',credential=credential)
|
|
222
|
-
if code
|
|
253
|
+
if not 200 <= code < 300:
|
|
223
254
|
raise ValueError(f"private support status unavailable (HTTP {code}); keep request {identifier}")
|
|
224
|
-
return result
|
|
255
|
+
return support_receipt(result, identifier)
|
|
@@ -56,7 +56,7 @@ _control_plane_push() {
|
|
|
56
56
|
local api_url api_key payload actor response revision
|
|
57
57
|
api_url=$(jq -r '.api_url // empty' "$CONFIG" 2>/dev/null)
|
|
58
58
|
[ -n "$api_url" ] || return 0
|
|
59
|
-
api_key=$(grep '^EGREGORE_API_KEY=' "$SCRIPT_DIR/.env" 2>/dev/null | cut -d'=' -f2-)
|
|
59
|
+
api_key=$(grep '^EGREGORE_API_KEY=' "$SCRIPT_DIR/.env" 2>/dev/null | cut -d'=' -f2- || true)
|
|
60
60
|
[ -n "$api_key" ] || return 0
|
|
61
61
|
actor=$(jq -r '.github_username // empty' "$SCRIPT_DIR/.egregore-state.json" 2>/dev/null)
|
|
62
62
|
payload=$(jq -c --arg by "$actor" '{
|
|
@@ -104,22 +104,78 @@ EOF
|
|
|
104
104
|
|
|
105
105
|
# ── verbs ────────────────────────────────────────────────────────────────
|
|
106
106
|
|
|
107
|
+
_hosting_status() {
|
|
108
|
+
# Read local configuration only. A saved key is not proof that the hosted
|
|
109
|
+
# service can be reached or that the credential is still accepted.
|
|
110
|
+
local api_key="" has_key=false slug recovery="egregore connect"
|
|
111
|
+
if [ -r "$SCRIPT_DIR/.env" ]; then
|
|
112
|
+
api_key=$(sed -n 's/^EGREGORE_API_KEY=//p' "$SCRIPT_DIR/.env" | tail -n 1)
|
|
113
|
+
api_key="${api_key#"${api_key%%[![:space:]]*}"}"
|
|
114
|
+
api_key="${api_key%"${api_key##*[![:space:]]}"}"
|
|
115
|
+
case "$api_key" in
|
|
116
|
+
\"*\"|\'*\') api_key="${api_key:1:${#api_key}-2}" ;;
|
|
117
|
+
esac
|
|
118
|
+
[ -n "${api_key//[[:space:]]/}" ] && has_key=true
|
|
119
|
+
fi
|
|
120
|
+
# Match publish-artifact.sh's route selection separately from the saved
|
|
121
|
+
# configuration check. Raw nonempty overrides (even malformed ones) select
|
|
122
|
+
# organisation upload, so they must never imply the public relay's expiry.
|
|
123
|
+
local effective_url="${EGREGORE_API_URL:-}" effective_key="${EGREGORE_API_KEY:-}" org_route=false
|
|
124
|
+
if [ -f "$SCRIPT_DIR/.env" ]; then
|
|
125
|
+
effective_url="${effective_url:-$(grep '^EGREGORE_API_URL=' "$SCRIPT_DIR/.env" 2>/dev/null | cut -d'=' -f2- || true)}"
|
|
126
|
+
effective_key="${effective_key:-$(grep '^EGREGORE_API_KEY=' "$SCRIPT_DIR/.env" 2>/dev/null | cut -d'=' -f2- || true)}"
|
|
127
|
+
fi
|
|
128
|
+
effective_url="${effective_url:-$(jq -r '.api_url // empty' "$CONFIG" 2>/dev/null)}"
|
|
129
|
+
if [ -n "$effective_url" ] && [ -n "$effective_key" ]; then org_route=true; fi
|
|
130
|
+
slug=$(jq -r '.slug // empty' "$CONFIG")
|
|
131
|
+
if _valid_token "$slug"; then recovery="egregore connect $slug"; fi
|
|
132
|
+
jq --argjson has_key "$has_key" --argjson org_route "$org_route" --arg recovery "$recovery" '
|
|
133
|
+
def nonblank: if type == "string" then test("\\S") else false end;
|
|
134
|
+
(.features.publishing != false) as $publishing |
|
|
135
|
+
(.features.public_relay == true) as $relay |
|
|
136
|
+
(((if .mode == null then "connected" else .mode end) == "connected") and
|
|
137
|
+
(.api_url | nonblank) and $has_key) as $configured |
|
|
138
|
+
(if $publishing == false then "disabled"
|
|
139
|
+
elif $configured then "connected_configured"
|
|
140
|
+
elif $org_route then "setup_incomplete"
|
|
141
|
+
elif $relay then "public_relay"
|
|
142
|
+
else "setup_incomplete" end) as $readiness |
|
|
143
|
+
{
|
|
144
|
+
domain: "hosting",
|
|
145
|
+
publishing: $publishing,
|
|
146
|
+
readiness: $readiness,
|
|
147
|
+
connection_configured: $configured,
|
|
148
|
+
public_relay: $relay,
|
|
149
|
+
connectivity: "not_checked",
|
|
150
|
+
recovery_command: (if $readiness == "setup_incomplete" then $recovery else null end)
|
|
151
|
+
}' "$CONFIG"
|
|
152
|
+
}
|
|
153
|
+
|
|
107
154
|
cmd_hosting() {
|
|
108
155
|
local action="${1:-status}"
|
|
109
|
-
# NEVER use jq's `// "true"` here: `//` treats an explicit `false` as empty and
|
|
110
|
-
# would return the default, silently defeating the off-switch. Test `!= false`
|
|
111
|
-
# so absent/true => enabled, explicit false => disabled. `enabled` is the
|
|
112
|
-
# resolved boolean string ("true"/"false").
|
|
113
|
-
local enabled; enabled="$(jq -r '.features.publishing != false' "$CONFIG" 2>/dev/null || echo "true")"
|
|
114
156
|
case "$action" in
|
|
115
157
|
status)
|
|
158
|
+
local status; status="$(_hosting_status)"
|
|
116
159
|
if [ "$JSON" = 1 ]; then
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
160
|
+
printf '%s\n' "$status"
|
|
161
|
+
else
|
|
162
|
+
case "$(printf '%s' "$status" | jq -r '.readiness')" in
|
|
163
|
+
disabled) echo "Hosting (egregore.xyz): OFF" ;;
|
|
164
|
+
connected_configured)
|
|
165
|
+
echo "Hosting (egregore.xyz): ACCESS CONFIGURED"
|
|
166
|
+
echo " Connection not checked." ;;
|
|
167
|
+
public_relay)
|
|
168
|
+
echo "Hosting (egregore.xyz): PUBLIC RELAY"
|
|
169
|
+
echo " Anyone with the link can read published artifacts for 7 days. Connection not checked." ;;
|
|
170
|
+
setup_incomplete)
|
|
171
|
+
echo "Hosting (egregore.xyz): SETUP INCOMPLETE"
|
|
172
|
+
echo " Publishing is enabled; connection setup is incomplete."
|
|
173
|
+
echo " Run: $(printf '%s' "$status" | jq -r '.recovery_command')" ;;
|
|
174
|
+
esac
|
|
175
|
+
fi ;;
|
|
120
176
|
on)
|
|
121
177
|
_save '.features.publishing = true'
|
|
122
|
-
|
|
178
|
+
JSON=0 cmd_hosting status ;;
|
|
123
179
|
off)
|
|
124
180
|
_save '.features.publishing = false'
|
|
125
181
|
echo "Hosting (egregore.xyz): OFF — artifacts will no longer upload to egregore.xyz" ;;
|
|
@@ -378,10 +434,12 @@ cmd_people() {
|
|
|
378
434
|
cmd_dump() {
|
|
379
435
|
# Always JSON — the launcher calls this once to render the whole settings screen.
|
|
380
436
|
local people; people="$(_people_json)"
|
|
381
|
-
|
|
437
|
+
local hosting_status; hosting_status="$(_hosting_status)"
|
|
438
|
+
jq --argjson people "$people" --argjson hosting_status "$hosting_status" '{
|
|
382
439
|
org: .org_name,
|
|
383
440
|
slug: .slug,
|
|
384
441
|
hosting: (.features.publishing != false),
|
|
442
|
+
hosting_status: $hosting_status,
|
|
385
443
|
public_relay: (.features.public_relay == true),
|
|
386
444
|
workflow: (
|
|
387
445
|
if ((if has("base_branch") then .base_branch else "develop" end) == "main") then "simple"
|
|
@@ -21,6 +21,26 @@ from .policy import scope_allows_path
|
|
|
21
21
|
KINDS = {"bug", "improvement", "question"}
|
|
22
22
|
AREAS = {"runtime", "onboarding", "connectors", "site", "other"}
|
|
23
23
|
SEVERITIES = {"critical", "high", "normal"}
|
|
24
|
+
SUPPORT_STATES = {"filed", "failed", "submitting", "uncertain"}
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def support_receipt(result, identifier):
|
|
28
|
+
"""Accept only a receipt for this request; filing never proves notification."""
|
|
29
|
+
if (not isinstance(result, dict) or result.get('request_id') != identifier
|
|
30
|
+
or not isinstance(result.get('submission_state'), str)
|
|
31
|
+
or result['submission_state'] not in SUPPORT_STATES):
|
|
32
|
+
raise ValueError(f"private support returned an invalid receipt; keep request {identifier} and use report-status")
|
|
33
|
+
state = result['submission_state']
|
|
34
|
+
notices = {
|
|
35
|
+
'filed': 'Filed with Egregore support. Telegram delivery has not been verified.',
|
|
36
|
+
'failed': 'Report preserved; support filing failed. Retry this same request.',
|
|
37
|
+
'submitting': 'Report preserved; support filing is not confirmed. Keep this request ID and check report-status.',
|
|
38
|
+
'uncertain': 'Report preserved; support filing needs reconciliation. Keep this request ID and check report-status.',
|
|
39
|
+
}
|
|
40
|
+
return {'request_id': identifier, 'submission_state': state,
|
|
41
|
+
'retryable': state == 'failed',
|
|
42
|
+
'reconciliation_needed': state in {'submitting', 'uncertain'},
|
|
43
|
+
'notification_state': 'unverified', 'notice': notices[state]}
|
|
24
44
|
|
|
25
45
|
|
|
26
46
|
def _scrub(value, *, public=False):
|
|
@@ -207,18 +227,29 @@ class FrameworkReporting:
|
|
|
207
227
|
return {'request_id':identifier,'status':result['status'],'github_url':result['github_url']}
|
|
208
228
|
credential, _ = self._verified_credential(state['destination'],state['authenticated_org'])
|
|
209
229
|
state['status'] = 'submitting'; GitHubIssues._save(path,state)
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
230
|
+
try:
|
|
231
|
+
code, result = self.api(self.root,state['destination'],'/api/v1/support-reports',
|
|
232
|
+
{'request_id':identifier, **state['payload']},credential=credential)
|
|
233
|
+
except OSError:
|
|
234
|
+
state['status'] = 'reconciliation_needed'; GitHubIssues._save(path,state)
|
|
235
|
+
raise OSError(f"private support response unavailable; keep request {identifier} and use report-status or confirm this same preview") from None
|
|
236
|
+
if not 200 <= code < 300:
|
|
213
237
|
state['status'] = 'reconciliation_needed'; GitHubIssues._save(path,state)
|
|
214
238
|
raise ValueError(f"private support request {identifier} needs recovery (HTTP {code}); use report-status or confirm this same preview")
|
|
215
|
-
|
|
216
|
-
|
|
239
|
+
try:
|
|
240
|
+
receipt = support_receipt(result, identifier)
|
|
241
|
+
except ValueError:
|
|
242
|
+
state['status'] = 'reconciliation_needed'; GitHubIssues._save(path,state)
|
|
243
|
+
raise
|
|
244
|
+
state['status'] = receipt['submission_state']; GitHubIssues._save(path,state)
|
|
245
|
+
if state['status'] != 'filed':
|
|
246
|
+
raise ValueError(f"private support request {identifier} needs recovery ({state['status']}); use report-status or confirm this same preview")
|
|
247
|
+
return receipt
|
|
217
248
|
|
|
218
249
|
def status(self, identifier):
|
|
219
250
|
path, state = self._load('report',identifier,write=False)
|
|
220
251
|
credential, _ = self._verified_credential(state['destination'],state['authenticated_org'])
|
|
221
252
|
code, result = self.api(self.root,state['destination'],f'/api/v1/support-reports/{identifier}',credential=credential)
|
|
222
|
-
if code
|
|
253
|
+
if not 200 <= code < 300:
|
|
223
254
|
raise ValueError(f"private support status unavailable (HTTP {code}); keep request {identifier}")
|
|
224
|
-
return result
|
|
255
|
+
return support_receipt(result, identifier)
|
|
@@ -56,7 +56,7 @@ _control_plane_push() {
|
|
|
56
56
|
local api_url api_key payload actor response revision
|
|
57
57
|
api_url=$(jq -r '.api_url // empty' "$CONFIG" 2>/dev/null)
|
|
58
58
|
[ -n "$api_url" ] || return 0
|
|
59
|
-
api_key=$(grep '^EGREGORE_API_KEY=' "$SCRIPT_DIR/.env" 2>/dev/null | cut -d'=' -f2-)
|
|
59
|
+
api_key=$(grep '^EGREGORE_API_KEY=' "$SCRIPT_DIR/.env" 2>/dev/null | cut -d'=' -f2- || true)
|
|
60
60
|
[ -n "$api_key" ] || return 0
|
|
61
61
|
actor=$(jq -r '.github_username // empty' "$SCRIPT_DIR/.egregore-state.json" 2>/dev/null)
|
|
62
62
|
payload=$(jq -c --arg by "$actor" '{
|
|
@@ -104,22 +104,78 @@ EOF
|
|
|
104
104
|
|
|
105
105
|
# ── verbs ────────────────────────────────────────────────────────────────
|
|
106
106
|
|
|
107
|
+
_hosting_status() {
|
|
108
|
+
# Read local configuration only. A saved key is not proof that the hosted
|
|
109
|
+
# service can be reached or that the credential is still accepted.
|
|
110
|
+
local api_key="" has_key=false slug recovery="egregore connect"
|
|
111
|
+
if [ -r "$SCRIPT_DIR/.env" ]; then
|
|
112
|
+
api_key=$(sed -n 's/^EGREGORE_API_KEY=//p' "$SCRIPT_DIR/.env" | tail -n 1)
|
|
113
|
+
api_key="${api_key#"${api_key%%[![:space:]]*}"}"
|
|
114
|
+
api_key="${api_key%"${api_key##*[![:space:]]}"}"
|
|
115
|
+
case "$api_key" in
|
|
116
|
+
\"*\"|\'*\') api_key="${api_key:1:${#api_key}-2}" ;;
|
|
117
|
+
esac
|
|
118
|
+
[ -n "${api_key//[[:space:]]/}" ] && has_key=true
|
|
119
|
+
fi
|
|
120
|
+
# Match publish-artifact.sh's route selection separately from the saved
|
|
121
|
+
# configuration check. Raw nonempty overrides (even malformed ones) select
|
|
122
|
+
# organisation upload, so they must never imply the public relay's expiry.
|
|
123
|
+
local effective_url="${EGREGORE_API_URL:-}" effective_key="${EGREGORE_API_KEY:-}" org_route=false
|
|
124
|
+
if [ -f "$SCRIPT_DIR/.env" ]; then
|
|
125
|
+
effective_url="${effective_url:-$(grep '^EGREGORE_API_URL=' "$SCRIPT_DIR/.env" 2>/dev/null | cut -d'=' -f2- || true)}"
|
|
126
|
+
effective_key="${effective_key:-$(grep '^EGREGORE_API_KEY=' "$SCRIPT_DIR/.env" 2>/dev/null | cut -d'=' -f2- || true)}"
|
|
127
|
+
fi
|
|
128
|
+
effective_url="${effective_url:-$(jq -r '.api_url // empty' "$CONFIG" 2>/dev/null)}"
|
|
129
|
+
if [ -n "$effective_url" ] && [ -n "$effective_key" ]; then org_route=true; fi
|
|
130
|
+
slug=$(jq -r '.slug // empty' "$CONFIG")
|
|
131
|
+
if _valid_token "$slug"; then recovery="egregore connect $slug"; fi
|
|
132
|
+
jq --argjson has_key "$has_key" --argjson org_route "$org_route" --arg recovery "$recovery" '
|
|
133
|
+
def nonblank: if type == "string" then test("\\S") else false end;
|
|
134
|
+
(.features.publishing != false) as $publishing |
|
|
135
|
+
(.features.public_relay == true) as $relay |
|
|
136
|
+
(((if .mode == null then "connected" else .mode end) == "connected") and
|
|
137
|
+
(.api_url | nonblank) and $has_key) as $configured |
|
|
138
|
+
(if $publishing == false then "disabled"
|
|
139
|
+
elif $configured then "connected_configured"
|
|
140
|
+
elif $org_route then "setup_incomplete"
|
|
141
|
+
elif $relay then "public_relay"
|
|
142
|
+
else "setup_incomplete" end) as $readiness |
|
|
143
|
+
{
|
|
144
|
+
domain: "hosting",
|
|
145
|
+
publishing: $publishing,
|
|
146
|
+
readiness: $readiness,
|
|
147
|
+
connection_configured: $configured,
|
|
148
|
+
public_relay: $relay,
|
|
149
|
+
connectivity: "not_checked",
|
|
150
|
+
recovery_command: (if $readiness == "setup_incomplete" then $recovery else null end)
|
|
151
|
+
}' "$CONFIG"
|
|
152
|
+
}
|
|
153
|
+
|
|
107
154
|
cmd_hosting() {
|
|
108
155
|
local action="${1:-status}"
|
|
109
|
-
# NEVER use jq's `// "true"` here: `//` treats an explicit `false` as empty and
|
|
110
|
-
# would return the default, silently defeating the off-switch. Test `!= false`
|
|
111
|
-
# so absent/true => enabled, explicit false => disabled. `enabled` is the
|
|
112
|
-
# resolved boolean string ("true"/"false").
|
|
113
|
-
local enabled; enabled="$(jq -r '.features.publishing != false' "$CONFIG" 2>/dev/null || echo "true")"
|
|
114
156
|
case "$action" in
|
|
115
157
|
status)
|
|
158
|
+
local status; status="$(_hosting_status)"
|
|
116
159
|
if [ "$JSON" = 1 ]; then
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
160
|
+
printf '%s\n' "$status"
|
|
161
|
+
else
|
|
162
|
+
case "$(printf '%s' "$status" | jq -r '.readiness')" in
|
|
163
|
+
disabled) echo "Hosting (egregore.xyz): OFF" ;;
|
|
164
|
+
connected_configured)
|
|
165
|
+
echo "Hosting (egregore.xyz): ACCESS CONFIGURED"
|
|
166
|
+
echo " Connection not checked." ;;
|
|
167
|
+
public_relay)
|
|
168
|
+
echo "Hosting (egregore.xyz): PUBLIC RELAY"
|
|
169
|
+
echo " Anyone with the link can read published artifacts for 7 days. Connection not checked." ;;
|
|
170
|
+
setup_incomplete)
|
|
171
|
+
echo "Hosting (egregore.xyz): SETUP INCOMPLETE"
|
|
172
|
+
echo " Publishing is enabled; connection setup is incomplete."
|
|
173
|
+
echo " Run: $(printf '%s' "$status" | jq -r '.recovery_command')" ;;
|
|
174
|
+
esac
|
|
175
|
+
fi ;;
|
|
120
176
|
on)
|
|
121
177
|
_save '.features.publishing = true'
|
|
122
|
-
|
|
178
|
+
JSON=0 cmd_hosting status ;;
|
|
123
179
|
off)
|
|
124
180
|
_save '.features.publishing = false'
|
|
125
181
|
echo "Hosting (egregore.xyz): OFF — artifacts will no longer upload to egregore.xyz" ;;
|
|
@@ -378,10 +434,12 @@ cmd_people() {
|
|
|
378
434
|
cmd_dump() {
|
|
379
435
|
# Always JSON — the launcher calls this once to render the whole settings screen.
|
|
380
436
|
local people; people="$(_people_json)"
|
|
381
|
-
|
|
437
|
+
local hosting_status; hosting_status="$(_hosting_status)"
|
|
438
|
+
jq --argjson people "$people" --argjson hosting_status "$hosting_status" '{
|
|
382
439
|
org: .org_name,
|
|
383
440
|
slug: .slug,
|
|
384
441
|
hosting: (.features.publishing != false),
|
|
442
|
+
hosting_status: $hosting_status,
|
|
385
443
|
public_relay: (.features.public_relay == true),
|
|
386
444
|
workflow: (
|
|
387
445
|
if ((if has("base_branch") then .base_branch else "develop" end) == "main") then "simple"
|
|
@@ -21,6 +21,26 @@ from .policy import scope_allows_path
|
|
|
21
21
|
KINDS = {"bug", "improvement", "question"}
|
|
22
22
|
AREAS = {"runtime", "onboarding", "connectors", "site", "other"}
|
|
23
23
|
SEVERITIES = {"critical", "high", "normal"}
|
|
24
|
+
SUPPORT_STATES = {"filed", "failed", "submitting", "uncertain"}
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def support_receipt(result, identifier):
|
|
28
|
+
"""Accept only a receipt for this request; filing never proves notification."""
|
|
29
|
+
if (not isinstance(result, dict) or result.get('request_id') != identifier
|
|
30
|
+
or not isinstance(result.get('submission_state'), str)
|
|
31
|
+
or result['submission_state'] not in SUPPORT_STATES):
|
|
32
|
+
raise ValueError(f"private support returned an invalid receipt; keep request {identifier} and use report-status")
|
|
33
|
+
state = result['submission_state']
|
|
34
|
+
notices = {
|
|
35
|
+
'filed': 'Filed with Egregore support. Telegram delivery has not been verified.',
|
|
36
|
+
'failed': 'Report preserved; support filing failed. Retry this same request.',
|
|
37
|
+
'submitting': 'Report preserved; support filing is not confirmed. Keep this request ID and check report-status.',
|
|
38
|
+
'uncertain': 'Report preserved; support filing needs reconciliation. Keep this request ID and check report-status.',
|
|
39
|
+
}
|
|
40
|
+
return {'request_id': identifier, 'submission_state': state,
|
|
41
|
+
'retryable': state == 'failed',
|
|
42
|
+
'reconciliation_needed': state in {'submitting', 'uncertain'},
|
|
43
|
+
'notification_state': 'unverified', 'notice': notices[state]}
|
|
24
44
|
|
|
25
45
|
|
|
26
46
|
def _scrub(value, *, public=False):
|
|
@@ -207,18 +227,29 @@ class FrameworkReporting:
|
|
|
207
227
|
return {'request_id':identifier,'status':result['status'],'github_url':result['github_url']}
|
|
208
228
|
credential, _ = self._verified_credential(state['destination'],state['authenticated_org'])
|
|
209
229
|
state['status'] = 'submitting'; GitHubIssues._save(path,state)
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
230
|
+
try:
|
|
231
|
+
code, result = self.api(self.root,state['destination'],'/api/v1/support-reports',
|
|
232
|
+
{'request_id':identifier, **state['payload']},credential=credential)
|
|
233
|
+
except OSError:
|
|
234
|
+
state['status'] = 'reconciliation_needed'; GitHubIssues._save(path,state)
|
|
235
|
+
raise OSError(f"private support response unavailable; keep request {identifier} and use report-status or confirm this same preview") from None
|
|
236
|
+
if not 200 <= code < 300:
|
|
213
237
|
state['status'] = 'reconciliation_needed'; GitHubIssues._save(path,state)
|
|
214
238
|
raise ValueError(f"private support request {identifier} needs recovery (HTTP {code}); use report-status or confirm this same preview")
|
|
215
|
-
|
|
216
|
-
|
|
239
|
+
try:
|
|
240
|
+
receipt = support_receipt(result, identifier)
|
|
241
|
+
except ValueError:
|
|
242
|
+
state['status'] = 'reconciliation_needed'; GitHubIssues._save(path,state)
|
|
243
|
+
raise
|
|
244
|
+
state['status'] = receipt['submission_state']; GitHubIssues._save(path,state)
|
|
245
|
+
if state['status'] != 'filed':
|
|
246
|
+
raise ValueError(f"private support request {identifier} needs recovery ({state['status']}); use report-status or confirm this same preview")
|
|
247
|
+
return receipt
|
|
217
248
|
|
|
218
249
|
def status(self, identifier):
|
|
219
250
|
path, state = self._load('report',identifier,write=False)
|
|
220
251
|
credential, _ = self._verified_credential(state['destination'],state['authenticated_org'])
|
|
221
252
|
code, result = self.api(self.root,state['destination'],f'/api/v1/support-reports/{identifier}',credential=credential)
|
|
222
|
-
if code
|
|
253
|
+
if not 200 <= code < 300:
|
|
223
254
|
raise ValueError(f"private support status unavailable (HTTP {code}); keep request {identifier}")
|
|
224
|
-
return result
|
|
255
|
+
return support_receipt(result, identifier)
|
|
@@ -56,7 +56,7 @@ _control_plane_push() {
|
|
|
56
56
|
local api_url api_key payload actor response revision
|
|
57
57
|
api_url=$(jq -r '.api_url // empty' "$CONFIG" 2>/dev/null)
|
|
58
58
|
[ -n "$api_url" ] || return 0
|
|
59
|
-
api_key=$(grep '^EGREGORE_API_KEY=' "$SCRIPT_DIR/.env" 2>/dev/null | cut -d'=' -f2-)
|
|
59
|
+
api_key=$(grep '^EGREGORE_API_KEY=' "$SCRIPT_DIR/.env" 2>/dev/null | cut -d'=' -f2- || true)
|
|
60
60
|
[ -n "$api_key" ] || return 0
|
|
61
61
|
actor=$(jq -r '.github_username // empty' "$SCRIPT_DIR/.egregore-state.json" 2>/dev/null)
|
|
62
62
|
payload=$(jq -c --arg by "$actor" '{
|
|
@@ -104,22 +104,78 @@ EOF
|
|
|
104
104
|
|
|
105
105
|
# ── verbs ────────────────────────────────────────────────────────────────
|
|
106
106
|
|
|
107
|
+
_hosting_status() {
|
|
108
|
+
# Read local configuration only. A saved key is not proof that the hosted
|
|
109
|
+
# service can be reached or that the credential is still accepted.
|
|
110
|
+
local api_key="" has_key=false slug recovery="egregore connect"
|
|
111
|
+
if [ -r "$SCRIPT_DIR/.env" ]; then
|
|
112
|
+
api_key=$(sed -n 's/^EGREGORE_API_KEY=//p' "$SCRIPT_DIR/.env" | tail -n 1)
|
|
113
|
+
api_key="${api_key#"${api_key%%[![:space:]]*}"}"
|
|
114
|
+
api_key="${api_key%"${api_key##*[![:space:]]}"}"
|
|
115
|
+
case "$api_key" in
|
|
116
|
+
\"*\"|\'*\') api_key="${api_key:1:${#api_key}-2}" ;;
|
|
117
|
+
esac
|
|
118
|
+
[ -n "${api_key//[[:space:]]/}" ] && has_key=true
|
|
119
|
+
fi
|
|
120
|
+
# Match publish-artifact.sh's route selection separately from the saved
|
|
121
|
+
# configuration check. Raw nonempty overrides (even malformed ones) select
|
|
122
|
+
# organisation upload, so they must never imply the public relay's expiry.
|
|
123
|
+
local effective_url="${EGREGORE_API_URL:-}" effective_key="${EGREGORE_API_KEY:-}" org_route=false
|
|
124
|
+
if [ -f "$SCRIPT_DIR/.env" ]; then
|
|
125
|
+
effective_url="${effective_url:-$(grep '^EGREGORE_API_URL=' "$SCRIPT_DIR/.env" 2>/dev/null | cut -d'=' -f2- || true)}"
|
|
126
|
+
effective_key="${effective_key:-$(grep '^EGREGORE_API_KEY=' "$SCRIPT_DIR/.env" 2>/dev/null | cut -d'=' -f2- || true)}"
|
|
127
|
+
fi
|
|
128
|
+
effective_url="${effective_url:-$(jq -r '.api_url // empty' "$CONFIG" 2>/dev/null)}"
|
|
129
|
+
if [ -n "$effective_url" ] && [ -n "$effective_key" ]; then org_route=true; fi
|
|
130
|
+
slug=$(jq -r '.slug // empty' "$CONFIG")
|
|
131
|
+
if _valid_token "$slug"; then recovery="egregore connect $slug"; fi
|
|
132
|
+
jq --argjson has_key "$has_key" --argjson org_route "$org_route" --arg recovery "$recovery" '
|
|
133
|
+
def nonblank: if type == "string" then test("\\S") else false end;
|
|
134
|
+
(.features.publishing != false) as $publishing |
|
|
135
|
+
(.features.public_relay == true) as $relay |
|
|
136
|
+
(((if .mode == null then "connected" else .mode end) == "connected") and
|
|
137
|
+
(.api_url | nonblank) and $has_key) as $configured |
|
|
138
|
+
(if $publishing == false then "disabled"
|
|
139
|
+
elif $configured then "connected_configured"
|
|
140
|
+
elif $org_route then "setup_incomplete"
|
|
141
|
+
elif $relay then "public_relay"
|
|
142
|
+
else "setup_incomplete" end) as $readiness |
|
|
143
|
+
{
|
|
144
|
+
domain: "hosting",
|
|
145
|
+
publishing: $publishing,
|
|
146
|
+
readiness: $readiness,
|
|
147
|
+
connection_configured: $configured,
|
|
148
|
+
public_relay: $relay,
|
|
149
|
+
connectivity: "not_checked",
|
|
150
|
+
recovery_command: (if $readiness == "setup_incomplete" then $recovery else null end)
|
|
151
|
+
}' "$CONFIG"
|
|
152
|
+
}
|
|
153
|
+
|
|
107
154
|
cmd_hosting() {
|
|
108
155
|
local action="${1:-status}"
|
|
109
|
-
# NEVER use jq's `// "true"` here: `//` treats an explicit `false` as empty and
|
|
110
|
-
# would return the default, silently defeating the off-switch. Test `!= false`
|
|
111
|
-
# so absent/true => enabled, explicit false => disabled. `enabled` is the
|
|
112
|
-
# resolved boolean string ("true"/"false").
|
|
113
|
-
local enabled; enabled="$(jq -r '.features.publishing != false' "$CONFIG" 2>/dev/null || echo "true")"
|
|
114
156
|
case "$action" in
|
|
115
157
|
status)
|
|
158
|
+
local status; status="$(_hosting_status)"
|
|
116
159
|
if [ "$JSON" = 1 ]; then
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
160
|
+
printf '%s\n' "$status"
|
|
161
|
+
else
|
|
162
|
+
case "$(printf '%s' "$status" | jq -r '.readiness')" in
|
|
163
|
+
disabled) echo "Hosting (egregore.xyz): OFF" ;;
|
|
164
|
+
connected_configured)
|
|
165
|
+
echo "Hosting (egregore.xyz): ACCESS CONFIGURED"
|
|
166
|
+
echo " Connection not checked." ;;
|
|
167
|
+
public_relay)
|
|
168
|
+
echo "Hosting (egregore.xyz): PUBLIC RELAY"
|
|
169
|
+
echo " Anyone with the link can read published artifacts for 7 days. Connection not checked." ;;
|
|
170
|
+
setup_incomplete)
|
|
171
|
+
echo "Hosting (egregore.xyz): SETUP INCOMPLETE"
|
|
172
|
+
echo " Publishing is enabled; connection setup is incomplete."
|
|
173
|
+
echo " Run: $(printf '%s' "$status" | jq -r '.recovery_command')" ;;
|
|
174
|
+
esac
|
|
175
|
+
fi ;;
|
|
120
176
|
on)
|
|
121
177
|
_save '.features.publishing = true'
|
|
122
|
-
|
|
178
|
+
JSON=0 cmd_hosting status ;;
|
|
123
179
|
off)
|
|
124
180
|
_save '.features.publishing = false'
|
|
125
181
|
echo "Hosting (egregore.xyz): OFF — artifacts will no longer upload to egregore.xyz" ;;
|
|
@@ -378,10 +434,12 @@ cmd_people() {
|
|
|
378
434
|
cmd_dump() {
|
|
379
435
|
# Always JSON — the launcher calls this once to render the whole settings screen.
|
|
380
436
|
local people; people="$(_people_json)"
|
|
381
|
-
|
|
437
|
+
local hosting_status; hosting_status="$(_hosting_status)"
|
|
438
|
+
jq --argjson people "$people" --argjson hosting_status "$hosting_status" '{
|
|
382
439
|
org: .org_name,
|
|
383
440
|
slug: .slug,
|
|
384
441
|
hosting: (.features.publishing != false),
|
|
442
|
+
hosting_status: $hosting_status,
|
|
385
443
|
public_relay: (.features.public_relay == true),
|
|
386
444
|
workflow: (
|
|
387
445
|
if ((if has("base_branch") then .base_branch else "develop" end) == "main") then "simple"
|
|
@@ -21,6 +21,26 @@ from .policy import scope_allows_path
|
|
|
21
21
|
KINDS = {"bug", "improvement", "question"}
|
|
22
22
|
AREAS = {"runtime", "onboarding", "connectors", "site", "other"}
|
|
23
23
|
SEVERITIES = {"critical", "high", "normal"}
|
|
24
|
+
SUPPORT_STATES = {"filed", "failed", "submitting", "uncertain"}
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def support_receipt(result, identifier):
|
|
28
|
+
"""Accept only a receipt for this request; filing never proves notification."""
|
|
29
|
+
if (not isinstance(result, dict) or result.get('request_id') != identifier
|
|
30
|
+
or not isinstance(result.get('submission_state'), str)
|
|
31
|
+
or result['submission_state'] not in SUPPORT_STATES):
|
|
32
|
+
raise ValueError(f"private support returned an invalid receipt; keep request {identifier} and use report-status")
|
|
33
|
+
state = result['submission_state']
|
|
34
|
+
notices = {
|
|
35
|
+
'filed': 'Filed with Egregore support. Telegram delivery has not been verified.',
|
|
36
|
+
'failed': 'Report preserved; support filing failed. Retry this same request.',
|
|
37
|
+
'submitting': 'Report preserved; support filing is not confirmed. Keep this request ID and check report-status.',
|
|
38
|
+
'uncertain': 'Report preserved; support filing needs reconciliation. Keep this request ID and check report-status.',
|
|
39
|
+
}
|
|
40
|
+
return {'request_id': identifier, 'submission_state': state,
|
|
41
|
+
'retryable': state == 'failed',
|
|
42
|
+
'reconciliation_needed': state in {'submitting', 'uncertain'},
|
|
43
|
+
'notification_state': 'unverified', 'notice': notices[state]}
|
|
24
44
|
|
|
25
45
|
|
|
26
46
|
def _scrub(value, *, public=False):
|
|
@@ -207,18 +227,29 @@ class FrameworkReporting:
|
|
|
207
227
|
return {'request_id':identifier,'status':result['status'],'github_url':result['github_url']}
|
|
208
228
|
credential, _ = self._verified_credential(state['destination'],state['authenticated_org'])
|
|
209
229
|
state['status'] = 'submitting'; GitHubIssues._save(path,state)
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
230
|
+
try:
|
|
231
|
+
code, result = self.api(self.root,state['destination'],'/api/v1/support-reports',
|
|
232
|
+
{'request_id':identifier, **state['payload']},credential=credential)
|
|
233
|
+
except OSError:
|
|
234
|
+
state['status'] = 'reconciliation_needed'; GitHubIssues._save(path,state)
|
|
235
|
+
raise OSError(f"private support response unavailable; keep request {identifier} and use report-status or confirm this same preview") from None
|
|
236
|
+
if not 200 <= code < 300:
|
|
213
237
|
state['status'] = 'reconciliation_needed'; GitHubIssues._save(path,state)
|
|
214
238
|
raise ValueError(f"private support request {identifier} needs recovery (HTTP {code}); use report-status or confirm this same preview")
|
|
215
|
-
|
|
216
|
-
|
|
239
|
+
try:
|
|
240
|
+
receipt = support_receipt(result, identifier)
|
|
241
|
+
except ValueError:
|
|
242
|
+
state['status'] = 'reconciliation_needed'; GitHubIssues._save(path,state)
|
|
243
|
+
raise
|
|
244
|
+
state['status'] = receipt['submission_state']; GitHubIssues._save(path,state)
|
|
245
|
+
if state['status'] != 'filed':
|
|
246
|
+
raise ValueError(f"private support request {identifier} needs recovery ({state['status']}); use report-status or confirm this same preview")
|
|
247
|
+
return receipt
|
|
217
248
|
|
|
218
249
|
def status(self, identifier):
|
|
219
250
|
path, state = self._load('report',identifier,write=False)
|
|
220
251
|
credential, _ = self._verified_credential(state['destination'],state['authenticated_org'])
|
|
221
252
|
code, result = self.api(self.root,state['destination'],f'/api/v1/support-reports/{identifier}',credential=credential)
|
|
222
|
-
if code
|
|
253
|
+
if not 200 <= code < 300:
|
|
223
254
|
raise ValueError(f"private support status unavailable (HTTP {code}); keep request {identifier}")
|
|
224
|
-
return result
|
|
255
|
+
return support_receipt(result, identifier)
|