@rschili/melodi-cli
Version:
iModel utility
4 lines • 153 kB
Source Map (JSON)
{
"version": 3,
"sources": ["../src/Workspace.ts", "../src/ConsoleHelper.ts", "../src/buildInfo.ts", "../package.json", "../src/Diagnostics.ts", "../src/EnvironmentManager.ts", "../src/Logic/NewFile.ts", "../src/UnifiedDb.ts", "../src/IModelConfig.ts", "../src/Logic/DbEditor.ts", "../src/Logic/SchemaEditor.ts", "../src/GithubBisSchemasHelper.ts", "../src/Workspace.UserConfig.ts", "../src/Logic/DbSettings.ts", "../src/Logic/Changesets.ts", "../src/Logic/Backup.ts", "../src/Logic/FileActions.ts", "../src/Logic/FileSelector.ts", "../src/Logger.ts", "../src/Runner.ts", "../src/index.ts"],
"sourcesContent": ["import * as fs from 'fs';\nimport path from \"path\";\nimport os from \"os\";\nimport { z } from \"zod/v4\";\nimport { globby } from 'globby';\nimport { SQLiteDb, SqliteStatement } from \"@itwin/core-backend\";\nimport { DbResult, OpenMode } from \"@itwin/core-bentley\";\nimport { printError } from \"./ConsoleHelper\";\nimport { SemVer } from \"semver\";\nimport { applicationVersion } from \"./Diagnostics\";\nimport { EnvironmentManager } from \"./EnvironmentManager\";\nimport { UserConfig } from \"./Workspace.UserConfig\";\n\nconst WorkspaceConfigSchema = z.object({\n melodiVersion: z.string(),\n ecsqlHistory: z.array(z.string()).optional(),\n});\n\nexport type WorkspaceConfig = z.infer<typeof WorkspaceConfigSchema>;\n\nexport const MelodiConfigFolderName = '.melodi';\nexport const CacheFolderName = '.itwinjs-cache';\nexport const ConfigFileName = 'config.json';\n\nexport type Workspace = {\n workspaceRootPath: string;\n workspaceConfigDirPath: string;\n config?: WorkspaceConfig;\n userConfig: UserConfig;\n files?: WorkspaceFile[];\n\n envManager: EnvironmentManager;\n}\n\nexport type WorkspaceFile = {\n relativePath: string;\n lastTouched: Date;\n parentChangeSetId?: string;\n beDbVersion?: SchemaVersion;\n ecDbVersion?: SchemaVersion;\n dgn_DbVersion?: SchemaVersion;\n bisCoreVersion?: SemVer;\n elements?: number; // Optional: number of bis_Element records in the iModel, if applicable\n}\n\nexport async function loadWorkspace(userConfig: UserConfig, root: string = process.cwd()): Promise<Workspace> {\n // check if there is a \".melodi\" subdirectory in the current working directory\n \n const workspaceRootPath = root;\n const userConfigDirPath = path.join(os.homedir(), MelodiConfigFolderName)\n const melodiConfigPath = path.join(workspaceRootPath, MelodiConfigFolderName);\n const cacheDirPath = path.join(workspaceRootPath, CacheFolderName);\n if (!fs.existsSync(workspaceRootPath) || !fs.lstatSync(workspaceRootPath).isDirectory()) {\n throw new Error(`The current working directory is not a valid directory: ${workspaceRootPath}`);\n }\n\n try {\n fs.accessSync(workspaceRootPath, fs.constants.R_OK | fs.constants.W_OK)\n }\n catch {\n throw new Error(`The current working directory is not accessible: ${workspaceRootPath}. Please check permissions.`);\n }\n\n const configPath = path.join(melodiConfigPath, ConfigFileName);\n const environment = new EnvironmentManager(cacheDirPath);\n if (!fs.existsSync(configPath)) {\n return {\n workspaceRootPath,\n workspaceConfigDirPath: melodiConfigPath,\n envManager: environment,\n userConfig\n };\n }\n\n const config = await readWorkspaceConfig(configPath);\n // create the user config directory if it doesn't exist\n if (!fs.existsSync(userConfigDirPath)) {\n await fs.promises.mkdir(userConfigDirPath, { recursive: true });\n }\n\n return {\n workspaceRootPath,\n workspaceConfigDirPath: melodiConfigPath,\n config,\n envManager: environment,\n userConfig\n };\n}\n\n// Read and validate config\nexport async function readWorkspaceConfig(configPath: string): Promise<WorkspaceConfig> {\n const data = await fs.promises.readFile(configPath, 'utf-8');\n const json = JSON.parse(data);\n return await WorkspaceConfigSchema.parseAsync(json);\n}\n\n// Save config (overwrite)\nexport async function saveWorkspaceConfig(ws: Workspace): Promise<void> {\n if(ws.config === undefined) {\n throw new Error(\"Workspace config is undefined. Please provide a valid config.\");\n }\n\n // Validate the config before saving\n if (!fs.existsSync(ws.workspaceConfigDirPath)) {\n await fs.promises.mkdir(ws.workspaceConfigDirPath, { recursive: true });\n }\n\n if (!fs.lstatSync(ws.workspaceConfigDirPath).isDirectory()) {\n throw new Error(`The workspace config directory is not a valid directory: ${ws.workspaceConfigDirPath}`);\n }\n\n try {\n fs.accessSync(ws.workspaceConfigDirPath, fs.constants.R_OK | fs.constants.W_OK);\n } catch {\n throw new Error(`The workspace config directory is not accessible: ${ws.workspaceConfigDirPath}. Please check permissions.`);\n }\n\n const configPath = path.join(ws.workspaceConfigDirPath, ConfigFileName);\n ws.config.melodiVersion = applicationVersion; // Ensure the version is up-to-date\n const data = JSON.stringify(ws.config, undefined, 2);\n await fs.promises.writeFile(configPath, data, 'utf-8');\n}\n\nexport async function detectWorkspaceFiles(ws: Workspace): Promise<void> {\n const patterns = [\n '**/*.bim',\n '**/*.ecdb',\n ];\n\n const ignore = [\n '**/.*', // Ignore dotfiles and dotfolders\n '**/.*/**', // Also ignore anything inside dotfolders\n ];\n\n const files = await globby(patterns, {\n cwd: ws.workspaceRootPath,\n absolute: false,\n deep: 2, // Limit to 2 levels deep\n dot: false, // Don't match dotfiles/folders\n ignore,\n caseSensitiveMatch: false,\n });\n\n const workspaceFiles: WorkspaceFile[] = files.map(file => {\n const absolutePath = path.join(ws.workspaceRootPath, file);\n\n const stats = fs.statSync(absolutePath);\n const lastTouched = new Date(Math.max(stats.mtime.getTime(), stats.birthtime.getTime(), stats.ctime.getTime()));\n\n return {\n relativePath: file,\n lastTouched,\n };\n });\n\n await readFileProps(ws, workspaceFiles);\n ws.files = workspaceFiles;\n}\n// Folder to hold context information for a file. for file /home/user/workspace/file.bim the folder would be /home/user/workspace/file_extras/\nexport function getFileContextFolderPath(root: string, relativeFilePath: string): string {\n const parsed = path.parse(relativeFilePath);\n const contextFolderName = `${parsed.name}_extras`;\n return path.join(root, parsed.dir, contextFolderName);\n}\n\nconst schemaVersionSchema = z.object({\n major: z.number(),\n minor: z.number(),\n sub1: z.number(),\n sub2: z.number(),\n});\n\nexport type SchemaVersion = z.infer<typeof schemaVersionSchema>;\n\nasync function readFileProps(ws: Workspace, files: WorkspaceFile[]): Promise<void> {\n if (files.length === 0) {\n return;\n }\n\n const db = new SQLiteDb();\n for (const file of files) {\n try {\n const absolutePath = path.join(ws.workspaceRootPath, file.relativePath);\n db.openDb(absolutePath, OpenMode.Readonly);\n db.withPreparedSqliteStatement(\"SELECT Name, Val FROM be_Local\", (stmt: SqliteStatement) => {\n while (stmt.step() === DbResult.BE_SQLITE_ROW) {\n const name = stmt.getValueString(0);\n if(name === \"ParentChangeSetId\") {\n file.parentChangeSetId = stmt.getValueString(1);\n }\n }\n });\n \n db.withPreparedSqliteStatement(\"SELECT Namespace, StrData FROM be_Prop WHERE Name = ?\", (stmt: SqliteStatement) => {\n stmt.bindString(1, \"SchemaVersion\");\n while (stmt.step() === DbResult.BE_SQLITE_ROW) {\n const namespace = stmt.getValueString(0);\n const schemaVersion = stmt.getValueString(1);\n const parsedSchemaVersion = schemaVersionSchema.safeParse(JSON.parse(schemaVersion));\n if (parsedSchemaVersion.success) {\n switch (namespace.toLowerCase()) {\n case \"be_db\":\n file.beDbVersion = parsedSchemaVersion.data;\n break;\n case \"ec_db\":\n file.ecDbVersion = parsedSchemaVersion.data;\n break;\n case \"dgn_db\":\n file.dgn_DbVersion = parsedSchemaVersion.data;\n break;\n default:\n console.warn(`Unknown schema version namespace: ${namespace}. This may not be supported by melodi.`);\n break;\n }\n }\n }\n });\n\n db.withPreparedSqliteStatement(\"SELECT VersionDigit1, VersionDigit2, VersionDigit3 from ec_Schema WHERE Name = ?\", (stmt: SqliteStatement) => {\n stmt.bindString(1, \"BisCore\");\n if (stmt.step() === DbResult.BE_SQLITE_ROW) {\n const major = stmt.getValueInteger(0);\n const minor = stmt.getValueInteger(1);\n const sub1 = stmt.getValueInteger(2);\n file.bisCoreVersion = new SemVer(`${major}.${minor}.${sub1}`);\n }\n });\n\n if(file.bisCoreVersion !== undefined) {\n db.withPreparedSqliteStatement(\"SELECT COUNT(*) FROM bis_Element\", (stmt: SqliteStatement) => {\n if (stmt.step() === DbResult.BE_SQLITE_ROW) {\n file.elements = stmt.getValueInteger(0);\n }\n });\n }\n\n db.closeDb();\n } catch (error) {\n printError(error, true);\n } finally {\n if(db.isOpen) {\n db.closeDb();\n }\n }\n }\n}\n", "import { log } from \"@clack/prompts\";\nimport chalk from \"chalk\";\n\nexport const formatPath = chalk.blueBright.underline;\nexport const formatError = chalk.redBright.bold;\nexport const formatWarning = chalk.yellowBright;\nexport const formatSuccess = chalk.greenBright.bold;\nexport const resetChar = \"\\x1B[0m\";\n\nexport function printError(error: unknown, printAsWarning: boolean = false): void {\n const formatter = printAsWarning ? formatWarning : formatError;\n const label = printAsWarning ? \"Warning\" : \"Error\";\n if (error instanceof Error) {\n console.error(formatter(`${label}: ${error.message}`));\n /*if (error.stack) {\n console.error(chalk.gray(error.stack));\n }*/\n } else {\n console.error(formatter(`${label}: ${String(error)}`));\n }\n}\n\nexport function logError(error: unknown): void {\n if (error instanceof Error) {\n log.error(error.message);\n /*if (error.stack) {\n console.error(chalk.gray(error.stack));\n }*/\n } else {\n log.error(`Error: ${String(error)}`);\n }\n}\n\nexport function generateColorizerMap<T>(values: T[]): Map<T, (text: string) => string> {\n const colorizerMap = new Map<T, (text: string) => string>();\n const colors = [\n chalk.redBright,\n chalk.greenBright,\n chalk.blueBright,\n chalk.yellowBright,\n chalk.cyanBright,\n chalk.magentaBright,\n chalk.whiteBright,\n ];\n\n const uniqueValues = Array.from(new Set(values));\n uniqueValues.forEach((value, index) => {\n const color = colors[index % colors.length];\n colorizerMap.set(value, color);\n });\n\n return colorizerMap;\n}\n\nconst msInSecond = 1000;\nconst msInMinute = msInSecond * 60;\nconst msInHour = msInMinute * 60;\nconst msInDay = msInHour * 24;\nconst msInYear = msInDay * 365.25;\nexport function timeSpanToString(span: number): string | undefined {\n if (span > msInYear * 100 || span <= 0) {\n return undefined;\n }\n\n if (span < msInMinute) {\n const seconds = Math.floor(span / msInSecond);\n return `${seconds} second${seconds !== 1 ? \"s\" : \"\"}`;\n } else if (span < msInHour) {\n const minutes = Math.floor(span / msInMinute);\n return `${minutes} minute${minutes !== 1 ? \"s\" : \"\"}`;\n } else if (span < msInDay) {\n const hours = Math.floor(span / msInHour);\n return `${hours} hour${hours !== 1 ? \"s\" : \"\"}`;\n } else if (span < msInYear) {\n const days = Math.floor(span / msInDay);\n return `${days} day${days !== 1 ? \"s\" : \"\"}`;\n } else {\n const years = Math.floor(span / msInYear);\n return `${years} year${years !== 1 ? \"s\" : \"\"}`;\n }\n}", "export const __BUILD_DATE__ = '2025-07-09T08:03:37Z';\n", "{\n \"name\": \"@rschili/melodi-cli\",\n \"version\": \"1.3.1\",\n \"description\": \"iModel utility\",\n \"main\": \"dist/index.mjs\",\n \"type\": \"module\",\n \"engines\": {\n \"node\": \">=22.14.0\"\n },\n \"scripts\": {\n \"typecheck\": \"tsc --noEmit\",\n \"build\": \"node esbuild.config.mjs\",\n \"test\": \"vitest\",\n \"start\": \"node dist/index.mjs\",\n \"lint\": \"eslint 'src/**/*.ts' --fix\",\n \"prebuild\": \"echo \\\"export const __BUILD_DATE__ = '$(date -u +%Y-%m-%dT%H:%M:%SZ)';\\\" > src/buildInfo.ts && npm run lint && npm run typecheck\"\n },\n \"bin\": {\n \"melodi\": \"dist/index.mjs\"\n },\n \"publishConfig\": {\n \"access\": \"public\"\n },\n \"files\": [\n \"dist\",\n \"LICENSE\",\n \"README.md\",\n \"CHANGELOG.md\"\n ],\n \"repository\": {\n \"type\": \"git\",\n \"url\": \"git+https://github.com/rschili/melodi-cli.git\"\n },\n \"keywords\": [\n \"itwin\",\n \"imodel\",\n \"bentley\",\n \"ecdb\",\n \"bim\"\n ],\n \"author\": \"Robert Schili\",\n \"license\": \"MIT\",\n \"bugs\": {\n \"url\": \"https://github.com/rschili/melodi-cli/issues\"\n },\n \"homepage\": \"https://github.com/rschili/melodi-cli#readme\",\n \"devDependencies\": {\n \"@eslint/js\": \"^9.30.1\",\n \"@types/node\": \"^24.0.10\",\n \"@typescript-eslint/eslint-plugin\": \"^8.36.0\",\n \"@typescript-eslint/parser\": \"^8.36.0\",\n \"esbuild\": \"^0.25.6\",\n \"esbuild-node-externals\": \"^1.18.0\",\n \"eslint\": \"^9.30.1\",\n \"globals\": \"^16.3.0\",\n \"typescript\": \"^5.8.3\",\n \"typescript-eslint\": \"^8.36.0\",\n \"vitest\": \"^3.2.4\"\n },\n \"dependencies\": {\n \"@clack/prompts\": \"^0.11.0\",\n \"@itwin/core-backend\": \"^5.0.2\",\n \"@itwin/core-bentley\": \"^5.0.2\",\n \"@itwin/core-common\": \"^5.0.2\",\n \"@itwin/ecschema-metadata\": \"^5.0.2\",\n \"@itwin/imodels-access-backend\": \"^6.0.1\",\n \"@itwin/imodels-access-common\": \"^6.0.1\",\n \"@itwin/imodels-client-authoring\": \"^6.0.1\",\n \"@itwin/itwins-client\": \"^1.6.1\",\n \"@itwin/node-cli-authorization\": \"^3.0.1\",\n \"@itwin/object-storage-azure\": \"^3.0.1\",\n \"@itwin/object-storage-core\": \"^3.0.1\",\n \"@itwin/object-storage-google\": \"^3.0.1\",\n \"@types/semver\": \"^7.7.0\",\n \"axios\": \"^1.10.0\",\n \"chalk\": \"^5.4.1\",\n \"emphasize\": \"^7.0.0\",\n \"globby\": \"^14.1.0\",\n \"gradient-string\": \"^3.0.0\",\n \"module-alias\": \"^2.2.3\",\n \"semver\": \"^7.7.2\",\n \"simple-update-notifier\": \"^2.0.0\",\n \"table\": \"^6.9.0\",\n \"zod\": \"^3.25.76\"\n },\n \"overrides\": {\n \"inversify\": \"7.5.2\",\n \"reflect-metadata\": \"^0.2.2\"\n }\n}\n", "import { __BUILD_DATE__ } from \"./buildInfo\";\nimport pkg from '../package.json'\nimport updateNotifier from 'simple-update-notifier'\n\nexport const applicationVersion: string = pkg.version;\n\nexport const applicationBuildDate: string = new Date(__BUILD_DATE__).toLocaleString();\n\nexport async function checkUpdates(): Promise<void> {\n await updateNotifier({ pkg: pkg });\n}", "import { select } from \"@clack/prompts\";\nimport { IModelHost } from \"@itwin/core-backend\";\nimport { IModelsClient, IModelsClientOptions } from \"@itwin/imodels-client-authoring\";\nimport { ITwinsAccessClient } from \"@itwin/itwins-client\";\nimport { NodeCliAuthorizationClient } from \"@itwin/node-cli-authorization\";\nimport { AzureClientStorage, BlockBlobClientWrapperFactory } from \"@itwin/object-storage-azure\";\nimport { StrategyClientStorage } from \"@itwin/object-storage-core\";\nimport { GoogleClientStorage } from \"@itwin/object-storage-google/lib/client/index.js\";\nimport { ClientStorageWrapperFactory } from \"@itwin/object-storage-google/lib/client/wrappers/index.js\";\nimport { AccessTokenAdapter } from \"@itwin/imodels-access-common\";\nimport { Authorization } from \"@itwin/imodels-client-management\";\nimport { BackendIModelsAccess } from \"@itwin/imodels-access-backend\";\n\n\nexport enum Environment {\n PROD = 'PROD',\n QA = 'QA'\n}\n\n/**\n * This manages all clients that need to be initialized based on a selected environment.\n * Makes it easier to switch between environments and ensures that the clients are properly initialized.\n */\nexport class EnvironmentManager {\n private _cacheDir: string;\n private _currentEnvironment: Environment = Environment.PROD;\n private _authClient?: NodeCliAuthorizationClient;\n private _iModelsClient?: IModelsClient;\n private _iTwinsClient?: ITwinsAccessClient;\n private _isSignedIn: boolean = false;\n private _isStartedUp: boolean = false;\n\n constructor(cacheDir: string) {\n this._cacheDir = cacheDir;\n }\n\n public get currentEnvironment(): Environment {\n return this._currentEnvironment;\n }\n\n public get cacheDirectory(): string {\n return this._cacheDir;\n }\n\n public async selectEnvironment(newEnvironment: Environment): Promise<void> {\n if(newEnvironment !== this._currentEnvironment) {\n await this.shutdown();\n this._currentEnvironment = newEnvironment;\n await this.startup();\n }\n }\n\n public async startup(): Promise<void> {\n if(this._isStartedUp) {\n return;\n }\n\n const hubAccess = new BackendIModelsAccess(this.iModelsClient);\n await IModelHost.startup({ \n cacheDir: this._cacheDir,\n hubAccess: hubAccess,\n authorizationClient: this.authClient,\n });\n this._isStartedUp = true;\n }\n\n public async shutdown(): Promise<void> {\n if(this._isStartedUp) {\n this._authClient = undefined;\n this._iModelsClient = undefined;\n this._iTwinsClient = undefined;\n this._isSignedIn = false;\n this._isStartedUp = false;\n\n await IModelHost.shutdown();\n }\n }\n\n public get authority(): string {\n if(this._currentEnvironment === Environment.PROD) {\n return \"https://ims.bentley.com/\";\n }\n\n if(this._currentEnvironment === Environment.QA) {\n return \"https://qa-ims.bentley.com/\";\n }\n\n throw new Error(`Unknown environment: ${this._currentEnvironment}`);\n }\n\n public get clientId(): string {\n switch (this._currentEnvironment) {\n case Environment.PROD:\n return \"native-b517RwSFtag94aBZ5lM40QCf6\";\n case Environment.QA:\n return \"native-jq2fZ8ZMoMjTKVDghCOpjY4JQ\";\n default:\n throw new Error(`Unknown environment: ${this._currentEnvironment}`);\n }\n }\n\n public async getAccessToken(): Promise<{scheme: string, token: string}> {\n if (this._authClient === undefined) {\n throw new Error(\"Authorization client is not initialized. Call signInIfNecessary() first.\");\n }\n\n const parts = (await this._authClient.getAccessToken()).split(\" \");\n return { scheme: parts[0], token: parts[1] };\n }\n\n public async getAuthorization(): Promise<Authorization> {\n if (this._authClient === undefined) {\n throw new Error(\"Authorization client is not initialized. Call signInIfNecessary() first.\");\n }\n\n return AccessTokenAdapter.toAuthorization(await this._authClient!.getAccessToken());\n }\n public get authClient(): NodeCliAuthorizationClient {\n if (!this._authClient) {\n this._authClient = new NodeCliAuthorizationClient({\n issuerUrl: this.authority,\n clientId: this.clientId,\n redirectUri: \"http://localhost:3000/signin-callback\",\n scope: \"itwin-platform\"\n });\n }\n\n return this._authClient;\n }\n\n public async signInIfNecessary(): Promise<void> {\n if (!this._isSignedIn) {\n await this.authClient.signIn();\n }\n }\n\n public get iModelsClient(): IModelsClient {\n if (!this._iModelsClient) {\n const iModelsClientOptions: IModelsClientOptions = {\n cloudStorage: new StrategyClientStorage([\n {\n instanceName: \"azure\",\n instance: new AzureClientStorage(new BlockBlobClientWrapperFactory()),\n },\n {\n instanceName: \"google\",\n instance: new GoogleClientStorage(new ClientStorageWrapperFactory()),\n }\n ]),\n api: this._currentEnvironment === Environment.QA ? { baseUrl: \"https://qa-api.bentley.com/imodels\" } : undefined,\n }\n\n this._iModelsClient = new IModelsClient(iModelsClientOptions);\n }\n return this._iModelsClient;\n }\n\n public get iTwinsClient(): ITwinsAccessClient {\n if (!this._iTwinsClient) {\n this._iTwinsClient = new ITwinsAccessClient(this._currentEnvironment === Environment.QA ? \"https://qa-api.bentley.com/itwins\" : undefined);\n }\n return this._iTwinsClient;\n }\n\n public async promptEnvironment() : Promise<Environment | symbol> {\n return await select({\n message: \"Select an environment\",\n options: [\n {label: \"PROD\", value: Environment.PROD },\n {label: \"QA\", value: Environment.QA },\n ],\n initialValue: this._currentEnvironment,\n });\n }\n}", "import { Workspace, WorkspaceFile } from \"../Workspace\";\nimport { log, select, text, isCancel, tasks, Option, spinner, confirm } from \"@clack/prompts\";\nimport { ITwin, ITwinSubClass } from \"@itwin/itwins-client\";\nimport chalk from \"chalk\";\nimport { generateColorizerMap, logError } from \"../ConsoleHelper\";\nimport { Guid } from \"@itwin/core-bentley\";\nimport { MinimalIModel, MinimalNamedVersion } from \"@itwin/imodels-client-management\";\nimport { existsSync } from \"node:fs\";\nimport { createECDb, createStandaloneDb, openBriefcaseDb } from \"../UnifiedDb\";\nimport { DbEditor } from \"./DbEditor\";\nimport fs from \"node:fs/promises\";\nimport { IModelConfig, saveIModelConfig } from \"../IModelConfig\";\nimport { applicationVersion } from \"../Diagnostics\";\nimport { CheckpointManager, ProgressStatus } from \"@itwin/core-backend\";\nimport path from \"path\";\nimport { ChangesetIdWithIndex } from \"@itwin/core-common\";\nimport { Changesets } from \"./Changesets\";\n\nexport class NewFile {\n public static async run(ws: Workspace): Promise<void> {\n const workspaceType = await select({\n message: 'Choose an option:',\n options: [\n { label: 'Download an iModel from iModelHub', value: \"__download__\" },\n { label: 'Initialize a new ECDb', value: \"__ecdb__\" },\n { label: 'Initialize a new standalone iModel', value: \"__standalone__\" },\n ],\n });\n\n if(isCancel(workspaceType)) {\n return; // User cancelled the prompt\n }\n\n switch (workspaceType) {\n case \"__download__\":\n return this.downloadFromHub(ws);\n case \"__ecdb__\":\n return this.initializeECDb(ws);\n case \"__standalone__\":\n return this.initializeStandaloneDb(ws);\n }\n }\n\n public static async initializeECDb(ws: Workspace): Promise<void> {\n const fileName = await text({\n message: \"Enter the name for the new ECDb file (without extension):\",\n });\n if(isCancel(fileName)) {\n return; // User cancelled the prompt\n }\n const filePath = path.join(ws.workspaceRootPath, fileName.trim() + \".ecdb\");\n const dirPath = path.dirname(filePath);\n if (!existsSync(dirPath)) {\n // Ensure the directory exists\n await fs.mkdir(dirPath, { recursive: true });\n }\n if (existsSync(filePath)) {\n log.error(`File \"${filePath}\" already exists. Please choose a different name.`);\n return this.initializeECDb(ws);\n }\n\n const db = createECDb(filePath);\n await DbEditor.run(ws, { relativePath: fileName.trim() + \".ecdb\", lastTouched: new Date() }, db);\n }\n\n public static async initializeStandaloneDb(ws: Workspace): Promise<void> {\n const fileName = await text({\n message: \"Enter the name for the new standalone iModel file (without extension):\",\n });\n if(isCancel(fileName)) {\n return; // User cancelled the prompt\n }\n const fileNameWithExt = fileName.trim() + \".bim\";\n const filePath = path.join(ws.workspaceRootPath, fileNameWithExt);\n const dirPath = path.dirname(filePath);\n if (!existsSync(dirPath)) {\n // Ensure the directory exists\n await fs.mkdir(dirPath, { recursive: true });\n }\n if (existsSync(filePath)) {\n log.error(`File \"${filePath}\" already exists. Please choose a different name.`);\n return this.initializeStandaloneDb(ws);\n }\n\n const rootSubject = await text({\n message: \"Enter the root subject name for the new standalone iModel:\",\n initialValue: fileName.trim(),\n });\n if(isCancel(rootSubject)) {\n return; // User cancelled the prompt\n }\n const db = createStandaloneDb(filePath, rootSubject.trim());\n await DbEditor.run(ws, { relativePath: fileNameWithExt, lastTouched: new Date() }, db);\n }\n\n public static async downloadFromHub(ws: Workspace): Promise<void> {\n const envManager = ws.envManager;\n const environment = await envManager.promptEnvironment();\n if(isCancel(environment)) {\n return; // User cancelled the prompt\n }\n\n let token: string = \"\";\n let iTwins: ITwin[] = [];\n await tasks([\n {\n title: `Setting up environment to ${environment.toString()}`,\n task: async () => {\n await envManager.selectEnvironment(environment);\n return `Environment set up to ${environment.toString()}`;\n },\n },\n {\n title: \"Authenticating...\",\n task: async () => {\n await envManager.signInIfNecessary();\n token = await envManager.authClient.getAccessToken();\n return \"Authenticated.\";\n }\n },\n {\n title: \"Detecting available iTwins...\",\n task: async () => {\n const iTwinsResponse = await envManager.iTwinsClient.queryAsync(token);\n if (iTwinsResponse.status !== 200) {\n throw new Error(`Failed to fetch iTwins: ${iTwinsResponse.error?.message ?? \"Unknown error\"}`);\n }\n\n iTwins = iTwinsResponse.data ?? [];\n return `Found ${iTwins.length} available iTwins.`;\n }\n }\n ]);\n\n const subClasses = iTwins.map(iTwin => iTwin.subClass ?? ITwinSubClass.Project);\n const colorizerMap = generateColorizerMap(subClasses);\n\n const iTwinChoices: Option<ITwin>[] = iTwins.map(iTwin => {\n const subClass = iTwin.subClass;\n let colorizedSubClass = '';\n if(subClass !== undefined) {\n const colorizer = colorizerMap.get(subClass);\n if(colorizer) {\n colorizedSubClass = colorizer(subClass);\n }\n }\n return {\n label: `${chalk.bold(iTwin.displayName)} (${iTwin.id}) ${colorizedSubClass}`,\n value: iTwin\n }});\n\n const thingToPull = await select<ITwin | \"iModel\" | \"iTwin\">({\n message: 'Select an iTwin',\n options: [\n { label: \"Pull by iModel ID\", value: \"iModel\" },\n { label: \"Pull by iTwin ID\", value: \"iTwin\" },\n ...iTwinChoices\n ],\n\n maxItems: 20,\n });\n\n if(isCancel(thingToPull)) {\n return; // User cancelled the prompt\n }\n\n let iTwinOrIModelId: string | undefined = undefined;\n let iModelId: string | undefined = undefined;\n if (thingToPull === \"iModel\" || thingToPull === \"iTwin\") {\n // in case of iTwin or iModel we need to ask for the ID\n const unverifiedId = await text({ message: \"Please provide the ID:\"});\n if(isCancel(unverifiedId)) {\n return; // User cancelled the prompt\n }\n\n if(!Guid.isV4Guid(unverifiedId.trim())) {\n log.error(\"The provided ID does not appear to be a valid GUID.\");\n return;\n }\n\n iTwinOrIModelId = unverifiedId.trim();\n } else {\n // in case of iTwin we can use the selected iTwin ID\n iTwinOrIModelId = thingToPull.id;\n }\n\n if( thingToPull === \"iModel\") {\n iModelId = iTwinOrIModelId;\n } else {\n // If the user selected iTwin or provided an ITWin ID, so we list the imodels for that\n const client = envManager.iModelsClient;\n const iModelIterator = await client.iModels.getMinimalList({\n authorization: async () => envManager.getAuthorization(),\n urlParams: {\n iTwinId: iTwinOrIModelId!,\n },\n });\n\n const iModelChoices: Option<MinimalIModel>[] = [];\n for await (const iModel of iModelIterator) {\n iModelChoices.push({\n label: `${chalk.bold(iModel.displayName)} (${iModel.id})`,\n value: iModel,\n });\n }\n if (iModelChoices.length === 0) {\n log.error(\"No iModels found for the provided iTwin ID.\");\n return;\n }\n\n const selectedIModel = await select({\n message: \"Select an iModel\",\n options: iModelChoices,\n maxItems: 20,\n });\n\n if(isCancel(selectedIModel)) {\n return; // User cancelled the prompt\n }\n\n const authCallback = () => envManager.getAuthorization();\n iModelId = selectedIModel.id;\n const imodel = await envManager.iModelsClient.iModels.getSingle({\n authorization: authCallback,\n iModelId\n });\n\n const config: IModelConfig = {\n melodiVersion: applicationVersion,\n iModelId: imodel.id,\n iTwinId: imodel.iTwinId,\n environment: environment,\n displayName: imodel.displayName ?? imodel.name ?? imodel.id,\n }\n\n const namedVersionsIterator = envManager.iModelsClient.namedVersions.getMinimalList({\n authorization: authCallback,\n iModelId\n });\n const namedVersionChoices: Option<MinimalNamedVersion>[] = [];\n for await (const nv of namedVersionsIterator) {\n namedVersionChoices.push({\n label: `${chalk.bold(nv.displayName)} (Changeset Index: ${nv.changesetIndex}, Changeset ID: ${nv.changesetId})`,\n value: nv,\n });\n }\n\n let selectedVersion: MinimalNamedVersion | \"__seed__\" | symbol = \"__seed__\";\n if(namedVersionChoices.length === 0) {\n log.info(\"There are no named versions for the selected iModel, so downloading seed.\");\n } else {\n selectedVersion = await select<MinimalNamedVersion | \"__seed__\">({\n message: \"Select which version of the iModel to download\",\n options: [\n { label: \"Seed iModel\", value: \"__seed__\" },\n ...namedVersionChoices,\n ],\n maxItems: 20,\n });\n if(isCancel(selectedVersion)) {\n return; // User cancelled the prompt\n }\n }\n\n const selectedNamedVersion = selectedVersion === \"__seed__\" ? undefined : (selectedVersion as MinimalNamedVersion);\n\n const checkpointId: ChangesetIdWithIndex = \n selectedNamedVersion === undefined || selectedNamedVersion.changesetId === null\n ? { id: \"\" }\n : { id: selectedNamedVersion.changesetId, index: selectedNamedVersion.changesetIndex };\n\n const name = (selectedNamedVersion === undefined ? imodel.displayName : (selectedNamedVersion.displayName ?? imodel.displayName));\n\n \n let selectedName: string | symbol;\n let relativePath: string;\n let absolutePath: string;\n while (true) {\n selectedName = await text({\n message: \"Enter a name for the downloaded iModel file (without extension):\",\n initialValue: name,\n });\n\n if (isCancel(selectedName)) {\n return; // User cancelled the prompt\n }\n\n relativePath = selectedName.trim() + \".bim\";\n absolutePath = path.join(ws.workspaceRootPath, relativePath);\n\n if (existsSync(absolutePath)) {\n log.error(`File \"${absolutePath}\" already exists. Please choose a different name.`);\n continue;\n }\n break;\n }\n const loader = spinner();\n try {\n loader.start(\"Downloading iModel file...\");\n await CheckpointManager.downloadCheckpoint({\n localFile: absolutePath,\n checkpoint: {\n iTwinId: imodel.iTwinId,\n iModelId,\n changeset: checkpointId,\n },\n onProgress: (loaded: number, total: number) => {\n if(loaded !== 0 && total > 0) {\n loader.message(`Downloading iModel file... ${(loaded / total * 100).toFixed(2)}%`);\n }\n return ProgressStatus.Continue;\n }\n });\n await saveIModelConfig(ws, relativePath, config);\n loader.stop(\"Downloaded successful.\");\n }\n catch (error: unknown) {\n loader.stop(\"Failed to download iModel file.\");\n logError(error);\n return;\n }\n\n const wsFile: WorkspaceFile = { relativePath, lastTouched: new Date() }\n\n log.message(\"Checking for available changesets...\");\n const changesets = await envManager.iModelsClient.changesets.getMinimalList({\n authorization: authCallback,\n iModelId\n });\n const changesetsArray = [];\n for await (const cs of changesets) {\n changesetsArray.push(cs);\n }\n const size = Changesets.calculateOverallFileSize(changesetsArray);\n if(changesetsArray.length > 0) {\n const downloadChangesets = await confirm({\n message: `Downloaded iModel has ${changesetsArray.length} changesets. Do you want to download them? (Total size to download: ${(size / (1024 * 1024)).toFixed(2)} MB)`,\n initialValue: true,\n });\n if(isCancel(downloadChangesets)) {\n return; // User cancelled the prompt\n }\n\n if(downloadChangesets) {\n await Changesets.downloadChangesets(ws, wsFile, iModelId);\n }\n\n } else {\n log.info(\"Downloaded iModel has no changesets available.\");\n }\n\n const db = await openBriefcaseDb(ws, wsFile);\n if(isCancel(db)) {\n return; // User cancelled the prompt\n }\n await DbEditor.run(ws, { relativePath, lastTouched: new Date() }, db);\n }\n }\n}", "import { select, isCancel, log } from \"@clack/prompts\"\nimport { BriefcaseDb, ECDb, ECDbOpenMode, ECSqlStatement, IModelDb, SnapshotDb, SQLiteDb, StandaloneDb } from \"@itwin/core-backend\";\nimport { ECSqlReader, QueryBinder, QueryOptions } from \"@itwin/core-common\";\nimport { OpenMode } from \"@itwin/core-bentley\";\nimport { IModelConfig, readIModelConfig } from \"./IModelConfig\";\nimport { Workspace, WorkspaceFile } from \"./Workspace\";\nimport path from \"path\";\n\n/**\n * Common interface for all DB implementations.\n * Add or adjust methods as needed.\n */\nexport type InnerDb = ECDb | StandaloneDb | SnapshotDb | BriefcaseDb | SQLiteDb\n\n/**\n * Generic wrapper that dispatches to a concrete DB implementation.\n * Implements Disposable; will forward dispose() if the concrete instance supports it.\n */\nexport class UnifiedDb implements Disposable {\n private readonly db: InnerDb;\n private readonly iModelConfig?: IModelConfig;\n\n public get innerDb(): InnerDb {\n return this.db;\n }\n\n public get config(): IModelConfig | undefined {\n return this.iModelConfig;\n }\n\n constructor(dbInstance: InnerDb, iModelConfig?: IModelConfig) {\n this.db = dbInstance;\n this.iModelConfig = iModelConfig;\n }\n\n public get isOpen(): boolean {\n if (this.db instanceof IModelDb) {\n return this.db.isOpen;\n }\n if (this.db instanceof ECDb) {\n return this.db.isOpen;\n }\n if (this.db instanceof SQLiteDb) {\n return this.db.isOpen;\n }\n throw new Error(\"Unsupported DB type for isOpen check.\");\n }\n\n public get supportsECSql(): boolean {\n return !(this.db instanceof SQLiteDb);\n }\n\n public get isReadOnly(): boolean {\n if (this.db instanceof IModelDb) {\n return this.db.isReadonly;\n }\n if (this.db instanceof ECDb) {\n return false; // ECDb cannot tell us if it is read-only, so we assume it is always read-only.\n }\n if (this.db instanceof SQLiteDb) {\n return this.db.isReadonly;\n }\n throw new Error(\"Unsupported DB type for isReadOnly check.\");\n }\n\n public createQueryReader(ecsql: string, params?: QueryBinder, config?: QueryOptions): ECSqlReader {\n if (this.db instanceof IModelDb) {\n return this.db.createQueryReader(ecsql, params, config);\n }\n if (this.db instanceof ECDb) {\n return this.db.createQueryReader(ecsql, params, config);\n }\n throw new Error(\"ECSql is not supported by this DB type.\");\n }\n\n public withECSqlStatement<T>(ecsql: string, callback: (stmt: ECSqlStatement) => T, logErrors?: boolean): T {\n if(!this.supportsECSql) {\n throw new Error(\"ECSql statements are not supported by this DB type.\");\n }\n\n if (this.db instanceof IModelDb) {\n return this.db.withStatement(ecsql, callback, logErrors);\n }\n if (this.db instanceof ECDb) {\n return this.db.withStatement(ecsql, callback, logErrors);\n }\n throw new Error(\"ECSql statements are not supported by this DB type.\");\n }\n\n public get supportsSchemas(): boolean {\n return this.db instanceof IModelDb || this.db instanceof ECDb;\n }\n\n public get supportsDumpSchemas(): boolean {\n return this.db instanceof IModelDb;\n }\n\n public async dumpSchemas(dir: string) : Promise<void> {\n if (this.db instanceof IModelDb) {\n await this.db.exportSchemas(dir);\n return;\n }\n throw new Error(\"Dumping schemas is not implemented by this DB type (native addon wants at least DgnDb for this). Try StandaloneDb.\");\n }\n\n public get supportsChangesets(): boolean {\n return this.db instanceof BriefcaseDb;\n }\n\n\n [Symbol.dispose](): void {\n // All IModelDb instances are not disposable.\n if (this.db instanceof IModelDb) { // Handles BriefcaseDb, SnapshotDb, StandaloneDb\n if(this.db.isOpen) {\n this.db.close();\n }\n return;\n }\n\n if (this.db instanceof ECDb) {\n if(this.db.isOpen) {\n this.db.closeDb();\n }\n this.db[Symbol.dispose]();\n return;\n }\n\n if (this.db instanceof SQLiteDb) {\n if(this.db.isOpen) {\n this.db.closeDb();\n }\n return;\n }\n }\n}\n\n/**\n * Factory and opener functions for each DB type.\n */\nexport async function openECDb(path: string): Promise<UnifiedDb | symbol> {\n const mode = await promptECDbOpenMode();\n if (isCancel(mode)) {\n return mode; // User cancelled the prompt\n }\n const db = new ECDb();\n db.openDb(path, mode);\n return new UnifiedDb(db);\n}\n\nexport function createECDb(path: string): UnifiedDb {\n const db = new ECDb();\n db.createDb(path);\n return new UnifiedDb(db);\n}\n\nexport async function openStandaloneDb(path: string): Promise<UnifiedDb | symbol> {\n const mode = await promptOpenMode();\n if (isCancel(mode)) {\n return mode; // User cancelled the prompt\n }\n const db = StandaloneDb.openFile(path, mode);\n return new UnifiedDb(db);\n}\n\nexport function createStandaloneDb(path: string, rootSubject: string): UnifiedDb {\n const db = StandaloneDb.createEmpty(path, { rootSubject: { name: rootSubject } });\n return new UnifiedDb(db);\n}\n\nexport async function openBriefcaseDb(workspace: Workspace, file: WorkspaceFile): Promise<UnifiedDb | symbol> {\n const config = await readIModelConfig(workspace, file.relativePath);\n if (config === undefined) {\n throw new Error(`No iModel config found for file ${file.relativePath}. This file should exist for pulled imodels.`);\n }\n const absolutePath = path.join(workspace.workspaceRootPath, file.relativePath);\n await workspace.envManager.selectEnvironment(config.environment);\n await workspace.envManager.signInIfNecessary();\n const mode = await promptOpenMode();\n const db = await BriefcaseDb.open({ fileName: absolutePath, key: config.iModelId, readonly: mode === OpenMode.Readonly });\n if(db.iModelId !== config.iModelId) {\n log.warn(`The iModel ID in the config (${config.iModelId}) does not match the opened iModel ID (${db.iModelId}). This may indicate a mismatch between the config and the file.`);\n }\n return new UnifiedDb(db);\n}\n\nasync function promptECDbOpenMode(): Promise<ECDbOpenMode | symbol> {\n return await select({\n message: 'Select the open mode for the file',\n options: [\n { label: \"Open in read-only mode\", value: ECDbOpenMode.Readonly },\n { label: \"Open in read-write mode\", value: ECDbOpenMode.ReadWrite },\n { label: \"Open the file in read-write mode and upgrade it to the latest file format version if necessary.\", value: ECDbOpenMode.FileUpgrade },\n ],\n });\n}\n\nasync function promptOpenMode(): Promise<OpenMode | symbol> {\n return await select({\n message: 'Select the open mode for the file',\n options: [\n { label: \"Open in read-only mode\", value: OpenMode.Readonly },\n { label: \"Open in read-write mode\", value: OpenMode.ReadWrite },\n ],\n });\n}\n\n/*\n //Snapshot iModels are a static point-in-time representation of the state of an iModel. Once created, they can not be modified.\n const db = SnapshotDb.openFile(path.join(ws.workspaceRootPath, file.relativePath));\n */", "import * as fs from \"fs\";\nimport path from \"path\";\nimport { z } from \"zod/v4\";\nimport { Environment } from \"./EnvironmentManager\";\nimport { logError } from \"./ConsoleHelper\";\nimport { applicationVersion } from \"./Diagnostics\";\nimport { getFileContextFolderPath, Workspace } from \"./Workspace\";\nimport { log } from \"@clack/prompts\";\n\nconst IModelConfigSchema = z.object({\n melodiVersion: z.string(),\n iModelId: z.string(),\n iTwinId: z.string().optional(),\n environment: z.enum([Environment.PROD, Environment.QA]),\n displayName: z.string(),\n});\n\nexport type IModelConfig = z.infer<typeof IModelConfigSchema>;\n\nconst IModelConfigFileName = \"config.json\";\n\n\nexport async function readIModelConfig(ws: Workspace, iModelRelativePath: string): Promise<IModelConfig | undefined> {\n try {\n const fileContextDir = getFileContextFolderPath(ws.workspaceRootPath, iModelRelativePath);\n const configPath = path.join(fileContextDir, IModelConfigFileName);\n if (!fs.existsSync(configPath)) {\n return undefined; // No config file found for this iModel\n }\n\n const data = await fs.promises.readFile(configPath, \"utf-8\");\n const json = JSON.parse(data);\n return await IModelConfigSchema.parseAsync(json);\n } catch (err: unknown) {\n log.error(\"Failed to read iModel config.\");\n logError(err);\n }\n return undefined;\n}\n\nexport async function saveIModelConfig(ws: Workspace, iModelRelativePath: string, cfg: IModelConfig): Promise<void> {\n const fileContextDir = getFileContextFolderPath(ws.workspaceRootPath, iModelRelativePath);\n const configPath = path.join(fileContextDir, IModelConfigFileName);\n\n if (!fs.existsSync(fileContextDir)) {\n await fs.promises.mkdir(fileContextDir, { recursive: true });\n }\n\n if (!fs.lstatSync(fileContextDir).isDirectory()) {\n throw new Error(`The iModel config directory is not a valid directory: ${fileContextDir}`);\n }\n\n try {\n fs.accessSync(fileContextDir, fs.constants.R_OK | fs.constants.W_OK);\n } catch {\n throw new Error(`The iModel config directory is not accessible: ${fileContextDir}. Please check permissions.`);\n }\n\n cfg.melodiVersion = applicationVersion; // Ensure the version is up-to-date\n const data = JSON.stringify(cfg, undefined, 2);\n await fs.promises.writeFile(configPath, data, \"utf-8\");\n}", "import { QueryBinder, QueryOptionsBuilder, QueryPropertyMetaData, QueryRowFormat } from \"@itwin/core-common\";\nimport chalk from \"chalk\";\nimport { stdin, stdout } from 'node:process';\nimport { createInterface } from \"node:readline/promises\";\nimport { ColumnUserConfig, table, TableUserConfig } from 'table';\nimport { formatWarning, logError, printError, resetChar } from \"../ConsoleHelper\";\nimport { UnifiedDb } from \"../UnifiedDb\";\nimport { saveWorkspaceConfig, Workspace, WorkspaceFile } from \"../Workspace\";\nimport { common, createEmphasize } from 'emphasize'\nimport { performance } from \"node:perf_hooks\";\nimport { log, select, isCancel } from \"@clack/prompts\"\nimport { SchemaEditor } from \"./SchemaEditor\";\nimport { DbSettings } from \"./DbSettings\";\n\nconst emphasize = createEmphasize(common);\n\nexport class DbEditor {\n public static async run(ws: Workspace, file: WorkspaceFile, db: UnifiedDb): Promise<void> {\n if (!db.isOpen) {\n throw new Error(`Db failed to open: ${file.relativePath}`);\n }\n\n while (true) {\n const experimentalEnabled = await DbSettings.getExperimentalFeaturesEnabled(db);\n const operation = await select({\n message: `${file.relativePath}${(db.isReadOnly ? ' (read-only)' : '')}`,\n options: [\n ...(db.supportsECSql ? [{ label: \"ECSql\", value: \"ECSql\" }] : []),\n /*{ label: \"Sqlite\", value: \"Sqlite\" },*/\n /*{ label: \"Check\", value: \"Check\" },*/\n ...(db.supportsSchemas ? [{ label: \"Schemas\", value: \"Schemas\" }] : []),\n ...(db.supportsChangesets ? [{ label: \"Changesets\", value: \"Chang