Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions apps/ade-cli/src/bootstrap.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1588,6 +1588,7 @@ export async function createAdeRuntime(args: {
remoteCommandExecutor: resolvedArgs.syncRuntime.remoteCommandExecutor,
getModelPickerStore: () => getSharedModelPickerStore(db),
cloudRelayStore,
syncTunnelClientService,
onCloudRelayEnabledChanged: (enabled) => {
// Same gate as startup: only the sync-hosting runtime may register
// the relay tunnel (see canHostRelayTunnel above).
Expand Down
75 changes: 75 additions & 0 deletions apps/ade-cli/src/cli.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3315,6 +3315,81 @@ describe("ADE CLI", () => {
expect(output).toContain("Git repository detected");
});

it("adds sync route health to doctor and names a loopback listener mismatch", () => {
const projectRoot = fs.mkdtempSync(path.join(os.tmpdir(), "ade-cli-doctor-sync-"));
fs.mkdirSync(path.join(projectRoot, ".ade"), { recursive: true });
try {
const plan = expectExecutePlan(buildCliPlan(["doctor"]));
expect(plan.steps).toContainEqual({
key: "syncStatus",
method: "sync.getStatus",
params: { includeTransferReadiness: false },
optional: true,
});
const summary = summarizeExecution({
plan,
connection: {
mode: "runtime-socket",
projectRoot,
workspaceRoot: projectRoot,
socketPath: path.join(projectRoot, ".ade", "ade.sock"),
},
values: {
rpcActions: { actions: [{}] },
actions: { actions: [{}] },
syncStatus: {
pairingConnectInfo: { port: 8787 },
routeHealth: {
listener: {
listenerBound: true,
loopbackAdeValidated: false,
reason: "Expected ADE 426 Upgrade Required; received 404 Not Found.",
},
tailscale: {
enabled: true,
tailscaleReachable: false,
reason: "Tailscale route points at the listener mismatch.",
},
relay: {
enabled: true,
relayControlConnected: true,
relayBridgeValidated: false,
reason: "Relay bridge refused the listener mismatch.",
},
},
},
},
} as any) as Record<string, any>;

expect(summary.sync).toMatchObject({
enabled: true,
usable: false,
status: "warning",
});
expect(summary.sync.failingRoutes).toEqual([
expect.stringContaining("listener"),
expect.stringContaining("tailscale"),
expect.stringContaining("relay"),
]);
expect(summary.sync.message).toContain("404 Not Found");
const output = formatOutput(summary, {
projectRoot,
workspaceRoot: projectRoot,
role: "agent",
headless: false,
requireSocket: false,
socketPath: null,
pretty: true,
text: true,
timeoutMs: 1000,
}, "doctor");
expect(output).toContain("Sync route failure");
expect(output).toContain("listener");
} finally {
fs.rmSync(projectRoot, { recursive: true, force: true });
}
});

it("detects project-local Linear credentials in doctor readiness", () => {
const previousAdeLinearApi = process.env.ADE_LINEAR_API;
const previousLinearApiKey = process.env.LINEAR_API_KEY;
Expand Down
75 changes: 75 additions & 0 deletions apps/ade-cli/src/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11510,6 +11510,12 @@ function buildCliPlan(
...actionStep("projectConfig", "project_config", "get"),
optional: true,
},
{
key: "syncStatus",
method: "sync.getStatus",
params: { includeTransferReadiness: false },
optional: true,
},
],
};
}
Expand Down Expand Up @@ -12126,6 +12132,70 @@ function checkStorageReadiness(projectRoot: string): ReadinessCheck {
}
}

function checkSyncReadiness(value: unknown): ReadinessCheck & {
enabled: boolean;
usable: boolean;
failingRoutes: string[];
} {
const snapshot = isRecord(value) ? value : null;
const routeHealth = snapshot && isRecord(snapshot.routeHealth) ? snapshot.routeHealth : null;
const listener = routeHealth && isRecord(routeHealth.listener) ? routeHealth.listener : null;
const tailscale = routeHealth && isRecord(routeHealth.tailscale) ? routeHealth.tailscale : null;
const relay = routeHealth && isRecord(routeHealth.relay) ? routeHealth.relay : null;
const enabled = Boolean(snapshot?.pairingConnectInfo) || relay?.enabled === true;
if (!snapshot || !routeHealth) {
return {
ready: false,
enabled: false,
usable: false,
status: "unavailable",
message: "Sync route health is unavailable.",
nextAction: "Run 'ade sync status --text' against the live ADE runtime.",
failingRoutes: [],
};
}
if (!enabled) {
return {
ready: true,
enabled: false,
usable: false,
status: "unavailable",
message: "Phone sync hosting is not enabled in this runtime.",
failingRoutes: [],
details: { routeHealth },
};
}

const failures: string[] = [];
if (listener?.listenerBound !== true || listener?.loopbackAdeValidated !== true) {
failures.push(`listener: ${asString(listener?.reason) ?? "loopback listener mismatch"}`);
}
if (tailscale?.enabled === true && tailscale?.tailscaleReachable !== true) {
failures.push(`tailscale: ${asString(tailscale.reason) ?? "published route is not reachable"}`);
}
if (
relay?.enabled === true
&& (relay?.relayControlConnected !== true || asString(relay?.reason) != null)
) {
failures.push(`relay: ${asString(relay.reason) ?? "control channel is not connected"}`);
}
const usable = failures.length === 0;
return {
ready: usable,
enabled: true,
usable,
status: usable ? "ready" : "warning",
message: usable
? "Enabled sync routes are usable."
: `Sync route failure: ${failures.join("; ")}`,
nextAction: usable
? undefined
: "Run 'ade sync status --text' and resolve the named listener or route failure.",
failingRoutes: failures,
details: { routeHealth },
};
}

function requireAdeLayout(): {
resolveAdeLayout: (projectRoot: string) => { secretsDir: string };
} {
Expand Down Expand Up @@ -12183,6 +12253,7 @@ function buildReadinessSnapshot(args: {
computerUse: checkComputerUseReadiness(),
path: checkPathReadiness(),
storage: checkStorageReadiness(connection.projectRoot),
sync: checkSyncReadiness(values.syncStatus),
};
const recommendations = Object.entries(checks)
.filter(([, check]) => check.nextAction)
Expand Down Expand Up @@ -12258,6 +12329,7 @@ function buildReadinessSnapshot(args: {
computerUse: checks.computerUse,
path: checks.path,
storage: checks.storage,
sync: checks.sync,
auth: {
localProjectAccess: projectInitialized && actions.length > 0,
providerSecretsExposed: false,
Expand Down Expand Up @@ -17064,6 +17136,8 @@ function formatTextOutput(
isRecord(value) && isRecord(value.path) ? value.path : {};
const storage =
isRecord(value) && isRecord(value.storage) ? value.storage : {};
const sync =
isRecord(value) && isRecord(value.sync) ? value.sync : {};
const recommendations =
isRecord(value) && Array.isArray(value.recommendations)
? value.recommendations
Expand All @@ -17087,6 +17161,7 @@ function formatTextOutput(
["computer use", computerUse.message],
["path", pathStatus.message],
["storage", storage.message],
["sync", sync.message],
["recommendation", isRecord(value) ? value.recommendation : null],
]),
...(recommendations.length
Expand Down
Loading