Skip to content
Draft
Show file tree
Hide file tree
Changes from 8 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,7 @@ USAGE
* [`mw autocomplete`](docs/autocomplete.md) - Display autocomplete installation instructions.
* [`mw backup`](docs/backup.md) - Manage backups of your projects
* [`mw context`](docs/context.md) - Save certain environment parameters for later use
* [`mw contributor`](docs/contributor.md) - Commands for mStudio marketplace contributors
* [`mw conversation`](docs/conversation.md) - Manage your support cases
* [`mw cronjob`](docs/cronjob.md) - Manage cronjobs of your projects
* [`mw database`](docs/database.md) - Manage databases (like MySQL and Redis) in your projects
Expand Down
111 changes: 111 additions & 0 deletions docs/contributor.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
`mw contributor`
================

Commands for mStudio marketplace contributors

* [`mw contributor extension deploy EXTENSION-MANIFEST`](#mw-contributor-extension-deploy-extension-manifest)
* [`mw contributor extension init EXTENSION-MANIFEST`](#mw-contributor-extension-init-extension-manifest)
* [`mw contributor extension publish EXTENSION-MANIFEST`](#mw-contributor-extension-publish-extension-manifest)
* [`mw contributor extension withdraw EXTENSION-MANIFEST`](#mw-contributor-extension-withdraw-extension-manifest)

## `mw contributor extension deploy EXTENSION-MANIFEST`

Deploy an extension manifest to the marketplace

```
USAGE
$ mw contributor extension deploy EXTENSION-MANIFEST [-q] [--create]

ARGUMENTS
EXTENSION-MANIFEST [default: ./mstudio-extension.yaml] file path to the extension manifest (as YAML or JSON)

FLAGS
-q, --quiet suppress process output and only display a machine-readable summary.
--[no-]create create the extension if it does not exist

DESCRIPTION
Deploy an extension manifest to the marketplace

FLAG DESCRIPTIONS
-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
scripts), you can use this flag to easily get the IDs of created resources for further processing.
```

## `mw contributor extension init EXTENSION-MANIFEST`

Init a new extension manifest file

```
USAGE
$ mw contributor extension init EXTENSION-MANIFEST [-q] [--overwrite]

ARGUMENTS
EXTENSION-MANIFEST [default: ./mstudio-extension.yaml] file path to the extension manifest (as YAML or JSON)

FLAGS
-q, --quiet suppress process output and only display a machine-readable summary.
--overwrite overwrite an existing extension manifest if found

DESCRIPTION
Init a new extension manifest file

This command will create a new extension manifest file. It only operates on your local file system; afterwards, use
the 'deploy' command to upload the manifest to the marketplace.

FLAG DESCRIPTIONS
-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
scripts), you can use this flag to easily get the IDs of created resources for further processing.
```

## `mw contributor extension publish EXTENSION-MANIFEST`

Publish an extension on the marketplace

```
USAGE
$ mw contributor extension publish EXTENSION-MANIFEST [-q]

ARGUMENTS
EXTENSION-MANIFEST [default: ./mstudio-extension.yaml] file path to the extension manifest (as YAML or JSON)

FLAGS
-q, --quiet suppress process output and only display a machine-readable summary.

DESCRIPTION
Publish an extension on the marketplace

FLAG DESCRIPTIONS
-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
scripts), you can use this flag to easily get the IDs of created resources for further processing.
```

## `mw contributor extension withdraw EXTENSION-MANIFEST`

Withdraw an extension from the marketplace

```
USAGE
$ mw contributor extension withdraw EXTENSION-MANIFEST --reason <value> [-q]

ARGUMENTS
EXTENSION-MANIFEST [default: ./mstudio-extension.yaml] file path to the extension manifest (as YAML or JSON)

FLAGS
-q, --quiet suppress process output and only display a machine-readable summary.
--reason=<value> (required) Reason for withdrawal

DESCRIPTION
Withdraw an extension from the marketplace

FLAG DESCRIPTIONS
-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
scripts), you can use this flag to easily get the IDs of created resources for further processing.
```
117 changes: 117 additions & 0 deletions src/commands/contributor/extension/deploy.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
import React from "react";
import { ExecRenderBaseCommand } from "../../../lib/basecommands/ExecRenderBaseCommand.js";
import {
makeProcessRenderer,
processFlags,
} from "../../../rendering/process/process_flags.js";
import { Flags } from "@oclif/core";
import { assertStatus } from "@mittwald/api-client";
import { Success } from "../../../rendering/react/components/Success.js";
import {
extensionManifestArg,
parseExtensionManifest,
} from "../../../lib/resources/extension/args_contributor.js";

export default class Deploy extends ExecRenderBaseCommand<typeof Deploy, void> {
static description = "Deploy an extension manifest to the marketplace";

static flags = {
...processFlags,
create: Flags.boolean({
description: "create the extension if it does not exist",
default: true,
allowNo: true,
}),
};

static args = {
"extension-manifest": extensionManifestArg({
required: true,
}),
};

protected async exec(): Promise<void> {
const p = makeProcessRenderer(this.flags, "Updating extension manifest");

const manifest = await parseExtensionManifest(
this.args["extension-manifest"],
);
const { contributorId, id } = manifest;

const existing = await p.runStep(
"Retrieving current extension state",
async () => {
const response =
await this.apiClient.marketplace.extensionGetOwnExtension({
contributorId,
extensionId: id,
});

if (response.status === 404) {
return null;
}

assertStatus(response, 200);

return response.data;
},
);

if (existing === null) {
if (!this.flags.create) {
await p.error("Extension does not exist, use --create to create it");
return;
}

await p.runStep("Registering extension", async () => {
if (manifest.deprecation) {
throw new Error(
'"deprecation" is not supported when creating a new extension',
);
}

await this.apiClient.marketplace.extensionRegisterExtension({
contributorId,
data: {
description: manifest.description,
detailedDescriptions: manifest.detailedDescriptions,
externalFrontends: manifest.externalFrontends,
frontendFragments: manifest.frontendFragments,
name: manifest.name,
scopes: manifest.scopes,
subTitle: manifest.subTitle,
support: manifest.support,
tags: manifest.tags,
webhookURLs: manifest.webhookUrls,
},
});
});
} else {
await p.runStep("Updating extension", async () => {
await this.apiClient.marketplace.extensionPatchExtension({
extensionId: manifest.id,
contributorId: manifest.contributorId,
data: {
deprecation: manifest.deprecation,
description: manifest.description,
detailedDescriptions: manifest.detailedDescriptions,
externalFrontends: manifest.externalFrontends,
frontendFragments: manifest.frontendFragments,
name: manifest.name,
scopes: manifest.scopes,
subTitle: manifest.subTitle,
support: manifest.support,
tags: manifest.tags,
webhookUrls: manifest.webhookUrls,
},
});
});
}

await p.complete(<Success>Extension deployed successfully</Success>);
}

protected render(): React.ReactNode {
return undefined;
}
}
124 changes: 124 additions & 0 deletions src/commands/contributor/extension/init.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
import React from "react";
import { ExecRenderBaseCommand } from "../../../lib/basecommands/ExecRenderBaseCommand.js";
import {
makeProcessRenderer,
processFlags,
} from "../../../rendering/process/process_flags.js";
import { Flags } from "@oclif/core";
import { Success } from "../../../rendering/react/components/Success.js";
import { extensionManifestArg } from "../../../lib/resources/extension/args_contributor.js";
import { ExtensionManifest } from "../../../lib/resources/extension/manifest.js";
import * as uuid from "uuid";
import { writeFile } from "fs/promises";
import yaml from "js-yaml";
import { Value } from "../../../rendering/react/components/Value.js";
import { pathExists } from "../../../lib/util/fs/pathExists.js";

export default class Init extends ExecRenderBaseCommand<typeof Init, void> {
static summary = "Init a new extension manifest file";
Comment thread
martin-helmich marked this conversation as resolved.
static description =
"This command will create a new extension manifest file. It only operates on your local file system; afterwards, use the 'deploy' command to upload the manifest to the marketplace.";

static flags = {
...processFlags,
overwrite: Flags.boolean({
description: "overwrite an existing extension manifest if found",
default: false,
}),
};

static args = {
"extension-manifest": extensionManifestArg({
required: true,
}),
};

protected async exec(): Promise<void> {
const p = makeProcessRenderer(
this.flags,
"Initializing extension manifest",
);

const { overwrite } = this.flags;

await p.runStep("generating extension manifest file", async () => {
const renderedConfiguration: ExtensionManifest = {
name: "my-extension",
contributorId: "TODO",
id: uuid.v4(),
description: "TODO",
detailedDescriptions: {
de: {
markdown: "TODO",
plain: "TODO",
},
en: {
markdown: "TODO",
plain: "TODO",
},
},
externalFrontends: [
{
name: "example",
url: "https://mstudio-extension.example/auth/oneclick?atrek=:accessTokenRetrievalKey&userId=:userId&instanceID=:extensionInstanceId",
},
],
frontendFragments: {
foo: {
url: "https://mstudio-extension.example/",
},
},
scopes: ["user:read"],
subTitle: {
de: "TODO",
en: "TODO",
},
support: {
email: "todo@mstudio-extension.example",
phone: "+49 0000 000000",
},
tags: ["TODO"],
webhookUrls: {
extensionAddedToContext: {
url: "https://mstudio-extension.example/webhooks",
},
extensionInstanceUpdated: {
url: "https://mstudio-extension.example/webhooks",
},
extensionInstanceSecretRotated: {
url: "https://mstudio-extension.example/webhooks",
},
extensionInstanceRemovedFromContext: {
url: "https://mstudio-extension.example/webhooks",
},
},
};

const manifestAlreadyExists = await pathExists(
this.args["extension-manifest"],
);

if (manifestAlreadyExists && !overwrite) {
throw new Error(
"File already exists. Use --overwrite to overwrite it.",
);
}

await writeFile(
this.args["extension-manifest"],
yaml.dump(renderedConfiguration),
);
});

await p.complete(
<Success>
Extension manifest file created at{" "}
<Value>{this.args["extension-manifest"]}</Value>
</Success>,
);
}

protected render(): React.ReactNode {
return undefined;
}
}
Loading