@ycadence/cli 0.2.1 → 0.4.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 +124 -8
- 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
|
@@ -168,7 +168,7 @@ async function* paginate(fetchPage, options = {}) {
|
|
|
168
168
|
}
|
|
169
169
|
|
|
170
170
|
// ../sdk/src/version.ts
|
|
171
|
-
var SDK_VERSION = "0.
|
|
171
|
+
var SDK_VERSION = "0.3.0";
|
|
172
172
|
|
|
173
173
|
// ../sdk/src/client.ts
|
|
174
174
|
var DEFAULT_RETRY = { retries: 2, baseDelayMs: 250, maxDelayMs: 8000 };
|
|
@@ -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) {
|
|
@@ -1683,6 +1685,17 @@ function closingState(states) {
|
|
|
1683
1685
|
}
|
|
1684
1686
|
return done;
|
|
1685
1687
|
}
|
|
1688
|
+
async function resolveTypeId(client, projectId, value) {
|
|
1689
|
+
if (isUuid(value))
|
|
1690
|
+
return value;
|
|
1691
|
+
const { types: types2 } = await client.listTypes(projectId);
|
|
1692
|
+
const named = types2.filter((type) => typeof type.id === "string" && typeof type.name === "string");
|
|
1693
|
+
const match = named.find((type) => type.name.toLowerCase() === value.toLowerCase());
|
|
1694
|
+
if (match === undefined) {
|
|
1695
|
+
throw new CliError(`No work item type "${value}" in this project. Types: ${named.map((type) => type.name).join(", ")}.`, "validation_failed", EXIT.validation);
|
|
1696
|
+
}
|
|
1697
|
+
return match.id;
|
|
1698
|
+
}
|
|
1686
1699
|
var KEY_SCAN_LIMIT = 2000;
|
|
1687
1700
|
async function resolveWorkItemId(client, value, projectId) {
|
|
1688
1701
|
if (isUuid(value))
|
|
@@ -1859,6 +1872,40 @@ function completionScript(root, shell) {
|
|
|
1859
1872
|
return fish(root);
|
|
1860
1873
|
}
|
|
1861
1874
|
|
|
1875
|
+
// src/handlers/auth.ts
|
|
1876
|
+
import { hostname } from "node:os";
|
|
1877
|
+
|
|
1878
|
+
// ../core/src/cli-setup.ts
|
|
1879
|
+
var SETUP_CODE_PREFIX = "ycs1.";
|
|
1880
|
+
function decodeSetupCode(code) {
|
|
1881
|
+
const trimmed = code.trim();
|
|
1882
|
+
if (!trimmed.startsWith(SETUP_CODE_PREFIX))
|
|
1883
|
+
return null;
|
|
1884
|
+
let parsed;
|
|
1885
|
+
try {
|
|
1886
|
+
parsed = JSON.parse(Buffer.from(trimmed.slice(SETUP_CODE_PREFIX.length), "base64url").toString("utf8"));
|
|
1887
|
+
} catch {
|
|
1888
|
+
return null;
|
|
1889
|
+
}
|
|
1890
|
+
if (typeof parsed !== "object" || parsed === null)
|
|
1891
|
+
return null;
|
|
1892
|
+
const { u, c, s, f } = parsed;
|
|
1893
|
+
if (typeof u !== "string" || typeof c !== "string" || typeof s !== "string")
|
|
1894
|
+
return null;
|
|
1895
|
+
if (f !== "device" && f !== "loopback")
|
|
1896
|
+
return null;
|
|
1897
|
+
try {
|
|
1898
|
+
const url = new URL(u);
|
|
1899
|
+
if (url.protocol !== "https:" && url.hostname !== "localhost")
|
|
1900
|
+
return null;
|
|
1901
|
+
} catch {
|
|
1902
|
+
return null;
|
|
1903
|
+
}
|
|
1904
|
+
if (c === "" || s === "")
|
|
1905
|
+
return null;
|
|
1906
|
+
return { url: u.replace(/\/+$/, ""), oauthClientId: c, oauthClientSecret: s, flow: f };
|
|
1907
|
+
}
|
|
1908
|
+
|
|
1862
1909
|
// src/handlers/auth.ts
|
|
1863
1910
|
function str(value) {
|
|
1864
1911
|
return typeof value === "string" ? value : undefined;
|
|
@@ -1869,11 +1916,44 @@ function keyHint(key) {
|
|
|
1869
1916
|
async function storedKey(context) {
|
|
1870
1917
|
return readEnv("API_KEY", context.env) ?? await context.secrets.get(secretAccount.apiKey(context.config.profile));
|
|
1871
1918
|
}
|
|
1919
|
+
async function requestCliToken(context, url, idToken) {
|
|
1920
|
+
let response;
|
|
1921
|
+
try {
|
|
1922
|
+
response = await context.fetch(`${url}/api/auth/iap/cli-token`, {
|
|
1923
|
+
method: "POST",
|
|
1924
|
+
headers: {
|
|
1925
|
+
Authorization: `Bearer ${idToken}`,
|
|
1926
|
+
"Content-Type": "application/json",
|
|
1927
|
+
Accept: "application/json"
|
|
1928
|
+
},
|
|
1929
|
+
body: JSON.stringify({ organization: context.config.org, host: hostname() })
|
|
1930
|
+
});
|
|
1931
|
+
} catch (error) {
|
|
1932
|
+
throw new CliError(`Could not reach ${new URL(url).host}: ${error instanceof Error ? error.message : "error"}`, "network_error", EXIT.tempfail);
|
|
1933
|
+
}
|
|
1934
|
+
if (response.status === 404)
|
|
1935
|
+
return null;
|
|
1936
|
+
const body = await response.json().catch(() => ({}));
|
|
1937
|
+
if (!response.ok || body.token === undefined || body.organization === undefined) {
|
|
1938
|
+
throw new CliError(`Signed in, but no token was issued: ${body.error ?? `HTTP ${response.status}`}`, body.code ?? "token_refused");
|
|
1939
|
+
}
|
|
1940
|
+
return { token: body.token, organization: body.organization };
|
|
1941
|
+
}
|
|
1872
1942
|
var loginHandler = async (context) => {
|
|
1873
1943
|
const profile = context.config.profile;
|
|
1874
1944
|
assertProfileName(profile);
|
|
1875
1945
|
const file = await context.configStore.load();
|
|
1876
1946
|
const stored = { ...file.profiles[profile] ?? {} };
|
|
1947
|
+
const setupFlag = str(context.flags.setup);
|
|
1948
|
+
const setup = setupFlag === undefined ? undefined : decodeSetupCode(setupFlag);
|
|
1949
|
+
if (setup === null) {
|
|
1950
|
+
throw new UsageError("That setup code could not be read. Copy the command again from Settings → Account → Command line.");
|
|
1951
|
+
}
|
|
1952
|
+
if (setup !== undefined) {
|
|
1953
|
+
stored.url = setup.url;
|
|
1954
|
+
stored.oauthClientId = setup.oauthClientId;
|
|
1955
|
+
stored.oauthFlow = setup.flow;
|
|
1956
|
+
}
|
|
1877
1957
|
const url = str(context.flags.url);
|
|
1878
1958
|
if (url !== undefined) {
|
|
1879
1959
|
try {
|
|
@@ -1907,12 +1987,12 @@ var loginHandler = async (context) => {
|
|
|
1907
1987
|
} else if (clientId === undefined) {
|
|
1908
1988
|
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
1989
|
} else {
|
|
1910
|
-
const clientSecret = context.env.YCADENCE_OAUTH_CLIENT_SECRET || await context.secrets.get(secretAccount.oauthClientSecret(profile));
|
|
1990
|
+
const clientSecret = context.env.YCADENCE_OAUTH_CLIENT_SECRET || setup?.oauthClientSecret || await context.secrets.get(secretAccount.oauthClientSecret(profile));
|
|
1911
1991
|
if (clientSecret === undefined || clientSecret === "") {
|
|
1912
|
-
throw new ConfigError("The OAuth client secret is missing.
|
|
1992
|
+
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
1993
|
}
|
|
1914
1994
|
const client = oauthClientFrom({ clientId, clientSecret, env: context.env });
|
|
1915
|
-
const tokens = context.flags.device === true ? await deviceLogin({
|
|
1995
|
+
const tokens = context.flags.device === true || stored.oauthFlow === "device" ? await deviceLogin({
|
|
1916
1996
|
client,
|
|
1917
1997
|
fetch: context.fetch,
|
|
1918
1998
|
print: (message) => context.notice(message),
|
|
@@ -1943,6 +2023,19 @@ var loginHandler = async (context) => {
|
|
|
1943
2023
|
await context.secrets.set(secretAccount.identityToken(profile), JSON.stringify({ token: identity.idToken, expiresAt: identity.expiresAt }));
|
|
1944
2024
|
}
|
|
1945
2025
|
signedIn = true;
|
|
2026
|
+
if (apiKey === undefined && identity !== undefined && stored.url !== undefined) {
|
|
2027
|
+
const issued = await requestCliToken(context, stored.url, identity.idToken);
|
|
2028
|
+
if (issued === null) {
|
|
2029
|
+
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.");
|
|
2030
|
+
} else {
|
|
2031
|
+
apiKey = issued.token;
|
|
2032
|
+
if (stored.org === undefined) {
|
|
2033
|
+
stored.org = issued.organization;
|
|
2034
|
+
file.profiles[profile] = stored;
|
|
2035
|
+
await context.configStore.save(file);
|
|
2036
|
+
}
|
|
2037
|
+
}
|
|
2038
|
+
}
|
|
1946
2039
|
}
|
|
1947
2040
|
if (apiKey !== undefined)
|
|
1948
2041
|
await context.secrets.set(secretAccount.apiKey(profile), apiKey);
|
|
@@ -2144,7 +2237,7 @@ var configPathHandler = async (context) => {
|
|
|
2144
2237
|
};
|
|
2145
2238
|
|
|
2146
2239
|
// src/version.ts
|
|
2147
|
-
var CLI_VERSION = "0.
|
|
2240
|
+
var CLI_VERSION = "0.4.0";
|
|
2148
2241
|
|
|
2149
2242
|
// src/handlers/doctor.ts
|
|
2150
2243
|
function legacyNote(name, env) {
|
|
@@ -2545,6 +2638,12 @@ var WORK_FIELD_FLAGS = {
|
|
|
2545
2638
|
placeholder: "<name|id>",
|
|
2546
2639
|
description: "State, by name or id"
|
|
2547
2640
|
},
|
|
2641
|
+
type: {
|
|
2642
|
+
name: "type",
|
|
2643
|
+
type: "string",
|
|
2644
|
+
placeholder: "<name|id>",
|
|
2645
|
+
description: "Work item type, by name or id"
|
|
2646
|
+
},
|
|
2548
2647
|
parent: {
|
|
2549
2648
|
name: "parent",
|
|
2550
2649
|
type: "string",
|
|
@@ -2677,6 +2776,9 @@ var createWork = async (context) => {
|
|
|
2677
2776
|
if (stateValue !== undefined) {
|
|
2678
2777
|
body.stateId = isUuid(stateValue) ? stateValue : findState(await listStates(client, projectId), stateValue).id;
|
|
2679
2778
|
}
|
|
2779
|
+
const typeValue = str6(context.flags.type);
|
|
2780
|
+
if (typeValue !== undefined)
|
|
2781
|
+
body.typeId = await resolveTypeId(client, projectId, typeValue);
|
|
2680
2782
|
const assignees = list(context.flags.assignee);
|
|
2681
2783
|
for (const assignee of assignees) {
|
|
2682
2784
|
if (!isUuid(assignee)) {
|
|
@@ -2725,6 +2827,10 @@ var updateWork = async (context) => {
|
|
|
2725
2827
|
if (stateValue !== undefined) {
|
|
2726
2828
|
body.stateId = isUuid(stateValue) ? stateValue : findState(await listStates(client, await context.projectId()), stateValue).id;
|
|
2727
2829
|
}
|
|
2830
|
+
const typeValue = str6(context.flags.type);
|
|
2831
|
+
if (typeValue !== undefined) {
|
|
2832
|
+
body.typeId = isUuid(typeValue) ? typeValue : await resolveTypeId(client, await context.projectId(), typeValue);
|
|
2833
|
+
}
|
|
2728
2834
|
if (context.flags["no-parent"] === true)
|
|
2729
2835
|
body.parentId = null;
|
|
2730
2836
|
const parent = str6(context.flags.parent);
|
|
@@ -2737,7 +2843,7 @@ var updateWork = async (context) => {
|
|
|
2737
2843
|
if (targetDate !== undefined)
|
|
2738
2844
|
body.targetDate = targetDate;
|
|
2739
2845
|
if (Object.keys(body).length === 0) {
|
|
2740
|
-
throw new UsageError("Nothing to update. Pass at least one of --title, --description, --priority, --state, --parent, --start-date, --target-date.");
|
|
2846
|
+
throw new UsageError("Nothing to update. Pass at least one of --title, --description, --priority, --state, --type, --parent, --start-date, --target-date.");
|
|
2741
2847
|
}
|
|
2742
2848
|
const version = context.flags["expect-version"];
|
|
2743
2849
|
const updated = await client.updateWorkItem(id, body, context.mutationOptions(version === undefined ? {} : { expectedVersion: version }));
|
|
@@ -2812,6 +2918,12 @@ var legacyProjectArg = {
|
|
|
2812
2918
|
description: "Project id (the older positional form of --project)"
|
|
2813
2919
|
};
|
|
2814
2920
|
var LOGIN_FLAGS = [
|
|
2921
|
+
{
|
|
2922
|
+
name: "setup",
|
|
2923
|
+
type: "string",
|
|
2924
|
+
placeholder: "<code>",
|
|
2925
|
+
description: "Setup code from Settings → Account → Command line (URL, OAuth client, flow)"
|
|
2926
|
+
},
|
|
2815
2927
|
{ name: "url", type: "string", placeholder: "<url>", description: "The instance URL" },
|
|
2816
2928
|
{ name: "api-key-stdin", type: "boolean", description: "Read the API key from stdin" },
|
|
2817
2929
|
{ name: "device", type: "boolean", description: "Use the device flow (no local browser)" },
|
|
@@ -2924,6 +3036,7 @@ var TREE = {
|
|
|
2924
3036
|
WORK_FIELD_FLAGS.descriptionFile,
|
|
2925
3037
|
WORK_FIELD_FLAGS.priority,
|
|
2926
3038
|
WORK_FIELD_FLAGS.state,
|
|
3039
|
+
WORK_FIELD_FLAGS.type,
|
|
2927
3040
|
{
|
|
2928
3041
|
name: "assignee",
|
|
2929
3042
|
type: "string",
|
|
@@ -2944,7 +3057,8 @@ var TREE = {
|
|
|
2944
3057
|
],
|
|
2945
3058
|
examples: [
|
|
2946
3059
|
'ycadence work create --project <projectId> --title "Fix login redirect" --priority high',
|
|
2947
|
-
'ycadence work create --title "Spike" --description-file notes.md --idempotency-key spike-1'
|
|
3060
|
+
'ycadence work create --title "Spike" --description-file notes.md --idempotency-key spike-1',
|
|
3061
|
+
'ycadence work create --project <projectId> --title "Pick a billing provider" --type Decision --label decisions'
|
|
2948
3062
|
],
|
|
2949
3063
|
run: createWork
|
|
2950
3064
|
},
|
|
@@ -2959,6 +3073,7 @@ var TREE = {
|
|
|
2959
3073
|
WORK_FIELD_FLAGS.descriptionFile,
|
|
2960
3074
|
WORK_FIELD_FLAGS.priority,
|
|
2961
3075
|
WORK_FIELD_FLAGS.state,
|
|
3076
|
+
WORK_FIELD_FLAGS.type,
|
|
2962
3077
|
WORK_FIELD_FLAGS.parent,
|
|
2963
3078
|
{ name: "no-parent", type: "boolean", description: "Remove the parent" },
|
|
2964
3079
|
WORK_FIELD_FLAGS.startDate,
|
|
@@ -3301,10 +3416,11 @@ var TREE = {
|
|
|
3301
3416
|
{
|
|
3302
3417
|
name: "login",
|
|
3303
3418
|
summary: "Save a profile, sign in for IAP, and store an API key",
|
|
3304
|
-
description: "
|
|
3419
|
+
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
3420
|
needsClient: false,
|
|
3306
3421
|
flags: LOGIN_FLAGS,
|
|
3307
3422
|
examples: [
|
|
3423
|
+
"ycadence login --setup ycs1.…",
|
|
3308
3424
|
"ycadence login --url https://ycadence.example.com --api-key-stdin < key.txt",
|
|
3309
3425
|
"ycadence login --profile yarlis --oauth-client-id <desktop client id> --iap-audience <iap client id>"
|
|
3310
3426
|
],
|