cli package

This commit is contained in:
Stephen Zhou
2026-04-14 22:51:15 +08:00
parent 2c065e8a21
commit 25885f2fa8
9 changed files with 164 additions and 32 deletions

25
packages/cli/bin/dify-cli.js Executable file
View File

@ -0,0 +1,25 @@
#!/usr/bin/env node
import { spawnSync } from 'node:child_process'
import path from 'node:path'
import process from 'node:process'
import { fileURLToPath } from 'node:url'
const packageRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..')
const entryFile = path.join(packageRoot, 'src', 'cli.ts')
const tsxImport = await import.meta.resolve('tsx')
const result = spawnSync(
process.execPath,
['--import', tsxImport, entryFile, ...process.argv.slice(2)],
{
cwd: process.cwd(),
env: process.env,
stdio: 'inherit',
},
)
if (result.error)
throw result.error
process.exit(result.status ?? 1)

16
packages/cli/package.json Normal file
View File

@ -0,0 +1,16 @@
{
"name": "@dify/cli",
"private": true,
"version": "0.0.0-private",
"type": "module",
"bin": {
"dify-cli": "./bin/dify-cli.js"
},
"dependencies": {
"tsx": "catalog:",
"typescript": "catalog:"
},
"devDependencies": {
"@types/node": "catalog:"
}
}

57
packages/cli/src/cli.ts Normal file
View File

@ -0,0 +1,57 @@
import process from 'node:process'
import { runMigrationCommand } from './no-unchecked-indexed-access/migrate'
import { runNormalizeCommand } from './no-unchecked-indexed-access/normalize'
import { runResetCommand } from './no-unchecked-indexed-access/reset'
import { runBatchMigrationCommand } from './no-unchecked-indexed-access/run'
type CommandHandler = (argv: string[]) => Promise<void>
const COMMANDS = new Map<string, CommandHandler>([
['migrate', runMigrationCommand],
['normalize', runNormalizeCommand],
['reset', runResetCommand],
['run', runBatchMigrationCommand],
])
function printUsage() {
console.log(`Usage:
dify-cli no-unchecked-indexed-access migrate [options]
dify-cli no-unchecked-indexed-access run [options]
dify-cli no-unchecked-indexed-access normalize
dify-cli no-unchecked-indexed-access reset`)
}
async function main() {
const [group, command, ...restArgs] = process.argv.slice(2)
if (!group || group === 'help' || group === '--help' || group === '-h') {
printUsage()
return
}
if (group !== 'no-unchecked-indexed-access') {
printUsage()
throw new Error(`Unknown command group: ${group}`)
}
if (!command || command === 'help' || command === '--help' || command === '-h') {
printUsage()
return
}
const handler = COMMANDS.get(command)
if (!handler) {
printUsage()
throw new Error(`Unknown command: ${command}`)
}
await handler(restArgs)
}
try {
await main()
}
catch (error) {
console.error(error instanceof Error ? error.message : error)
process.exitCode = 1
}

View File

@ -1,7 +1,6 @@
import fs from 'node:fs/promises'
import path from 'node:path'
import process from 'node:process'
import { pathToFileURL } from 'node:url'
import ts from 'typescript'
const SUPPORTED_EXTENSIONS = new Set(['.ts', '.tsx', '.mts', '.cts'])
@ -44,6 +43,9 @@ export function parseArgs(argv: string[]): CliOptions {
if (!arg)
continue
if (arg === '--')
continue
if (arg === '--write') {
options.write = true
continue
@ -153,9 +155,6 @@ function isTargetFile(fileName: string): boolean {
if (fileName.endsWith('.d.ts'))
return false
if (fileName.endsWith(`${path.sep}scripts${path.sep}migrate-no-unchecked-indexed-access.ts`))
return false
return !fileName.includes(`${path.sep}.next${path.sep}`)
}
@ -1642,9 +1641,6 @@ export async function runMigration(options: CliOptions) {
return { converged, totalEdits }
}
async function main() {
await runMigration(parseArgs(process.argv.slice(2)))
export async function runMigrationCommand(argv: string[]) {
await runMigration(parseArgs(argv))
}
if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href)
await main()

View File

@ -1,7 +1,7 @@
import fs from 'node:fs/promises'
import path from 'node:path'
import process from 'node:process'
import { normalizeMalformedAssertions } from './migrate-no-unchecked-indexed-access'
import { normalizeMalformedAssertions } from './migrate'
const ROOT = process.cwd()
const EXTENSIONS = new Set(['.ts', '.tsx'])
@ -46,4 +46,6 @@ async function main() {
console.log(`Normalized malformed assertion syntax in ${changedFileCount} file(s).`)
}
await main()
export async function runNormalizeCommand(_argv: string[]) {
await main()
}

View File

@ -9,15 +9,29 @@ const execFileAsync = promisify(execFile)
const PRESERVED_FILES = new Set([
'web/package.json',
'web/tsconfig.json',
'web/scripts/migrate-no-unchecked-indexed-access.ts',
'web/scripts/run-no-unchecked-indexed-access-migration.ts',
'web/scripts/normalize-no-unchecked-indexed-access-migration.ts',
'web/scripts/reset-no-unchecked-indexed-access-migration.ts',
])
async function getTrackedChangedFiles(): Promise<string[]> {
async function findWorkspaceRoot(startDirectory: string): Promise<string> {
let currentDirectory = path.resolve(startDirectory)
while (true) {
try {
await fs.access(path.join(currentDirectory, 'pnpm-workspace.yaml'))
return currentDirectory
}
catch {}
const parentDirectory = path.dirname(currentDirectory)
if (parentDirectory === currentDirectory)
throw new Error('Could not find workspace root from the current directory.')
currentDirectory = parentDirectory
}
}
async function getTrackedChangedFiles(repoRoot: string): Promise<string[]> {
const { stdout } = await execFileAsync('git', ['diff', '--name-only', 'HEAD', '--', 'web'], {
cwd: path.resolve(process.cwd(), '..'),
cwd: repoRoot,
maxBuffer: 1024 * 1024 * 8,
})
@ -27,9 +41,9 @@ async function getTrackedChangedFiles(): Promise<string[]> {
.filter(Boolean)
}
async function getUntrackedFiles(): Promise<string[]> {
async function getUntrackedFiles(repoRoot: string): Promise<string[]> {
const { stdout } = await execFileAsync('git', ['ls-files', '--others', '--exclude-standard', '--', 'web'], {
cwd: path.resolve(process.cwd(), '..'),
cwd: repoRoot,
maxBuffer: 1024 * 1024 * 8,
})
@ -40,9 +54,9 @@ async function getUntrackedFiles(): Promise<string[]> {
}
async function main() {
const repoRoot = path.resolve(process.cwd(), '..')
const trackedChangedFiles = await getTrackedChangedFiles()
const untrackedFiles = await getUntrackedFiles()
const repoRoot = await findWorkspaceRoot(process.cwd())
const trackedChangedFiles = await getTrackedChangedFiles(repoRoot)
const untrackedFiles = await getUntrackedFiles(repoRoot)
let restoredCount = 0
for (const relativePath of trackedChangedFiles) {
@ -71,4 +85,6 @@ async function main() {
console.log(`Restored ${restoredCount} tracked file(s) and removed ${removedCount} untracked file(s).`)
}
await main()
export async function runResetCommand(_argv: string[]) {
await main()
}

View File

@ -2,7 +2,7 @@ import { execFile } from 'node:child_process'
import path from 'node:path'
import process from 'node:process'
import { promisify } from 'node:util'
import { runMigration, SUPPORTED_DIAGNOSTIC_CODES } from './migrate-no-unchecked-indexed-access'
import { runMigration, SUPPORTED_DIAGNOSTIC_CODES } from './migrate'
const execFileAsync = promisify(execFile)
const DIAGNOSTIC_PATTERN = /^(.+?\.(?:ts|tsx))\((\d+),(\d+)\): error TS(\d+): (.+)$/
@ -37,6 +37,9 @@ function parseArgs(argv: string[]): CliOptions {
for (let i = 0; i < argv.length; i += 1) {
const arg = argv[i]
if (arg === '--')
continue
if (arg === '--verbose') {
options.verbose = true
continue
@ -166,9 +169,7 @@ function chunk<T>(items: T[], size: number): T[][] {
return batches
}
async function main() {
const options = parseArgs(process.argv.slice(2))
async function runBatchMigration(options: CliOptions) {
for (let round = 1; round <= options.maxRounds; round += 1) {
const { diagnostics, exitCode, rawOutput } = await runTypeCheck(options.project)
if (exitCode === 0) {
@ -226,4 +227,6 @@ async function main() {
process.exitCode = 1
}
await main()
export async function runBatchMigrationCommand(argv: string[]) {
await runBatchMigration(parseArgs(argv))
}

16
pnpm-lock.yaml generated
View File

@ -599,6 +599,19 @@ importers:
specifier: 'catalog:'
version: 0.1.16(@types/node@25.6.0)(@voidzero-dev/vite-plus-core@0.1.16(@types/node@25.6.0)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3))(happy-dom@20.9.0)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3)
packages/cli:
dependencies:
tsx:
specifier: 'catalog:'
version: 4.21.0
typescript:
specifier: 'catalog:'
version: 6.0.2
devDependencies:
'@types/node':
specifier: 'catalog:'
version: 25.6.0
packages/iconify-collections:
devDependencies:
iconify-import-svg:
@ -953,6 +966,9 @@ importers:
'@chromatic-com/storybook':
specifier: 'catalog:'
version: 5.1.2(storybook@10.3.5(@testing-library/dom@10.4.1)(react-dom@19.2.5(react@19.2.5))(react@19.2.5))
'@dify/cli':
specifier: workspace:*
version: link:../packages/cli
'@dify/iconify-collections':
specifier: workspace:*
version: link:../packages/iconify-collections

View File

@ -39,10 +39,10 @@
"lint:fix": "vp run lint --fix",
"lint:quiet": "vp run lint --quiet",
"lint:tss": "tsslint --project tsconfig.json",
"migrate:no-unchecked-indexed-access": "tsx ./scripts/migrate-no-unchecked-indexed-access.ts",
"migrate:no-unchecked-indexed-access:all": "tsx ./scripts/run-no-unchecked-indexed-access-migration.ts",
"migrate:no-unchecked-indexed-access:normalize": "tsx ./scripts/normalize-no-unchecked-indexed-access-migration.ts",
"migrate:no-unchecked-indexed-access:reset": "tsx ./scripts/reset-no-unchecked-indexed-access-migration.ts",
"migrate:no-unchecked-indexed-access": "dify-cli no-unchecked-indexed-access migrate",
"migrate:no-unchecked-indexed-access:all": "dify-cli no-unchecked-indexed-access run",
"migrate:no-unchecked-indexed-access:normalize": "dify-cli no-unchecked-indexed-access normalize",
"migrate:no-unchecked-indexed-access:reset": "dify-cli no-unchecked-indexed-access reset",
"preinstall": "npx only-allow pnpm",
"refactor-component": "node ./scripts/refactor-component.js",
"start": "node ./scripts/copy-and-start.mjs",
@ -163,6 +163,7 @@
"devDependencies": {
"@antfu/eslint-config": "catalog:",
"@chromatic-com/storybook": "catalog:",
"@dify/cli": "workspace:*",
"@dify/iconify-collections": "workspace:*",
"@egoist/tailwindcss-icons": "catalog:",
"@eslint-react/eslint-plugin": "catalog:",