From ffb1b0a4f8f7c5b914777306ebce4c8c5c29a26b Mon Sep 17 00:00:00 2001 From: Vilius Albrechtas Date: Wed, 9 Apr 2025 15:34:12 +0300 Subject: [PATCH 1/8] Include context command functionality --- package-lock.json | 16 ++-- package.json | 8 +- src/commands/context/clear.ts | 17 +++++ src/commands/context/info.ts | 17 +++++ src/commands/context/set.ts | 39 ++++++++++ src/commands/imodel/info.ts | 1 + src/commands/itwin/info.ts | 1 + src/extensions/base-command.ts | 82 ++++++++++++++++++++- src/services/general-models/user-context.ts | 4 + 9 files changed, 172 insertions(+), 13 deletions(-) create mode 100644 src/commands/context/clear.ts create mode 100644 src/commands/context/info.ts create mode 100644 src/commands/context/set.ts create mode 100644 src/services/general-models/user-context.ts diff --git a/package-lock.json b/package-lock.json index ecd6ddbb..ebfaea37 100644 --- a/package-lock.json +++ b/package-lock.json @@ -14,7 +14,7 @@ "@itwin/node-cli-authorization": "^2.0.3", "@itwin/object-storage-core": "^2.2.5", "@itwin/service-authorization": "^1.2.2", - "@oclif/core": "^4.2.8", + "@oclif/core": "^4.2.10", "@oclif/plugin-help": "^6", "@oclif/plugin-plugins": "^5", "@opentelemetry/api": "^1.9.0", @@ -2374,13 +2374,13 @@ } }, "node_modules/@oclif/core": { - "version": "4.2.8", - "resolved": "https://registry.npmjs.org/@oclif/core/-/core-4.2.8.tgz", - "integrity": "sha512-OWv4Va6bERxIhrYcnUGzyhGRqktc64lJO6cZ3UwkzJDpfR8ZrbCxRfKRBBah1i8kzUlOAeAXnpbMBMah3skKwA==", + "version": "4.2.10", + "resolved": "https://registry.npmjs.org/@oclif/core/-/core-4.2.10.tgz", + "integrity": "sha512-fAqcXgqkUm4v5FYy7qWP4w1HaOlVSVJveah+yVTo5Nm5kTiXhmD5mQQ7+knGeBaStyrtQy6WardoC2xSic9rlQ==", "license": "MIT", "dependencies": { "ansi-escapes": "^4.3.2", - "ansis": "^3.16.0", + "ansis": "^3.17.0", "clean-stack": "^3.0.1", "cli-spinners": "^2.9.2", "debug": "^4.4.0", @@ -3865,9 +3865,9 @@ } }, "node_modules/ansis": { - "version": "3.16.0", - "resolved": "https://registry.npmjs.org/ansis/-/ansis-3.16.0.tgz", - "integrity": "sha512-sU7d/tfZiYrsIAXbdL/CNZld5bCkruzwT5KmqmadCJYxuLxHAOBjidxD5+iLmN/6xEfjcQq1l7OpsiCBlc4LzA==", + "version": "3.17.0", + "resolved": "https://registry.npmjs.org/ansis/-/ansis-3.17.0.tgz", + "integrity": "sha512-0qWUglt9JEqLFr3w1I1pbrChn1grhaiAR2ocX1PP/flRmxgtwTzPFFFnfIlD6aMOLQZgSuCRlidD70lvx8yhzg==", "license": "ISC", "engines": { "node": ">=14" diff --git a/package.json b/package.json index f66f199d..7f61994c 100644 --- a/package.json +++ b/package.json @@ -13,7 +13,7 @@ "@itwin/node-cli-authorization": "^2.0.3", "@itwin/object-storage-core": "^2.2.5", "@itwin/service-authorization": "^1.2.2", - "@oclif/core": "^4.2.8", + "@oclif/core": "^4.2.10", "@oclif/plugin-help": "^6", "@oclif/plugin-plugins": "^5", "@opentelemetry/api": "^1.9.0", @@ -75,11 +75,13 @@ "description": "Say hello to the world and others" } }, - "additionalHelpFlags": ["-h"] + "additionalHelpFlags": [ + "-h" + ] }, "repository": "iTwin/itwin-cli", "scripts": { - "clean": "shx rm -rf dist", + "clean": "shx rm -rf dist tsconfig.tsbuildinfo", "build": "npm run clean && tsc -b", "lint": "eslint . --ext .ts", "postpack": "shx rm -f oclif.manifest.json", diff --git a/src/commands/context/clear.ts b/src/commands/context/clear.ts new file mode 100644 index 00000000..ecc7ad1d --- /dev/null +++ b/src/commands/context/clear.ts @@ -0,0 +1,17 @@ +import BaseCommand from "../../extensions/base-command.js"; + +export default class ClearContext extends BaseCommand { + static description = "Clear the context of the current session."; + + static examples = [ + { + command: `<%= config.bin %> <%= command.id %>`, + description: 'Example 1: Clear the context of the current session' + } + ]; + + async run() { + this.clearContext(); + return this.logAndReturnResult({ result: "Context cleared." }); + } + } \ No newline at end of file diff --git a/src/commands/context/info.ts b/src/commands/context/info.ts new file mode 100644 index 00000000..43802925 --- /dev/null +++ b/src/commands/context/info.ts @@ -0,0 +1,17 @@ +import BaseCommand from "../../extensions/base-command.js"; + +export default class InfoContext extends BaseCommand { + static description = "Display the current context of the session."; + + static examples = [ + { + command: `<%= config.bin %> <%= command.id %>`, + description: 'Example 1: Display the current context of the session' + } + ]; + + async run() { + const context = this.getContext(); + return this.logAndReturnResult(context); + } +} \ No newline at end of file diff --git a/src/commands/context/set.ts b/src/commands/context/set.ts new file mode 100644 index 00000000..6616c28a --- /dev/null +++ b/src/commands/context/set.ts @@ -0,0 +1,39 @@ +import { Flags } from "@oclif/core"; + +import BaseCommand from "../../extensions/base-command.js"; + +export default class SetContext extends BaseCommand { + static description = "Create a new context for the current session."; + + static examples = [ + { + command: `<%= config.bin %> <%= command.id %>`, + description: 'Example 1: Create a new context for the current session' + } + ]; + + static flags = { + "imodel-id": Flags.string({ + char: 'm', + description: "The ID of the iModel to create a context for.", + helpValue: '', + required: false + }), + "itwin-id": Flags.string({ + char: 'i', + description: "The ID of the iTwin to create a context for.", + helpValue: '', + required: false, + }), + }; + + async run() { + const { flags } = await this.parse(SetContext); + if (!flags["itwin-id"] && !flags["imodel-id"]) { + this.error("Either --itwin-id or --imodel-id must be provided."); + } + + const context = await this.setContext(flags["itwin-id"], flags["imodel-id"]); + return this.logAndReturnResult(context); + } +} \ No newline at end of file diff --git a/src/commands/imodel/info.ts b/src/commands/imodel/info.ts index 88274a20..529c46e3 100644 --- a/src/commands/imodel/info.ts +++ b/src/commands/imodel/info.ts @@ -21,6 +21,7 @@ export class IModelInfo extends BaseCommand { "imodel-id": Flags.string({ char: 'm', description: 'The ID of the iModel to retrieve information for.', + env: 'ITP_IMODEL_ID', helpValue: '', required: true }), diff --git a/src/commands/itwin/info.ts b/src/commands/itwin/info.ts index 31bc101c..c8b7c97e 100644 --- a/src/commands/itwin/info.ts +++ b/src/commands/itwin/info.ts @@ -21,6 +21,7 @@ export default class ITwinInfo extends BaseCommand { "itwin-id": Flags.string({ char: 'i', description: 'The ID of the iTwin to retrieve information about.', + env: 'ITP_ITWIN_ID', helpValue: '', required: true, }), diff --git a/src/extensions/base-command.ts b/src/extensions/base-command.ts index 44d6405d..2db342c9 100644 --- a/src/extensions/base-command.ts +++ b/src/extensions/base-command.ts @@ -5,12 +5,14 @@ import { Authorization, AuthorizationCallback, IModelsClient } from '@itwin/imodels-client-management'; import { ITwinsAccessClient } from '@itwin/itwins-client'; -import { Command, Flags } from '@oclif/core'; +import { Command, Config, Flags } from '@oclif/core'; +import { Input, ParserOutput } from '@oclif/core/interfaces'; import { Table } from 'console-table-printer'; import 'dotenv/config'; import * as fs from 'node:fs'; import * as path from 'node:path'; +import { ArgOutput, FlagOutput } from '../../node_modules/@oclif/core/lib/interfaces/parser.js'; import { AccessControlClient } from '../services/access-control-client/access-control-client.js'; import { AccessControlMemberClient } from '../services/access-control-client/access-control-member-client.js'; import { AuthorizationClient } from '../services/authorization-client/authorization-client.js'; @@ -19,6 +21,7 @@ import { ITwinPlatformApiClient } from '../services/iTwin-api-client.js'; import { StorageApiClient } from '../services/storage-client/storage-api-client.js'; import { SynchronizationApiClient } from '../services/synchronizationClient/synchronization-api-client.js'; import { UserApiClient } from '../services/user-client/user-api-client.js'; +import { UserContext } from '../services/general-models/user-context.js'; import { configuration } from './configuration.js'; export default abstract class BaseCommand extends Command { @@ -45,6 +48,15 @@ export default abstract class BaseCommand extends Command { static enableJsonFlag = true; + protected async clearContext() { + const contextPath = this.config.cacheDir + '/context.json'; + if (fs.existsSync(contextPath)) { + fs.rmSync(contextPath, { force: true }); + } + + this.debug(`Cleared context file: ${contextPath}`); + } + protected async getAccessControlApiClient() { const token = await this.getAccessToken(); const url = this.getBaseApiUrl(); @@ -87,7 +99,6 @@ export default abstract class BaseCommand extends Command { return config?.apiUrl ?? 'https://api.bentley.com'; } - protected async getChangeElementApiClient() { return new ChangedElementsApiClient(await this.getITwinApiClient()); } @@ -127,6 +138,23 @@ export default abstract class BaseCommand extends Command { return config; } + protected getContext() { + const contextPath = this.config.cacheDir + '/context.json'; + if (!fs.existsSync(contextPath)) { + return; + } + + try { + const contextFile = fs.readFileSync(contextPath, 'utf8'); + const context = JSON.parse(contextFile) as UserContext; + + return context; + } + catch (error) { + this.debug("Error parsing context file:", error); + } + } + protected getIModelClient() : IModelsClient { const baseUrl = `${this.getBaseApiUrl()}/imodels`; @@ -137,6 +165,12 @@ export default abstract class BaseCommand extends Command { }); } + protected getIModelId() : string | undefined { + const context = this.getContext(); + + return context?.iModelId; + } + protected getITwinAccessClient(): ITwinsAccessClient { const baseUrl = `${this.getBaseApiUrl()}/itwins`; return new ITwinsAccessClient(baseUrl); @@ -148,6 +182,12 @@ export default abstract class BaseCommand extends Command { return new ITwinPlatformApiClient(this.getBaseApiUrl(), token); } + protected getITwinId() : string | undefined { + const context = this.getContext(); + + return context?.iTwinId; + } + protected async getStorageApiClient() { return new StorageApiClient(await this.getITwinApiClient()); } @@ -193,8 +233,46 @@ export default abstract class BaseCommand extends Command { } } + protected override async parse( + options?: Input, + argv?: string[] + ): Promise> { + if(options?.flags) { + const context = this.getContext(); + + if (options.flags['itwin-id'] && !this.argv.includes('--itwin-id') && !this.argv.includes('-i') && context?.iTwinId) { + this.argv.push('--itwin-id', context.iTwinId); + } + + if (options.flags['imodel-id'] && !this.argv.includes('--imodel-id') && !this.argv.includes('-m') && context?.iModelId) { + this.argv.push('--imodel-id', context.iModelId); + } + } + + const parsed = await super.parse(options, argv); + + return parsed; + } + protected async runCommand(command: string, args: string[]) : Promise { const mergedArgs = [...args, '--silent']; return this.config.runCommand(command, mergedArgs); } + + protected async setContext(iTwinId: string | undefined, iModelId: string | undefined) { + const contextPath = this.config.cacheDir + '/context.json'; + + const context: UserContext = { + iModelId, + iTwinId + }; + + if (!fs.existsSync(this.config.cacheDir)) { + fs.mkdirSync(this.config.cacheDir, { recursive: true }); + } + + fs.writeFileSync(contextPath, JSON.stringify(context, null, 2), 'utf8'); + + return context; + } } \ No newline at end of file diff --git a/src/services/general-models/user-context.ts b/src/services/general-models/user-context.ts new file mode 100644 index 00000000..a4935aec --- /dev/null +++ b/src/services/general-models/user-context.ts @@ -0,0 +1,4 @@ +export type UserContext = { + iModelId?: string; + iTwinId?: string; +} \ No newline at end of file From f1469744555f65adca9bb8ca53ca78d52aa6bc4f Mon Sep 17 00:00:00 2001 From: Vilius Albrechtas Date: Wed, 9 Apr 2025 16:02:45 +0300 Subject: [PATCH 2/8] Fixed linter errors --- src/extensions/base-command.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/extensions/base-command.ts b/src/extensions/base-command.ts index 2db342c9..dbd298c4 100644 --- a/src/extensions/base-command.ts +++ b/src/extensions/base-command.ts @@ -5,7 +5,7 @@ import { Authorization, AuthorizationCallback, IModelsClient } from '@itwin/imodels-client-management'; import { ITwinsAccessClient } from '@itwin/itwins-client'; -import { Command, Config, Flags } from '@oclif/core'; +import { Command, Flags } from '@oclif/core'; import { Input, ParserOutput } from '@oclif/core/interfaces'; import { Table } from 'console-table-printer'; import 'dotenv/config'; @@ -21,7 +21,6 @@ import { ITwinPlatformApiClient } from '../services/iTwin-api-client.js'; import { StorageApiClient } from '../services/storage-client/storage-api-client.js'; import { SynchronizationApiClient } from '../services/synchronizationClient/synchronization-api-client.js'; import { UserApiClient } from '../services/user-client/user-api-client.js'; -import { UserContext } from '../services/general-models/user-context.js'; import { configuration } from './configuration.js'; export default abstract class BaseCommand extends Command { From ca01635dc1306a4d1b2c114b90d3065261188a24 Mon Sep 17 00:00:00 2001 From: Vilius Albrechtas Date: Wed, 9 Apr 2025 16:07:38 +0300 Subject: [PATCH 3/8] Included missing import --- src/extensions/base-command.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/extensions/base-command.ts b/src/extensions/base-command.ts index dbd298c4..99dab3f2 100644 --- a/src/extensions/base-command.ts +++ b/src/extensions/base-command.ts @@ -17,6 +17,7 @@ import { AccessControlClient } from '../services/access-control-client/access-co import { AccessControlMemberClient } from '../services/access-control-client/access-control-member-client.js'; import { AuthorizationClient } from '../services/authorization-client/authorization-client.js'; import { ChangedElementsApiClient } from '../services/changed-elements-client/changed-elements-api-client.js'; +import { UserContext } from '../services/general-models/user-context.js'; import { ITwinPlatformApiClient } from '../services/iTwin-api-client.js'; import { StorageApiClient } from '../services/storage-client/storage-api-client.js'; import { SynchronizationApiClient } from '../services/synchronizationClient/synchronization-api-client.js'; From 946b52405a98e1ba354857394a43219f45e5bbac Mon Sep 17 00:00:00 2001 From: Vilius Albrechtas Date: Thu, 10 Apr 2025 10:48:05 +0300 Subject: [PATCH 4/8] Add iModel and iTwin verification --- src/commands/context/set.ts | 26 ++++++++++++++++++--- src/extensions/base-command.ts | 2 +- src/services/general-models/user-context.ts | 2 +- 3 files changed, 25 insertions(+), 5 deletions(-) diff --git a/src/commands/context/set.ts b/src/commands/context/set.ts index 6616c28a..10141399 100644 --- a/src/commands/context/set.ts +++ b/src/commands/context/set.ts @@ -1,3 +1,5 @@ +import { IModel } from "@itwin/imodels-client-management"; +import { ITwin } from "@itwin/itwins-client"; import { Flags } from "@oclif/core"; import BaseCommand from "../../extensions/base-command.js"; @@ -29,11 +31,29 @@ export default class SetContext extends BaseCommand { async run() { const { flags } = await this.parse(SetContext); - if (!flags["itwin-id"] && !flags["imodel-id"]) { - this.error("Either --itwin-id or --imodel-id must be provided."); + const iModelId = flags["imodel-id"]; + let iTwinId = flags["itwin-id"]; + + // If iModelId is provided, check if it exists + // and verify that it belongs to the specified iTwinId + if(iModelId) { + const iModel = await this.runCommand("imodel:info", ["--imodel-id", iModelId]); + if(iTwinId && iModel.iTwinId !== flags["itwin-id"]) { + this.error("The iModel ID does not match the iTwin ID."); + } + + iTwinId = iModel.iTwinId; } + // If iTwinId is provided, check if it exists + else if (iTwinId) { + await this.runCommand("itwin:info", ["--itwin-id", iTwinId]); + } + // If neither iModelId nor iTwinId is provided, throw an error + else { + this.error("Either --itwin-id or --imodel-id must be provided."); + } - const context = await this.setContext(flags["itwin-id"], flags["imodel-id"]); + const context = await this.setContext(iTwinId, iModelId); return this.logAndReturnResult(context); } } \ No newline at end of file diff --git a/src/extensions/base-command.ts b/src/extensions/base-command.ts index 99dab3f2..bc6744eb 100644 --- a/src/extensions/base-command.ts +++ b/src/extensions/base-command.ts @@ -259,7 +259,7 @@ export default abstract class BaseCommand extends Command { return this.config.runCommand(command, mergedArgs); } - protected async setContext(iTwinId: string | undefined, iModelId: string | undefined) { + protected async setContext(iTwinId: string, iModelId: string | undefined) { const contextPath = this.config.cacheDir + '/context.json'; const context: UserContext = { diff --git a/src/services/general-models/user-context.ts b/src/services/general-models/user-context.ts index a4935aec..b5efb397 100644 --- a/src/services/general-models/user-context.ts +++ b/src/services/general-models/user-context.ts @@ -1,4 +1,4 @@ export type UserContext = { iModelId?: string; - iTwinId?: string; + iTwinId: string; } \ No newline at end of file From 95a7d4de6131930b3ede475a76440465f2a69f27 Mon Sep 17 00:00:00 2001 From: Vilius Albrechtas Date: Thu, 10 Apr 2025 14:58:43 +0300 Subject: [PATCH 5/8] Add integration tests --- integration-tests/context.ts/context.test.ts | 118 +++++++++++++++++++ src/commands/context/set.ts | 2 +- src/extensions/base-command.ts | 6 +- src/services/general-models/user-context.ts | 2 +- 4 files changed, 125 insertions(+), 3 deletions(-) create mode 100644 integration-tests/context.ts/context.test.ts diff --git a/integration-tests/context.ts/context.test.ts b/integration-tests/context.ts/context.test.ts new file mode 100644 index 00000000..bc182185 --- /dev/null +++ b/integration-tests/context.ts/context.test.ts @@ -0,0 +1,118 @@ +import { IModel } from "@itwin/imodels-client-management"; +import { ITwin } from "@itwin/itwins-client"; +import { runCommand } from "@oclif/test"; +import { expect } from "chai"; + +import { loginToCli } from "../utils/helpers"; + +describe('Context Integration Tests', () => { + let iTwin: ITwin; + let iModel: IModel; + let anotherITwin: ITwin; + + before(async () => { + await loginToCli(); + const name = `IntegrationTestITwin_${new Date().toISOString()}`; + const iTwinResult = await runCommand(`itwin create --name "${name}" --class Thing --sub-class Asset`); + expect(iTwinResult.error).to.be.undefined; + expect(iTwinResult.result).to.not.be.undefined; + iTwin = iTwinResult.result as ITwin; + + const iModelName = `IntegrationTestIModel_${new Date().toISOString()}`; + const iModelResult = await runCommand(`imodel create --name "${iModelName}" --itwin-id ${iTwin.id}`); + expect(iModelResult.error).to.be.undefined; + expect(iModelResult.result).to.not.be.undefined; + iModel = iModelResult.result as IModel; + + const anotherITwinName = `AnotherITwin_${new Date().toISOString()}`; + const anotherITwinResult = await runCommand(`itwin create --name "${anotherITwinName}" --class Thing --sub-class Asset`); + expect(anotherITwinResult.error).to.be.undefined; + expect(anotherITwinResult.result).to.not.be.undefined; + anotherITwin = anotherITwinResult.result as ITwin; + }); + + after(async () => { + await runCommand(`itwin delete --id ${iTwin.id}`); + await runCommand(`itwin delete --id ${anotherITwin.id}`); + }); + + beforeEach(async () => { + const output = await runCommand('context clear'); + expect(output.error).to.be.undefined; + }); + + it('should clear the context', async () => { + const output = await runCommand('context clear'); + expect(output.error).to.be.undefined; + expect(output.stdout).to.contain('Context cleared.'); + + const outputInfo = await runCommand('context info'); + expect(outputInfo.error).to.be.undefined; + expect(outputInfo.result).to.be.undefined; + }); + + it('should set the context', async () => { + const output = await runCommand(`context set --itwin-id ${iTwin.id} --imodel-id ${iModel.id}`); + expect(output.error).to.be.undefined; + expect(output.result).to.deep.equal({ iModelId: iModel.id, iTwinId: iTwin.id }); + }); + + it('should fail to set context with invalid iTwin ID', async () => { + const invalidITwinId = "invalid-id"; + const output = await runCommand(`context set --itwin-id ${invalidITwinId}`); + expect(output.error).to.not.be.undefined; + expect(output.error?.message).to.contain('Requested iTwin is not available.'); + }); + + it('should fail to set context with mismatched iModel and iTwin IDs', async () => { + const output = await runCommand(`context set --itwin-id ${anotherITwin.id} --imodel-id ${iModel.id}`); + expect(output.error).to.not.be.undefined; + expect(output.error?.message).to.contain(`The iModel ID ${iModel.id} does not belong to the specified iTwin ID ${anotherITwin.id}.`); + }); + + it('should fail to set context without iTwin or iModel ID', async () => { + const output = await runCommand('context set'); + expect(output.error).to.not.be.undefined; + expect(output.error?.message).to.contain('Either --itwin-id or --imodel-id must be provided.'); + }); + + it('should display the current context', async () => { + await runCommand(`context set --itwin-id ${iTwin.id} --imodel-id ${iModel.id}`); + const output = await runCommand('context info'); + expect(output.error).to.be.undefined; + expect(output.result).to.deep.equal({ iModelId: iModel.id, iTwinId: iTwin.id }); + }); + + it('should display undefined context after clearing', async () => { + await runCommand('context clear'); + const output = await runCommand('context info'); + expect(output.error).to.be.undefined; + expect(output.result).to.be.undefined; + }); + + it('should clear context multiple times without error', async () => { + await runCommand('context clear'); + const output = await runCommand('context clear'); + expect(output.error).to.be.undefined; + expect(output.stdout).to.contain('Context cleared.'); + }); + + it('should set the context with only an iTwin ID', async () => { + const output = await runCommand(`context set --itwin-id ${iTwin.id}`); + expect(output.error).to.be.undefined; + expect(output.result).to.deep.equal({ iModelId: undefined, iTwinId: iTwin.id }); + }); + + it('should set the context with only an iModel ID and resolve the correct iTwin ID', async () => { + const output = await runCommand(`context set --imodel-id ${iModel.id}`); + expect(output.error).to.be.undefined; + expect(output.result).to.deep.equal({ iModelId: iModel.id, iTwinId: iTwin.id }); + }); + + it('should fail to set context with only an iModel ID if the iModel does not exist', async () => { + const invalidIModelId = "invalid-id"; + const output = await runCommand(`context set --imodel-id ${invalidIModelId}`); + expect(output.error).to.not.be.undefined; + expect(output.error?.message).to.contain('Requested iModel is not available.'); + }); +}); \ No newline at end of file diff --git a/src/commands/context/set.ts b/src/commands/context/set.ts index 10141399..4c2c741a 100644 --- a/src/commands/context/set.ts +++ b/src/commands/context/set.ts @@ -39,7 +39,7 @@ export default class SetContext extends BaseCommand { if(iModelId) { const iModel = await this.runCommand("imodel:info", ["--imodel-id", iModelId]); if(iTwinId && iModel.iTwinId !== flags["itwin-id"]) { - this.error("The iModel ID does not match the iTwin ID."); + this.error(`The iModel ID ${iModelId} does not belong to the specified iTwin ID ${iTwinId}.`); } iTwinId = iModel.iTwinId; diff --git a/src/extensions/base-command.ts b/src/extensions/base-command.ts index bc6744eb..9898c928 100644 --- a/src/extensions/base-command.ts +++ b/src/extensions/base-command.ts @@ -138,7 +138,7 @@ export default abstract class BaseCommand extends Command { return config; } - protected getContext() { + protected getContext() : UserContext | undefined { const contextPath = this.config.cacheDir + '/context.json'; if (!fs.existsSync(contextPath)) { return; @@ -147,6 +147,10 @@ export default abstract class BaseCommand extends Command { try { const contextFile = fs.readFileSync(contextPath, 'utf8'); const context = JSON.parse(contextFile) as UserContext; + + if(!context.iModelId && !context.iTwinId) { + return undefined; + } return context; } diff --git a/src/services/general-models/user-context.ts b/src/services/general-models/user-context.ts index b5efb397..a4935aec 100644 --- a/src/services/general-models/user-context.ts +++ b/src/services/general-models/user-context.ts @@ -1,4 +1,4 @@ export type UserContext = { iModelId?: string; - iTwinId: string; + iTwinId?: string; } \ No newline at end of file From edaf473de85885c30ca90f0ccdbd166b2631a5f1 Mon Sep 17 00:00:00 2001 From: Vilius Albrechtas Date: Fri, 11 Apr 2025 11:22:16 +0300 Subject: [PATCH 6/8] Add documentation for the commands and remove mentions of session --- docs/_sidebar.md | 4 ++++ docs/context/clear.md | 10 ++++++++++ docs/context/info.md | 10 ++++++++++ docs/context/overview.md | 9 +++++++++ docs/context/set.md | 26 ++++++++++++++++++++++++++ src/commands/context/clear.ts | 8 ++++---- src/commands/context/info.ts | 4 ++-- src/commands/context/set.ts | 4 ++-- 8 files changed, 67 insertions(+), 8 deletions(-) create mode 100644 docs/context/clear.md create mode 100644 docs/context/info.md create mode 100644 docs/context/overview.md create mode 100644 docs/context/set.md diff --git a/docs/_sidebar.md b/docs/_sidebar.md index 0cf8830d..77e2801f 100644 --- a/docs/_sidebar.md +++ b/docs/_sidebar.md @@ -10,6 +10,10 @@ * [info](/docs/auth/info.md) * [login](/docs/auth/login.md) * [logout](/docs/auth/logout.md) +* [context](/docs/context/overview.md) + * [set](/docs/context/set.md) + * [clear](/docs/context/clear.md) + * [info](/docs/context/info.md) * [api](/docs/api.md) * [itwin](/docs/itwin/overview.md) * [create](/docs/itwin/create.md) diff --git a/docs/context/clear.md b/docs/context/clear.md new file mode 100644 index 00000000..e0c85467 --- /dev/null +++ b/docs/context/clear.md @@ -0,0 +1,10 @@ +# itp context clear + +Clear the cached context. + +## Examples + +```bash +# Example 1: Clear the cached context +itp context clear +``` diff --git a/docs/context/info.md b/docs/context/info.md new file mode 100644 index 00000000..866a73dc --- /dev/null +++ b/docs/context/info.md @@ -0,0 +1,10 @@ +# itp context info + +Display the cached context. + +## Examples + +```bash +# Example 1: Display the cached context +itp context info +``` diff --git a/docs/context/overview.md b/docs/context/overview.md new file mode 100644 index 00000000..dadcf8f7 --- /dev/null +++ b/docs/context/overview.md @@ -0,0 +1,9 @@ +# itp context + +Work with iTwin CLI context. + +## Available Commands + +- [itp context set](set.md) +- [itp context info](info.md) +- [itp context clear](clear.md) diff --git a/docs/context/set.md b/docs/context/set.md new file mode 100644 index 00000000..2019ae30 --- /dev/null +++ b/docs/context/set.md @@ -0,0 +1,26 @@ +# itp context set + +Set a new cached context. + +## Options + +- **`--imodel-id`** + The ID of the iModel to create a context for. + **Type:** `string` **Required:** No + +- **`--itwin-id`** + The ID of the iTwin to create a context for. + **Type:** `string` **Required:** No + +## Examples + +```bash +# Example 1: Set a new cached context using an iTwin ID +itp context set --itwin-id 12345 + +# Example 2: Set a new cached context using an iModel ID +itp context set --imodel-id 67890 + +# Example 3: Error when neither --itwin-id nor --imodel-id is provided +itp context set +``` diff --git a/src/commands/context/clear.ts b/src/commands/context/clear.ts index ecc7ad1d..fe14ea9d 100644 --- a/src/commands/context/clear.ts +++ b/src/commands/context/clear.ts @@ -1,12 +1,12 @@ import BaseCommand from "../../extensions/base-command.js"; export default class ClearContext extends BaseCommand { - static description = "Clear the context of the current session."; - + static description = "Clear the cached context."; + static examples = [ { command: `<%= config.bin %> <%= command.id %>`, - description: 'Example 1: Clear the context of the current session' + description: 'Example 1: Clear the cached context' } ]; @@ -14,4 +14,4 @@ export default class ClearContext extends BaseCommand { this.clearContext(); return this.logAndReturnResult({ result: "Context cleared." }); } - } \ No newline at end of file +} \ No newline at end of file diff --git a/src/commands/context/info.ts b/src/commands/context/info.ts index 43802925..0f8abb81 100644 --- a/src/commands/context/info.ts +++ b/src/commands/context/info.ts @@ -1,12 +1,12 @@ import BaseCommand from "../../extensions/base-command.js"; export default class InfoContext extends BaseCommand { - static description = "Display the current context of the session."; + static description = "Display the cached context."; static examples = [ { command: `<%= config.bin %> <%= command.id %>`, - description: 'Example 1: Display the current context of the session' + description: 'Example 1: Display the cached context' } ]; diff --git a/src/commands/context/set.ts b/src/commands/context/set.ts index 4c2c741a..1740b8ba 100644 --- a/src/commands/context/set.ts +++ b/src/commands/context/set.ts @@ -5,12 +5,12 @@ import { Flags } from "@oclif/core"; import BaseCommand from "../../extensions/base-command.js"; export default class SetContext extends BaseCommand { - static description = "Create a new context for the current session."; + static description = "Set a new cached context."; static examples = [ { command: `<%= config.bin %> <%= command.id %>`, - description: 'Example 1: Create a new context for the current session' + description: 'Example 1: Set a new cached context' } ]; From 1238d37eece3d443e1bf25afd729ad83c47f51a8 Mon Sep 17 00:00:00 2001 From: Vilius Albrechtas Date: Fri, 11 Apr 2025 14:11:15 +0300 Subject: [PATCH 7/8] Update to include short flags in the docs --- docs/context/set.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/context/set.md b/docs/context/set.md index 2019ae30..8354ef05 100644 --- a/docs/context/set.md +++ b/docs/context/set.md @@ -4,11 +4,11 @@ Set a new cached context. ## Options -- **`--imodel-id`** +- **`-m, --imodel-id`** The ID of the iModel to create a context for. **Type:** `string` **Required:** No -- **`--itwin-id`** +- **`-i, --itwin-id`** The ID of the iTwin to create a context for. **Type:** `string` **Required:** No From 06381448c385bbbac6f664b8b934ecb51ae8b63a Mon Sep 17 00:00:00 2001 From: Vilius Albrechtas Date: Mon, 14 Apr 2025 11:02:02 +0300 Subject: [PATCH 8/8] Fixing integration tests --- .../formating/formatting.test.ts | 24 ++++++++++++------- 1 file changed, 16 insertions(+), 8 deletions(-) diff --git a/integration-tests/formating/formatting.test.ts b/integration-tests/formating/formatting.test.ts index 2ce2ad55..4f5ae93b 100644 --- a/integration-tests/formating/formatting.test.ts +++ b/integration-tests/formating/formatting.test.ts @@ -4,6 +4,11 @@ import { expect } from "chai"; describe('Command formatting tests', async () => { let allCommands : CommandWithFlags[] = []; + const commandToExcludeFromTests = [ + "help", + "plugins" + ]; + before(async () => { const config = await Config.load({ devPlugins: false, @@ -11,12 +16,13 @@ describe('Command formatting tests', async () => { userPlugins: false, }); - allCommands = config.commands.filter(command => !command.id.startsWith("plugins") && !command.id.startsWith("help") && !command.hidden) - .map((command) => - ({ - cmd: command, - flags: Object.entries(command.flags), - })); + allCommands = config.commands.filter(command => + !commandToExcludeFromTests.some(excluded => command.id.startsWith(excluded)) && !command.hidden + ).map((command) => + ({ + cmd: command, + flags: Object.entries(command.flags), + })); }); it('Should ensure all commands have a description', async () => { @@ -39,7 +45,8 @@ describe('Command formatting tests', async () => { }); it('Should ensure all itwin-id flags have env properties', async () => { - for (const command of allCommands) { + // Exclude context:set command from this test as it has a special case for itwin-id flag + for (const command of allCommands.filter(cmd => cmd.cmd.id !== "context:set")) { const iTwinIdFlag = command.flags.find(([name, _]) => name === "itwin-id"); if (iTwinIdFlag) { expect(iTwinIdFlag[1].env, `Flag 'itwin-id' in command '${command.cmd.id}' is missing the 'env' property`).to.be.a('string').and.be.equals("ITP_ITWIN_ID"); @@ -48,7 +55,8 @@ describe('Command formatting tests', async () => { }); it('Should ensure all imodel-id flags have env properties', async () => { - for (const command of allCommands) { + // Exclude context:set command from this test as it has a special case for imodel-id flag + for (const command of allCommands.filter(cmd => cmd.cmd.id !== "context:set")) { const iTwinIdFlag = command.flags.find(([name, _]) => name === "imodel-id"); if (iTwinIdFlag) { expect(iTwinIdFlag[1].env, `Flag 'imodel-id' in command '${command.cmd.id}' is missing the 'env' property`).to.be.a('string').and.be.equals("ITP_IMODEL_ID");