@ycadence/cli 0.2.1 → 0.3.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.
Files changed (3) hide show
  1. package/README.md +22 -3
  2. package/dist/bin.js +94 -5
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -30,7 +30,27 @@ npm install -g @yarlisaisolutions/ycadence-cli
30
30
  The package installs two commands that behave the same: `ycadence`, and `devpulse`,
31
31
  the name from before the rename.
32
32
 
33
- ## Configure
33
+ ## Sign in
34
+
35
+ **Behind Google IAP** (the Yarlis instances): open the web app, go to **Settings →
36
+ Account → Command line**, click **Copy command**, and run it. It looks like this:
37
+
38
+ ```sh
39
+ ycadence login --setup ycs1.…
40
+ ```
41
+
42
+ It prints a code and a google.com/device link. Approve it with your work account and the
43
+ CLI finishes by itself: the instance issues this machine a personal token, and everything
44
+ secret goes into your OS keychain. Then:
45
+
46
+ ```sh
47
+ ycadence doctor
48
+ ```
49
+
50
+ The full walkthrough, troubleshooting and the admin setup are in
51
+ [docs/cli/getting-started.md](https://github.com/YarlisAISolutions/dev-pulse/blob/main/docs/cli/getting-started.md).
52
+
53
+ **Anywhere else**, sign in with a personal token from **Settings → Account → API tokens**:
34
54
 
35
55
  ```sh
36
56
  ycadence login --url https://ycadence.example.com --api-key-stdin < key.txt
@@ -41,8 +61,7 @@ ycadence doctor # checks configuration and connecti
41
61
  Profiles live in `~/.config/ycadence/config.json` (`$XDG_CONFIG_HOME` aware); switch with
42
62
  `--profile <name>` or `ycadence config use <name>`. The API key is kept in the OS keychain
43
63
  (macOS Keychain, or the Secret Service through `secret-tool`), else in a `credentials.json`
44
- with mode 0600. For an instance behind Google IAP, `ycadence login --oauth-client-id …
45
- --iap-audience …` signs in with Google and refreshes the identity token itself.
64
+ with mode 0600.
46
65
 
47
66
  Environment variables still win, so CI needs nothing else:
48
67
 
package/dist/bin.js CHANGED
@@ -535,6 +535,7 @@ var PROFILE_KEYS = [
535
535
  "project",
536
536
  "format",
537
537
  "oauthClientId",
538
+ "oauthFlow",
538
539
  "iapAudience"
539
540
  ];
540
541
  var PROFILE_KEY_HELP = {
@@ -543,6 +544,7 @@ var PROFILE_KEY_HELP = {
543
544
  project: "Default project id (or identifier, once the server lists projects)",
544
545
  format: "Default output format: table, json or ndjson",
545
546
  oauthClientId: "Desktop OAuth client id used by `ycadence login` for IAP",
547
+ oauthFlow: "How `ycadence login` signs in to Google: device or loopback",
546
548
  iapAudience: "The IAP OAuth client id; the audience of the identity token"
547
549
  };
548
550
  function configDirectory(env) {
@@ -1859,6 +1861,40 @@ function completionScript(root, shell) {
1859
1861
  return fish(root);
1860
1862
  }
1861
1863
 
1864
+ // src/handlers/auth.ts
1865
+ import { hostname } from "node:os";
1866
+
1867
+ // ../core/src/cli-setup.ts
1868
+ var SETUP_CODE_PREFIX = "ycs1.";
1869
+ function decodeSetupCode(code) {
1870
+ const trimmed = code.trim();
1871
+ if (!trimmed.startsWith(SETUP_CODE_PREFIX))
1872
+ return null;
1873
+ let parsed;
1874
+ try {
1875
+ parsed = JSON.parse(Buffer.from(trimmed.slice(SETUP_CODE_PREFIX.length), "base64url").toString("utf8"));
1876
+ } catch {
1877
+ return null;
1878
+ }
1879
+ if (typeof parsed !== "object" || parsed === null)
1880
+ return null;
1881
+ const { u, c, s, f } = parsed;
1882
+ if (typeof u !== "string" || typeof c !== "string" || typeof s !== "string")
1883
+ return null;
1884
+ if (f !== "device" && f !== "loopback")
1885
+ return null;
1886
+ try {
1887
+ const url = new URL(u);
1888
+ if (url.protocol !== "https:" && url.hostname !== "localhost")
1889
+ return null;
1890
+ } catch {
1891
+ return null;
1892
+ }
1893
+ if (c === "" || s === "")
1894
+ return null;
1895
+ return { url: u.replace(/\/+$/, ""), oauthClientId: c, oauthClientSecret: s, flow: f };
1896
+ }
1897
+
1862
1898
  // src/handlers/auth.ts
1863
1899
  function str(value) {
1864
1900
  return typeof value === "string" ? value : undefined;
@@ -1869,11 +1905,44 @@ function keyHint(key) {
1869
1905
  async function storedKey(context) {
1870
1906
  return readEnv("API_KEY", context.env) ?? await context.secrets.get(secretAccount.apiKey(context.config.profile));
1871
1907
  }
1908
+ async function requestCliToken(context, url, idToken) {
1909
+ let response;
1910
+ try {
1911
+ response = await context.fetch(`${url}/api/auth/iap/cli-token`, {
1912
+ method: "POST",
1913
+ headers: {
1914
+ Authorization: `Bearer ${idToken}`,
1915
+ "Content-Type": "application/json",
1916
+ Accept: "application/json"
1917
+ },
1918
+ body: JSON.stringify({ organization: context.config.org, host: hostname() })
1919
+ });
1920
+ } catch (error) {
1921
+ throw new CliError(`Could not reach ${new URL(url).host}: ${error instanceof Error ? error.message : "error"}`, "network_error", EXIT.tempfail);
1922
+ }
1923
+ if (response.status === 404)
1924
+ return null;
1925
+ const body = await response.json().catch(() => ({}));
1926
+ if (!response.ok || body.token === undefined || body.organization === undefined) {
1927
+ throw new CliError(`Signed in, but no token was issued: ${body.error ?? `HTTP ${response.status}`}`, body.code ?? "token_refused");
1928
+ }
1929
+ return { token: body.token, organization: body.organization };
1930
+ }
1872
1931
  var loginHandler = async (context) => {
1873
1932
  const profile = context.config.profile;
1874
1933
  assertProfileName(profile);
1875
1934
  const file = await context.configStore.load();
1876
1935
  const stored = { ...file.profiles[profile] ?? {} };
1936
+ const setupFlag = str(context.flags.setup);
1937
+ const setup = setupFlag === undefined ? undefined : decodeSetupCode(setupFlag);
1938
+ if (setup === null) {
1939
+ throw new UsageError("That setup code could not be read. Copy the command again from Settings → Account → Command line.");
1940
+ }
1941
+ if (setup !== undefined) {
1942
+ stored.url = setup.url;
1943
+ stored.oauthClientId = setup.oauthClientId;
1944
+ stored.oauthFlow = setup.flow;
1945
+ }
1877
1946
  const url = str(context.flags.url);
1878
1947
  if (url !== undefined) {
1879
1948
  try {
@@ -1907,12 +1976,12 @@ var loginHandler = async (context) => {
1907
1976
  } else if (clientId === undefined) {
1908
1977
  context.notice("No OAuth client is configured, so the IAP sign-in is skipped. That is right for an instance that is not behind IAP. For one that is, a person must first create the Desktop OAuth client (see docs/cli/work-management.md), then run `ycadence login --oauth-client-id <id> --iap-audience <IAP client id>`.");
1909
1978
  } else {
1910
- const clientSecret = context.env.YCADENCE_OAUTH_CLIENT_SECRET || await context.secrets.get(secretAccount.oauthClientSecret(profile));
1979
+ const clientSecret = context.env.YCADENCE_OAUTH_CLIENT_SECRET || setup?.oauthClientSecret || await context.secrets.get(secretAccount.oauthClientSecret(profile));
1911
1980
  if (clientSecret === undefined || clientSecret === "") {
1912
- throw new ConfigError("The OAuth client secret is missing. Set YCADENCE_OAUTH_CLIENT_SECRET for this login; it is then kept in the credential store.");
1981
+ throw new ConfigError("The OAuth client secret is missing. Copy the sign-in command from Settings → Account → Command line, or set YCADENCE_OAUTH_CLIENT_SECRET for this login; it is then kept in the credential store.");
1913
1982
  }
1914
1983
  const client = oauthClientFrom({ clientId, clientSecret, env: context.env });
1915
- const tokens = context.flags.device === true ? await deviceLogin({
1984
+ const tokens = context.flags.device === true || stored.oauthFlow === "device" ? await deviceLogin({
1916
1985
  client,
1917
1986
  fetch: context.fetch,
1918
1987
  print: (message) => context.notice(message),
@@ -1943,6 +2012,19 @@ var loginHandler = async (context) => {
1943
2012
  await context.secrets.set(secretAccount.identityToken(profile), JSON.stringify({ token: identity.idToken, expiresAt: identity.expiresAt }));
1944
2013
  }
1945
2014
  signedIn = true;
2015
+ if (apiKey === undefined && identity !== undefined && stored.url !== undefined) {
2016
+ const issued = await requestCliToken(context, stored.url, identity.idToken);
2017
+ if (issued === null) {
2018
+ context.notice("This server cannot issue CLI tokens yet. Create a personal token under Settings → Account → API tokens, then run `ycadence login --api-key-stdin` with it on stdin.");
2019
+ } else {
2020
+ apiKey = issued.token;
2021
+ if (stored.org === undefined) {
2022
+ stored.org = issued.organization;
2023
+ file.profiles[profile] = stored;
2024
+ await context.configStore.save(file);
2025
+ }
2026
+ }
2027
+ }
1946
2028
  }
1947
2029
  if (apiKey !== undefined)
1948
2030
  await context.secrets.set(secretAccount.apiKey(profile), apiKey);
@@ -2144,7 +2226,7 @@ var configPathHandler = async (context) => {
2144
2226
  };
2145
2227
 
2146
2228
  // src/version.ts
2147
- var CLI_VERSION = "0.2.1";
2229
+ var CLI_VERSION = "0.3.0";
2148
2230
 
2149
2231
  // src/handlers/doctor.ts
2150
2232
  function legacyNote(name, env) {
@@ -2812,6 +2894,12 @@ var legacyProjectArg = {
2812
2894
  description: "Project id (the older positional form of --project)"
2813
2895
  };
2814
2896
  var LOGIN_FLAGS = [
2897
+ {
2898
+ name: "setup",
2899
+ type: "string",
2900
+ placeholder: "<code>",
2901
+ description: "Setup code from Settings → Account → Command line (URL, OAuth client, flow)"
2902
+ },
2815
2903
  { name: "url", type: "string", placeholder: "<url>", description: "The instance URL" },
2816
2904
  { name: "api-key-stdin", type: "boolean", description: "Read the API key from stdin" },
2817
2905
  { name: "device", type: "boolean", description: "Use the device flow (no local browser)" },
@@ -3301,10 +3389,11 @@ var TREE = {
3301
3389
  {
3302
3390
  name: "login",
3303
3391
  summary: "Save a profile, sign in for IAP, and store an API key",
3304
- description: "Signs in with Google (loopback, or --device) against the configured Desktop OAuth client for an IAP identity token, when one is configured. The API key is read from stdin and kept in the OS keychain.",
3392
+ description: "Behind IAP, run the command from Settings → Account → Command line: `--setup` carries the URL and OAuth client, the device flow signs you in to Google, and the instance issues this machine a personal token. Elsewhere, pass --url and an API key on stdin. Credentials are kept in the OS keychain.",
3305
3393
  needsClient: false,
3306
3394
  flags: LOGIN_FLAGS,
3307
3395
  examples: [
3396
+ "ycadence login --setup ycs1.…",
3308
3397
  "ycadence login --url https://ycadence.example.com --api-key-stdin < key.txt",
3309
3398
  "ycadence login --profile yarlis --oauth-client-id <desktop client id> --iap-audience <iap client id>"
3310
3399
  ],
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ycadence/cli",
3
- "version": "0.2.1",
3
+ "version": "0.3.0",
4
4
  "description": "YCadence from the command line: work items, states, cycles and imports.",
5
5
  "keywords": [
6
6
  "ycadence",