diff options
author | Chocobozzz <me@florianbigard.com> | 2023-07-31 14:34:36 +0200 |
---|---|---|
committer | Chocobozzz <me@florianbigard.com> | 2023-08-11 15:02:33 +0200 |
commit | 3a4992633ee62d5edfbb484d9c6bcb3cf158489d (patch) | |
tree | e4510b39bdac9c318fdb4b47018d08f15368b8f0 /apps/peertube-cli | |
parent | 04d1da5621d25d59bd5fa1543b725c497bf5d9a8 (diff) | |
download | PeerTube-3a4992633ee62d5edfbb484d9c6bcb3cf158489d.tar.gz PeerTube-3a4992633ee62d5edfbb484d9c6bcb3cf158489d.tar.zst PeerTube-3a4992633ee62d5edfbb484d9c6bcb3cf158489d.zip |
Migrate server to ESM
Sorry for the very big commit that may lead to git log issues and merge
conflicts, but it's a major step forward:
* Server can be faster at startup because imports() are async and we can
easily lazy import big modules
* Angular doesn't seem to support ES import (with .js extension), so we
had to correctly organize peertube into a monorepo:
* Use yarn workspace feature
* Use typescript reference projects for dependencies
* Shared projects have been moved into "packages", each one is now a
node module (with a dedicated package.json/tsconfig.json)
* server/tools have been moved into apps/ and is now a dedicated app
bundled and published on NPM so users don't have to build peertube
cli tools manually
* server/tests have been moved into packages/ so we don't compile
them every time we want to run the server
* Use isolatedModule option:
* Had to move from const enum to const
(https://www.typescriptlang.org/docs/handbook/enums.html#objects-vs-enums)
* Had to explictely specify "type" imports when used in decorators
* Prefer tsx (that uses esbuild under the hood) instead of ts-node to
load typescript files (tests with mocha or scripts):
* To reduce test complexity as esbuild doesn't support decorator
metadata, we only test server files that do not import server
models
* We still build tests files into js files for a faster CI
* Remove unmaintained peertube CLI import script
* Removed some barrels to speed up execution (less imports)
Diffstat (limited to 'apps/peertube-cli')
-rw-r--r-- | apps/peertube-cli/.npmignore | 4 | ||||
-rw-r--r-- | apps/peertube-cli/README.md | 43 | ||||
-rw-r--r-- | apps/peertube-cli/package.json | 19 | ||||
-rw-r--r-- | apps/peertube-cli/scripts/build.js | 27 | ||||
-rw-r--r-- | apps/peertube-cli/scripts/watch.js | 7 | ||||
-rw-r--r-- | apps/peertube-cli/src/peertube-auth.ts | 171 | ||||
-rw-r--r-- | apps/peertube-cli/src/peertube-get-access-token.ts | 39 | ||||
-rw-r--r-- | apps/peertube-cli/src/peertube-plugins.ts | 167 | ||||
-rw-r--r-- | apps/peertube-cli/src/peertube-redundancy.ts | 186 | ||||
-rw-r--r-- | apps/peertube-cli/src/peertube-upload.ts | 167 | ||||
-rw-r--r-- | apps/peertube-cli/src/peertube.ts | 64 | ||||
-rw-r--r-- | apps/peertube-cli/src/shared/cli.ts | 195 | ||||
-rw-r--r-- | apps/peertube-cli/src/shared/index.ts | 1 | ||||
-rw-r--r-- | apps/peertube-cli/tsconfig.json | 15 | ||||
-rw-r--r-- | apps/peertube-cli/yarn.lock | 374 |
15 files changed, 1479 insertions, 0 deletions
diff --git a/apps/peertube-cli/.npmignore b/apps/peertube-cli/.npmignore new file mode 100644 index 000000000..af17b9f32 --- /dev/null +++ b/apps/peertube-cli/.npmignore | |||
@@ -0,0 +1,4 @@ | |||
1 | src | ||
2 | meta.json | ||
3 | tsconfig.json | ||
4 | scripts | ||
diff --git a/apps/peertube-cli/README.md b/apps/peertube-cli/README.md new file mode 100644 index 000000000..b5b379090 --- /dev/null +++ b/apps/peertube-cli/README.md | |||
@@ -0,0 +1,43 @@ | |||
1 | # PeerTube CLI | ||
2 | |||
3 | ## Usage | ||
4 | |||
5 | See https://docs.joinpeertube.org/maintain/tools#remote-tools | ||
6 | |||
7 | ## Dev | ||
8 | |||
9 | ## Install dependencies | ||
10 | |||
11 | ```bash | ||
12 | cd peertube-root | ||
13 | yarn install --pure-lockfile | ||
14 | cd apps/peertube-cli && yarn install --pure-lockfile | ||
15 | ``` | ||
16 | |||
17 | ## Develop | ||
18 | |||
19 | ```bash | ||
20 | cd peertube-root | ||
21 | npm run dev:peertube-cli | ||
22 | ``` | ||
23 | |||
24 | ## Build | ||
25 | |||
26 | ```bash | ||
27 | cd peertube-root | ||
28 | npm run build:peertube-cli | ||
29 | ``` | ||
30 | |||
31 | ## Run | ||
32 | |||
33 | ```bash | ||
34 | cd peertube-root | ||
35 | node apps/peertube-cli/dist/peertube-cli.js --help | ||
36 | ``` | ||
37 | |||
38 | ## Publish on NPM | ||
39 | |||
40 | ```bash | ||
41 | cd peertube-root | ||
42 | (cd apps/peertube-cli && npm version patch) && npm run build:peertube-cli && (cd apps/peertube-cli && npm publish --access=public) | ||
43 | ``` | ||
diff --git a/apps/peertube-cli/package.json b/apps/peertube-cli/package.json new file mode 100644 index 000000000..a78319be2 --- /dev/null +++ b/apps/peertube-cli/package.json | |||
@@ -0,0 +1,19 @@ | |||
1 | { | ||
2 | "name": "@peertube/peertube-cli", | ||
3 | "version": "1.0.1", | ||
4 | "type": "module", | ||
5 | "main": "dist/peertube.js", | ||
6 | "bin": "dist/peertube.js", | ||
7 | "engines": { | ||
8 | "node": ">=16.x" | ||
9 | }, | ||
10 | "scripts": {}, | ||
11 | "license": "AGPL-3.0", | ||
12 | "private": false, | ||
13 | "devDependencies": { | ||
14 | "application-config": "^2.0.0", | ||
15 | "cli-table3": "^0.6.0", | ||
16 | "netrc-parser": "^3.1.6" | ||
17 | }, | ||
18 | "dependencies": {} | ||
19 | } | ||
diff --git a/apps/peertube-cli/scripts/build.js b/apps/peertube-cli/scripts/build.js new file mode 100644 index 000000000..a9139acfa --- /dev/null +++ b/apps/peertube-cli/scripts/build.js | |||
@@ -0,0 +1,27 @@ | |||
1 | import * as esbuild from 'esbuild' | ||
2 | import { readFileSync } from 'fs' | ||
3 | |||
4 | const packageJSON = JSON.parse(readFileSync(new URL('../package.json', import.meta.url))) | ||
5 | |||
6 | export const esbuildOptions = { | ||
7 | entryPoints: [ './src/peertube.ts' ], | ||
8 | bundle: true, | ||
9 | platform: 'node', | ||
10 | format: 'esm', | ||
11 | target: 'node16', | ||
12 | external: [ | ||
13 | './lib-cov/fluent-ffmpeg', | ||
14 | 'pg-hstore' | ||
15 | ], | ||
16 | outfile: './dist/peertube.js', | ||
17 | banner: { | ||
18 | js: `const require = (await import("node:module")).createRequire(import.meta.url);` + | ||
19 | `const __filename = (await import("node:url")).fileURLToPath(import.meta.url);` + | ||
20 | `const __dirname = (await import("node:path")).dirname(__filename);` | ||
21 | }, | ||
22 | define: { | ||
23 | 'process.env.PACKAGE_VERSION': `'${packageJSON.version}'` | ||
24 | } | ||
25 | } | ||
26 | |||
27 | await esbuild.build(esbuildOptions) | ||
diff --git a/apps/peertube-cli/scripts/watch.js b/apps/peertube-cli/scripts/watch.js new file mode 100644 index 000000000..94e57199c --- /dev/null +++ b/apps/peertube-cli/scripts/watch.js | |||
@@ -0,0 +1,7 @@ | |||
1 | import * as esbuild from 'esbuild' | ||
2 | import { esbuildOptions } from './build.js' | ||
3 | |||
4 | const context = await esbuild.context(esbuildOptions) | ||
5 | |||
6 | // Enable watch mode | ||
7 | await context.watch() | ||
diff --git a/apps/peertube-cli/src/peertube-auth.ts b/apps/peertube-cli/src/peertube-auth.ts new file mode 100644 index 000000000..1d30207c7 --- /dev/null +++ b/apps/peertube-cli/src/peertube-auth.ts | |||
@@ -0,0 +1,171 @@ | |||
1 | import CliTable3 from 'cli-table3' | ||
2 | import prompt from 'prompt' | ||
3 | import { Command } from '@commander-js/extra-typings' | ||
4 | import { assignToken, buildServer, getNetrc, getSettings, writeSettings } from './shared/index.js' | ||
5 | |||
6 | export function defineAuthProgram () { | ||
7 | const program = new Command() | ||
8 | .name('auth') | ||
9 | .description('Register your accounts on remote instances to use them with other commands') | ||
10 | |||
11 | program | ||
12 | .command('add') | ||
13 | .description('remember your accounts on remote instances for easier use') | ||
14 | .option('-u, --url <url>', 'Server url') | ||
15 | .option('-U, --username <username>', 'Username') | ||
16 | .option('-p, --password <token>', 'Password') | ||
17 | .option('--default', 'add the entry as the new default') | ||
18 | .action(options => { | ||
19 | /* eslint-disable no-import-assign */ | ||
20 | prompt.override = options | ||
21 | prompt.start() | ||
22 | prompt.get({ | ||
23 | properties: { | ||
24 | url: { | ||
25 | description: 'instance url', | ||
26 | conform: value => isURLaPeerTubeInstance(value), | ||
27 | message: 'It should be an URL (https://peertube.example.com)', | ||
28 | required: true | ||
29 | }, | ||
30 | username: { | ||
31 | conform: value => typeof value === 'string' && value.length !== 0, | ||
32 | message: 'Name must be only letters, spaces, or dashes', | ||
33 | required: true | ||
34 | }, | ||
35 | password: { | ||
36 | hidden: true, | ||
37 | replace: '*', | ||
38 | required: true | ||
39 | } | ||
40 | } | ||
41 | }, async (_, result) => { | ||
42 | |||
43 | // Check credentials | ||
44 | try { | ||
45 | // Strip out everything after the domain:port. | ||
46 | // See https://github.com/Chocobozzz/PeerTube/issues/3520 | ||
47 | result.url = stripExtraneousFromPeerTubeUrl(result.url) | ||
48 | |||
49 | const server = buildServer(result.url) | ||
50 | await assignToken(server, result.username, result.password) | ||
51 | } catch (err) { | ||
52 | console.error(err.message) | ||
53 | process.exit(-1) | ||
54 | } | ||
55 | |||
56 | await setInstance(result.url, result.username, result.password, options.default) | ||
57 | |||
58 | process.exit(0) | ||
59 | }) | ||
60 | }) | ||
61 | |||
62 | program | ||
63 | .command('del <url>') | ||
64 | .description('Unregisters a remote instance') | ||
65 | .action(async url => { | ||
66 | await delInstance(url) | ||
67 | |||
68 | process.exit(0) | ||
69 | }) | ||
70 | |||
71 | program | ||
72 | .command('list') | ||
73 | .description('List registered remote instances') | ||
74 | .action(async () => { | ||
75 | const [ settings, netrc ] = await Promise.all([ getSettings(), getNetrc() ]) | ||
76 | |||
77 | const table = new CliTable3({ | ||
78 | head: [ 'instance', 'login' ], | ||
79 | colWidths: [ 30, 30 ] | ||
80 | }) as any | ||
81 | |||
82 | settings.remotes.forEach(element => { | ||
83 | if (!netrc.machines[element]) return | ||
84 | |||
85 | table.push([ | ||
86 | element, | ||
87 | netrc.machines[element].login | ||
88 | ]) | ||
89 | }) | ||
90 | |||
91 | console.log(table.toString()) | ||
92 | |||
93 | process.exit(0) | ||
94 | }) | ||
95 | |||
96 | program | ||
97 | .command('set-default <url>') | ||
98 | .description('Set an existing entry as default') | ||
99 | .action(async url => { | ||
100 | const settings = await getSettings() | ||
101 | const instanceExists = settings.remotes.includes(url) | ||
102 | |||
103 | if (instanceExists) { | ||
104 | settings.default = settings.remotes.indexOf(url) | ||
105 | await writeSettings(settings) | ||
106 | |||
107 | process.exit(0) | ||
108 | } else { | ||
109 | console.log('<url> is not a registered instance.') | ||
110 | process.exit(-1) | ||
111 | } | ||
112 | }) | ||
113 | |||
114 | program.addHelpText('after', '\n\n Examples:\n\n' + | ||
115 | ' $ peertube auth add -u https://peertube.cpy.re -U "PEERTUBE_USER" --password "PEERTUBE_PASSWORD"\n' + | ||
116 | ' $ peertube auth add -u https://peertube.cpy.re -U root\n' + | ||
117 | ' $ peertube auth list\n' + | ||
118 | ' $ peertube auth del https://peertube.cpy.re\n' | ||
119 | ) | ||
120 | |||
121 | return program | ||
122 | } | ||
123 | |||
124 | // --------------------------------------------------------------------------- | ||
125 | // Private | ||
126 | // --------------------------------------------------------------------------- | ||
127 | |||
128 | async function delInstance (url: string) { | ||
129 | const [ settings, netrc ] = await Promise.all([ getSettings(), getNetrc() ]) | ||
130 | |||
131 | const index = settings.remotes.indexOf(url) | ||
132 | settings.remotes.splice(index) | ||
133 | |||
134 | if (settings.default === index) settings.default = -1 | ||
135 | |||
136 | await writeSettings(settings) | ||
137 | |||
138 | delete netrc.machines[url] | ||
139 | |||
140 | await netrc.save() | ||
141 | } | ||
142 | |||
143 | async function setInstance (url: string, username: string, password: string, isDefault: boolean) { | ||
144 | const [ settings, netrc ] = await Promise.all([ getSettings(), getNetrc() ]) | ||
145 | |||
146 | if (settings.remotes.includes(url) === false) { | ||
147 | settings.remotes.push(url) | ||
148 | } | ||
149 | |||
150 | if (isDefault || settings.remotes.length === 1) { | ||
151 | settings.default = settings.remotes.length - 1 | ||
152 | } | ||
153 | |||
154 | await writeSettings(settings) | ||
155 | |||
156 | netrc.machines[url] = { login: username, password } | ||
157 | await netrc.save() | ||
158 | } | ||
159 | |||
160 | function isURLaPeerTubeInstance (url: string) { | ||
161 | return url.startsWith('http://') || url.startsWith('https://') | ||
162 | } | ||
163 | |||
164 | function stripExtraneousFromPeerTubeUrl (url: string) { | ||
165 | // Get everything before the 3rd /. | ||
166 | const urlLength = url.includes('/', 8) | ||
167 | ? url.indexOf('/', 8) | ||
168 | : url.length | ||
169 | |||
170 | return url.substring(0, urlLength) | ||
171 | } | ||
diff --git a/apps/peertube-cli/src/peertube-get-access-token.ts b/apps/peertube-cli/src/peertube-get-access-token.ts new file mode 100644 index 000000000..3e0013182 --- /dev/null +++ b/apps/peertube-cli/src/peertube-get-access-token.ts | |||
@@ -0,0 +1,39 @@ | |||
1 | import { Command } from '@commander-js/extra-typings' | ||
2 | import { assignToken, buildServer } from './shared/index.js' | ||
3 | |||
4 | export function defineGetAccessProgram () { | ||
5 | const program = new Command() | ||
6 | .name('get-access-token') | ||
7 | .description('Get a peertube access token') | ||
8 | .alias('token') | ||
9 | |||
10 | program | ||
11 | .option('-u, --url <url>', 'Server url') | ||
12 | .option('-n, --username <username>', 'Username') | ||
13 | .option('-p, --password <token>', 'Password') | ||
14 | .action(async options => { | ||
15 | try { | ||
16 | if ( | ||
17 | !options.url || | ||
18 | !options.username || | ||
19 | !options.password | ||
20 | ) { | ||
21 | if (!options.url) console.error('--url field is required.') | ||
22 | if (!options.username) console.error('--username field is required.') | ||
23 | if (!options.password) console.error('--password field is required.') | ||
24 | |||
25 | process.exit(-1) | ||
26 | } | ||
27 | |||
28 | const server = buildServer(options.url) | ||
29 | await assignToken(server, options.username, options.password) | ||
30 | |||
31 | console.log(server.accessToken) | ||
32 | } catch (err) { | ||
33 | console.error('Cannot get access token: ' + err.message) | ||
34 | process.exit(-1) | ||
35 | } | ||
36 | }) | ||
37 | |||
38 | return program | ||
39 | } | ||
diff --git a/apps/peertube-cli/src/peertube-plugins.ts b/apps/peertube-cli/src/peertube-plugins.ts new file mode 100644 index 000000000..c9da56266 --- /dev/null +++ b/apps/peertube-cli/src/peertube-plugins.ts | |||
@@ -0,0 +1,167 @@ | |||
1 | import CliTable3 from 'cli-table3' | ||
2 | import { isAbsolute } from 'path' | ||
3 | import { Command } from '@commander-js/extra-typings' | ||
4 | import { PluginType, PluginType_Type } from '@peertube/peertube-models' | ||
5 | import { assignToken, buildServer, CommonProgramOptions, getServerCredentials } from './shared/index.js' | ||
6 | |||
7 | export function definePluginsProgram () { | ||
8 | const program = new Command() | ||
9 | |||
10 | program | ||
11 | .name('plugins') | ||
12 | .description('Manage instance plugins/themes') | ||
13 | .alias('p') | ||
14 | |||
15 | program | ||
16 | .command('list') | ||
17 | .description('List installed plugins') | ||
18 | .option('-u, --url <url>', 'Server url') | ||
19 | .option('-U, --username <username>', 'Username') | ||
20 | .option('-p, --password <token>', 'Password') | ||
21 | .option('-t, --only-themes', 'List themes only') | ||
22 | .option('-P, --only-plugins', 'List plugins only') | ||
23 | .action(async options => { | ||
24 | try { | ||
25 | await pluginsListCLI(options) | ||
26 | } catch (err) { | ||
27 | console.error('Cannot list plugins: ' + err.message) | ||
28 | process.exit(-1) | ||
29 | } | ||
30 | }) | ||
31 | |||
32 | program | ||
33 | .command('install') | ||
34 | .description('Install a plugin or a theme') | ||
35 | .option('-u, --url <url>', 'Server url') | ||
36 | .option('-U, --username <username>', 'Username') | ||
37 | .option('-p, --password <token>', 'Password') | ||
38 | .option('-P --path <path>', 'Install from a path') | ||
39 | .option('-n, --npm-name <npmName>', 'Install from npm') | ||
40 | .option('--plugin-version <pluginVersion>', 'Specify the plugin version to install (only available when installing from npm)') | ||
41 | .action(async options => { | ||
42 | try { | ||
43 | await installPluginCLI(options) | ||
44 | } catch (err) { | ||
45 | console.error('Cannot install plugin: ' + err.message) | ||
46 | process.exit(-1) | ||
47 | } | ||
48 | }) | ||
49 | |||
50 | program | ||
51 | .command('update') | ||
52 | .description('Update a plugin or a theme') | ||
53 | .option('-u, --url <url>', 'Server url') | ||
54 | .option('-U, --username <username>', 'Username') | ||
55 | .option('-p, --password <token>', 'Password') | ||
56 | .option('-P --path <path>', 'Update from a path') | ||
57 | .option('-n, --npm-name <npmName>', 'Update from npm') | ||
58 | .action(async options => { | ||
59 | try { | ||
60 | await updatePluginCLI(options) | ||
61 | } catch (err) { | ||
62 | console.error('Cannot update plugin: ' + err.message) | ||
63 | process.exit(-1) | ||
64 | } | ||
65 | }) | ||
66 | |||
67 | program | ||
68 | .command('uninstall') | ||
69 | .description('Uninstall a plugin or a theme') | ||
70 | .option('-u, --url <url>', 'Server url') | ||
71 | .option('-U, --username <username>', 'Username') | ||
72 | .option('-p, --password <token>', 'Password') | ||
73 | .option('-n, --npm-name <npmName>', 'NPM plugin/theme name') | ||
74 | .action(async options => { | ||
75 | try { | ||
76 | await uninstallPluginCLI(options) | ||
77 | } catch (err) { | ||
78 | console.error('Cannot uninstall plugin: ' + err.message) | ||
79 | process.exit(-1) | ||
80 | } | ||
81 | }) | ||
82 | |||
83 | return program | ||
84 | } | ||
85 | |||
86 | // ---------------------------------------------------------------------------- | ||
87 | |||
88 | async function pluginsListCLI (options: CommonProgramOptions & { onlyThemes?: true, onlyPlugins?: true }) { | ||
89 | const { url, username, password } = await getServerCredentials(options) | ||
90 | const server = buildServer(url) | ||
91 | await assignToken(server, username, password) | ||
92 | |||
93 | let pluginType: PluginType_Type | ||
94 | if (options.onlyThemes) pluginType = PluginType.THEME | ||
95 | if (options.onlyPlugins) pluginType = PluginType.PLUGIN | ||
96 | |||
97 | const { data } = await server.plugins.list({ start: 0, count: 100, sort: 'name', pluginType }) | ||
98 | |||
99 | const table = new CliTable3({ | ||
100 | head: [ 'name', 'version', 'homepage' ], | ||
101 | colWidths: [ 50, 20, 50 ] | ||
102 | }) as any | ||
103 | |||
104 | for (const plugin of data) { | ||
105 | const npmName = plugin.type === PluginType.PLUGIN | ||
106 | ? 'peertube-plugin-' + plugin.name | ||
107 | : 'peertube-theme-' + plugin.name | ||
108 | |||
109 | table.push([ | ||
110 | npmName, | ||
111 | plugin.version, | ||
112 | plugin.homepage | ||
113 | ]) | ||
114 | } | ||
115 | |||
116 | console.log(table.toString()) | ||
117 | } | ||
118 | |||
119 | async function installPluginCLI (options: CommonProgramOptions & { path?: string, npmName?: string, pluginVersion?: string }) { | ||
120 | if (!options.path && !options.npmName) { | ||
121 | throw new Error('You need to specify the npm name or the path of the plugin you want to install.') | ||
122 | } | ||
123 | |||
124 | if (options.path && !isAbsolute(options.path)) { | ||
125 | throw new Error('Path should be absolute.') | ||
126 | } | ||
127 | |||
128 | const { url, username, password } = await getServerCredentials(options) | ||
129 | const server = buildServer(url) | ||
130 | await assignToken(server, username, password) | ||
131 | |||
132 | await server.plugins.install({ npmName: options.npmName, path: options.path, pluginVersion: options.pluginVersion }) | ||
133 | |||
134 | console.log('Plugin installed.') | ||
135 | } | ||
136 | |||
137 | async function updatePluginCLI (options: CommonProgramOptions & { path?: string, npmName?: string }) { | ||
138 | if (!options.path && !options.npmName) { | ||
139 | throw new Error('You need to specify the npm name or the path of the plugin you want to update.') | ||
140 | } | ||
141 | |||
142 | if (options.path && !isAbsolute(options.path)) { | ||
143 | throw new Error('Path should be absolute.') | ||
144 | } | ||
145 | |||
146 | const { url, username, password } = await getServerCredentials(options) | ||
147 | const server = buildServer(url) | ||
148 | await assignToken(server, username, password) | ||
149 | |||
150 | await server.plugins.update({ npmName: options.npmName, path: options.path }) | ||
151 | |||
152 | console.log('Plugin updated.') | ||
153 | } | ||
154 | |||
155 | async function uninstallPluginCLI (options: CommonProgramOptions & { npmName?: string }) { | ||
156 | if (!options.npmName) { | ||
157 | throw new Error('You need to specify the npm name of the plugin/theme you want to uninstall.') | ||
158 | } | ||
159 | |||
160 | const { url, username, password } = await getServerCredentials(options) | ||
161 | const server = buildServer(url) | ||
162 | await assignToken(server, username, password) | ||
163 | |||
164 | await server.plugins.uninstall({ npmName: options.npmName }) | ||
165 | |||
166 | console.log('Plugin uninstalled.') | ||
167 | } | ||
diff --git a/apps/peertube-cli/src/peertube-redundancy.ts b/apps/peertube-cli/src/peertube-redundancy.ts new file mode 100644 index 000000000..56fc6366b --- /dev/null +++ b/apps/peertube-cli/src/peertube-redundancy.ts | |||
@@ -0,0 +1,186 @@ | |||
1 | import bytes from 'bytes' | ||
2 | import CliTable3 from 'cli-table3' | ||
3 | import { URL } from 'url' | ||
4 | import { Command } from '@commander-js/extra-typings' | ||
5 | import { forceNumber, uniqify } from '@peertube/peertube-core-utils' | ||
6 | import { HttpStatusCode, VideoRedundanciesTarget } from '@peertube/peertube-models' | ||
7 | import { assignToken, buildServer, CommonProgramOptions, getServerCredentials } from './shared/index.js' | ||
8 | |||
9 | export function defineRedundancyProgram () { | ||
10 | const program = new Command() | ||
11 | .name('redundancy') | ||
12 | .description('Manage instance redundancies') | ||
13 | .alias('r') | ||
14 | |||
15 | program | ||
16 | .command('list-remote-redundancies') | ||
17 | .description('List remote redundancies on your videos') | ||
18 | .option('-u, --url <url>', 'Server url') | ||
19 | .option('-U, --username <username>', 'Username') | ||
20 | .option('-p, --password <token>', 'Password') | ||
21 | .action(async options => { | ||
22 | try { | ||
23 | await listRedundanciesCLI({ target: 'my-videos', ...options }) | ||
24 | } catch (err) { | ||
25 | console.error('Cannot list remote redundancies: ' + err.message) | ||
26 | process.exit(-1) | ||
27 | } | ||
28 | }) | ||
29 | |||
30 | program | ||
31 | .command('list-my-redundancies') | ||
32 | .description('List your redundancies of remote videos') | ||
33 | .option('-u, --url <url>', 'Server url') | ||
34 | .option('-U, --username <username>', 'Username') | ||
35 | .option('-p, --password <token>', 'Password') | ||
36 | .action(async options => { | ||
37 | try { | ||
38 | await listRedundanciesCLI({ target: 'remote-videos', ...options }) | ||
39 | } catch (err) { | ||
40 | console.error('Cannot list redundancies: ' + err.message) | ||
41 | process.exit(-1) | ||
42 | } | ||
43 | }) | ||
44 | |||
45 | program | ||
46 | .command('add') | ||
47 | .description('Duplicate a video in your redundancy system') | ||
48 | .option('-u, --url <url>', 'Server url') | ||
49 | .option('-U, --username <username>', 'Username') | ||
50 | .option('-p, --password <token>', 'Password') | ||
51 | .requiredOption('-v, --video <videoId>', 'Video id to duplicate', parseInt) | ||
52 | .action(async options => { | ||
53 | try { | ||
54 | await addRedundancyCLI(options) | ||
55 | } catch (err) { | ||
56 | console.error('Cannot duplicate video: ' + err.message) | ||
57 | process.exit(-1) | ||
58 | } | ||
59 | }) | ||
60 | |||
61 | program | ||
62 | .command('remove') | ||
63 | .description('Remove a video from your redundancies') | ||
64 | .option('-u, --url <url>', 'Server url') | ||
65 | .option('-U, --username <username>', 'Username') | ||
66 | .option('-p, --password <token>', 'Password') | ||
67 | .requiredOption('-v, --video <videoId>', 'Video id to remove from redundancies', parseInt) | ||
68 | .action(async options => { | ||
69 | try { | ||
70 | await removeRedundancyCLI(options) | ||
71 | } catch (err) { | ||
72 | console.error('Cannot remove redundancy: ' + err) | ||
73 | process.exit(-1) | ||
74 | } | ||
75 | }) | ||
76 | |||
77 | return program | ||
78 | } | ||
79 | |||
80 | // ---------------------------------------------------------------------------- | ||
81 | |||
82 | async function listRedundanciesCLI (options: CommonProgramOptions & { target: VideoRedundanciesTarget }) { | ||
83 | const { target } = options | ||
84 | |||
85 | const { url, username, password } = await getServerCredentials(options) | ||
86 | const server = buildServer(url) | ||
87 | await assignToken(server, username, password) | ||
88 | |||
89 | const { data } = await server.redundancy.listVideos({ start: 0, count: 100, sort: 'name', target }) | ||
90 | |||
91 | const table = new CliTable3({ | ||
92 | head: [ 'video id', 'video name', 'video url', 'files', 'playlists', 'by instances', 'total size' ] | ||
93 | }) as any | ||
94 | |||
95 | for (const redundancy of data) { | ||
96 | const webVideoFiles = redundancy.redundancies.files | ||
97 | const streamingPlaylists = redundancy.redundancies.streamingPlaylists | ||
98 | |||
99 | let totalSize = '' | ||
100 | if (target === 'remote-videos') { | ||
101 | const tmp = webVideoFiles.concat(streamingPlaylists) | ||
102 | .reduce((a, b) => a + b.size, 0) | ||
103 | |||
104 | // FIXME: don't use external dependency to stringify bytes: we already have the functions in the client | ||
105 | totalSize = bytes(tmp) | ||
106 | } | ||
107 | |||
108 | const instances = uniqify( | ||
109 | webVideoFiles.concat(streamingPlaylists) | ||
110 | .map(r => r.fileUrl) | ||
111 | .map(u => new URL(u).host) | ||
112 | ) | ||
113 | |||
114 | table.push([ | ||
115 | redundancy.id.toString(), | ||
116 | redundancy.name, | ||
117 | redundancy.url, | ||
118 | webVideoFiles.length, | ||
119 | streamingPlaylists.length, | ||
120 | instances.join('\n'), | ||
121 | totalSize | ||
122 | ]) | ||
123 | } | ||
124 | |||
125 | console.log(table.toString()) | ||
126 | } | ||
127 | |||
128 | async function addRedundancyCLI (options: { video: number } & CommonProgramOptions) { | ||
129 | const { url, username, password } = await getServerCredentials(options) | ||
130 | const server = buildServer(url) | ||
131 | await assignToken(server, username, password) | ||
132 | |||
133 | if (!options.video || isNaN(options.video)) { | ||
134 | throw new Error('You need to specify the video id to duplicate and it should be a number.') | ||
135 | } | ||
136 | |||
137 | try { | ||
138 | await server.redundancy.addVideo({ videoId: options.video }) | ||
139 | |||
140 | console.log('Video will be duplicated by your instance!') | ||
141 | } catch (err) { | ||
142 | if (err.message.includes(HttpStatusCode.CONFLICT_409)) { | ||
143 | throw new Error('This video is already duplicated by your instance.') | ||
144 | } | ||
145 | |||
146 | if (err.message.includes(HttpStatusCode.NOT_FOUND_404)) { | ||
147 | throw new Error('This video id does not exist.') | ||
148 | } | ||
149 | |||
150 | throw err | ||
151 | } | ||
152 | } | ||
153 | |||
154 | async function removeRedundancyCLI (options: CommonProgramOptions & { video: number }) { | ||
155 | const { url, username, password } = await getServerCredentials(options) | ||
156 | const server = buildServer(url) | ||
157 | await assignToken(server, username, password) | ||
158 | |||
159 | if (!options.video || isNaN(options.video)) { | ||
160 | throw new Error('You need to specify the video id to remove from your redundancies') | ||
161 | } | ||
162 | |||
163 | const videoId = forceNumber(options.video) | ||
164 | |||
165 | const myVideoRedundancies = await server.redundancy.listVideos({ target: 'my-videos' }) | ||
166 | let videoRedundancy = myVideoRedundancies.data.find(r => videoId === r.id) | ||
167 | |||
168 | if (!videoRedundancy) { | ||
169 | const remoteVideoRedundancies = await server.redundancy.listVideos({ target: 'remote-videos' }) | ||
170 | videoRedundancy = remoteVideoRedundancies.data.find(r => videoId === r.id) | ||
171 | } | ||
172 | |||
173 | if (!videoRedundancy) { | ||
174 | throw new Error('Video redundancy not found.') | ||
175 | } | ||
176 | |||
177 | const ids = videoRedundancy.redundancies.files | ||
178 | .concat(videoRedundancy.redundancies.streamingPlaylists) | ||
179 | .map(r => r.id) | ||
180 | |||
181 | for (const id of ids) { | ||
182 | await server.redundancy.removeVideo({ redundancyId: id }) | ||
183 | } | ||
184 | |||
185 | console.log('Video redundancy removed!') | ||
186 | } | ||
diff --git a/apps/peertube-cli/src/peertube-upload.ts b/apps/peertube-cli/src/peertube-upload.ts new file mode 100644 index 000000000..443f8ce1f --- /dev/null +++ b/apps/peertube-cli/src/peertube-upload.ts | |||
@@ -0,0 +1,167 @@ | |||
1 | import { access, constants } from 'fs/promises' | ||
2 | import { isAbsolute } from 'path' | ||
3 | import { inspect } from 'util' | ||
4 | import { Command } from '@commander-js/extra-typings' | ||
5 | import { VideoPrivacy } from '@peertube/peertube-models' | ||
6 | import { PeerTubeServer } from '@peertube/peertube-server-commands' | ||
7 | import { assignToken, buildServer, getServerCredentials, listOptions } from './shared/index.js' | ||
8 | |||
9 | type UploadOptions = { | ||
10 | url?: string | ||
11 | username?: string | ||
12 | password?: string | ||
13 | thumbnail?: string | ||
14 | preview?: string | ||
15 | file?: string | ||
16 | videoName?: string | ||
17 | category?: string | ||
18 | licence?: string | ||
19 | language?: string | ||
20 | tags?: string | ||
21 | nsfw?: true | ||
22 | videoDescription?: string | ||
23 | privacy?: number | ||
24 | channelName?: string | ||
25 | noCommentsEnabled?: true | ||
26 | support?: string | ||
27 | noWaitTranscoding?: true | ||
28 | noDownloadEnabled?: true | ||
29 | } | ||
30 | |||
31 | export function defineUploadProgram () { | ||
32 | const program = new Command('upload') | ||
33 | .description('Upload a video on a PeerTube instance') | ||
34 | .alias('up') | ||
35 | |||
36 | program | ||
37 | .option('-u, --url <url>', 'Server url') | ||
38 | .option('-U, --username <username>', 'Username') | ||
39 | .option('-p, --password <token>', 'Password') | ||
40 | .option('-b, --thumbnail <thumbnailPath>', 'Thumbnail path') | ||
41 | .option('-v, --preview <previewPath>', 'Preview path') | ||
42 | .option('-f, --file <file>', 'Video absolute file path') | ||
43 | .option('-n, --video-name <name>', 'Video name') | ||
44 | .option('-c, --category <category_number>', 'Category number') | ||
45 | .option('-l, --licence <licence_number>', 'Licence number') | ||
46 | .option('-L, --language <language_code>', 'Language ISO 639 code (fr or en...)') | ||
47 | .option('-t, --tags <tags>', 'Video tags', listOptions) | ||
48 | .option('-N, --nsfw', 'Video is Not Safe For Work') | ||
49 | .option('-d, --video-description <description>', 'Video description') | ||
50 | .option('-P, --privacy <privacy_number>', 'Privacy', parseInt) | ||
51 | .option('-C, --channel-name <channel_name>', 'Channel name') | ||
52 | .option('--no-comments-enabled', 'Disable video comments') | ||
53 | .option('-s, --support <support>', 'Video support text') | ||
54 | .option('--no-wait-transcoding', 'Do not wait transcoding before publishing the video') | ||
55 | .option('--no-download-enabled', 'Disable video download') | ||
56 | .option('-v, --verbose <verbose>', 'Verbosity, from 0/\'error\' to 4/\'debug\'', 'info') | ||
57 | .action(async options => { | ||
58 | try { | ||
59 | const { url, username, password } = await getServerCredentials(options) | ||
60 | |||
61 | if (!options.videoName || !options.file) { | ||
62 | if (!options.videoName) console.error('--video-name is required.') | ||
63 | if (!options.file) console.error('--file is required.') | ||
64 | |||
65 | process.exit(-1) | ||
66 | } | ||
67 | |||
68 | if (isAbsolute(options.file) === false) { | ||
69 | console.error('File path should be absolute.') | ||
70 | process.exit(-1) | ||
71 | } | ||
72 | |||
73 | await run({ ...options, url, username, password }) | ||
74 | } catch (err) { | ||
75 | console.error('Cannot upload video: ' + err.message) | ||
76 | process.exit(-1) | ||
77 | } | ||
78 | }) | ||
79 | |||
80 | return program | ||
81 | } | ||
82 | |||
83 | // --------------------------------------------------------------------------- | ||
84 | // Private | ||
85 | // --------------------------------------------------------------------------- | ||
86 | |||
87 | async function run (options: UploadOptions) { | ||
88 | const { url, username, password } = options | ||
89 | |||
90 | const server = buildServer(url) | ||
91 | await assignToken(server, username, password) | ||
92 | |||
93 | await access(options.file, constants.F_OK) | ||
94 | |||
95 | console.log('Uploading %s video...', options.videoName) | ||
96 | |||
97 | const baseAttributes = await buildVideoAttributesFromCommander(server, options) | ||
98 | |||
99 | const attributes = { | ||
100 | ...baseAttributes, | ||
101 | |||
102 | fixture: options.file, | ||
103 | thumbnailfile: options.thumbnail, | ||
104 | previewfile: options.preview | ||
105 | } | ||
106 | |||
107 | try { | ||
108 | await server.videos.upload({ attributes }) | ||
109 | console.log(`Video ${options.videoName} uploaded.`) | ||
110 | process.exit(0) | ||
111 | } catch (err) { | ||
112 | const message = err.message || '' | ||
113 | if (message.includes('413')) { | ||
114 | console.error('Aborted: user quota is exceeded or video file is too big for this PeerTube instance.') | ||
115 | } else { | ||
116 | console.error(inspect(err)) | ||
117 | } | ||
118 | |||
119 | process.exit(-1) | ||
120 | } | ||
121 | } | ||
122 | |||
123 | async function buildVideoAttributesFromCommander (server: PeerTubeServer, options: UploadOptions, defaultAttributes: any = {}) { | ||
124 | const defaultBooleanAttributes = { | ||
125 | nsfw: false, | ||
126 | commentsEnabled: true, | ||
127 | downloadEnabled: true, | ||
128 | waitTranscoding: true | ||
129 | } | ||
130 | |||
131 | const booleanAttributes: { [id in keyof typeof defaultBooleanAttributes]: boolean } | {} = {} | ||
132 | |||
133 | for (const key of Object.keys(defaultBooleanAttributes)) { | ||
134 | if (options[key] !== undefined) { | ||
135 | booleanAttributes[key] = options[key] | ||
136 | } else if (defaultAttributes[key] !== undefined) { | ||
137 | booleanAttributes[key] = defaultAttributes[key] | ||
138 | } else { | ||
139 | booleanAttributes[key] = defaultBooleanAttributes[key] | ||
140 | } | ||
141 | } | ||
142 | |||
143 | const videoAttributes = { | ||
144 | name: options.videoName || defaultAttributes.name, | ||
145 | category: options.category || defaultAttributes.category || undefined, | ||
146 | licence: options.licence || defaultAttributes.licence || undefined, | ||
147 | language: options.language || defaultAttributes.language || undefined, | ||
148 | privacy: options.privacy || defaultAttributes.privacy || VideoPrivacy.PUBLIC, | ||
149 | support: options.support || defaultAttributes.support || undefined, | ||
150 | description: options.videoDescription || defaultAttributes.description || undefined, | ||
151 | tags: options.tags || defaultAttributes.tags || undefined | ||
152 | } | ||
153 | |||
154 | Object.assign(videoAttributes, booleanAttributes) | ||
155 | |||
156 | if (options.channelName) { | ||
157 | const videoChannel = await server.channels.get({ channelName: options.channelName }) | ||
158 | |||
159 | Object.assign(videoAttributes, { channelId: videoChannel.id }) | ||
160 | |||
161 | if (!videoAttributes.support && videoChannel.support) { | ||
162 | Object.assign(videoAttributes, { support: videoChannel.support }) | ||
163 | } | ||
164 | } | ||
165 | |||
166 | return videoAttributes | ||
167 | } | ||
diff --git a/apps/peertube-cli/src/peertube.ts b/apps/peertube-cli/src/peertube.ts new file mode 100644 index 000000000..e3565bb1a --- /dev/null +++ b/apps/peertube-cli/src/peertube.ts | |||
@@ -0,0 +1,64 @@ | |||
1 | #!/usr/bin/env node | ||
2 | |||
3 | import { Command } from '@commander-js/extra-typings' | ||
4 | import { defineAuthProgram } from './peertube-auth.js' | ||
5 | import { defineGetAccessProgram } from './peertube-get-access-token.js' | ||
6 | import { definePluginsProgram } from './peertube-plugins.js' | ||
7 | import { defineRedundancyProgram } from './peertube-redundancy.js' | ||
8 | import { defineUploadProgram } from './peertube-upload.js' | ||
9 | import { getSettings, version } from './shared/index.js' | ||
10 | |||
11 | const program = new Command() | ||
12 | |||
13 | program | ||
14 | .version(version, '-v, --version') | ||
15 | .usage('[command] [options]') | ||
16 | |||
17 | program.addCommand(defineAuthProgram()) | ||
18 | program.addCommand(defineUploadProgram()) | ||
19 | program.addCommand(defineRedundancyProgram()) | ||
20 | program.addCommand(definePluginsProgram()) | ||
21 | program.addCommand(defineGetAccessProgram()) | ||
22 | |||
23 | // help on no command | ||
24 | if (!process.argv.slice(2).length) { | ||
25 | const logo = 'â–‘Pâ–‘eâ–‘eâ–‘râ–‘Tâ–‘uâ–‘bâ–‘eâ–‘' | ||
26 | console.log(` | ||
27 | ___/),.._ ` + logo + ` | ||
28 | /' ,. ."'._ | ||
29 | ( "' '-.__"-._ ,- | ||
30 | \\'='='), "\\ -._-"-. -"/ | ||
31 | / ""/"\\,_\\,__"" _" /,- | ||
32 | / / -" _/"/ | ||
33 | / | ._\\\\ |\\ |_.".-" / | ||
34 | / | __\\)|)|),/|_." _,." | ||
35 | / \\_." " ") | ).-""---''-- | ||
36 | ( "/.""7__-""'' | ||
37 | | " ."._--._ | ||
38 | \\ \\ (_ __ "" ".,_ | ||
39 | \\.,. \\ "" -"".-" | ||
40 | ".,_, (",_-,,,-".- | ||
41 | "'-,\\_ __,-" | ||
42 | ",)" ") | ||
43 | /"\\-" | ||
44 | ,"\\/ | ||
45 | _,.__/"\\/_ (the CLI for red chocobos) | ||
46 | / \\) "./, ". | ||
47 | --/---"---" "-) )---- by Chocobozzz et al.\n`) | ||
48 | } | ||
49 | |||
50 | getSettings() | ||
51 | .then(settings => { | ||
52 | const state = (settings.default === undefined || settings.default === -1) | ||
53 | ? 'no instance selected, commands will require explicit arguments' | ||
54 | : 'instance ' + settings.remotes[settings.default] + ' selected' | ||
55 | |||
56 | program | ||
57 | .addHelpText('after', '\n\n State: ' + state + '\n\n' + | ||
58 | ' Examples:\n\n' + | ||
59 | ' $ peertube auth add -u "PEERTUBE_URL" -U "PEERTUBE_USER" --password "PEERTUBE_PASSWORD"\n' + | ||
60 | ' $ peertube up <videoFile>\n' | ||
61 | ) | ||
62 | .parse(process.argv) | ||
63 | }) | ||
64 | .catch(err => console.error(err)) | ||
diff --git a/apps/peertube-cli/src/shared/cli.ts b/apps/peertube-cli/src/shared/cli.ts new file mode 100644 index 000000000..080eb8237 --- /dev/null +++ b/apps/peertube-cli/src/shared/cli.ts | |||
@@ -0,0 +1,195 @@ | |||
1 | import applicationConfig from 'application-config' | ||
2 | import { Netrc } from 'netrc-parser' | ||
3 | import { join } from 'path' | ||
4 | import { createLogger, format, transports } from 'winston' | ||
5 | import { UserRole } from '@peertube/peertube-models' | ||
6 | import { getAppNumber, isTestInstance, root } from '@peertube/peertube-node-utils' | ||
7 | import { PeerTubeServer } from '@peertube/peertube-server-commands' | ||
8 | |||
9 | export type CommonProgramOptions = { | ||
10 | url?: string | ||
11 | username?: string | ||
12 | password?: string | ||
13 | } | ||
14 | |||
15 | let configName = 'PeerTube/CLI' | ||
16 | if (isTestInstance()) configName += `-${getAppNumber()}` | ||
17 | |||
18 | const config = applicationConfig(configName) | ||
19 | |||
20 | const version: string = process.env.PACKAGE_VERSION | ||
21 | |||
22 | async function getAdminTokenOrDie (server: PeerTubeServer, username: string, password: string) { | ||
23 | const token = await server.login.getAccessToken(username, password) | ||
24 | const me = await server.users.getMyInfo({ token }) | ||
25 | |||
26 | if (me.role.id !== UserRole.ADMINISTRATOR) { | ||
27 | console.error('You must be an administrator.') | ||
28 | process.exit(-1) | ||
29 | } | ||
30 | |||
31 | return token | ||
32 | } | ||
33 | |||
34 | interface Settings { | ||
35 | remotes: any[] | ||
36 | default: number | ||
37 | } | ||
38 | |||
39 | async function getSettings () { | ||
40 | const defaultSettings: Settings = { | ||
41 | remotes: [], | ||
42 | default: -1 | ||
43 | } | ||
44 | |||
45 | const data = await config.read() as Promise<Settings> | ||
46 | |||
47 | return Object.keys(data).length === 0 | ||
48 | ? defaultSettings | ||
49 | : data | ||
50 | } | ||
51 | |||
52 | async function getNetrc () { | ||
53 | const netrc = isTestInstance() | ||
54 | ? new Netrc(join(root(import.meta.url), 'test' + getAppNumber(), 'netrc')) | ||
55 | : new Netrc() | ||
56 | |||
57 | await netrc.load() | ||
58 | |||
59 | return netrc | ||
60 | } | ||
61 | |||
62 | function writeSettings (settings: Settings) { | ||
63 | return config.write(settings) | ||
64 | } | ||
65 | |||
66 | function deleteSettings () { | ||
67 | return config.trash() | ||
68 | } | ||
69 | |||
70 | function getRemoteObjectOrDie ( | ||
71 | options: CommonProgramOptions, | ||
72 | settings: Settings, | ||
73 | netrc: Netrc | ||
74 | ): { url: string, username: string, password: string } { | ||
75 | |||
76 | function exitIfNoOptions (optionNames: string[], errorPrefix: string = '') { | ||
77 | let exit = false | ||
78 | |||
79 | for (const key of optionNames) { | ||
80 | if (!options[key]) { | ||
81 | if (exit === false && errorPrefix) console.error(errorPrefix) | ||
82 | |||
83 | console.error(`--${key} field is required`) | ||
84 | exit = true | ||
85 | } | ||
86 | } | ||
87 | |||
88 | if (exit) process.exit(-1) | ||
89 | } | ||
90 | |||
91 | // If username or password are specified, both are mandatory | ||
92 | if (options.username || options.password) { | ||
93 | exitIfNoOptions([ 'username', 'password' ]) | ||
94 | } | ||
95 | |||
96 | // If no available machines, url, username and password args are mandatory | ||
97 | if (Object.keys(netrc.machines).length === 0) { | ||
98 | exitIfNoOptions([ 'url', 'username', 'password' ], 'No account found in netrc') | ||
99 | } | ||
100 | |||
101 | if (settings.remotes.length === 0 || settings.default === -1) { | ||
102 | exitIfNoOptions([ 'url' ], 'No default instance found') | ||
103 | } | ||
104 | |||
105 | let url: string = options.url | ||
106 | let username: string = options.username | ||
107 | let password: string = options.password | ||
108 | |||
109 | if (!url && settings.default !== -1) url = settings.remotes[settings.default] | ||
110 | |||
111 | const machine = netrc.machines[url] | ||
112 | if ((!username || !password) && !machine) { | ||
113 | console.error('Cannot find existing configuration for %s.', url) | ||
114 | process.exit(-1) | ||
115 | } | ||
116 | |||
117 | if (!username && machine) username = machine.login | ||
118 | if (!password && machine) password = machine.password | ||
119 | |||
120 | return { url, username, password } | ||
121 | } | ||
122 | |||
123 | function listOptions (val: any) { | ||
124 | return val.split(',') | ||
125 | } | ||
126 | |||
127 | function getServerCredentials (options: CommonProgramOptions) { | ||
128 | return Promise.all([ getSettings(), getNetrc() ]) | ||
129 | .then(([ settings, netrc ]) => { | ||
130 | return getRemoteObjectOrDie(options, settings, netrc) | ||
131 | }) | ||
132 | } | ||
133 | |||
134 | function buildServer (url: string) { | ||
135 | return new PeerTubeServer({ url }) | ||
136 | } | ||
137 | |||
138 | async function assignToken (server: PeerTubeServer, username: string, password: string) { | ||
139 | const bodyClient = await server.login.getClient() | ||
140 | const client = { id: bodyClient.client_id, secret: bodyClient.client_secret } | ||
141 | |||
142 | const body = await server.login.login({ client, user: { username, password } }) | ||
143 | |||
144 | server.accessToken = body.access_token | ||
145 | } | ||
146 | |||
147 | function getLogger (logLevel = 'info') { | ||
148 | const logLevels = { | ||
149 | 0: 0, | ||
150 | error: 0, | ||
151 | 1: 1, | ||
152 | warn: 1, | ||
153 | 2: 2, | ||
154 | info: 2, | ||
155 | 3: 3, | ||
156 | verbose: 3, | ||
157 | 4: 4, | ||
158 | debug: 4 | ||
159 | } | ||
160 | |||
161 | const logger = createLogger({ | ||
162 | levels: logLevels, | ||
163 | format: format.combine( | ||
164 | format.splat(), | ||
165 | format.simple() | ||
166 | ), | ||
167 | transports: [ | ||
168 | new (transports.Console)({ | ||
169 | level: logLevel | ||
170 | }) | ||
171 | ] | ||
172 | }) | ||
173 | |||
174 | return logger | ||
175 | } | ||
176 | |||
177 | // --------------------------------------------------------------------------- | ||
178 | |||
179 | export { | ||
180 | version, | ||
181 | getLogger, | ||
182 | getSettings, | ||
183 | getNetrc, | ||
184 | getRemoteObjectOrDie, | ||
185 | writeSettings, | ||
186 | deleteSettings, | ||
187 | |||
188 | getServerCredentials, | ||
189 | |||
190 | listOptions, | ||
191 | |||
192 | getAdminTokenOrDie, | ||
193 | buildServer, | ||
194 | assignToken | ||
195 | } | ||
diff --git a/apps/peertube-cli/src/shared/index.ts b/apps/peertube-cli/src/shared/index.ts new file mode 100644 index 000000000..a1fc9470b --- /dev/null +++ b/apps/peertube-cli/src/shared/index.ts | |||
@@ -0,0 +1 @@ | |||
export * from './cli.js' | |||
diff --git a/apps/peertube-cli/tsconfig.json b/apps/peertube-cli/tsconfig.json new file mode 100644 index 000000000..636bdb95a --- /dev/null +++ b/apps/peertube-cli/tsconfig.json | |||
@@ -0,0 +1,15 @@ | |||
1 | { | ||
2 | "extends": "../../tsconfig.base.json", | ||
3 | "compilerOptions": { | ||
4 | "baseUrl": "./", | ||
5 | "outDir": "./dist", | ||
6 | "rootDir": "src", | ||
7 | "tsBuildInfoFile": "./dist/.tsbuildinfo" | ||
8 | }, | ||
9 | "references": [ | ||
10 | { "path": "../../packages/core-utils" }, | ||
11 | { "path": "../../packages/models" }, | ||
12 | { "path": "../../packages/node-utils" }, | ||
13 | { "path": "../../packages/server-commands" } | ||
14 | ] | ||
15 | } | ||
diff --git a/apps/peertube-cli/yarn.lock b/apps/peertube-cli/yarn.lock new file mode 100644 index 000000000..76b36ee73 --- /dev/null +++ b/apps/peertube-cli/yarn.lock | |||
@@ -0,0 +1,374 @@ | |||
1 | # THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. | ||
2 | # yarn lockfile v1 | ||
3 | |||
4 | |||
5 | "@babel/code-frame@^7.0.0": | ||
6 | version "7.22.10" | ||
7 | resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.22.10.tgz#1c20e612b768fefa75f6e90d6ecb86329247f0a3" | ||
8 | integrity sha512-/KKIMG4UEL35WmI9OlvMhurwtytjvXoFcGNrOvyG9zIzA8YmPjVtIZUf7b05+TPO7G7/GEmLHDaoCgACHl9hhA== | ||
9 | dependencies: | ||
10 | "@babel/highlight" "^7.22.10" | ||
11 | chalk "^2.4.2" | ||
12 | |||
13 | "@babel/helper-validator-identifier@^7.22.5": | ||
14 | version "7.22.5" | ||
15 | resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.22.5.tgz#9544ef6a33999343c8740fa51350f30eeaaaf193" | ||
16 | integrity sha512-aJXu+6lErq8ltp+JhkJUfk1MTGyuA4v7f3pA+BJ5HLfNC6nAQ0Cpi9uOquUj8Hehg0aUiHzWQbOVJGao6ztBAQ== | ||
17 | |||
18 | "@babel/highlight@^7.22.10": | ||
19 | version "7.22.10" | ||
20 | resolved "https://registry.yarnpkg.com/@babel/highlight/-/highlight-7.22.10.tgz#02a3f6d8c1cb4521b2fd0ab0da8f4739936137d7" | ||
21 | integrity sha512-78aUtVcT7MUscr0K5mIEnkwxPE0MaxkR5RxRwuHaQ+JuU5AmTPhY+do2mdzVTnIJJpyBglql2pehuBIWHug+WQ== | ||
22 | dependencies: | ||
23 | "@babel/helper-validator-identifier" "^7.22.5" | ||
24 | chalk "^2.4.2" | ||
25 | js-tokens "^4.0.0" | ||
26 | |||
27 | "@colors/colors@1.5.0": | ||
28 | version "1.5.0" | ||
29 | resolved "https://registry.yarnpkg.com/@colors/colors/-/colors-1.5.0.tgz#bb504579c1cae923e6576a4f5da43d25f97bdbd9" | ||
30 | integrity sha512-ooWCrlZP11i8GImSjTHYHLkvFDP48nS4+204nGb1RiX/WXYHmJA2III9/e2DWVabCESdW7hBAEzHRqUn9OUVvQ== | ||
31 | |||
32 | ansi-regex@^5.0.1: | ||
33 | version "5.0.1" | ||
34 | resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-5.0.1.tgz#082cb2c89c9fe8659a311a53bd6a4dc5301db304" | ||
35 | integrity sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ== | ||
36 | |||
37 | ansi-styles@^3.2.1: | ||
38 | version "3.2.1" | ||
39 | resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-3.2.1.tgz#41fbb20243e50b12be0f04b8dedbf07520ce841d" | ||
40 | integrity sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA== | ||
41 | dependencies: | ||
42 | color-convert "^1.9.0" | ||
43 | |||
44 | application-config-path@^0.1.0: | ||
45 | version "0.1.1" | ||
46 | resolved "https://registry.yarnpkg.com/application-config-path/-/application-config-path-0.1.1.tgz#8b5ac64ff6afdd9bd70ce69f6f64b6998f5f756e" | ||
47 | integrity sha512-zy9cHePtMP0YhwG+CfHm0bgwdnga2X3gZexpdCwEj//dpb+TKajtiC8REEUJUSq6Ab4f9cgNy2l8ObXzCXFkEw== | ||
48 | |||
49 | application-config@^2.0.0: | ||
50 | version "2.0.0" | ||
51 | resolved "https://registry.yarnpkg.com/application-config/-/application-config-2.0.0.tgz#15b4d54d61c0c082f9802227e3e85de876b47747" | ||
52 | integrity sha512-NC5/0guSZK3/UgUDfCk/riByXzqz0owL1L3r63JPSBzYk5QALrp3bLxbsR7qeSfvYfFmAhnp3dbqYsW3U9MpZQ== | ||
53 | dependencies: | ||
54 | application-config-path "^0.1.0" | ||
55 | load-json-file "^6.2.0" | ||
56 | write-json-file "^4.2.0" | ||
57 | |||
58 | chalk@^2.4.2: | ||
59 | version "2.4.2" | ||
60 | resolved "https://registry.yarnpkg.com/chalk/-/chalk-2.4.2.tgz#cd42541677a54333cf541a49108c1432b44c9424" | ||
61 | integrity sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ== | ||
62 | dependencies: | ||
63 | ansi-styles "^3.2.1" | ||
64 | escape-string-regexp "^1.0.5" | ||
65 | supports-color "^5.3.0" | ||
66 | |||
67 | cli-table3@^0.6.0: | ||
68 | version "0.6.3" | ||
69 | resolved "https://registry.yarnpkg.com/cli-table3/-/cli-table3-0.6.3.tgz#61ab765aac156b52f222954ffc607a6f01dbeeb2" | ||
70 | integrity sha512-w5Jac5SykAeZJKntOxJCrm63Eg5/4dhMWIcuTbo9rpE+brgaSZo0RuNJZeOyMgsUdhDeojvgyQLmjI+K50ZGyg== | ||
71 | dependencies: | ||
72 | string-width "^4.2.0" | ||
73 | optionalDependencies: | ||
74 | "@colors/colors" "1.5.0" | ||
75 | |||
76 | color-convert@^1.9.0: | ||
77 | version "1.9.3" | ||
78 | resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-1.9.3.tgz#bb71850690e1f136567de629d2d5471deda4c1e8" | ||
79 | integrity sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg== | ||
80 | dependencies: | ||
81 | color-name "1.1.3" | ||
82 | |||
83 | color-name@1.1.3: | ||
84 | version "1.1.3" | ||
85 | resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.3.tgz#a7d0558bd89c42f795dd42328f740831ca53bc25" | ||
86 | integrity sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw== | ||
87 | |||
88 | cross-spawn@^6.0.0: | ||
89 | version "6.0.5" | ||
90 | resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-6.0.5.tgz#4a5ec7c64dfae22c3a14124dbacdee846d80cbc4" | ||
91 | integrity sha512-eTVLrBSt7fjbDygz805pMnstIs2VTBNkRm0qxZd+M7A5XDdxVRWO5MxGBXZhjY4cqLYLdtrGqRf8mBPmzwSpWQ== | ||
92 | dependencies: | ||
93 | nice-try "^1.0.4" | ||
94 | path-key "^2.0.1" | ||
95 | semver "^5.5.0" | ||
96 | shebang-command "^1.2.0" | ||
97 | which "^1.2.9" | ||
98 | |||
99 | debug@^3.1.0: | ||
100 | version "3.2.7" | ||
101 | resolved "https://registry.yarnpkg.com/debug/-/debug-3.2.7.tgz#72580b7e9145fb39b6676f9c5e5fb100b934179a" | ||
102 | integrity sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ== | ||
103 | dependencies: | ||
104 | ms "^2.1.1" | ||
105 | |||
106 | detect-indent@^6.0.0: | ||
107 | version "6.1.0" | ||
108 | resolved "https://registry.yarnpkg.com/detect-indent/-/detect-indent-6.1.0.tgz#592485ebbbf6b3b1ab2be175c8393d04ca0d57e6" | ||
109 | integrity sha512-reYkTUJAZb9gUuZ2RvVCNhVHdg62RHnJ7WJl8ftMi4diZ6NWlciOzQN88pUhSELEwflJht4oQDv0F0BMlwaYtA== | ||
110 | |||
111 | emoji-regex@^8.0.0: | ||
112 | version "8.0.0" | ||
113 | resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-8.0.0.tgz#e818fd69ce5ccfcb404594f842963bf53164cc37" | ||
114 | integrity sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A== | ||
115 | |||
116 | error-ex@^1.3.1: | ||
117 | version "1.3.2" | ||
118 | resolved "https://registry.yarnpkg.com/error-ex/-/error-ex-1.3.2.tgz#b4ac40648107fdcdcfae242f428bea8a14d4f1bf" | ||
119 | integrity sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g== | ||
120 | dependencies: | ||
121 | is-arrayish "^0.2.1" | ||
122 | |||
123 | escape-string-regexp@^1.0.5: | ||
124 | version "1.0.5" | ||
125 | resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz#1b61c0562190a8dff6ae3bb2cf0200ca130b86d4" | ||
126 | integrity sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg== | ||
127 | |||
128 | execa@^0.10.0: | ||
129 | version "0.10.0" | ||
130 | resolved "https://registry.yarnpkg.com/execa/-/execa-0.10.0.tgz#ff456a8f53f90f8eccc71a96d11bdfc7f082cb50" | ||
131 | integrity sha512-7XOMnz8Ynx1gGo/3hyV9loYNPWM94jG3+3T3Y8tsfSstFmETmENCMU/A/zj8Lyaj1lkgEepKepvd6240tBRvlw== | ||
132 | dependencies: | ||
133 | cross-spawn "^6.0.0" | ||
134 | get-stream "^3.0.0" | ||
135 | is-stream "^1.1.0" | ||
136 | npm-run-path "^2.0.0" | ||
137 | p-finally "^1.0.0" | ||
138 | signal-exit "^3.0.0" | ||
139 | strip-eof "^1.0.0" | ||
140 | |||
141 | get-stream@^3.0.0: | ||
142 | version "3.0.0" | ||
143 | resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-3.0.0.tgz#8e943d1358dc37555054ecbe2edb05aa174ede14" | ||
144 | integrity sha512-GlhdIUuVakc8SJ6kK0zAFbiGzRFzNnY4jUuEbV9UROo4Y+0Ny4fjvcZFVTeDA4odpFyOQzaw6hXukJSq/f28sQ== | ||
145 | |||
146 | graceful-fs@^4.1.15: | ||
147 | version "4.2.11" | ||
148 | resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.11.tgz#4183e4e8bf08bb6e05bbb2f7d2e0c8f712ca40e3" | ||
149 | integrity sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ== | ||
150 | |||
151 | has-flag@^3.0.0: | ||
152 | version "3.0.0" | ||
153 | resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-3.0.0.tgz#b5d454dc2199ae225699f3467e5a07f3b955bafd" | ||
154 | integrity sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw== | ||
155 | |||
156 | imurmurhash@^0.1.4: | ||
157 | version "0.1.4" | ||
158 | resolved "https://registry.yarnpkg.com/imurmurhash/-/imurmurhash-0.1.4.tgz#9218b9b2b928a238b13dc4fb6b6d576f231453ea" | ||
159 | integrity sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA== | ||
160 | |||
161 | is-arrayish@^0.2.1: | ||
162 | version "0.2.1" | ||
163 | resolved "https://registry.yarnpkg.com/is-arrayish/-/is-arrayish-0.2.1.tgz#77c99840527aa8ecb1a8ba697b80645a7a926a9d" | ||
164 | integrity sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg== | ||
165 | |||
166 | is-fullwidth-code-point@^3.0.0: | ||
167 | version "3.0.0" | ||
168 | resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz#f116f8064fe90b3f7844a38997c0b75051269f1d" | ||
169 | integrity sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg== | ||
170 | |||
171 | is-plain-obj@^2.0.0: | ||
172 | version "2.1.0" | ||
173 | resolved "https://registry.yarnpkg.com/is-plain-obj/-/is-plain-obj-2.1.0.tgz#45e42e37fccf1f40da8e5f76ee21515840c09287" | ||
174 | integrity sha512-YWnfyRwxL/+SsrWYfOpUtz5b3YD+nyfkHvjbcanzk8zgyO4ASD67uVMRt8k5bM4lLMDnXfriRhOpemw+NfT1eA== | ||
175 | |||
176 | is-stream@^1.1.0: | ||
177 | version "1.1.0" | ||
178 | resolved "https://registry.yarnpkg.com/is-stream/-/is-stream-1.1.0.tgz#12d4a3dd4e68e0b79ceb8dbc84173ae80d91ca44" | ||
179 | integrity sha512-uQPm8kcs47jx38atAcWTVxyltQYoPT68y9aWYdV6yWXSyW8mzSat0TL6CiWdZeCdF3KrAvpVtnHbTv4RN+rqdQ== | ||
180 | |||
181 | is-typedarray@^1.0.0: | ||
182 | version "1.0.0" | ||
183 | resolved "https://registry.yarnpkg.com/is-typedarray/-/is-typedarray-1.0.0.tgz#e479c80858df0c1b11ddda6940f96011fcda4a9a" | ||
184 | integrity sha512-cyA56iCMHAh5CdzjJIa4aohJyeO1YbwLi3Jc35MmRU6poroFjIGZzUzupGiRPOjgHg9TLu43xbpwXk523fMxKA== | ||
185 | |||
186 | isexe@^2.0.0: | ||
187 | version "2.0.0" | ||
188 | resolved "https://registry.yarnpkg.com/isexe/-/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10" | ||
189 | integrity sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw== | ||
190 | |||
191 | js-tokens@^4.0.0: | ||
192 | version "4.0.0" | ||
193 | resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499" | ||
194 | integrity sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ== | ||
195 | |||
196 | json-parse-even-better-errors@^2.3.0: | ||
197 | version "2.3.1" | ||
198 | resolved "https://registry.yarnpkg.com/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz#7c47805a94319928e05777405dc12e1f7a4ee02d" | ||
199 | integrity sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w== | ||
200 | |||
201 | lines-and-columns@^1.1.6: | ||
202 | version "1.2.4" | ||
203 | resolved "https://registry.yarnpkg.com/lines-and-columns/-/lines-and-columns-1.2.4.tgz#eca284f75d2965079309dc0ad9255abb2ebc1632" | ||
204 | integrity sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg== | ||
205 | |||
206 | load-json-file@^6.2.0: | ||
207 | version "6.2.0" | ||
208 | resolved "https://registry.yarnpkg.com/load-json-file/-/load-json-file-6.2.0.tgz#5c7770b42cafa97074ca2848707c61662f4251a1" | ||
209 | integrity sha512-gUD/epcRms75Cw8RT1pUdHugZYM5ce64ucs2GEISABwkRsOQr0q2wm/MV2TKThycIe5e0ytRweW2RZxclogCdQ== | ||
210 | dependencies: | ||
211 | graceful-fs "^4.1.15" | ||
212 | parse-json "^5.0.0" | ||
213 | strip-bom "^4.0.0" | ||
214 | type-fest "^0.6.0" | ||
215 | |||
216 | make-dir@^3.0.0: | ||
217 | version "3.1.0" | ||
218 | resolved "https://registry.yarnpkg.com/make-dir/-/make-dir-3.1.0.tgz#415e967046b3a7f1d185277d84aa58203726a13f" | ||
219 | integrity sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw== | ||
220 | dependencies: | ||
221 | semver "^6.0.0" | ||
222 | |||
223 | ms@^2.1.1: | ||
224 | version "2.1.3" | ||
225 | resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.3.tgz#574c8138ce1d2b5861f0b44579dbadd60c6615b2" | ||
226 | integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA== | ||
227 | |||
228 | netrc-parser@^3.1.6: | ||
229 | version "3.1.6" | ||
230 | resolved "https://registry.yarnpkg.com/netrc-parser/-/netrc-parser-3.1.6.tgz#7243c9ec850b8e805b9bdc7eae7b1450d4a96e72" | ||
231 | integrity sha512-lY+fmkqSwntAAjfP63jB4z5p5WbuZwyMCD3pInT7dpHU/Gc6Vv90SAC6A0aNiqaRGHiuZFBtiwu+pu8W/Eyotw== | ||
232 | dependencies: | ||
233 | debug "^3.1.0" | ||
234 | execa "^0.10.0" | ||
235 | |||
236 | nice-try@^1.0.4: | ||
237 | version "1.0.5" | ||
238 | resolved "https://registry.yarnpkg.com/nice-try/-/nice-try-1.0.5.tgz#a3378a7696ce7d223e88fc9b764bd7ef1089e366" | ||
239 | integrity sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ== | ||
240 | |||
241 | npm-run-path@^2.0.0: | ||
242 | version "2.0.2" | ||
243 | resolved "https://registry.yarnpkg.com/npm-run-path/-/npm-run-path-2.0.2.tgz#35a9232dfa35d7067b4cb2ddf2357b1871536c5f" | ||
244 | integrity sha512-lJxZYlT4DW/bRUtFh1MQIWqmLwQfAxnqWG4HhEdjMlkrJYnJn0Jrr2u3mgxqaWsdiBc76TYkTG/mhrnYTuzfHw== | ||
245 | dependencies: | ||
246 | path-key "^2.0.0" | ||
247 | |||
248 | p-finally@^1.0.0: | ||
249 | version "1.0.0" | ||
250 | resolved "https://registry.yarnpkg.com/p-finally/-/p-finally-1.0.0.tgz#3fbcfb15b899a44123b34b6dcc18b724336a2cae" | ||
251 | integrity sha512-LICb2p9CB7FS+0eR1oqWnHhp0FljGLZCWBE9aix0Uye9W8LTQPwMTYVGWQWIw9RdQiDg4+epXQODwIYJtSJaow== | ||
252 | |||
253 | parse-json@^5.0.0: | ||
254 | version "5.2.0" | ||
255 | resolved "https://registry.yarnpkg.com/parse-json/-/parse-json-5.2.0.tgz#c76fc66dee54231c962b22bcc8a72cf2f99753cd" | ||
256 | integrity sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg== | ||
257 | dependencies: | ||
258 | "@babel/code-frame" "^7.0.0" | ||
259 | error-ex "^1.3.1" | ||
260 | json-parse-even-better-errors "^2.3.0" | ||
261 | lines-and-columns "^1.1.6" | ||
262 | |||
263 | path-key@^2.0.0, path-key@^2.0.1: | ||
264 | version "2.0.1" | ||
265 | resolved "https://registry.yarnpkg.com/path-key/-/path-key-2.0.1.tgz#411cadb574c5a140d3a4b1910d40d80cc9f40b40" | ||
266 | integrity sha512-fEHGKCSmUSDPv4uoj8AlD+joPlq3peND+HRYyxFz4KPw4z926S/b8rIuFs2FYJg3BwsxJf6A9/3eIdLaYC+9Dw== | ||
267 | |||
268 | semver@^5.5.0: | ||
269 | version "5.7.2" | ||
270 | resolved "https://registry.yarnpkg.com/semver/-/semver-5.7.2.tgz#48d55db737c3287cd4835e17fa13feace1c41ef8" | ||
271 | integrity sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g== | ||
272 | |||
273 | semver@^6.0.0: | ||
274 | version "6.3.1" | ||
275 | resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.1.tgz#556d2ef8689146e46dcea4bfdd095f3434dffcb4" | ||
276 | integrity sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA== | ||
277 | |||
278 | shebang-command@^1.2.0: | ||
279 | version "1.2.0" | ||
280 | resolved "https://registry.yarnpkg.com/shebang-command/-/shebang-command-1.2.0.tgz#44aac65b695b03398968c39f363fee5deafdf1ea" | ||
281 | integrity sha512-EV3L1+UQWGor21OmnvojK36mhg+TyIKDh3iFBKBohr5xeXIhNBcx8oWdgkTEEQ+BEFFYdLRuqMfd5L84N1V5Vg== | ||
282 | dependencies: | ||
283 | shebang-regex "^1.0.0" | ||
284 | |||
285 | shebang-regex@^1.0.0: | ||
286 | version "1.0.0" | ||
287 | resolved "https://registry.yarnpkg.com/shebang-regex/-/shebang-regex-1.0.0.tgz#da42f49740c0b42db2ca9728571cb190c98efea3" | ||
288 | integrity sha512-wpoSFAxys6b2a2wHZ1XpDSgD7N9iVjg29Ph9uV/uaP9Ex/KXlkTZTeddxDPSYQpgvzKLGJke2UU0AzoGCjNIvQ== | ||
289 | |||
290 | signal-exit@^3.0.0, signal-exit@^3.0.2: | ||
291 | version "3.0.7" | ||
292 | resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.7.tgz#a9a1767f8af84155114eaabd73f99273c8f59ad9" | ||
293 | integrity sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ== | ||
294 | |||
295 | sort-keys@^4.0.0: | ||
296 | version "4.2.0" | ||
297 | resolved "https://registry.yarnpkg.com/sort-keys/-/sort-keys-4.2.0.tgz#6b7638cee42c506fff8c1cecde7376d21315be18" | ||
298 | integrity sha512-aUYIEU/UviqPgc8mHR6IW1EGxkAXpeRETYcrzg8cLAvUPZcpAlleSXHV2mY7G12GphSH6Gzv+4MMVSSkbdteHg== | ||
299 | dependencies: | ||
300 | is-plain-obj "^2.0.0" | ||
301 | |||
302 | string-width@^4.2.0: | ||
303 | version "4.2.3" | ||
304 | resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" | ||
305 | integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== | ||
306 | dependencies: | ||
307 | emoji-regex "^8.0.0" | ||
308 | is-fullwidth-code-point "^3.0.0" | ||
309 | strip-ansi "^6.0.1" | ||
310 | |||
311 | strip-ansi@^6.0.1: | ||
312 | version "6.0.1" | ||
313 | resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" | ||
314 | integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== | ||
315 | dependencies: | ||
316 | ansi-regex "^5.0.1" | ||
317 | |||
318 | strip-bom@^4.0.0: | ||
319 | version "4.0.0" | ||
320 | resolved "https://registry.yarnpkg.com/strip-bom/-/strip-bom-4.0.0.tgz#9c3505c1db45bcedca3d9cf7a16f5c5aa3901878" | ||
321 | integrity sha512-3xurFv5tEgii33Zi8Jtp55wEIILR9eh34FAW00PZf+JnSsTmV/ioewSgQl97JHvgjoRGwPShsWm+IdrxB35d0w== | ||
322 | |||
323 | strip-eof@^1.0.0: | ||
324 | version "1.0.0" | ||
325 | resolved "https://registry.yarnpkg.com/strip-eof/-/strip-eof-1.0.0.tgz#bb43ff5598a6eb05d89b59fcd129c983313606bf" | ||
326 | integrity sha512-7FCwGGmx8mD5xQd3RPUvnSpUXHM3BWuzjtpD4TXsfcZ9EL4azvVVUscFYwD9nx8Kh+uCBC00XBtAykoMHwTh8Q== | ||
327 | |||
328 | supports-color@^5.3.0: | ||
329 | version "5.5.0" | ||
330 | resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-5.5.0.tgz#e2e69a44ac8772f78a1ec0b35b689df6530efc8f" | ||
331 | integrity sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow== | ||
332 | dependencies: | ||
333 | has-flag "^3.0.0" | ||
334 | |||
335 | type-fest@^0.6.0: | ||
336 | version "0.6.0" | ||
337 | resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.6.0.tgz#8d2a2370d3df886eb5c90ada1c5bf6188acf838b" | ||
338 | integrity sha512-q+MB8nYR1KDLrgr4G5yemftpMC7/QLqVndBmEEdqzmNj5dcFOO4Oo8qlwZE3ULT3+Zim1F8Kq4cBnikNhlCMlg== | ||
339 | |||
340 | typedarray-to-buffer@^3.1.5: | ||
341 | version "3.1.5" | ||
342 | resolved "https://registry.yarnpkg.com/typedarray-to-buffer/-/typedarray-to-buffer-3.1.5.tgz#a97ee7a9ff42691b9f783ff1bc5112fe3fca9080" | ||
343 | integrity sha512-zdu8XMNEDepKKR+XYOXAVPtWui0ly0NtohUscw+UmaHiAWT8hrV1rr//H6V+0DvJ3OQ19S979M0laLfX8rm82Q== | ||
344 | dependencies: | ||
345 | is-typedarray "^1.0.0" | ||
346 | |||
347 | which@^1.2.9: | ||
348 | version "1.3.1" | ||
349 | resolved "https://registry.yarnpkg.com/which/-/which-1.3.1.tgz#a45043d54f5805316da8d62f9f50918d3da70b0a" | ||
350 | integrity sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ== | ||
351 | dependencies: | ||
352 | isexe "^2.0.0" | ||
353 | |||
354 | write-file-atomic@^3.0.0: | ||
355 | version "3.0.3" | ||
356 | resolved "https://registry.yarnpkg.com/write-file-atomic/-/write-file-atomic-3.0.3.tgz#56bd5c5a5c70481cd19c571bd39ab965a5de56e8" | ||
357 | integrity sha512-AvHcyZ5JnSfq3ioSyjrBkH9yW4m7Ayk8/9My/DD9onKeu/94fwrMocemO2QAJFAlnnDN+ZDS+ZjAR5ua1/PV/Q== | ||
358 | dependencies: | ||
359 | imurmurhash "^0.1.4" | ||
360 | is-typedarray "^1.0.0" | ||
361 | signal-exit "^3.0.2" | ||
362 | typedarray-to-buffer "^3.1.5" | ||
363 | |||
364 | write-json-file@^4.2.0: | ||
365 | version "4.3.0" | ||
366 | resolved "https://registry.yarnpkg.com/write-json-file/-/write-json-file-4.3.0.tgz#908493d6fd23225344af324016e4ca8f702dd12d" | ||
367 | integrity sha512-PxiShnxf0IlnQuMYOPPhPkhExoCQuTUNPOa/2JWCYTmBquU9njyyDuwRKN26IZBlp4yn1nt+Agh2HOOBl+55HQ== | ||
368 | dependencies: | ||
369 | detect-indent "^6.0.0" | ||
370 | graceful-fs "^4.1.15" | ||
371 | is-plain-obj "^2.0.0" | ||
372 | make-dir "^3.0.0" | ||
373 | sort-keys "^4.0.0" | ||
374 | write-file-atomic "^3.0.0" | ||