Skip to content

Commit dcd0e6f

Browse files
committed
Merge branch 'main' of https://github.com/iTwin/itwin-cli into wolfo951/context-implementation
2 parents 1238d37 + 8c26a00 commit dcd0e6f

67 files changed

Lines changed: 1317 additions & 940 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.github/workflows/ci.yaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,4 +49,5 @@ jobs:
4949
ITP_ISSUER_URL: ${{ vars.ITP_ISSUER_URL }}
5050
ITP_SERVICE_CLIENT_ID: ${{ vars.ITP_SERVICE_CLIENT_ID }}
5151
ITP_SERVICE_CLIENT_SECRET: ${{ secrets.ITP_SERVICE_CLIENT_SECRET }}
52+
ITP_MAILINATOR_API_KEY: ${{ secrets.ITP_MAILINATOR_API_KEY }}
5253
run: npm run test

.vscode/launch.json

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,18 @@
1818
"<itwin-id>"
1919
]
2020
},
21+
{
22+
"type": "node",
23+
"request": "launch",
24+
"name": "Launch docs generator",
25+
"skipFiles": [
26+
"<node_internals>/**"
27+
],
28+
"program": "${workspaceFolder}\\bin\\run",
29+
"args": [
30+
"docs-generator"
31+
]
32+
},
2133
{
2234
"name": "Debug All Tests",
2335
"skipFiles": [
@@ -30,7 +42,7 @@
3042
"--forbid-only",
3143
"--timeout",
3244
"999999",
33-
"integration-tests/**/api.test.ts"
45+
"integration-tests/**/*.test.ts"
3446
],
3547
"console": "integratedTerminal",
3648
"internalConsoleOptions": "neverOpen"

docs/_sidebar.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
* [iTwin CLI Manual](/docs/)
2+
* [iTwin 101](/docs/itwin101.md)
23
* [Quickstart](/docs/quickstart.md)
34
* [Workflows](/docs/workflows/overview.md)
45

docs/imodel/view/cesium-sandcastle.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,8 @@ Setup iModel and get URL to view it in Cesium Sandcastle.
77
## Options
88

99
- **`--changeset-id`**
10-
Changeset id to be viewed in Cesium Sandcastle.
11-
**Type:** `string` **Required:** Yes
10+
Changeset id to be viewed in Cesium Sandcastle. If not provided, the latest changeset will be used.
11+
**Type:** `string` **Required:** No
1212

1313
- **`-m, --imodel-id`**
1414
iModel id to be viewed in Cesium Sandcastle.

docs/itwin101.md

Lines changed: 134 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,134 @@
1+
# iTwin 101
2+
3+
**A developer-friendly introduction to Bentley's iTwin Platform.**
4+
5+
The infrastructure industry has undergone a major shift — from static design files to **cloud-connected systems** that reflect how assets behave in real time.
6+
7+
This shift is driven by the rise of **digital twins**: living models that combine infrastructure design with data from numerous cloud sources for a complete view of the asset. For example, a dam reporting structural health and energy output, or a smart building tracking occupancy, lighting, and air quality. It’s no longer just about designing — it’s about how an asset lives and performs in the real world.
8+
9+
**Bentley Systems**, a long-standing leader in infrastructure software, created the **iTwin Platform** to support this evolution. While desktop tools like **MicroStation** and **AutoCAD** have supported infrastructure design for decades, the iTwin Platform brings this data into the cloud — where teams can visualize entire assets, integrate live data, and build connected solutions across industries.
10+
11+
Following Bentley’s acquisition of **Cesium**, high-performance 3D geospatial visualization is now a built-in part of the iTwin ecosystem. **Cesium** is an open platform for streaming large-scale 3D data, and its developer sandbox, **Sandcastle**, makes it easy to prototype and interact with infrastructure in a global context.
12+
13+
The **iTwin CLI** ties it all together — letting you manage iTwins, bring design data into the cloud, and visualize your models in Cesium Sandcastle — all from the command line.
14+
15+
---
16+
17+
### 🎓 What Is a Digital Twin?
18+
19+
A **digital twin** is more than a design model. It’s a cloud-based, real-time digital representation of a physical asset—like a building, bridge, highway, or dam. A digital twin integrates multiple data layers:
20+
21+
- **👩‍🔧 Design Data**: CAD and BIM models created using tools like MicroStation, Revit, AutoCAD, etc.
22+
- **🌍 Reality Data**: Photogrammetry, point clouds, and 3D meshes captured by drones or scanners.
23+
- **📈 Sensor Data**: Live IoT data reflecting asset performance, environmental conditions, and more.
24+
- **📂 External Repositories**: Additional cloud data sources that enrich the digital twin's context.
25+
26+
What makes a digital twin powerful is the way these sources come together in the cloud to form a "living" version of an asset—something that's not just visual, but interactive and analyzable.
27+
28+
---
29+
30+
### 🪧 Enter the iTwin
31+
32+
The **iTwin** is Bentley’s implementation of a digital twin—a central hub that brings together all the data related to an infrastructure asset. It's built to be open, extensible, and deeply interoperable.
33+
34+
At its core, an iTwin includes several key components:
35+
36+
#### 🧱 iModel: Design Data, Standardized
37+
38+
An **iModel** is a specialized cloud repository for 3D design data. It acts as a **common data environment** that unifies models from various design applications (MicroStation, Revit, Civil3D, etc.) into a single, coherent format.
39+
40+
> Have a Revit file for a building? Add it to the iModel.
41+
> Got utility designs from MicroStation? Add them too.
42+
43+
The iModel isn’t just a storage format—it acts like **Git for infrastructure**. Designers "synchronize" their changes, which are stored as **changesets**. This allows for collaborative workflows, version tracking, and comparison of changes between versions.
44+
45+
#### 🌎 Reality Data: Contextual Visualization
46+
47+
Drones, scanners, and photogrammetry can capture detailed environmental data around your asset. This is imported into the iTwin to create photorealistic context and increase the accuracy of both design and decision-making.
48+
49+
#### 📊 Sensor Data: Real-Time Awareness
50+
51+
Digital twins aren’t just static snapshots. By integrating sensor or IoT data, iTwins reflect real-world conditions in real time. Think:
52+
53+
- Energy usage
54+
- Equipment performance
55+
- Occupancy levels
56+
- Fault detection
57+
58+
This makes the iTwin a true operational tool, not just a design archive.
59+
60+
#### 📚 External Data: Extendable by Nature
61+
62+
Need to connect to a custom database, web API, or document store? The iTwin is designed to consume and present external data as part of its interface. Through widgets, charts, or overlays, the twin can be tailored to meet specific business or project needs.
63+
64+
---
65+
66+
### ⚙️ The Role of the iTwin CLI
67+
68+
The **iTwin CLI** is a command-line tool that lets developers and technical users interact with the iTwin Platform through simple text commands. It simplifies many common tasks:
69+
70+
- ✍️ **Create and manage iTwins**
71+
- 🛡️ **Set up access control and user roles**
72+
- 🔍 **Query or inspect metadata**
73+
- 📂 **Create iModels and populate them with design files**
74+
- 📰 **Track and compare changes with changesets**
75+
- 🔢 **Create named versions to mark project milestones**
76+
-**Synchronize design data into the iModel**
77+
- 🌍 **Visualize the iTwin in Cesium Sandcastle**
78+
79+
It serves as a lightweight, scriptable gateway into a powerful ecosystem.
80+
81+
---
82+
83+
### 🗂️ iModels and Version Control
84+
85+
Think of the iModel like Git. Multiple designers can work in parallel and synchronize changes when ready. Each sync produces a set of **changesets** that are stored with full history. This allows you to:
86+
87+
- 📋 See who made which change
88+
- 🕓 Inspect previous states
89+
- ⚖️ Compare two versions to highlight changed elements
90+
91+
You can also create **named versions** at key milestones: design freezes, review submissions, final approvals, etc.
92+
93+
This not only supports tracking and review, but also enables advanced visualization and automation possibilities.
94+
95+
---
96+
97+
### 🗃️ Storage and Synchronization
98+
99+
iTwins contain a dedicated storage repository where design files and project documents can be centrally managed. This storage acts as a staging area for incoming files before they're synchronized into an iModel.
100+
101+
The iTwin CLI supports using this shared storage to:
102+
103+
- 📤 Upload and organize design files
104+
- 🔄 Sync them into the iModel
105+
- 📎 Add documents and media (PDFs, photos, drawings)
106+
107+
Files uploaded into iTwin storage can be selectively synchronized using **connections** to control what gets added to the iModel and when.
108+
109+
---
110+
111+
### 🌐 Geospatial Visualization with Cesium
112+
113+
One of the most exciting features of the iTwin CLI is integration with **Cesium Sandcastle**, CesiumJS's interactive 3D globe environment.
114+
115+
> Push your iTwin to Sandcastle and instantly visualize infrastructure assets in a real-world geospatial context.
116+
117+
Design data becomes globally interactive. Combined with reality meshes and IoT data, it opens up new opportunities for:
118+
119+
- High-performance 3D visualization
120+
- Infrastructure analytics
121+
- Immersive design review
122+
- Public or stakeholder presentations
123+
124+
---
125+
126+
### ✨ Wrapping Up
127+
128+
The iTwin CLI makes it easy to build, manage, and explore digital twins using the iTwin Platform. For anyone interested in geospatial technology and digital twin workflows, it opens a new path to work with infrastructure data at scale, integrate diverse data sources, and publish them to a geospatially rich, interactive viewer.
129+
130+
Whether you’re just exploring or already building, the iTwin CLI is your entry point into a next-generation platform for digital infrastructure.
131+
132+
---
133+
134+
**Next up:** [Quickstart Guide →](/docs/quickstart.md)

integration-tests/access-control/member/owner.ts

Lines changed: 23 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -10,12 +10,13 @@ import { expect } from "chai";
1010
import { groupMember } from "../../../src/services/access-control-client/models/group-members";
1111
import { ownerResponse } from "../../../src/services/access-control-client/models/owner";
1212
import { User } from "../../../src/services/user-client/models/user";
13+
import { fetchEmailsAndGetInvitationLink } from "../../utils/helpers";
1314

1415
const tests = () => {
1516
let iTwinId: string;
17+
const iTwinName: string = `cli-itwin-integration-test-${new Date().toISOString()}`;
1618

1719
before(async () => {
18-
const iTwinName = `cli-itwin-integration-test-${new Date().toISOString()}`;
1920
const iTwin = await runCommand<ITwin>(`itwin create --class Thing --sub-class Asset --name ${iTwinName}`);
2021
expect(iTwin.result?.id).is.not.undefined;
2122
iTwinId = iTwin.result!.id!;
@@ -26,13 +27,27 @@ const tests = () => {
2627
expect(result.stdout).to.contain('deleted');
2728
});
2829

29-
it('Should add new owner to an iTwin', async () => {
30-
const emailToAdd = 'itwin.cli.qa-testaccount@be-mailinator.eastus.cloudapp.azure.com';
31-
const owner = await runCommand<ownerResponse>(`access-control member owner add -i ${iTwinId} --email ${emailToAdd}`);
32-
expect(owner.result).is.not.undefined;
33-
expect(owner.result!.member).is.null;
34-
expect(owner.result!.invitation).is.not.undefined;
35-
expect(owner.result!.invitation.email).to.equal(emailToAdd);
30+
it('Should invite an external member to an iTwin, accept invitation and remove owner member', async () => {
31+
const emailToAdd = 'iTwin.CLI.QA.IntegrationTest@bentley.m8r.co';
32+
const invitedOwner = await runCommand<ownerResponse>(`access-control member owner add -i ${iTwinId} --email ${emailToAdd}`);
33+
expect(invitedOwner.result).is.not.undefined;
34+
expect(invitedOwner.result!.member).is.null;
35+
expect(invitedOwner.result!.invitation).is.not.undefined;
36+
expect(invitedOwner.result!.invitation.email.toLowerCase()).to.equal(emailToAdd.toLowerCase());
37+
38+
const invitationLink = await fetchEmailsAndGetInvitationLink(emailToAdd.split('@')[0], iTwinName);
39+
40+
await fetch(invitationLink);
41+
42+
const usersInfo = await runCommand<groupMember[]>(`access-control member owner list --itwin-id ${iTwinId}`);
43+
expect(usersInfo.result).is.not.undefined;
44+
expect(usersInfo.result!.length).to.be.equal(2);
45+
const joinedUser = usersInfo.result?.filter(user => user.email.toLowerCase() === emailToAdd.toLowerCase())[0];
46+
expect(joinedUser).to.not.be.undefined;
47+
48+
const deletionResult = await runCommand<{result: string}>(`access-control member owner delete --itwin-id ${iTwinId} --member-id ${joinedUser?.id}`);
49+
expect(deletionResult.result).to.not.be.undefined;
50+
expect(deletionResult.result!.result).to.be.equal("deleted");
3651
});
3752

3853
it('Should list owners of an iTwin', async () => {

integration-tests/access-control/member/user.ts

Lines changed: 37 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,15 +7,16 @@ import { ITwin } from "@itwin/itwins-client";
77
import { runCommand } from "@oclif/test";
88
import { expect } from "chai";
99

10-
import { member } from "../../../src/services/access-control-client/models/members";
10+
import { member, membersResponse } from "../../../src/services/access-control-client/models/members";
1111
import { Role } from "../../../src/services/access-control-client/models/role";
1212
import { User } from "../../../src/services/user-client/models/user";
13+
import { fetchEmailsAndGetInvitationLink } from "../../utils/helpers";
1314

1415
const tests = () => {
1516
let iTwinId: string;
17+
const iTwinName: string = `cli-itwin-integration-test-${new Date().toISOString()}`;
1618

1719
before(async () => {
18-
const iTwinName = `cli-itwin-integration-test-${new Date().toISOString()}`;
1920
const iTwin = await runCommand<ITwin>(`itwin create --class Thing --sub-class Asset --name ${iTwinName}`);
2021
expect(iTwin.result?.id).is.not.undefined;
2122
iTwinId = iTwin.result!.id!;
@@ -26,7 +27,39 @@ const tests = () => {
2627
expect(result.stdout).to.contain('deleted');
2728
});
2829

29-
it('Should dispaly owner info of an iTwin in member info', async () => {
30+
it('Should invite an external member to an iTwin, accept sent invitation and remove user member', async () => {
31+
const newRole = await runCommand<Role>(`access-control role create -i ${iTwinId} -n "Test Role 1" -d "Test Role Description"`);
32+
expect(newRole.result).is.not.undefined;
33+
expect(newRole.result!.id).is.not.undefined;
34+
35+
const emailToAdd = 'iTwin.CLI.QA.IntegrationTest@bentley.m8r.co';
36+
37+
const invitedUser = await runCommand<membersResponse>(`access-control member user add --itwin-id ${iTwinId} --members "[{"email": "${emailToAdd}", "roleIds": ["${newRole.result!.id}"]}]"`);
38+
39+
expect(invitedUser.result).to.not.be.undefined;
40+
expect(invitedUser.result!.invitations.length).to.be.equal(1);
41+
expect(invitedUser.result!.invitations[0].email.toLowerCase()).to.be.equal(emailToAdd.toLowerCase());
42+
expect(invitedUser.result!.invitations[0].roles.length).to.be.equal(1);
43+
expect(invitedUser.result!.invitations[0].roles[0].id).to.be.equal(newRole.result!.id);
44+
45+
const invitationLink = await fetchEmailsAndGetInvitationLink(emailToAdd.split('@')[0], iTwinName);
46+
47+
await fetch(invitationLink);
48+
49+
const usersInfo = await runCommand<member[]>(`access-control member user list --itwin-id ${iTwinId}`);
50+
expect(usersInfo.result).is.not.undefined;
51+
expect(usersInfo.result!.length).to.be.equal(2);
52+
const joinedUser = usersInfo.result?.filter(user => user.email.toLowerCase() === emailToAdd.toLowerCase())[0];
53+
expect(joinedUser).to.not.be.undefined;
54+
expect(joinedUser?.roles.length).to.be.equal(1);
55+
expect(joinedUser?.roles[0].id).to.be.equal(newRole.result!.id);
56+
57+
const deletionResult = await runCommand<{result: string}>(`access-control member user delete --itwin-id ${iTwinId} --member-id ${joinedUser?.id}`);
58+
expect(deletionResult.result).to.not.be.undefined;
59+
expect(deletionResult.result!.result).to.be.equal("deleted");
60+
});
61+
62+
it('Should display owner info of an iTwin in member info', async () => {
3063
const userInfo = await runCommand<User>(`user me`);
3164
expect(userInfo.result).is.not.undefined;
3265

@@ -37,7 +70,7 @@ const tests = () => {
3770
});
3871

3972
it('Should add new member to an iTwin and update the role', async () => {
40-
const newRole = await runCommand<Role>(`access-control role create -i ${iTwinId} -n "Test Role" -d "Test Role Description"`);
73+
const newRole = await runCommand<Role>(`access-control role create -i ${iTwinId} -n "Test Role 2" -d "Test Role Description"`);
4174
expect(newRole.result).is.not.undefined;
4275
expect(newRole.result!.id).is.not.undefined;
4376

integration-tests/formating/formatting.test.ts

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ describe('Command formatting tests', async () => {
1111
userPlugins: false,
1212
});
1313

14-
allCommands = config.commands.filter(command => !command.id.startsWith("plugins") && !command.id.startsWith("help"))
14+
allCommands = config.commands.filter(command => !command.id.startsWith("plugins") && !command.id.startsWith("help") && !command.hidden)
1515
.map((command) =>
1616
({
1717
cmd: command,
@@ -32,12 +32,29 @@ describe('Command formatting tests', async () => {
3232
expect(flag, `Flag '${flagName}' in command '${command.cmd.id}' is missing the 'required' property`).to.have.property('required');
3333

3434
if(flag.type === 'option') {
35-
console.log(flag);
3635
expect(flag, `Flag '${flagName}' in command '${command.cmd.id}' is missing a valid 'helpValue'`).to.have.property('helpValue').to.be.a('string').and.not.be.empty;
3736
}
3837
}
3938
}
4039
});
40+
41+
it('Should ensure all itwin-id flags have env properties', async () => {
42+
for (const command of allCommands) {
43+
const iTwinIdFlag = command.flags.find(([name, _]) => name === "itwin-id");
44+
if (iTwinIdFlag) {
45+
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");
46+
}
47+
}
48+
});
49+
50+
it('Should ensure all imodel-id flags have env properties', async () => {
51+
for (const command of allCommands) {
52+
const iTwinIdFlag = command.flags.find(([name, _]) => name === "imodel-id");
53+
if (iTwinIdFlag) {
54+
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");
55+
}
56+
}
57+
});
4158
});
4259

4360
type CommandWithFlags = {

0 commit comments

Comments
 (0)