@otto-code/cli 0.7.5 → 0.8.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/cli.js +22 -2
- package/dist/commands/agent/delete.d.ts +1 -1
- package/dist/commands/agent/delete.js +2 -2
- package/dist/commands/agent/detach.d.ts +9 -0
- package/dist/commands/agent/detach.js +38 -0
- package/dist/commands/agent/index.js +9 -1
- package/dist/commands/agent/open.d.ts +11 -0
- package/dist/commands/agent/open.js +61 -0
- package/dist/commands/agent/run.d.ts +35 -0
- package/dist/commands/agent/run.js +142 -49
- package/dist/commands/agent/send.js +1 -1
- package/dist/commands/agent/update.d.ts +33 -0
- package/dist/commands/agent/update.js +72 -25
- package/dist/commands/clone.d.ts +17 -0
- package/dist/commands/clone.js +65 -0
- package/dist/commands/daemon/local-daemon.d.ts +15 -1
- package/dist/commands/daemon/local-daemon.js +14 -7
- package/dist/commands/daemon/pair.js +6 -2
- package/dist/commands/heartbeat/index.d.ts +3 -0
- package/dist/commands/heartbeat/index.js +139 -0
- package/dist/commands/hub/cloud-device-authorization.d.ts +45 -0
- package/dist/commands/hub/cloud-device-authorization.js +92 -0
- package/dist/commands/hub/device-authorization.d.ts +37 -0
- package/dist/commands/hub/device-authorization.js +87 -0
- package/dist/commands/hub/index.d.ts +30 -0
- package/dist/commands/hub/index.js +85 -0
- package/dist/commands/hub-disabled.d.ts +22 -0
- package/dist/commands/hub-disabled.js +34 -0
- package/dist/commands/onboard.js +6 -2
- package/dist/commands/open.d.ts +2 -0
- package/dist/commands/open.js +22 -17
- package/dist/commands/provider/ls.js +1 -1
- package/dist/commands/schedule/create.d.ts +1 -0
- package/dist/commands/schedule/create.js +1 -0
- package/dist/commands/schedule/index.js +6 -6
- package/dist/commands/schedule/inspect.js +3 -0
- package/dist/commands/schedule/logs.js +2 -1
- package/dist/commands/schedule/ls.js +3 -1
- package/dist/commands/schedule/pause.js +2 -1
- package/dist/commands/schedule/resume.js +2 -1
- package/dist/commands/schedule/run-once.js +2 -1
- package/dist/commands/schedule/shared.d.ts +3 -0
- package/dist/commands/schedule/shared.js +35 -23
- package/dist/commands/schedule/update.js +2 -1
- package/dist/commands/script/index.d.ts +3 -0
- package/dist/commands/script/index.js +19 -0
- package/dist/commands/script/ls.d.ts +6 -0
- package/dist/commands/script/ls.js +20 -0
- package/dist/commands/script/schema.d.ts +5 -0
- package/dist/commands/script/schema.js +13 -0
- package/dist/commands/script/shared.d.ts +11 -0
- package/dist/commands/script/shared.js +59 -0
- package/dist/commands/script/start.d.ts +6 -0
- package/dist/commands/script/start.js +23 -0
- package/dist/commands/script/stop.d.ts +6 -0
- package/dist/commands/script/stop.js +23 -0
- package/dist/commands/workspace/archive.d.ts +12 -0
- package/dist/commands/workspace/archive.js +41 -0
- package/dist/commands/workspace/create.d.ts +49 -0
- package/dist/commands/workspace/create.js +114 -0
- package/dist/commands/workspace/index.d.ts +3 -0
- package/dist/commands/workspace/index.js +30 -0
- package/dist/commands/workspace/ls.d.ts +7 -0
- package/dist/commands/workspace/ls.js +28 -0
- package/dist/commands/workspace/shared.d.ts +12 -0
- package/dist/commands/workspace/shared.js +20 -0
- package/dist/output/pairing.d.ts +8 -0
- package/dist/output/pairing.js +24 -0
- package/dist/utils/client.d.ts +9 -0
- package/dist/utils/client.js +9 -0
- package/dist/utils/duration.d.ts +1 -1
- package/dist/utils/duration.js +8 -7
- package/dist/utils/paths.js +1 -1
- package/dist/utils/provider-model.d.ts +1 -1
- package/dist/utils/provider-model.js +1 -1
- package/package.json +9 -7
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
const START_TIMEOUT_MS = 15000;
|
|
3
|
+
const activationUrlSchema = z.url({ protocol: /^https?$/u });
|
|
4
|
+
const authorizationSchema = z.object({
|
|
5
|
+
deviceCode: z.string().min(32),
|
|
6
|
+
userCode: z.string().min(1),
|
|
7
|
+
verificationUri: activationUrlSchema,
|
|
8
|
+
verificationUriComplete: activationUrlSchema,
|
|
9
|
+
expiresAt: z.string().datetime(),
|
|
10
|
+
interval: z.number().int().min(5),
|
|
11
|
+
});
|
|
12
|
+
const pollSchema = z.discriminatedUnion("status", [
|
|
13
|
+
z.object({ status: z.literal("pending"), interval: z.number().int().min(5) }),
|
|
14
|
+
z.object({ status: z.literal("slow_down"), interval: z.number().int().min(5) }),
|
|
15
|
+
z.object({
|
|
16
|
+
status: z.literal("approved"),
|
|
17
|
+
interval: z.number().int().min(5),
|
|
18
|
+
enrollmentToken: z.string().min(32),
|
|
19
|
+
}),
|
|
20
|
+
z.object({ status: z.literal("denied"), interval: z.number().int().min(5) }),
|
|
21
|
+
z.object({ status: z.literal("expired"), interval: z.number().int().min(5) }),
|
|
22
|
+
z.object({ status: z.literal("enrolled"), interval: z.number().int().min(5) }),
|
|
23
|
+
z.object({ status: z.literal("retry_later") }),
|
|
24
|
+
]);
|
|
25
|
+
export class CloudDeviceAuthorizationClient {
|
|
26
|
+
constructor(startTimeoutMilliseconds = START_TIMEOUT_MS) {
|
|
27
|
+
this.startTimeoutMilliseconds = startTimeoutMilliseconds;
|
|
28
|
+
}
|
|
29
|
+
async start(hubUrl, displayName) {
|
|
30
|
+
const signal = AbortSignal.timeout(this.startTimeoutMilliseconds);
|
|
31
|
+
try {
|
|
32
|
+
const response = await fetch(endpoint(hubUrl, "/api/device-authorizations/"), {
|
|
33
|
+
method: "POST",
|
|
34
|
+
headers: { "content-type": "application/json" },
|
|
35
|
+
body: JSON.stringify({ displayName }),
|
|
36
|
+
signal,
|
|
37
|
+
});
|
|
38
|
+
if (!response.ok)
|
|
39
|
+
throw new Error(`Cloud registration failed (${response.status})`);
|
|
40
|
+
return authorizationSchema.parse(await response.json());
|
|
41
|
+
}
|
|
42
|
+
catch (error) {
|
|
43
|
+
if (signal.aborted) {
|
|
44
|
+
throw new Error("Cloud registration start timed out", { cause: error });
|
|
45
|
+
}
|
|
46
|
+
throw error;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
async poll(hubUrl, deviceCode, timeoutMilliseconds) {
|
|
50
|
+
const signal = AbortSignal.timeout(timeoutMilliseconds);
|
|
51
|
+
let response;
|
|
52
|
+
try {
|
|
53
|
+
response = await fetch(endpoint(hubUrl, "/api/device-authorizations/poll"), {
|
|
54
|
+
method: "POST",
|
|
55
|
+
headers: { "content-type": "application/json" },
|
|
56
|
+
body: JSON.stringify({ deviceCode }),
|
|
57
|
+
signal,
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
catch {
|
|
61
|
+
return { status: "retry_later" };
|
|
62
|
+
}
|
|
63
|
+
if ([408, 425, 429].includes(response.status) || response.status >= 500) {
|
|
64
|
+
return { status: "retry_later" };
|
|
65
|
+
}
|
|
66
|
+
if (!response.ok)
|
|
67
|
+
throw new Error(`Cloud registration poll failed (${response.status})`);
|
|
68
|
+
let body;
|
|
69
|
+
try {
|
|
70
|
+
body = await response.json();
|
|
71
|
+
}
|
|
72
|
+
catch (error) {
|
|
73
|
+
if (signal.aborted || error instanceof TypeError)
|
|
74
|
+
return { status: "retry_later" };
|
|
75
|
+
throw error;
|
|
76
|
+
}
|
|
77
|
+
return pollSchema.parse(body);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
function endpoint(hubUrl, pathname) {
|
|
81
|
+
const url = new URL(hubUrl);
|
|
82
|
+
if (!["http:", "https:"].includes(url.protocol) ||
|
|
83
|
+
url.username ||
|
|
84
|
+
url.password ||
|
|
85
|
+
url.search ||
|
|
86
|
+
url.hash) {
|
|
87
|
+
throw new Error("Hub URL must be an HTTP or HTTPS origin without credentials or a query");
|
|
88
|
+
}
|
|
89
|
+
url.pathname = `${url.pathname.replace(/\/$/u, "")}${pathname}`;
|
|
90
|
+
return url.toString();
|
|
91
|
+
}
|
|
92
|
+
//# sourceMappingURL=cloud-device-authorization.js.map
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { type CloudDeviceAuthorization } from "./cloud-device-authorization.js";
|
|
2
|
+
export interface AuthorizationWaiter {
|
|
3
|
+
wait(milliseconds: number): Promise<void>;
|
|
4
|
+
now(): number;
|
|
5
|
+
}
|
|
6
|
+
export interface BrowserOpener {
|
|
7
|
+
open(url: string): Promise<void>;
|
|
8
|
+
}
|
|
9
|
+
type BrowserLaunch = (command: string, args: string[]) => Promise<void>;
|
|
10
|
+
interface SystemBrowserOptions {
|
|
11
|
+
hostPlatform?: NodeJS.Platform;
|
|
12
|
+
launch?: BrowserLaunch;
|
|
13
|
+
}
|
|
14
|
+
export declare class SystemBrowser implements BrowserOpener {
|
|
15
|
+
private readonly hostPlatform;
|
|
16
|
+
private readonly launch;
|
|
17
|
+
constructor(options?: SystemBrowserOptions);
|
|
18
|
+
open(url: string): Promise<void>;
|
|
19
|
+
}
|
|
20
|
+
export interface AuthorizationReporter {
|
|
21
|
+
instructions(verificationUri: string, userCode: string): void;
|
|
22
|
+
}
|
|
23
|
+
interface DeviceAuthorizationWorkflowOptions {
|
|
24
|
+
cloud: CloudDeviceAuthorization;
|
|
25
|
+
waiter: AuthorizationWaiter;
|
|
26
|
+
browser: BrowserOpener;
|
|
27
|
+
reporter: AuthorizationReporter;
|
|
28
|
+
openBrowser?: boolean;
|
|
29
|
+
}
|
|
30
|
+
export declare class DeviceAuthorizationWorkflow {
|
|
31
|
+
private readonly options;
|
|
32
|
+
constructor(options: DeviceAuthorizationWorkflowOptions);
|
|
33
|
+
authorize(hubUrl: string, displayName: string): Promise<string>;
|
|
34
|
+
}
|
|
35
|
+
export declare function createDeviceAuthorizationWorkflow(): DeviceAuthorizationWorkflow;
|
|
36
|
+
export {};
|
|
37
|
+
//# sourceMappingURL=device-authorization.d.ts.map
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
import { platform } from "node:os";
|
|
3
|
+
import { CloudDeviceAuthorizationClient, } from "./cloud-device-authorization.js";
|
|
4
|
+
export class SystemBrowser {
|
|
5
|
+
constructor(options = {}) {
|
|
6
|
+
this.hostPlatform = options.hostPlatform ?? platform();
|
|
7
|
+
this.launch = options.launch ?? launchDetached;
|
|
8
|
+
}
|
|
9
|
+
async open(url) {
|
|
10
|
+
if (this.hostPlatform === "win32") {
|
|
11
|
+
await this.launch("rundll32.exe", ["url.dll,FileProtocolHandler", url]);
|
|
12
|
+
return;
|
|
13
|
+
}
|
|
14
|
+
await this.launch(this.hostPlatform === "darwin" ? "open" : "xdg-open", [url]);
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
export class DeviceAuthorizationWorkflow {
|
|
18
|
+
constructor(options) {
|
|
19
|
+
this.options = options;
|
|
20
|
+
}
|
|
21
|
+
async authorize(hubUrl, displayName) {
|
|
22
|
+
const authorization = await this.options.cloud.start(hubUrl, displayName);
|
|
23
|
+
this.options.reporter.instructions(authorization.verificationUri, authorization.userCode);
|
|
24
|
+
if (this.options.openBrowser !== false) {
|
|
25
|
+
await this.options.browser.open(authorization.verificationUriComplete).catch(() => undefined);
|
|
26
|
+
}
|
|
27
|
+
let interval = authorization.interval;
|
|
28
|
+
const expiresAt = Date.parse(authorization.expiresAt);
|
|
29
|
+
while (true) {
|
|
30
|
+
const remaining = expiresAt - this.options.waiter.now();
|
|
31
|
+
if (remaining <= 0)
|
|
32
|
+
throw new Error("Daemon registration expired");
|
|
33
|
+
await this.options.waiter.wait(Math.min(interval * 1000, remaining));
|
|
34
|
+
if (this.options.waiter.now() >= expiresAt)
|
|
35
|
+
throw new Error("Daemon registration expired");
|
|
36
|
+
const pollLifetime = expiresAt - this.options.waiter.now();
|
|
37
|
+
if (pollLifetime <= 0)
|
|
38
|
+
throw new Error("Daemon registration expired");
|
|
39
|
+
const outcome = await this.options.cloud.poll(hubUrl, authorization.deviceCode, pollLifetime);
|
|
40
|
+
if (this.options.waiter.now() >= expiresAt)
|
|
41
|
+
throw new Error("Daemon registration expired");
|
|
42
|
+
if (outcome.status === "retry_later")
|
|
43
|
+
continue;
|
|
44
|
+
interval = outcome.interval;
|
|
45
|
+
if (outcome.status === "approved")
|
|
46
|
+
return outcome.enrollmentToken;
|
|
47
|
+
if (outcome.status === "denied")
|
|
48
|
+
throw new Error("Daemon registration was denied");
|
|
49
|
+
if (outcome.status === "expired")
|
|
50
|
+
throw new Error("Daemon registration expired");
|
|
51
|
+
if (outcome.status === "enrolled") {
|
|
52
|
+
throw new Error("Daemon registration was already used");
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
export function createDeviceAuthorizationWorkflow() {
|
|
58
|
+
return new DeviceAuthorizationWorkflow({
|
|
59
|
+
cloud: new CloudDeviceAuthorizationClient(),
|
|
60
|
+
waiter: {
|
|
61
|
+
wait: (milliseconds) => new Promise((resolve) => setTimeout(resolve, milliseconds)),
|
|
62
|
+
now: Date.now,
|
|
63
|
+
},
|
|
64
|
+
browser: new SystemBrowser(),
|
|
65
|
+
reporter: {
|
|
66
|
+
instructions(verificationUri, userCode) {
|
|
67
|
+
process.stderr.write(`Open ${verificationUri} and enter code ${userCode}\n`);
|
|
68
|
+
},
|
|
69
|
+
},
|
|
70
|
+
openBrowser: process.stderr.isTTY === true,
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
async function launchDetached(command, args) {
|
|
74
|
+
await new Promise((resolve, reject) => {
|
|
75
|
+
const child = spawn(command, args, {
|
|
76
|
+
detached: true,
|
|
77
|
+
shell: false,
|
|
78
|
+
stdio: "ignore",
|
|
79
|
+
});
|
|
80
|
+
child.once("spawn", () => {
|
|
81
|
+
child.unref();
|
|
82
|
+
resolve();
|
|
83
|
+
});
|
|
84
|
+
child.once("error", reject);
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
//# sourceMappingURL=device-authorization.js.map
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { Command } from "commander";
|
|
2
|
+
interface HubCommandClient {
|
|
3
|
+
connectHub(url: string, token: string): Promise<{
|
|
4
|
+
status: HubStatus;
|
|
5
|
+
}>;
|
|
6
|
+
getHubStatus(): Promise<{
|
|
7
|
+
status: HubStatus;
|
|
8
|
+
}>;
|
|
9
|
+
disconnectHub(force: boolean): Promise<{
|
|
10
|
+
status: HubStatus;
|
|
11
|
+
warning?: string;
|
|
12
|
+
}>;
|
|
13
|
+
close(): Promise<void>;
|
|
14
|
+
}
|
|
15
|
+
interface HubStatus {
|
|
16
|
+
state: string;
|
|
17
|
+
daemonId: string | null;
|
|
18
|
+
hubOrigin: string | null;
|
|
19
|
+
scopes: string[];
|
|
20
|
+
connectedAt: string | null;
|
|
21
|
+
lastError: string | null;
|
|
22
|
+
}
|
|
23
|
+
interface HubCommandEnvironment {
|
|
24
|
+
connect(host: string | undefined): Promise<HubCommandClient>;
|
|
25
|
+
authorize(url: string, displayName: string): Promise<string>;
|
|
26
|
+
displayName(): string;
|
|
27
|
+
}
|
|
28
|
+
export declare function createHubCommand(environment?: HubCommandEnvironment): Command;
|
|
29
|
+
export {};
|
|
30
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { Command } from "commander";
|
|
2
|
+
import { hostname } from "node:os";
|
|
3
|
+
import { withOutput } from "../../output/index.js";
|
|
4
|
+
import { addJsonAndDaemonHostOptions } from "../../utils/command-options.js";
|
|
5
|
+
import { connectToDaemon } from "../../utils/client.js";
|
|
6
|
+
import { createDeviceAuthorizationWorkflow } from "./device-authorization.js";
|
|
7
|
+
const productionEnvironment = {
|
|
8
|
+
connect: (host) => connectToDaemon({ host }),
|
|
9
|
+
authorize: (url, displayName) => createDeviceAuthorizationWorkflow().authorize(url, displayName),
|
|
10
|
+
displayName: hostname,
|
|
11
|
+
};
|
|
12
|
+
const schema = {
|
|
13
|
+
idField: "state",
|
|
14
|
+
columns: [
|
|
15
|
+
{ header: "STATE", field: "state" },
|
|
16
|
+
{ header: "HUB", field: "hub" },
|
|
17
|
+
{ header: "DAEMON", field: "daemonId" },
|
|
18
|
+
{ header: "SCOPES", field: "scopes" },
|
|
19
|
+
{ header: "CONNECTED", field: "connectedAt" },
|
|
20
|
+
{ header: "ERROR", field: "error" },
|
|
21
|
+
{ header: "WARNING", field: "warning" },
|
|
22
|
+
],
|
|
23
|
+
};
|
|
24
|
+
function result(status, warning) {
|
|
25
|
+
return {
|
|
26
|
+
type: "list",
|
|
27
|
+
data: [
|
|
28
|
+
{
|
|
29
|
+
state: status.state,
|
|
30
|
+
daemonId: status.daemonId,
|
|
31
|
+
hub: status.hubOrigin,
|
|
32
|
+
scopes: status.scopes.join(", "),
|
|
33
|
+
connectedAt: status.connectedAt,
|
|
34
|
+
error: status.lastError,
|
|
35
|
+
warning,
|
|
36
|
+
},
|
|
37
|
+
],
|
|
38
|
+
schema,
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
async function withClient(environment, host, action) {
|
|
42
|
+
const client = await environment.connect(host);
|
|
43
|
+
try {
|
|
44
|
+
return await action(client);
|
|
45
|
+
}
|
|
46
|
+
finally {
|
|
47
|
+
await client.close().catch(() => undefined);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
export function createHubCommand(environment = productionEnvironment) {
|
|
51
|
+
const hub = new Command("hub").description("Manage this daemon's Otto Hub relationship");
|
|
52
|
+
addJsonAndDaemonHostOptions(hub.command("connect").argument("<url>").option("--token <token>")).action(withOutput(async (...args) => {
|
|
53
|
+
const url = args[0];
|
|
54
|
+
const options = args.at(-2);
|
|
55
|
+
return withClient(environment, options.host, async (client) => {
|
|
56
|
+
if (options.token !== undefined) {
|
|
57
|
+
return result((await client.connectHub(url, options.token)).status);
|
|
58
|
+
}
|
|
59
|
+
const existing = (await client.getHubStatus()).status;
|
|
60
|
+
if (existing.state !== "not_connected" && existing.state !== "revoked") {
|
|
61
|
+
throw new Error("This daemon already has a Hub relationship");
|
|
62
|
+
}
|
|
63
|
+
const token = await environment.authorize(url, suggestedDisplayName(environment.displayName()));
|
|
64
|
+
return result((await client.connectHub(url, token)).status);
|
|
65
|
+
});
|
|
66
|
+
}));
|
|
67
|
+
addJsonAndDaemonHostOptions(hub.command("status")).action(withOutput(async (...args) => {
|
|
68
|
+
const options = args.at(-2);
|
|
69
|
+
return withClient(environment, options.host, async (client) => result((await client.getHubStatus()).status));
|
|
70
|
+
}));
|
|
71
|
+
addJsonAndDaemonHostOptions(hub
|
|
72
|
+
.command("disconnect")
|
|
73
|
+
.option("--force", "Remove local authority even if the Hub is offline")).action(withOutput(async (...args) => {
|
|
74
|
+
const options = args.at(-2);
|
|
75
|
+
return withClient(environment, options.host, async (client) => {
|
|
76
|
+
const response = await client.disconnectHub(options.force ?? false);
|
|
77
|
+
return result(response.status, response.warning);
|
|
78
|
+
});
|
|
79
|
+
}));
|
|
80
|
+
return hub;
|
|
81
|
+
}
|
|
82
|
+
function suggestedDisplayName(value) {
|
|
83
|
+
return value.trim().slice(0, 100) || "Otto daemon";
|
|
84
|
+
}
|
|
85
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* DISABLED(hub): CLI half of the Hub switch.
|
|
3
|
+
*
|
|
4
|
+
* `packages/server/src/server/hub-disabled.ts` carries the full rationale. The
|
|
5
|
+
* short version: Hub is a documented permanent exclusion that landed anyway in
|
|
6
|
+
* the Paseo v0.2.5 merge, and we turn it off by redirecting import specifiers
|
|
7
|
+
* rather than deleting wiring, so upstream's edits to the call sites keep
|
|
8
|
+
* auto-merging.
|
|
9
|
+
*
|
|
10
|
+
* `cli.ts` still calls `createHubCommand()` on a byte-identical line. It just
|
|
11
|
+
* resolves here instead of `./commands/hub/index.js`, which keeps that whole
|
|
12
|
+
* subtree, and the daemon client calls it makes, out of the CLI's module graph.
|
|
13
|
+
*
|
|
14
|
+
* The command stays registered, carries "(disabled in this build)" in its own
|
|
15
|
+
* description, and answers every invocation with one honest sentence. That
|
|
16
|
+
* beats removing it outright: `otto hub connect` explains itself instead of
|
|
17
|
+
* dying on "unknown command". Hiding it from `--help` would need an option on
|
|
18
|
+
* `addCommand`, and that call site is deliberately left untouched.
|
|
19
|
+
*/
|
|
20
|
+
import { Command } from "commander";
|
|
21
|
+
export declare function createHubCommand(_environment?: unknown): Command;
|
|
22
|
+
//# sourceMappingURL=hub-disabled.d.ts.map
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* DISABLED(hub): CLI half of the Hub switch.
|
|
3
|
+
*
|
|
4
|
+
* `packages/server/src/server/hub-disabled.ts` carries the full rationale. The
|
|
5
|
+
* short version: Hub is a documented permanent exclusion that landed anyway in
|
|
6
|
+
* the Paseo v0.2.5 merge, and we turn it off by redirecting import specifiers
|
|
7
|
+
* rather than deleting wiring, so upstream's edits to the call sites keep
|
|
8
|
+
* auto-merging.
|
|
9
|
+
*
|
|
10
|
+
* `cli.ts` still calls `createHubCommand()` on a byte-identical line. It just
|
|
11
|
+
* resolves here instead of `./commands/hub/index.js`, which keeps that whole
|
|
12
|
+
* subtree, and the daemon client calls it makes, out of the CLI's module graph.
|
|
13
|
+
*
|
|
14
|
+
* The command stays registered, carries "(disabled in this build)" in its own
|
|
15
|
+
* description, and answers every invocation with one honest sentence. That
|
|
16
|
+
* beats removing it outright: `otto hub connect` explains itself instead of
|
|
17
|
+
* dying on "unknown command". Hiding it from `--help` would need an option on
|
|
18
|
+
* `addCommand`, and that call site is deliberately left untouched.
|
|
19
|
+
*/
|
|
20
|
+
import { Command } from "commander";
|
|
21
|
+
const HUB_DISABLED_MESSAGE = "Otto Hub is disabled in this build. See docs/upstream-merges.md.";
|
|
22
|
+
export function createHubCommand(_environment) {
|
|
23
|
+
return new Command("hub")
|
|
24
|
+
.description("Manage this daemon's Otto Hub relationship (disabled in this build)")
|
|
25
|
+
.addHelpText("after", `\n${HUB_DISABLED_MESSAGE}`)
|
|
26
|
+
.allowUnknownOption()
|
|
27
|
+
.allowExcessArguments()
|
|
28
|
+
.argument("[command...]", "Hub subcommand")
|
|
29
|
+
.action(() => {
|
|
30
|
+
process.stderr.write(`${HUB_DISABLED_MESSAGE}\n`);
|
|
31
|
+
process.exitCode = 1;
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
//# sourceMappingURL=hub-disabled.js.map
|
package/dist/commands/onboard.js
CHANGED
|
@@ -5,6 +5,7 @@ import path from "node:path";
|
|
|
5
5
|
import { generateLocalPairingOffer, loadConfig, loadPersistedConfig, } from "@otto-code/server";
|
|
6
6
|
import { resolveLocalOttoHome, resolveLocalDaemonState, resolveTcpHostFromListen, startLocalDaemonDetached, tailDaemonLog, } from "./daemon/local-daemon.js";
|
|
7
7
|
import { tryConnectToDaemon } from "../utils/client.js";
|
|
8
|
+
import { formatPairingInstructions } from "../output/pairing.js";
|
|
8
9
|
const DEFAULT_READY_TIMEOUT_MS = 10 * 60 * 1000;
|
|
9
10
|
const READY_PROBE_TIMEOUT_MS = 1200;
|
|
10
11
|
class OnboardCancelledError extends Error {
|
|
@@ -406,8 +407,11 @@ export async function runOnboard(options) {
|
|
|
406
407
|
}
|
|
407
408
|
return;
|
|
408
409
|
}
|
|
409
|
-
|
|
410
|
-
|
|
410
|
+
process.stdout.write(formatPairingInstructions({
|
|
411
|
+
url: pairing.url,
|
|
412
|
+
qr: pairing.qr,
|
|
413
|
+
columns: process.stdout.columns,
|
|
414
|
+
}));
|
|
411
415
|
printNextSteps(pairing.url, ottoHome, richUi);
|
|
412
416
|
if (richUi) {
|
|
413
417
|
outro("Otto is ready!");
|
package/dist/commands/open.d.ts
CHANGED
|
@@ -1,2 +1,4 @@
|
|
|
1
|
+
import { type AgentDeepLinkTarget } from "@otto-code/protocol/agent-deep-link";
|
|
1
2
|
export declare function openDesktopWithProject(projectPath: string): Promise<void>;
|
|
3
|
+
export declare function openDesktopWithAgent(target: AgentDeepLinkTarget): Promise<void>;
|
|
2
4
|
//# sourceMappingURL=open.d.ts.map
|
package/dist/commands/open.js
CHANGED
|
@@ -2,6 +2,7 @@ import { existsSync } from "node:fs";
|
|
|
2
2
|
import { homedir } from "node:os";
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
import { spawnProcess } from "@otto-code/server";
|
|
5
|
+
import { buildAgentDeepLink } from "@otto-code/protocol/agent-deep-link";
|
|
5
6
|
function findDesktopApp() {
|
|
6
7
|
if (process.platform === "darwin") {
|
|
7
8
|
const candidates = ["/Applications/Otto.app", path.join(homedir(), "Applications", "Otto.app")];
|
|
@@ -52,25 +53,26 @@ function spawnDetached(command, args) {
|
|
|
52
53
|
env: cleanEnvForDesktopLaunch(),
|
|
53
54
|
}).unref();
|
|
54
55
|
}
|
|
56
|
+
function launchDesktop(args) {
|
|
57
|
+
if (process.env.OTTO_DESKTOP_CLI === "1") {
|
|
58
|
+
throw new Error("Cannot open Otto Desktop while running in desktop CLI passthrough mode.");
|
|
59
|
+
}
|
|
60
|
+
const desktopApp = findDesktopApp();
|
|
61
|
+
if (!desktopApp) {
|
|
62
|
+
throw new Error("Otto desktop app not found. Install it from https://github.com/Draek2077/otto-code/releases");
|
|
63
|
+
}
|
|
64
|
+
if (process.platform === "darwin") {
|
|
65
|
+
// -n forces a new instance even if the app is already running. The new
|
|
66
|
+
// instance relays its argv to the existing one through Electron's
|
|
67
|
+
// single-instance lock. -g keeps the terminal in the foreground.
|
|
68
|
+
spawnDetached("open", ["-n", "-g", "-a", desktopApp, "--args", ...args]);
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
spawnDetached(desktopApp, args);
|
|
72
|
+
}
|
|
55
73
|
export async function openDesktopWithProject(projectPath) {
|
|
56
74
|
try {
|
|
57
|
-
|
|
58
|
-
throw new Error("Cannot open a desktop project while running in desktop CLI passthrough mode.");
|
|
59
|
-
}
|
|
60
|
-
const desktopApp = findDesktopApp();
|
|
61
|
-
if (!desktopApp) {
|
|
62
|
-
throw new Error("Otto desktop app not found. Install it from https://github.com/Draek2077/otto-code/releases");
|
|
63
|
-
}
|
|
64
|
-
if (process.platform === "darwin") {
|
|
65
|
-
// -n forces a new instance even if the app is already running.
|
|
66
|
-
// The new instance hits requestSingleInstanceLock(), fails, and relays
|
|
67
|
-
// the argv to the first instance via the second-instance event.
|
|
68
|
-
// -g keeps the terminal in the foreground (better CLI UX).
|
|
69
|
-
// Without -n, macOS just activates the existing window and drops --args.
|
|
70
|
-
spawnDetached("open", ["-n", "-g", "-a", desktopApp, "--args", projectPath]);
|
|
71
|
-
return;
|
|
72
|
-
}
|
|
73
|
-
spawnDetached(desktopApp, [projectPath]);
|
|
75
|
+
launchDesktop([projectPath]);
|
|
74
76
|
}
|
|
75
77
|
catch (error) {
|
|
76
78
|
const message = error instanceof Error ? error.message : String(error);
|
|
@@ -78,4 +80,7 @@ export async function openDesktopWithProject(projectPath) {
|
|
|
78
80
|
process.exitCode = 1;
|
|
79
81
|
}
|
|
80
82
|
}
|
|
83
|
+
export async function openDesktopWithAgent(target) {
|
|
84
|
+
launchDesktop([buildAgentDeepLink(target)]);
|
|
85
|
+
}
|
|
81
86
|
//# sourceMappingURL=open.js.map
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { AGENT_PROVIDER_DEFINITIONS } from "@otto-code/protocol/provider-manifest";
|
|
2
2
|
import { tryConnectToDaemon } from "../../utils/client.js";
|
|
3
|
-
/** Derive provider list from the manifest
|
|
3
|
+
/** Derive provider list from the manifest - single source of truth */
|
|
4
4
|
const PROVIDERS = AGENT_PROVIDER_DEFINITIONS.map((def) => ({
|
|
5
5
|
provider: def.id,
|
|
6
6
|
label: def.label,
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Command } from "commander";
|
|
1
|
+
import { Command, Option } from "commander";
|
|
2
2
|
import { withOutput } from "../../output/index.js";
|
|
3
3
|
import { addJsonAndDaemonHostOptions } from "../../utils/command-options.js";
|
|
4
4
|
import { runCreateCommand } from "./create.js";
|
|
@@ -16,16 +16,16 @@ export function createScheduleCommand() {
|
|
|
16
16
|
.command("create")
|
|
17
17
|
.description("Create a schedule")
|
|
18
18
|
.argument("<prompt>", "Prompt to run on the schedule")
|
|
19
|
-
.option("--every <duration>", "
|
|
19
|
+
.option("--every <duration>", "Cron-compatible cadence preset (for example: 5m, 1h)")
|
|
20
20
|
.option("--cron <expr>", "Cron cadence expression")
|
|
21
21
|
.option("--timezone <iana>", "IANA time zone for cron cadence (default: UTC)")
|
|
22
22
|
.option("--name <name>", "Optional schedule name")
|
|
23
|
-
.
|
|
23
|
+
.addOption(new Option("--target <target>", "Legacy schedule target").hideHelp())
|
|
24
24
|
.option("--provider <provider>", "Agent provider, or provider/model (e.g. codex or codex/gpt-5.4)")
|
|
25
25
|
.option("--mode <mode>", "Provider-specific mode (e.g. claude bypassPermissions, opencode build)")
|
|
26
|
+
.option("--thinking <id>", "Thinking option ID for new-agent runs")
|
|
26
27
|
.option("--cwd <path>", "Working directory (default: current; required with --host)")
|
|
27
|
-
.option("--run-now", "Fire one immediate run on creation
|
|
28
|
-
.option("--no-run-now", "Wait the full interval before the first run (only with --every)")
|
|
28
|
+
.option("--run-now", "Fire one immediate run on creation")
|
|
29
29
|
.option("--max-runs <n>", "Maximum number of runs")
|
|
30
30
|
.option("--expires-in <duration>", "Time to live for the schedule")).action(withOutput(runCreateCommand));
|
|
31
31
|
addJsonAndDaemonHostOptions(schedule.command("ls").description("List schedules")).action(withOutput(runLsCommand));
|
|
@@ -48,7 +48,7 @@ export function createScheduleCommand() {
|
|
|
48
48
|
.command("update")
|
|
49
49
|
.description("Update an existing schedule in place")
|
|
50
50
|
.argument("<id>", "Schedule ID")
|
|
51
|
-
.option("--every <duration>", "
|
|
51
|
+
.option("--every <duration>", "Cron-compatible cadence preset (for example: 5m, 1h)")
|
|
52
52
|
.option("--cron <expr>", "Switch to cron cadence expression")
|
|
53
53
|
.option("--timezone <iana>", "IANA time zone for cron cadence (requires --cron)")
|
|
54
54
|
.option("--name <name>", "Rename the schedule (empty string clears the name)")
|
|
@@ -7,6 +7,9 @@ export async function runInspectCommand(id, options, _command) {
|
|
|
7
7
|
if (payload.error || !payload.schedule) {
|
|
8
8
|
throw new Error(payload.error ?? `Schedule not found: ${id}`);
|
|
9
9
|
}
|
|
10
|
+
if (payload.schedule.target.type !== "new-agent") {
|
|
11
|
+
throw new Error(`Schedule not found: ${id}`);
|
|
12
|
+
}
|
|
10
13
|
const rows = createScheduleInspectRows(payload.schedule);
|
|
11
14
|
return {
|
|
12
15
|
type: "list",
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import { scheduleLogSchema, toScheduleLogRow } from "./schema.js";
|
|
2
|
-
import { connectScheduleClient, toScheduleCommandError, } from "./shared.js";
|
|
2
|
+
import { connectScheduleClient, requireNewAgentSchedule, toScheduleCommandError, } from "./shared.js";
|
|
3
3
|
export async function runLogsCommand(id, options, _command) {
|
|
4
4
|
const { client } = await connectScheduleClient(options.host);
|
|
5
5
|
try {
|
|
6
|
+
await requireNewAgentSchedule(client, id);
|
|
6
7
|
const payload = await client.scheduleLogs({ id });
|
|
7
8
|
if (payload.error) {
|
|
8
9
|
throw new Error(payload.error);
|
|
@@ -9,7 +9,9 @@ export async function runLsCommand(options, _command) {
|
|
|
9
9
|
}
|
|
10
10
|
return {
|
|
11
11
|
type: "list",
|
|
12
|
-
data: payload.schedules
|
|
12
|
+
data: payload.schedules
|
|
13
|
+
.filter((schedule) => schedule.target.type === "new-agent")
|
|
14
|
+
.map(toScheduleRow),
|
|
13
15
|
schema: scheduleSchema,
|
|
14
16
|
};
|
|
15
17
|
}
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import { scheduleSchema } from "./schema.js";
|
|
2
|
-
import { connectScheduleClient, toScheduleCommandError, toScheduleRow, } from "./shared.js";
|
|
2
|
+
import { connectScheduleClient, requireNewAgentSchedule, toScheduleCommandError, toScheduleRow, } from "./shared.js";
|
|
3
3
|
export async function runPauseCommand(id, options, _command) {
|
|
4
4
|
const { client } = await connectScheduleClient(options.host);
|
|
5
5
|
try {
|
|
6
|
+
await requireNewAgentSchedule(client, id);
|
|
6
7
|
const payload = await client.schedulePause({ id });
|
|
7
8
|
if (payload.error || !payload.schedule) {
|
|
8
9
|
throw new Error(payload.error ?? `Failed to pause schedule: ${id}`);
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import { scheduleSchema } from "./schema.js";
|
|
2
|
-
import { connectScheduleClient, toScheduleCommandError, toScheduleRow, } from "./shared.js";
|
|
2
|
+
import { connectScheduleClient, requireNewAgentSchedule, toScheduleCommandError, toScheduleRow, } from "./shared.js";
|
|
3
3
|
export async function runResumeCommand(id, options, _command) {
|
|
4
4
|
const { client } = await connectScheduleClient(options.host);
|
|
5
5
|
try {
|
|
6
|
+
await requireNewAgentSchedule(client, id);
|
|
6
7
|
const payload = await client.scheduleResume({ id });
|
|
7
8
|
if (payload.error || !payload.schedule) {
|
|
8
9
|
throw new Error(payload.error ?? `Failed to resume schedule: ${id}`);
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import { scheduleSchema } from "./schema.js";
|
|
2
|
-
import { connectScheduleClient, toScheduleCommandError, toScheduleRow, } from "./shared.js";
|
|
2
|
+
import { connectScheduleClient, requireNewAgentSchedule, toScheduleCommandError, toScheduleRow, } from "./shared.js";
|
|
3
3
|
export async function runRunOnceCommand(id, options, _command) {
|
|
4
4
|
const { client } = await connectScheduleClient(options.host);
|
|
5
5
|
try {
|
|
6
|
+
await requireNewAgentSchedule(client, id);
|
|
6
7
|
const payload = await client.scheduleRunOnce({ id });
|
|
7
8
|
if (payload.error || !payload.schedule) {
|
|
8
9
|
throw new Error(payload.error ?? `Failed to run schedule once: ${id}`);
|
|
@@ -8,6 +8,7 @@ export declare function connectScheduleClient(host: string | undefined): Promise
|
|
|
8
8
|
host: string;
|
|
9
9
|
}>;
|
|
10
10
|
export declare function toScheduleCommandError(code: string, action: string, error: unknown): CommandError;
|
|
11
|
+
export declare function requireNewAgentSchedule(client: ScheduleDaemonClient, id: string): Promise<void>;
|
|
11
12
|
export declare function formatCadence(cadence: ScheduleCadence): string;
|
|
12
13
|
export declare function formatTarget(target: ScheduleTarget | ScheduleListItem["target"]): string;
|
|
13
14
|
export declare function formatDurationMs(durationMs: number): string;
|
|
@@ -20,6 +21,7 @@ export declare function parseScheduleCreateInput(options: {
|
|
|
20
21
|
target?: string;
|
|
21
22
|
provider?: string;
|
|
22
23
|
mode?: string;
|
|
24
|
+
thinking?: string;
|
|
23
25
|
cwd?: string;
|
|
24
26
|
host?: string;
|
|
25
27
|
maxRuns?: string;
|
|
@@ -43,6 +45,7 @@ export interface ScheduleUpdateOptionsInput {
|
|
|
43
45
|
clearExpires?: boolean;
|
|
44
46
|
}
|
|
45
47
|
export declare function parseScheduleUpdateInput(options: ScheduleUpdateOptionsInput): UpdateScheduleInput;
|
|
48
|
+
export declare function compileEveryPresetToCron(value: string): string;
|
|
46
49
|
export interface ScheduleRow {
|
|
47
50
|
id: string;
|
|
48
51
|
name: string | null;
|