From 4a187e0811ef656eda0e01b7304363daab627415 Mon Sep 17 00:00:00 2001 From: Martin Helmich Date: Wed, 24 Jan 2024 18:23:40 +0100 Subject: [PATCH 1/3] Make app-installation flag project-scoped, so that short IDs work(-ish) --- src/ExtendedBaseCommand.tsx | 2 +- src/lib/app/flags.tsx | 20 +++++++++++++++++--- src/rendering/react/RenderBaseCommand.tsx | 2 +- 3 files changed, 19 insertions(+), 5 deletions(-) diff --git a/src/ExtendedBaseCommand.tsx b/src/ExtendedBaseCommand.tsx index 06571932..8f02a627 100644 --- a/src/ExtendedBaseCommand.tsx +++ b/src/ExtendedBaseCommand.tsx @@ -25,7 +25,7 @@ export abstract class ExtendedBaseCommand< } public async withAppInstallationId( - command: CommandType<"installation"> | "flag" | "arg", + command: CommandType<"installation" | "project"> | "flag" | "arg", ): Promise { return withAppInstallationId( this.apiClient, diff --git a/src/lib/app/flags.tsx b/src/lib/app/flags.tsx index fa6d670c..efe4e196 100644 --- a/src/lib/app/flags.tsx +++ b/src/lib/app/flags.tsx @@ -6,7 +6,7 @@ import React from "react"; import { Text } from "ink"; import { assertStatus } from "@mittwald/api-client-commons"; import { ProcessRenderer } from "../../rendering/process/process.js"; -import { projectFlags } from "../project/flags.js"; +import { makeProjectFlagSet, projectFlags } from "../project/flags.js"; import { ProcessFlags, processFlags, @@ -19,13 +19,27 @@ import { OutputFlags, } from "@oclif/core/lib/interfaces/parser.js"; import { generatePasswordWithSpecialChars } from "../password.js"; -import { makeFlagSet } from "../context_flags.js"; +import { isUuid } from "../../Helpers.js"; export const { flags: appInstallationFlags, args: appInstallationArgs, withId: withAppInstallationId, -} = makeFlagSet("installation", "i", { displayName: "app installation" }); +} = makeProjectFlagSet("installation", "i", { + displayName: "app installation", + normalize: async (apiClient, projectId, id): Promise => { + if (isUuid(id)) { + return id; + } + + const appInstallations = await apiClient.app.listAppinstallations({ + projectId, + }); + assertStatus(appInstallations, 200); + + return appInstallations.data.find((inst) => inst.shortId === id)?.id ?? id; + }, +}); export type AvailableFlagName = keyof AvailableFlags; diff --git a/src/rendering/react/RenderBaseCommand.tsx b/src/rendering/react/RenderBaseCommand.tsx index bad9dc84..e5f8e517 100644 --- a/src/rendering/react/RenderBaseCommand.tsx +++ b/src/rendering/react/RenderBaseCommand.tsx @@ -77,7 +77,7 @@ export abstract class RenderBaseCommand< protected abstract render(): ReactNode; protected useAppInstallationId( - command: CommandType<"installation"> | "flag" | "arg", + command: CommandType<"installation" | "project"> | "flag" | "arg", ): string { return usePromise(() => this.withAppInstallationId(command), []); } From 1362242f08a484517269da63ecbcbede25219d9e Mon Sep 17 00:00:00 2001 From: Martin Helmich Date: Thu, 25 Jan 2024 13:35:19 +0100 Subject: [PATCH 2/3] Add support for storing installation ID in context --- src/commands/context/set.ts | 15 +++++++++++++++ src/lib/app/flags.tsx | 29 ++++++++++++++++++----------- src/lib/context.ts | 3 +++ src/lib/project/flags.ts | 6 ++++-- 4 files changed, 40 insertions(+), 13 deletions(-) diff --git a/src/commands/context/set.ts b/src/commands/context/set.ts index 89caa810..82b382ba 100644 --- a/src/commands/context/set.ts +++ b/src/commands/context/set.ts @@ -6,6 +6,7 @@ import { normalizeProjectIdToUuid, normalizeServerIdToUuid, } from "../../Helpers.js"; +import { normalizeAppInstallationId } from "../../lib/app/flags.js"; export class Set extends BaseCommand { static summary = "Set context values for the current project, org or server"; @@ -21,6 +22,10 @@ export class Set extends BaseCommand { "org-id": Flags.string({ description: "ID or short ID of an organization", }), + "installation-id": Flags.string({ + description: "ID or short ID of an app installation", + aliases: ["app-id", "app-installation-id"], + }), }; public async run(): Promise { @@ -53,5 +58,15 @@ export class Set extends BaseCommand { await ctx.setOrgId(orgId); this.log(`Set organization ID to ${orgId}`); } + + if (flags["installation-id"]) { + const installationId = await normalizeAppInstallationId( + this.apiClient, + (await ctx.projectId())?.value ?? "", + flags["installation-id"], + ); + await ctx.setAppInstallationId(installationId); + this.log(`Set installation ID to ${installationId}`); + } } } diff --git a/src/lib/app/flags.tsx b/src/lib/app/flags.tsx index efe4e196..bd00f40d 100644 --- a/src/lib/app/flags.tsx +++ b/src/lib/app/flags.tsx @@ -27,19 +27,26 @@ export const { withId: withAppInstallationId, } = makeProjectFlagSet("installation", "i", { displayName: "app installation", - normalize: async (apiClient, projectId, id): Promise => { - if (isUuid(id)) { - return id; - } + normalize: normalizeAppInstallationId, + supportsContext: true, +}); + +export async function normalizeAppInstallationId( + apiClient: MittwaldAPIV2Client, + projectId: string, + id: string, +): Promise { + if (isUuid(id)) { + return id; + } - const appInstallations = await apiClient.app.listAppinstallations({ - projectId, - }); - assertStatus(appInstallations, 200); + const appInstallations = await apiClient.app.listAppinstallations({ + projectId, + }); + assertStatus(appInstallations, 200); - return appInstallations.data.find((inst) => inst.shortId === id)?.id ?? id; - }, -}); + return appInstallations.data.find((inst) => inst.shortId === id)?.id ?? id; +} export type AvailableFlagName = keyof AvailableFlags; diff --git a/src/lib/context.ts b/src/lib/context.ts index fefe2c29..4155d093 100644 --- a/src/lib/context.ts +++ b/src/lib/context.ts @@ -79,8 +79,11 @@ export class Context { public setProjectId = (id: string) => this.setContextValue("project-id", id); public setServerId = (id: string) => this.setContextValue("server-id", id); public setOrgId = (id: string) => this.setContextValue("org-id", id); + public setAppInstallationId = (id: string) => + this.setContextValue("installation-id", id); public projectId = () => this.getContextValue("project-id"); public serverId = () => this.getContextValue("server-id"); public orgId = () => this.getContextValue("org-id"); + public appInstallationId = () => this.getContextValue("installation-id"); } diff --git a/src/lib/project/flags.ts b/src/lib/project/flags.ts index 640fee14..06173175 100644 --- a/src/lib/project/flags.ts +++ b/src/lib/project/flags.ts @@ -29,6 +29,7 @@ export type ProjectFlagSetOpts = { normalize: SubNormalizeFn; shortIDName: string; displayName: string; + supportsContext: boolean; }; export function makeProjectFlagSet( @@ -40,6 +41,7 @@ export function makeProjectFlagSet( normalize = (_1, _2, id) => id, shortIDName = "short ID", displayName = name, + supportsContext = false, } = opts; const flagName: ContextKey = `${name}-id`; @@ -47,7 +49,7 @@ export function makeProjectFlagSet( ...projectFlags, [flagName]: Flags.string({ char, - required: true, + required: !supportsContext, summary: `ID or ${shortIDName} of a ${displayName}`, description: `May contain a ${shortIDName} or a full ID of a ${displayName}.`, default: undefined, @@ -57,7 +59,7 @@ export function makeProjectFlagSet( const args = { [flagName]: Args.string({ description: `ID or ${shortIDName} of a ${displayName}`, - required: true, + required: !supportsContext, }), } as ContextArgs; From 300562af2d4a6714fe8862e6cadf41797750b42d Mon Sep 17 00:00:00 2001 From: Martin Helmich Date: Thu, 25 Jan 2024 14:05:42 +0100 Subject: [PATCH 3/3] Update README --- README.md | 22 +++++++++++++++------- src/lib/project/flags.ts | 19 ++++++++++++++++--- 2 files changed, 31 insertions(+), 10 deletions(-) diff --git a/README.md b/README.md index fd7a73c4..e3f2feeb 100644 --- a/README.md +++ b/README.md @@ -1784,12 +1784,13 @@ Set context values for the current project, org or server ``` USAGE - $ mw context set [--project-id ] [--server-id ] [--org-id ] + $ mw context set [--project-id ] [--server-id ] [--org-id ] [--installation-id ] FLAGS - --org-id= ID or short ID of an organization - --project-id= ID or short ID of a project - --server-id= ID or short ID of a server + --installation-id= ID or short ID of an app installation + --org-id= ID or short ID of an organization + --project-id= ID or short ID of a project + --server-id= ID or short ID of a server DESCRIPTION Set context values for the current project, org or server @@ -1936,12 +1937,14 @@ Create a new cron job ``` USAGE - $ mw cronjob create --description --interval [-i ] [-q] [--disable] [--email ] - [--url | --command ] [--interpreter ] + $ mw cronjob create --description --interval [-p ] [-i ] [-q] [--disable] [--email + ] [--url | --command ] [--interpreter ] FLAGS -i, --installation-id= ID or short ID of an app installation; this flag is optional if a default app installation is set in the context + -p, --project-id= ID or short ID of a project; this flag is optional if a default project is set in the + context -q, --quiet suppress process output and only display a machine-readable summary. --command= Command to execute for the cron job; either this or `--url` is required. --description= (required) Description of the cron job @@ -1956,10 +1959,15 @@ FLAG DESCRIPTIONS ID or short ID of an app installation; this flag is optional if a default app installation is set in the context - May contain a short ID or a full ID of an app installation; you can also use the "mw context set + May contain a short ID or a full ID of an app installation.; you can also use the "mw context set --installation-id=" command to persistently set a default app installation for all commands that accept this flag. + -p, --project-id= ID or short ID of a project; this flag is optional if a default project is set in the context + + May contain a short ID or a full ID of a project; you can also use the "mw context set --project-id=" command + to persistently set a default project for all commands that accept this flag. + -q, --quiet suppress process output and only display a machine-readable summary. This flag controls if you want to see the process output or only a summary. When using mw non-interactively (e.g. in diff --git a/src/lib/project/flags.ts b/src/lib/project/flags.ts index 06173175..8eaedb60 100644 --- a/src/lib/project/flags.ts +++ b/src/lib/project/flags.ts @@ -43,6 +43,7 @@ export function makeProjectFlagSet( displayName = name, supportsContext = false, } = opts; + const article = displayName.match(/^[aeiou]/i) ? "an" : "a"; const flagName: ContextKey = `${name}-id`; const flags = { @@ -50,19 +51,31 @@ export function makeProjectFlagSet( [flagName]: Flags.string({ char, required: !supportsContext, - summary: `ID or ${shortIDName} of a ${displayName}`, - description: `May contain a ${shortIDName} or a full ID of a ${displayName}.`, + summary: `ID or ${shortIDName} of ${article} ${displayName}`, + description: `May contain a ${shortIDName} or a full ID of ${article} ${displayName}.`, default: undefined, }), } as ContextFlags; const args = { [flagName]: Args.string({ - description: `ID or ${shortIDName} of a ${displayName}`, + description: `ID or ${shortIDName} of ${article} ${displayName}`, required: !supportsContext, }), } as ContextArgs; + if (supportsContext) { + flags[ + flagName + ].summary += `; this flag is optional if a default ${displayName} is set in the context`; + flags[ + flagName + ].description += `; you can also use the "<%= config.bin %> context set --${flagName}=" command to persistently set a default ${displayName} for all commands that accept this flag.`; + args[ + flagName + ].description += `; this argument is optional if a default ${displayName} is set in the context`; + } + const idFromArgsOrFlag = ( flags: FlagOutput, args: ArgOutput,