@viraatdas/rudder 2.11.3 → 2.12.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cloud.d.ts +1 -0
- package/dist/cloud.js +617 -56
- package/dist/cloud.js.map +1 -1
- package/dist/jj.js +21 -6
- package/dist/jj.js.map +1 -1
- package/dist/native/darwin-arm64/rudder-native +0 -0
- package/dist/native/darwin-x64/rudder-native +0 -0
- package/dist/native/linux-x64/rudder-native +0 -0
- package/dist/types.d.ts +1 -0
- package/package.json +1 -1
package/dist/cloud.js
CHANGED
|
@@ -99,6 +99,13 @@ const BULKY_HOME_BASENAME_PATTERNS = [
|
|
|
99
99
|
/\.jsonl$/,
|
|
100
100
|
];
|
|
101
101
|
export async function runCloudCommand(command, args, options = {}) {
|
|
102
|
+
// Hidden diagnostic flag: measure keystroke round-trip latency instead of
|
|
103
|
+
// starting an interactive attach. Parsed here because main.ts forwards
|
|
104
|
+
// unknown flags through as positional args.
|
|
105
|
+
if (args.includes("--latency-probe")) {
|
|
106
|
+
args = args.filter((arg) => arg !== "--latency-probe");
|
|
107
|
+
options = { ...options, latencyProbe: true };
|
|
108
|
+
}
|
|
102
109
|
const subcommand = args[0] ?? "";
|
|
103
110
|
const rest = args.slice(1);
|
|
104
111
|
if (command === "cloud" && subcommand === "help") {
|
|
@@ -203,6 +210,12 @@ export async function runCloudCommand(command, args, options = {}) {
|
|
|
203
210
|
case "runtime":
|
|
204
211
|
await runtime(rest, options);
|
|
205
212
|
return;
|
|
213
|
+
case "region":
|
|
214
|
+
await configureRegion(rest, options);
|
|
215
|
+
return;
|
|
216
|
+
case "secrets":
|
|
217
|
+
await secretsCommand(rest, options);
|
|
218
|
+
return;
|
|
206
219
|
default:
|
|
207
220
|
// A bare `rudder cloud "<text>"` / `rudder sail "<text>"` is the documented
|
|
208
221
|
// way to launch a worker ON that task (the instance name is derived from it).
|
|
@@ -1113,27 +1126,30 @@ async function createSnapshot(repoRoot, requestedHomePaths, options = {}) {
|
|
|
1113
1126
|
? await copyProjectEnvFiles(repoRoot, repoStage)
|
|
1114
1127
|
: 0;
|
|
1115
1128
|
const rudderState = options.includeRudderState ? await copyRudderState(repoRoot, repoStage) : undefined;
|
|
1116
|
-
const
|
|
1129
|
+
const includeCredentials = options.includeCredentials !== false;
|
|
1117
1130
|
const includedHomePaths = [];
|
|
1118
|
-
for (const homePath of homePaths) {
|
|
1119
|
-
const copied = await copyHomePath(homePath, homeStage);
|
|
1120
|
-
if (copied) {
|
|
1121
|
-
includedHomePaths.push(shortenHome(homePath));
|
|
1122
|
-
}
|
|
1123
|
-
}
|
|
1124
|
-
// On macOS, Claude Code stores its OAuth token in the Keychain rather than
|
|
1125
|
-
// ~/.claude/.credentials.json, so the home-paths copy above doesn't pick it
|
|
1126
|
-
// up. Extract it from the Keychain and stage it as a credentials file so
|
|
1127
|
-
// the cloud worker boots already logged in.
|
|
1128
|
-
if (await stageClaudeKeychainCredentials(homeStage)) {
|
|
1129
|
-
includedHomePaths.push("~/.claude/.credentials.json (keychain)");
|
|
1130
|
-
}
|
|
1131
|
-
const capturedEnv = captureCloudEnv(Boolean(options.migration));
|
|
1132
1131
|
let capturedEnvCount = 0;
|
|
1133
|
-
if (
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1132
|
+
if (includeCredentials) {
|
|
1133
|
+
const homePaths = normalizeHomePaths(requestedHomePaths);
|
|
1134
|
+
for (const homePath of homePaths) {
|
|
1135
|
+
const copied = await copyHomePath(homePath, homeStage);
|
|
1136
|
+
if (copied) {
|
|
1137
|
+
includedHomePaths.push(shortenHome(homePath));
|
|
1138
|
+
}
|
|
1139
|
+
}
|
|
1140
|
+
// On macOS, Claude Code stores its OAuth token in the Keychain rather than
|
|
1141
|
+
// ~/.claude/.credentials.json, so the home-paths copy above doesn't pick it
|
|
1142
|
+
// up. Extract it from the Keychain and stage it as a credentials file so
|
|
1143
|
+
// the cloud worker boots already logged in.
|
|
1144
|
+
if (await stageClaudeKeychainCredentials(homeStage)) {
|
|
1145
|
+
includedHomePaths.push("~/.claude/.credentials.json (keychain)");
|
|
1146
|
+
}
|
|
1147
|
+
const capturedEnv = captureCloudEnv(Boolean(options.migration));
|
|
1148
|
+
if (Object.keys(capturedEnv).length > 0) {
|
|
1149
|
+
await ensureDir(path.join(stageDir, "env"));
|
|
1150
|
+
await writeJson(path.join(stageDir, "env", "cloud-env.json"), capturedEnv);
|
|
1151
|
+
capturedEnvCount = Object.keys(capturedEnv).length;
|
|
1152
|
+
}
|
|
1137
1153
|
}
|
|
1138
1154
|
let migratedAgentsCount = 0;
|
|
1139
1155
|
if (options.migration && options.migration.plan.migrated.length > 0) {
|
|
@@ -1466,25 +1482,34 @@ function normalizeHomePaths(requested) {
|
|
|
1466
1482
|
}
|
|
1467
1483
|
return paths;
|
|
1468
1484
|
}
|
|
1469
|
-
|
|
1485
|
+
// On macOS, Claude Code keeps its OAuth token in the Keychain instead of
|
|
1486
|
+
// ~/.claude/.credentials.json. Read it so cloud workspaces boot logged in.
|
|
1487
|
+
async function readClaudeKeychainCredentials() {
|
|
1470
1488
|
if (process.platform !== "darwin") {
|
|
1471
|
-
return
|
|
1489
|
+
return null;
|
|
1472
1490
|
}
|
|
1473
1491
|
if (!commandExists("security")) {
|
|
1474
|
-
return
|
|
1492
|
+
return null;
|
|
1475
1493
|
}
|
|
1476
1494
|
const result = await runCommand("security", ["find-generic-password", "-s", "Claude Code-credentials", "-w"], { allowFailure: true });
|
|
1477
1495
|
if (result.code !== 0) {
|
|
1478
|
-
return
|
|
1496
|
+
return null;
|
|
1479
1497
|
}
|
|
1480
1498
|
const payload = result.stdout.trim();
|
|
1481
1499
|
if (!payload || !payload.startsWith("{")) {
|
|
1482
|
-
return
|
|
1500
|
+
return null;
|
|
1483
1501
|
}
|
|
1484
1502
|
try {
|
|
1485
1503
|
JSON.parse(payload);
|
|
1486
1504
|
}
|
|
1487
1505
|
catch {
|
|
1506
|
+
return null;
|
|
1507
|
+
}
|
|
1508
|
+
return payload;
|
|
1509
|
+
}
|
|
1510
|
+
async function stageClaudeKeychainCredentials(homeStage) {
|
|
1511
|
+
const payload = await readClaudeKeychainCredentials();
|
|
1512
|
+
if (!payload) {
|
|
1488
1513
|
return false;
|
|
1489
1514
|
}
|
|
1490
1515
|
const targetDir = path.join(homeStage, ".claude");
|
|
@@ -1688,6 +1713,10 @@ async function workspaceCommand(args, options) {
|
|
|
1688
1713
|
await workspaceAttach(rest, options);
|
|
1689
1714
|
return;
|
|
1690
1715
|
}
|
|
1716
|
+
if (sub === "create") {
|
|
1717
|
+
await workspaceCreate(rest, options);
|
|
1718
|
+
return;
|
|
1719
|
+
}
|
|
1691
1720
|
if (sub === "share") {
|
|
1692
1721
|
await workspaceShare(options);
|
|
1693
1722
|
return;
|
|
@@ -1704,39 +1733,394 @@ async function workspaceCommand(args, options) {
|
|
|
1704
1733
|
await workspaceList(options);
|
|
1705
1734
|
return;
|
|
1706
1735
|
}
|
|
1707
|
-
throw new Error("Usage: rudder cloud workspace [attach [id]|share|status|pause|resume|stop|list]");
|
|
1736
|
+
throw new Error("Usage: rudder cloud workspace [attach [id|owner/repo]|create <owner/repo>|share|status|pause|resume|stop|list]");
|
|
1737
|
+
}
|
|
1738
|
+
const GITHUB_SLUG_RE = /^[\w.-]+\/[\w.-]+$/;
|
|
1739
|
+
async function githubSlugFromOrigin(repoRoot) {
|
|
1740
|
+
const result = await runCommand("git", ["remote", "get-url", "origin"], {
|
|
1741
|
+
cwd: repoRoot,
|
|
1742
|
+
allowFailure: true,
|
|
1743
|
+
});
|
|
1744
|
+
if (result.code !== 0) {
|
|
1745
|
+
return null;
|
|
1746
|
+
}
|
|
1747
|
+
const match = result.stdout.trim().match(/github\.com[:/]([\w.-]+\/[\w.-]+?)(?:\.git)?$/);
|
|
1748
|
+
return match ? match[1] : null;
|
|
1749
|
+
}
|
|
1750
|
+
// Cloud-native workspace: the worker clones the repo from GitHub directly —
|
|
1751
|
+
// full history, real origin remote, no local-directory upload.
|
|
1752
|
+
async function workspaceCreate(args, options) {
|
|
1753
|
+
const slug = (args[0] ?? "").trim().replace(/\.git$/, "");
|
|
1754
|
+
if (!GITHUB_SLUG_RE.test(slug)) {
|
|
1755
|
+
throw new Error("Usage: rudder cloud workspace create <owner/repo> [--branch <name>] [--region <code>]");
|
|
1756
|
+
}
|
|
1757
|
+
let branch;
|
|
1758
|
+
let region;
|
|
1759
|
+
for (let i = 1; i < args.length; i += 1) {
|
|
1760
|
+
const arg = args[i] ?? "";
|
|
1761
|
+
if (arg === "--branch")
|
|
1762
|
+
branch = args[++i];
|
|
1763
|
+
else if (arg.startsWith("--branch="))
|
|
1764
|
+
branch = arg.slice("--branch=".length);
|
|
1765
|
+
else if (arg === "--region")
|
|
1766
|
+
region = args[++i];
|
|
1767
|
+
else if (arg.startsWith("--region="))
|
|
1768
|
+
region = arg.slice("--region=".length);
|
|
1769
|
+
else
|
|
1770
|
+
throw new Error(`Unknown option: ${arg}`);
|
|
1771
|
+
}
|
|
1772
|
+
const client = await cloudClient({ requireToken: true });
|
|
1773
|
+
// Preflight: private clones and pushes need GitHub credentials from the vault.
|
|
1774
|
+
try {
|
|
1775
|
+
const secretsResult = await client.request("/api/rudder/secrets", { method: "GET" });
|
|
1776
|
+
const secrets = secretsResult?.secrets ?? [];
|
|
1777
|
+
const hasGitCreds = secrets.some((secret) => (secret.kind === "file" && secret.name.startsWith("~/.config/gh/"))
|
|
1778
|
+
|| (secret.kind === "env" && (secret.name === "GITHUB_TOKEN" || secret.name === "GH_TOKEN")));
|
|
1779
|
+
if (!hasGitCreds && !options.json) {
|
|
1780
|
+
process.stderr.write("Warning: no GitHub credentials in the cloud vault; private repos will fail to clone. Run `rudder cloud secrets sync` first.\n");
|
|
1781
|
+
}
|
|
1782
|
+
}
|
|
1783
|
+
catch {
|
|
1784
|
+
// Old server or unconfigured vault; the worker will report clone failures.
|
|
1785
|
+
}
|
|
1786
|
+
if (!options.json) {
|
|
1787
|
+
process.stderr.write(`Creating cloud workspace for ${slug}...\n`);
|
|
1788
|
+
}
|
|
1789
|
+
const effectiveRegion = region ?? await explicitCloudRegion();
|
|
1790
|
+
const result = await client.request("/api/rudder/workspace/create", {
|
|
1791
|
+
method: "POST",
|
|
1792
|
+
body: {
|
|
1793
|
+
repo: slug,
|
|
1794
|
+
...(branch ? { branch } : {}),
|
|
1795
|
+
...(effectiveRegion ? { region: effectiveRegion } : {}),
|
|
1796
|
+
},
|
|
1797
|
+
});
|
|
1798
|
+
await attachToWorkspaceResult(result, options);
|
|
1799
|
+
}
|
|
1800
|
+
async function workspaceAttachByRepo(slug, options) {
|
|
1801
|
+
const normalized = slug.trim().replace(/\.git$/, "");
|
|
1802
|
+
const client = await cloudClient({ requireToken: true });
|
|
1803
|
+
try {
|
|
1804
|
+
await client.request(`/api/rudder/workspace/lookup?repo=${encodeURIComponent(normalized)}`, { method: "GET" });
|
|
1805
|
+
}
|
|
1806
|
+
catch {
|
|
1807
|
+
throw new Error(`No cloud workspace for ${normalized}. Create one with \`rudder cloud workspace create ${normalized}\`.`);
|
|
1808
|
+
}
|
|
1809
|
+
// The create endpoint reuses/warm-restarts an existing clone workspace.
|
|
1810
|
+
const result = await client.request("/api/rudder/workspace/create", {
|
|
1811
|
+
method: "POST",
|
|
1812
|
+
body: { repo: normalized },
|
|
1813
|
+
});
|
|
1814
|
+
await attachToWorkspaceResult(result, options);
|
|
1815
|
+
}
|
|
1816
|
+
const MAX_SECRET_VALUE_BYTES = 1024 * 1024;
|
|
1817
|
+
async function secretsCommand(args, options) {
|
|
1818
|
+
const sub = args[0] ?? "";
|
|
1819
|
+
const rest = args.slice(1);
|
|
1820
|
+
switch (sub) {
|
|
1821
|
+
case "set":
|
|
1822
|
+
await secretsSet(rest, options);
|
|
1823
|
+
return;
|
|
1824
|
+
case "list":
|
|
1825
|
+
case "ls":
|
|
1826
|
+
await secretsList(options);
|
|
1827
|
+
return;
|
|
1828
|
+
case "rm":
|
|
1829
|
+
case "remove":
|
|
1830
|
+
case "delete":
|
|
1831
|
+
await secretsRm(rest, options);
|
|
1832
|
+
return;
|
|
1833
|
+
case "sync":
|
|
1834
|
+
await secretsSync(options);
|
|
1835
|
+
return;
|
|
1836
|
+
default:
|
|
1837
|
+
throw new Error("Usage: rudder cloud secrets [set <NAME> [value] | set --file <~/path> [source] | list | rm <NAME> | sync]");
|
|
1838
|
+
}
|
|
1839
|
+
}
|
|
1840
|
+
// Convert an absolute or ~-prefixed path into the canonical tilde form the
|
|
1841
|
+
// vault stores file secrets under. Only paths inside $HOME are allowed.
|
|
1842
|
+
function toTildePath(input) {
|
|
1843
|
+
const trimmed = input.trim();
|
|
1844
|
+
const resolved = path.resolve(expandHome(trimmed));
|
|
1845
|
+
const home = os.homedir();
|
|
1846
|
+
if (!isInside(home, resolved) || resolved === home) {
|
|
1847
|
+
throw new Error(`File secrets must live inside your home directory: ${input}`);
|
|
1848
|
+
}
|
|
1849
|
+
return `~/${path.relative(home, resolved).split(path.sep).join("/")}`;
|
|
1850
|
+
}
|
|
1851
|
+
async function readStdinAll() {
|
|
1852
|
+
const chunks = [];
|
|
1853
|
+
for await (const chunk of process.stdin) {
|
|
1854
|
+
chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
|
|
1855
|
+
}
|
|
1856
|
+
return Buffer.concat(chunks).toString("utf8");
|
|
1857
|
+
}
|
|
1858
|
+
async function secretsSet(args, options) {
|
|
1859
|
+
const client = await cloudClient({ requireToken: true });
|
|
1860
|
+
if (args[0] === "--file") {
|
|
1861
|
+
const target = args[1];
|
|
1862
|
+
if (!target) {
|
|
1863
|
+
throw new Error("Usage: rudder cloud secrets set --file <~/path> [localSourcePath]");
|
|
1864
|
+
}
|
|
1865
|
+
const tildePath = toTildePath(target);
|
|
1866
|
+
const sourcePath = path.resolve(expandHome(args[2] ?? target));
|
|
1867
|
+
const content = await fsp.readFile(sourcePath);
|
|
1868
|
+
if (content.length > MAX_SECRET_VALUE_BYTES) {
|
|
1869
|
+
throw new Error(`${sourcePath} is ${content.length} bytes; file secrets are capped at ${MAX_SECRET_VALUE_BYTES}`);
|
|
1870
|
+
}
|
|
1871
|
+
await client.request("/api/rudder/secrets/item", {
|
|
1872
|
+
method: "PUT",
|
|
1873
|
+
body: {
|
|
1874
|
+
name: tildePath,
|
|
1875
|
+
kind: "file",
|
|
1876
|
+
filePath: tildePath,
|
|
1877
|
+
valueBase64: content.toString("base64"),
|
|
1878
|
+
source: "manual",
|
|
1879
|
+
},
|
|
1880
|
+
});
|
|
1881
|
+
if (options.json) {
|
|
1882
|
+
printJson({ ok: true, name: tildePath, kind: "file" });
|
|
1883
|
+
}
|
|
1884
|
+
else {
|
|
1885
|
+
console.log(`Stored file secret ${tildePath} (${content.length} bytes). Takes effect on next workspace boot.`);
|
|
1886
|
+
}
|
|
1887
|
+
return;
|
|
1888
|
+
}
|
|
1889
|
+
const name = args[0];
|
|
1890
|
+
if (!name) {
|
|
1891
|
+
throw new Error("Usage: rudder cloud secrets set <NAME> [value] (or pipe the value on stdin)");
|
|
1892
|
+
}
|
|
1893
|
+
let value = args[1];
|
|
1894
|
+
if (value === undefined) {
|
|
1895
|
+
value = process.stdin.isTTY
|
|
1896
|
+
? await promptSecret(`Value for ${name}`)
|
|
1897
|
+
: (await readStdinAll()).replace(/\r?\n$/, "");
|
|
1898
|
+
}
|
|
1899
|
+
if (!value) {
|
|
1900
|
+
throw new Error(`No value provided for ${name}.`);
|
|
1901
|
+
}
|
|
1902
|
+
if (Buffer.byteLength(value, "utf8") > MAX_SECRET_VALUE_BYTES) {
|
|
1903
|
+
throw new Error(`Value for ${name} exceeds the ${MAX_SECRET_VALUE_BYTES}-byte cap`);
|
|
1904
|
+
}
|
|
1905
|
+
await client.request("/api/rudder/secrets/item", {
|
|
1906
|
+
method: "PUT",
|
|
1907
|
+
body: {
|
|
1908
|
+
name,
|
|
1909
|
+
kind: "env",
|
|
1910
|
+
valueBase64: Buffer.from(value, "utf8").toString("base64"),
|
|
1911
|
+
source: "manual",
|
|
1912
|
+
},
|
|
1913
|
+
});
|
|
1914
|
+
if (options.json) {
|
|
1915
|
+
printJson({ ok: true, name, kind: "env" });
|
|
1916
|
+
}
|
|
1917
|
+
else {
|
|
1918
|
+
console.log(`Stored env secret ${name}. Takes effect on next workspace boot.`);
|
|
1919
|
+
}
|
|
1920
|
+
}
|
|
1921
|
+
async function secretsList(options) {
|
|
1922
|
+
const client = await cloudClient({ requireToken: true });
|
|
1923
|
+
const result = await client.request("/api/rudder/secrets", { method: "GET" });
|
|
1924
|
+
const secrets = result?.secrets ?? [];
|
|
1925
|
+
if (options.json) {
|
|
1926
|
+
printJson(result);
|
|
1927
|
+
return;
|
|
1928
|
+
}
|
|
1929
|
+
if (secrets.length === 0) {
|
|
1930
|
+
console.log("No cloud secrets stored. Run `rudder cloud secrets sync` to import your local credentials.");
|
|
1931
|
+
return;
|
|
1932
|
+
}
|
|
1933
|
+
const nameWidth = Math.max(4, ...secrets.map((secret) => secret.name.length));
|
|
1934
|
+
console.log(`${"NAME".padEnd(nameWidth)} KIND SIZE UPDATED`);
|
|
1935
|
+
for (const secret of secrets) {
|
|
1936
|
+
const size = `${secret.sizeBytes}B`.padEnd(8);
|
|
1937
|
+
console.log(`${secret.name.padEnd(nameWidth)} ${secret.kind.padEnd(4)} ${size} ${secret.updatedAt}`);
|
|
1938
|
+
}
|
|
1939
|
+
console.log(`\n${secrets.length} secret(s). Values are never shown; rotate with \`rudder cloud secrets set\`.`);
|
|
1940
|
+
}
|
|
1941
|
+
async function secretsRm(args, options) {
|
|
1942
|
+
const name = args[0];
|
|
1943
|
+
if (!name) {
|
|
1944
|
+
throw new Error("Usage: rudder cloud secrets rm <NAME|~/path>");
|
|
1945
|
+
}
|
|
1946
|
+
const client = await cloudClient({ requireToken: true });
|
|
1947
|
+
const normalized = name.startsWith("~") || name.startsWith("/") ? toTildePath(name) : name;
|
|
1948
|
+
await client.request(`/api/rudder/secrets/item?name=${encodeURIComponent(normalized)}`, {
|
|
1949
|
+
method: "DELETE",
|
|
1950
|
+
});
|
|
1951
|
+
if (options.json) {
|
|
1952
|
+
printJson({ ok: true, name: normalized });
|
|
1953
|
+
}
|
|
1954
|
+
else {
|
|
1955
|
+
console.log(`Removed cloud secret ${normalized}.`);
|
|
1956
|
+
}
|
|
1957
|
+
}
|
|
1958
|
+
async function collectHomeSecretFiles() {
|
|
1959
|
+
const home = os.homedir();
|
|
1960
|
+
const out = [];
|
|
1961
|
+
const walk = async (target) => {
|
|
1962
|
+
const stat = await fsp.lstat(target).catch(() => null);
|
|
1963
|
+
if (!stat) {
|
|
1964
|
+
return;
|
|
1965
|
+
}
|
|
1966
|
+
if (stat.isSymbolicLink()) {
|
|
1967
|
+
return;
|
|
1968
|
+
}
|
|
1969
|
+
if (stat.isDirectory()) {
|
|
1970
|
+
if (!(await shouldIncludeSnapshotPath(target))) {
|
|
1971
|
+
return;
|
|
1972
|
+
}
|
|
1973
|
+
const entries = await fsp.readdir(target).catch(() => []);
|
|
1974
|
+
for (const entry of entries) {
|
|
1975
|
+
await walk(path.join(target, entry));
|
|
1976
|
+
}
|
|
1977
|
+
return;
|
|
1978
|
+
}
|
|
1979
|
+
if (!stat.isFile() || !(await shouldIncludeSnapshotPath(target))) {
|
|
1980
|
+
return;
|
|
1981
|
+
}
|
|
1982
|
+
out.push({
|
|
1983
|
+
tildePath: `~/${path.relative(home, target).split(path.sep).join("/")}`,
|
|
1984
|
+
absolute: target,
|
|
1985
|
+
size: stat.size,
|
|
1986
|
+
});
|
|
1987
|
+
};
|
|
1988
|
+
for (const root of normalizeHomePaths([])) {
|
|
1989
|
+
await walk(root);
|
|
1990
|
+
}
|
|
1991
|
+
return out;
|
|
1992
|
+
}
|
|
1993
|
+
// One-time (re-runnable) import of the credentials that used to ride inside
|
|
1994
|
+
// every workspace snapshot: the DEFAULT_HOME_PATHS allowlist, the macOS
|
|
1995
|
+
// Keychain Claude token, and the captured env vars.
|
|
1996
|
+
async function secretsSync(options) {
|
|
1997
|
+
const client = await cloudClient({ requireToken: true });
|
|
1998
|
+
const items = [];
|
|
1999
|
+
const skipped = [];
|
|
2000
|
+
for (const file of await collectHomeSecretFiles()) {
|
|
2001
|
+
if (file.size === 0) {
|
|
2002
|
+
continue;
|
|
2003
|
+
}
|
|
2004
|
+
if (file.size > MAX_SECRET_VALUE_BYTES) {
|
|
2005
|
+
skipped.push({ name: file.tildePath, reason: `${file.size} bytes exceeds the per-secret cap` });
|
|
2006
|
+
continue;
|
|
2007
|
+
}
|
|
2008
|
+
const content = await fsp.readFile(file.absolute).catch(() => null);
|
|
2009
|
+
if (!content) {
|
|
2010
|
+
skipped.push({ name: file.tildePath, reason: "unreadable" });
|
|
2011
|
+
continue;
|
|
2012
|
+
}
|
|
2013
|
+
items.push({
|
|
2014
|
+
name: file.tildePath,
|
|
2015
|
+
kind: "file",
|
|
2016
|
+
filePath: file.tildePath,
|
|
2017
|
+
valueBase64: content.toString("base64"),
|
|
2018
|
+
source: "sync",
|
|
2019
|
+
});
|
|
2020
|
+
}
|
|
2021
|
+
// Keychain read can pop a macOS auth dialog, so only attempt it when a
|
|
2022
|
+
// human is at the terminal to answer it.
|
|
2023
|
+
if (isTty()) {
|
|
2024
|
+
const keychainPayload = await readClaudeKeychainCredentials();
|
|
2025
|
+
if (keychainPayload) {
|
|
2026
|
+
items.push({
|
|
2027
|
+
name: "~/.claude/.credentials.json",
|
|
2028
|
+
kind: "file",
|
|
2029
|
+
filePath: "~/.claude/.credentials.json",
|
|
2030
|
+
valueBase64: Buffer.from(keychainPayload + "\n", "utf8").toString("base64"),
|
|
2031
|
+
source: "sync",
|
|
2032
|
+
});
|
|
2033
|
+
}
|
|
2034
|
+
}
|
|
2035
|
+
for (const [name, value] of Object.entries(captureCloudEnv())) {
|
|
2036
|
+
items.push({
|
|
2037
|
+
name,
|
|
2038
|
+
kind: "env",
|
|
2039
|
+
valueBase64: Buffer.from(value, "utf8").toString("base64"),
|
|
2040
|
+
source: "sync",
|
|
2041
|
+
});
|
|
2042
|
+
}
|
|
2043
|
+
if (items.length === 0) {
|
|
2044
|
+
throw new Error("Found nothing to sync: no allowlisted credential files or matching env vars.");
|
|
2045
|
+
}
|
|
2046
|
+
const response = await client.request("/api/rudder/secrets/bulk", { method: "POST", body: { items } });
|
|
2047
|
+
const results = response?.results ?? [];
|
|
2048
|
+
const stored = results.filter((entry) => entry.ok);
|
|
2049
|
+
const failed = results.filter((entry) => !entry.ok);
|
|
2050
|
+
if (options.json) {
|
|
2051
|
+
printJson({ stored: stored.length, failed, skipped });
|
|
2052
|
+
return;
|
|
2053
|
+
}
|
|
2054
|
+
console.log(`Synced ${stored.length} secret(s) to the cloud vault:`);
|
|
2055
|
+
for (const entry of stored) {
|
|
2056
|
+
console.log(` ${entry.name}`);
|
|
2057
|
+
}
|
|
2058
|
+
for (const entry of failed) {
|
|
2059
|
+
console.log(` FAILED ${entry.name}: ${entry.error ?? "unknown error"}`);
|
|
2060
|
+
}
|
|
2061
|
+
for (const entry of skipped) {
|
|
2062
|
+
console.log(` SKIPPED ${entry.name}: ${entry.reason}`);
|
|
2063
|
+
}
|
|
2064
|
+
console.log("\nNew cloud workspaces will now boot with these secrets; snapshots stop carrying local credentials.");
|
|
1708
2065
|
}
|
|
1709
2066
|
function computeWorkspaceKey(repoRoot) {
|
|
1710
2067
|
const normalized = path.resolve(repoRoot);
|
|
1711
2068
|
return createHash("sha256").update(normalized).digest("hex").slice(0, 32);
|
|
1712
2069
|
}
|
|
1713
|
-
|
|
1714
|
-
|
|
1715
|
-
|
|
1716
|
-
|
|
2070
|
+
// Worker placement policy: with the single-region relay in the middle of every
|
|
2071
|
+
// attach, echo latency = RTT(client↔relay) + RTT(relay↔worker), so the worker
|
|
2072
|
+
// belongs NEXT TO THE RELAY, not next to the user. We therefore only send a
|
|
2073
|
+
// region when the user explicitly asked for one (env var or `rudder cloud
|
|
2074
|
+
// region <code>`); otherwise the server places the worker in its own region.
|
|
2075
|
+
async function explicitCloudRegion() {
|
|
2076
|
+
const envRegion = process.env.RUDDER_CLOUD_REGION?.trim().toLowerCase();
|
|
2077
|
+
if (envRegion) {
|
|
2078
|
+
return envRegion;
|
|
2079
|
+
}
|
|
2080
|
+
const state = await loadCloudAuth().catch(() => null);
|
|
2081
|
+
return state?.defaultRegion?.trim().toLowerCase() || undefined;
|
|
2082
|
+
}
|
|
2083
|
+
async function configureRegion(args, options) {
|
|
2084
|
+
const value = (args[0] ?? "").trim().toLowerCase();
|
|
2085
|
+
const state = await loadCloudAuth();
|
|
2086
|
+
if (!state) {
|
|
2087
|
+
throw new Error("Not logged in to Rudder Cloud. Run `rudder login` first.");
|
|
1717
2088
|
}
|
|
1718
|
-
if (
|
|
1719
|
-
|
|
2089
|
+
if (!value) {
|
|
2090
|
+
const current = state.defaultRegion ?? "";
|
|
2091
|
+
if (options.json) {
|
|
2092
|
+
printJson({ region: current || null });
|
|
2093
|
+
}
|
|
2094
|
+
else if (current) {
|
|
2095
|
+
console.log(`Cloud worker region override: ${current} (run \`rudder cloud region clear\` to let the server choose).`);
|
|
2096
|
+
}
|
|
2097
|
+
else {
|
|
2098
|
+
console.log("No region override set: workers are placed next to the relay for lowest typing latency.");
|
|
2099
|
+
}
|
|
2100
|
+
return;
|
|
1720
2101
|
}
|
|
1721
|
-
|
|
1722
|
-
|
|
1723
|
-
|
|
1724
|
-
|
|
1725
|
-
// Fly request-id format: <ulid>-<region>
|
|
1726
|
-
const dash = requestId.lastIndexOf("-");
|
|
1727
|
-
const region = dash > 0 ? requestId.slice(dash + 1).trim().toLowerCase() : "";
|
|
1728
|
-
if (region && region.length <= 6 && /^[a-z]+$/.test(region)) {
|
|
1729
|
-
cachedFlyRegion = region;
|
|
1730
|
-
return region;
|
|
1731
|
-
}
|
|
2102
|
+
if (value === "clear" || value === "none" || value === "auto") {
|
|
2103
|
+
await saveCloudAuth({ ...state, defaultRegion: undefined, updatedAt: nowIso() });
|
|
2104
|
+
if (options.json) {
|
|
2105
|
+
printJson({ ok: true, region: null });
|
|
1732
2106
|
}
|
|
2107
|
+
else {
|
|
2108
|
+
console.log("Cleared region override; the server will place workers next to the relay.");
|
|
2109
|
+
}
|
|
2110
|
+
return;
|
|
1733
2111
|
}
|
|
1734
|
-
|
|
1735
|
-
|
|
2112
|
+
if (!/^[a-z]{3,6}$/.test(value)) {
|
|
2113
|
+
throw new Error(`Invalid Fly region code: ${value}`);
|
|
2114
|
+
}
|
|
2115
|
+
await saveCloudAuth({ ...state, defaultRegion: value, updatedAt: nowIso() });
|
|
2116
|
+
if (options.json) {
|
|
2117
|
+
printJson({ ok: true, region: value });
|
|
2118
|
+
}
|
|
2119
|
+
else {
|
|
2120
|
+
console.log(`Cloud worker region override set to ${value}. Note: placing workers away from the relay increases typing latency.`);
|
|
1736
2121
|
}
|
|
1737
|
-
return undefined;
|
|
1738
2122
|
}
|
|
1739
|
-
async function computeSnapshotFingerprint(repoRoot, _requestedHomePaths) {
|
|
2123
|
+
async function computeSnapshotFingerprint(repoRoot, _requestedHomePaths, vaultActive = false) {
|
|
1740
2124
|
const hash = createHash("sha256");
|
|
1741
2125
|
// Repo state: HEAD commit + the porcelain dirty file list. Two attaches
|
|
1742
2126
|
// from the same repo at the same commit with no edits should produce the
|
|
@@ -1750,6 +2134,13 @@ async function computeSnapshotFingerprint(repoRoot, _requestedHomePaths) {
|
|
|
1750
2134
|
if (status.code === 0) {
|
|
1751
2135
|
hash.update(`repo:status:${status.stdout}\n`);
|
|
1752
2136
|
}
|
|
2137
|
+
// With the vault active the snapshot carries no credentials, so credential
|
|
2138
|
+
// changes must NOT change the fingerprint: a mismatch triggers the
|
|
2139
|
+
// destructive destroy+recreate path server-side, and rotation already takes
|
|
2140
|
+
// effect on the next boot via the supervisor's vault fetch.
|
|
2141
|
+
if (vaultActive) {
|
|
2142
|
+
return hash.digest("hex").slice(0, 32);
|
|
2143
|
+
}
|
|
1753
2144
|
// macOS Keychain claude credentials: hash content so re-logging in
|
|
1754
2145
|
// invalidates the cache but a steady-state user keeps it.
|
|
1755
2146
|
if (process.platform === "darwin") {
|
|
@@ -1770,9 +2161,28 @@ async function computeSnapshotFingerprint(repoRoot, _requestedHomePaths) {
|
|
|
1770
2161
|
}
|
|
1771
2162
|
return hash.digest("hex").slice(0, 32);
|
|
1772
2163
|
}
|
|
2164
|
+
// True when the account has vault secrets on this control plane, meaning
|
|
2165
|
+
// snapshots should stop carrying local credentials. Any failure (old server,
|
|
2166
|
+
// vault unconfigured, network) degrades to legacy snapshot behavior.
|
|
2167
|
+
async function accountHasVaultSecrets(client) {
|
|
2168
|
+
if (process.env.RUDDER_CLOUD_LEGACY_SNAPSHOT_SECRETS === "1") {
|
|
2169
|
+
return false;
|
|
2170
|
+
}
|
|
2171
|
+
try {
|
|
2172
|
+
const result = await client.request("/api/rudder/secrets", { method: "GET" });
|
|
2173
|
+
return (result?.secrets ?? []).length > 0;
|
|
2174
|
+
}
|
|
2175
|
+
catch {
|
|
2176
|
+
return false;
|
|
2177
|
+
}
|
|
2178
|
+
}
|
|
1773
2179
|
async function workspaceAttach(args, options) {
|
|
1774
2180
|
const explicitId = args[0];
|
|
1775
2181
|
if (explicitId) {
|
|
2182
|
+
if (explicitId.includes("/")) {
|
|
2183
|
+
await workspaceAttachByRepo(explicitId, options);
|
|
2184
|
+
return;
|
|
2185
|
+
}
|
|
1776
2186
|
await workspaceAttachById(explicitId, options);
|
|
1777
2187
|
return;
|
|
1778
2188
|
}
|
|
@@ -1783,13 +2193,31 @@ async function workspaceAttach(args, options) {
|
|
|
1783
2193
|
if (!options.json) {
|
|
1784
2194
|
process.stderr.write(`Resolving cloud workspace for ${repoName}...\n`);
|
|
1785
2195
|
}
|
|
2196
|
+
// Prefer an existing cloud-native (clone-based) workspace for this repo's
|
|
2197
|
+
// origin over uploading a snapshot of the local directory.
|
|
2198
|
+
const originSlug = await githubSlugFromOrigin(repoRoot);
|
|
2199
|
+
if (originSlug && isTty() && !options.json) {
|
|
2200
|
+
const cloneWorkspace = await client.request(`/api/rudder/workspace/lookup?repo=${encodeURIComponent(originSlug)}`, { method: "GET" }).catch(() => null);
|
|
2201
|
+
if (cloneWorkspace) {
|
|
2202
|
+
const useClone = await promptConfirm(`A cloud-native workspace for ${originSlug} exists. Attach it instead of uploading a local snapshot?`, true);
|
|
2203
|
+
if (useClone) {
|
|
2204
|
+
const result = await client.request("/api/rudder/workspace/create", {
|
|
2205
|
+
method: "POST",
|
|
2206
|
+
body: { repo: originSlug },
|
|
2207
|
+
});
|
|
2208
|
+
await attachToWorkspaceResult(result, options);
|
|
2209
|
+
return;
|
|
2210
|
+
}
|
|
2211
|
+
}
|
|
2212
|
+
}
|
|
1786
2213
|
// Kick off the non-interactive work in parallel. planAgentMigration can
|
|
1787
2214
|
// call promptConfirm for a TTY prompt, so we serialize it AFTER the
|
|
1788
2215
|
// parallel work resolves to avoid garbled stdout during the prompt.
|
|
1789
|
-
const [region,
|
|
1790
|
-
|
|
1791
|
-
|
|
2216
|
+
const [region, vaultActive] = await Promise.all([
|
|
2217
|
+
explicitCloudRegion(),
|
|
2218
|
+
accountHasVaultSecrets(client),
|
|
1792
2219
|
]);
|
|
2220
|
+
const fingerprint = await computeSnapshotFingerprint(repoRoot, options.homePaths ?? [], vaultActive);
|
|
1793
2221
|
const migrationPlan = await planAgentMigration(repoRoot, options);
|
|
1794
2222
|
const mustUploadSnapshot = Boolean(migrationPlan && migrationPlan.migrated.length > 0);
|
|
1795
2223
|
const baseBody = {
|
|
@@ -1820,6 +2248,7 @@ async function workspaceAttach(args, options) {
|
|
|
1820
2248
|
}
|
|
1821
2249
|
const snapshot = await createSnapshot(repoRoot, options.homePaths ?? [], {
|
|
1822
2250
|
includeRudderState: true,
|
|
2251
|
+
includeCredentials: !vaultActive,
|
|
1823
2252
|
migration: migrationPlan ? { repoName, plan: migrationPlan } : undefined,
|
|
1824
2253
|
});
|
|
1825
2254
|
try {
|
|
@@ -1891,7 +2320,8 @@ async function attachToWorkspaceResult(result, options) {
|
|
|
1891
2320
|
printJson(record);
|
|
1892
2321
|
return;
|
|
1893
2322
|
}
|
|
1894
|
-
|
|
2323
|
+
// The latency probe is non-interactive by design; it must not be gated on a TTY.
|
|
2324
|
+
if (!options.latencyProbe && (!process.stdin.isTTY || !process.stdout.isTTY)) {
|
|
1895
2325
|
process.stderr.write(`Workspace ${workspaceId} is ready. Run \`rudder cloud workspace attach\` from a TTY to take over.\n`);
|
|
1896
2326
|
return;
|
|
1897
2327
|
}
|
|
@@ -1904,7 +2334,7 @@ async function workspaceAttachById(workspaceId, options) {
|
|
|
1904
2334
|
if (options.json) {
|
|
1905
2335
|
printJson({ id: workspaceId, attaching: true });
|
|
1906
2336
|
}
|
|
1907
|
-
else if (!process.stdin.isTTY || !process.stdout.isTTY) {
|
|
2337
|
+
else if (!options.latencyProbe && (!process.stdin.isTTY || !process.stdout.isTTY)) {
|
|
1908
2338
|
process.stderr.write(`Workspace ${workspaceId}: attach requires a TTY.\n`);
|
|
1909
2339
|
return;
|
|
1910
2340
|
}
|
|
@@ -2084,7 +2514,8 @@ async function runAttach(target, options) {
|
|
|
2084
2514
|
+ `/api/rudder/${target.kind}/${encodeURIComponent(target.id)}/attach`;
|
|
2085
2515
|
const stdin = process.stdin;
|
|
2086
2516
|
const stdout = process.stdout;
|
|
2087
|
-
const
|
|
2517
|
+
const probeMode = Boolean(options.latencyProbe);
|
|
2518
|
+
const isInteractive = Boolean(stdin.isTTY && stdout.isTTY) && !probeMode;
|
|
2088
2519
|
return await new Promise((resolve, reject) => {
|
|
2089
2520
|
const socket = new WebSocket(wsUrl, {
|
|
2090
2521
|
headers: { authorization: `Bearer ${token}` },
|
|
@@ -2215,6 +2646,91 @@ async function runAttach(target, options) {
|
|
|
2215
2646
|
}
|
|
2216
2647
|
};
|
|
2217
2648
|
process.once("SIGINT", onSigint);
|
|
2649
|
+
// --latency-probe state: transport samples resolve on a `probe-reply`
|
|
2650
|
+
// control frame from the worker (pure WS relay round trip); echo samples
|
|
2651
|
+
// resolve on the next binary frame after sending a printable keystroke
|
|
2652
|
+
// (full pipeline including the remote TUI render).
|
|
2653
|
+
const probeReplies = new Map();
|
|
2654
|
+
let probeEchoWaiter = null;
|
|
2655
|
+
let probeSeq = 0;
|
|
2656
|
+
const runLatencyProbe = async () => {
|
|
2657
|
+
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
2658
|
+
const SAMPLES = 20;
|
|
2659
|
+
const transport = [];
|
|
2660
|
+
const echo = [];
|
|
2661
|
+
// Let the remote dashboard finish its initial redraw burst so spinner
|
|
2662
|
+
// frames don't get mistaken for keystroke echoes.
|
|
2663
|
+
await sleep(750);
|
|
2664
|
+
for (let i = 0; i < SAMPLES; i += 1) {
|
|
2665
|
+
if (socket.readyState !== WebSocket.OPEN)
|
|
2666
|
+
break;
|
|
2667
|
+
const id = ++probeSeq;
|
|
2668
|
+
const sentAt = performance.now();
|
|
2669
|
+
const repliedAt = await new Promise((resolveReply) => {
|
|
2670
|
+
const timer = setTimeout(() => {
|
|
2671
|
+
probeReplies.delete(id);
|
|
2672
|
+
resolveReply(null);
|
|
2673
|
+
}, 2000);
|
|
2674
|
+
probeReplies.set(id, (at) => {
|
|
2675
|
+
clearTimeout(timer);
|
|
2676
|
+
probeReplies.delete(id);
|
|
2677
|
+
resolveReply(at);
|
|
2678
|
+
});
|
|
2679
|
+
socket.send(JSON.stringify({ type: "probe", id }));
|
|
2680
|
+
});
|
|
2681
|
+
if (repliedAt !== null)
|
|
2682
|
+
transport.push(repliedAt - sentAt);
|
|
2683
|
+
await sleep(100);
|
|
2684
|
+
}
|
|
2685
|
+
for (let i = 0; i < SAMPLES; i += 1) {
|
|
2686
|
+
if (socket.readyState !== WebSocket.OPEN)
|
|
2687
|
+
break;
|
|
2688
|
+
const sentAt = performance.now();
|
|
2689
|
+
const echoedAt = await new Promise((resolveFrame) => {
|
|
2690
|
+
const timer = setTimeout(() => {
|
|
2691
|
+
probeEchoWaiter = null;
|
|
2692
|
+
resolveFrame(null);
|
|
2693
|
+
}, 2000);
|
|
2694
|
+
probeEchoWaiter = (at) => {
|
|
2695
|
+
clearTimeout(timer);
|
|
2696
|
+
probeEchoWaiter = null;
|
|
2697
|
+
resolveFrame(at);
|
|
2698
|
+
};
|
|
2699
|
+
socket.send(Buffer.from("a"), { binary: true });
|
|
2700
|
+
});
|
|
2701
|
+
if (echoedAt !== null)
|
|
2702
|
+
echo.push(echoedAt - sentAt);
|
|
2703
|
+
// Undo the probe keystroke so the remote input box is left untouched.
|
|
2704
|
+
socket.send(Buffer.from("\x7f"), { binary: true });
|
|
2705
|
+
await sleep(250);
|
|
2706
|
+
}
|
|
2707
|
+
const stats = (values) => {
|
|
2708
|
+
if (values.length === 0)
|
|
2709
|
+
return "no samples (timed out)";
|
|
2710
|
+
const sorted = [...values].sort((a, b) => a - b);
|
|
2711
|
+
const at = (q) => sorted[Math.min(sorted.length - 1, Math.floor(q * sorted.length))] ?? 0;
|
|
2712
|
+
const max = sorted[sorted.length - 1] ?? 0;
|
|
2713
|
+
return `p50 ${at(0.5).toFixed(1)}ms p95 ${at(0.95).toFixed(1)}ms max ${max.toFixed(1)}ms (${sorted.length}/${SAMPLES} samples)`;
|
|
2714
|
+
};
|
|
2715
|
+
const report = [
|
|
2716
|
+
"",
|
|
2717
|
+
`Latency probe · ${target.label}`,
|
|
2718
|
+
` transport RTT ${stats(transport)}`,
|
|
2719
|
+
` keystroke echo ${stats(echo)}`,
|
|
2720
|
+
"",
|
|
2721
|
+
].join("\n");
|
|
2722
|
+
if (options.json) {
|
|
2723
|
+
process.stdout.write(`${JSON.stringify({ target: target.label, transportMs: transport, echoMs: echo })}\n`);
|
|
2724
|
+
}
|
|
2725
|
+
else {
|
|
2726
|
+
process.stderr.write(report);
|
|
2727
|
+
}
|
|
2728
|
+
result = "exited";
|
|
2729
|
+
try {
|
|
2730
|
+
socket.close(1000, "probe-done");
|
|
2731
|
+
}
|
|
2732
|
+
catch { /* ignore */ }
|
|
2733
|
+
};
|
|
2218
2734
|
socket.on("open", () => {
|
|
2219
2735
|
opened = true;
|
|
2220
2736
|
// Disable Nagle on the underlying TCP socket so single keystrokes don't
|
|
@@ -2263,12 +2779,47 @@ async function runAttach(target, options) {
|
|
|
2263
2779
|
// ignore
|
|
2264
2780
|
}
|
|
2265
2781
|
}
|
|
2266
|
-
|
|
2267
|
-
|
|
2782
|
+
if (!probeMode) {
|
|
2783
|
+
stdin.resume();
|
|
2784
|
+
stdin.on("data", onStdin);
|
|
2785
|
+
}
|
|
2786
|
+
else {
|
|
2787
|
+
// A worker that never boots would otherwise hang the probe forever:
|
|
2788
|
+
// there is no TTY and no human to Ctrl+C it.
|
|
2789
|
+
const firstFrameDeadline = setTimeout(() => {
|
|
2790
|
+
if (!firstFrameRendered) {
|
|
2791
|
+
process.stderr.write("Latency probe timed out waiting for the first remote frame (worker did not boot?).\n");
|
|
2792
|
+
result = "failed";
|
|
2793
|
+
try {
|
|
2794
|
+
socket.close(1000, "probe-timeout");
|
|
2795
|
+
}
|
|
2796
|
+
catch { /* ignore */ }
|
|
2797
|
+
}
|
|
2798
|
+
}, 120_000);
|
|
2799
|
+
firstFrameDeadline.unref?.();
|
|
2800
|
+
}
|
|
2268
2801
|
stdout.on("resize", onResize);
|
|
2269
2802
|
});
|
|
2270
2803
|
socket.on("message", (data, isBinary) => {
|
|
2271
2804
|
if (isBinary && Buffer.isBuffer(data)) {
|
|
2805
|
+
if (probeMode) {
|
|
2806
|
+
// Frames are timing signals here, not screen content: the first one
|
|
2807
|
+
// marks the dashboard as live (start probing), later ones resolve a
|
|
2808
|
+
// pending keystroke-echo sample.
|
|
2809
|
+
if (!firstFrameRendered) {
|
|
2810
|
+
firstFrameRendered = true;
|
|
2811
|
+
void runLatencyProbe().catch((err) => {
|
|
2812
|
+
process.stderr.write(`Latency probe failed: ${err instanceof Error ? err.message : String(err)}\n`);
|
|
2813
|
+
try {
|
|
2814
|
+
socket.close(1000, "probe-failed");
|
|
2815
|
+
}
|
|
2816
|
+
catch { /* ignore */ }
|
|
2817
|
+
});
|
|
2818
|
+
return;
|
|
2819
|
+
}
|
|
2820
|
+
probeEchoWaiter?.(performance.now());
|
|
2821
|
+
return;
|
|
2822
|
+
}
|
|
2272
2823
|
if (!firstFrameRendered) {
|
|
2273
2824
|
firstFrameRendered = true;
|
|
2274
2825
|
splash?.handoff();
|
|
@@ -2317,6 +2868,12 @@ async function runAttach(target, options) {
|
|
|
2317
2868
|
return;
|
|
2318
2869
|
}
|
|
2319
2870
|
const message = payload;
|
|
2871
|
+
if (message.type === "probe-reply") {
|
|
2872
|
+
if (typeof message.id === "number") {
|
|
2873
|
+
probeReplies.get(message.id)?.(performance.now());
|
|
2874
|
+
}
|
|
2875
|
+
return;
|
|
2876
|
+
}
|
|
2320
2877
|
if (message.type === "exit") {
|
|
2321
2878
|
result = message.code === 0 ? "exited" : "failed";
|
|
2322
2879
|
if (typeof process.exitCode !== "number" && message.code !== undefined) {
|
|
@@ -2514,8 +3071,12 @@ Usage:
|
|
|
2514
3071
|
print the copy/paste setup for the whole flow
|
|
2515
3072
|
rudder cloud slack [manifest]
|
|
2516
3073
|
print Slack setup (one thread per instance in the shared channel)
|
|
2517
|
-
rudder cloud workspace [attach [id]|share|status [--json]|pause <id>|resume <id>|stop <id>|list]
|
|
2518
|
-
shared cloud workspace for this repo
|
|
3074
|
+
rudder cloud workspace [attach [id|owner/repo]|create <owner/repo>|share|status [--json]|pause <id>|resume <id>|stop <id>|list]
|
|
3075
|
+
shared cloud workspace for this repo; \`create\` clones from GitHub (cloud-native, no local upload)
|
|
3076
|
+
rudder cloud secrets [set <NAME> [value]|set --file <~/path>|list|rm <NAME>|sync]
|
|
3077
|
+
manage the encrypted cloud secrets vault; \`sync\` imports your local credentials once
|
|
3078
|
+
rudder cloud region [<fly-region>|clear]
|
|
3079
|
+
override worker placement (default: next to the relay for lowest typing latency)
|
|
2519
3080
|
rudder cloud bootstrap <id>
|
|
2520
3081
|
rudder cloud runtime [fly|byoc]
|
|
2521
3082
|
rudder cloud setup-byoc <ssh-host> compatibility alias
|