Seed Project
Overview
Available since io.Connect Desktop 10.0
The io.Connect seed project is the main approach for delivering, customizing, and packaging io.Connect Desktop. The seed project enables you to control the entire process of creating your own custom distribution package, reducing the time for adopting new platform versions.
The seed project provides the following benefits:
- installing the latest or a specific version of the platform components;
- integrating the platform license key;
- customizing the platform system configuration;
- customizing the platform assets like logos, icons, splash screen, loader, and more;
- customizing various branding features via the CLI configuration (the product name and description, the names and icons of the produced artifacts);
- development and production modes that support different configuration settings;
- templates for the customizable system apps (e.g., Workspaces App, Web Group App, Launchpad);
- adding your own custom apps to be packaged with your platform build;
- test templates for writing and executing your own tests;
- producing artifacts (installers, bundles) for Windows and macOS;
- code signing settings;
- publishing and auto update settings;
- CI/CD integration settings;
The @interopio/iocd-cli library provides an io.Connect CLI which is a necessary tool for creating and managing a seed project.
⚠️ Note that you can use
npxto execute the CLI commands without installing the@interopio/iocd-clilibrary globally. Some of the CLI commands are available as script definitions in thepackage.jsonfile of the@interopio/iocd-clilibrary and can be executed vianpm.
ℹ️ For more details on the available io.Connect CLI commands, see the CLI section.
Requirements
To be able to create and use an io.Connect seed project, the following requirements must be met:
- Node.js v22 or later;
- npm v9 or later;
- OS: Windows 10 or later, macOS 11 or later;
- License: the seed project requires a valid license key for io.Connect Desktop;
- Windows code signing:
signtool.exe(part of the Windows SDK) is required to sign binaries on Windows;
Quick Start
Execute the following steps to quickly setup your seed project.
- Create the seed project:
Use npx to execute the create package command directly:
npx @interopio/iocd-cli@latest createFollow the prompts and provide a product name, select the desired platform components, select which templates to add for customization, and provide a valid license key.
Install the necessary dependencies for the seed project:
npm install- Prepare the project for usage by executing the following command. This will install the selected components and the dependencies for all customizable apps:
npm run setup- To run your seed project in development mode in order to test your platform and your interop-enabled apps, execute the following command:
npm run dev- After customizing and testing your platform project, you can produce build artifacts both for Windows and for macOS by executing the following command:
npm run build- Use the build artifacts to distribute and deploy your platform project.
⚠️ Note that the io.Connect Desktop installation folder can be placed at any location. The io.Connect platform runs independently of its location in the file system. However, you should consider the path length limitations on Windows which allow for a maximum of a 256-character path string. The longest path in the io.Connect Desktop installation is 91 characters. You should consider this when deciding on a location for the installation bundle when using a bundle deployment, or if you decide to move the default installation folder to a new location when using an installer.
Project Structure
The following demonstrates the basic structure of the seed project:
.
├── .agents/ # Agent Skills with project-specific workflows and guidance.
├── apps/ # Contains the customizable system apps. You can place your own apps here to be bundled with the package.
├── components/ # Downloaded platform components.
│ ├── iocd/ # The main platform component (required).
│ └── <other-components> # Additional licensed components (devTools, demos, and more).
├── config/ # Seed project configuration files.
│ ├── forge.config.js # Electron Forge configuration.
│ ├── iocd.cli.config.json # io.Connect CLI configuration.
│ └── iocd.license.key # License key file.
├── modifications/ # Contains all platform customizations (icons, images, and more) and configuration overrides.
│ ├── base/ # Modification that will be applied in all modes (development/build).
│ ├── dev/ # Modifications that will be applied only in development mode.
│ └── build/ # Modifications that will be applied only during production builds.
├── tests/ # Automated tests.
│ └── tests/
│ ├── single-app.spec.ts # Single-app UI example.
│ └── workspace-interop.spec.ts # Workspace and multi-app Interop example.
├── .github/workflows/ # Pre-configured GitHub Actions workflow example for CI/CD automation.
├── package.json
└── README.mdComponents
The following table lists the currently available components that can be installed via the seed project:
| Component | Description | Platforms |
|---|---|---|
"iocd" |
Required. The main io.Connect Desktop app runtime. | Windows, macOS |
"devTools" |
Developer tools for io.Connect Desktop. | Windows, macOS |
"demos" |
A set of web-based demo apps showcasing various features of io.Connect Desktop. | Windows, macOS |
"java-demos" |
A set of Java demo apps showcasing io.Connect Desktop integration. | Windows, macOS |
"bbg" |
Bloomberg Terminal V3 integration adapter. | Windows, macOS |
"bbg-mdf" |
Bloomberg Market Data Feed integration adapter. | Windows |
"excel" |
Microsoft Excel integration adapter for io.Connect Desktop. | Windows |
"fidessa" |
Fidessa integration adapter for io.Connect Desktop. | Windows |
"citrix" |
Citrix integration adapter for io.Connect Desktop. | Windows |
"ibchat" |
IB Chat integration adapter for io.Connect Desktop. | Windows |
"outlook" |
Microsoft Outlook integration adapter for io.Connect Desktop. | Windows |
"word" |
Microsoft Word integration adapter for io.Connect Desktop. | Windows |
"ppt" |
Microsoft PowerPoint integration adapter for io.Connect Desktop. | Windows |
Configuration
Basic Settings
The metadata for your product can be configured in the package.json file:
{
"name": "my-product-slug",
"version": "1.0.0",
"description": "My product description.",
"productName": "My Product Name",
"author": "My Company"
}The values specified in these fields are automatically populated by the io.Connect CLI when you execute the create command.
The following table describes the purpose of each property:
| Property | Type | Description |
|---|---|---|
"author" |
string |
Used as a company name (also used to auto-generate copyright). |
"description" |
string |
Used as a product description in installers and metadata. |
"name" |
string |
Used as a project identifier (e.g., folder name, executable name, package name, and more). |
"productName" |
string |
Used as a human-readable product name displayed to users in UIs. |
"version" |
string |
Used as a version for your platform app. |
The settings for configuring the io.Connect CLI are in the iocd.cli.config.json file located in the /config folder.
The following is a basic configuration for the io.Connect CLI specifying the store from which to download the platform components and the list of components and component versions to retrieve:
{
"components": {
"store": {
"github": {
"repo": "interopio/iocd-components/releases"
}
},
"list": {
"iocd": "latest"
}
}
}ℹ️ For details on all available configuration settings for the io.Connect CLI, see the complete JSON schema for the
iocd.cli.config.jsonfile. The schema is located in the/node_modules/@interopio/iocd-cli/dist/schemasfolder. Add a$schemaproperty to your configuration file to enable IDE autocompletion and validation.
License Key
io.Connect Desktop requires a valid license key to operate.
ℹ️ For details on how to obtain a license key for io.Connect Desktop 10.0, contact us.
During the process of creating the seed project, you will be prompted to provide a valid license key for io.Connect Desktop. Copy and paste your license key when prompted to continue the project creation. If you don't provide a valid license key, you won't be able to create a seed project.
The io.Connect CLI will create automatically an iocd.license.key file located in the /config folder of the seed project. You can use this file to manage your license key later.
⚠️ Note that you must never commit the
iocd.license.keyfile in your project repository to prevent leaking your license key. By default, this file is ignored in the.gitignoresettings.
For the purpose of setting up CI/CD automation, you can also use the IOCD_LICENSE_KEY environment variable to store and obtain the license key from your GitHub secrets during build.
Customization
Modifications
Modifications are the recommended way to customize io.Connect Desktop components without directly editing component files. They allow you to change configurations, add apps, modify assets, and override default behavior while preserving your changes across component updates.
ℹ️ For more details and guides on applying modifications, see the following articles on the Developer Community site:
Key Concepts:
- Modifications are stored separately from components in the
modifications/folder - Changes persist when components are updated or reinstalled
- Mode-specific modifications allow different behavior in dev vs. build mode
- Files are copied or merged into components when running
npm run devornpm run build
Directory Structure:
modifications/
├── base/ # Applied in all modes.
│ └── iocd/
│ ├── config/
│ │ └── apps/
│ └── assets/
├── dev/ # Applied only in dev mode
│ └── iocd/
│ └── config/
└── build/ # Applied only in build mode.
└── iocd/
└── config/Processing Order:
- Base modifications (
modifications/base/) are applied first - Mode-specific modifications (
modifications/dev/ormodifications/build/) are applied second, overriding base when needed
Common Use Cases:
- Replace logos and icons in
modifications/base/iocd/assets/ - Configure system settings in
modifications/base/iocd/config/ - Add app definitions in
modifications/base/iocd/apps/ - Use different URLs for dev (localhost) vs. build (file://) in mode-specific folders
Special File Types:
.json.merge- Deep merges with existing JSON instead of replacing.delete- Removes the corresponding file.replacemarker - Replaces entire directory (place in directory to replace)
Deep Merging with .json.merge Files
The .json.merge extension enables you to selectively modify JSON configuration files without replacing them entirely. This is particularly useful when you want to override specific settings while preserving all other default values.
How It Works
When a file is named with the .json.merge extension (e.g., system.json.merge), it is deep-merged with the corresponding target file (system.json). The merge process:
- Recursively traverses nested objects
- Adds new properties from the merge file
- Overrides existing properties with values from the merge file
- Preserves properties not mentioned in the merge file
Example: Object Deep Merge
If the target system.json contains:
{
"logging": {
"level": "info",
"appender": "default",
"maxFiles": 10
},
"features": {
"autoSave": true,
"notifications": true
}
}And your merge file system.json.merge contains:
{
"logging": {
"level": "debug"
},
"features": {
"devTools": true
}
}The result will be:
{
"logging": {
"level": "debug",
"appender": "default",
"maxFiles": 10
},
"features": {
"autoSave": true,
"notifications": true,
"devTools": true
}
}Notice that:
logging.levelis overridden to"debug"logging.appenderandlogging.maxFilesare preservedfeatures.devToolsis addedfeatures.autoSaveandfeatures.notificationsare preserved
Named Merge Files
You can have multiple merge files (e.g., *.json.merge-*) targeting the same JSON file by adding a suffix after .json.merge-. This lets you organize your modifications into separate, focused merge files:
system.json.merge # Base merge (applied on all platforms)
system.json.merge-autoUpdate # Auto update settings
system.json.merge-launchpad # Launchpad-related overrides
system.json.merge-myFeature # Custom feature settingsAll of the above will merge into system.json. The target file is determined by the part before .json.merge.
When multiple merge files target the same JSON file, modifications/base/ is applied before modifications/<mode>/. Within each directory, files are applied in the order returned by the file system.
Platform-Specific Merge Files
If a merge file suffix contains a platform identifier (darwin or win32), it will only be applied on that platform:
system.json.merge-darwin- Applied only on macOSsystem.json.merge-win32- Applied only on Windowssystem.json.merge-feature1-win32- Applied only on Windows (with custom naming)
Merge files without a platform identifier in the suffix are applied on all platforms.
Array Merging in .json.merge Files
Available since @interopio/iocd-cli 0.0.71.
When using .json.merge files, arrays are concatenated by default with the existing arrays in the target file. This allows you to add new items to existing arrays without losing the original content.
You can control array merge behavior using special directive objects:
| Directive | Behavior | Description |
|---|---|---|
| (default) | Concatenate | Arrays are merged by appending new items to existing ones |
$replace |
Replace entirely | The array is completely replaced with the new values |
$prepend |
Add to beginning | New items are added at the start of the existing array |
$append |
Add to end | Explicitly adds items to the end (same as default) |
$remove |
Remove items | Matching items are removed from the existing array |
Example: Default Array Concatenation
If the target apps.json contains:
{
"apps": ["app1", "app2"]
}And your merge file apps.json.merge contains:
{
"apps": ["app3", "app4"]
}The result will be:
{
"apps": ["app1", "app2", "app3", "app4"]
}Example: Replacing an Array
To completely replace an array instead of concatenating, use the $replace directive:
{
"apps": { "$replace": ["only-this-app"] }
}This is useful when you want to override a list entirely rather than extend it.
Example: Prepending to an Array
To add items at the beginning of an existing array, use the $prepend directive:
{
"startupScripts": { "$prepend": ["init.js", "setup.js"] }
}This ensures your items appear before any existing items in the array.
Example: Removing Items from an Array
To remove specific items from an existing array, use the $remove directive:
{
"disabledPlugins": { "$remove": ["legacy-plugin", "deprecated-feature"] }
}For objects within arrays, the removal uses deep comparison:
{
"apps": { "$remove": [{ "name": "unwanted-app" }] }
}Combining Object and Array Merging
You can combine object deep merging with array directives in the same merge file:
{
"logging": {
"level": "debug"
},
"features": {
"enabledPlugins": ["my-plugin"],
"disabledPlugins": { "$remove": ["old-plugin"] }
},
"startupApps": { "$prepend": ["dashboard"] }
}This example:
- Overrides
logging.levelto"debug" - Concatenates
"my-plugin"tofeatures.enabledPlugins - Removes
"old-plugin"fromfeatures.disabledPlugins - Prepends
"dashboard"to thestartupAppsarray
System Configuration
The io.Connect Desktop system configuration (the system.json file) controls platform-wide settings like auto-updates, logging, and feature flags. To customize these settings, create modification files that merge with the default configuration.
Location:
modifications/base/iocd/config/system.json.mergeThe following example demonstrates how to enable auto updates:
{
"autoUpdater": {
"enabled": true,
"updateSource": {
"type": "Service",
"baseUrl": "https://updates.yourcompany.com"
},
"updateIntervalMinutes": 60
}
}The following example demonstrates how to configure the system logging:
{
"logging": {
"level": "info",
"appender": "default"
}
}Mode-Specific Configurations
Using different settings for development and production build mode:
Dev mode (modifications/dev/iocd/config/system.json.merge):
{
"logging": {
"level": "debug"
},
"features": {
"devTools": true
}
}Build mode (modifications/build/iocd/config/system.json.merge):
{
"logging": {
"level": "error"
},
"features": {
"telemetry": true
}
}When modifying the system configuration, consider the following:
- Use
.mergeextension to deep merge with existing configuration. - Without
.merge, the file completely replaces the original. - Changes are applied when running
npm run devornpm run build. - Environment variables like
${PRODUCT_VERSION}are automatically expanded.
Icons & Images
The native app icon, tray icon, and platform branding images are controlled via the modifications/base/iocd/assets/images/ folder. Create the images/ folder if it doesn't exist, and add the image files you want to override.
Use the exact file names shown below, because the CLI, Electron Forge configuration, and platform runtime look for these paths during development and packaging.
Native App Icons
| File | Purpose | Requirements |
|---|---|---|
logo.ico |
Windows executable icon. The CLI embeds it into the main executable during npm run dev and npm run build. The Electron Forge Squirrel maker also uses it as the setup executable icon. |
Use a Windows .ico file with multiple sizes, such as 16x16, 32x32, 48x48, and 256x256. |
logo.icns |
App icon for macOS. The CLI copies it into the app bundle's Contents/Resources/ folder and sets CFBundleIconFile to logo.icns. The Electron Forge DMG maker also uses it for the generated .dmg. |
Use a macOS .icns file with multiple sizes. You can create it with Icon Composer or iconutil. |
logo.png |
Image asset used by the platform UI and branding. | Use a PNG file, recommended size 512x512. This doesn't replace the native Windows .exe or macOS .app icon by itself. |
On Windows, close any running instance of the app before running the command again, because the CLI has to patch the executable icon and metadata. If the Windows executable icon is stored somewhere else, update the win.exe.exeIconPath setting in config/iocd.cli.config.json or set the WIN_EXE_ICON_PATH environment variable. The default path is recommended because it also keeps the Squirrel installer icon aligned with the app icon.
On macOS, keep the file name logo.icns. The CLI expects this name when it updates the app bundle and signs the result.
Tray Icons
modifications/base/iocd/assets/
└── images/
├── tray-light.ico
├── tray-dark.ico
├── tray.ico
├── tray-alert-light.ico
├── tray-alert-dark.ico
├── tray-alert.ico
├── logo-alert.ico
├── trayTemplate.png
├── trayTemplate@2X.png
├── tray-alertTemplate.png
└── tray-alertTemplate@2x.pngOn Windows, the tray icon changes with the current platform theme:
| State | Lookup order |
|---|---|
| Light theme | tray-light.ico, then tray.ico, then logo.ico |
| Dark theme | tray-dark.ico, then tray.ico, then logo.ico |
| Light theme with active notifications | tray-alert-light.ico, then tray-alert.ico, then logo-alert.ico, then the current tray icon |
| Dark theme with active notifications | tray-alert-dark.ico, then tray-alert.ico, then logo-alert.ico, then the current tray icon |
On macOS, the platform uses template PNG files instead of .ico files:
| State | Lookup order | High-DPI companion |
|---|---|---|
| Default | trayTemplate.png |
trayTemplate@2X.png |
| Active notifications | tray-alertTemplate.png, then the current tray icon |
tray-alertTemplate@2x.png |
The alert tray icon is used only when the platform has active notifications, the notification badge is enabled, and the tray notifications item is enabled. The tray section in system.json controls whether the tray menu and its items are shown; it doesn't provide custom icon file names.
After replacing any of these files, run the platform again or create a new package:
npm run dev
# or
npm run buildApps
io.Connect Desktop includes several built-in apps that you can replace with your own custom versions:
- Workspaces - Workspace management UI
- Groups - Window grouping UI
- Splash Screen - Platform splash screen
- Launchpad - App launcher
- Notifications - Notification center
Apps in the apps/ folder allow you to customize these built-in apps or add entirely new functionality. This approach keeps your core platform apps in the same repository, eliminating the need to manage them across multiple repositories and simplifying version control, deployment, and coordination.
Using Template Apps
Template apps can be selected during project creation or added later:
# Add a template app
npx @interopio/iocd-cli@latest apps add workspaces
# List available templates
npx @interopio/iocd-cli@latest apps listAvailable templates:
workspaces- Workspace managementgroups- Window groupingsplash- Loading screenlaunchpad- App launchertests- Automated testing framework (automatically included in new projects)
Once added, build your implementation on top of the template in the apps/ folder.
Creating Custom Apps
For custom apps, create a folder in apps/ with an iocd.app.json file:
apps/
└── my-custom-app/
├── iocd.app.json # Defines modifications and build behavior
├── package.json
└── src/Example iocd.app.json:
In this example:
- Dev mode (triggered by
npm run dev): Executes astartscript (typically starts a dev server with hot-reload) and copies the development app configuration so the app definition points to the dev server - Build mode (triggered by
npm run build): Executes abuildscript (typically builds optimized production assets) and copies the final built assets to the modifications folder
{
"dev": {
"script": "start",
"modifications": [
{
"source": "/config/my-app-dev.json",
"destination": "/modifications/dev/iocd/config/apps/my-app.json"
}
]
},
"build": {
"script": "build",
"modifications": [
{
"source": "/dist/",
"destination": "/modifications/build/iocd/assets/my-app"
}
]
}
}The iocd.app.json file defines:
- base: Array of modifications applied in all modes
- dev: Object with optional
script(npm script name) andmodificationsarray - build: Object with optional
script(npm script name) andmodificationsarray
Managing Apps
# Install app dependencies
npx @interopio/iocd-cli@latest apps install
# Start apps in dev mode
npm run apps-dev
# Build apps for production
npx @interopio/iocd-cli@latest apps buildDevelopment
Development mode allows you to quickly start io.Connect Desktop with all your configured changes and apps, enabling rapid iteration and testing during development.
When you run dev mode, the CLI will:
- Apply all relevant modifications - Base modifications and dev-specific modifications are copied to components
- Launch the platform - Starts the platform with all changes applied
npm run dev⚠️ Note that in order to run any custom apps you may have in the
/appsfolder, you must runnpm run apps-devin a separate terminal to start them. If you don't start them, io.Connect Desktop may fail to start.
To pass additional command line arguments to the launched io.Connect Desktop process, use the --extraLaunchArgs option:
npm run dev -- --extraLaunchArgs "--inspect --someFlag"Installing & Updating Components
To install a component or update an existing component while in development mode, use the following command:
npx @interopio/iocd-cli@latest components install <component-name>This command will download and install the latest version of the specified component.
If you want to install a specific version, use:
npx @interopio/iocd-cli@latest components install <component-name>@<version>Using Pre-Releases
You can also use pre-release versions of the platform components via the io.Connect CLI. The list of all published releases is available at https://releases.interop.io/components.html.
To enable using a pre-release version, set the "usePrereleases" property to true in the "components.store.github" object in the config/iocd.cli.config.json file:
{
"components": {
"store": {
"github": {
"repo": "interopio/iocd-components/releases",
"usePrereleases": true
}
}
}
}After enabling the flag, install the desired pre-release version by specifying the exact version:
npx @interopio/iocd-cli@latest components install iocd@10.1.0⚠️ Note that pre-release versions are intended for testing and evaluation purposes. They may contain experimental features or changes that aren't yet fully stable. It's recommended to use stable releases for production environments.
Pre-Release Channels
Available since @interopio/iocd-cli 0.0.80.
Pre-release versions use channel tags to indicate their stability level:
build- Build pre-releases (e.g.,v10.0.0-build.20250927121929). These are automated build artifacts.dev- Development pre-releases (e.g.,v10.0.0-dev.260314T1935). These are development snapshots.nightly- Nightly pre-releases (e.g.,v10.0.0-nightly.260319T0800). These are nightly build snapshots.
When "usePrereleases" is true and you use "latest" as the version, the CLI will consider all pre-release channels by default. You can restrict which channels are allowed for "latest" resolution using the "prereleaseChannels" property:
{
"components": {
"store": {
"github": {
"repo": "interopio/iocd-components/releases",
"usePrereleases": true,
"prereleaseChannels": ["build"]
}
},
"list": {
"iocd": "latest"
}
}
}In this example, resolving "latest" will only consider stable releases and -build pre-releases. The -dev and -nightly channels will be excluded.
You can still install a specific pre-release version from any channel, regardless of the "prereleaseChannels" setting:
npx @interopio/iocd-cli@latest components install iocd@10.1.0-nightly.260319T0800⚠️ The
"prereleaseChannels"setting only applies to"latest"resolution. It is ignored when"usePrereleases"isfalse. When"usePrereleases"isfalse, no pre-release versions are available at all — not even for explicit version requests.
Downloading Components to a Local Store
Available since @interopio/iocd-cli 0.0.76.
You can download components from a remote store (GitHub or S3) to a local directory using the download command. By default, the command downloads the latest version of each component:
npx @interopio/iocd-cli@latest components download ./my-local-storeTo download only specific components, use the --components option with a semicolon-separated list. You can specify exact versions with @<version> or use latest:
npx @interopio/iocd-cli@latest components download ./my-local-store --components "iocd@10.0.3;devTools@latest"To overwrite existing files without prompting, use the --force flag:
npx @interopio/iocd-cli@latest components download --force ./my-local-storeThe downloaded components are saved in the local store format and can be used as a local component store by configuring the "components.store.local" property in config/iocd.cli.config.json.
Testing
Available since @interopio/iocd-cli 1.0.0.
The seed project includes an end-to-end (E2E) setup for testing your customized io.Connect Desktop platform and the apps running in it. The setup starts a real platform process in an isolated test environment and enables tests to use both browser automation and the io.Connect APIs.
The generated test setup combines TypeScript, WebdriverIO, Mocha, and Chai with @interopio/wdio-iocd-service. By default, specifications run in the WebdriverIO worker via the platform-wdio runner. The service starts a dedicated io.Connect Desktop instance and connects the test process to it. Tests can:
- start apps and use io.Connect APIs;
- switch the WebdriverIO browser context to an app window;
- interact with the app UI and inspect its DOM;
- test communication and workflows involving multiple apps;
- import and restore Layouts and Workspaces;
- verify platform configuration and behavior.
These tests complement, but don't replace, unit tests. Use unit tests for isolated app logic and E2E tests for behavior that depends on a running io.Connect Desktop platform, actual app windows, io.Connect APIs, platform configuration, or communication between apps.
The paths in this section assume the default testsDirectory value of "tests". If you set a custom testsDirectory in config/iocd.cli.config.json, adjust the paths accordingly. Unless noted otherwise, execute all commands from the seed root after installing the seed dependencies.
ℹ️ For a seed project, use the versions and configuration generated in its
tests/package. Configuration examples for installing@interopio/wdio-iocd-servicedirectly outside a seed may target a different package version and lifecycle.
Architecture
The following illustrates the test lifecycle:
npm test
│
└── iocd test
│
├── Locates the seed test package and installs its dependencies when necessary.
├── Resolves the executable from the configured iocd component.
├── Makes the seed license available to the test platform.
└── Starts WebdriverIO in the tests package.
│
└── @interopio/wdio-iocd-service
│
├── Selects the ChromeDriver version matching the platform Electron version.
├── Uses @interopio/iocd-launcher to start io.Connect Desktop.
├── Connects WebdriverIO and the test io.Connect API to the platform.
├── Runs the test specifications.
└── Stops ChromeDriver and io.Connect Desktop.With the default platform-wdio runner, each specification runs in the WebdriverIO worker — a separate Node.js process, not the npm test/iocd test launcher process and not the app windows. Your describe/it/expect and the io API run in that worker, while $ and browser drive the io.Connect Windows remotely over ChromeDriver.
The setup combines public testing tools with libraries provided by interop.io:
| Tool | Source | Responsibility |
|---|---|---|
| TypeScript | Public | Test language and static type checking. |
| WebdriverIO | Public | Browser automation, selectors, element interaction, waits, and browser state inspection. |
| Mocha | Public | Test suites, test cases, and setup and cleanup hooks. |
| Chai | Public | Assertions for API values and app state. |
@interopio/iocd-cli |
interop.io | Owns the seed test infrastructure, resolves the seed platform executable and license, and starts the test package. |
@interopio/wdio-iocd-service |
interop.io | Integrates io.Connect Desktop with WebdriverIO, manages ChromeDriver, exposes the test io.Connect API, and adds browser commands specific to io.Connect Desktop. |
@interopio/iocd-launcher |
interop.io | Resolves, starts, monitors, and stops the io.Connect Desktop process. It is used internally by the WebdriverIO service. |
@interopio/desktop |
interop.io | Provides the io.Connect APIs used from tests. |
@interopio/workspaces-api |
interop.io | Adds the Workspaces API to the test io.Connect API. Other optional io.Connect libraries can be added in the same way. |
The io.Connect CLI and the WebdriverIO service have separate responsibilities. The CLI knows the structure and configuration of the seed project. The service knows how to create a WebdriverIO test session for io.Connect Desktop. Tests should normally be started via the CLI rather than by invoking WebdriverIO directly.
Test Project Structure
The test setup is automatically included in newly generated seed projects:
.
├── .agents/
│ └── skills/
│ └── iocd-wdio-testing/
│ ├── SKILL.md # Instructions for compatible coding agents.
│ └── references/ # Setup, authoring, configuration, and troubleshooting guidance.
├── config/
│ ├── iocd.cli.config.json
│ └── iocd.license.key
├── tests/ # Separate Node.js test package.
│ ├── package.json
│ ├── package-lock.json
│ ├── tsconfig.json
│ ├── wdio.config.ts # WebdriverIO and io.Connect Desktop service configuration.
│ ├── config/
│ │ └── system.json # Platform overrides used only by tests.
│ └── tests/
│ ├── assets/
│ │ └── applications/ # Static fixture apps used by the specifications.
│ ├── common/
│ │ └── iocd.ts # Polling, event, and cleanup helpers.
│ ├── config/
│ │ └── apps.json # Definitions for the fixture apps.
│ ├── single-app.spec.ts # Single-app UI example.
│ ├── channel-selector.spec.ts # Built-in Channel Selector UI example.
│ ├── shared-contexts.spec.ts # Cross-app Shared Contexts example.
│ ├── workspace-interop.spec.ts # Workspace and multi-app Interop example.
│ ├── interop.spec.ts # Test-driven Interop request/response example.
│ ├── notifications.spec.ts # Notification Panel example.
│ └── workspace-add-app.spec.ts # Add an app to a Workspace via the UI popup.
└── package.jsonThe top-level tests/ folder is a separate Node.js package. Paths passed to the underlying WebdriverIO runner are resolved from this package. For example, the root command npm test -- --spec ./tests/single-app.spec.ts selects <seed>/tests/tests/single-app.spec.ts.
The test system.json registers tests/tests/config/apps.json as a path app store and uses an isolated user data folder. Test runs therefore don't modify the regular user profile of the installed or development platform. It is applied on top of the platform configuration your seed already defines - see Configuration & Isolation.
⚠️ The io.Connect CLI generates the package manifests, TypeScript and WebdriverIO configuration, default fixtures, and examples. The setup command refuses to overwrite an existing test directory. Back up or remove the directory explicitly before creating a fresh test package.
Setting Up Tests
New Seed Project
The test infrastructure is included automatically in a new seed project. Complete the regular seed setup first:
npm install
npm run setupRun all tests from the seed root:
npm testOn the first run, the CLI installs the dependencies of the nested test package if they aren't already installed. It then resolves the executable from the configured iocd component and supplies its path to the WebdriverIO configuration via the BINARY_PATH environment variable.
If the platform executable isn't available yet, start the seed in development mode once. Close the development platform before running the tests, because the WebdriverIO service starts and manages a separate platform instance:
npm run dev
# After verifying that the platform starts, close it and then run the tests.
npm testThe CLI also makes the license from config/iocd.license.key available to the isolated test platform. Don't include the license value in specifications, test configuration, logs, or source control outside the seed's established license handling.
Missing Test Infrastructure
To add the current test infrastructure to a seed where the test package is missing, execute the following from the seed root:
npx @interopio/iocd-cli@latest test setupThe command creates the test package and agent skill, installs the test dependencies, and validates the setup with the TypeScript compiler.
Replacing an Existing Test Package
The CLI doesn't overwrite or migrate an existing test directory. If you want to replace an older test package with the current setup:
- Inspect the existing directory for specifications, assets, app definitions, configuration, scripts, or dependencies that must be retained.
- Back up the directory, or remove it if it contains only generated files you don't need.
- Run the setup command from the seed root:
npx @interopio/iocd-cli@latest test setup- Transfer any required user-authored files from the backup to the new test package and adapt them to the current configuration.
- Type-check the package and run the transferred specifications individually.
If the configured test directory already exists, test setup stops without changing it and instructs you to back it up or remove it first.
Running Tests
Run commands from the seed root so the CLI can prepare the platform executable and license:
# Run all specifications.
npm test
# Run one specification while developing or debugging it.
npm test -- --spec ./tests/single-app.spec.ts
# Run another specific test file.
npm test -- --spec ./tests/workspace-interop.spec.tsArguments after -- are forwarded to WebdriverIO. For all available arguments, see the WebdriverIO CLI documentation.
To type-check the specifications without starting the platform, execute the following from the test package:
cd tests
npm run typecheck⚠️ Invoking
npx wdio run ./wdio.config.tsdirectly from the test package isn't the normal seed workflow. A direct invocation bypasses the CLI preparation and requires you to provide a validBINARY_PATHand equivalent license setup manually.
Testing a Single App
The generated single-app.spec.ts demonstrates the basic test workflow. Its fixture app contains a button that increments a counter. The test starts the app in io.Connect Desktop, switches WebdriverIO to the app window, clicks the button, and verifies the resulting app state:
import { expect } from "chai";
import type { IOConnectDesktop } from "@interopio/desktop";
import { $, browser } from "@wdio/globals";
describe("IOCD seed", () => {
let instance: IOConnectDesktop.AppManager.Instance | undefined;
afterEach(async () => {
await instance?.stop().catch(() => undefined);
instance = undefined;
});
it("increments the counter when the button is pressed", async () => {
const io = await browser.io();
instance = await io.appManager.application("web")!.start();
const window = await instance.getWindow();
await browser.switchById(window.id);
await $("#increment-counter").waitAndClick();
const counter = await $("#counter");
await browser.waitUntil(async () => (await counter.getText()) === "1", {
timeoutMsg: "Expected the counter to increment to 1"
});
expect(await counter.getText()).to.equal("1");
});
});The example contains the main building blocks for an app UI test:
- Obtain the io.Connect API with
const io = await browser.io()inside a test or hook. - Start the registered app via an io.Connect API.
- Retrieve the app window and pass its ID to
browser.switchById(). - Use standard WebdriverIO selectors and element commands to drive the UI.
- Wait for observable state before asserting with Chai.
- Stop the app in
afterEach()even when a test fails.
To test one of your own apps, register or expose the app to the test platform, use stable selectors in its UI, and add a focused *.spec.ts file under tests/tests/.
Testing Multiple Apps
The generated workspace-interop.spec.ts demonstrates a more complete integration workflow involving two apps:
- The test imports a Workspace Layout containing the
people-listandperson-detailsfixture apps. - It restores the Workspace and force-loads both app windows.
- It switches WebdriverIO to the list app and selects one of five names.
- The list app invokes the
IOCD.Sample.DisplayPersonInterop method. - The details app handles the invocation and displays the selected name.
- The test switches to the details app and verifies its DOM.
- The cleanup hook closes the Workspace and removes the imported Layout.
The following excerpt shows the essential test flow:
const io = await browser.io();
workspace = await io.workspaces!.restoreWorkspace(layoutName);
const workspaceWindows = workspace.getAllWindows();
const peopleListWindow = workspaceWindows.find(window => window.appName === "people-list");
const personDetailsWindow = workspaceWindows.find(window => window.appName === "person-details");
expect(peopleListWindow, "people-list workspace window").to.exist;
expect(personDetailsWindow, "person-details workspace window").to.exist;
await Promise.all([
peopleListWindow!.forceLoad(),
personDetailsWindow!.forceLoad()
]);
await browser.switchById(peopleListWindow!.getGdWindow().id);
const people = $$("[data-person-name]");
const peopleCount = await people.length;
const selectedPerson = await people[Math.floor(Math.random() * peopleCount)];
const expectedName = await selectedPerson.getText();
await selectedPerson.waitForEnabled();
await selectedPerson.click();
await browser.switchById(personDetailsWindow!.getGdWindow().id);
const displayedPerson = await $("#selected-person");
await browser.waitUntil(async () => (await displayedPerson.getText()) === expectedName);
expect(await displayedPerson.getText()).to.equal(expectedName);Fixture app definitions are located in tests/tests/config/apps.json, and their static assets are located under tests/tests/assets/applications/. Add test-only apps there when a workflow requires controlled app behavior. Keep production app definitions and test fixtures separate unless the purpose of the test is to exercise the actual production app package.
The Workspaces API is an optional library. The generated configuration installs @interopio/workspaces-api and lists it in iocd:options.ioInitOptions.libraries. To use another optional io.Connect library:
- Add the package to
tests/package.json. - Add its module specifier to
iocd:options.ioInitOptions.librariesintests/wdio.config.ts. - Access the resulting API via the runtime
ioobject.
Invoking Interop Methods from a Test
The generated interop.spec.ts shows the request/response Interop pattern driven from the test process itself, rather than only between two apps. The test starts the interop-responder fixture app, which registers the IOCD.Sample.Add Interop method, waits for the method to be published, invokes it, and asserts the returned value:
const io = await browser.io();
instance = await io.appManager.application("interop-responder")!.start();
// The app registers the method asynchronously, so wait for it before invoking.
await waitFor(() =>
io.interop.methods().some(method => method.name === "IOCD.Sample.Add")
);
const result = await io.interop.invoke("IOCD.Sample.Add", { a: 2, b: 3 });
expect((result.returned as { sum: number }).sum).to.equal(5);The waitFor() helper is generated in tests/tests/common/iocd.ts. Use the same wait-for-registration pattern whenever a test invokes a method that an app registers asynchronously.
Testing the Channel Selector
The generated channel-selector.spec.ts drives the built-in Channel Selector from the caption of the channel-client fixture app. The test reads the configured Channels with io.channels.all() and selects the first one, so it works with whatever Channel set your seed defines. The fixture app enables the Channels API with IODesktop({ channels: true }) and displays its current Channel whenever onChannelsChanged() fires.
The Channel Selector is part of the Web Group UI, rather than the app DOM. The test therefore uses the Group model exported by @interopio/wdio-iocd-service/models to reach the caption control:
const io = await browser.io();
const [channelName] = await io.channels.all();
instance = await io.appManager.application("channel-client")!.start({}, { mode: "flat" });
const window = await instance.getWindow();
await browser.switchById(window.id);
await browser.waitUntil(async () => (await $("#channel-status").getText()) === "Ready");
const group = new Group(window.groupId!);
const [frame] = await group.frames.waitForCount(1);
const channelSelector = frame.captionBar.channelSelector;
await group.switchTo();
await channelSelector.waitForDisplayed();
await channelSelector.click();
await switchToChannelSelectorPopup();
await $(`[data-testid="channel-selector-channel-${channelName}"]`).waitAndClick();The selector popup is a separate web window, so the generated switchToChannelSelectorPopup() helper scans the available WebDriver handles and switches to the one containing the selector panel. After selecting a Channel, the test switches back to the app with browser.switchById() and waits until the app UI displays the selected Channel. This verifies the user interaction and its effect on the client app, rather than only asserting built-in UI markup.
The example assumes single Channel mode. Channel mode is a platform-wide setting defined by your seed configuration, so testing single and multi Channel behavior requires separate platform launches with the corresponding configuration. See Testing Multiple Platform Configurations.
Testing Shared Contexts
The generated shared-contexts.spec.ts demonstrates data sharing between two instances of the shared-context-client fixture app. Each instance subscribes to the same Shared Context. The test clicks a button in the publisher instance, which calls io.contexts.set(), then switches to the subscriber instance and verifies that it rendered the update:
const io = await browser.io();
const app = io.appManager.application("shared-context-client")!;
const publisher = await app.start({}, { title: "Context Publisher" });
const subscriber = await app.start({}, { title: "Context Subscriber" });
const publisherWindow = await publisher.getWindow();
const subscriberWindow = await subscriber.getWindow();
// Wait until the receiving app has subscribed before publishing the update.
await browser.switchById(subscriberWindow.id);
await browser.waitUntil(async () => (await $("#context-status").getText()) === "Ready");
await browser.switchById(publisherWindow.id);
await $("#share-client").waitAndClick();
await browser.switchById(subscriberWindow.id);
await browser.waitUntil(async () => (await $("#selected-client").getText()) === "Ada Lovelace");Subscribe before triggering an update so the receiving app can't miss it. Shared Contexts are persistent platform state, so the example destroys its test context before and after each test and stops both app instances during cleanup.
Testing Notifications
The generated notifications.spec.ts raises a notification and verifies it in the Notification Panel. It is the sample that uses the panel-specific service commands browser.switchToNotificationPanel() and element.waitForChildrenCount():
const io = await browser.io();
await io.notifications.configure({ enable: true });
await io.notifications.raise({ title: "Trade executed", body: "AAPL 100 @ 190.25" });
// switchToNotificationPanel() only switches context, so open the panel first.
await io.notifications.panel.show();
await browser.switchToNotificationPanel();
const list = await $(".io-notification-list");
await list.waitForChildrenCount(".io-notification", 1);
const title = await $(".io-notification .io-notification-body-content .io-title");
expect(await title.getText()).to.equal("Trade executed");Notifications are disabled on a fresh platform, so the test enables them with io.notifications.configure() before raising anything and clears them in afterEach().
Adding an App to a Workspace via the UI
The generated workspace-add-app.spec.ts adds an app to a Workspace by driving the Workspaces App UI with WebdriverIO clicks - the frame's add button and the add-application popup - instead of calling the workspace.addWindow() API. It relies on the platform running in universal mode, where the popup renders inside the frame window rather than in a separate window. Check the mode your seed configures and adapt the specification to it, instead of changing the mode via the test overrides:
const io = await browser.io();
workspace = await io.workspaces!.createWorkspace({ children: [] });
await browser.switchById(workspace.frameId);
// Open the add-application popup from the empty container's add button.
await $(".wsp-empty-container button.add-button").waitAndClick();
// Filter the popup and click the target app row.
await $(".add-application #appSearch").setValue("Workspace Widget");
await $(
".add-application .applicationsList .list-group-item, .add-application li.io-list-item"
).waitAndClick();
// The click adds the app via the real UI; confirm via the Workspaces API.
const current = (await io.workspaces!.getAllWorkspaces()).find(w => w.id === workspace!.id)!;
expect(current.getAllWindows()[0].appName).to.equal("workspace-widget");The workspace-widget fixture app is marked with customProperties.includeInWorkspaces, so it appears in the add-application popup. The popup DOM belongs to the Workspaces App, so its selectors can change between platform versions; prefer the workspace.addWindow() API when you only need to arrange Workspace state rather than exercise the popup UI itself.
Available Test APIs
WebdriverIO
Use standard WebdriverIO APIs for browser automation:
$()and$$()for locating elements;- element commands such as
click(),setValue(),getText(),waitForDisplayed(), andwaitForEnabled(); - browser commands such as
waitUntil()andgetWindowHandles(); - WebdriverIO configuration hooks, reporters, and CLI filters.
For the complete API, see the WebdriverIO documentation.
Test Framework and Assertions
The generated setup uses Mocha and Chai:
describe()groups related behavior;it()defines a test case;beforeEach()creates isolated state for each test;afterEach()cleans resources even after a failed test;expect()from Chai verifies API values and app state.
Import the Chai assertion explicitly:
import { expect } from "chai";io.Connect APIs
The WebdriverIO service creates the test io.Connect API before runtime hooks and test cases execute. Obtain it inside a test or hook with browser.io():
const io = await browser.io();Call browser.io() only inside it(), beforeEach(), afterEach(), or other runtime hooks. The io.Connect API isn't available while Mocha is evaluating a describe() callback.
Use the io.Connect APIs for behavior that belongs to the platform rather than the browser, for example:
- starting and stopping apps;
- invoking or registering Interop methods;
- reading and updating Shared Contexts or Channels;
- opening and managing windows;
- importing, restoring, and removing Layouts;
- creating and manipulating Workspaces.
The fully resolved platform configuration is available synchronously as globalThis.ioCDPlatformConfig when configuration-dependent test suites must be created at describe() time.
WebdriverIO Commands for io.Connect Desktop
IOCDService() adds commands to the WebdriverIO browser and element objects before the specifications execute. These commands complement the standard WebdriverIO API with operations for accessing the test io.Connect API, selecting io.Connect Windows, controlling the clock in the active app window, and waiting for common UI conditions.
The browser commands available in the generated platform-wdio setup are:
| Command | Description |
|---|---|
browser.io() |
Returns the test io.Connect API in the platform-wdio runner. Call it inside a test or hook, then use the returned io object. |
browser.switchById(targetId) |
Switches the active WebDriver context to an app window, Web Group, or Workspace frame with the specified io.Connect Window ID. |
browser.switchToLaunchpad() |
Switches the active WebDriver context to the running Launchpad window. |
browser.switchToNotificationPanel() |
Switches the active WebDriver context to the open Notification Panel window. |
browser.travelTime(offsetMs) |
Shifts Date and Date.now() by the specified number of milliseconds in the active browser context. |
browser.restoreTime() |
Restores the original Date implementation in the active browser context after travelTime(). |
Accessing the Test io.Connect API
The browser.io() command returns the test io.Connect API:
const io = await browser.io();
const interopMethods = io.interop.methods();The API is available at test runtime after the service has connected to the platform. Call browser.io() inside each test or hook that needs the io.Connect APIs.
Switching Between io.Connect Windows
Standard WebdriverIO commands operate on one active WebDriver window handle. The io.Connect APIs, however, identify app windows and platform UI windows with io.Connect Window IDs. The browser.switchById() command maps an io.Connect Window ID to its WebDriver handle and makes that window the active context.
Use it after starting an app and obtaining its io.Connect Window:
const io = await browser.io();
const instance = await io.appManager.application("web")!.start();
const window = await instance.getWindow();
await browser.switchById(window.id);
await $("#increment-counter").waitAndClick();The target may also be an underlying window from a Workspace:
const workspaceWindow = workspace.getAllWindows()[0];
await workspaceWindow.forceLoad();
await browser.switchById(workspaceWindow.getGdWindow().id);The command scans the available WebDriver handles until it finds the io.Connect Window ID and waits up to five seconds by default. It throws if the target isn't found. It doesn't start an app, restore a Workspace, or open a platform UI window; the target must already exist. The target must also be a web-based io.Connect Window exposed to ChromeDriver. This command doesn't provide UI automation for native app windows.
The browser.switchToLaunchpad() and browser.switchToNotificationPanel() commands are shortcuts for platform UI windows whose app names are known to the service:
await browser.switchToLaunchpad();
// Interact with elements in the Launchpad.
// Open the Notification Panel via the app workflow first.
await browser.switchToNotificationPanel();
// Interact with elements in the Notification Panel.These commands only switch the active context: switchToLaunchpad() requires the Launchpad to be running, and switchToNotificationPanel() requires the Notification Panel to be open. Both commands throw if the respective window isn't available.
Controlling Time in an App Window
The browser.travelTime() command patches the JavaScript Date constructor and Date.now() in the active browser context. The offset is in milliseconds and may be positive or negative. Use it for UI behavior based on the current date or time:
await browser.switchById(window.id);
try {
await browser.travelTime(5 * 60 * 1000);
// Verify UI behavior five minutes in the future.
} finally {
await browser.restoreTime();
}Time travel affects only the active browser context. It doesn't change the platform clock, other app windows, or the scheduling of timers such as setTimeout(). Restore time before switching away from the window, or switch back to the affected window before calling browser.restoreTime().
Waiting for Elements
The service also adds the following element commands:
| Command | Description |
|---|---|
element.waitAndClick(options?) |
Waits for the element to exist, be displayed, and be clickable, and then clicks it. The options.timeout value defaults to 5000 milliseconds. |
element.waitForChildrenCount(selector, count, options?) |
Waits until the element has exactly count child elements matching selector. The options.timeout value defaults to 5000 milliseconds. |
For example:
await $("#save").waitAndClick({ timeout: 10000 });
const orders = await $("#orders");
await orders.waitForChildrenCount("li", 5, { timeout: 10000 });Both commands use WebdriverIO waits and throw a timeout error when the expected condition isn't reached.
Built-In io.Connect Desktop UI Models
The @interopio/wdio-iocd-service/models entry point provides page objects for built-in web UI owned by io.Connect Desktop, such as Web Groups, caption bars, tabs, Channel Selectors, and window controls. Use ordinary WebdriverIO selectors for UI owned by your client apps.
The models encapsulate generated element IDs, internal DOM structure, WebDriver context switching, and common waits behind semantic operations such as group.switchTo(), group.frames.waitForCount(), tab.close(), and waitForDisplayed(). Prefer an available model over copying internal platform selectors into a specification.
The service version currently generated in the seed exports the following models and types:
| Area | Models and Types |
|---|---|
| Group and frames | Group, Frame, WebGroupElement |
| Captions and tabs | CaptionBar, Caption, Editor, MoveArea, TabBar, TabHeader, Tab, Tabs, PinnedTabs |
| Caption controls | ChannelSelector, CloseButton, HtmlButtons, ButtonsContainer, StandardButton, StandardButtons, CustomButton, CustomButtons |
| Sizing and snapping | SizingBorderGroup, SizingBorderOrientation, SizingBorder, SizingBorderType, ApproachingMarker, and the NeighborFrames type |
Import the models from the package subpath:
import { Group } from "@interopio/wdio-iocd-service/models";
const group = new Group(window.groupId!);
const [frame] = await group.frames.waitForCount(1);
await group.switchTo();
await frame.captionBar.channelSelector.waitForDisplayed();
await frame.captionBar.channelSelector.click();Model exports vary by @interopio/wdio-iocd-service version. The installed package is the source of truth; inspect tests/node_modules/@interopio/wdio-iocd-service/dist/models/index.d.ts before using a model. Newer service versions may add models such as ChannelSelectorPopup, PlatformDialog, ModalsDialog, or Alert, but these aren't exported by the service version currently generated in the seed.
Models reduce direct coupling to built-in markup, but remain coupled to the matching io.Connect Desktop and WebdriverIO service versions. If no suitable model exists, prefer stable data-testid, role, or other semantic selectors and isolate any version-sensitive built-in selector in a local page object.
Common Patterns
Wait for App Readiness
The instance.getWindow() method resolves when the app window is available, but this doesn't guarantee that the app has finished initializing. The app may still be initializing IODesktop(), loading optional libraries, registering Interop methods, subscribing to Channels or Shared Contexts, attaching event handlers, or rendering its initial UI.
Before interacting with the app, wait for an observable, app-owned signal that proves the functionality required by the next action is ready. Suitable signals include:
- a dedicated DOM status or element;
- an action control becoming enabled;
- an Interop method appearing in
io.interop.methods(); - a subscription-ready state exposed by the app;
- another observable API state directly related to the behavior under test.
The generated Channel Selector example waits until the fixture app has initialized the Channels API and registered its membership listener:
const window = await instance.getWindow();
await browser.switchById(window.id);
await browser.waitUntil(async () => (await $("#channel-status").getText()) === "Ready", {
timeoutMsg: "Expected the Channel client to initialize its Channels API"
});The generated Interop example uses an API-level readiness signal because the next action depends on a method registration:
await waitFor(() =>
io.interop.methods().some(method => method.name === "IOCD.Sample.Add")
);Choose the narrowest readiness signal that matches the operation the test is about to perform. Don't use a fixed pause to cover an unknown initialization race; it makes the test slower without guaranteeing that the app is ready.
Wait for State, Not Time
Use events, WebdriverIO wait commands, browser.waitUntil(), or the generated waitFor() helper. Avoid fixed pauses such as browser.pause(2000), because they make tests slower and less reliable.
await browser.waitUntil(async () => (await status.getText()) === "Complete", {
timeout: 5000,
timeoutMsg: "Expected the operation to complete"
});When testing an event, subscribe before triggering the action so the test can't miss a fast event.
For event-based behavior, the generated waitForEvent() helper establishes the subscription before the action, applies a predicate, provides a descriptive timeout, and unsubscribes when the event resolves or times out. Register its idempotent unsubscribe function with createCleanupRegistry() so a test failure can't leak the subscription into a later test:
import { createCleanupRegistry, waitForEvent } from "./common/iocd";
let cleanups = createCleanupRegistry();
beforeEach(() => {
cleanups = createCleanupRegistry();
});
afterEach(async () => {
await cleanups.run();
});
const contextUpdate = await waitForEvent<{ clientName?: string }>(
listener => io.contexts.subscribe(contextName, data => listener(data)),
{
predicate: data => data.clientName === clientName,
timeoutMsg: `Expected ${contextName} to select ${clientName}`
}
);
cleanups.add(contextUpdate.unsubscribe);
// The subscription exists before the user action can publish the update.
await $("#share-client").waitAndClick();
const receivedContext = await contextUpdate.result;
expect(receivedContext.clientName).to.equal(clientName);Run subscription cleanup before destroying the context or other resource it observes. The cleanup registry runs all registered functions in reverse order, continues if one fails, and rethrows the first cleanup error after attempting the rest.
Save and Restore Global Layouts Safely
Global Layout operations have broad defaults. When saving a Global Layout in a test, provide the exact app instance IDs that belong to the scenario. Omitting the instances list may capture other running instances, including the WebdriverIO test client.
Restoring a Global Layout closes visible running instances by default because closeRunningInstances defaults to true. The closeMe option also defaults to true in this case. Set closeMe: false when the test must remain alive while the restore closes and replaces the app instances under test:
const layoutName = `test-orders-${Date.now()}`;
await io.layouts.save({
name: layoutName,
type: "Global",
instances: [ordersInstance.id, detailsInstance.id]
});
await io.layouts.restore({
name: layoutName,
type: "Global",
closeRunningInstances: true,
closeMe: false
});Use a unique, descriptive Layout name for each test and track it as soon as it is allocated, so afterEach() can remove it even when save or restore fails:
afterEach(async () => {
const io = await browser.io();
await io.layouts.remove("Global", layoutName).catch(() => undefined);
});Remove only the Layouts created by the test. Don't clear all user Layouts: the generated test platform uses isolated user data, but tests in the same run still share that test environment and may execute cleanup after a partial failure.
Isolate Each Test
Create mutable platform state in beforeEach() or in the test itself. Clean all resources in afterEach():
- app instances;
- windows and Workspaces;
- imported Layouts;
- Interop registrations and event subscriptions;
- Shared Contexts or other persistent test state;
- modified browser time.
Await cleanup operations. Catch cleanup errors only when a resource may legitimately have already been removed.
Use Stable Test Data and Selectors
Prefer dedicated IDs, data-* attributes, roles, or other stable selectors over selectors that depend on presentation or DOM nesting. Give fixture apps and Layouts unique, descriptive names to avoid collisions.
Deterministic input generally produces the easiest failures to reproduce. If a test selects random data, capture the selected value and include it in assertion messages and logs.
Arrange with API, Act via UI, Assert via API
Use io.Connect APIs to create controlled initial state, WebdriverIO to perform the real user workflow, and an io.Connect API to verify the resulting platform state. Add a DOM assertion when the visible result is also part of the behavior.
This pattern prevents a test from passing merely because the built-in UI changed visually while the underlying platform operation failed. The generated workspace-add-app.spec.ts demonstrates the complete flow:
// Arrange platform state via the API.
workspace = await io.workspaces!.createWorkspace({ children: [] });
await browser.switchById(workspace.frameId);
// Act via the real Workspaces UI.
await $(".wsp-empty-container button.add-button").waitAndClick();
await $(
".add-application .applicationsList .list-group-item, .add-application li.io-list-item"
).waitAndClick();
// Assert the underlying platform result via the API.
const current = (await io.workspaces!.getAllWorkspaces())
.find(candidate => candidate.id === workspace!.id)!;
const [addedWindow] = current.getAllWindows();
expect(addedWindow.appName).to.equal("workspace-widget");
// Assert the user-visible result when it matters too.
await addedWindow.forceLoad();
await browser.switchById(addedWindow.getGdWindow().id);
await $("#widget-ready").waitForDisplayed();Use the API only to arrange state and observe results when the purpose is to test a user workflow. Don't bypass the behavior under test by invoking the app action or business logic directly from the test.
Keep Tests Focused
Test one behavior per test case and run the specification alone while developing it. Extract a shared helper only when behavior is repeated and the helper makes the test intent clearer. Keep generic runtime helpers under tests/tests/common/ and workflow-specific helpers close to their specifications.
Configuration & Isolation
The generated tests/wdio.config.ts composes IOCDBaseConfiguration with IOCDService() and defines one platform-wdio capability. The capability receives the executable from process.env.BINARY_PATH, which is set by the CLI.
The generated configuration also defines:
| Value | Purpose |
|---|---|
BINARY_PATH |
Absolute path to the executable from the seed's configured iocd component. Supplied by iocd test. |
WDIO_ROOT_DIR |
Absolute path to the test package. Used by test configuration placeholders. |
WDIO_TESTS_ROOT |
Absolute path to tests/tests/. Used by fixture app definitions and assets. |
iocd:options.configOverrides |
Additional platform configuration applied only to the test process. |
iocd:options.ioInitOptions |
APIs and optional libraries used to construct the test io.Connect API. |
The configuration the platform runs with during a test is built from three layers, each deep-merged over the previous one:
| Layer | Owner | Location |
|---|---|---|
| Default system configuration | io.Connect Desktop | Shipped inside the installed iocd component. |
| Seed modifications | Your seed project | modifications/**/iocd/config/system.json.merge, applied both in development mode and in the packaged build. See System Configuration. |
| Test overrides | Your test package | tests/config/system.json, listed in iocd:options.configOverrides and applied only to the test process. |
Tests therefore run against the same platform configuration as your built app, with the test overrides on top. Keep those overrides minimal. Use them for test-scoped settings - fixture app stores, isolated user data and cache folders, log and crash dump locations, or disabling auto updates and telemetry for a test run. Don't use them to change platform behavior such as the window management mode, the Channels, the io.Connect Gateway, or authentication. Those settings define the platform your users receive: change them via the seed modifications, otherwise your tests validate a platform that you don't ship. To cover another configuration that your product supports, add a separate platform launch for it instead - see Testing Multiple Platform Configurations. If your platform is configured with an interactive SSO login, keep that configuration and automate the sign-in instead - see Automating the Platform SSO Login.
The generated test platform uses a dedicated tests/user-data folder. Treat it as disposable test output, don't commit it to source control, and don't depend on state left by a previous run. Add it to the seed .gitignore if it isn't already ignored. Don't point tests at the normal user data folder or mutate developer machine state that can't be restored during cleanup.
The default and recommended runner is platform-wdio. The service also supports platform-window, platform-node, and standalone for specialized scenarios. These modes have different execution and bundling constraints and aren't required for normal seed app testing. For details, see the package documentation installed with @interopio/wdio-iocd-service.
Testing Multiple Platform Configurations
Platform-wide settings - the Channel mode, the window group type, the default dialogs, the io.Connect Gateway - are fixed for the lifetime of a platform process. A test can't switch them at runtime, so a test that requires a configuration different from the one your seed ships needs its own platform launch.
Define one WebdriverIO capability per platform configuration in the generated tests/wdio.config.ts. Each capability starts its own io.Connect Desktop instance with its own configuration overrides and runs only the specifications written for it:
- Add a configuration file next to the base one, for example
tests/config/system.multi-channel.json, containing only the keys that differ:
{
"windows": {
"channelSelector": {
"type": "multi"
}
}
}Place the specifications that require it in a dedicated folder, for example
tests/tests/multi-channel/.Add a capability for that configuration. It uses the same executable, runner, and
ioInitOptionsas the default one, and differs only inconfigOverridesand the specifications it runs:
const ioInitOptions = {
appManager: "full",
layouts: "full",
channels: true,
libraries: ["@interopio/workspaces-api"]
};
const capability = (specs: string[], extraConfig: string[] = [], exclude: string[] = []) => ({
browserName: "iocd",
"iocd:runner": "platform-wdio" as const,
"iocd:options": {
binary,
ioInitOptions,
// Merged in order over the platform defaults and your seed modifications.
configOverrides: [path.resolve(rootDir, "config/system.json"), ...extraConfig]
},
// The nested array groups the specifications in a single worker,
// so the platform starts once for this capability.
"wdio:specs": [specs],
"wdio:exclude": exclude
});
export const config: WebdriverIO.Config = {
// The rest of the generated configuration.
capabilities: [
// The configuration your seed ships, without the specifications written for the variant.
capability(["./tests/**/*.spec.ts"], [], ["./tests/multi-channel/**/*.spec.ts"]),
// The multi Channel configuration and only the specifications that require it.
capability(
["./tests/multi-channel/**/*.spec.ts"],
[path.resolve(rootDir, "config/system.multi-channel.json")]
)
]
};Observe the following rules when defining additional capabilities:
- Exclude the variant specifications from the default capability with
wdio:exclude. Otherwise, they also run under the default configuration, which they weren't written for. - Keep the default capability on the configuration your seed ships. Additional capabilities are for the other configurations your product supports, not a way to make a failing test pass. Settings that all your users receive belong in the seed modifications.
- Capabilities run sequentially, because the service sets
maxInstancesto1. Each one starts and stops a platform instance, so group the specifications by configuration instead of adding a capability per specification file. - The
--specargument overrides the spec patterns defined by the capabilities, sonpm test -- --spec ./tests/multi-channel/channels.spec.tsruns that specification under every capability. Write configuration-dependent specifications so that they detect the configuration they run under.
To detect the active configuration, read globalThis.ioCDPlatformConfig. It holds the resolved system configuration and, unlike the io.Connect API, is available synchronously at describe() time, so it can also drive skipping or test generation:
type PlatformConfigView = {
windows?: { channelSelector?: { type?: "single" | "multi" } };
};
const selectorType = (globalThis as { ioCDPlatformConfig?: PlatformConfigView })
.ioCDPlatformConfig?.windows?.channelSelector?.type;
describe("Multi Channel selection", function() {
before(function() {
if (selectorType !== "multi") {
this.skip();
}
});
// Tests that require the multi Channel configuration.
});Don't treat a missing value as a disabled setting. A test asserting that something doesn't happen would then pass under every configuration, including the one where the behavior is expected. Skip explicitly, or fail with a message identifying the configuration that was found.
Automating the Platform SSO Login
Available since @interopio/wdio-iocd-service 1.0.0-next.3.
If your platform is configured with an interactive login (the ssoAuth system configuration), io.Connect Desktop displays a login page during startup and reports readiness only after a user signs in. The test platform starts with the same configuration, so without automating the sign-in, the test run waits on the login window until the startup timeout expires and no test executes.
The login option of IOCDService() automates the sign-in. While the platform is starting, the service finds the login window, drives it with the handler you provide, and continues the run once the platform becomes ready. This enables the test suite to run against the authenticated platform - with the same identity, session, and apps your users get:
// In tests/wdio.config.ts.
export const config: WebdriverIO.Config = {
...IOCDBaseConfiguration,
services: IOCDService({
login: {
window: { url: /login\.mycompany\.com/ },
handler: async browser => {
await browser.$("#username").setValue(process.env.IOCD_TEST_USER!);
await browser.$("#password").setValue(process.env.IOCD_TEST_PASSWORD!);
await browser.$("button[type=submit]").click();
}
}
}),
// The rest of the generated configuration.
};The login option has the following properties:
| Property | Type | Description |
|---|---|---|
window |
object |
How to recognize the login window among the startup windows. The url and title properties accept a substring or a regular expression, and at least one of them is required when window is set. Matching by url is the recommended approach for identity provider pages. If window is omitted, the platform's own SSO app window is used. |
handler |
(browser) => Promise<void> |
Drives the login page. Receives a WebdriverIO browser attached to the login window. The WebdriverIO globals (browser, $, $$) are also set while the handler runs, so page objects work unchanged. |
timeoutMs |
number |
Budget in milliseconds for finding the login window and running the handler. Defaults to 120000. |
readyTimeoutMs |
number |
Budget in milliseconds for the platform to become ready after the handler has completed. Guards against a handler that finishes without actually signing in. Defaults to 120000. |
onNoLoginWindow |
"skip" | "fail" |
What to do when the platform becomes ready without showing a login window - e.g., when the test user data still holds a valid session. Defaults to "skip". |
Consider the following when automating the login:
- The handler runs while the platform is still starting: the io.Connect APIs and
browser.io()aren't available yet - only plain WebdriverIO commands against the login page. - The session created by your identity provider belongs to the platform process itself, so the apps started by your tests are authenticated exactly as in production.
- Provide the credentials of a dedicated test account via environment variables or CI secrets. Never store credentials in specifications or configuration files.
- Any login failure - a login window that never appears, a handler that throws or exceeds
timeoutMs, or a platform that doesn't become ready withinreadyTimeoutMsafter the handler - stops the platform and reports the specifications as failed with the reason. The login is never retried. - Multi-factor authentication prompts and native OS dialogs, such as integrated Windows authentication or client certificate pickers, can't be automated with WebDriver. Use a test account exempt from such factors, or a non-interactive authentication configuration for the test platform.
- The
loginoption also accepts a function receiving the WebdriverIO capability and returning the login options, so capabilities with different configurations can sign in differently or not at all.
ℹ️ The login automation doesn't require platform support - the service discovers the login window itself, so it works with any io.Connect Desktop version your seed uses.
Troubleshooting
Missing Platform Executable
If the CLI reports that the platform executable is missing, ensure that the configured iocd component is installed. Complete npm run setup or start the seed once with npm run dev. Close the development platform before running npm test so the test service can start its isolated platform instance.
License Prompt or License Failure
Run tests from the seed root via npm test so the CLI can use config/iocd.license.key. In CI, provide the license via the secure mechanism used by your seed setup. Never hard-code a license in a specification or commit it to an additional test fixture.
Platform Startup Timeout
Temporarily set iocd:options.enableLogging to true in tests/wdio.config.ts and inspect the platform output. Check for invalid configuration paths, license failures, gateway port conflicts, and stale test user data before increasing startupTimeoutMs.
If a login window appears and stays open during startup, your platform is configured with an interactive SSO login and startup is blocked until a user signs in. Automate the sign-in with the login option - see Automating the Platform SSO Login.
Missing io.Connect API
If browser.io() reports that the API isn't available, check that it is being called inside a runtime hook or test case rather than at module or describe() scope.
If an optional API such as io.workspaces is missing, verify that its package is installed in tests/package.json and its module specifier is listed in ioInitOptions.libraries.
Flaky UI Test
Run only the failing specification, replace fixed delays with observable waits, subscribe before actions, and verify that cleanup is awaited. Use a useful timeoutMsg so CI output identifies the state that wasn't reached.
Optional video recording can be enabled via IOCDService({ videoRecording: { ... } }). Videos are retained for failed specifications and can help diagnose UI failures that aren't reproducible locally.
CI/CD
Tests don't run automatically merely because a specification is present in the seed. Add an explicit npm test step to your CI workflow on a supported Windows or macOS runner after the seed dependencies and platform components have been prepared.
The runner must have:
- a supported Node.js and npm version;
- the seed dependencies installed;
- the configured
iocdcomponent available; - a valid license supplied via CI secrets or the seed's secure license setup;
- permission to start desktop processes and ChromeDriver.
Run focused specifications during development, but run the full suite as the merge or release gate. Preserve useful failure artifacts such as reporter output, platform logs, screenshots, and optional videos. Never publish the license, unredacted user data, or other secrets as CI artifacts.
Agentic Test Development
Generated seed projects include an Agent Skill at .agents/skills/iocd-wdio-testing/SKILL.md. Compatible coding agents can discover this skill when asked to set up, write, run, or debug io.Connect Desktop tests.
The skill contains project-specific instructions and references for:
- setting up missing test infrastructure via the CLI;
- replacing an existing test package only after explicit user approval;
- registering test apps and adding test assets;
- using WebdriverIO and the
browser.io()API correctly; - adding optional io.Connect libraries;
- applying synchronization and cleanup patterns;
- running focused type checks and specifications;
- troubleshooting common platform and test failures.
Example requests to a coding agent include:
Set up the io.Connect Desktop test infrastructure in this seed.
Write a test that starts MyApp, submits its login form, and verifies the signed-in state.
Add a Workspace test for communication between the Orders and Details apps.
Debug the failing io.Connect Desktop WebdriverIO specification and run only that specification while iterating.The skill instructs agents to inspect an existing test package and request explicit approval before backing it up, removing it, or replacing it. Review generated test behavior, selectors, cleanup, and configuration like any other code change, and don't allow an agent to expose license values or other secrets.
Building for Production
The build process creates production-ready installers for distribution. It orchestrates multiple steps to produce signed, optimized packages ready for deployment.
What happens during build:
- Build all apps - Executes build scripts for each app in production mode
- Reinstall components - Downloads and installs fresh component versions
- Apply all modifications - Copies base and build-specific modifications to components
- Code sign binaries - Signs executables and libraries (if configured)
- Create build artifacts - Generates installers (
.exesetup on Windows,.dmgon macOS,.ziparchives) - Publish to release server - Uploads artifacts for auto-updates (if configured)
Build System:
The build process is based on Electron Forge, a complete toolchain for building and packaging Electron apps. The main configuration is located in /config/forge.config.js, where you can customize makers, publishers, and build behavior.
Build Commands
# Build installer
npm run build
# Build options
npm run build -- --output custom/path # Custom output directory
npm run build -- --publish-only # Skip build, only publish
npm run build-skip-install # Skip component installationBuild Output
Depending on the OS, you can configure Electron Forge makers to produce different types of artifacts. The following configurations have been tested:
Additional components are packaged under the runtime components/ directory inside the built io.Connect Desktop app. If your project includes only the required iocd component, the build output won't create an empty components/ directory.
Windows
- Squirrel.Windows installer (
.exesetup)
You can change the install GIF by replacing the install.mp4 in assets folder.
If using a Windows installer, you can hide the installer UI during the installation process by using the
--silentcommand line argument:
io-connect-desktop-setup.exe --silentThe app will be installed into %LocalAppData%\YourAppName. Squirrel is opinionated and does not allow changing this path.
By default the
/UserDatafolder generated by the platform will stay in the %LocalAppData%\interop.io\io.Connect Desktop folder. You can change this by adding the followingsystem.json.merge-win32file inmodifications/base/iocd/config/system.json.merge-win32(the merge file has win32 suffix which indicates it is only applied on Windows OS):
{
"folders": {
"userData": "%LocalAppData%/${PRODUCT_SLUG}/UserData/%IO_CD_ENV%-%IO_CD_REGION%",
"cache": {
"location": "%LocalAppData%/${PRODUCT_SLUG}/Cache/%IO_CD_ENV%-%IO_CD_REGION%",
"copy": true
}
}
}- Portable ZIP archive
macOS
DMG disk image
ZIP archive
The full list of makers is available in the Electron Forge documentation.
Code Signing
Code signing ensures that your app is trusted by operating systems and users can verify that it hasn't been compromised. Both Windows and macOS require code signing for distribution.
The settings for code signing are located under the "codeSign" property of the "win" top-level key for Windows and the "mac" top-level key for macOS in the iocd.cli.config.json seed project configuration file.
Windows
Prerequisites
The io.Connect CLI uses signtool.exe (part of the Windows SDK) to sign binaries. It must be available on your system PATH before running a build with code signing enabled.
In CI environments (e.g., GitHub Actions), signtool.exe is typically already available or added to the PATH by the workflow.
The "type" property of the "codeSign" object under the "win" top-level key accepts the following values:
| Value | Description |
|---|---|
"custom" |
Use a custom signing script. |
"signtool" |
Use a certificate and a sign tool to sign. The certificate can be retrieved from a PFX certificate file or from the Windows Certificate Store. |
"off" |
Disable code signing. |
- PFX Certificate File
The is the most common method for code-signing on Windows. It uses a PFX certificate with a password. The PFX file contains both the certificate and the private key needed by the sign tool.
To configure signing via a PFX certificate file, provide the path to the file and the PFX password:
// In `config/iocd.cli.config.json`.
{
"win": {
"codeSign": {
"type": "signtool",
"pfxPath": "path/to/certificate.pfx",
"pfxPassword": "${WIN_PFX_PASS}"
}
}
}- Certificate from the Windows Certificate Store
This method uses a certificate that already exists in the Windows Certificate Store. This is the approach you would use with services like DigiCert KeyLocker. For instance, with KeyLocker, you sync the certificate into the local Windows Certificate Store and sign using the certificate hash. The advantage is that the private key never leaves the HSM which is good for more security-sensitive environments.
To configure signing via a certificate from the Windows Certificate Store, provide the certificate SHA-1:
// In `config/iocd.cli.config.json`.
{
"win": {
"codeSign": {
"type": "signtool",
"certificateSha1": "${WIN_CERT_SHA1}"
}
}
}When configuring your code signing settings, consider the following:
⚠️ Note that if using DigiCert KeyLocker, you must run
smctl windows certsyncbefore building to sync certificates to the Windows Certificate Store.
- Custom Script
For advanced signing scenarios, you can provide a custom signing script:
// In `config/iocd.cli.config.json`.
{
"win": {
"codeSign": {
"type": "custom",
"customCodeSignScriptPath": "path/to/custom-sign.js"
}
}
}Your custom script should export a function that receives the binary path and config:
// custom-sign.js
module.exports = async function(binaryPath, config) {
// Your custom signing logic here.
console.log(`Signing ${binaryPath}`);
// Use any signing tool or API you need.
};- Signing Squirrel Installer Binaries
The io.Connect CLI signs all platform binaries during the build process. However, the Squirrel.Windows maker produces additional stub binaries (e.g., Setup.exe, Update.exe, <APP-NAME>.exe) as part of the installer creation step. These stub binaries are generated after the CLI code signing phase and therefore aren't signed automatically.
To ensure the Squirrel-produced binaries are also signed, add the windowsSign configuration to your Squirrel maker in config/forge.config.js:
const path = require("path");
module.exports = {
makers: [
{
name: "@electron-forge/maker-squirrel",
config: {
// ...other Squirrel config,
windowsSign: {
certificateFile: path.resolve(__dirname, "certs/certificate.pfx"),
certificatePassword: process.env.WINDOWS_CERTIFICATE_PASSWORD,
timestampServer: "http://timestamp.digicert.com",
signToolPath: "C:\\Program Files (x86)\\Windows Kits\\10\\bin\\10.0.26100.0\\x64\\signtool.exe",
},
},
},
],
};⚠️ Note that the
signToolPathmay vary depending on your Windows SDK version. Adjust the path to match your installed version.
- Excluding Folders from Signing
Available since @interopio/iocd-cli 0.0.74.
You can exclude specific folders from code signing on Windows by using the "excludeFolders" property of the "codeSign" object. This is useful when certain folders contain third-party binaries that are already signed or don't need to be signed with your certificate.
The "excludeFolders" property accepts an array of glob patterns. Paths are matched relative to the io.Connect Desktop component directory using forward slashes. By default, no folders are excluded — all binaries found in the build output are signed.
Supported glob syntax:
| Pattern | Description |
|---|---|
** |
Matches any number of path segments (including zero). |
* |
Matches anything within a single path segment. |
? |
Matches a single character. |
! (prefix) |
Negates a pattern (re-includes previously excluded paths). |
Patterns are evaluated in order and the last matching pattern wins (similar to .gitignore).
Example configuration that excludes all binaries under components/ and assets/gw-legacy/, but re-includes components/my-addon/:
// In `config/iocd.cli.config.json`.
{
"win": {
"codeSign": {
"type": "signtool",
"pfxPath": "path/to/certificate.pfx",
"pfxPassword": "${WIN_PFX_PASS}",
"excludeFolders": [
"components/**",
"assets/gw-legacy/**",
"!components/my-addon/**"
]
}
}
}- Signing Squirrel-Generated Stub Executables
During the build process, the io.Connect CLI signs all platform binaries and the final setup .exe installer. However, during the installer creation process, Squirrel.Windows internally generates stub executables — Update.exe (responsible for managing app updates) and <app-name>.exe (the app launcher shortcut target). These stubs are created by Squirrel as part of building the installer package, so they cannot be signed by the CLI's signing step which runs separately.
To sign these Squirrel-generated stubs, add a windowsSign object to the @electron-forge/maker-squirrel configuration in config/forge.config.js:
// In `config/forge.config.js`.
const path = require("path");
module.exports = {
makers: [
{
name: "@electron-forge/maker-squirrel",
config: {
// ... other Squirrel config options,
windowsSign: {
certificateFile: path.resolve(__dirname, "certs/certificate.pfx"),
certificatePassword: process.env.WINDOWS_CERTIFICATE_PASSWORD,
timestampServer: "http://timestamp.digicert.com",
signToolPath: "C:\\Program Files (x86)\\Windows Kits\\10\\bin\\10.0.26100.0\\x64\\signtool.exe",
},
},
},
],
};The following table describes the windowsSign properties:
| Property | Type | Description |
|---|---|---|
certificateFile |
string |
Path to the PFX certificate file used for signing the Squirrel-generated stubs. |
certificatePassword |
string |
Password for the PFX certificate. Use an environment variable to avoid hardcoding secrets. |
timestampServer |
string |
URL of the timestamp server to use for timestamping the signature. |
signToolPath |
string |
(Optional) Explicit path to signtool.exe. Required only if signtool.exe is not on your system PATH. |
⚠️ Note that the
signToolPathmay vary depending on the Windows SDK version installed on your system. Verify the path before configuring it.
⚠️ Never hardcode the
certificatePasswordvalue directly in your configuration file. Always use an environment variable (e.g.,process.env.WINDOWS_CERTIFICATE_PASSWORD) and store the actual password in your CI/CD secrets or a secure credential store.
⚠️ Without this configuration, the
Update.exeand<app-name>.exestubs inside the installer will be unsigned. This may trigger Windows SmartScreen warnings or antivirus flags for end users.
macOS
On macOS the process is a little different and more tightly controlled. It also requires an extra step called notarization.
Code Signing Configuration
The "type" property of the "codeSign" object under the "mac" top-level key accepts the following values:
| Value | Description |
|---|---|
"certificate" |
Use a .p12 certificate file. |
"custom" |
Use a custom signing script. |
"keychain" |
Use a certificate from macOS Keychain. |
"off" |
Disable code signing. |
- Keychain
This method uses a certificate from macOS Keychain. This is the mode to use if you want to use an Apple Developer ID certificate. You must download the certificate from the Apple Developer Portal, import it into your Keychain, and it will become available for code signing tools.
// In `config/iocd.cli.config.json`.
{
"mac": {
"codeSign": {
"type": "keychain",
"identity": "Developer ID Application: Your Company (TEAM_ID)"
}
}
}You can use the following properties of the "codeSign" object to specify your Developer ID Application identity and Keychain to use:
| Property | Type | Description |
|---|---|---|
"identity" |
string |
Developer ID Application identity or SHA-1 hash (optional, auto-selected if not specified). |
"keychain" |
string |
Keychain name or path (optional, uses the default Keychain if not specified). |
- Certificate
This method uses a .p12 certificate file that's imported into a temporary keychain during the build. The temporary keychain is removed after the build.
// In `config/iocd.cli.config.json`.
{
"mac": {
"codeSign": {
"type": "certificate",
"certificatePath": "./certs/mac-cert.p12",
"certificatePassword": "${MAC_CERT_PASSWORD}"
}
}
}- Custom Script
For advanced signing scenarios, you can provide a custom signing script:
// In `config/iocd.cli.config.json`.
{
"mac": {
"codeSign": {
"type": "custom",
"customCodeSignScriptPath": "path/to/custom-sign.js"
}
}
}Your custom script should export a function that receives the app bundle path and config:
// custom-sign.js
module.exports = async function(appBundlePath, config) {
// Your custom signing logic here
console.log(`Signing ${appBundlePath}`);
// Use any signing tool or API you need
};Notarization Configuration
Notarization is required for distributing any macOS app outside the App Store. This is how Apple verifies that the app is safe and hasn't been compromised. If an app isn't notarized, users will see warnings or may not be able to run it at all.
The notarization settings are located under the "notarization" property of the "mac" top-level key:
// In `config/iocd.cli.config.json`.
{
"mac": {
"notarization": {
"type": "notarytool",
"appleId": "${MAC_NOTARIZATION_APPLE_ID}",
"appleIdPassword": "${MAC_NOTARIZATION_APPLE_ID_PASSWORD}",
"appleTeamId": "${MAC_NOTARIZATION_APPLE_TEAM_ID}"
}
}
}⚠️ Note that when configuring your notarization settings, you should consider the following:
- Notarization is required for distribution outside the Mac App Store.
- Use an app-specific password (not your regular Apple ID password).
- Generate an app-specific password at
appleid.apple.com.- You can set
"type"to"off"to skip notarization (for development builds).
- Custom Script
For advanced notarization scenarios, you can provide a custom notarization script:
{
"mac": {
"notarization": {
"type": "custom",
"customNotarizationScriptPath": "path/to/custom-notarize.js"
}
}
}Your custom script should export a function that receives the app bundle path and config:
// custom-notarize.js
module.exports = async function(appBundlePath, config) {
// Your custom notarization logic here.
console.log(`Notarizing ${appBundlePath}`);
// Use any notarization tool or API you need.
};Install Tasks
Available since io.Connect Desktop 10.0.4
io.Connect Desktop supports executing custom install and uninstall tasks via command-line triggers, allowing automated execution of scripts and commands during installation and uninstallation processes on both Windows and macOS.
This feature enables you to:
- Copy configuration files to system locations after installation
- Register protocol handlers after installation
- Download additional resources after installation
- Clean up user data and configurations before uninstallation
- Unregister protocols or file associations before uninstallation
- Backup user preferences before uninstallation
Overview
When using installers built with the seed project, io.Connect Desktop automatically responds to installation lifecycle events via command-line arguments. The Squirrel installer (used on Windows) automatically passes these arguments to the executable during install/uninstall operations.
The following command-line arguments trigger task execution:
| Argument | Platform | Description |
|---|---|---|
--post-install |
Cross-platform | Execute install tasks (manual trigger). |
--pre-uninstall |
Cross-platform | Execute uninstall tasks (manual trigger). |
--squirrel-install |
Windows | Initial installation (triggered by Squirrel installer). |
--squirrel-uninstall |
Windows | App uninstall (triggered by Squirrel installer). |
--squirrel-updated |
Windows | App update (triggered by Squirrel installer). |
ℹ️ The
--post-installand--pre-uninstallflags can be used if you aren't using a Squirrel-based installer or if you want to manually test the install tasks.
Example:
# Windows
io-connect-desktop.exe --post-install
# macOS
open -a "io.Connect Desktop" --args --post-installConfiguration
Install and uninstall tasks are defined in platform-specific JSON configuration files within the install/ folder:
tasks.win32.json- Windows taskstasks.darwin.json- macOS tasks
Platform values:
win32- Windowsdarwin- macOS
File location depends on the deployment method:
- Packaged Windows:
[iocd-exe-directory]/install/(next to the .exe) - Packaged macOS:
[YourApp].app/Contents/install/(in the Contents folder)
To add install tasks to your seed project, create the task files in the modifications/build/iocd/install/ directory:
modifications/
└── build/
└── iocd/
└── install/
├── tasks.win32.json (Windows)
└── tasks.darwin.json (macOS)This ensures that the task files are included in your production build.
Task Schema
The task files follow this structure:
{
"installTasks": [
{
"type": "run",
"args": {
"filename": "path/to/executable",
"args": "command line arguments",
"dir": "working/directory",
"hide": true,
"unattended": false,
"exitCode0": "success",
"exitCode1": "Custom error message",
"exitCodeN": "success"
}
}
],
"uninstallTasks": [
{
"type": "run",
"args": { /* same structure */ }
}
]
}Each task supports the following properties:
| Property | Type | Description |
|---|---|---|
filename |
string |
Required. Path to the executable or script to run. Supports environment variable expansion (e.g., %TEMP%, %APPDATA%). |
args |
string |
Command-line arguments to pass to the executable. The full command is passed to the system shell for parsing. |
dir |
string |
Working directory for the process. Defaults to the install folder if not specified. Supports environment variables. |
hide |
boolean |
Hide the process window (Windows only). Default: false. |
unattended |
boolean |
Suppress error dialogs. If true, errors are logged only. Default: false. |
exitCode0 to exitCodeN |
string |
Custom exit code handling. Use "success" to mark a non-zero exit code as success, or provide a custom error message. Default: exit code 0 is success, any other is an error. |
⚠️ Important: Task execution is synchronous and sequential. Tasks run one at a time in the order defined. If any task fails (non-zero exit code without a success mapping), execution stops immediately and no subsequent tasks run.
Examples
Windows Batch Script
{
"installTasks": [
{
"type": "run",
"args": {
"filename": "cmd.exe",
"args": "/c copy-resources.bat",
"hide": true,
"exitCode0": "success",
"exitCode1": "Error copying resources."
}
}
],
"uninstallTasks": [
{
"type": "run",
"args": {
"filename": "cmd.exe",
"args": "/c cleanup.bat",
"hide": true
}
}
]
}macOS Shell Script
{
"installTasks": [
{
"type": "run",
"args": {
"filename": "/bin/bash",
"args": "setup.sh --silent",
"hide": false
}
}
],
"uninstallTasks": [
{
"type": "run",
"args": {
"filename": "/bin/bash",
"args": "cleanup.sh",
"dir": "/tmp"
}
}
]
}Custom Exit Code Mapping
You can map specific exit codes to success or custom error messages:
{
"installTasks": [
{
"type": "run",
"args": {
"filename": "installer.exe",
"args": "--verify",
"exitCode0": "success",
"exitCode1": "Error validating the installation.",
"exitCode2": "Error contacting server.",
"exitCode3": "success",
"exitCode4": "Network timeout - please retry."
}
}
]
}In this example:
- Exit codes 0 and 3 are considered successful
- Exit codes 1, 2, and 4 will show custom error messages
- Any other exit code will show a default error message
Environment Variable Expansion
All path-related properties (filename, args, dir) support environment variable expansion using the %VAR% syntax:
{
"args": {
"filename": "%APPDATA%\\MyCompany\\install-script.bat",
"dir": "%LOCALAPPDATA%\\MyCompany"
}
}Platform-Provided Variables
The following environment variables are automatically set by io.Connect Desktop and available for use in all install tasks:
| Variable | Description | Example Value |
|---|---|---|
IO_CD_EXE_NAME |
Executable file name | io-connect-desktop.exe (Windows)io-connect-desktop (macOS) |
IO_CD_ROOT_DIR |
Root installation directory | C:\Users\...\io-connect-desktop\app-10.0.4 (Windows)/Applications/io.Connect Desktop.app/Contents (macOS) |
IO_CD_INSTALL_DIR |
Directory containing install tasks | C:\Users\...\io-connect-desktop\app-10.0.4\install (Windows)/Applications/io.Connect Desktop.app/Contents/install (macOS) |
Troubleshooting
Tasks not executing:
- Verify that the
install/tasks.win32.json(Windows) orinstall/tasks.darwin.json(macOS) file exists in the correct location - Check that the file contains valid JSON
- Ensure the executable paths are correct and accessible
Tasks failing:
- Review the console logs for error messages
- Test the script manually to verify it works
- Check file permissions (especially on macOS - scripts need execute permission)
- Verify environment variables are correctly expanded
Silent installation fails:
- Ensure all tasks have
"unattended": trueto prevent error dialogs - Test each task individually to identify which one is failing
- Consider adding custom exit code mappings for expected error conditions
CI/CD Automation
Continuous Integration and Continuous Deployment (CI/CD) automates building, testing, and publishing your io.Connect Desktop platform. The seed project includes a GitHub Actions workflow located in .github/workflows/build.yml. It produces signed installers and can publish them to a release server.
Auto Updates
Setting up auto updates allows your users to automatically receive new versions of your platform.
Requirements:
Auto-updates are supported for specific build artifact types:
- Windows: Squirrel.Windows installers (
.exesetup files) - macOS: DMG on macOS
Other build outputs (portable ZIP on Windows, ZIP on macOS) do not support automatic updates.
Update Source Types
io.Connect Desktop supports two types of update sources:
- StaticStorage (Simple - No Special Server Required)
This is the simpler approach that does not require a special update server. You only need a static file server (such as a CDN, web server, or cloud storage) where the RELEASES files can be accessed via HTTP/HTTPS.
How It Works:
- macOS: The platform appends
/RELEASES.jsonto yourbaseUrland expects a JSON file with update metadata - Windows: The platform uses the
baseUrldirectly and expects a Squirrel.Windows RELEASES file at that location
Example Configuration:
Create or modify modifications/base/iocd/config/system.json.merge:
{
"autoUpdater": {
"enabled": true,
"updateSource": {
"type": "StaticStorage",
"baseUrl": "https://cdn.yourcompany.com/updates"
},
"updateIntervalMinutes": 60
}
}With this configuration:
- macOS will check:
https://cdn.yourcompany.com/updates/RELEASES.json - Windows will check:
https://cdn.yourcompany.com/updates(for the RELEASES file)
When to Use:
- You want a simple deployment without maintaining a dedicated update service
- You have a static file server, CDN, or cloud storage (e.g., AWS S3, Azure Blob Storage, GitHub Pages)
- You don't need dynamic routing based on channels or complex version logic
- Service (Advanced - Requires Update Service)
This approach requires a dedicated update service that can handle dynamic routing based on platform, version, and channel.
How It Works:
The platform constructs a dynamic URL in the format: <baseUrl>/update/<platform>/<version>/<channel>
Example Configuration:
Create or modify modifications/base/iocd/config/system.json.merge:
{
"autoUpdater": {
"enabled": true,
"updateSource": {
"type": "Service",
"baseUrl": "https://updates.yourcompany.com",
"channel": "stable"
},
"updateIntervalMinutes": 60
}
}With this configuration, the platform will check URLs like:
https://updates.yourcompany.com/update/darwin/1.0.0/stable(macOS)https://updates.yourcompany.com/update/win32/1.0.0/stable(Windows)
When to Use:
- You need different update channels (stable, beta, dev)
- You want server-side logic for update eligibility (e.g., phased rollouts, A/B testing)
- You need to track update requests or analytics
- You're already using an update service like Nucleus, Hazel, or a custom solution
Setting Up Auto Updates
Execute the following steps to set up platform auto updates:
- Using StaticStorage (Recommended for Most Users)
StaticStorage supports two approaches: automated publishing with Electron Forge publishers (recommended), or manual file management (advanced).
Approach 1: Automated with Electron Forge Publishers (Recommended)
This approach uses Electron Forge publishers to automatically upload installers and generate RELEASES files.
- Choose your hosting platform:
- S3 Publisher - Amazon S3 buckets
- GitHub Publisher - GitHub Releases (for public repos)
- Other compatible static storage providers
- Configure the publisher in
config/forge.config.js:
Example: S3 Publisher
module.exports = {
publishers: [
{
name: '@electron-forge/publisher-s3',
config: {
bucket: 'my-app-updates',
region: 'us-east-1',
public: true,
// Credentials via AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY env vars
}
}
],
makers: [
{
name: '@electron-forge/maker-squirrel',
config: {
// For Windows delta updates
remoteReleases: 'https://my-app-updates.s3.amazonaws.com'
}
},
{
name: '@electron-forge/maker-zip',
config: {
// For macOS RELEASES.json generation
macUpdateManifestBaseUrl: 'https://my-app-updates.s3.amazonaws.com/darwin/x64'
}
}
]
};Example: GitHub Publisher
module.exports = {
publishers: [
{
name: '@electron-forge/publisher-github',
config: {
repository: {
owner: 'your-org',
name: 'your-repo'
},
prerelease: false
}
}
]
};Important: The
macUpdateManifestBaseUrloption requires using an Electron Forge publisher (like S3 or GitHub). It will not work with purely manual file uploads.
- Configure io.Connect Desktop to check for updates:
Create or modify modifications/base/iocd/config/system.json.merge:
{
"autoUpdater": {
"enabled": true,
"updateSource": {
"type": "StaticStorage",
"baseUrl": "https://my-app-updates.s3.amazonaws.com"
},
"updateIntervalMinutes": 60
}
}- Build and publish:
npm run buildThe build process will:
- Generate installers and RELEASES files
- Automatically upload everything to your configured publisher
- Update the RELEASES.json with the new version (macOS)
- Test the auto updates:
- Install your platform using the published installer
- Publish a new version
- Launch the platform and verify it detects and installs the update
Approach 2: Manual File Management (Advanced)
If you cannot use an Electron Forge publisher, you can manually manage files. This requires understanding the RELEASES file formats.
Windows: The Squirrel.Windows maker automatically generates:
{ProductName} Setup.exe- The installer{ProductName}-full.nupkg- The full packageRELEASES- The update metadata file
Simply upload these files to your hosting location. The RELEASES file is plain text with nupkg metadata.
macOS: For macOS without a publisher, you must manually create and maintain the RELEASES.json file. The format is:
{
"currentRelease": "1.0.0",
"releases": [
{
"version": "1.0.0",
"updateTo": {
"version": "1.0.0",
"pub_date": "2024-01-15T10:00:00Z",
"notes": "Initial release",
"name": "1.0.0",
"url": "https://cdn.yourcompany.com/updates/MyApp-1.0.0-mac.zip"
}
}
]
}Each time you release a new version, manually update this file to add the new release entry and update currentRelease.
Manual Upload Steps:
- Build installers:
npm run build - Find generated files in
dist/orout/directory - For Windows: Upload
RELEASES,.exe, and.nupkgfiles - For macOS: Create/update
RELEASES.jsonmanually and upload with.zipfile - Ensure all URLs in RELEASES files are accessible
- Test updates by installing the platform and verifying it detects new versions
- Using Service (Advanced)
- Set up an update service:
You can use existing solutions or build your own:
- Existing services: Nucleus, Hazel, or other update servers (see Electron update services)
- Custom solution: Build your own update server that responds to requests at
<baseUrl>/update/<platform>/<version>/<channel>
- Configure io.Connect Desktop to check the update service:
Create or modify modifications/base/iocd/config/system.json.merge:
{
"autoUpdater": {
"enabled": true,
"updateSource": {
"type": "Service",
"baseUrl": "https://updates.yourcompany.com",
"channel": "stable"
},
"updateIntervalMinutes": 60
}
}- Publish updates to your service:
Choose one of the following approaches:
Automatic publishing during build - configure publishers in config/forge.config.js:
module.exports = {
publishers: [
{
name: "@electron-forge/publisher-electron-release-server",
config: {
baseUrl: "https://updates.yourcompany.com",
username: process.env.RELEASE_SERVER_USERNAME,
password: process.env.RELEASE_SERVER_PASSWORD
}
}
]
};Manual upload - after building, manually upload the installer artifacts to your update service using its API or admin interface.
- Test the auto updates:
- Install your platform using the signed installer
- Publish a new version to your update service
- Launch the platform and verify it detects and installs the update
CLI
The io.Connect CLI is available via the @interopio/iocd-cli library, which provides various commands for creating and manipulating a seed project for io.Connect Desktop.
To install the library, execute the following command:
npm install -g @interopio/iocd-cli@latestYou can also use npx to directly execute package commands without global installation of the package:
npx @interopio/iocd-cli@latest createSome of the commands are defined under the "scripts" property of the generated seed project's package.json file and can be executed via npm:
# Invokes `iocd dev`.
npm run dev
# Invokes `iocd test`.
npm run test
# Invokes `iocd setup`.
npm run setup
# Invokes `iocd build`.
npm run build
# Invokes `iocd build --skip-install`.
npm run build-skip-install
# Invokes `iocd apps dev`.
npm run apps-dev
# Invokes `iocd components install`.
npm run components-installCommands
The io.Connect CLI offers the following main commands:
| Command | Description |
|---|---|
apps |
Command with sub-commands for managing the customizable system apps in the seed project. |
build <options> |
Builds and packages the io.Connect Desktop platform for distribution. |
components |
Command with sub-commands for managing the seed project components. |
create <options> |
Creates a new seed project. Accepts options for configuring the seed project. |
dev |
Starts io.Connect Desktop in development mode (the platform isn't packaged yet). |
help <command> |
Displays help for the specified command. |
license |
Displays current license key information. |
modifications |
Command with sub-commands for managing the seed project modifications. |
setup |
Prepares the seed project for first usage. This will install the specified components and will install the dependencies for all customizable system apps. |
test |
Runs the test suite. |
create
The create command accepts the following options:
| Option | Description |
|---|---|
--source <url/path> |
Override component source during setup |
--non-interactive |
Run in non-interactive mode using provided options |
--product-name <name> |
Product name for non-interactive mode |
--folder-name <name> |
Folder name for non-interactive mode |
--components <components> |
Comma-separated list of components (e.g., "iocd,demos,devTools") |
--applications <applications> |
Comma-separated list of apps (e.g., "groups,workspaces") |
--license-key <key> |
License key for non-interactive mode. Required unless IOCD_LICENSE_KEY is set or config/iocd.license.key exists. |
--use-current-folder |
Install in current folder (non-interactive mode) |
-h, --help |
Display help for command |
dev
The dev command accepts the following options:
| Option | Description |
|---|---|
--extraLaunchArgs <arguments> |
Extra launch arguments passed to the io.Connect Desktop executable. The arguments are split by space. |
-h, --help |
Display help for command |
build
The build command accepts the following options:
| Option | Description |
|---|---|
--output <path> |
Output directory for packages, relative to the project root. If omitted, packages remain under dist/make. |
--publish-only |
Skip the build phase and only publish packages (does nothing if no publish is configured) (default: false) |
--skip-install |
Skip installing components during the build process (default: false) |
-h, --help |
Display help for command |
test
The test command runs the configured test suite. It also provides the following sub-command:
| Command | Description |
|---|---|
setup |
Creates and validates the current automated test infrastructure when the configured test directory is missing. Refuses to overwrite an existing directory. |
components
The components command has the following sub-commands:
| Command | Description |
|---|---|
uninstall <name> |
Remove a component from the project |
install [name] |
Install all components listed in iocd.cli.config.json or a specific component by name |
list |
List all installed components |
browse |
Browse all available components from currently selected store |
pin |
Pin all "latest" component versions to their current specific versions for reproducible builds |
download [options] [output-dir] |
Download components from the current store to a local directory in local store format. By default, the latest version of each component is downloaded. Use -c or --components with a semicolon-separated list to download specific components and versions, and -f or --force to overwrite existing files without prompting. |
help [command] |
Display help for command |
apps
The apps command has the following sub-commands:
| Command | Description |
|---|---|
install [options] |
Install dependencies for all template apps |
build [options] |
Build all template apps and copy output to modifications folder. Requires prior installation of dependencies. |
dev [options] |
Start all template apps in development mode |
list |
List all template apps that are added to the seed project, and all available templates |
add <appName> |
Add a new template app from the registry |
help [command] |
Display help for command |
modifications
The modifications command has the following sub-commands:
| Command | Description |
|---|---|
apply [component] |
Apply all modifications for the selected mode. The optional component argument is currently reserved and component-specific apply isn't implemented. |
help [command] |
Display help for command |
The modifications apply command accepts the following options:
| Option | Description |
|---|---|
--mode <mode> |
Mode to apply modifications for: dev or build. Defaults to dev. |
-h, --help |
Display help for command |
Troubleshooting
This guide addresses common issues when running the io.Connect CLI.
1. Node.js SSL/TLS Certificate Errors
Symptoms:
SELF_SIGNED_CERT_IN_CHAINerrorsunable to verify the first certificate- CLI cannot connect to GitHub or npm registry
Root Cause: Corporate proxies often intercept HTTPS traffic and re-sign requests with an internal certificate. Node.js doesn't trust these certificates by default, causing HTTPS requests to fail.
Solutions:
Option A: Upgrade to Node.js 24+ (Recommended)
Starting from Node.js 24, Node uses the operating system's trust store. If your corporate certificate is already trusted by your OS (which is usually the case), no manual configuration is needed.
# Check your Node.js version
node --version
# If below v24, upgrade Node.jsFor Node.js 24+, you can also explicitly enable system certificates:
Windows (PowerShell - current session):
$env:NODE_OPTIONS = "--use-system-ca"
npm run setupWindows (PowerShell - permanent for current user):
[Environment]::SetEnvironmentVariable("NODE_OPTIONS", "--use-system-ca", "User")
# Restart your terminal for changes to take effectmacOS/Linux (current session):
NODE_OPTIONS="--use-system-ca" npm run setupmacOS/Linux (permanent):
# Add to your shell profile (~/.zshrc or ~/.bashrc)
echo 'export NODE_OPTIONS="--use-system-ca $NODE_OPTIONS"' >> ~/.zshrc
source ~/.zshrcOption B: Disable TLS Verification (Development Only)
⚠️ Warning: This disables security checks. Use only in development environments.
Windows (PowerShell - current session):
$env:NODE_TLS_REJECT_UNAUTHORIZED = "0"
npm run setupWindows (PowerShell - permanent for current user):
[Environment]::SetEnvironmentVariable("NODE_TLS_REJECT_UNAUTHORIZED", "0", "User")
# Restart your terminal for changes to take effectmacOS/Linux (current session):
NODE_TLS_REJECT_UNAUTHORIZED=0 npm run setupmacOS/Linux (permanent - not recommended):
echo 'export NODE_TLS_REJECT_UNAUTHORIZED=0' >> ~/.zshrc
source ~/.zshrc2. Node.js Proxy Configuration Issues
Symptoms:
npm run setupfails with network errors- GitHub API requests fail even though the browser can access GitHub
ECONNREFUSEDor timeout errors- CLI cannot download components from GitHub
- "Network error: fetch failed" with retries
Root Cause: Node.js does not automatically use system proxy settings (WinHTTP on Windows, system preferences on macOS). Even if your browser works behind a proxy, Node.js apps need explicit proxy configuration.
Solution:
The CLI (version 0.0.54+) automatically detects and uses your system proxy settings. If you're experiencing proxy-related issues, ensure you're running the latest version of the CLI:
npm i @interopio/iocd-cli@latestAfter updating, run the setup command again. The CLI will automatically route requests via your system's configured proxy.
Verification:
If you want to verify your proxy settings are correctly configured at the system level:
Windows (PowerShell):
# Check system proxy (WinHTTP)
netsh winhttp show proxy
# Test if GitHub API is reachable
curl https://api.github.com/repos/interopio/iocd-components/releases?per_page=1macOS/Linux:
# Check network configuration
scutil --proxy
# Test connectivity
curl -I https://api.github.comIf the browser/curl works but the CLI still fails after updating, there may be a TLS certificate issue (see next section).
3. npm Registry Errors
Symptoms:
E400 Bad Requestwhen installing packagesnpm error 400 Bad Request - GET https://registry.npmjs.org/@interopio%2f...- Timeouts when downloading npm packages
Root Cause: The npm client uses its own SSL policy and doesn't automatically inherit the Node.js TLS settings. Corporate proxies may also block or interfere with npm registry requests.
Solutions:
Disable npm Strict SSL
npm config set strict-ssl falseConfigure Corporate npm Registry
If your organization has an internal npm registry or proxy:
# Set your corporate registry
npm config set registry https://your-corporate-registry.com/
# Or use a scoped registry for @interopio packages
npm config set @interopio:registry https://registry.npmjs.org/View Current npm Configuration
npm config get registry
npm config list4. npm Proxy Configuration
Symptoms:
npm installfails withECONNREFUSEDor timeout errors- Package downloads hang or fail while CLI operations (like downloading components) work fine
npm ERR! networkerrors during dependency installation
Root Cause: The CLI uses its own HTTP client with automatic proxy support from environment variables. However, npm has its own network stack and may not inherit these settings. It requires explicit proxy configuration via its own config system.
Solution:
The CLI automatically uses proxy settings from HTTP_PROXY/HTTPS_PROXY environment variables for its own network operations (downloading components from GitHub). However, npm package installations are handled by npm itself, which may need separate proxy configuration:
npm config set proxy http://your-proxy-server:port
npm config set https-proxy http://your-proxy-server:portNote: In most enterprise environments, IT departments already configure
HTTP_PROXY/HTTPS_PROXYenvironment variables system-wide. If these are set, the CLI will use them automatically. Check with your IT team if you're unsure whether these are configured.
5. Build fails with "License not found":
- Ensure that the
IOCD_LICENSE_KEYenvironment variable is set or create aniocd.license.keyfile in the/configfolder with your license key.
6. Code signing fails on Windows:
- Verify that
signtool.exeis available on your systemPATH. Runsigntoolin your terminal to check. If the command is not found, install the Windows SDK or run your build from a Developer Command Prompt for Visual Studio. See the Code Signing > Windows section for more details. - Verify that the certificate is valid and not expired.
- Check that
"pfxPassword"is set correctly. - Verify that exactly one of
"pfxPath"or"certificateSha1"is configured (not both).
7. Notarization for macOS fails:
- Use an app-specific password, not a regular Apple ID password.
- Generate an app-specific password at
appleid.apple.com. - Verify that your Apple Developer Program membership is active.
- Ensure that
"appleTeamId"matches your Developer Team ID.
8. The platform won't start in development mode:
- Run
npm run setupto verify component installation. - Verify that that the
components/iocddirectory exists. - Review the logs in the system
/tempdirectory.
9. Components not downloading:
- Verify network connectivity.
- Verify the component store configuration in the
iocd.cli.config.jsonfile located in the/configfolder. - For GitHub stores: ensure that the repository access and credentials are correct.
CLI Changelog
1.0
1.0.3
Release date: 19.08.2026
Improvements & Bug Fixes
- Prepare the seed project before running the generated tests.
1.0.2
Release date: 18.08.2026
Improvements & Bug Fixes
- Pin
@interopio/wdio-iocd-serviceto version1.0.0-next.3in the generated test package and add guidance for automating an SSO login during platform startup.
1.0.1
Release date: 16.08.2026
Improvements & Bug Fixes
- Fix the generated Workspace test specification.
1.0.0
Release date: 14.08.2026
Breaking Changes
- Seed projects now generate a WebdriverIO 9 test package and expose the
iocd testandiocd test setupcommands. Existing seeds with an older test setup must back up or remove their test directory and runiocd test setupto regenerate it. See Replacing an Existing Test Package.New Features
- Add the E2E test infrastructure for seed projects: a generated WebdriverIO test package based on
@interopio/wdio-iocd-service, theiocd testandiocd test setupcommands, and an Agent Skill for coding agents. See Testing.
0.0
0.0.92
Release date: 11.08.2026
Improvements & Bug Fixes
- Update dependencies to resolve npm audit vulnerabilities.
0.0.91
Release date: 11.08.2026
Improvements & Bug Fixes
- Prefer ZIP component archives when downloading components on macOS.
0.0.90
Release date: 18.06.2026
Improvements & Bug Fixes
- Upgrade the
concurrentlydependency to v10.
0.0.89
Release date: 03.06.2026
Improvements & Bug Fixes
- Remove the original executable before restoring the unsigned executable during code signing.
0.0.88
Release date: 02.06.2026
Improvements & Bug Fixes
- Move the unsigned executable storage from
components/_tempto.iocd-cli/unsigned-exe, keeping the components folder free of signing artifacts.
0.0.87
Release date: 01.06.2026
Improvements & Bug Fixes
- Replace
extract-zipwithadm-zipfor Node.js 24 compatibility.
0.0.86
Release date: 01.06.2026
Improvements & Bug Fixes
- Replace
extract-zipwithadm-zipfor Node.js 24 compatibility.
0.0.85
Release date: 26.05.2026
New Features
- Add support for selecting extra components in the
createcommand.Improvements & Bug Fixes
- Clear temporary folders on macOS when the package name changes.
- Update dependencies to resolve npm audit vulnerabilities.
- Code-sign the
rceditexecutable.
0.0.84
Release date: 07.05.2026
New Features
- Extend the
components downloadcommand to support downloading only specific components with specific versions, rather than downloading all components. This allows more targeted downloads when only certain components are needed.Improvements & Bug Fixes
- Move unsigned executable caching from sign-time to install-time, improving build performance by avoiding redundant caching operations during the code signing phase.
- Change tests to use the
downloadcommand instead of their own implementation, improving test consistency and reducing code duplication.
0.0.83
Release date: 21.04.2026
New Features
- Support forwarding arguments to the test script, allowing users to pass additional options (e.g.,
--spec,--suite) directly to the underlying test runner when executingnpm run test.
0.0.82
Release date: 08.04.2026
Improvements & Bug Fixes
- Add schema validation with static warning and runtime warning, helping users catch configuration errors early by validating the
iocd.cli.config.jsonfile against the JSON schema.- Update dependencies to resolve npm audit vulnerabilities.
0.0.81
Release date: 20.03.2026
Improvements & Bug Fixes
- Fix stable build-tagged releases being filtered out incorrectly when resolving component versions, ensuring that stable releases with build metadata are properly recognized.
0.0.80
Release date: 19.03.2026
New Features
- Add support for
nightlyprerelease tag in component versions (e.g.,v10.0.0-nightly.20250927121929), in addition to the existingbuildanddevtags.- Add
prereleaseChannelsconfig option to restrict which prerelease channels are allowed when resolving"latest"(e.g.,["build"]to only use build tags). See Pre-Release Channels for details.
0.0.79
Release date: 17.03.2026
Bug Fixes
- Resolve npm audit vulnerabilities.
0.0.78
Release date: 16.03.2026
New Features
- Add support for
devprerelease tag in component versions (e.g.,v10.0.0-dev.20250927121929), in addition to the existingbuildtag.
0.0.77
Release date: 16.03.2026
Bug Fixes
- Match prerelease versions via
fullVersionin the version selector, allowing exact prerelease version requests (e.g.,iocd@10.1.0-dev.260314T1935).- CI fixes for npm trusted publisher and prerelease publishing.
0.0.76
Release date: 10.03.2026
New Features
- The
components downloadcommand now downloads only the latest version of each component by default.
0.0.75
Release date: 10.03.2026
New Features
- Do not create an empty
componentsfolder in the packagediocdoutput when no components other thaniocdare included in the project.
0.0.74
Release date: 02.03.2026
New Features
- Add
excludeFoldersoption for Windows code signing — allows specifying glob patterns to exclude folders from code signing. Supports**,*,?wildcards and!negation patterns (last matching pattern wins). By default, no folders are excluded.
0.0.73
Release date: 27.02.2026
New Features
- Skip code signing for binaries that are already signed, reducing build time and avoiding re-signing issues.
Improvements & Bug Fixes
- Apply npm audit fixes to test template package-lock files to resolve security vulnerabilities.
0.0.72
Release date: 30.01.2026
New Features
- Register adapter components (bbg-mdf, excel, fidessa, outlook, word) in the CLI component registry with platform support metadata.
0.0.71
Release date: 29.01.2026
New Features
- Add array merge directives (
$replace,$prepend,$append,$remove) for.json.mergefiles, giving fine-grained control over how arrays are combined during modifications.
0.0.70
Release date: 27.01.2026
Improvements & Bug Fixes
- Trigger a version.
0.0.69
Release date: 22.01.2026
New Features
- Enable cross-compilation of Windows installers on Linux by adding rcedit support and removing platform-specific OS calls.
0.0.68
Release date: 21.01.2026
Improvements & Bug Fixes
- Change platform default to win32 for components (was darwin).
0.0.67
Release date: 21.01.2026
Improvements & Bug Fixes
- Change platform default to win32 for components (was darwin).
0.0.66
Release date: 14.01.2026
Improvements & Bug Fixes
- Fix Windows executable icon patching to source the icon from the modifications folder instead of the default location.
0.0.65
Release date: 13.01.2026
Improvements & Bug Fixes
- Fix modification tracker so that deleting a modification source file correctly removes the corresponding output file from the component.
0.0.64
Release date: 10.01.2026
Improvements & Bug Fixes
- Replace OS-specific shell-based ZIP extraction (PowerShell on Windows, unzip on macOS) with the cross-platform
extract-zipnpm package.
0.0.63
Release date: 09.01.2026
New Features
- Read
entitlements.mac.plistand.nuspectemplatefrom the CLI. These files were previously in the templates dir, now are bundled with the CLI and read from there. This allows easier updates and less confusion when users create new projects.
0.0.62
Release date: 05.01.2026
New Features
- Support for specifying a custom tests directory in the CLI configuration.
0.0.60
Release date: 31.12.2025
New Features
- Automatically reset all modifications before running
devorbuildcommands to ensure a clean state.- Display notification when a newer version of the CLI is available.
Improvements & Bug Fixes
- Relaxed config schema validation to allow partial configuration files.
- Updated
.gitignorehandling to only ignore the top-levelcomponentsfolder, not nested ones.
0.0.59
Release date: 12.12.2025
New Features
- Support passing additional arguments to the Electron process in
devmode.- New
--outputargument for thebuildcommand to specify a custom output directory.- Export CLI commands for use as a library in other projects.
Improvements & Bug Fixes
- Automatically install dependencies when adding new apps to a project.
- Updated Launchpad Vite configuration for improved build performance.
0.0.57
Release date: 09.12.2025
New Features
- Auto include tests when setting up a new project with
createcommand.
0.0.56
Release date: 06.12.2025
Improvements & Bug Fixes
- Improved error logging message.
0.0.55
Release date: 06.12.2025
Improvements & Bug Fixes
- Fix for versions that contain a letter in the build timestamp.
0.0.54
Release date: 04.12.2025
New Features
- Add HTTP/HTTPS proxy support for all CLI network requests, enabling use behind corporate proxies and firewalls.
0.0.53
Release date: 01.12.2025
New Features
- Log Node.js and CLI versions at the beginning of each log file for easier debugging.
0.0.52
Release date: 30.11.2025
New Features
- Register the Bloomberg (BBG) component in the CLI component registry for download and inclusion in builds.
0.0.51
Release date: 28.11.2025
New Features
- Add an overwrite mode for component modifications, allowing files to completely replace targets instead of being merged.
0.0.49
Release date: 26.11.2025
Improvements & Bug Fixes
- Change configs to rely on
package.jsonwhere possible.- Add UI version.
- Updates in wdio.config for tests.
- Fix for eslint setup.
0.0.43
Release date: 20.11.2025
New Features
- Support variable substitution of product slug and product name in modification files, enabling dynamic per-product configuration.
0.0.42
Release date: 20.11.2025
New Features
- Add OS-specific modification folders (
modifications-win32,modifications-darwin) so different platforms can have distinct configuration overrides.- Embed product version in the Windows setup executable filename.
Improvements & Bug Fixes
- Various fixes; fix build.
0.0.35
Release date: 16.11.2025
Improvements & Bug Fixes
- Update Node.js requirement to 22.0.0.
- Minor fix.
0.0.33
Release date: 16.11.2025
Improvements & Bug Fixes
- Updated README.
- Simplify config.
- Changed pfx to signtool.
0.0.29
Release date: 13.11.2025
New Features
- Enable shell mode for child process execution to support system PATH resolution.
0.0.28
Release date: 13.11.2025
Improvements & Bug Fixes
- Using signtool for signing; remove explicit digicert support.
0.0.27
Release date: 10.11.2025
New Features
- Add
license.copyLicenseToOutputconfig option to automatically include the license file in the build output.
0.0.26
Release date: 06.11.2025
New Features
- Add end-to-end test infrastructure for CLI commands.
- Add license file handling and validation.
- Add JSON schema for CLI configuration file (
iocd.cli.config.json) validation.
0.0.24
Release date: 30.10.2025
New Features
- Add end-to-end test support with small improvements.
- Basic test scaffolding.
- Download all components.
0.0.23
Release date: 15.10.2025
Improvements & Bug Fixes
- Logger fix.
- Setup Apple code-signing.
0.0.22
Release date: 13.10.2025
Improvements & Bug Fixes
- Fix icons and ability to sign using certificate on Mac.
0.0.20
Release date: 12.10.2025
New Features
- Automatically generate and manage
.gitignorefiles for new projects.
0.0.18
Release date: 12.10.2025
New Features
- Allow the
createcommand to initialize a project in the current directory if it is empty.
0.0.17
Release date: 12.10.2025
New Features
- Add build configuration templates for Windows and macOS installers.
0.0.15
Release date: 11.10.2025
New Features
- Add
--skip-componentsCLI flag to the build command, allowing builds without downloading or updating components.
0.0.14
Release date: 11.10.2025
Improvements & Bug Fixes
- Add GitHub token to the GitHub request.
0.0.9
Release date: 09.10.2025
New Features
- Add support for top-level modifications that apply across all components, in addition to per-component modifications.
0.0.6
Release date: 07.10.2025
New Features
- Integrate Sentry error tracking for automatic crash reporting and diagnostics.
- Add internal presentation materials.
0.0.4
Release date: 04.10.2025
New Features
- Pin component versions after initial project setup to ensure reproducible builds.
- Add per-component modification folders (
modifications-xxx) for targeted config overrides.- Auto-update support with
system.json.mergetemplate generation.
0.0.3
Release date: 28.09.2025
New Features
- Added demos component support.
- Add flag for prerelease.
- Added doc about components storage.
0.0.2
Release date: 26.09.2025
New Features
- License checks.
- Licensing.
0.0.1
Release date: 22.09.2025
New Features
- Initial release.
- CLI project setup with create command.
- Components selection in the create phase.
- Ability to add apps.
- S3 support for components storage.
- Local storage support with same structure as S3.
- Launchpad component.
- Components remove functionality.
- Installer/build command for Windows and macOS.
- Code signing support for macOS and Windows.
- Splash screen template.
- Auto-update configuration support.
- GitHub workflow for CI/CD.