diff --git a/packages/cli/package.json b/packages/cli/package.json index 88de858f9..8b2dd7cc1 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -50,6 +50,7 @@ "@osdk/monorepo.tsup": "workspace:~", "@osdk/shared.net.errors": "workspace:~", "@osdk/shared.net.fetch": "workspace:~", + "@osdk/widget-api.unstable": "workspace:~", "@types/archiver": "^6.0.2", "@types/ngeohash": "^0.6.8", "@types/node": "^18.0.0", diff --git a/packages/cli/src/cli.ts b/packages/cli/src/cli.ts index 49249a817..7bebca2f7 100644 --- a/packages/cli/src/cli.ts +++ b/packages/cli/src/cli.ts @@ -21,7 +21,7 @@ import { consola } from "consola"; import type { Argv } from "yargs"; import auth from "./commands/auth/index.js"; import site from "./commands/site/index.js"; -import { logConfigFileMiddleware } from "./yargs/logConfigFileMiddleware.js"; +import widget from "./commands/widget/index.js"; export async function cli(args: string[] = process.argv) { consola.info( @@ -33,7 +33,6 @@ export async function cli(args: string[] = process.argv) { // Special handling where failures happen before yargs does its error handling within .fail try { return await base - .middleware(logConfigFileMiddleware) .command(site) .command({ command: "unstable", @@ -43,6 +42,7 @@ export async function cli(args: string[] = process.argv) { return argv .command(typescript) .command(auth) + .command(widget) .demandCommand(); }, handler: (_args) => {}, diff --git a/packages/cli/src/commands/site/deploy/index.ts b/packages/cli/src/commands/site/deploy/index.ts index 0cc4836b7..7b1944ab8 100644 --- a/packages/cli/src/commands/site/deploy/index.ts +++ b/packages/cli/src/commands/site/deploy/index.ts @@ -17,13 +17,12 @@ import { isValidSemver, YargsCheckError } from "@osdk/cli.common"; import type { AutoVersionConfigType, - LoadedFoundryConfig, SiteConfig, } from "@osdk/foundry-config-json"; import type { CommandModule } from "yargs"; import configLoader from "../../../util/configLoader.js"; import type { CommonSiteArgs } from "../CommonSiteArgs.js"; -import { logDeployCommandConfigFileOverride } from "./logDeployCommandConfigFileOverride.js"; +import { logSiteDeployCommandConfigFileOverride } from "./logSiteDeployCommandConfigFileOverride.js"; import type { SiteDeployArgs } from "./SiteDeployArgs.js"; const command: CommandModule< @@ -33,8 +32,7 @@ const command: CommandModule< command: "deploy", describe: "Deploy a new site version", builder: async (argv) => { - const config: LoadedFoundryConfig<"site"> | undefined = - await configLoader(); + const config = await configLoader("site"); const siteConfig: SiteConfig | undefined = config?.foundryConfig.site; const directory = siteConfig?.directory; const autoVersion = siteConfig?.autoVersion; @@ -134,7 +132,6 @@ const command: CommandModule< } const gitTagPrefixValue = args.gitTagPrefix ?? gitTagPrefix; - // Future proofing for when we support other autoVersion types if (gitTagPrefixValue != null && autoVersionType !== "git-describe") { throw new YargsCheckError( `--gitTagPrefix is only supported when --autoVersion=git-describe`, @@ -155,7 +152,7 @@ const command: CommandModule< return true; }).middleware((args) => - logDeployCommandConfigFileOverride( + logSiteDeployCommandConfigFileOverride( args, siteConfig, ) diff --git a/packages/cli/src/commands/site/deploy/logDeployCommandConfigFileOverride.ts b/packages/cli/src/commands/site/deploy/logSiteDeployCommandConfigFileOverride.ts similarity index 96% rename from packages/cli/src/commands/site/deploy/logDeployCommandConfigFileOverride.ts rename to packages/cli/src/commands/site/deploy/logSiteDeployCommandConfigFileOverride.ts index 1ab71c189..619b7c823 100644 --- a/packages/cli/src/commands/site/deploy/logDeployCommandConfigFileOverride.ts +++ b/packages/cli/src/commands/site/deploy/logSiteDeployCommandConfigFileOverride.ts @@ -19,7 +19,7 @@ import { consola } from "consola"; import type { Arguments } from "yargs"; import type { SiteDeployArgs } from "./SiteDeployArgs.js"; -export async function logDeployCommandConfigFileOverride( +export async function logSiteDeployCommandConfigFileOverride( args: Arguments, config: SiteConfig | undefined, ) { diff --git a/packages/cli/src/commands/site/index.ts b/packages/cli/src/commands/site/index.ts index fc09182d2..43d0f8edb 100644 --- a/packages/cli/src/commands/site/index.ts +++ b/packages/cli/src/commands/site/index.ts @@ -16,10 +16,10 @@ import type { CliCommonArgs } from "@osdk/cli.common"; import { YargsCheckError } from "@osdk/cli.common"; -import type { LoadedFoundryConfig } from "@osdk/foundry-config-json"; import type { CommandModule } from "yargs"; import type { ThirdPartyAppRid } from "../../net/ThirdPartyAppRid.js"; import configLoader from "../../util/configLoader.js"; +import { logConfigFileMiddleware } from "../../yargs/logConfigFileMiddleware.js"; import type { CommonSiteArgs } from "./CommonSiteArgs.js"; import deploy from "./deploy/index.js"; import { logSiteCommandConfigFileOverride } from "./logSiteCommandConfigFileOverride.js"; @@ -29,8 +29,7 @@ const command: CommandModule = { command: "site", describe: "Manage your site", builder: async (argv) => { - const config: LoadedFoundryConfig<"site"> | undefined = - await configLoader(); + const config = await configLoader("site"); const application = config?.foundryConfig.site.application; const foundryUrl = config?.foundryConfig.foundryUrl; return argv @@ -74,9 +73,10 @@ const command: CommandModule = { } return true; }) - .middleware((args) => - logSiteCommandConfigFileOverride(args, config?.foundryConfig) - ) + .middleware((args) => { + logConfigFileMiddleware("site"); + logSiteCommandConfigFileOverride(args, config?.foundryConfig); + }) .demandCommand(); }, handler: async (args) => {}, diff --git a/packages/cli/src/commands/site/version/delete/index.ts b/packages/cli/src/commands/site/version/delete/index.ts index 5ae0686e1..59192bd95 100644 --- a/packages/cli/src/commands/site/version/delete/index.ts +++ b/packages/cli/src/commands/site/version/delete/index.ts @@ -29,7 +29,7 @@ const command: CommandModule< .positional("version", { type: "string", demandOption: true, - description: "Version to set as live", + description: "Version to delete", }) .option("yes", { alias: "y", diff --git a/packages/cli/src/commands/widget/CommonWidgetArgs.ts b/packages/cli/src/commands/widget/CommonWidgetArgs.ts new file mode 100644 index 000000000..bb652800f --- /dev/null +++ b/packages/cli/src/commands/widget/CommonWidgetArgs.ts @@ -0,0 +1,25 @@ +/* + * Copyright 2023 Palantir Technologies, Inc. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import type { CliCommonArgs } from "@osdk/cli.common"; +import type { WidgetRid } from "../../net/WidgetRid.js"; + +export interface CommonWidgetArgs extends CliCommonArgs { + rid: WidgetRid; + foundryUrl: string; + token?: string; + tokenFile?: string; +} diff --git a/packages/cli/src/commands/widget/deploy/WidgetDeployArgs.ts b/packages/cli/src/commands/widget/deploy/WidgetDeployArgs.ts new file mode 100644 index 000000000..f05c4daf7 --- /dev/null +++ b/packages/cli/src/commands/widget/deploy/WidgetDeployArgs.ts @@ -0,0 +1,21 @@ +/* + * Copyright 2023 Palantir Technologies, Inc. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import type { CommonWidgetArgs } from "../CommonWidgetArgs.js"; + +export interface WidgetDeployArgs extends CommonWidgetArgs { + directory: string; +} diff --git a/packages/cli/src/commands/widget/deploy/index.ts b/packages/cli/src/commands/widget/deploy/index.ts new file mode 100644 index 000000000..88add6ec4 --- /dev/null +++ b/packages/cli/src/commands/widget/deploy/index.ts @@ -0,0 +1,58 @@ +/* + * Copyright 2023 Palantir Technologies, Inc. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import type { WidgetConfig } from "@osdk/foundry-config-json"; +import type { CommandModule } from "yargs"; +import configLoader from "../../../util/configLoader.js"; +import type { CommonWidgetArgs } from "../CommonWidgetArgs.js"; +import { logWidgetDeployCommandConfigFileOverride } from "./logWidgetDeployCommandConfigFileOverride.js"; +import type { WidgetDeployArgs } from "./WidgetDeployArgs.js"; + +const command: CommandModule< + CommonWidgetArgs, + WidgetDeployArgs +> = { + command: "deploy", + describe: "Deploy a new widget version", + builder: async (argv) => { + const config = await configLoader("widget"); + const widgetConfig: WidgetConfig | undefined = config?.foundryConfig.widget; + const directory = widgetConfig?.directory; + + return argv + .options({ + directory: { + type: "string", + description: "Directory containing widget files", + ...directory + ? { default: directory } + : { demandOption: true }, + }, + }) + .group( + ["directory"], + "Deploy Options", + ).middleware((args) => + logWidgetDeployCommandConfigFileOverride(args, widgetConfig) + ); + }, + handler: async (args) => { + const command = await import("./widgetDeployCommand.mjs"); + await command.default(args); + }, +}; + +export default command; diff --git a/packages/cli/src/commands/widget/deploy/logWidgetDeployCommandConfigFileOverride.ts b/packages/cli/src/commands/widget/deploy/logWidgetDeployCommandConfigFileOverride.ts new file mode 100644 index 000000000..f7583668b --- /dev/null +++ b/packages/cli/src/commands/widget/deploy/logWidgetDeployCommandConfigFileOverride.ts @@ -0,0 +1,31 @@ +/* + * Copyright 2023 Palantir Technologies, Inc. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import type { WidgetConfig } from "@osdk/foundry-config-json"; +import { consola } from "consola"; +import type { Arguments } from "yargs"; +import type { WidgetDeployArgs } from "./WidgetDeployArgs.js"; + +export async function logWidgetDeployCommandConfigFileOverride( + args: Arguments, + config: WidgetConfig | undefined, +) { + if (config?.directory != null && args.directory !== config.directory) { + consola.debug( + `Overriding "directory" from config file with ${args.directory}`, + ); + } +} diff --git a/packages/cli/src/commands/widget/deploy/widgetDeployCommand.mts b/packages/cli/src/commands/widget/deploy/widgetDeployCommand.mts new file mode 100644 index 000000000..ccbd5f46e --- /dev/null +++ b/packages/cli/src/commands/widget/deploy/widgetDeployCommand.mts @@ -0,0 +1,170 @@ +/* + * Copyright 2023 Palantir Technologies, Inc. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { consola } from "consola"; + +import { createInternalClientContext } from "#net"; +import { ExitProcessError } from "@osdk/cli.common"; +import type { WidgetManifest } from "@osdk/widget-api.unstable"; +import { MANIFEST_FILE_LOCATION } from "@osdk/widget-api.unstable"; +import archiver from "archiver"; +import * as fs from "node:fs"; +import path from "node:path"; +import { Readable } from "node:stream"; +import prettyBytes from "pretty-bytes"; +import { createFetch } from "../../../net/createFetch.mjs"; +import type { InternalClientContext } from "../../../net/internalClientContext.mjs"; +import type { WidgetRid } from "../../../net/WidgetRid.js"; +import { loadToken } from "../../../util/token.js"; +import type { WidgetDeployArgs } from "./WidgetDeployArgs.js"; + +export default async function widgetDeployCommand( + { + rid, + foundryUrl, + directory, + token, + tokenFile, + }: WidgetDeployArgs, +) { + const loadedToken = await loadToken(token, tokenFile); + const tokenProvider = () => loadedToken; + const clientCtx = createInternalClientContext(foundryUrl, tokenProvider); + + consola.debug( + `Using directory for widget files: "${path.resolve(directory)}`, + ); + const stat = await fs.promises.stat(directory); + if (!stat.isDirectory()) { + throw new ExitProcessError( + 2, + "Specified path exists but is not a directory", + ); + } + + const widgetVersion = await findWidgetVersion(rid, directory); + + consola.start("Zipping widget files"); + const archive = archiver("zip").directory(directory, false); + logArchiveStats(archive); + + consola.start("Uploading widget files"); + await Promise.all([ + uploadVersion( + clientCtx, + rid, + widgetVersion, + Readable.toWeb(archive) as ReadableStream, // This cast is because the dom fetch doesn't align type wise with streams + ), + archive.finalize(), + ]); + consola.success("Upload complete"); + + consola.start("Publishing widget manifest"); + await publishManifest(clientCtx, rid, widgetVersion); + consola.success(`Deployed ${widgetVersion} successfully`); +} + +async function findWidgetVersion( + rid: WidgetRid, + directory: string, +): Promise { + try { + const manifestContent = await fs.promises.readFile( + path.resolve(directory, MANIFEST_FILE_LOCATION), + "utf8", + ); + const manifest: WidgetManifest = JSON.parse(manifestContent); + const widget = Object.values(manifest.widgets).find(w => w.rid === rid); + if (widget == null) { + throw new Error(`Unable to find widget ${rid} in manifest`); + } + if (widget.version == null) { + throw new Error(`Found widget ${rid} in manifest but missing version`); + } + return widget.version; + } catch (e) { + throw new ExitProcessError( + 2, + `Unable to process manifest at ${MANIFEST_FILE_LOCATION}${ + e instanceof Error ? `: ${e.message}` : "" + }`, + ); + } +} + +async function uploadVersion( + ctx: InternalClientContext, + // TODO: make repository rid + widgetRid: WidgetRid, + version: string, + zipFile: ReadableStream | Blob | BufferSource, +): Promise { + const fetch = createFetch(ctx.tokenProvider); + const url = + `${ctx.foundryUrl}/artifacts/api/repositories/${widgetRid}/contents/release/siteasset/versions/zip/${version}`; + + await fetch( + url, + { + method: "PUT", + body: zipFile, + headers: { + "Content-Type": "application/octet-stream", + }, + duplex: "half", // Node hates me + } satisfies RequestInit & { duplex: "half" } as any, + ); +} + +async function publishManifest( + ctx: InternalClientContext, + // TODO: make repository rid + widgetRid: WidgetRid, + version: string, +): Promise { + const fetch = createFetch(ctx.tokenProvider); + const url = + `${ctx.foundryUrl}/view-registry/api/repositories/${widgetRid}/publish-manifest`; + + await fetch( + url, + { + method: "POST", + body: JSON.stringify({ version }), + headers: { + "Content-Type": "application/json", + }, + }, + ); +} + +function logArchiveStats(archive: archiver.Archiver): void { + let archiveStats = { fileCount: 0, bytes: 0 }; + archive.on("progress", (progress) => { + archiveStats = { + fileCount: progress.entries.total, + bytes: progress.fs.totalBytes, + }; + }); + archive.on("finish", () => { + consola.info( + `Zipped ${ + prettyBytes(archiveStats.bytes, { binary: true }) + } total over ${archiveStats.fileCount} files`, + ); + }); +} diff --git a/packages/cli/src/commands/widget/index.ts b/packages/cli/src/commands/widget/index.ts new file mode 100644 index 000000000..4213793d8 --- /dev/null +++ b/packages/cli/src/commands/widget/index.ts @@ -0,0 +1,83 @@ +/* + * Copyright 2024 Palantir Technologies, Inc. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { type CliCommonArgs, YargsCheckError } from "@osdk/cli.common"; +import type { CommandModule } from "yargs"; +import type { WidgetRid } from "../../net/WidgetRid.js"; +import configLoader from "../../util/configLoader.js"; +import { logConfigFileMiddleware } from "../../yargs/logConfigFileMiddleware.js"; +import type { CommonWidgetArgs } from "./CommonWidgetArgs.js"; +import deploy from "./deploy/index.js"; +import { logWidgetCommandConfigFileOverride } from "./logWidgetCommandConfigFileOverride.js"; +import version from "./version/index.js"; + +const command: CommandModule = { + command: "widget", + describe: "Manage your widget", + builder: async (argv) => { + const config = await configLoader("widget"); + const rid = config?.foundryConfig.widget.rid; + const foundryUrl = config?.foundryConfig.foundryUrl; + return argv.options({ + rid: { + type: "string", + coerce: (rid) => rid as WidgetRid, + ...rid + ? { default: rid } + : { demandOption: true }, + description: "Widget resource identifier (rid)", + }, + foundryUrl: { + coerce: (foundryUrl) => foundryUrl.replace(/\/$/, ""), + type: "string", + ...foundryUrl + ? { default: foundryUrl } + : { demandOption: true }, + description: "URL for the Foundry stack", + }, + token: { + type: "string", + conflicts: "tokenFile", + description: "Foundry API token", + }, + tokenFile: { + type: "string", + conflicts: "token", + description: "Path to file containing Foundry API token", + }, + }) + .group( + ["rid", "foundryUrl", "token", "tokenFile"], + "Common Options", + ) + .command(version) + .command(deploy) + .check((args) => { + if (!args.foundryUrl.startsWith("https://")) { + throw new YargsCheckError("foundryUrl must start with https://"); + } + return true; + }) + .middleware((args) => { + logConfigFileMiddleware("widget"); + logWidgetCommandConfigFileOverride(args, config?.foundryConfig); + }) + .demandCommand(); + }, + handler: async (args) => {}, +}; + +export default command; diff --git a/packages/cli/src/commands/widget/logWidgetCommandConfigFileOverride.ts b/packages/cli/src/commands/widget/logWidgetCommandConfigFileOverride.ts new file mode 100644 index 000000000..4042e127c --- /dev/null +++ b/packages/cli/src/commands/widget/logWidgetCommandConfigFileOverride.ts @@ -0,0 +1,40 @@ +/* + * Copyright 2023 Palantir Technologies, Inc. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import type { FoundryConfig } from "@osdk/foundry-config-json"; +import { consola } from "consola"; +import type { Arguments } from "yargs"; +import type { CommonWidgetArgs } from "./CommonWidgetArgs.js"; + +export async function logWidgetCommandConfigFileOverride( + args: Arguments, + config: FoundryConfig<"widget"> | undefined, +) { + if ( + config?.widget.rid != null + && args.rid !== config.widget.rid + ) { + consola.debug( + `Overriding "rid" from config file with ${args.rid}`, + ); + } + + if (config?.foundryUrl != null && args.foundryUrl !== config.foundryUrl) { + consola.debug( + `Overriding "foundryUrl" from config file with ${args.foundryUrl}`, + ); + } +} diff --git a/packages/cli/src/commands/widget/version/delete/VersionDeleteArgs.ts b/packages/cli/src/commands/widget/version/delete/VersionDeleteArgs.ts new file mode 100644 index 000000000..376fd6870 --- /dev/null +++ b/packages/cli/src/commands/widget/version/delete/VersionDeleteArgs.ts @@ -0,0 +1,22 @@ +/* + * Copyright 2023 Palantir Technologies, Inc. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import type { CommonWidgetArgs } from "../../CommonWidgetArgs.js"; + +export interface VersionDeleteArgs extends CommonWidgetArgs { + version: string; + yes?: boolean; +} diff --git a/packages/cli/src/commands/widget/version/delete/index.ts b/packages/cli/src/commands/widget/version/delete/index.ts new file mode 100644 index 000000000..56b671451 --- /dev/null +++ b/packages/cli/src/commands/widget/version/delete/index.ts @@ -0,0 +1,47 @@ +/* + * Copyright 2023 Palantir Technologies, Inc. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import type { CommandModule } from "yargs"; +import type { CommonWidgetArgs } from "../../CommonWidgetArgs.js"; +import type { VersionDeleteArgs } from "./VersionDeleteArgs.js"; + +const command: CommandModule< + CommonWidgetArgs, + VersionDeleteArgs +> = { + command: "delete ", + describe: "Delete widget version", + builder: (argv) => { + return argv + .positional("version", { + type: "string", + demandOption: true, + description: "Version to delete", + }) + .option("yes", { + alias: "y", + type: "boolean", + description: "Automatically confirm destructive changes", + }) + .group(["yes"], "Delete Options"); + }, + handler: async (args) => { + const command = await import("./versionDeleteCommand.mjs"); + await command.default(args); + }, +}; + +export default command; diff --git a/packages/cli/src/commands/widget/version/delete/versionDeleteCommand.mts b/packages/cli/src/commands/widget/version/delete/versionDeleteCommand.mts new file mode 100644 index 000000000..565be56e2 --- /dev/null +++ b/packages/cli/src/commands/widget/version/delete/versionDeleteCommand.mts @@ -0,0 +1,85 @@ +/* + * Copyright 2023 Palantir Technologies, Inc. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { createInternalClientContext } from "#net"; +import { consola } from "consola"; +import { colorize } from "consola/utils"; +import { handlePromptCancel } from "../../../../consola/handlePromptCancel.js"; +import { createFetch } from "../../../../net/createFetch.mjs"; +import type { InternalClientContext } from "../../../../net/internalClientContext.mjs"; +import type { WidgetRid } from "../../../../net/WidgetRid.js"; +import { loadToken } from "../../../../util/token.js"; +import type { VersionDeleteArgs } from "./VersionDeleteArgs.js"; + +export default async function versionDeleteCommand( + { version, yes, rid, foundryUrl, token, tokenFile }: VersionDeleteArgs, +) { + if (!yes) { + const confirmed = await consola.prompt( + `Are you sure you want to delete the version ${version}?\n${ + colorize("bold", "This action cannot be undone.") + }`, + { type: "confirm" }, + ); + handlePromptCancel(confirmed); + } + + consola.start(`Deleting version ${version}`); + const loadedToken = await loadToken(token, tokenFile); + const tokenProvider = () => loadedToken; + const clientCtx = createInternalClientContext(foundryUrl, tokenProvider); + // TODO: Look at type of locator and decide whether to confirm delete site version + await Promise.all([ + deleteViewRelease(clientCtx, rid, version), + deleteVersion(clientCtx, rid, version), + ]); + consola.success(`Deleted version ${version}`); +} + +async function deleteViewRelease( + ctx: InternalClientContext, + // TODO: make repository rid + widgetRid: WidgetRid, + version: string, +): Promise { + const fetch = createFetch(ctx.tokenProvider); + const url = + `${ctx.foundryUrl}/view-registry/api/views/${widgetRid}/releases/${version}`; + await fetch( + url, + { + method: "DELETE", + }, + ); +} + +async function deleteVersion( + ctx: InternalClientContext, + // TODO: make repository rid + widgetRid: WidgetRid, + version: string, +): Promise { + const fetch = createFetch(ctx.tokenProvider); + const url = + `${ctx.foundryUrl}/artifacts/api/repositories/${widgetRid}/contents/release/siteasset/versions/${version}`; + + await fetch( + url, + { + method: "DELETE", + }, + ); +} diff --git a/packages/cli/src/commands/widget/version/index.ts b/packages/cli/src/commands/widget/version/index.ts new file mode 100644 index 000000000..b17f0fabd --- /dev/null +++ b/packages/cli/src/commands/widget/version/index.ts @@ -0,0 +1,39 @@ +/* + * Copyright 2023 Palantir Technologies, Inc. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import type { CommandModule } from "yargs"; +import type { CommonWidgetArgs } from "../CommonWidgetArgs.js"; +import deleteCmd from "./delete/index.js"; +import info from "./info/index.js"; +import list from "./list/index.js"; + +const command: CommandModule< + CommonWidgetArgs, + CommonWidgetArgs +> = { + command: "version", + describe: "Manage widget versions", + builder: (argv) => { + return argv + .command(list) + .command(info) + .command(deleteCmd) + .demandCommand(); + }, + handler: async (args) => {}, +}; + +export default command; diff --git a/packages/cli/src/commands/widget/version/info/VersionInfoArgs.ts b/packages/cli/src/commands/widget/version/info/VersionInfoArgs.ts new file mode 100644 index 000000000..e2c6eddf6 --- /dev/null +++ b/packages/cli/src/commands/widget/version/info/VersionInfoArgs.ts @@ -0,0 +1,21 @@ +/* + * Copyright 2023 Palantir Technologies, Inc. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import type { CommonWidgetArgs } from "../../CommonWidgetArgs.js"; + +export interface VersionInfoArgs extends CommonWidgetArgs { + version: string; +} diff --git a/packages/cli/src/commands/widget/version/info/index.ts b/packages/cli/src/commands/widget/version/info/index.ts new file mode 100644 index 000000000..b7cc6c618 --- /dev/null +++ b/packages/cli/src/commands/widget/version/info/index.ts @@ -0,0 +1,41 @@ +/* + * Copyright 2023 Palantir Technologies, Inc. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import type { CommandModule } from "yargs"; +import type { CommonWidgetArgs } from "../../CommonWidgetArgs.js"; +import type { VersionInfoArgs } from "./VersionInfoArgs.js"; + +const command: CommandModule< + CommonWidgetArgs, + VersionInfoArgs +> = { + command: "info ", + describe: "Load info about widget version", + builder: (argv) => { + return argv + .positional("version", { + type: "string", + demandOption: true, + description: "Version to load", + }); + }, + handler: async (args) => { + const command = await import("./versionInfoCommand.mjs"); + await command.default(args); + }, +}; + +export default command; diff --git a/packages/cli/src/commands/widget/version/info/versionInfoCommand.mts b/packages/cli/src/commands/widget/version/info/versionInfoCommand.mts new file mode 100644 index 000000000..f5ecd911a --- /dev/null +++ b/packages/cli/src/commands/widget/version/info/versionInfoCommand.mts @@ -0,0 +1,48 @@ +/* + * Copyright 2023 Palantir Technologies, Inc. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { createInternalClientContext } from "#net"; +import { consola } from "consola"; +import { createFetch } from "../../../../net/createFetch.mjs"; +import type { InternalClientContext } from "../../../../net/internalClientContext.mjs"; +import type { WidgetRid } from "../../../../net/WidgetRid.js"; +import { loadToken } from "../../../../util/token.js"; +import type { VersionInfoArgs } from "./VersionInfoArgs.js"; + +export default async function versionInfoCommand( + { version, foundryUrl, rid, token, tokenFile }: VersionInfoArgs, +) { + const loadedToken = await loadToken(token, tokenFile); + const tokenProvider = () => loadedToken; + const clientCtx = createInternalClientContext(foundryUrl, tokenProvider); + consola.start("Loading version info"); + const response = await getViewRelease(clientCtx, rid, version); + consola.success(`Loaded version info for ${version}`); + consola.log(JSON.stringify(response, null, 2)); +} + +async function getViewRelease( + ctx: InternalClientContext, + // TODO: make repository rid + widgetRid: WidgetRid, + version: string, +): Promise { + const fetch = createFetch(ctx.tokenProvider); + const url = + `${ctx.foundryUrl}/view-registry/api/views/${widgetRid}/releases/${version}`; + const response = await fetch(url); + return response.json(); +} diff --git a/packages/cli/src/commands/widget/version/list/VersionListArgs.ts b/packages/cli/src/commands/widget/version/list/VersionListArgs.ts new file mode 100644 index 000000000..3c37e904d --- /dev/null +++ b/packages/cli/src/commands/widget/version/list/VersionListArgs.ts @@ -0,0 +1,19 @@ +/* + * Copyright 2023 Palantir Technologies, Inc. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import type { CommonWidgetArgs } from "../../CommonWidgetArgs.js"; + +export interface VersionListArgs extends CommonWidgetArgs {} diff --git a/packages/cli/src/commands/widget/version/list/index.ts b/packages/cli/src/commands/widget/version/list/index.ts new file mode 100644 index 000000000..21bed6b01 --- /dev/null +++ b/packages/cli/src/commands/widget/version/list/index.ts @@ -0,0 +1,36 @@ +/* + * Copyright 2023 Palantir Technologies, Inc. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import type { CommandModule } from "yargs"; +import type { CommonWidgetArgs } from "../../CommonWidgetArgs.js"; +import type { VersionListArgs } from "./VersionListArgs.js"; + +const command: CommandModule< + CommonWidgetArgs, + VersionListArgs +> = { + command: "list", + describe: "List widget versions", + builder: (argv) => { + return argv; + }, + handler: async (args) => { + const command = await import("./versionListCommand.mjs"); + await command.default(args); + }, +}; + +export default command; diff --git a/packages/cli/src/commands/widget/version/list/versionListCommand.mts b/packages/cli/src/commands/widget/version/list/versionListCommand.mts new file mode 100644 index 000000000..0aa4a80ac --- /dev/null +++ b/packages/cli/src/commands/widget/version/list/versionListCommand.mts @@ -0,0 +1,66 @@ +/* + * Copyright 2023 Palantir Technologies, Inc. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { createInternalClientContext } from "#net"; +import { consola } from "consola"; +import { createFetch } from "../../../../net/createFetch.mjs"; +import type { InternalClientContext } from "../../../../net/internalClientContext.mjs"; +import type { WidgetRid } from "../../../../net/WidgetRid.js"; +import { loadToken } from "../../../../util/token.js"; +import type { VersionListArgs } from "./VersionListArgs.js"; + +export default async function versionListCommand( + { foundryUrl, rid, token, tokenFile }: VersionListArgs, +) { + const loadedToken = await loadToken(token, tokenFile); + const tokenProvider = () => loadedToken; + const clientCtx = createInternalClientContext(foundryUrl, tokenProvider); + consola.start("Fetching versions"); + + const response = await listViewReleases(clientCtx, rid); + if (response.releases.length === 0) { + consola.info("No widget versions found"); + return; + } + + consola.success("Found versions:"); + + const semver = await import("semver"); + const sortedVersions = semver.rsort( + response.releases.map(v => v.version).filter(v => semver.valid(v)), + ); + for (const version of sortedVersions) { + consola.log( + ` - ${version}`, + ); + } +} + +async function listViewReleases( + ctx: InternalClientContext, + // TODO: make repository rid + widgetRid: WidgetRid, +): Promise<{ + releases: Array<{ + rid: WidgetRid; + version: string; + }>; +}> { + const fetch = createFetch(ctx.tokenProvider); + const url = `${ctx.foundryUrl}/view-registry/api/views/${widgetRid}/releases`; + const response = await fetch(url); + return response.json(); +} diff --git a/packages/cli/src/net/WidgetRid.ts b/packages/cli/src/net/WidgetRid.ts new file mode 100644 index 000000000..c84e93cfb --- /dev/null +++ b/packages/cli/src/net/WidgetRid.ts @@ -0,0 +1,17 @@ +/* + * Copyright 2023 Palantir Technologies, Inc. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export type WidgetRid = `ri.viewregistry..view.${string}`; diff --git a/packages/cli/src/util/configLoader.ts b/packages/cli/src/util/configLoader.ts index cf768cd96..1e28ff48d 100644 --- a/packages/cli/src/util/configLoader.ts +++ b/packages/cli/src/util/configLoader.ts @@ -18,17 +18,48 @@ import { ExitProcessError } from "@osdk/cli.common"; import type { LoadedFoundryConfig } from "@osdk/foundry-config-json"; import { loadFoundryConfig } from "@osdk/foundry-config-json"; -let configPromise: +let siteConfigPromise: | Promise | undefined> | undefined = undefined; +let widgetConfigPromise: + | Promise | undefined> + | undefined = undefined; + +function getConfig( + type: "site", +): Promise | undefined>; +function getConfig( + type: "widget", +): Promise | undefined>; +function getConfig( + type: "site" | "widget", +): Promise | undefined>; +function getConfig( + type: "site" | "widget", +): Promise | undefined> { + if (type === "site") { + return getSiteConfig(); + } else { + return getWidgetConfig(); + } +} + +function getSiteConfig(): Promise | undefined> { + if (siteConfigPromise == null) { + siteConfigPromise = loadFoundryConfig("site").catch((e) => { + throw new ExitProcessError(2, e instanceof Error ? e.message : undefined); + }); + } + return siteConfigPromise; +} -function getConfig(): Promise | undefined> { - if (configPromise == null) { - configPromise = loadFoundryConfig("site").catch((e) => { +function getWidgetConfig(): Promise | undefined> { + if (widgetConfigPromise == null) { + widgetConfigPromise = loadFoundryConfig("widget").catch((e) => { throw new ExitProcessError(2, e instanceof Error ? e.message : undefined); }); } - return configPromise; + return widgetConfigPromise; } export default getConfig; diff --git a/packages/cli/src/yargs/logConfigFileMiddleware.ts b/packages/cli/src/yargs/logConfigFileMiddleware.ts index abcd1990f..6fbae6339 100644 --- a/packages/cli/src/yargs/logConfigFileMiddleware.ts +++ b/packages/cli/src/yargs/logConfigFileMiddleware.ts @@ -18,10 +18,10 @@ import { consola } from "consola"; import getConfig from "../util/configLoader.js"; let firstTime = true; -export async function logConfigFileMiddleware() { +export async function logConfigFileMiddleware(type: "site" | "widget") { if (firstTime) { firstTime = false; - const config = getConfig(); + const config = getConfig(type); const configFilePath = (await config)?.configFilePath; if (configFilePath) { consola.debug( diff --git a/packages/foundry-config-json/src/config.ts b/packages/foundry-config-json/src/config.ts index 0333def10..371236c00 100644 --- a/packages/foundry-config-json/src/config.ts +++ b/packages/foundry-config-json/src/config.ts @@ -167,7 +167,7 @@ export async function loadFoundryConfig( if (!validate(foundryConfig)) { throw new Error( - `The configuration file does not match the expected schema: ${ + `The configuration file ${configFilePath} does not match the expected schema: ${ ajv.errorsText(validate.errors) }`, ); diff --git a/packages/foundry-config-json/src/index.ts b/packages/foundry-config-json/src/index.ts index 0862e8ec1..fab3b1e78 100644 --- a/packages/foundry-config-json/src/index.ts +++ b/packages/foundry-config-json/src/index.ts @@ -24,4 +24,5 @@ export type { LoadedFoundryConfig, PackageJsonAutoVersionConfig, SiteConfig, + WidgetConfig, } from "./config.js"; diff --git a/packages/monorepo.cspell/dict.osdk.txt b/packages/monorepo.cspell/dict.osdk.txt index 0b03804bc..f837e2451 100644 --- a/packages/monorepo.cspell/dict.osdk.txt +++ b/packages/monorepo.cspell/dict.osdk.txt @@ -7,3 +7,4 @@ paperplane Pressable Tamagui unistyles +siteasset diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index bfc33f3c2..dd37d2704 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1220,6 +1220,9 @@ importers: '@osdk/shared.net.fetch': specifier: workspace:~ version: link:../shared.net.fetch + '@osdk/widget-api.unstable': + specifier: workspace:~ + version: link:../widget.api.unstable '@types/archiver': specifier: ^6.0.2 version: 6.0.2