@logto/tunnel 0.1.0 → 0.2.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.
@@ -0,0 +1,4 @@
1
+ import type { CommandModule } from 'yargs';
2
+ import { type DeployCommandArgs } from './types.js';
3
+ declare const tunnel: CommandModule<unknown, DeployCommandArgs>;
4
+ export default tunnel;
@@ -0,0 +1,77 @@
1
+ import { isValidUrl } from '@logto/core-kit';
2
+ import chalk from 'chalk';
3
+ import ora from 'ora';
4
+ import { consoleLog } from '../../utils.js';
5
+ import { checkExperienceAndZipPathInputs, deployToLogtoCloud } from './utils.js';
6
+ const tunnel = {
7
+ command: ['deploy'],
8
+ describe: 'Deploy your custom UI assets to Logto Cloud',
9
+ builder: (yargs) => yargs
10
+ .options({
11
+ auth: {
12
+ describe: 'Auth credentials of your Logto M2M application. E.g.: <app-id>:<app-secret> (Docs: https://docs.logto.io/docs/recipes/interact-with-management-api/#create-an-m2m-app)',
13
+ type: 'string',
14
+ },
15
+ endpoint: {
16
+ describe: 'Logto endpoint URI that points to your Logto Cloud instance. E.g.: https://<tenant-id>.logto.app/',
17
+ type: 'string',
18
+ },
19
+ path: {
20
+ alias: ['experience-path'],
21
+ describe: 'The local folder path of your custom sign-in experience assets.',
22
+ type: 'string',
23
+ },
24
+ resource: {
25
+ alias: ['management-api-resource'],
26
+ describe: 'Logto Management API resource indicator. Required if using custom domain.',
27
+ type: 'string',
28
+ },
29
+ verbose: {
30
+ describe: 'Show verbose output.',
31
+ type: 'boolean',
32
+ default: false,
33
+ },
34
+ zip: {
35
+ alias: ['zip-path'],
36
+ describe: 'The local folder path of your existing zip package.',
37
+ type: 'string',
38
+ },
39
+ })
40
+ .epilog(`Refer to our documentation for more details:\n${chalk.blue('https://docs.logto.io/docs/references/tunnel-cli/deploy')}`),
41
+ handler: async (options) => {
42
+ const { auth, endpoint, path: experiencePath, resource: managementApiResource, verbose, zip: zipPath, } = options;
43
+ if (!auth) {
44
+ consoleLog.fatal('Must provide valid Machine-to-Machine (M2M) authentication credentials. E.g. `--auth <app-id>:<app-secret>` or add `LOGTO_AUTH` to your environment variables.');
45
+ }
46
+ if (!endpoint || !isValidUrl(endpoint)) {
47
+ consoleLog.fatal('A valid Logto endpoint URI must be provided. E.g. `--endpoint https://<tenant-id>.logto.app/` or add `LOGTO_ENDPOINT` to your environment variables.');
48
+ }
49
+ await checkExperienceAndZipPathInputs(experiencePath, zipPath);
50
+ const spinner = ora();
51
+ if (verbose) {
52
+ consoleLog.plain(`${chalk.bold('Starting deployment...')} ${chalk.gray('(with verbose output)')}`);
53
+ }
54
+ else {
55
+ spinner.start('Deploying your custom UI assets to Logto Cloud...');
56
+ }
57
+ await deployToLogtoCloud({
58
+ auth,
59
+ endpoint,
60
+ experiencePath,
61
+ managementApiResource,
62
+ verbose,
63
+ zipPath,
64
+ });
65
+ if (!verbose) {
66
+ spinner.succeed('Deploying your custom UI assets to Logto Cloud... Done.');
67
+ }
68
+ const endpointUrl = new URL(endpoint);
69
+ spinner.succeed(`🎉 ${chalk.bold(chalk.green('Deployment successful!'))}`);
70
+ consoleLog.plain(`${chalk.green('➜')} You can try your own sign-in UI on Logto Cloud now.`);
71
+ consoleLog.plain(`${chalk.green('➜')} Make sure the Logto endpoint URI in your app is set to:`);
72
+ consoleLog.plain(` ${chalk.blue(chalk.bold(endpointUrl.href))}`);
73
+ consoleLog.plain(`${chalk.green('➜')} If you are using social sign-in, make sure the social redirect URI is set to:`);
74
+ consoleLog.plain(` ${chalk.blue(chalk.bold(`${endpointUrl.href}callback/<connector-id>`))}`);
75
+ },
76
+ };
77
+ export default tunnel;
@@ -0,0 +1,8 @@
1
+ export type DeployCommandArgs = {
2
+ auth?: string;
3
+ endpoint?: string;
4
+ path?: string;
5
+ zip?: string;
6
+ resource?: string;
7
+ verbose: boolean;
8
+ };
@@ -0,0 +1,11 @@
1
+ type DeployArgs = {
2
+ auth: string;
3
+ endpoint: string;
4
+ experiencePath?: string;
5
+ zipPath?: string;
6
+ managementApiResource?: string;
7
+ verbose: boolean;
8
+ };
9
+ export declare const checkExperienceAndZipPathInputs: (experiencePath?: string, zipPath?: string) => Promise<void>;
10
+ export declare const deployToLogtoCloud: ({ auth, endpoint, experiencePath, managementApiResource, verbose, zipPath, }: DeployArgs) => Promise<void>;
11
+ export {};
@@ -0,0 +1,159 @@
1
+ import { existsSync } from 'node:fs';
2
+ import { readFile } from 'node:fs/promises';
3
+ import path from 'node:path';
4
+ import { appendPath } from '@silverhand/essentials';
5
+ import AdmZip from 'adm-zip';
6
+ import chalk from 'chalk';
7
+ import ora from 'ora';
8
+ import { consoleLog } from '../../utils.js';
9
+ export const checkExperienceAndZipPathInputs = async (experiencePath, zipPath) => {
10
+ if (zipPath && experiencePath) {
11
+ consoleLog.fatal('You can only specify either `--zip-path` or `--experience-path`. Please check your input and environment variables.');
12
+ }
13
+ if (!zipPath && !experiencePath) {
14
+ consoleLog.fatal('A valid path to your experience asset folder or zip package must be provided. You can specify either `--zip-path` or `--experience-path` options or corresponding environment variables.');
15
+ }
16
+ if (zipPath) {
17
+ if (!existsSync(zipPath)) {
18
+ consoleLog.fatal(`The specified zip file does not exist: ${zipPath}`);
19
+ }
20
+ const zipFile = new AdmZip(zipPath);
21
+ const zipEntries = zipFile.getEntries();
22
+ const hasIndexHtmlInRoot = zipEntries.some(({ entryName }) => {
23
+ const parts = entryName.split('/');
24
+ return parts.length <= 2 && parts.at(-1) === 'index.html';
25
+ });
26
+ if (!hasIndexHtmlInRoot) {
27
+ consoleLog.fatal('The provided zip must contain an "index.html" file in the root directory.');
28
+ }
29
+ }
30
+ if (experiencePath && !existsSync(path.join(experiencePath, 'index.html'))) {
31
+ consoleLog.fatal(`The provided experience path must contain an "index.html" file.`);
32
+ }
33
+ };
34
+ export const deployToLogtoCloud = async ({ auth, endpoint, experiencePath, managementApiResource, verbose, zipPath, }) => {
35
+ const spinner = ora();
36
+ if (verbose) {
37
+ spinner.start(`[1/4] ${zipPath ? 'Reading zip' : 'Zipping'} files...`);
38
+ }
39
+ const zipBuffer = await getZipBuffer(experiencePath, zipPath);
40
+ if (verbose) {
41
+ spinner.succeed(`[1/4] ${zipPath ? 'Reading zip' : 'Zipping'} files... Done.`);
42
+ }
43
+ try {
44
+ if (verbose) {
45
+ spinner.start('[2/4] Exchanging access token...');
46
+ }
47
+ const endpointUrl = new URL(endpoint);
48
+ const tokenResponse = await getAccessToken(auth, endpointUrl, managementApiResource);
49
+ if (verbose) {
50
+ spinner.succeed('[2/4] Exchanging access token... Done.');
51
+ spinner.succeed(`Token exchange response:\n${chalk.gray(JSON.stringify(tokenResponse, undefined, 2))}`);
52
+ spinner.start('[3/4] Uploading zip...');
53
+ }
54
+ const accessToken = tokenResponse.access_token;
55
+ const uploadResult = await uploadCustomUiAssets(accessToken, endpointUrl, zipBuffer);
56
+ if (verbose) {
57
+ spinner.succeed('[3/4] Uploading zip... Done.');
58
+ spinner.succeed(`Received response:\n${chalk.gray(JSON.stringify(uploadResult, undefined, 2))}`);
59
+ spinner.start('[4/4] Saving changes to your tenant...');
60
+ }
61
+ await saveChangesToSie(accessToken, endpointUrl, uploadResult.customUiAssetId);
62
+ if (verbose) {
63
+ spinner.succeed('[4/4] Saving changes to your tenant... Done.');
64
+ }
65
+ }
66
+ catch (error) {
67
+ spinner.fail();
68
+ const errorMessage = error instanceof Error ? error.message : String(error);
69
+ consoleLog.fatal(chalk.red(errorMessage));
70
+ }
71
+ };
72
+ const getZipBuffer = async (experiencePath, zipPath) => {
73
+ if (!experiencePath && !zipPath) {
74
+ consoleLog.fatal('Must specify either `--experience-path` or `--zip-path`.');
75
+ }
76
+ if (zipPath) {
77
+ return readFile(zipPath);
78
+ }
79
+ if (!experiencePath) {
80
+ consoleLog.fatal('Invalid experience path input.');
81
+ }
82
+ const zip = new AdmZip();
83
+ await zip.addLocalFolderPromise(experiencePath, {
84
+ filter: (filename) => !isHiddenEntry(filename),
85
+ });
86
+ return zip.toBuffer();
87
+ };
88
+ const getAccessToken = async (auth, endpoint, managementApiResource) => {
89
+ const tokenEndpoint = appendPath(endpoint, '/oidc/token').href;
90
+ const resource = managementApiResource ?? getManagementApiResourceFromEndpointUri(endpoint);
91
+ const response = await fetch(tokenEndpoint, {
92
+ method: 'POST',
93
+ headers: {
94
+ 'Content-Type': 'application/x-www-form-urlencoded',
95
+ Authorization: `Basic ${Buffer.from(auth).toString('base64')}`,
96
+ },
97
+ body: new URLSearchParams({
98
+ grant_type: 'client_credentials',
99
+ resource,
100
+ scope: 'all',
101
+ }).toString(),
102
+ });
103
+ if (!response.ok) {
104
+ await throwRequestError(response);
105
+ }
106
+ return response.json();
107
+ };
108
+ const uploadCustomUiAssets = async (accessToken, endpoint, zipBuffer) => {
109
+ const form = new FormData();
110
+ const blob = new Blob([zipBuffer], { type: 'application/zip' });
111
+ const timestamp = Math.floor(Date.now() / 1000);
112
+ form.append('file', blob, `custom-ui-${timestamp}.zip`);
113
+ const response = await fetch(appendPath(endpoint, '/api/sign-in-exp/default/custom-ui-assets'), {
114
+ method: 'POST',
115
+ body: form,
116
+ headers: {
117
+ Authorization: `Bearer ${accessToken}`,
118
+ Accept: 'application/json',
119
+ },
120
+ });
121
+ if (!response.ok) {
122
+ await throwRequestError(response);
123
+ }
124
+ return response.json();
125
+ };
126
+ const saveChangesToSie = async (accessToken, endpointUrl, customUiAssetId) => {
127
+ const timestamp = Math.floor(Date.now() / 1000);
128
+ const response = await fetch(appendPath(endpointUrl, '/api/sign-in-exp'), {
129
+ method: 'PATCH',
130
+ headers: {
131
+ Authorization: `Bearer ${accessToken}`,
132
+ Accept: 'application/json',
133
+ 'Content-Type': 'application/json',
134
+ },
135
+ body: JSON.stringify({
136
+ customUiAssets: { id: customUiAssetId, createdAt: timestamp },
137
+ }),
138
+ });
139
+ if (!response.ok) {
140
+ await throwRequestError(response);
141
+ }
142
+ return response.json();
143
+ };
144
+ const throwRequestError = async (response) => {
145
+ const errorDetails = await response.text();
146
+ throw new Error(`[${response.status}] ${errorDetails}`);
147
+ };
148
+ const getTenantIdFromEndpointUri = (endpoint) => {
149
+ const splitted = endpoint.hostname.split('.');
150
+ return splitted.length > 2 ? splitted[0] : 'default';
151
+ };
152
+ const getManagementApiResourceFromEndpointUri = (endpoint) => {
153
+ const tenantId = getTenantIdFromEndpointUri(endpoint);
154
+ // This resource domain is fixed to `logto.app` for all environments (prod, staging, and dev)
155
+ return `https://${tenantId}.logto.app/api`;
156
+ };
157
+ const isHiddenEntry = (entryName) => {
158
+ return entryName.split('/').some((part) => part.startsWith('.'));
159
+ };
@@ -2,7 +2,7 @@ import http from 'node:http';
2
2
  import { isValidUrl } from '@logto/core-kit';
3
3
  import { conditional } from '@silverhand/essentials';
4
4
  import chalk from 'chalk';
5
- import { consoleLog } from '../utils.js';
5
+ import { consoleLog } from '../../utils.js';
6
6
  import { checkExperienceInput, createLogtoResponseHandler, createProxy, createStaticFileProxy, isLogtoRequestPath, } from './utils.js';
7
7
  const tunnel = {
8
8
  command: ['$0'],
@@ -19,7 +19,7 @@ const tunnel = {
19
19
  type: 'string',
20
20
  },
21
21
  endpoint: {
22
- describe: 'Logto endpoint URI that points to your Logto Cloud instance. E.g.: https://<tenant-id>.logto.app/',
22
+ describe: `Logto endpoint URI that points to your Logto Cloud instance. E.g.: https://<tenant-id>.logto.app/`,
23
23
  type: 'string',
24
24
  },
25
25
  port: {
@@ -34,10 +34,10 @@ const tunnel = {
34
34
  default: false,
35
35
  },
36
36
  }),
37
- handler: async ({ 'experience-uri': url, 'experience-path': path, endpoint, port, verbose }) => {
38
- checkExperienceInput(url, path);
37
+ handler: async ({ 'experience-uri': uri, 'experience-path': path, endpoint, port, verbose }) => {
38
+ checkExperienceInput(uri, path);
39
39
  if (!endpoint || !isValidUrl(endpoint)) {
40
- consoleLog.fatal('A valid Logto endpoint URI must be provided.');
40
+ consoleLog.fatal('A valid Logto endpoint URI must be provided. E.g. `--endpoint https://<tenant-id>.logto.app/` or add `LOGTO_ENDPOINT` to your environment variables.');
41
41
  }
42
42
  const logtoEndpointUrl = new URL(endpoint);
43
43
  const startServer = (port) => {
@@ -50,7 +50,7 @@ const tunnel = {
50
50
  tunnelServiceUrl,
51
51
  verbose,
52
52
  }));
53
- const proxyExperienceServerRequest = conditional(url && createProxy(url));
53
+ const proxyExperienceServerRequest = conditional(uri && createProxy(uri));
54
54
  const proxyExperienceStaticFileRequest = conditional(path && createStaticFileProxy(path));
55
55
  const server = http.createServer((request, response) => {
56
56
  consoleLog.info(`[${chalk.green(request.method)}] ${request.url}`);
@@ -69,21 +69,16 @@ const tunnel = {
69
69
  });
70
70
  server.listen(port, () => {
71
71
  const serviceUrl = new URL(`http://localhost:${port}`);
72
- consoleLog.info(`🎉 Logto tunnel service is running!
73
- ${chalk.green('➜')} Your custom sign-in UI is hosted on: ${chalk.blue(serviceUrl.href)}
74
-
75
- ${chalk.green('➜')} Don't forget to update Logto endpoint URI in your app:
76
-
77
- ${chalk.gray('From:')} ${chalk.bold(endpoint)}
78
- ${chalk.gray('To:')} ${chalk.bold(serviceUrl.href)}
79
-
80
- ${chalk.green('➜')} If you are using social sign-in, make sure the social redirect URI is also set to:
81
-
82
- ${chalk.bold(`${serviceUrl.href}callback/<connector-id>`)}
83
-
84
- ${chalk.green('➜')} ${chalk.gray(`Press ${chalk.white('Ctrl+C')} to stop the tunnel service.`)}
85
- ${chalk.green('➜')} ${chalk.gray(`Use ${chalk.white('--verbose')} to print verbose output.`)}
86
- `);
72
+ consoleLog.plain(`${chalk.green('✔')} 🎉 Logto tunnel service is running!`);
73
+ consoleLog.plain(`${chalk.green('➜')} Your custom sign-in UI is hosted on:`);
74
+ consoleLog.plain(` ${chalk.blue(chalk.bold(serviceUrl.href))}`);
75
+ consoleLog.plain(`${chalk.green('➜')} Remember to update Logto endpoint URI in your app:`);
76
+ consoleLog.plain(` ${chalk.gray('From:')} ${chalk.blue(chalk.bold(endpoint))}`);
77
+ consoleLog.plain(` ${chalk.gray('To:')} ${chalk.blue(chalk.bold(serviceUrl.href))}`);
78
+ consoleLog.plain(`${chalk.green('')} If you are using social sign-in, make sure the social redirect URI is also set to:`);
79
+ consoleLog.plain(` ${chalk.blue(chalk.bold(`${serviceUrl.href}callback/<connector-id>`))}\n`);
80
+ consoleLog.plain(`${chalk.green('➜')} ${chalk.gray(`Press ${chalk.white('Ctrl+C')} to stop the tunnel service.`)}`);
81
+ consoleLog.plain(`${chalk.green('➜')} ${chalk.gray(`Use ${chalk.white('--verbose')} to print verbose output.`)}`);
87
82
  });
88
83
  server.on('error', (error) => {
89
84
  if ('code' in error && error.code === 'EADDRINUSE') {
@@ -6,7 +6,7 @@ import { conditional, trySafe } from '@silverhand/essentials';
6
6
  import chalk from 'chalk';
7
7
  import { createProxyMiddleware, responseInterceptor } from 'http-proxy-middleware';
8
8
  import mime from 'mime';
9
- import { consoleLog } from '../utils.js';
9
+ import { consoleLog } from '../../utils.js';
10
10
  export const createProxy = (targetUrl, onProxyResponse) => {
11
11
  const hasResponseHandler = Boolean(onProxyResponse);
12
12
  return createProxyMiddleware({
@@ -97,13 +97,15 @@ export const checkExperienceInput = (url, staticPath) => {
97
97
  consoleLog.fatal('Only one of the experience URI or path can be provided.');
98
98
  }
99
99
  if (!staticPath && !url) {
100
- consoleLog.fatal('Either a sign-in experience URI or local path must be provided.');
100
+ consoleLog.fatal(`Either a sign-in experience URI or local path must be provided.
101
+
102
+ Specify --help for available options`);
101
103
  }
102
104
  if (url && !isValidUrl(url)) {
103
- consoleLog.fatal('A valid sign-in experience URI must be provided. E.g.: http://localhost:4000');
105
+ consoleLog.fatal('A valid sign-in experience URI must be provided. E.g. `--experience-uri http://localhost:4000` or add `LOGTO_EXPERIENCE_URI` to your environment variables.');
104
106
  }
105
107
  if (staticPath && !existsSync(path.join(staticPath, index))) {
106
- consoleLog.fatal('The provided path does not contain a valid index.html file.');
108
+ consoleLog.fatal('The provided path must contain a valid index.html file.');
107
109
  }
108
110
  };
109
111
  /**
@@ -0,0 +1 @@
1
+ export {};
package/lib/index.js CHANGED
@@ -1,24 +1,20 @@
1
1
  import chalk from 'chalk';
2
2
  import dotenv from 'dotenv';
3
+ import { findUp } from 'find-up';
3
4
  import yargs from 'yargs';
4
5
  import { hideBin } from 'yargs/helpers';
5
- import tunnel from './commands/index.js';
6
+ import deploy from './commands/deploy/index.js';
7
+ import tunnel from './commands/tunnel/index.js';
6
8
  import { packageJson } from './package-json.js';
7
9
  import { consoleLog } from './utils.js';
10
+ dotenv.config({ path: await findUp('.env', {}) });
8
11
  void yargs(hideBin(process.argv))
9
12
  .version(false)
10
- .option('env', {
11
- alias: ['e', 'env-file'],
12
- describe: 'The path to your `.env` file',
13
- type: 'string',
14
- })
13
+ .env('LOGTO')
15
14
  .option('version', {
16
15
  alias: 'v',
17
16
  describe: 'Print CLI version',
18
17
  type: 'boolean',
19
- })
20
- .middleware(({ env }) => {
21
- dotenv.config({ path: env });
22
18
  })
23
19
  .middleware(({ version }) => {
24
20
  if (version) {
@@ -28,9 +24,11 @@ void yargs(hideBin(process.argv))
28
24
  }
29
25
  }, true)
30
26
  .command(tunnel)
27
+ .command(deploy)
31
28
  .showHelpOnFail(false, `Specify ${chalk.green('--help')} for available options`)
32
- .strict()
29
+ .strictCommands()
33
30
  .parserConfiguration({
34
31
  'dot-notation': false,
35
32
  })
33
+ .epilog(`Refer to our documentation for more details:\n${chalk.blue('https://docs.logto.io/docs/references/tunnel-cli')}`)
36
34
  .parse();
@@ -41,16 +41,20 @@ export declare const packageJson: {
41
41
  "@logto/core-kit": string;
42
42
  "@logto/shared": string;
43
43
  "@silverhand/essentials": string;
44
+ "adm-zip": string;
44
45
  chalk: string;
45
46
  dotenv: string;
47
+ "find-up": string;
46
48
  "http-proxy-middleware": string;
47
49
  mime: string;
50
+ ora: string;
48
51
  yargs: string;
49
52
  zod: string;
50
53
  };
51
54
  devDependencies: {
52
55
  "@silverhand/eslint-config": string;
53
56
  "@silverhand/ts-config": string;
57
+ "@types/adm-zip": string;
54
58
  "@types/node": string;
55
59
  "@types/yargs": string;
56
60
  "@vitest/coverage-v8": string;
@@ -1,6 +1,6 @@
1
1
  export const packageJson = {
2
2
  "name": "@logto/tunnel",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
4
4
  "description": "A CLI tool that creates tunnel service to Logto Cloud for local development.",
5
5
  "author": "Silverhand Inc. <contact@silverhand.io>",
6
6
  "homepage": "https://github.com/logto-io/logto#readme",
@@ -44,16 +44,20 @@ export const packageJson = {
44
44
  "@logto/core-kit": "workspace:^",
45
45
  "@logto/shared": "workspace:^",
46
46
  "@silverhand/essentials": "^2.9.1",
47
+ "adm-zip": "^0.5.14",
47
48
  "chalk": "^5.3.0",
48
49
  "dotenv": "^16.4.5",
50
+ "find-up": "^7.0.0",
49
51
  "http-proxy-middleware": "^3.0.0",
50
52
  "mime": "^4.0.4",
53
+ "ora": "^8.0.1",
51
54
  "yargs": "^17.6.0",
52
55
  "zod": "^3.23.8"
53
56
  },
54
57
  "devDependencies": {
55
58
  "@silverhand/eslint-config": "6.0.1",
56
59
  "@silverhand/ts-config": "6.0.0",
60
+ "@types/adm-zip": "^0.5.5",
57
61
  "@types/node": "^20.9.5",
58
62
  "@types/yargs": "^17.0.13",
59
63
  "@vitest/coverage-v8": "^2.0.0",
package/lib/utils.js CHANGED
@@ -1,12 +1,2 @@
1
1
  import { ConsoleLog } from '@logto/shared';
2
- // The explicit type annotation is required to make `.fatal()`
3
- // works correctly without `return`:
4
- //
5
- // ```ts
6
- // const foo: number | undefined;
7
- // consoleLog.fatal();
8
- // typeof foo // Still `number | undefined` without explicit type annotation
9
- // ```
10
- //
11
- // For now I have no idea why.
12
2
  export const consoleLog = new ConsoleLog();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@logto/tunnel",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
4
4
  "description": "A CLI tool that creates tunnel service to Logto Cloud for local development.",
5
5
  "author": "Silverhand Inc. <contact@silverhand.io>",
6
6
  "homepage": "https://github.com/logto-io/logto#readme",
@@ -29,18 +29,22 @@
29
29
  },
30
30
  "dependencies": {
31
31
  "@silverhand/essentials": "^2.9.1",
32
+ "adm-zip": "^0.5.14",
32
33
  "chalk": "^5.3.0",
33
34
  "dotenv": "^16.4.5",
35
+ "find-up": "^7.0.0",
34
36
  "http-proxy-middleware": "^3.0.0",
35
37
  "mime": "^4.0.4",
38
+ "ora": "^8.0.1",
36
39
  "yargs": "^17.6.0",
37
40
  "zod": "^3.23.8",
38
- "@logto/shared": "^3.1.1",
39
- "@logto/core-kit": "^2.5.0"
41
+ "@logto/core-kit": "^2.5.0",
42
+ "@logto/shared": "^3.1.1"
40
43
  },
41
44
  "devDependencies": {
42
45
  "@silverhand/eslint-config": "6.0.1",
43
46
  "@silverhand/ts-config": "6.0.0",
47
+ "@types/adm-zip": "^0.5.5",
44
48
  "@types/node": "^20.9.5",
45
49
  "@types/yargs": "^17.0.13",
46
50
  "@vitest/coverage-v8": "^2.0.0",
File without changes
File without changes
File without changes
File without changes