UNPKG

@rschili/melodi-cli

Version:
1,433 lines (1,410 loc) 76.1 kB
#!/usr/bin/env node // src/Workspace.ts import * as fs from "fs"; import path from "path"; import os from "os"; import { z } from "zod/v4"; import { globby } from "globby"; import { SQLiteDb } from "@itwin/core-backend"; import { DbResult, OpenMode } from "@itwin/core-bentley"; // src/ConsoleHelper.ts import { log } from "@clack/prompts"; import chalk from "chalk"; var formatPath = chalk.blueBright.underline; var formatError = chalk.redBright.bold; var formatWarning = chalk.yellowBright; var formatSuccess = chalk.greenBright.bold; var resetChar = "\x1B[0m"; function printError(error, printAsWarning = false) { const formatter = printAsWarning ? formatWarning : formatError; const label = printAsWarning ? "Warning" : "Error"; if (error instanceof Error) { console.error(formatter(`${label}: ${error.message}`)); } else { console.error(formatter(`${label}: ${String(error)}`)); } } function logError(error) { if (error instanceof Error) { log.error(error.message); } else { log.error(`Error: ${String(error)}`); } } function generateColorizerMap(values) { const colorizerMap = /* @__PURE__ */ new Map(); const colors = [ chalk.redBright, chalk.greenBright, chalk.blueBright, chalk.yellowBright, chalk.cyanBright, chalk.magentaBright, chalk.whiteBright ]; const uniqueValues = Array.from(new Set(values)); uniqueValues.forEach((value, index) => { const color = colors[index % colors.length]; colorizerMap.set(value, color); }); return colorizerMap; } var msInSecond = 1e3; var msInMinute = msInSecond * 60; var msInHour = msInMinute * 60; var msInDay = msInHour * 24; var msInYear = msInDay * 365.25; function timeSpanToString(span) { if (span > msInYear * 100 || span <= 0) { return void 0; } if (span < msInMinute) { const seconds = Math.floor(span / msInSecond); return `${seconds} second${seconds !== 1 ? "s" : ""}`; } else if (span < msInHour) { const minutes = Math.floor(span / msInMinute); return `${minutes} minute${minutes !== 1 ? "s" : ""}`; } else if (span < msInDay) { const hours = Math.floor(span / msInHour); return `${hours} hour${hours !== 1 ? "s" : ""}`; } else if (span < msInYear) { const days = Math.floor(span / msInDay); return `${days} day${days !== 1 ? "s" : ""}`; } else { const years = Math.floor(span / msInYear); return `${years} year${years !== 1 ? "s" : ""}`; } } // src/Workspace.ts import { SemVer } from "semver"; // src/buildInfo.ts var __BUILD_DATE__ = "2025-07-09T08:03:37Z"; // package.json var package_default = { name: "@rschili/melodi-cli", version: "1.3.1", description: "iModel utility", main: "dist/index.mjs", type: "module", engines: { node: ">=22.14.0" }, scripts: { typecheck: "tsc --noEmit", build: "node esbuild.config.mjs", test: "vitest", start: "node dist/index.mjs", lint: "eslint 'src/**/*.ts' --fix", prebuild: `echo "export const __BUILD_DATE__ = '$(date -u +%Y-%m-%dT%H:%M:%SZ)';" > src/buildInfo.ts && npm run lint && npm run typecheck` }, bin: { melodi: "dist/index.mjs" }, publishConfig: { access: "public" }, files: [ "dist", "LICENSE", "README.md", "CHANGELOG.md" ], repository: { type: "git", url: "git+https://github.com/rschili/melodi-cli.git" }, keywords: [ "itwin", "imodel", "bentley", "ecdb", "bim" ], author: "Robert Schili", license: "MIT", bugs: { url: "https://github.com/rschili/melodi-cli/issues" }, homepage: "https://github.com/rschili/melodi-cli#readme", devDependencies: { "@eslint/js": "^9.30.1", "@types/node": "^24.0.10", "@typescript-eslint/eslint-plugin": "^8.36.0", "@typescript-eslint/parser": "^8.36.0", esbuild: "^0.25.6", "esbuild-node-externals": "^1.18.0", eslint: "^9.30.1", globals: "^16.3.0", typescript: "^5.8.3", "typescript-eslint": "^8.36.0", vitest: "^3.2.4" }, dependencies: { "@clack/prompts": "^0.11.0", "@itwin/core-backend": "^5.0.2", "@itwin/core-bentley": "^5.0.2", "@itwin/core-common": "^5.0.2", "@itwin/ecschema-metadata": "^5.0.2", "@itwin/imodels-access-backend": "^6.0.1", "@itwin/imodels-access-common": "^6.0.1", "@itwin/imodels-client-authoring": "^6.0.1", "@itwin/itwins-client": "^1.6.1", "@itwin/node-cli-authorization": "^3.0.1", "@itwin/object-storage-azure": "^3.0.1", "@itwin/object-storage-core": "^3.0.1", "@itwin/object-storage-google": "^3.0.1", "@types/semver": "^7.7.0", axios: "^1.10.0", chalk: "^5.4.1", emphasize: "^7.0.0", globby: "^14.1.0", "gradient-string": "^3.0.0", "module-alias": "^2.2.3", semver: "^7.7.2", "simple-update-notifier": "^2.0.0", table: "^6.9.0", zod: "^3.25.76" }, overrides: { inversify: "7.5.2", "reflect-metadata": "^0.2.2" } }; // src/Diagnostics.ts import updateNotifier from "simple-update-notifier"; var applicationVersion = package_default.version; var applicationBuildDate = new Date(__BUILD_DATE__).toLocaleString(); async function checkUpdates() { await updateNotifier({ pkg: package_default }); } // src/EnvironmentManager.ts import { select } from "@clack/prompts"; import { IModelHost } from "@itwin/core-backend"; import { IModelsClient } from "@itwin/imodels-client-authoring"; import { ITwinsAccessClient } from "@itwin/itwins-client"; import { NodeCliAuthorizationClient } from "@itwin/node-cli-authorization"; import { AzureClientStorage, BlockBlobClientWrapperFactory } from "@itwin/object-storage-azure"; import { StrategyClientStorage } from "@itwin/object-storage-core"; import { GoogleClientStorage } from "@itwin/object-storage-google/lib/client/index.js"; import { ClientStorageWrapperFactory } from "@itwin/object-storage-google/lib/client/wrappers/index.js"; import { AccessTokenAdapter } from "@itwin/imodels-access-common"; import { BackendIModelsAccess } from "@itwin/imodels-access-backend"; var EnvironmentManager = class { _cacheDir; _currentEnvironment = "PROD" /* PROD */; _authClient; _iModelsClient; _iTwinsClient; _isSignedIn = false; _isStartedUp = false; constructor(cacheDir) { this._cacheDir = cacheDir; } get currentEnvironment() { return this._currentEnvironment; } get cacheDirectory() { return this._cacheDir; } async selectEnvironment(newEnvironment) { if (newEnvironment !== this._currentEnvironment) { await this.shutdown(); this._currentEnvironment = newEnvironment; await this.startup(); } } async startup() { if (this._isStartedUp) { return; } const hubAccess = new BackendIModelsAccess(this.iModelsClient); await IModelHost.startup({ cacheDir: this._cacheDir, hubAccess, authorizationClient: this.authClient }); this._isStartedUp = true; } async shutdown() { if (this._isStartedUp) { this._authClient = void 0; this._iModelsClient = void 0; this._iTwinsClient = void 0; this._isSignedIn = false; this._isStartedUp = false; await IModelHost.shutdown(); } } get authority() { if (this._currentEnvironment === "PROD" /* PROD */) { return "https://ims.bentley.com/"; } if (this._currentEnvironment === "QA" /* QA */) { return "https://qa-ims.bentley.com/"; } throw new Error(`Unknown environment: ${this._currentEnvironment}`); } get clientId() { switch (this._currentEnvironment) { case "PROD" /* PROD */: return "native-b517RwSFtag94aBZ5lM40QCf6"; case "QA" /* QA */: return "native-jq2fZ8ZMoMjTKVDghCOpjY4JQ"; default: throw new Error(`Unknown environment: ${this._currentEnvironment}`); } } async getAccessToken() { if (this._authClient === void 0) { throw new Error("Authorization client is not initialized. Call signInIfNecessary() first."); } const parts = (await this._authClient.getAccessToken()).split(" "); return { scheme: parts[0], token: parts[1] }; } async getAuthorization() { if (this._authClient === void 0) { throw new Error("Authorization client is not initialized. Call signInIfNecessary() first."); } return AccessTokenAdapter.toAuthorization(await this._authClient.getAccessToken()); } get authClient() { if (!this._authClient) { this._authClient = new NodeCliAuthorizationClient({ issuerUrl: this.authority, clientId: this.clientId, redirectUri: "http://localhost:3000/signin-callback", scope: "itwin-platform" }); } return this._authClient; } async signInIfNecessary() { if (!this._isSignedIn) { await this.authClient.signIn(); } } get iModelsClient() { if (!this._iModelsClient) { const iModelsClientOptions = { cloudStorage: new StrategyClientStorage([ { instanceName: "azure", instance: new AzureClientStorage(new BlockBlobClientWrapperFactory()) }, { instanceName: "google", instance: new GoogleClientStorage(new ClientStorageWrapperFactory()) } ]), api: this._currentEnvironment === "QA" /* QA */ ? { baseUrl: "https://qa-api.bentley.com/imodels" } : void 0 }; this._iModelsClient = new IModelsClient(iModelsClientOptions); } return this._iModelsClient; } get iTwinsClient() { if (!this._iTwinsClient) { this._iTwinsClient = new ITwinsAccessClient(this._currentEnvironment === "QA" /* QA */ ? "https://qa-api.bentley.com/itwins" : void 0); } return this._iTwinsClient; } async promptEnvironment() { return await select({ message: "Select an environment", options: [ { label: "PROD", value: "PROD" /* PROD */ }, { label: "QA", value: "QA" /* QA */ } ], initialValue: this._currentEnvironment }); } }; // src/Workspace.ts var WorkspaceConfigSchema = z.object({ melodiVersion: z.string(), ecsqlHistory: z.array(z.string()).optional() }); var MelodiConfigFolderName = ".melodi"; var CacheFolderName = ".itwinjs-cache"; var ConfigFileName = "config.json"; async function loadWorkspace(userConfig, root = process.cwd()) { const workspaceRootPath = root; const userConfigDirPath = path.join(os.homedir(), MelodiConfigFolderName); const melodiConfigPath = path.join(workspaceRootPath, MelodiConfigFolderName); const cacheDirPath = path.join(workspaceRootPath, CacheFolderName); if (!fs.existsSync(workspaceRootPath) || !fs.lstatSync(workspaceRootPath).isDirectory()) { throw new Error(`The current working directory is not a valid directory: ${workspaceRootPath}`); } try { fs.accessSync(workspaceRootPath, fs.constants.R_OK | fs.constants.W_OK); } catch { throw new Error(`The current working directory is not accessible: ${workspaceRootPath}. Please check permissions.`); } const configPath = path.join(melodiConfigPath, ConfigFileName); const environment = new EnvironmentManager(cacheDirPath); if (!fs.existsSync(configPath)) { return { workspaceRootPath, workspaceConfigDirPath: melodiConfigPath, envManager: environment, userConfig }; } const config = await readWorkspaceConfig(configPath); if (!fs.existsSync(userConfigDirPath)) { await fs.promises.mkdir(userConfigDirPath, { recursive: true }); } return { workspaceRootPath, workspaceConfigDirPath: melodiConfigPath, config, envManager: environment, userConfig }; } async function readWorkspaceConfig(configPath) { const data = await fs.promises.readFile(configPath, "utf-8"); const json = JSON.parse(data); return await WorkspaceConfigSchema.parseAsync(json); } async function saveWorkspaceConfig(ws) { if (ws.config === void 0) { throw new Error("Workspace config is undefined. Please provide a valid config."); } if (!fs.existsSync(ws.workspaceConfigDirPath)) { await fs.promises.mkdir(ws.workspaceConfigDirPath, { recursive: true }); } if (!fs.lstatSync(ws.workspaceConfigDirPath).isDirectory()) { throw new Error(`The workspace config directory is not a valid directory: ${ws.workspaceConfigDirPath}`); } try { fs.accessSync(ws.workspaceConfigDirPath, fs.constants.R_OK | fs.constants.W_OK); } catch { throw new Error(`The workspace config directory is not accessible: ${ws.workspaceConfigDirPath}. Please check permissions.`); } const configPath = path.join(ws.workspaceConfigDirPath, ConfigFileName); ws.config.melodiVersion = applicationVersion; const data = JSON.stringify(ws.config, void 0, 2); await fs.promises.writeFile(configPath, data, "utf-8"); } async function detectWorkspaceFiles(ws) { const patterns = [ "**/*.bim", "**/*.ecdb" ]; const ignore = [ "**/.*", // Ignore dotfiles and dotfolders "**/.*/**" // Also ignore anything inside dotfolders ]; const files = await globby(patterns, { cwd: ws.workspaceRootPath, absolute: false, deep: 2, // Limit to 2 levels deep dot: false, // Don't match dotfiles/folders ignore, caseSensitiveMatch: false }); const workspaceFiles = files.map((file) => { const absolutePath = path.join(ws.workspaceRootPath, file); const stats = fs.statSync(absolutePath); const lastTouched = new Date(Math.max(stats.mtime.getTime(), stats.birthtime.getTime(), stats.ctime.getTime())); return { relativePath: file, lastTouched }; }); await readFileProps(ws, workspaceFiles); ws.files = workspaceFiles; } function getFileContextFolderPath(root, relativeFilePath) { const parsed = path.parse(relativeFilePath); const contextFolderName = `${parsed.name}_extras`; return path.join(root, parsed.dir, contextFolderName); } var schemaVersionSchema = z.object({ major: z.number(), minor: z.number(), sub1: z.number(), sub2: z.number() }); async function readFileProps(ws, files) { if (files.length === 0) { return; } const db = new SQLiteDb(); for (const file of files) { try { const absolutePath = path.join(ws.workspaceRootPath, file.relativePath); db.openDb(absolutePath, OpenMode.Readonly); db.withPreparedSqliteStatement("SELECT Name, Val FROM be_Local", (stmt) => { while (stmt.step() === DbResult.BE_SQLITE_ROW) { const name = stmt.getValueString(0); if (name === "ParentChangeSetId") { file.parentChangeSetId = stmt.getValueString(1); } } }); db.withPreparedSqliteStatement("SELECT Namespace, StrData FROM be_Prop WHERE Name = ?", (stmt) => { stmt.bindString(1, "SchemaVersion"); while (stmt.step() === DbResult.BE_SQLITE_ROW) { const namespace = stmt.getValueString(0); const schemaVersion = stmt.getValueString(1); const parsedSchemaVersion = schemaVersionSchema.safeParse(JSON.parse(schemaVersion)); if (parsedSchemaVersion.success) { switch (namespace.toLowerCase()) { case "be_db": file.beDbVersion = parsedSchemaVersion.data; break; case "ec_db": file.ecDbVersion = parsedSchemaVersion.data; break; case "dgn_db": file.dgn_DbVersion = parsedSchemaVersion.data; break; default: console.warn(`Unknown schema version namespace: ${namespace}. This may not be supported by melodi.`); break; } } } }); db.withPreparedSqliteStatement("SELECT VersionDigit1, VersionDigit2, VersionDigit3 from ec_Schema WHERE Name = ?", (stmt) => { stmt.bindString(1, "BisCore"); if (stmt.step() === DbResult.BE_SQLITE_ROW) { const major2 = stmt.getValueInteger(0); const minor2 = stmt.getValueInteger(1); const sub1 = stmt.getValueInteger(2); file.bisCoreVersion = new SemVer(`${major2}.${minor2}.${sub1}`); } }); if (file.bisCoreVersion !== void 0) { db.withPreparedSqliteStatement("SELECT COUNT(*) FROM bis_Element", (stmt) => { if (stmt.step() === DbResult.BE_SQLITE_ROW) { file.elements = stmt.getValueInteger(0); } }); } db.closeDb(); } catch (error) { printError(error, true); } finally { if (db.isOpen) { db.closeDb(); } } } } // src/Logic/NewFile.ts import { log as log7, select as select6, text, isCancel as isCancel5, tasks, spinner, confirm as confirm2 } from "@clack/prompts"; import { ITwinSubClass } from "@itwin/itwins-client"; import chalk5 from "chalk"; import { Guid } from "@itwin/core-bentley"; import { existsSync as existsSync5 } from "node:fs"; // src/UnifiedDb.ts import { select as select2, isCancel, log as log3 } from "@clack/prompts"; import { BriefcaseDb, ECDb, ECDbOpenMode, IModelDb, SQLiteDb as SQLiteDb2, StandaloneDb } from "@itwin/core-backend"; import { OpenMode as OpenMode2 } from "@itwin/core-bentley"; // src/IModelConfig.ts import * as fs2 from "fs"; import path2 from "path"; import { z as z2 } from "zod/v4"; import { log as log2 } from "@clack/prompts"; var IModelConfigSchema = z2.object({ melodiVersion: z2.string(), iModelId: z2.string(), iTwinId: z2.string().optional(), environment: z2.enum(["PROD" /* PROD */, "QA" /* QA */]), displayName: z2.string() }); var IModelConfigFileName = "config.json"; async function readIModelConfig(ws, iModelRelativePath) { try { const fileContextDir = getFileContextFolderPath(ws.workspaceRootPath, iModelRelativePath); const configPath = path2.join(fileContextDir, IModelConfigFileName); if (!fs2.existsSync(configPath)) { return void 0; } const data = await fs2.promises.readFile(configPath, "utf-8"); const json = JSON.parse(data); return await IModelConfigSchema.parseAsync(json); } catch (err) { log2.error("Failed to read iModel config."); logError(err); } return void 0; } async function saveIModelConfig(ws, iModelRelativePath, cfg) { const fileContextDir = getFileContextFolderPath(ws.workspaceRootPath, iModelRelativePath); const configPath = path2.join(fileContextDir, IModelConfigFileName); if (!fs2.existsSync(fileContextDir)) { await fs2.promises.mkdir(fileContextDir, { recursive: true }); } if (!fs2.lstatSync(fileContextDir).isDirectory()) { throw new Error(`The iModel config directory is not a valid directory: ${fileContextDir}`); } try { fs2.accessSync(fileContextDir, fs2.constants.R_OK | fs2.constants.W_OK); } catch { throw new Error(`The iModel config directory is not accessible: ${fileContextDir}. Please check permissions.`); } cfg.melodiVersion = applicationVersion; const data = JSON.stringify(cfg, void 0, 2); await fs2.promises.writeFile(configPath, data, "utf-8"); } // src/UnifiedDb.ts import path3 from "path"; var UnifiedDb = class { db; iModelConfig; get innerDb() { return this.db; } get config() { return this.iModelConfig; } constructor(dbInstance, iModelConfig) { this.db = dbInstance; this.iModelConfig = iModelConfig; } get isOpen() { if (this.db instanceof IModelDb) { return this.db.isOpen; } if (this.db instanceof ECDb) { return this.db.isOpen; } if (this.db instanceof SQLiteDb2) { return this.db.isOpen; } throw new Error("Unsupported DB type for isOpen check."); } get supportsECSql() { return !(this.db instanceof SQLiteDb2); } get isReadOnly() { if (this.db instanceof IModelDb) { return this.db.isReadonly; } if (this.db instanceof ECDb) { return false; } if (this.db instanceof SQLiteDb2) { return this.db.isReadonly; } throw new Error("Unsupported DB type for isReadOnly check."); } createQueryReader(ecsql, params, config) { if (this.db instanceof IModelDb) { return this.db.createQueryReader(ecsql, params, config); } if (this.db instanceof ECDb) { return this.db.createQueryReader(ecsql, params, config); } throw new Error("ECSql is not supported by this DB type."); } withECSqlStatement(ecsql, callback, logErrors) { if (!this.supportsECSql) { throw new Error("ECSql statements are not supported by this DB type."); } if (this.db instanceof IModelDb) { return this.db.withStatement(ecsql, callback, logErrors); } if (this.db instanceof ECDb) { return this.db.withStatement(ecsql, callback, logErrors); } throw new Error("ECSql statements are not supported by this DB type."); } get supportsSchemas() { return this.db instanceof IModelDb || this.db instanceof ECDb; } get supportsDumpSchemas() { return this.db instanceof IModelDb; } async dumpSchemas(dir) { if (this.db instanceof IModelDb) { await this.db.exportSchemas(dir); return; } throw new Error("Dumping schemas is not implemented by this DB type (native addon wants at least DgnDb for this). Try StandaloneDb."); } get supportsChangesets() { return this.db instanceof BriefcaseDb; } [Symbol.dispose]() { if (this.db instanceof IModelDb) { if (this.db.isOpen) { this.db.close(); } return; } if (this.db instanceof ECDb) { if (this.db.isOpen) { this.db.closeDb(); } this.db[Symbol.dispose](); return; } if (this.db instanceof SQLiteDb2) { if (this.db.isOpen) { this.db.closeDb(); } return; } } }; async function openECDb(path11) { const mode = await promptECDbOpenMode(); if (isCancel(mode)) { return mode; } const db = new ECDb(); db.openDb(path11, mode); return new UnifiedDb(db); } function createECDb(path11) { const db = new ECDb(); db.createDb(path11); return new UnifiedDb(db); } async function openStandaloneDb(path11) { const mode = await promptOpenMode(); if (isCancel(mode)) { return mode; } const db = StandaloneDb.openFile(path11, mode); return new UnifiedDb(db); } function createStandaloneDb(path11, rootSubject) { const db = StandaloneDb.createEmpty(path11, { rootSubject: { name: rootSubject } }); return new UnifiedDb(db); } async function openBriefcaseDb(workspace, file) { const config = await readIModelConfig(workspace, file.relativePath); if (config === void 0) { throw new Error(`No iModel config found for file ${file.relativePath}. This file should exist for pulled imodels.`); } const absolutePath = path3.join(workspace.workspaceRootPath, file.relativePath); await workspace.envManager.selectEnvironment(config.environment); await workspace.envManager.signInIfNecessary(); const mode = await promptOpenMode(); const db = await BriefcaseDb.open({ fileName: absolutePath, key: config.iModelId, readonly: mode === OpenMode2.Readonly }); if (db.iModelId !== config.iModelId) { log3.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.`); } return new UnifiedDb(db); } async function promptECDbOpenMode() { return await select2({ message: "Select the open mode for the file", options: [ { label: "Open in read-only mode", value: ECDbOpenMode.Readonly }, { label: "Open in read-write mode", value: ECDbOpenMode.ReadWrite }, { label: "Open the file in read-write mode and upgrade it to the latest file format version if necessary.", value: ECDbOpenMode.FileUpgrade } ] }); } async function promptOpenMode() { return await select2({ message: "Select the open mode for the file", options: [ { label: "Open in read-only mode", value: OpenMode2.Readonly }, { label: "Open in read-write mode", value: OpenMode2.ReadWrite } ] }); } // src/Logic/DbEditor.ts import { QueryBinder, QueryOptionsBuilder as QueryOptionsBuilder2, QueryRowFormat as QueryRowFormat2 } from "@itwin/core-common"; import chalk4 from "chalk"; import { stdin, stdout } from "node:process"; import { createInterface } from "node:readline/promises"; import { table as table2 } from "table"; import { common, createEmphasize } from "emphasize"; import { performance } from "node:perf_hooks"; import { log as log6, select as select5, isCancel as isCancel4 } from "@clack/prompts"; // src/Logic/SchemaEditor.ts import { QueryOptionsBuilder, QueryRowFormat } from "@itwin/core-common"; import chalk2 from "chalk"; import semver from "semver"; import { table } from "table"; // src/GithubBisSchemasHelper.ts import z3 from "zod/v4"; import fs3 from "fs/promises"; import axios from "axios"; import path4 from "path"; var GithubBisSchemasRootUrl = "https://raw.githubusercontent.com/iTwin/bis-schemas/refs/heads/master/"; var SchemaInventoryPath = "SchemaInventory.json"; var ETagCacheFileName = "etag.json"; var SchemaInventorySchema = z3.record(z3.string(), z3.array( z3.object({ name: z3.string(), path: z3.string().optional(), released: z3.boolean(), version: z3.string() }) )); var ETagCacheSchema = z3.record(z3.string(), z3.string()); async function loadSchemaInventory(cacheDirectory) { const etagCacheFilePath = path4.join(cacheDirectory, ETagCacheFileName); let etagCache = {}; await fs3.mkdir(cacheDirectory, { recursive: true }); if (await fileExists(etagCacheFilePath)) { const fileContents = await fs3.readFile(etagCacheFilePath, "utf-8"); etagCache = ETagCacheSchema.parse(JSON.parse(fileContents)); } const schemaInventory = await fetchUrl(GithubBisSchemasRootUrl, SchemaInventoryPath, cacheDirectory, etagCache); await fs3.writeFile(etagCacheFilePath, JSON.stringify(etagCache, null, 2), "utf-8"); const parsedInventory = SchemaInventorySchema.parse(JSON.parse(schemaInventory)); return parsedInventory; } async function fetchUrl(rootUrl, subUrl, cacheDirectory, etagCache) { const url = new URL(subUrl, rootUrl); const cacheFilePath = path4.join(cacheDirectory, subUrl.replace(/\//g, "_")); const etag = etagCache[url.href]; const options = { responseType: "text", validateStatus: (status) => status === 200 || status === 304 }; if (etag && await fileExists(cacheFilePath)) { options.headers = { "If-None-Match": etag }; } const response = await axios.get(url.href, options); if (response.status === 304) { const cachedContent = await fs3.readFile(cacheFilePath, "utf-8"); return cachedContent; } etagCache[url.href] = response.headers.etag; await fs3.writeFile(cacheFilePath, response.data, "utf-8"); return response.data; } async function fileExists(filePath) { try { const stats = await fs3.stat(filePath); return stats.isFile(); } catch { return false; } } // src/Logic/SchemaEditor.ts import { log as log4, select as select3, isCancel as isCancel2 } from "@clack/prompts"; // src/Workspace.UserConfig.ts import * as fs4 from "fs"; import path5 from "path"; import os2 from "os"; import { z as z4 } from "zod/v4"; var LogLevel = /* @__PURE__ */ ((LogLevel4) => { LogLevel4[LogLevel4["Trace"] = 0] = "Trace"; LogLevel4[LogLevel4["Info"] = 1] = "Info"; LogLevel4[LogLevel4["Warning"] = 2] = "Warning"; LogLevel4[LogLevel4["Error"] = 3] = "Error"; LogLevel4[LogLevel4["None"] = 4] = "None"; return LogLevel4; })(LogLevel || {}); var UserConfigSchema = z4.object({ melodiVersion: z4.string(), logging: z4.enum(LogLevel).optional() }); var AppDirectoryName = "melodi"; var WinConfigRelativePath = path5.join(AppDirectoryName, "config"); var WinCacheRelativePath = path5.join(AppDirectoryName, "cache"); var ConfigRelativePath = path5.join(".config", AppDirectoryName); var CacheRelativePath = path5.join(".cache", AppDirectoryName); var UserConfigFileName = "config.json"; async function readUserConfig() { try { const userConfigDir = getUserConfigDir(); const userConfigPath = path5.join(userConfigDir, UserConfigFileName); if (fs4.existsSync(getUserConfigDir())) { const data = await fs4.promises.readFile(userConfigPath, "utf-8"); const json = JSON.parse(data); return await UserConfigSchema.parseAsync(json); } } catch (err) { console.error(formatError("Failed to read user config. Using default config.")); printError(err); } return { melodiVersion: applicationVersion, logging: 4 /* None */ }; } function getUserConfigDir() { if (process.platform === "win32" && process.env.LOCALAPPDATA) { return path5.join(process.env.LOCALAPPDATA, WinConfigRelativePath); } if (process.env.XDG_CONFIG_HOME) { return path5.join(process.env.XDG_CONFIG_HOME, AppDirectoryName); } return path5.join(os2.homedir(), ConfigRelativePath); } function getUserCacheDir() { if (process.platform === "win32" && process.env.LOCALAPPDATA) { return path5.join(process.env.LOCALAPPDATA, WinCacheRelativePath); } if (process.env.XDG_CACHE_HOME) { return path5.join(process.env.XDG_CACHE_HOME, AppDirectoryName); } return path5.join(os2.homedir(), CacheRelativePath); } async function saveUserConfig(cfg) { const userConfigDir = getUserConfigDir(); const userConfigPath = path5.join(userConfigDir, UserConfigFileName); if (!fs4.existsSync(userConfigDir)) { await fs4.promises.mkdir(userConfigDir, { recursive: true }); } if (!fs4.lstatSync(userConfigDir).isDirectory()) { throw new Error(`The user config directory is not a valid directory: ${userConfigDir}`); } try { fs4.accessSync(userConfigDir, fs4.constants.R_OK | fs4.constants.W_OK); } catch { throw new Error(`The user config directory is not accessible: ${userConfigDir}. Please check permissions.`); } cfg.melodiVersion = applicationVersion; const data = JSON.stringify(cfg, void 0, 2); await fs4.promises.writeFile(userConfigPath, data, "utf-8"); } // src/Logic/SchemaEditor.ts import path6 from "node:path"; import { mkdirSync } from "node:fs"; var SchemaEditor = class { static async run(ws, file, db) { const queryOptions = new QueryOptionsBuilder(); queryOptions.setRowFormat(QueryRowFormat.UseECSqlPropertyIndexes); const reader = db.createQueryReader( "SELECT Name, VersionMajor ,VersionWrite, VersionMinor FROM meta.ECSchemaDef", void 0, queryOptions.getOptions() ); const schemasInDb = await reader.toArray(); const availableSchemas = await loadSchemaInventory(getUserCacheDir()); const schemaInfoMap = {}; for (const row of schemasInDb) { const name = row[0]; const versionString = `${row[1]}.${row[2]}.${row[3]}`; const version = semver.parse(versionString); if (!version) { console.log(formatWarning(`Schema ${row[0]} has an invalid version: ${row[1]}`)); continue; } schemaInfoMap[name] = { name, version }; } for (const [outerName, schemaGroup] of Object.entries(availableSchemas)) { for (const schema of schemaGroup) { if (!schema.released || !schema.path) continue; if (schema.name !== outerName) { console.log(formatWarning(`Schema name mismatch: expected ${outerName}, got ${schema.name}`)); continue; } const cleanedVersion = stripLeadingZeros(schema.version); const version = semver.parse(cleanedVersion); if (!version) { console.log(formatWarning(`Schema ${schema.name} has an invalid version: ${schema.version}`)); continue; } const existingSchema = schemaInfoMap[schema.name]; if (existingSchema) { if (!existingSchema.latestVersion || semver.lt(existingSchema.latestVersion, version)) { existingSchema.latestVersion = version; existingSchema.path = schema.path; } } else { schemaInfoMap[schema.name] = { name: schema.name, latestVersion: version, path: schema.path }; } } } const schemaTable = [ ["Name", "Current Version", "Latest published Version"] ]; for (const schema of Object.values(schemaInfoMap)) { if (!schema.version) { continue; } let latestVersion = ""; if (schema.latestVersion) { if (semver.eq(schema.version, schema.latestVersion)) { latestVersion = chalk2.green(schema.latestVersion.toString() + "\n(github)"); } else if (semver.lt(schema.version, schema.latestVersion)) { latestVersion = chalk2.yellowBright(schema.latestVersion.toString() + "\n(github)"); } else { latestVersion = chalk2.magenta(schema.latestVersion.toString() + "\n(github)"); } } schemaTable.push([ schema.name, schema.version.toString(), latestVersion ]); } console.log(table(schemaTable)); const schemaOption = await select3({ message: "Select a schema to update", options: [ { label: "Import a schema", value: "__import__" }, { label: "Import multiple schemas", value: "__import_multiple__" }, { label: "Dump all schemas as XML", value: "__dump__" }, { label: "(Back)", value: "__back__" } ] }); if (isCancel2(schemaOption) || schemaOption === "__back__") { return; } if (schemaOption === "__dump__") { const currentTime = Math.floor((Date.now() - (/* @__PURE__ */ new Date("2020-01-01")).getTime()) / 1e3).toString(36); const dumpPath = path6.join(getFileContextFolderPath(ws.workspaceRootPath, file.relativePath), `schemas_dump_${currentTime}`); log4.info(`Dumping all schemas to: ${dumpPath}`); mkdirSync(dumpPath, { recursive: true }); await db.dumpSchemas(dumpPath); return; } } }; function stripLeadingZeros(str) { return str.replace(/(^|\.)0+(?=\d)/g, "$1"); } // src/Logic/DbSettings.ts import { select as select4, isCancel as isCancel3, confirm, log as log5 } from "@clack/prompts"; import { DbResult as DbResult2 } from "@itwin/core-bentley"; import chalk3 from "chalk"; var DbSettings = class { static async getExperimentalFeaturesEnabled(db) { if (!db.supportsECSql) { return false; } let result = false; db.withECSqlStatement("PRAGMA experimental_features_enabled", (stmt) => { if (stmt.step() !== DbResult2.BE_SQLITE_ROW) { log5.error("Failed to read experimental features enabled flag"); return; } result = stmt.getValue(0).getBoolean(); }); return result; } static async setExperimentalFeaturesEnabled(db, enabled) { if (!db.supportsECSql) { throw new Error("Experimental features can only be set for IModelDb or ECDb."); } db.withECSqlStatement(`PRAGMA experimental_features_enabled=${enabled ? "true" : "false"}`, (stmt) => { const result = stmt.step(); if (result != DbResult2.BE_SQLITE_ROW) { log5.error(`Failed to set experimental features enabled flag`); } }); } static async run(db) { while (true) { const experimentalEnabled = await this.getExperimentalFeaturesEnabled(db); const choice = await select4( { message: "Select a setting to change:", options: [ { label: `Experimental features enabled: ${experimentalEnabled ? chalk3.greenBright("true") : chalk3.redBright("false")})`, value: "experimental" }, { label: "Back", value: "back" } ] } ); if (isCancel3(choice) || choice === "back") { return; } if (choice === "experimental") { const newExperimentalEnabled = await confirm({ message: "Enable experimental features?", initialValue: experimentalEnabled }); if (isCancel3(newExperimentalEnabled)) { continue; } await this.setExperimentalFeaturesEnabled(db, newExperimentalEnabled); } } } }; // src/Logic/DbEditor.ts var emphasize = createEmphasize(common); var DbEditor = class { static async run(ws, file, db) { if (!db.isOpen) { throw new Error(`Db failed to open: ${file.relativePath}`); } while (true) { const experimentalEnabled = await DbSettings.getExperimentalFeaturesEnabled(db); const operation = await select5({ message: `${file.relativePath}${db.isReadOnly ? " (read-only)" : ""}`, options: [ ...db.supportsECSql ? [{ label: "ECSql", value: "ECSql" }] : [], /*{ label: "Sqlite", value: "Sqlite" },*/ /*{ label: "Check", value: "Check" },*/ ...db.supportsSchemas ? [{ label: "Schemas", value: "Schemas" }] : [], ...db.supportsChangesets ? [{ label: "Changesets", value: "Changesets" }] : [], { label: `Settings (Experimental features enabled: ${experimentalEnabled ? chalk4.greenBright("true") : chalk4.redBright("false")})`, value: "Settings" }, { label: "Close", value: "Close" } ] }); if (operation === "Close" || isCancel4(operation)) { return; } try { switch (operation) { case "ECSql": console.log(); log6.message("ECSql editor. (press up/down for history, Ctrl+C to exit, use semicolon to end statement)"); console.log(); while (await this.runECSql(ws, db)) { } break; case "Sqlite": console.log("Sqlite operation selected."); break; case "Stats": console.log("Stats operation selected."); break; case "Schemas": await SchemaEditor.run(ws, file, db); break; case "Changesets": console.log("Changesets operation selected."); break; case "Settings": await DbSettings.run(db); break; } } catch (error) { logError(error); } } } static async getClassName(db, classIdHex, cache) { const params = new QueryBinder(); params.bindId(1, classIdHex); const reader = db.createQueryReader( `SELECT Name FROM meta.ECClassDef WHERE ECInstanceId = ? LIMIT 1`, params, { rowFormat: QueryRowFormat2.UseECSqlPropertyIndexes } ); const rows = await reader.toArray(); if (rows.length === 0) { cache[classIdHex] = "UnknownClass"; } else { const className = rows[0][0]; cache[classIdHex] = className; } return cache[classIdHex]; } static async runECSql(ws, db) { const queryOptions = new QueryOptionsBuilder2(); queryOptions.setRowFormat(QueryRowFormat2.UseECSqlPropertyIndexes); queryOptions.setLimit({ count: 101 }); queryOptions.setAbbreviateBlobs(true); const history = ws.config?.ecsqlHistory ?? []; const rl = createInterface({ input: stdin, output: stdout, terminal: true, prompt: "ECSql> ", history }); let interrupted = false; rl.on("SIGINT", () => { interrupted = true; rl.close(); console.log("\n"); }); let ecsql = ""; rl.prompt(); for await (const line of rl) { if (ecsql === "") ecsql = line; else ecsql += "\n" + line; if (line.trim().endsWith(";")) { rl.close(); break; } rl.prompt(); } if (interrupted) { return false; } const newLength = history.length; if (newLength > 10) { ws.config.ecsqlHistory = history.slice(10); } await saveWorkspaceConfig(ws); let rows = []; let metadata = []; const classIdCache = {}; const startTicks = performance.now(); let queryDuration = 0; try { const reader = db.createQueryReader(ecsql, void 0, queryOptions.getOptions()); rows = await reader.toArray(); if (rows === void 0 || rows.length === 0) { console.log("No rows returned."); return true; } metadata = await reader.getMetaData(); if (metadata.length === 0) { console.log("No metadata returned."); return true; } queryDuration = performance.now() - startTicks; } catch (error) { console.error(formatWarning(`ECSql query failed: ${ecsql}`)); printError(error); return true; } const output = []; const headerRow = metadata.map((col) => col.name); output.push(headerRow); const jsonCells = []; const maxRowIndex = rows.length > 100 ? 99 : rows.length - 1; for (let colIndex = 0; colIndex < metadata.length; colIndex++) { const colInfo = metadata[colIndex]; for (let rowIndex = 0; rowIndex <= maxRowIndex; rowIndex++) { const row = rows[rowIndex]; const { value: cValue, detectedType } = await this.formatValue(row[colIndex], colInfo, db, classIdCache); let value = cValue; if (detectedType === "json") { jsonCells.push({ rowIndex: rowIndex + 2, colIndex }); } if (colIndex === 0) { output.push(new Array(metadata.length)); } if (value === null || value === void 0) { value = ""; } if (value !== null && value !== void 0) { const formattedValue = String(value); output[rowIndex + 1][colIndex] = formattedValue; } } } const widths = this.calculateColumnWidths(output, process.stdout.columns); const columns = []; for (let i = 0; i < output[0].length; i++) { const meta = metadata[i]; const width = widths[i]; const isNumericType = meta.typeName === "int" || meta.typeName === "double" || meta.typeName === "long"; const alignment = isNumericType ? "right" : "left"; columns.push({ alignment, width, wrapWord: false }); } const config = { columns, spanningCells: [ { col: 0, row: 0, colSpan: output[0].length, alignment: "center" } ] }; const formattedSql = emphasize.highlight("sql", ecsql).value; for (let i = 0; i < metadata.length; i++) { const value = output[0][i]; const meta = metadata[i]; const typeName = meta.extendedType ?? meta.typeName; output[0][i] = `${chalk4.bold(value)} ${chalk4.italic(typeName)}`; } output.unshift([formattedSql, ...Array(headerRow.length - 1).fill("")]); if (jsonCells.length > 0) { for (const cell of jsonCells) { const highlighted = emphasize.highlight("json", output[cell.rowIndex][cell.colIndex]); if (highlighted.value) { output[cell.rowIndex][cell.colIndex] = highlighted.value + resetChar; } } } console.log(table2(output, config)); if (rows.length > 100) { console.log(formatWarning("More than 100 rows returned. Only the first 100 rows are displayed.")); } if (queryDuration < 1e3) { console.log(`Executed in ${queryDuration.toFixed()} ms.`); } else { console.log(`Executed in ${(queryDuration / 1e3).toFixed(2)} seconds.`); } return true; } static async formatValue(value, colInfo, db, classIdCache) { if (value === null || value === void 0) { return { value: "" }; } if (typeof value === "string") { try { if (value && (colInfo.extendedType?.toLowerCase() === "json" || colInfo.typeName.toLowerCase() === "string") && value.startsWith("{")) { const jsonValue = JSON.parse(value); if (typeof jsonValue === "object" && jsonValue) { return { value: JSON.stringify(jsonValue, null, 2), detectedType: "json" }; } } } catch { } return { value }; } if (typeof value === "number" || typeof value === "boolean") { return { value: String(value) }; } if (colInfo.typeName === "navigation") { if (typeof value === "object" && value !== null && "Id" in value && "RelECClassId" in value) { const id = value.Id; const classId = value.RelECClassId; if (!id || !classId) { return { value: "" }; } const className = await this.getClassName(db, classId, classIdCache); return { value: `${className} ${id}` }; } return { value: "" }; } if (Array.isArray(value)) { return { value: `[${value.map((v) => this.formatValue(v, colInfo, db, classIdCache)).join(", ")}]` }; } return { value: JSON.stringify(value) }; } static calculateColumnWidths(data, maxWidth) { if (data.length === 0) { return []; } if (maxWidth < 80) maxWidth = 80; let columnWidths = []; const minWidthPerColumn = 8; const minRequiredWidth = data[0].length * minWidthPerColumn; if (maxWidth < minRequiredWidth) { columnWidths = new Array(data[0].length).fill(minWidthPerColumn); } else { for (const row of data) { for (let i = 0; i < row.length; i++) { const cell = row[i]; if (!cell) continue; if (!columnWidths[i] || cell.length > columnWidths[i]) { const width = this.calculateWidth(cell); columnWidths[i] = width; } } } let totalWidth = columnWidths.reduce((sum, w) => sum + w, 0) + columnWidths.length * 3; if (totalWidth > maxWidth) { while (totalWidth > maxWidth) { let maxColIdx = 0; for (let i = 1; i < columnWidths.length; i++) { if (columnWidths[i] > columnWidths[maxColIdx]) { maxColIdx = i; } } columnWidths[maxColIdx] = Math.max(8, Math.floor(columnWidths[maxColIdx] / 2)); totalWidth = columnWidths.reduce((sum, w) => sum + w, 0) + columnWidths.length * 3; } } } return columnWidths; } static calculateWidth(cell) { if (cell.includes("\n")) { return cell.split("\n").reduce((max, line) => Math.max(max, line.length), 0); } return cell.length; } }; // src/Logic/NewFile.ts import fs6 from "node:fs/promises"; import { CheckpointManager, ProgressStatus } from "@itwin/core-backend"; import path8 from "path"; // src/Logic/Changesets.ts import fs5 from "node:fs/promises"; import path7 from "path"; import { z as z5 } from "zod/v4"; import { ContainingChanges } from "@itwin/imodels-client-management"; import { existsSync as existsSync4, mkdirSync as mkdirSync2 } from "node:fs"; var ChangesetListSchema = z5.array(z5.object({ id: z5.string(), displayName: z5.string(), index: z5.number(), parentId: z5.string(), pushDateTime: z5.string(), containingChanges: z5.enum(ContainingChanges), fileSize: z5.number(), relativePath: z5.string().optional() // path relative to the })); var Changesets = class _Changesets { static getChangesetsFolder(ws, file) { const contextFolder = getFileContextFolderPath(ws.workspaceRootPath, file.relativePath); const changesetsDir = path7.join(contextFolder, "changesets"); return changesetsDir; } static getChangesetRelativePath(ws, file, absolutePath) { const changesetsDir = this.getChangesetsFolder(ws, file); if (!absolutePath.startsWith(changesetsDir)) { throw new Error(`Absolute path ${absolutePath} does not start with changesets directory ${changesetsDir}`); } const relativePath = path7.relative(changesetsDir, absolutePath); if (!relativePath) { throw new Error(`Could not determine relative path for ${absolutePath} in changesets directory ${changesetsDir}`); } return relativePath; } static calculateOverallFileSize(changesets) { return changesets.reduce((total, changeset) => total + (changeset.fileSize || 0), 0); } static downloadedChangesetsToChangesetList(ws, file, downloadedChangesets) { const list = []; for (const changeset of downloadedChangesets) { const relativePath = this.getChangesetRelativePath(ws, file, changeset.filePath); list.push({ id: changeset.id, displayName: changeset.displayName, index: changeset.index, parentId: changeset.parentId, pushDateTime: changeset.pushDateTime, containingChanges: changeset.containingChanges, fileSize: changeset.fileSize, relativePath }); } return list; } static async readChangesetListFromFile(ws, file) { const changesetsDir = this.getChangesetsFolder(ws, file); const changesetListFile = path7.join(changesetsDir, "changeset-list.json"); if (!existsSync4(changesetListFile)) { return []; } const content = await fs5.readFile(changesetListFile, "utf-8"); try { return ChangesetListSchema.parse(JSON.parse(content)); } catch (error) { throw new Error(`Failed to parse changeset list from file ${changesetListFile}: ${error}`); } } static async writeChangesetListToFile(ws, file, changesetList) { const changesetsDir = this.getChangesetsFolder(ws, file); if (!existsSync4(changesetsDir)) { mkdirSync2(changesetsDir, { recursive: true }); } const changesetListFile = path7.join(changesetsDir, "changeset-list.json"); await fs5.writeFile(changesetListFile, JSON.stringify(changesetList, null, 2), "utf-8"); } static async downloadChangesets(ws, file, iModelId) { const changesetsDir = this.getChangesetsFolder(ws, file); if (!existsSync4(changesetsDir)) { mkdirSync2(changesetsDir, { recursive: true }); } if (!existsSync4(changesetsDir)) { await fs5.mkdir(changesetsDir, { recursive: true }); } const downloaded = await ws.envManager.iModelsClient.changesets.downloadList({ authorization: () => ws.envManager.getAuthorization(), iModelId, targetDirectoryPath: changesetsDir }); const cacheList = _Changesets.downloadedChangesetsToChangesetList(ws, file, downloaded); await _Changesets.writeChangesetListToFile(ws, file, cacheList); } }; // src/Logic/NewFile.ts var NewFile = class { static async run(w