@docmd/plugin-installer 0.5.4 → 0.6.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/LICENSE CHANGED
@@ -1,6 +1,6 @@
1
1
  MIT License
2
2
 
3
- Copyright (c) 2025 docmd.io
3
+ Copyright (c) 2025-present docmd.io
4
4
 
5
5
  Permission is hereby granted, free of charge, to any person obtaining a copy
6
6
  of this software and associated documentation files (the "Software"), to deal
package/README.md CHANGED
@@ -28,6 +28,7 @@ docmd remove search
28
28
  * [**@docmd/themes**](https://www.npmjs.com/package/@docmd/themes) - Official themes (Sky, Ruby, Retro).
29
29
 
30
30
  **Plugins**
31
+ * [**@docmd/plugin-installer**](https://www.npmjs.com/package/@docmd/plugin-installer) - Plugin installer for docmd.
31
32
  * [**@docmd/plugin-search**](https://www.npmjs.com/package/@docmd/plugin-search) - Offline full-text search.
32
33
  * [**@docmd/plugin-pwa**](https://www.npmjs.com/package/@docmd/plugin-pwa) - Progressive Web App support.
33
34
  * [**@docmd/plugin-mermaid**](https://www.npmjs.com/package/@docmd/plugin-mermaid) - Diagrams and flowcharts.
@@ -0,0 +1,7 @@
1
+ declare function installPlugin(pluginInput: string, opts?: {
2
+ verbose?: boolean;
3
+ }): Promise<void>;
4
+ declare function removePlugin(pluginInput: string, opts?: {
5
+ verbose?: boolean;
6
+ }): Promise<void>;
7
+ export { installPlugin, removePlugin };
package/dist/index.js ADDED
@@ -0,0 +1,220 @@
1
+ import fs from 'fs';
2
+ import path from 'path';
3
+ import { execSync } from 'child_process';
4
+ import chalk from 'chalk';
5
+ import { createRequire } from 'module';
6
+ const require = createRequire(import.meta.url);
7
+ const pluginsRegistry = require('../registry/plugins.json');
8
+ /**
9
+ * Detects the package manager used in the current project by looking for lockfiles upwards.
10
+ * Defaults to 'npm' if no lockfile is found.
11
+ */
12
+ function getPackageManager(cwd) {
13
+ let dir = cwd;
14
+ while (dir !== path.parse(dir).root) {
15
+ if (fs.existsSync(path.join(dir, 'pnpm-lock.yaml')))
16
+ return 'pnpm';
17
+ if (fs.existsSync(path.join(dir, 'yarn.lock')))
18
+ return 'yarn';
19
+ if (fs.existsSync(path.join(dir, 'bun.lockb')))
20
+ return 'bun';
21
+ if (fs.existsSync(path.join(dir, 'package-lock.json')))
22
+ return 'npm';
23
+ dir = path.dirname(dir);
24
+ }
25
+ return 'npm';
26
+ }
27
+ /**
28
+ * Resolves plugin metadata from the registry, or builds a fallback object.
29
+ */
30
+ function resolvePluginMeta(name) {
31
+ if (pluginsRegistry[name]) {
32
+ return pluginsRegistry[name];
33
+ }
34
+ // Fallback for unofficial/custom plugins
35
+ return {
36
+ package: name,
37
+ description: "Custom community plugin",
38
+ configKey: name.replace('@docmd/plugin-', ''), // Best effort guess
39
+ defaultConfig: "{}"
40
+ };
41
+ }
42
+ /**
43
+ * Reads config and safely injects the plugin to the `plugins` object.
44
+ */
45
+ function injectPluginToConfig(configPath, meta) {
46
+ let content = '';
47
+ if (fs.existsSync(configPath)) {
48
+ content = fs.readFileSync(configPath, 'utf8');
49
+ }
50
+ else {
51
+ // Scaffold minimal config if missing
52
+ content = `module.exports = {\n plugins: {}\n};\n`;
53
+ }
54
+ // Strip comments to avoid false positives (e.g., `// analytics: {}`)
55
+ const strippedContent = content.replace(/\/\/.*$/gm, '').replace(/\/\*[\s\S]*?\*\//g, '');
56
+ const configKey = meta.configKey;
57
+ // Check if plugin is already inside
58
+ if (strippedContent.includes(`'${configKey}'`) || strippedContent.includes(`"${configKey}"`) || strippedContent.includes(`\`${configKey}\``) || strippedContent.includes(`${configKey}:`)) {
59
+ return false; // Already installed
60
+ }
61
+ // Look for `plugins: {`
62
+ const pluginsRegex = /plugins\s*:\s*\{/;
63
+ const match = content.match(pluginsRegex);
64
+ // Dynamically format multi-line configs to match indentation
65
+ const formattedConfig = meta.defaultConfig.replace(/\n/g, '\n ');
66
+ const injectStr = `'${configKey}': ${formattedConfig}`;
67
+ if (match) {
68
+ content = content.replace(pluginsRegex, `plugins: {\n ${injectStr},`);
69
+ }
70
+ else {
71
+ // If there's no plugins object, we can try to inject it before the last closing brace
72
+ const moduleExportsRegex = /module\.exports\s*=\s*(?:defineConfig\()?\{([\s\S]*?)\}(?:\))?;?/g;
73
+ let matchE;
74
+ let lastMatch;
75
+ while ((matchE = moduleExportsRegex.exec(content)) !== null) {
76
+ lastMatch = matchE;
77
+ }
78
+ if (lastMatch) {
79
+ const closingBraceIndex = lastMatch.index + lastMatch[0].lastIndexOf('}');
80
+ const prefix = content.substring(0, closingBraceIndex);
81
+ const suffix = content.substring(closingBraceIndex);
82
+ const insert = prefix.trim().endsWith(',') || prefix.trim().endsWith('{') ?
83
+ `\n plugins: {\n ${injectStr}\n }\n` :
84
+ `,\n plugins: {\n ${injectStr}\n }\n`;
85
+ content = prefix + insert + suffix;
86
+ }
87
+ else {
88
+ console.warn(chalk.yellow(`Could not automatically inject plugin into config file. Please add '${configKey}': ${meta.defaultConfig} manually to the plugins object.`));
89
+ return false;
90
+ }
91
+ }
92
+ fs.writeFileSync(configPath, content, 'utf8');
93
+ return true;
94
+ }
95
+ /**
96
+ * Removes the plugin from the config file.
97
+ */
98
+ function removePluginFromConfig(configPath, meta) {
99
+ if (!fs.existsSync(configPath))
100
+ return false;
101
+ let content = fs.readFileSync(configPath, 'utf8');
102
+ const configKey = meta.configKey;
103
+ if (!content.includes(configKey)) {
104
+ return false;
105
+ }
106
+ // This regex handles both multiline well-formatted configs, and poorly formatted tight inline configs
107
+ // generated by the scaffolding fallback e.g. `'search': {},}`
108
+ const re1 = new RegExp(`\\s*['"\`]?${configKey}['"\`]?\\s*:\\s*\\{[^}]*\\}\\s*,?\\s*`, 'gm');
109
+ // Replace active entries with empty string
110
+ const newContent = content.replace(re1, '');
111
+ if (content === newContent) {
112
+ return false; // Regex didn't match (e.g. config differs from expected format)
113
+ }
114
+ fs.writeFileSync(configPath, newContent, 'utf8');
115
+ return true;
116
+ }
117
+ async function installPlugin(pluginInput, opts = {}) {
118
+ const cwd = process.cwd();
119
+ const pkgManager = getPackageManager(cwd);
120
+ const meta = resolvePluginMeta(pluginInput);
121
+ const packageName = meta.package;
122
+ if (opts.verbose) {
123
+ console.log(chalk.cyan(`Installing ${packageName} using ${pkgManager}...`));
124
+ }
125
+ else {
126
+ process.stdout.write(chalk.cyan(`Installing ${packageName}... `));
127
+ }
128
+ let installCmd = '';
129
+ if (pkgManager === 'npm')
130
+ installCmd = `npm install ${packageName}`;
131
+ else if (pkgManager === 'yarn')
132
+ installCmd = `yarn add ${packageName}`;
133
+ else if (pkgManager === 'pnpm')
134
+ installCmd = `pnpm add ${packageName}`;
135
+ else if (pkgManager === 'bun')
136
+ installCmd = `bun add ${packageName}`;
137
+ // Use `--no-save` fallback for global / raw directories gracefully if it's npm
138
+ if (pkgManager === 'npm' && !fs.existsSync(path.join(cwd, 'package.json'))) {
139
+ installCmd += ' --no-save';
140
+ }
141
+ try {
142
+ const stdioMode = opts.verbose ? 'inherit' : 'pipe';
143
+ execSync(installCmd, { stdio: stdioMode, cwd });
144
+ if (!opts.verbose)
145
+ process.stdout.write(chalk.green(`Done\n`));
146
+ else
147
+ console.log(chalk.green(`Successfully installed ${packageName}.`));
148
+ const configPath = path.join(cwd, 'docmd.config.js');
149
+ console.log(chalk.cyan(`Injecting '${meta.configKey}' into docmd.config.js...`));
150
+ const injected = injectPluginToConfig(configPath, meta);
151
+ if (injected) {
152
+ console.log(chalk.green(`Successfully activated '${meta.configKey}' in config.`));
153
+ }
154
+ else {
155
+ console.log(chalk.yellow(`Plugin '${meta.configKey}' was already in config or could not be injected.`));
156
+ }
157
+ }
158
+ catch (err) {
159
+ if (!opts.verbose)
160
+ process.stdout.write(chalk.red(`Failed\n`));
161
+ console.error(chalk.red(`❌ Could not install plugin '${packageName}'.`));
162
+ if (opts.verbose) {
163
+ console.error(chalk.dim(err.message));
164
+ if (err.stdout)
165
+ console.error(err.stdout.toString());
166
+ if (err.stderr)
167
+ console.error(err.stderr.toString());
168
+ }
169
+ else {
170
+ console.error(chalk.yellow(`Run with --verbose to see detailed logs.`));
171
+ }
172
+ }
173
+ }
174
+ async function removePlugin(pluginInput, opts = {}) {
175
+ const cwd = process.cwd();
176
+ const pkgManager = getPackageManager(cwd);
177
+ const meta = resolvePluginMeta(pluginInput);
178
+ const packageName = meta.package;
179
+ if (opts.verbose) {
180
+ console.log(chalk.cyan(`Removing ${packageName} using ${pkgManager}...`));
181
+ }
182
+ else {
183
+ process.stdout.write(chalk.cyan(`Removing ${packageName}... `));
184
+ }
185
+ let uninstallCmd = '';
186
+ if (pkgManager === 'npm')
187
+ uninstallCmd = `npm uninstall ${packageName}`;
188
+ else if (pkgManager === 'yarn')
189
+ uninstallCmd = `yarn remove ${packageName}`;
190
+ else if (pkgManager === 'pnpm')
191
+ uninstallCmd = `pnpm remove ${packageName}`;
192
+ else if (pkgManager === 'bun')
193
+ uninstallCmd = `bun remove ${packageName}`;
194
+ try {
195
+ const stdioMode = opts.verbose ? 'inherit' : 'pipe';
196
+ execSync(uninstallCmd, { stdio: stdioMode, cwd });
197
+ if (!opts.verbose)
198
+ process.stdout.write(chalk.green(`Done\n`));
199
+ else
200
+ console.log(chalk.green(`Successfully uninstalled ${packageName}.`));
201
+ const configPath = path.join(cwd, 'docmd.config.js');
202
+ console.log(chalk.cyan(`Removing '${meta.configKey}' from docmd.config.js...`));
203
+ const removed = removePluginFromConfig(configPath, meta);
204
+ if (removed) {
205
+ console.log(chalk.green(`Successfully deactivated '${meta.configKey}' in config.`));
206
+ }
207
+ else {
208
+ console.log(chalk.yellow(`Plugin '${meta.configKey}' was not found in config or could not be removed automatically.`));
209
+ }
210
+ }
211
+ catch (err) {
212
+ if (!opts.verbose)
213
+ process.stdout.write(chalk.red(`Failed\n`));
214
+ console.error(chalk.red(`❌ Could not remove plugin '${packageName}'.`));
215
+ if (opts.verbose) {
216
+ console.error(chalk.dim(err.message));
217
+ }
218
+ }
219
+ }
220
+ export { installPlugin, removePlugin };
package/package.json CHANGED
@@ -1,8 +1,16 @@
1
1
  {
2
2
  "name": "@docmd/plugin-installer",
3
- "version": "0.5.4",
3
+ "version": "0.6.0",
4
4
  "description": "Installer utility to add and remove plugins for docmd.",
5
- "main": "index.js",
5
+ "type": "module",
6
+ "main": "dist/index.js",
7
+ "types": "dist/index.d.ts",
8
+ "scripts": {
9
+ "build": "tsc"
10
+ },
11
+ "files": [
12
+ "dist"
13
+ ],
6
14
  "dependencies": {
7
15
  "chalk": "^4.1.2"
8
16
  },
@@ -33,5 +41,8 @@
33
41
  },
34
42
  "homepage": "https://docmd.io",
35
43
  "funding": "https://github.com/sponsors/mgks",
36
- "license": "MIT"
44
+ "license": "MIT",
45
+ "devDependencies": {
46
+ "@types/node": "^25.4.0"
47
+ }
37
48
  }
package/index.js DELETED
@@ -1,229 +0,0 @@
1
- const fs = require('fs');
2
- const path = require('path');
3
- const { execSync } = require('child_process');
4
- const chalk = require('chalk');
5
-
6
- // Load the official plugins registry
7
- const pluginsRegistry = require('./registry/plugins.json');
8
-
9
- /**
10
- * Detects the package manager used in the current project by looking for lockfiles upwards.
11
- * Defaults to 'npm' if no lockfile is found.
12
- */
13
- function getPackageManager(cwd) {
14
- let dir = cwd;
15
- while (dir !== path.parse(dir).root) {
16
- if (fs.existsSync(path.join(dir, 'pnpm-lock.yaml'))) return 'pnpm';
17
- if (fs.existsSync(path.join(dir, 'yarn.lock'))) return 'yarn';
18
- if (fs.existsSync(path.join(dir, 'bun.lockb'))) return 'bun';
19
- if (fs.existsSync(path.join(dir, 'package-lock.json'))) return 'npm';
20
- dir = path.dirname(dir);
21
- }
22
- return 'npm';
23
- }
24
-
25
- /**
26
- * Resolves plugin metadata from the registry, or builds a fallback object.
27
- */
28
- function resolvePluginMeta(name) {
29
- if (pluginsRegistry[name]) {
30
- return pluginsRegistry[name];
31
- }
32
-
33
- // Fallback for unofficial/custom plugins
34
- return {
35
- package: name,
36
- description: "Custom community plugin",
37
- configKey: name.replace('@docmd/plugin-', ''), // Best effort guess
38
- defaultConfig: "{}"
39
- };
40
- }
41
-
42
- /**
43
- * Reads config and safely injects the plugin to the `plugins` object.
44
- */
45
- function injectPluginToConfig(configPath, meta) {
46
- let content = '';
47
- if (fs.existsSync(configPath)) {
48
- content = fs.readFileSync(configPath, 'utf8');
49
- } else {
50
- // Scaffold minimal config if missing
51
- content = `module.exports = {\n plugins: {}\n};\n`;
52
- }
53
-
54
- // Strip comments to avoid false positives (e.g., `// analytics: {}`)
55
- const strippedContent = content.replace(/\/\/.*$/gm, '').replace(/\/\*[\s\S]*?\*\//g, '');
56
-
57
- const configKey = meta.configKey;
58
-
59
- // Check if plugin is already inside
60
- if (strippedContent.includes(`'${configKey}'`) || strippedContent.includes(`"${configKey}"`) || strippedContent.includes(`\`${configKey}\``) || strippedContent.includes(`${configKey}:`)) {
61
- return false; // Already installed
62
- }
63
-
64
- // Look for `plugins: {`
65
- const pluginsRegex = /plugins\s*:\s*\{/;
66
- const match = content.match(pluginsRegex);
67
-
68
- // Dynamically format multi-line configs to match indentation
69
- const formattedConfig = meta.defaultConfig.replace(/\n/g, '\n ');
70
- const injectStr = `'${configKey}': ${formattedConfig}`;
71
-
72
- if (match) {
73
- content = content.replace(pluginsRegex, `plugins: {\n ${injectStr},`);
74
- } else {
75
- // If there's no plugins object, we can try to inject it before the last closing brace
76
- const moduleExportsRegex = /module\.exports\s*=\s*(?:defineConfig\()?\{([\s\S]*?)\}(?:\))?;?/g;
77
- let matchE;
78
- let lastMatch;
79
- while ((matchE = moduleExportsRegex.exec(content)) !== null) {
80
- lastMatch = matchE;
81
- }
82
- if (lastMatch) {
83
- const closingBraceIndex = lastMatch.index + lastMatch[0].lastIndexOf('}');
84
- const prefix = content.substring(0, closingBraceIndex);
85
- const suffix = content.substring(closingBraceIndex);
86
-
87
- const insert = prefix.trim().endsWith(',') || prefix.trim().endsWith('{') ?
88
- `\n plugins: {\n ${injectStr}\n }\n` :
89
- `,\n plugins: {\n ${injectStr}\n }\n`;
90
-
91
- content = prefix + insert + suffix;
92
- } else {
93
- console.warn(chalk.yellow(`Could not automatically inject plugin into config file. Please add '${configKey}': ${meta.defaultConfig} manually to the plugins object.`));
94
- return false;
95
- }
96
- }
97
-
98
- fs.writeFileSync(configPath, content, 'utf8');
99
- return true;
100
- }
101
-
102
- /**
103
- * Removes the plugin from the config file.
104
- */
105
- function removePluginFromConfig(configPath, meta) {
106
- if (!fs.existsSync(configPath)) return false;
107
-
108
- let content = fs.readFileSync(configPath, 'utf8');
109
- const configKey = meta.configKey;
110
-
111
- if (!content.includes(configKey)) {
112
- return false;
113
- }
114
-
115
- // This regex handles both multiline well-formatted configs, and poorly formatted tight inline configs
116
- // generated by the scaffolding fallback e.g. `'search': {},}`
117
- const re1 = new RegExp(`\\s*['"\`]?${configKey}['"\`]?\\s*:\\s*\\{[^}]*\\}\\s*,?\\s*`, 'gm');
118
-
119
- // Replace active entries with empty string
120
- const newContent = content.replace(re1, '');
121
-
122
- if (content === newContent) {
123
- return false; // Regex didn't match (e.g. config differs from expected format)
124
- }
125
-
126
- fs.writeFileSync(configPath, newContent, 'utf8');
127
- return true;
128
- }
129
-
130
-
131
- async function installPlugin(pluginInput, opts = {}) {
132
- const cwd = process.cwd();
133
- const pkgManager = getPackageManager(cwd);
134
- const meta = resolvePluginMeta(pluginInput);
135
- const packageName = meta.package;
136
-
137
- if (opts.verbose) {
138
- console.log(chalk.cyan(`Installing ${packageName} using ${pkgManager}...`));
139
- } else {
140
- process.stdout.write(chalk.cyan(`Installing ${packageName}... `));
141
- }
142
-
143
- let installCmd = '';
144
- if (pkgManager === 'npm') installCmd = `npm install ${packageName}`;
145
- else if (pkgManager === 'yarn') installCmd = `yarn add ${packageName}`;
146
- else if (pkgManager === 'pnpm') installCmd = `pnpm add ${packageName}`;
147
- else if (pkgManager === 'bun') installCmd = `bun add ${packageName}`;
148
-
149
- // Use `--no-save` fallback for global / raw directories gracefully if it's npm
150
- if (pkgManager === 'npm' && !fs.existsSync(path.join(cwd, 'package.json'))) {
151
- installCmd += ' --no-save';
152
- }
153
-
154
- try {
155
- const stdioMode = opts.verbose ? 'inherit' : 'pipe';
156
- execSync(installCmd, { stdio: stdioMode, cwd });
157
-
158
- if (!opts.verbose) process.stdout.write(chalk.green(`Done\n`));
159
- else console.log(chalk.green(`Successfully installed ${packageName}.`));
160
-
161
- const configPath = path.join(cwd, 'docmd.config.js');
162
- console.log(chalk.cyan(`Injecting '${meta.configKey}' into docmd.config.js...`));
163
-
164
- const injected = injectPluginToConfig(configPath, meta);
165
- if (injected) {
166
- console.log(chalk.green(`Successfully activated '${meta.configKey}' in config.`));
167
- } else {
168
- console.log(chalk.yellow(`Plugin '${meta.configKey}' was already in config or could not be injected.`));
169
- }
170
- } catch (err) {
171
- if (!opts.verbose) process.stdout.write(chalk.red(`Failed\n`));
172
- console.error(chalk.red(`❌ Could not install plugin '${packageName}'.`));
173
- if (opts.verbose) {
174
- console.error(chalk.dim(err.message));
175
- if (err.stdout) console.error(err.stdout.toString());
176
- if (err.stderr) console.error(err.stderr.toString());
177
- } else {
178
- console.error(chalk.yellow(`Run with --verbose to see detailed logs.`));
179
- }
180
- }
181
- }
182
-
183
- async function removePlugin(pluginInput, opts = {}) {
184
- const cwd = process.cwd();
185
- const pkgManager = getPackageManager(cwd);
186
- const meta = resolvePluginMeta(pluginInput);
187
- const packageName = meta.package;
188
-
189
- if (opts.verbose) {
190
- console.log(chalk.cyan(`Removing ${packageName} using ${pkgManager}...`));
191
- } else {
192
- process.stdout.write(chalk.cyan(`Removing ${packageName}... `));
193
- }
194
-
195
- let uninstallCmd = '';
196
- if (pkgManager === 'npm') uninstallCmd = `npm uninstall ${packageName}`;
197
- else if (pkgManager === 'yarn') uninstallCmd = `yarn remove ${packageName}`;
198
- else if (pkgManager === 'pnpm') uninstallCmd = `pnpm remove ${packageName}`;
199
- else if (pkgManager === 'bun') uninstallCmd = `bun remove ${packageName}`;
200
-
201
- try {
202
- const stdioMode = opts.verbose ? 'inherit' : 'pipe';
203
- execSync(uninstallCmd, { stdio: stdioMode, cwd });
204
-
205
- if (!opts.verbose) process.stdout.write(chalk.green(`Done\n`));
206
- else console.log(chalk.green(`Successfully uninstalled ${packageName}.`));
207
-
208
- const configPath = path.join(cwd, 'docmd.config.js');
209
- console.log(chalk.cyan(`Removing '${meta.configKey}' from docmd.config.js...`));
210
-
211
- const removed = removePluginFromConfig(configPath, meta);
212
- if (removed) {
213
- console.log(chalk.green(`Successfully deactivated '${meta.configKey}' in config.`));
214
- } else {
215
- console.log(chalk.yellow(`Plugin '${meta.configKey}' was not found in config or could not be removed automatically.`));
216
- }
217
- } catch (err) {
218
- if (!opts.verbose) process.stdout.write(chalk.red(`Failed\n`));
219
- console.error(chalk.red(`❌ Could not remove plugin '${packageName}'.`));
220
- if (opts.verbose) {
221
- console.error(chalk.dim(err.message));
222
- }
223
- }
224
- }
225
-
226
- module.exports = {
227
- installPlugin,
228
- removePlugin
229
- };
@@ -1,44 +0,0 @@
1
- {
2
- "analytics": {
3
- "package": "@docmd/plugin-analytics",
4
- "description": "Analytics injection plugin for docmd",
5
- "configKey": "analytics",
6
- "defaultConfig": "{}"
7
- },
8
- "search": {
9
- "package": "@docmd/plugin-search",
10
- "description": "Local search implementation for docmd",
11
- "configKey": "search",
12
- "defaultConfig": "{}"
13
- },
14
- "seo": {
15
- "package": "@docmd/plugin-seo",
16
- "description": "SEO meta tag generator for docmd",
17
- "configKey": "seo",
18
- "defaultConfig": "{}"
19
- },
20
- "sitemap": {
21
- "package": "@docmd/plugin-sitemap",
22
- "description": "Sitemap generator for docmd",
23
- "configKey": "sitemap",
24
- "defaultConfig": "{}"
25
- },
26
- "mermaid": {
27
- "package": "@docmd/plugin-mermaid",
28
- "description": "Mermaid.js diagram support for docmd",
29
- "configKey": "mermaid",
30
- "defaultConfig": "{}"
31
- },
32
- "llms": {
33
- "package": "@docmd/plugin-llms",
34
- "description": "LLM integration plugin for docmd",
35
- "configKey": "llms",
36
- "defaultConfig": "{}"
37
- },
38
- "pwa": {
39
- "package": "@docmd/plugin-pwa",
40
- "description": "Progressive Web App support for docmd",
41
- "configKey": "pwa",
42
- "defaultConfig": "{}"
43
- }
44
- }