@trycadence/cli 0.1.0 → 0.1.2

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.
Files changed (2) hide show
  1. package/dist/cadence +190 -34
  2. package/package.json +1 -1
package/dist/cadence CHANGED
@@ -1447,7 +1447,12 @@ function createCadenceClient(options = {}) {
1447
1447
  rpc,
1448
1448
  health: () => getCadenceHealth(healthOptions),
1449
1449
  auth: {
1450
- login: (input) => rpc.auth.login.mutate(input)
1450
+ login: (input) => rpc.auth.login.mutate(input),
1451
+ cli: {
1452
+ start: (input) => rpc.auth.cli.start.mutate(input),
1453
+ poll: (input) => rpc.auth.cli.poll.query(input),
1454
+ complete: (input) => rpc.auth.cli.complete.mutate(input)
1455
+ }
1451
1456
  },
1452
1457
  events: {
1453
1458
  list: (input) => rpc.events.list.query(input)
@@ -1481,6 +1486,11 @@ function createCadenceClient(options = {}) {
1481
1486
  create: (input) => rpc.changesets.create.mutate(input),
1482
1487
  get: (input) => rpc.changesets.get.query(input),
1483
1488
  list: (input) => rpc.changesets.list.query(input)
1489
+ },
1490
+ projects: {
1491
+ default: () => rpc.projects.default.query(),
1492
+ list: () => rpc.projects.list.query(),
1493
+ resolve: (input) => rpc.projects.resolve.query(input)
1484
1494
  }
1485
1495
  };
1486
1496
  }
@@ -1494,6 +1504,7 @@ var ticketPriorities = ["low", "normal", "high", "urgent"];
1494
1504
  var ticketStatuses = ["backlog", "ready", "in_progress", "blocked", "review", "done", "abandoned"];
1495
1505
  var defaultLeaseTtlSeconds = 5 * 60 * 60;
1496
1506
  var defaultCliApiBaseUrl = "https://cadenceapi.deploy.lvl8studios.com";
1507
+ var defaultCliWebBaseUrl = "https://cadence.deploy.lvl8studios.com";
1497
1508
 
1498
1509
  class CliError extends Error {
1499
1510
  code;
@@ -1534,6 +1545,7 @@ var knownCommandPaths = [
1534
1545
  ["tickets", "get"],
1535
1546
  ["events", "list"],
1536
1547
  ["work", "overview"],
1548
+ ["projects", "list"],
1537
1549
  ["init"],
1538
1550
  ["status"],
1539
1551
  ["help"]
@@ -1724,8 +1736,44 @@ function getCredentialStore(options) {
1724
1736
  async function readPromptText(options, message) {
1725
1737
  return options.readText ? options.readText(message) : promptText(message);
1726
1738
  }
1727
- async function readPromptSecret(options, message) {
1728
- return options.readSecret ? options.readSecret(message) : promptSecret(message);
1739
+ function isInteractive(options) {
1740
+ return options.isInteractive ?? Boolean(process.stdin.isTTY && process.stderr.isTTY);
1741
+ }
1742
+ function getCliWebBaseUrl(config, parsed, options) {
1743
+ return parsed.options["web-base-url"] ?? options.env?.CADENCE_WEB_BASE_URL ?? process.env.CADENCE_WEB_BASE_URL ?? deriveWebBaseUrl(config.server);
1744
+ }
1745
+ function deriveWebBaseUrl(server) {
1746
+ try {
1747
+ const url = new URL(server);
1748
+ if ((url.hostname === "localhost" || url.hostname === "127.0.0.1") && url.port === "3000") {
1749
+ url.port = "3001";
1750
+ return url.toString().replace(/\/$/, "");
1751
+ }
1752
+ } catch {
1753
+ return defaultCliWebBaseUrl;
1754
+ }
1755
+ return defaultCliWebBaseUrl;
1756
+ }
1757
+ async function openBrowser(url, options) {
1758
+ if (options.openBrowser) {
1759
+ await options.openBrowser(url);
1760
+ return;
1761
+ }
1762
+ if (process.platform === "darwin") {
1763
+ const result = spawnSync("open", [url]);
1764
+ if (result.status !== 0) {
1765
+ throw new CliError("BROWSER_OPEN_FAILED", "Could not open the browser for Cadence login.", {
1766
+ loginUrl: url
1767
+ });
1768
+ }
1769
+ }
1770
+ }
1771
+ async function sleep2(milliseconds, options) {
1772
+ if (options.sleep) {
1773
+ await options.sleep(milliseconds);
1774
+ return;
1775
+ }
1776
+ await new Promise((resolve) => setTimeout(resolve, milliseconds));
1729
1777
  }
1730
1778
  function encodeCredential(credential) {
1731
1779
  return JSON.stringify(credential);
@@ -1773,19 +1821,6 @@ async function promptText(message) {
1773
1821
  readline.close();
1774
1822
  }
1775
1823
  }
1776
- async function promptSecret(message) {
1777
- if (!process.stdin.isTTY || !process.stderr.isTTY) {
1778
- throw new CliError("AUTH_INTERACTIVE_REQUIRED", "Interactive auth requires a TTY.");
1779
- }
1780
- spawnSync("stty", ["-echo"], { stdio: ["inherit", "ignore", "ignore"] });
1781
- try {
1782
- return await promptText(message);
1783
- } finally {
1784
- spawnSync("stty", ["echo"], { stdio: ["inherit", "ignore", "ignore"] });
1785
- process.stderr.write(`
1786
- `);
1787
- }
1788
- }
1789
1824
  function successEnvelope(data, meta) {
1790
1825
  return {
1791
1826
  success: true,
@@ -1816,11 +1851,12 @@ function helpText() {
1816
1851
  "Cadence CLI",
1817
1852
  "",
1818
1853
  "Usage:",
1819
- " cadence init --project <project-id> [--json]",
1854
+ " cadence init [--project <project-id|org/project>] [--json]",
1820
1855
  " cadence auth login [--json]",
1821
1856
  " cadence auth status [--json]",
1822
1857
  " cadence auth logout [--json]",
1823
1858
  " cadence status [--project <project-id>] [--json]",
1859
+ " cadence projects list [--json]",
1824
1860
  " cadence work overview [--project <project-id>] [--json]",
1825
1861
  " cadence tickets get <ticket-id> [--project <project-id>] [--json]",
1826
1862
  " cadence tickets list [--project <project-id>] [--status <status>] [--json]",
@@ -1841,11 +1877,11 @@ function helpText() {
1841
1877
  " cadence events list [--project <project-id>] [--ticket <ticket-id>] [--changeset <changeset-id>] [--session <session-id>] [--json]",
1842
1878
  "",
1843
1879
  "Global flags:",
1844
- " --project <id> Cadence project ID",
1880
+ " --project <id> Cadence project ID or org/project slug",
1845
1881
  " --json Print stable JSON envelope",
1846
1882
  "",
1847
1883
  "Auth options:",
1848
- " --email <email> Email for auth login"
1884
+ " --web-base-url <url> Browser login base URL"
1849
1885
  ].join(`
1850
1886
  `);
1851
1887
  }
@@ -1973,25 +2009,28 @@ async function runStatus(parsed, options) {
1973
2009
  async function runInit(parsed, options) {
1974
2010
  const cwd = options.cwd ?? process.cwd();
1975
2011
  const repoConfigPath = join(cwd, ".cadence", "config.json");
1976
- const projectId = parsed.flags.project;
1977
- if (!projectId) {
1978
- throw new CliError("CLI_USAGE", "init requires --project.");
1979
- }
2012
+ const config = await resolveCliConfig(parsed.flags, {
2013
+ ...options,
2014
+ cwd
2015
+ });
2016
+ const client = await createClient(config, options);
2017
+ const project = await selectProject(parsed, client, options);
2018
+ const projectId = project.id;
1980
2019
  const updates = {
1981
2020
  projectId,
1982
2021
  ...parsed.flags.server ? { server: parsed.flags.server } : {}
1983
2022
  };
1984
2023
  await mergeConfigFile(repoConfigPath, updates);
1985
- const config = await resolveCliConfig(parsed.flags, {
1986
- ...options,
1987
- cwd
1988
- });
1989
2024
  const data = {
1990
2025
  repoConfigPath,
1991
2026
  projectId,
2027
+ project,
1992
2028
  ...parsed.flags.server ? { server: parsed.flags.server } : {}
1993
2029
  };
1994
- const meta = commandMeta(parsed, config);
2030
+ const meta = commandMeta(parsed, {
2031
+ ...config,
2032
+ projectId
2033
+ });
1995
2034
  if (parsed.flags.json) {
1996
2035
  return {
1997
2036
  stdout: formatJson(successEnvelope(data, meta)),
@@ -2006,6 +2045,69 @@ async function runInit(parsed, options) {
2006
2045
  exitCode: 0
2007
2046
  };
2008
2047
  }
2048
+ function isUuid(value) {
2049
+ return /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i.test(value);
2050
+ }
2051
+ async function resolveProjectReference(projectReference, client) {
2052
+ if (isUuid(projectReference)) {
2053
+ return {
2054
+ id: projectReference,
2055
+ orgSlug: null,
2056
+ projectSlug: null,
2057
+ name: null
2058
+ };
2059
+ }
2060
+ const [orgSlug, projectSlug, extra] = projectReference.split("/");
2061
+ if (!orgSlug || !projectSlug || extra) {
2062
+ throw new CliError("CLI_USAGE", "--project must be a project ID or org/project slug.");
2063
+ }
2064
+ return await client.projects.resolve({
2065
+ orgSlug,
2066
+ projectSlug
2067
+ });
2068
+ }
2069
+ function formatProjectChoice(project, index) {
2070
+ const slug = project.orgSlug && project.projectSlug ? `${project.orgSlug}/${project.projectSlug}` : project.id;
2071
+ const name = project.name ? ` ${project.name}` : "";
2072
+ return `${index + 1}. ${slug}${name}`;
2073
+ }
2074
+ async function selectProject(parsed, client, options) {
2075
+ if (parsed.flags.project) {
2076
+ return await resolveProjectReference(parsed.flags.project, client);
2077
+ }
2078
+ const projects = await client.projects.list();
2079
+ if (projects.length === 1) {
2080
+ return projects[0];
2081
+ }
2082
+ if (parsed.flags.json || !isInteractive(options)) {
2083
+ throw new CliError("PROJECT_REQUIRED", "Choose a Cadence project with --project.", {
2084
+ projects
2085
+ });
2086
+ }
2087
+ if (projects.length === 0) {
2088
+ throw new CliError("PROJECT_REQUIRED", "No accessible Cadence projects were found.");
2089
+ }
2090
+ const message = [
2091
+ "Choose a Cadence project:",
2092
+ ...projects.map(formatProjectChoice),
2093
+ "",
2094
+ "Project number, ID, or org/project: "
2095
+ ].join(`
2096
+ `);
2097
+ const answer = (await readPromptText(options, message)).trim();
2098
+ const selectedIndex = Number(answer);
2099
+ const selected = Number.isInteger(selectedIndex) && selectedIndex > 0 ? projects[selectedIndex - 1] : undefined;
2100
+ if (selected) {
2101
+ return selected;
2102
+ }
2103
+ const matching = projects.find((project) => project.id === answer || `${project.orgSlug}/${project.projectSlug}` === answer);
2104
+ if (!matching) {
2105
+ throw new CliError("PROJECT_REQUIRED", "Selected project was not in the accessible project list.", {
2106
+ projects
2107
+ });
2108
+ }
2109
+ return matching;
2110
+ }
2009
2111
  async function runAuthCommand(parsed, options) {
2010
2112
  const config = await resolveCliConfig(parsed.flags, options);
2011
2113
  const store = getCredentialStore(options);
@@ -2014,18 +2116,43 @@ async function runAuthCommand(parsed, options) {
2014
2116
  switch (parsed.command.name) {
2015
2117
  case "auth.login":
2016
2118
  {
2017
- await requireCredentialStore(store);
2018
2119
  const client = await createClient(config, options);
2019
- const credential = await client.auth.login({
2020
- email: parsed.options.email ?? await readPromptText(options, "Email: "),
2021
- password: await readPromptSecret(options, "Password: ")
2120
+ const challenge = await client.auth.cli.start({
2121
+ loginBaseUrl: getCliWebBaseUrl(config, parsed, options)
2122
+ });
2123
+ if (parsed.flags.json || !isInteractive(options)) {
2124
+ throw new CliError("HUMAN_AUTH_REQUIRED", "Cadence login must be completed by a human in the browser.", {
2125
+ loginUrl: challenge.loginUrl,
2126
+ deviceCode: challenge.deviceCode,
2127
+ expiresAt: challenge.expiresAt
2128
+ });
2129
+ }
2130
+ await requireCredentialStore(store);
2131
+ await openBrowser(challenge.loginUrl, options);
2132
+ let poll = await client.auth.cli.poll({
2133
+ deviceId: challenge.deviceId,
2134
+ deviceCode: challenge.deviceCode
2022
2135
  });
2023
- await store.setCredential(config.server, encodeCredential(credential));
2136
+ while (poll.status === "pending") {
2137
+ await sleep2(poll.pollIntervalSeconds * 1000, options);
2138
+ poll = await client.auth.cli.poll({
2139
+ deviceId: challenge.deviceId,
2140
+ deviceCode: challenge.deviceCode
2141
+ });
2142
+ }
2143
+ if (poll.status === "expired") {
2144
+ throw new CliError("AUTH_LOGIN_EXPIRED", "Cadence browser login expired.");
2145
+ }
2146
+ if (poll.status === "denied") {
2147
+ throw new CliError("AUTH_LOGIN_DENIED", "Cadence browser login was denied.");
2148
+ }
2149
+ await store.setCredential(config.server, encodeCredential(poll.credential));
2024
2150
  await mergeConfigFile(config.globalConfigPath, { server: config.server });
2025
2151
  data = {
2026
2152
  server: config.server,
2027
2153
  credentialStored: true,
2028
- globalConfigPath: config.globalConfigPath
2154
+ globalConfigPath: config.globalConfigPath,
2155
+ loginUrl: challenge.loginUrl
2029
2156
  };
2030
2157
  }
2031
2158
  break;
@@ -2153,6 +2280,32 @@ async function runReadCommand(parsed, options) {
2153
2280
  exitCode: 0
2154
2281
  };
2155
2282
  }
2283
+ async function runProjectCommand(parsed, options) {
2284
+ const config = await resolveCliConfig(parsed.flags, options);
2285
+ const client = await createClient(config, options);
2286
+ const meta = commandMeta(parsed, config);
2287
+ let data;
2288
+ switch (parsed.command.name) {
2289
+ case "projects.list":
2290
+ data = await client.projects.list();
2291
+ break;
2292
+ default:
2293
+ throw new CliError("CLI_USAGE", `Unknown command: ${parsed.command.path.join(" ")}`);
2294
+ }
2295
+ if (parsed.flags.json) {
2296
+ return {
2297
+ stdout: formatJson(successEnvelope(data, meta)),
2298
+ stderr: "",
2299
+ exitCode: 0
2300
+ };
2301
+ }
2302
+ return {
2303
+ stdout: `${JSON.stringify(data, null, 2)}
2304
+ `,
2305
+ stderr: "",
2306
+ exitCode: 0
2307
+ };
2308
+ }
2156
2309
  async function runIntakeCommand(parsed, options) {
2157
2310
  const config = await resolveCliConfig(parsed.flags, options);
2158
2311
  const projectId = requireProjectId(config);
@@ -2376,6 +2529,9 @@ async function runCli(argv, options = {}) {
2376
2529
  if (parsed.command.name === "events.list" || parsed.command.name === "work.overview" || parsed.command.name === "tickets.get" || parsed.command.name === "tickets.list" || parsed.command.name === "sessions.current" || parsed.command.name === "changesets.get" || parsed.command.name === "changesets.list") {
2377
2530
  return await runReadCommand(parsed, options);
2378
2531
  }
2532
+ if (parsed.command.name === "projects.list") {
2533
+ return await runProjectCommand(parsed, options);
2534
+ }
2379
2535
  if (parsed.command.name === "intake" || parsed.command.name === "intake.dismiss" || parsed.command.name === "tickets.attach" || parsed.command.name === "tickets.create" || parsed.command.name === "tickets.update" || parsed.command.name === "tickets.claim" || parsed.command.name === "tickets.release" || parsed.command.name === "tickets.complete" || parsed.command.name === "sessions.start" || parsed.command.name === "sessions.end" || parsed.command.name === "changesets.create") {
2380
2536
  return await runIntakeCommand(parsed, options);
2381
2537
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@trycadence/cli",
3
- "version": "0.1.0",
3
+ "version": "0.1.2",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "bin": {