@ycadence/cli 0.2.0 → 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.
- package/README.md +22 -3
- package/dist/bin.js +102 -7
- 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
|
-
##
|
|
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.
|
|
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.
|
|
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.
|
|
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: "
|
|
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
|
],
|
|
@@ -3591,8 +3680,12 @@ ${text}`);
|
|
|
3591
3680
|
globals = options;
|
|
3592
3681
|
const secrets = context.secrets ?? (directory === undefined ? lazySecretStore(async () => detectSecretStore({ directory: undefined, env: { YCADENCE_CREDENTIAL_STORE: "file" } })) : lazySecretStore(() => detectSecretStore({ directory, env: context.env })));
|
|
3593
3682
|
const notice = (message) => {
|
|
3594
|
-
if (
|
|
3683
|
+
if (options.quiet)
|
|
3684
|
+
return;
|
|
3685
|
+
if (context.notice === undefined)
|
|
3595
3686
|
stderr.push(message);
|
|
3687
|
+
else
|
|
3688
|
+
context.notice(message);
|
|
3596
3689
|
};
|
|
3597
3690
|
const debug = (message) => {
|
|
3598
3691
|
if (options.verbose)
|
|
@@ -3767,7 +3860,9 @@ var result = await run(process.argv.slice(2), {
|
|
|
3767
3860
|
readStdin,
|
|
3768
3861
|
isTTY: process.stdin.isTTY === true && process.stderr.isTTY === true,
|
|
3769
3862
|
confirm,
|
|
3770
|
-
openUrl
|
|
3863
|
+
openUrl,
|
|
3864
|
+
notice: (message) => process.stderr.write(`${message}
|
|
3865
|
+
`)
|
|
3771
3866
|
});
|
|
3772
3867
|
if (result.stdout !== "")
|
|
3773
3868
|
process.stdout.write(`${result.stdout}
|