botapp-cli 0.1.1 → 0.2.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/bin/bot.js CHANGED
@@ -721,34 +721,8 @@ Configured for ${serverUrl}`));
721
721
  return;
722
722
  }
723
723
  const profileName = pickProfileName(serverUrl);
724
- if (health.mode === "local" && health.token) {
725
- const config2 = loadProfile();
726
- config2.profiles[profileName] = {
727
- server: serverUrl,
728
- app_url: appUrl ?? serverUrl,
729
- token: health.token,
730
- user_id: "local",
731
- agent_id: "local-agent"
732
- };
733
- config2.active_profile = profileName;
734
- saveProfile(config2);
735
- console.log(pc4.green("Logged in (local mode)"));
736
- console.log(` Profile: ${pc4.bold(profileName)}`);
737
- console.log(` Server: ${pc4.cyan(serverUrl)}`);
738
- const paired = await tryLocalDaemonPair(serverUrl, hostname());
739
- if (paired) {
740
- console.log(
741
- ` Daemon: ${pc4.bold(paired.name)} ${pc4.dim(`(${paired.id})`)}`
742
- );
743
- await autoRegisterAgents(opts);
744
- await autoStartDaemon(opts, serverUrl, paired.id);
745
- } else {
746
- console.log(pc4.yellow(` Daemon: pairing failed \u2014 run \`bot pair\` later`));
747
- }
748
- return;
749
- }
750
- if (opts.noBrowser) {
751
- console.error(pc4.red("Hosted server needs browser auth but --no-browser was passed."));
724
+ if (opts.browser === false) {
725
+ console.error(pc4.red("Login needs browser auth but --no-browser was passed."));
752
726
  process.exitCode = 1;
753
727
  return;
754
728
  }
@@ -912,35 +886,6 @@ async function autoRegisterAgents(opts) {
912
886
  console.log(pc4.yellow(` Agents: registration failed (${e.message})`));
913
887
  }
914
888
  }
915
- async function tryLocalDaemonPair(serverUrl, name) {
916
- try {
917
- const tokRes = await fetch(`${serverUrl}/api/daemon/pairing-tokens`, {
918
- method: "POST",
919
- headers: { "Content-Type": "application/json" },
920
- body: JSON.stringify({ name })
921
- });
922
- if (!tokRes.ok) return null;
923
- const tokData = await tokRes.json();
924
- if (!tokData.token) return null;
925
- const pairRes = await fetch(`${serverUrl}/api/daemon/pair`, {
926
- method: "POST",
927
- headers: { "Content-Type": "application/json" },
928
- body: JSON.stringify({ token: tokData.token, name, machine: name })
929
- });
930
- if (!pairRes.ok) return null;
931
- const data = await pairRes.json();
932
- if (!data.daemon?.id || !data.token) return null;
933
- saveDaemonProfile({
934
- server: serverUrl,
935
- daemonId: data.daemon.id,
936
- daemonName: data.daemon.name,
937
- token: data.token
938
- });
939
- return { id: data.daemon.id, name: data.daemon.name };
940
- } catch {
941
- return null;
942
- }
943
- }
944
889
  function pickProfileName(serverUrl) {
945
890
  try {
946
891
  const host = new URL(serverUrl).hostname;
@@ -955,7 +900,7 @@ async function fetchHealth(serverUrl) {
955
900
  try {
956
901
  const res = await fetch(`${serverUrl}/health`, { signal: AbortSignal.timeout(5e3) });
957
902
  if (!res.ok) return null;
958
- return await res.json();
903
+ return { ok: true };
959
904
  } catch {
960
905
  return null;
961
906
  }
@@ -1851,106 +1796,8 @@ var skillCommand = skillSingle;
1851
1796
 
1852
1797
  // src/commands/pairing.ts
1853
1798
  import { hostname as hostname2 } from "os";
1854
- import { Command as Command17 } from "commander";
1855
- import pc18 from "picocolors";
1856
- var pairingCommand = new Command17("pairing").alias("pair").description("Pair this machine as a botapp daemon").option("--token <token>", "Pairing token (skip browser flow)").option("--name <name>", "Daemon name", hostname2()).option("--app-url <url>", "Dashboard URL (defaults to server URL)").option("--no-browser", "Error instead of opening a browser").action(async (opts, cmd) => {
1857
- const globalOpts = cmd.parent?.opts() ?? {};
1858
- const serverUrl = resolveServerUrl(globalOpts.server);
1859
- const name = opts.name;
1860
- try {
1861
- const grant = await obtainPairingToken({
1862
- serverUrl,
1863
- appUrl: opts.appUrl,
1864
- name,
1865
- explicitToken: opts.token,
1866
- allowBrowser: opts.browser !== false
1867
- });
1868
- if (!grant) {
1869
- console.error(
1870
- pc18.red(
1871
- "No pairing token and browser flow disabled. Pass --token, or drop --no-browser."
1872
- )
1873
- );
1874
- process.exitCode = 1;
1875
- return;
1876
- }
1877
- if (grant.userEmail) {
1878
- console.log(pc18.green(`Authenticated as ${pc18.bold(grant.userEmail)}`));
1879
- }
1880
- const res = await fetch(`${serverUrl}/api/daemon/pair`, {
1881
- method: "POST",
1882
- headers: { "Content-Type": "application/json" },
1883
- body: JSON.stringify({
1884
- token: grant.pairingToken,
1885
- name,
1886
- machine: hostname2()
1887
- })
1888
- });
1889
- const data = await res.json().catch(() => ({}));
1890
- if (!res.ok) {
1891
- console.error(pc18.red(`Error: ${data.error ?? res.statusText}`));
1892
- process.exitCode = 1;
1893
- return;
1894
- }
1895
- saveDaemonProfile({
1896
- server: serverUrl,
1897
- daemonId: data.daemon.id,
1898
- daemonName: data.daemon.name,
1899
- token: data.token
1900
- });
1901
- console.log(pc18.green(`Paired daemon: ${pc18.bold(data.daemon.name)}`));
1902
- console.log(` ID: ${data.daemon.id}`);
1903
- console.log(` Server: ${serverUrl}`);
1904
- console.log(pc18.dim("\nNext: botapp daemon agent add codex --command codex"));
1905
- console.log(pc18.dim("Then: botapp daemon run"));
1906
- } catch (e) {
1907
- console.error(pc18.red(`Error: ${e.message}`));
1908
- process.exitCode = 1;
1909
- }
1910
- });
1911
- async function obtainPairingToken(opts) {
1912
- if (opts.explicitToken) return { pairingToken: opts.explicitToken };
1913
- const localToken = await tryLocalFastPath(opts.serverUrl, opts.name);
1914
- if (localToken) return { pairingToken: localToken };
1915
- if (!opts.allowBrowser) return null;
1916
- const appUrl = opts.appUrl ?? opts.serverUrl;
1917
- const result = await runBrowserAuth({
1918
- serverUrl: opts.serverUrl,
1919
- appUrl,
1920
- scope: "pair",
1921
- name: opts.name
1922
- });
1923
- if (!result.pairingToken) {
1924
- throw new Error("server returned no pairing token");
1925
- }
1926
- return { pairingToken: result.pairingToken, userEmail: result.userEmail };
1927
- }
1928
- async function tryLocalFastPath(serverUrl, name) {
1929
- let mode = null;
1930
- try {
1931
- const res = await fetch(`${serverUrl}/health`, {
1932
- signal: AbortSignal.timeout(3e3)
1933
- });
1934
- if (!res.ok) return null;
1935
- const data = await res.json().catch(() => ({}));
1936
- mode = data.mode ?? null;
1937
- } catch {
1938
- return null;
1939
- }
1940
- if (mode !== "local") return null;
1941
- try {
1942
- const res = await fetch(`${serverUrl}/api/daemon/pairing-tokens`, {
1943
- method: "POST",
1944
- headers: { "Content-Type": "application/json" },
1945
- body: JSON.stringify({ name })
1946
- });
1947
- if (!res.ok) return null;
1948
- const data = await res.json().catch(() => ({}));
1949
- return data.token ?? null;
1950
- } catch {
1951
- return null;
1952
- }
1953
- }
1799
+ import { Command as Command18 } from "commander";
1800
+ import pc19 from "picocolors";
1954
1801
 
1955
1802
  // src/commands/daemon.ts
1956
1803
  import { spawn as spawn5 } from "child_process";
@@ -1959,14 +1806,14 @@ import { existsSync as existsSync10, readdirSync, readFileSync as readFileSync8,
1959
1806
  import { homedir as homedir7 } from "os";
1960
1807
  import { join as join7, resolve as resolve6 } from "path";
1961
1808
  import { createInterface as createInterface3 } from "readline";
1962
- import { Command as Command18 } from "commander";
1809
+ import { Command as Command17 } from "commander";
1963
1810
  import { WebSocket as WebSocket2 } from "ws";
1964
- import pc19 from "picocolors";
1965
- var daemonCommand = new Command18("daemon").description("Manage and run the local botapp daemon");
1811
+ import pc18 from "picocolors";
1812
+ var daemonCommand = new Command17("daemon").description("Manage and run the local botapp daemon");
1966
1813
  daemonCommand.command("run").description("Run the local daemon and wait for server jobs").action(async () => {
1967
1814
  const profile = loadDaemonProfile();
1968
1815
  if (!profile) {
1969
- console.error(pc19.red("No paired daemon found. Run `bot pair` first."));
1816
+ console.error(pc18.red("No paired daemon found. Run `bot pair` first."));
1970
1817
  process.exitCode = 1;
1971
1818
  return;
1972
1819
  }
@@ -1975,37 +1822,37 @@ daemonCommand.command("run").description("Run the local daemon and wait for serv
1975
1822
  daemonCommand.command("stop").description("Stop the background daemon started by `bot launch`").action(async () => {
1976
1823
  const pidFile = join7(homedir7(), ".botapp", "daemon.pid");
1977
1824
  if (!existsSync10(pidFile)) {
1978
- console.log(pc19.yellow("No background daemon PID file found."));
1825
+ console.log(pc18.yellow("No background daemon PID file found."));
1979
1826
  return;
1980
1827
  }
1981
1828
  try {
1982
1829
  const pid = parseInt(readFileSync8(pidFile, "utf-8").trim(), 10);
1983
1830
  if (!Number.isInteger(pid) || pid <= 0) {
1984
- console.error(pc19.red(`Invalid PID in ${pidFile}`));
1831
+ console.error(pc18.red(`Invalid PID in ${pidFile}`));
1985
1832
  process.exitCode = 1;
1986
1833
  return;
1987
1834
  }
1988
1835
  process.kill(pid, "SIGTERM");
1989
- console.log(pc19.green(`Stopped daemon (pid ${pid})`));
1836
+ console.log(pc18.green(`Stopped daemon (pid ${pid})`));
1990
1837
  } catch (e) {
1991
- console.error(pc19.red(`Failed to stop daemon: ${e.message}`));
1838
+ console.error(pc18.red(`Failed to stop daemon: ${e.message}`));
1992
1839
  process.exitCode = 1;
1993
1840
  }
1994
1841
  });
1995
1842
  daemonCommand.command("agent").description("Manage agents hosted by this daemon").addCommand(
1996
- new Command18("list").description("List registered daemon agents").action(async () => {
1843
+ new Command17("list").description("List registered daemon agents").action(async () => {
1997
1844
  const profile = requireProfile();
1998
1845
  if (!profile) return;
1999
1846
  const data = await daemonRequest(profile.server, profile.token, "/api/daemon/self/agents");
2000
1847
  for (const agent of data.agents ?? []) {
2001
- console.log(`${pc19.bold(agent.name)} ${pc19.dim(`(${agent.id})`)}`);
1848
+ console.log(`${pc18.bold(agent.name)} ${pc18.dim(`(${agent.id})`)}`);
2002
1849
  console.log(` Kind: ${agent.kind}`);
2003
1850
  console.log(` Command: ${agent.command} ${(agent.args ?? []).join(" ")}`);
2004
1851
  if (agent.cwd) console.log(` CWD: ${agent.cwd}`);
2005
1852
  }
2006
1853
  })
2007
1854
  ).addCommand(createDaemonAgentConfigCommand()).addCommand(
2008
- new Command18("add").description("Register a local agent command").argument("<name>", "Name, e.g. codex, claude-code, openclaw, hermes, or hermes-agent").requiredOption("--command <command>", "Executable command").option(
1855
+ new Command17("add").description("Register a local agent command").argument("<name>", "Name, e.g. codex, claude-code, openclaw, hermes, or hermes-agent").requiredOption("--command <command>", "Executable command").option(
2009
1856
  "--kind <kind>",
2010
1857
  "Agent adapter kind: acp, codex, claude-code, openclaw, hermes, hermes-agent, or shell",
2011
1858
  "acp"
@@ -2029,12 +1876,12 @@ daemonCommand.command("agent").description("Manage agents hosted by this daemon"
2029
1876
  }
2030
1877
  }
2031
1878
  );
2032
- console.log(pc19.green(`Registered daemon agent: ${pc19.bold(data.agent.name)}`));
1879
+ console.log(pc18.green(`Registered daemon agent: ${pc18.bold(data.agent.name)}`));
2033
1880
  console.log(` ID: ${data.agent.id}`);
2034
1881
  console.log(` Command: ${data.agent.command} ${(data.agent.args ?? []).join(" ")}`);
2035
1882
  })
2036
1883
  ).addCommand(
2037
- new Command18("remove").alias("rm").description("Remove a daemon agent by id or name").argument("<agent>", "Daemon agent id or name").action(async (agent) => {
1884
+ new Command17("remove").alias("rm").description("Remove a daemon agent by id or name").argument("<agent>", "Daemon agent id or name").action(async (agent) => {
2038
1885
  const profile = requireProfile();
2039
1886
  if (!profile) return;
2040
1887
  const data = await daemonRequest(
@@ -2043,7 +1890,7 @@ daemonCommand.command("agent").description("Manage agents hosted by this daemon"
2043
1890
  `/api/daemon/self/agents/${encodeURIComponent(agent)}`,
2044
1891
  { method: "DELETE" }
2045
1892
  );
2046
- console.log(pc19.green(`Removed daemon agent: ${pc19.bold(data.agent.name)}`));
1893
+ console.log(pc18.green(`Removed daemon agent: ${pc18.bold(data.agent.name)}`));
2047
1894
  console.log(` ID: ${data.agent.id}`);
2048
1895
  })
2049
1896
  );
@@ -2070,7 +1917,7 @@ async function runDaemonConnectionLoop(profile) {
2070
1917
  process.once("SIGINT", stop);
2071
1918
  process.once("SIGTERM", stop);
2072
1919
  while (!stopped) {
2073
- console.log(pc19.blue(`Connecting daemon ${profile.daemonName} to ${profile.server}...`));
1920
+ console.log(pc18.blue(`Connecting daemon ${profile.daemonName} to ${profile.server}...`));
2074
1921
  const outcome = await runDaemonSocket(wsUrl, (ws) => {
2075
1922
  activeWs = ws;
2076
1923
  });
@@ -2078,7 +1925,7 @@ async function runDaemonConnectionLoop(profile) {
2078
1925
  if (stopped) break;
2079
1926
  const waitMs = outcome.opened ? baseRetryMs : retryMs;
2080
1927
  retryMs = outcome.opened ? baseRetryMs : Math.min(retryMs * 2, maxRetryMs);
2081
- console.log(pc19.yellow(`Retrying daemon connection in ${Math.round(waitMs / 1e3)}s...`));
1928
+ console.log(pc18.yellow(`Retrying daemon connection in ${Math.round(waitMs / 1e3)}s...`));
2082
1929
  await new Promise((resolveWait) => {
2083
1930
  resolveRetry = resolveWait;
2084
1931
  retryTimer = setTimeout(() => {
@@ -2104,7 +1951,7 @@ function runDaemonSocket(wsUrl, setActiveWs) {
2104
1951
  }
2105
1952
  ws.on("open", () => {
2106
1953
  opened = true;
2107
- console.log(pc19.green("Daemon connected"));
1954
+ console.log(pc18.green("Daemon connected"));
2108
1955
  ws.send(JSON.stringify({ type: "daemon_ready" }));
2109
1956
  ping = setInterval(() => {
2110
1957
  if (ws.readyState === WebSocket2.OPEN) ws.send(JSON.stringify({ type: "ping" }));
@@ -2114,11 +1961,11 @@ function runDaemonSocket(wsUrl, setActiveWs) {
2114
1961
  void handleFrame(ws, raw.toString());
2115
1962
  });
2116
1963
  ws.on("close", (code, reason) => {
2117
- console.log(pc19.yellow(`Daemon disconnected (${code}: ${reason.toString() || "no reason"})`));
1964
+ console.log(pc18.yellow(`Daemon disconnected (${code}: ${reason.toString() || "no reason"})`));
2118
1965
  finish();
2119
1966
  });
2120
1967
  ws.on("error", (err) => {
2121
- console.error(pc19.red(`Daemon WebSocket error: ${err.message}`));
1968
+ console.error(pc18.red(`Daemon WebSocket error: ${err.message}`));
2122
1969
  if (!opened) {
2123
1970
  if (ws.readyState !== WebSocket2.CLOSED) ws.terminate();
2124
1971
  finish();
@@ -2130,7 +1977,7 @@ async function handleFrame(ws, raw) {
2130
1977
  const frame = JSON.parse(raw);
2131
1978
  if (frame.type === "daemon_job") {
2132
1979
  const job = frame.job;
2133
- console.log(pc19.blue(`Running ${job.agent.name} job ${job.id}`));
1980
+ console.log(pc18.blue(`Running ${job.agent.name} job ${job.id}`));
2134
1981
  ws.send(JSON.stringify({ type: "daemon_job_started", jobId: job.id }));
2135
1982
  try {
2136
1983
  const result = await runAgentJob(job, (update) => {
@@ -2148,7 +1995,7 @@ async function handleFrame(ws, raw) {
2148
1995
  status: "succeeded",
2149
1996
  result
2150
1997
  }));
2151
- console.log(pc19.green(`Completed job ${job.id}`));
1998
+ console.log(pc18.green(`Completed job ${job.id}`));
2152
1999
  } catch (e) {
2153
2000
  ws.send(JSON.stringify({
2154
2001
  type: "daemon_job_result",
@@ -2156,12 +2003,12 @@ async function handleFrame(ws, raw) {
2156
2003
  status: "failed",
2157
2004
  error: e.message
2158
2005
  }));
2159
- console.error(pc19.red(`Job ${job.id} failed: ${e.message}`));
2006
+ console.error(pc18.red(`Job ${job.id} failed: ${e.message}`));
2160
2007
  }
2161
2008
  return;
2162
2009
  }
2163
2010
  if (frame.type === "error") {
2164
- console.error(pc19.red(`Server error: ${frame.message}`));
2011
+ console.error(pc18.red(`Server error: ${frame.message}`));
2165
2012
  }
2166
2013
  }
2167
2014
  async function runAgentJob(job, update) {
@@ -3036,7 +2883,7 @@ Invalid ACP stdout: ${line}`;
3036
2883
  clientInfo: {
3037
2884
  name: "botapp-daemon",
3038
2885
  title: "botapp daemon",
3039
- version: "0.1.0"
2886
+ version: "0.1.2"
3040
2887
  }
3041
2888
  });
3042
2889
  const session = await request2("session/new", {
@@ -3057,7 +2904,7 @@ Invalid ACP stdout: ${line}`;
3057
2904
  function requireProfile() {
3058
2905
  const profile = loadDaemonProfile();
3059
2906
  if (!profile) {
3060
- console.error(pc19.red("No paired daemon found. Run `bot pair` first."));
2907
+ console.error(pc18.red("No paired daemon found. Run `bot pair` first."));
3061
2908
  process.exitCode = 1;
3062
2909
  return null;
3063
2910
  }
@@ -3160,8 +3007,104 @@ async function waitForChild(child, timeoutMs, label) {
3160
3007
  });
3161
3008
  }
3162
3009
 
3010
+ // src/commands/pairing.ts
3011
+ var pairingCommand = new Command18("pairing").alias("pair").description("Pair this machine as a botapp daemon").option("--token <token>", "Pairing token (skip browser flow)").option("--name <name>", "Daemon name", hostname2()).option("--app-url <url>", "Dashboard URL (defaults to server URL)").option("--no-browser", "Error instead of opening a browser").option("--run", "After pairing, register local agents and run the daemon (single-process onboarding)").action(async (opts, cmd) => {
3012
+ const globalOpts = cmd.parent?.opts() ?? {};
3013
+ const serverUrl = resolveServerUrl(globalOpts.server);
3014
+ const name = opts.name;
3015
+ try {
3016
+ const grant = await obtainPairingToken({
3017
+ serverUrl,
3018
+ appUrl: opts.appUrl,
3019
+ name,
3020
+ explicitToken: opts.token,
3021
+ allowBrowser: opts.browser !== false
3022
+ });
3023
+ if (!grant) {
3024
+ console.error(
3025
+ pc19.red(
3026
+ "No pairing token and browser flow disabled. Pass --token, or drop --no-browser."
3027
+ )
3028
+ );
3029
+ process.exitCode = 1;
3030
+ return;
3031
+ }
3032
+ if (grant.userEmail) {
3033
+ console.log(pc19.green(`Authenticated as ${pc19.bold(grant.userEmail)}`));
3034
+ }
3035
+ const res = await fetch(`${serverUrl}/api/daemon/pair`, {
3036
+ method: "POST",
3037
+ headers: { "Content-Type": "application/json" },
3038
+ body: JSON.stringify({
3039
+ token: grant.pairingToken,
3040
+ name,
3041
+ machine: hostname2()
3042
+ })
3043
+ });
3044
+ const data = await res.json().catch(() => ({}));
3045
+ if (!res.ok) {
3046
+ console.error(pc19.red(`Error: ${data.error ?? res.statusText}`));
3047
+ process.exitCode = 1;
3048
+ return;
3049
+ }
3050
+ saveDaemonProfile({
3051
+ server: serverUrl,
3052
+ daemonId: data.daemon.id,
3053
+ daemonName: data.daemon.name,
3054
+ token: data.token
3055
+ });
3056
+ console.log(pc19.green(`Paired daemon: ${pc19.bold(data.daemon.name)}`));
3057
+ console.log(` ID: ${data.daemon.id}`);
3058
+ console.log(` Server: ${serverUrl}`);
3059
+ if (opts.run) {
3060
+ try {
3061
+ const summary = await registerAllDetectedAgents({
3062
+ server: serverUrl,
3063
+ token: data.token,
3064
+ cwd: process.cwd()
3065
+ });
3066
+ if (summary.created.length > 0) {
3067
+ console.log(pc19.green(`Registered agents: ${summary.created.join(", ")}`));
3068
+ } else if (summary.detected === 0) {
3069
+ console.log(pc19.dim("No local agent CLIs detected (codex, claude, \u2026) \u2014 chat will be unavailable until one is installed."));
3070
+ }
3071
+ } catch (err) {
3072
+ console.log(pc19.yellow(`Agent auto-registration failed: ${err?.message ?? err}`));
3073
+ }
3074
+ const profile = loadDaemonProfile();
3075
+ if (!profile) {
3076
+ console.error(pc19.red("Pairing succeeded but daemon profile is missing \u2014 cannot run."));
3077
+ process.exitCode = 1;
3078
+ return;
3079
+ }
3080
+ await runDaemonConnectionLoop(profile);
3081
+ return;
3082
+ }
3083
+ console.log(pc19.dim("\nNext: botapp daemon agent add codex --command codex"));
3084
+ console.log(pc19.dim("Then: botapp daemon run"));
3085
+ } catch (e) {
3086
+ console.error(pc19.red(`Error: ${e.message}`));
3087
+ process.exitCode = 1;
3088
+ }
3089
+ });
3090
+ async function obtainPairingToken(opts) {
3091
+ if (opts.explicitToken) return { pairingToken: opts.explicitToken };
3092
+ if (!opts.allowBrowser) return null;
3093
+ const appUrl = opts.appUrl ?? opts.serverUrl;
3094
+ const result = await runBrowserAuth({
3095
+ serverUrl: opts.serverUrl,
3096
+ appUrl,
3097
+ scope: "pair",
3098
+ name: opts.name
3099
+ });
3100
+ if (!result.pairingToken) {
3101
+ throw new Error("server returned no pairing token");
3102
+ }
3103
+ return { pairingToken: result.pairingToken, userEmail: result.userEmail };
3104
+ }
3105
+
3163
3106
  // src/index.ts
3164
- var version = "0.1.0";
3107
+ var version = "0.1.2";
3165
3108
  var program = new Command19().name("bot").description("botapp CLI \u2014 operate apps from the command line").version(version).enablePositionalOptions(true).option("--json", "Output as JSON").option("-s, --server <url>", "Server URL override").option("-t, --token <token>", "Auth token override").option("-v, --verbose", "Verbose output");
3166
3109
  program.addCommand(launchCommand);
3167
3110
  program.addCommand(runCommand);