Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 8 additions & 8 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

8 changes: 5 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -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",
Expand Down
17 changes: 17 additions & 0 deletions src/commands/context/clear.ts
Original file line number Diff line number Diff line change
@@ -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." });
}
}
17 changes: 17 additions & 0 deletions src/commands/context/info.ts
Original file line number Diff line number Diff line change
@@ -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);
}
}
59 changes: 59 additions & 0 deletions src/commands/context/set.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
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";

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: '<string>',
required: false
}),
"itwin-id": Flags.string({
char: 'i',
description: "The ID of the iTwin to create a context for.",
helpValue: '<string>',
required: false,
}),
};

async run() {
const { flags } = await this.parse(SetContext);
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>("imodel:info", ["--imodel-id", iModelId]);
if(iTwinId && iModel.iTwinId !== flags["itwin-id"]) {
this.error("The iModel ID does not match the iTwin ID.");
Comment thread
Nadegamra marked this conversation as resolved.
Outdated
}

iTwinId = iModel.iTwinId;
}
// If iTwinId is provided, check if it exists
else if (iTwinId) {
await this.runCommand<ITwin>("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(iTwinId, iModelId);
return this.logAndReturnResult(context);
}
}
1 change: 1 addition & 0 deletions src/commands/imodel/info.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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: '<string>',
required: true
}),
Expand Down
1 change: 1 addition & 0 deletions src/commands/itwin/info.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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: '<string>',
required: true,
}),
Expand Down
80 changes: 79 additions & 1 deletion src/extensions/base-command.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +6,18 @@
import { Authorization, AuthorizationCallback, IModelsClient } from '@itwin/imodels-client-management';
import { ITwinsAccessClient } from '@itwin/itwins-client';
import { Command, 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';
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';
Expand Down Expand Up @@ -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();
Expand Down Expand Up @@ -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());
}
Expand Down Expand Up @@ -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`;

Expand All @@ -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);
Expand All @@ -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());
}
Expand Down Expand Up @@ -193,8 +233,46 @@ export default abstract class BaseCommand extends Command {
}
}

protected override async parse<F extends FlagOutput, B extends FlagOutput, A extends ArgOutput>(
options?: Input<F, B, A>,
argv?: string[]
): Promise<ParserOutput<F, B, A>> {
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<T>(command: string, args: string[]) : Promise<T> {
const mergedArgs = [...args, '--silent'];
return this.config.runCommand<T>(command, mergedArgs);
}

protected async setContext(iTwinId: string, 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;
}
}
4 changes: 4 additions & 0 deletions src/services/general-models/user-context.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export type UserContext = {
iModelId?: string;
iTwinId: string;
}