@logto/cli 1.0.2 → 1.1.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.
@@ -1,4 +1,4 @@
1
- import fs from 'fs/promises';
1
+ import fs from 'node:fs/promises';
2
2
  import chalk from 'chalk';
3
3
  import { log } from '../../utils.js';
4
4
  import { getConnectorPackagesFrom } from './utils.js';
@@ -1,9 +1,9 @@
1
- import { exec } from 'child_process';
2
- import { existsSync } from 'fs';
3
- import fs from 'fs/promises';
4
- import path from 'path';
5
- import { promisify } from 'util';
6
- import { assert, conditionalString, trySafe } from '@silverhand/essentials';
1
+ import { exec } from 'node:child_process';
2
+ import { existsSync } from 'node:fs';
3
+ import fs from 'node:fs/promises';
4
+ import path from 'node:path';
5
+ import { promisify } from 'node:util';
6
+ import { assert, conditionalArray, conditionalString, trySafe } from '@silverhand/essentials';
7
7
  import chalk from 'chalk';
8
8
  import { got } from 'got';
9
9
  import inquirer from 'inquirer';
@@ -142,7 +142,7 @@ const fetchOfficialConnectorList = async (includingCloudConnectors = false) => {
142
142
  for (let page = 0;; ++page) {
143
143
  // eslint-disable-next-line no-await-in-loop
144
144
  const { objects } = await fetchList(page * 20, 20);
145
- const excludeList = ['mock', 'kit', ...(includingCloudConnectors ? [] : ['logto'])];
145
+ const excludeList = ['mock', 'kit', ...conditionalArray(!includingCloudConnectors && 'logto')];
146
146
  // eslint-disable-next-line @silverhand/fp/no-mutating-methods
147
147
  packages.push(...objects
148
148
  .filter(({ package: { name, scope } }) => scope === 'logto' &&
@@ -1,7 +1,7 @@
1
- import { existsSync } from 'fs';
2
- import fs from 'fs/promises';
1
+ import { existsSync } from 'node:fs';
2
+ import fs from 'node:fs/promises';
3
+ import path from 'node:path';
3
4
  import { fileURLToPath } from 'node:url';
4
- import path from 'path';
5
5
  import { findPackage } from '@logto/shared';
6
6
  import { getPathInModule } from '../../../utils.js';
7
7
  const currentDirname = path.dirname(fileURLToPath(import.meta.url));
@@ -1,4 +1,4 @@
1
- import { readFile } from 'fs/promises';
1
+ import { readFile } from 'node:fs/promises';
2
2
  import { LogtoOidcConfigKey, logtoConfigGuards } from '@logto/schemas';
3
3
  import { getEnvAsStringArray } from '@silverhand/essentials';
4
4
  import chalk from 'chalk';
@@ -1,5 +1,5 @@
1
- import { readdir, readFile } from 'fs/promises';
2
- import path from 'path';
1
+ import { readdir, readFile } from 'node:fs/promises';
2
+ import path from 'node:path';
3
3
  import { createDefaultAdminConsoleConfig, defaultTenantId, adminTenantId, defaultManagementApi, createAdminDataInAdminTenant, createMeApiInAdminTenant, createDefaultSignInExperience, createAdminTenantSignInExperience, createDefaultAdminConsoleApplication, createCloudApi, createTenantApplicationRole, createTenantMachineToMachineApplication, createAdminTenantApplicationRole, CloudScope, } from '@logto/schemas';
4
4
  import { Tenants } from '@logto/schemas/models';
5
5
  import { sql } from 'slonik';
@@ -1,5 +1,5 @@
1
- import { generateKeyPair } from 'crypto';
2
- import { promisify } from 'util';
1
+ import { generateKeyPair } from 'node:crypto';
2
+ import { promisify } from 'node:util';
3
3
  import { generateStandardId } from '@logto/core-kit';
4
4
  export const generateOidcPrivateKey = async (type = 'ec') => {
5
5
  if (type === 'rsa') {
@@ -1,8 +1,8 @@
1
- import { execSync } from 'child_process';
2
- import { existsSync } from 'fs';
3
- import fs from 'fs/promises';
4
- import os from 'os';
5
- import path from 'path';
1
+ import { execSync } from 'node:child_process';
2
+ import { existsSync } from 'node:fs';
3
+ import fs from 'node:fs/promises';
4
+ import os from 'node:os';
5
+ import path from 'node:path';
6
6
  import { assert } from '@silverhand/essentials';
7
7
  import chalk from 'chalk';
8
8
  import inquirer from 'inquirer';
@@ -0,0 +1,8 @@
1
+ import type { CommandModule } from 'yargs';
2
+ declare const create: CommandModule<{
3
+ path?: string;
4
+ }, {
5
+ path?: string;
6
+ 'language-tag': string;
7
+ }>;
8
+ export default create;
@@ -0,0 +1,75 @@
1
+ import { existsSync } from 'node:fs';
2
+ import fs from 'node:fs/promises';
3
+ import path from 'node:path';
4
+ import { isLanguageTag } from '@logto/language-kit';
5
+ import { isBuiltInLanguageTag as isPhrasesBuiltInLanguageTag } from '@logto/phrases';
6
+ import { isBuiltInLanguageTag as isPhrasesUiBuiltInLanguageTag } from '@logto/phrases-ui';
7
+ import { conditionalString } from '@silverhand/essentials';
8
+ import { log } from '../../utils.js';
9
+ import { inquireInstancePath } from '../connector/utils.js';
10
+ import { createOpenaiApi, translate } from './openai.js';
11
+ import { baseLanguage, readBaseLocaleFiles } from './utils.js';
12
+ const createFullTranslation = async (instancePath, packageName, languageTag) => {
13
+ const directory = path.join(await inquireInstancePath(instancePath), 'packages', packageName, 'src/locales');
14
+ const files = await readBaseLocaleFiles(directory);
15
+ log.info('Found ' +
16
+ String(files.length) +
17
+ ' file' +
18
+ conditionalString(files.length !== 1 && 's') +
19
+ ' in ' +
20
+ packageName +
21
+ ' to translate');
22
+ const openai = createOpenaiApi();
23
+ /* eslint-disable no-await-in-loop */
24
+ for (const file of files) {
25
+ const relativePath = path.relative(path.join(directory, baseLanguage.toLowerCase()), file);
26
+ const targetPath = path.join(directory, languageTag.toLowerCase(), relativePath);
27
+ const getTranslationPath = async () => {
28
+ if (existsSync(targetPath)) {
29
+ const currentContent = await fs.readFile(targetPath, 'utf8');
30
+ if (currentContent.includes('// UNTRANSLATED')) {
31
+ return targetPath;
32
+ }
33
+ log.info(`Target path ${targetPath} exists and has no untranslated mark, skipping`);
34
+ return;
35
+ }
36
+ return file;
37
+ };
38
+ const translationPath = await getTranslationPath();
39
+ if (!translationPath) {
40
+ continue;
41
+ }
42
+ log.info(`Translating ${translationPath}`);
43
+ const result = await translate(openai, languageTag, translationPath);
44
+ if (!result) {
45
+ log.error(`Unable to translate ${translationPath}`);
46
+ }
47
+ await fs.mkdir(path.parse(targetPath).dir, { recursive: true });
48
+ await fs.writeFile(targetPath, result);
49
+ log.succeed(`Translated ${targetPath}`);
50
+ }
51
+ /* eslint-enable no-await-in-loop */
52
+ };
53
+ const create = {
54
+ command: ['create <language-tag>', 'c'],
55
+ describe: 'Create a new language translation',
56
+ builder: (yargs) => yargs.positional('language-tag', {
57
+ describe: 'The language tag to create, e.g. `af-ZA`.',
58
+ type: 'string',
59
+ demandOption: true,
60
+ }),
61
+ handler: async ({ path: inputPath, languageTag }) => {
62
+ if (!isLanguageTag(languageTag)) {
63
+ log.error('Invalid language tag. Run `logto translate list-tags` to see available list.');
64
+ }
65
+ if (isPhrasesBuiltInLanguageTag(languageTag)) {
66
+ log.info(languageTag + ' is a built-in tag of phrases, updating untranslated phrases');
67
+ }
68
+ await createFullTranslation(inputPath, 'phrases', languageTag);
69
+ if (isPhrasesUiBuiltInLanguageTag(languageTag)) {
70
+ log.info(languageTag + ' is a built-in tag of phrases-ui, updating untranslated phrases');
71
+ }
72
+ await createFullTranslation(inputPath, 'phrases-ui', languageTag);
73
+ },
74
+ };
75
+ export default create;
@@ -0,0 +1,3 @@
1
+ import type { CommandModule } from 'yargs';
2
+ declare const translate: CommandModule;
3
+ export default translate;
@@ -0,0 +1,18 @@
1
+ import { noop } from '@silverhand/essentials';
2
+ import create from './create.js';
3
+ import listTags from './list-tags.js';
4
+ const translate = {
5
+ command: ['translate', 't'],
6
+ describe: 'Commands for Logto translation',
7
+ builder: (yargs) => yargs
8
+ .option('path', {
9
+ alias: 'p',
10
+ type: 'string',
11
+ describe: 'The path to your Logto instance directory',
12
+ })
13
+ .command(create)
14
+ .command(listTags)
15
+ .demandCommand(1),
16
+ handler: noop,
17
+ };
18
+ export default translate;
@@ -0,0 +1,3 @@
1
+ import type { CommandModule } from 'yargs';
2
+ declare const listTags: CommandModule<Record<string, unknown>>;
3
+ export default listTags;
@@ -0,0 +1,18 @@
1
+ import { languages } from '@logto/language-kit';
2
+ import { isBuiltInLanguageTag as isPhrasesBuiltInLanguageTag } from '@logto/phrases';
3
+ import { isBuiltInLanguageTag as isPhrasesUiBuiltInLanguageTag } from '@logto/phrases-ui';
4
+ import chalk from 'chalk';
5
+ const listTags = {
6
+ command: ['list-tags', 'list'],
7
+ describe: 'List all available language tags',
8
+ handler: async () => {
9
+ for (const tag of Object.keys(languages)) {
10
+ console.log(...[
11
+ tag,
12
+ isPhrasesBuiltInLanguageTag(tag) && chalk.blue('phrases'),
13
+ isPhrasesUiBuiltInLanguageTag(tag) && chalk.blue('phrases-ui'),
14
+ ].filter(Boolean));
15
+ }
16
+ },
17
+ };
18
+ export default listTags;
@@ -0,0 +1,4 @@
1
+ import { type LanguageTag } from '@logto/language-kit';
2
+ import { type Got } from 'got';
3
+ export declare const createOpenaiApi: () => Got;
4
+ export declare const translate: (api: Got, languageTag: LanguageTag, filePath: string) => Promise<string | undefined>;
@@ -0,0 +1,73 @@
1
+ import fs from 'node:fs/promises';
2
+ import { languages } from '@logto/language-kit';
3
+ import { trySafe } from '@silverhand/essentials';
4
+ import { got, HTTPError } from 'got';
5
+ import { HttpsProxyAgent } from 'hpagent';
6
+ import { z } from 'zod';
7
+ import { getProxy, log } from '../../utils.js';
8
+ export const createOpenaiApi = () => {
9
+ const proxy = getProxy();
10
+ return got.extend({
11
+ prefixUrl: 'https://api.openai.com/v1',
12
+ headers: {
13
+ Authorization: `Bearer ${process.env.OPENAI_API_KEY ?? ''}`,
14
+ },
15
+ timeout: { request: 300_000 },
16
+ ...(proxy && { agent: { https: new HttpsProxyAgent({ proxy, timeout: 300_000 }) } }),
17
+ });
18
+ };
19
+ const gptResponseGuard = z.object({
20
+ choices: z
21
+ .object({
22
+ message: z.object({ role: z.string(), content: z.string() }),
23
+ finish_reason: z.string(),
24
+ })
25
+ .array(),
26
+ });
27
+ export const translate = async (api, languageTag, filePath) => {
28
+ const fileContent = await fs.readFile(filePath, 'utf8');
29
+ const response = await trySafe(api
30
+ .post('chat/completions', {
31
+ json: {
32
+ model: 'gpt-3.5-turbo',
33
+ messages: [
34
+ {
35
+ role: 'user',
36
+ content: `Given the following code snippet, only translate object values to ${languages[languageTag]}, keep all object keys original, output ts code only: \n \`\`\`ts\n${fileContent}\n\`\`\``,
37
+ },
38
+ ],
39
+ },
40
+ })
41
+ .json(), (error) => {
42
+ log.warn(`Error while translating ${filePath}:`, String(error));
43
+ if (error instanceof HTTPError) {
44
+ log.warn(error.response.body);
45
+ }
46
+ });
47
+ if (!response) {
48
+ return;
49
+ }
50
+ const guarded = gptResponseGuard.safeParse(response);
51
+ if (!guarded.success) {
52
+ log.warn(`Error while guarding response for ${filePath}:`, response);
53
+ return;
54
+ }
55
+ const [entity] = guarded.data.choices;
56
+ if (!entity) {
57
+ log.warn(`No choice found in response when translating ${filePath}`);
58
+ return;
59
+ }
60
+ if (entity.finish_reason !== 'stop') {
61
+ log.warn(`Unexpected finish reason ${entity.finish_reason} for ${filePath}`);
62
+ }
63
+ const { content } = entity.message;
64
+ const matched = /```(?:ts)?\n(.*)```/s.exec(content)?.[1];
65
+ if (!matched) {
66
+ // Treat as pure code
67
+ if (['const ', 'import '].some((prefix) => content.startsWith(prefix))) {
68
+ return content;
69
+ }
70
+ log.warn('No matching code snippet from response:', content);
71
+ }
72
+ return matched;
73
+ };
@@ -0,0 +1,3 @@
1
+ export declare const baseLanguage = "en";
2
+ export declare const readLocaleFiles: (directory: string) => Promise<string[]>;
3
+ export declare const readBaseLocaleFiles: (directory: string) => Promise<string[]>;
@@ -0,0 +1,22 @@
1
+ import fs from 'node:fs/promises';
2
+ import path from 'node:path';
3
+ import { log } from '../../utils.js';
4
+ export const baseLanguage = 'en';
5
+ export const readLocaleFiles = async (directory) => {
6
+ const entities = await fs.readdir(directory, { withFileTypes: true });
7
+ const result = await Promise.all(entities.map(async (entity) => {
8
+ if (entity.isDirectory()) {
9
+ return readLocaleFiles(path.join(directory, entity.name));
10
+ }
11
+ return entity.name.endsWith('.ts') ? path.join(directory, entity.name) : [];
12
+ }));
13
+ return result.flat();
14
+ };
15
+ export const readBaseLocaleFiles = async (directory) => {
16
+ const enDirectory = path.join(directory, baseLanguage.toLowerCase());
17
+ const stat = await fs.stat(enDirectory);
18
+ if (!stat.isDirectory()) {
19
+ log.error(directory, 'has no `' + baseLanguage.toLowerCase() + '` directory');
20
+ }
21
+ return readLocaleFiles(enDirectory);
22
+ };
@@ -1,4 +1,4 @@
1
- import path from 'path';
1
+ import path from 'node:path';
2
2
  import connectorKitMeta from '@logto/connector-kit/package.json' assert { type: 'json' };
3
3
  import { satisfies } from 'semver';
4
4
  import { isKeyInObject } from './utils.js';
@@ -1,6 +1,6 @@
1
- import { existsSync } from 'fs';
2
- import { readFile } from 'fs/promises';
3
- import path from 'path';
1
+ import { existsSync } from 'node:fs';
2
+ import { readFile } from 'node:fs/promises';
3
+ import path from 'node:path';
4
4
  import { ConnectorError, ConnectorErrorCodes, ConnectorType } from '@logto/connector-kit';
5
5
  import { notImplemented } from './consts.js';
6
6
  export function validateConnectorModule(connector) {
package/lib/index.js CHANGED
@@ -5,6 +5,7 @@ import { hideBin } from 'yargs/helpers';
5
5
  import connector from './commands/connector/index.js';
6
6
  import database from './commands/database/index.js';
7
7
  import install from './commands/install/index.js';
8
+ import translate from './commands/translate/index.js';
8
9
  import { packageJson } from './package-json.js';
9
10
  import { cliConfig, ConfigKey } from './utils.js';
10
11
  void yargs(hideBin(process.argv))
@@ -42,6 +43,7 @@ void yargs(hideBin(process.argv))
42
43
  .command(install)
43
44
  .command(database)
44
45
  .command(connector)
46
+ .command(translate)
45
47
  .demandCommand(1)
46
48
  .showHelpOnFail(false, `Specify ${chalk.green('--help')} for available options`)
47
49
  .strict()
@@ -42,6 +42,9 @@ export declare const packageJson: {
42
42
  dependencies: {
43
43
  "@logto/connector-kit": string;
44
44
  "@logto/core-kit": string;
45
+ "@logto/language-kit": string;
46
+ "@logto/phrases": string;
47
+ "@logto/phrases-ui": string;
45
48
  "@logto/schemas": string;
46
49
  "@logto/shared": string;
47
50
  "@silverhand/essentials": string;
@@ -1,6 +1,6 @@
1
1
  export const packageJson = {
2
2
  "name": "@logto/cli",
3
- "version": "1.0.2",
3
+ "version": "1.1.0",
4
4
  "description": "Logto CLI.",
5
5
  "author": "Silverhand Inc. <contact@silverhand.io>",
6
6
  "homepage": "https://github.com/logto-io/logto#readme",
@@ -45,6 +45,9 @@ export const packageJson = {
45
45
  "dependencies": {
46
46
  "@logto/connector-kit": "workspace:^",
47
47
  "@logto/core-kit": "workspace:^",
48
+ "@logto/language-kit": "workspace:^",
49
+ "@logto/phrases": "workspace:^",
50
+ "@logto/phrases-ui": "workspace:^",
48
51
  "@logto/schemas": "workspace:^",
49
52
  "@logto/shared": "workspace:^",
50
53
  "@silverhand/essentials": "^2.5.0",
@@ -69,8 +72,8 @@ export const packageJson = {
69
72
  "zod": "^3.20.2"
70
73
  },
71
74
  "devDependencies": {
72
- "@silverhand/eslint-config": "2.0.1",
73
- "@silverhand/ts-config": "2.0.3",
75
+ "@silverhand/eslint-config": "3.0.0",
76
+ "@silverhand/ts-config": "3.0.0",
74
77
  "@types/inquirer": "^9.0.0",
75
78
  "@types/jest": "^29.4.0",
76
79
  "@types/node": "^18.11.18",
package/lib/utils.d.ts CHANGED
@@ -8,6 +8,7 @@ type Log = Readonly<{
8
8
  error: (...args: Parameters<typeof console.log>) => never;
9
9
  }>;
10
10
  export declare const log: Log;
11
+ export declare const getProxy: () => string | undefined;
11
12
  export declare const downloadFile: (url: string, destination: string) => Promise<unknown>;
12
13
  export declare const getPathInModule: (moduleName: string, relativePath?: string) => string;
13
14
  export declare const oraPromise: <T>(promise: PromiseLike<T>, options?: Options, exitOnError?: boolean) => Promise<T>;
package/lib/utils.js CHANGED
@@ -1,8 +1,8 @@
1
- import { execSync } from 'child_process';
2
- import { createWriteStream, existsSync } from 'fs';
3
- import { readdir, readFile } from 'fs/promises';
4
- import { createRequire } from 'module';
5
- import path from 'path';
1
+ import { execSync } from 'node:child_process';
2
+ import { createWriteStream, existsSync } from 'node:fs';
3
+ import { readdir, readFile } from 'node:fs/promises';
4
+ import { createRequire } from 'node:module';
5
+ import path from 'node:path';
6
6
  import { conditionalString } from '@silverhand/essentials';
7
7
  import chalk from 'chalk';
8
8
  import { got } from 'got';
@@ -32,10 +32,13 @@ export const log = Object.freeze({
32
32
  process.exit(1);
33
33
  },
34
34
  });
35
- export const downloadFile = async (url, destination) => {
35
+ export const getProxy = () => {
36
36
  const { HTTPS_PROXY, HTTP_PROXY, https_proxy, http_proxy } = process.env;
37
+ return HTTPS_PROXY ?? https_proxy ?? HTTP_PROXY ?? http_proxy;
38
+ };
39
+ export const downloadFile = async (url, destination) => {
37
40
  const file = createWriteStream(destination);
38
- const proxy = HTTPS_PROXY ?? https_proxy ?? HTTP_PROXY ?? http_proxy;
41
+ const proxy = getProxy();
39
42
  const stream = got.stream(url, {
40
43
  ...(proxy && { agent: { https: new HttpsProxyAgent({ proxy }) } }),
41
44
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@logto/cli",
3
- "version": "1.0.2",
3
+ "version": "1.1.0",
4
4
  "description": "Logto CLI.",
5
5
  "author": "Silverhand Inc. <contact@silverhand.io>",
6
6
  "homepage": "https://github.com/logto-io/logto#readme",
@@ -50,12 +50,15 @@
50
50
  "zod": "^3.20.2",
51
51
  "@logto/connector-kit": "^1.1.0",
52
52
  "@logto/core-kit": "^1.1.0",
53
- "@logto/schemas": "^1.0.1",
54
- "@logto/shared": "^1.0.1"
53
+ "@logto/language-kit": "^1.0.0",
54
+ "@logto/phrases": "^1.1.0",
55
+ "@logto/phrases-ui": "^1.1.0",
56
+ "@logto/schemas": "^1.1.0",
57
+ "@logto/shared": "^1.0.3"
55
58
  },
56
59
  "devDependencies": {
57
- "@silverhand/eslint-config": "2.0.1",
58
- "@silverhand/ts-config": "2.0.3",
60
+ "@silverhand/eslint-config": "3.0.0",
61
+ "@silverhand/ts-config": "3.0.0",
59
62
  "@types/inquirer": "^9.0.0",
60
63
  "@types/jest": "^29.4.0",
61
64
  "@types/node": "^18.11.18",