Skip to content
9 changes: 9 additions & 0 deletions .github/workflows/build-options.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,11 @@
],
"unity-version": [
"4.7.2",
"5.6.7f1 (e80cc3114ac1)",
"2017.x",
"2018.x",
"2019.x",
"2020.x",
"2021.x",
"2022.x",
"6000.0.x",
Expand Down Expand Up @@ -34,6 +39,10 @@
{
"os": "macos-latest",
"unity-version": "4.7.2"
},
{
"os": "ubuntu-latest",
"unity-version": "5.6.7f1 (e80cc3114ac1)"
}
]
}
2 changes: 1 addition & 1 deletion .github/workflows/integration-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ jobs:
permissions:
contents: read
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v5
with:
sparse-checkout: .github/
- uses: RageAgainstThePixel/job-builder@v1
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ jobs:
permissions:
contents: read
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v5
- uses: actions/setup-node@v4
with:
node-version: 24.x
Expand Down
22 changes: 19 additions & 3 deletions .github/workflows/unity-build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,9 @@ jobs:
timeout-minutes: 30
env:
UNITY_PROJECT_PATH: '' # Set from create-project step
RUN_BUILD: '' # Set to true if the build pipeline package can be installed and used
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v5
- uses: actions/setup-node@v4
with:
node-version: 24.x
Expand Down Expand Up @@ -73,15 +74,30 @@ jobs:
echo "Error: UNITY_PROJECT_PATH is not set"
exit 1
fi
# check if the project can be built. Only Unity 2019.4+ and newer majors support the build pipeline package
version="${{ matrix.unity-version }}"
# extract major and minor (minor may be empty if version is just '2019' etc.)
major=$(echo "$version" | cut -d'.' -f1)
minor=$(echo "$version" | cut -d'.' -f2)
if [ -z "$minor" ]; then
minor=0
fi
# numeric comparison: enable build for major > 2019 or major == 2019 and minor >= 4
if [ "$major" -gt 2019 ] || { [ "$major" -eq 2019 ] && [ "$minor" -ge 4 ]; }; then
echo "Proceeding with build for Unity version $version"
echo "RUN_BUILD=true" >> $GITHUB_ENV
else
echo "Skipping build: Unity version $version does not support the build pipeline package (requires 2019.4+)"
fi
- name: Install OpenUPM and build pipeline package
shell: bash
if: ${{ matrix.unity-version != '4.7.2' && matrix.unity-version != '5.6.7' }}
if: ${{ env.RUN_BUILD == 'true' }}
run: |
npm install -g openupm-cli
cd "${UNITY_PROJECT_PATH}"
openupm add com.utilities.buildpipeline
- name: Build project
if: ${{ matrix.unity-version != '4.7.2' && matrix.unity-version != '5.6.7' }}
if: ${{ env.RUN_BUILD == 'true' }}
shell: bash
run: |
unity-cli run --log-name Validate -quit -nographics -batchmode -executeMethod Utilities.Editor.BuildPipeline.UnityPlayerBuildTools.ValidateProject -importTMProEssentialsAsset
Expand Down
16 changes: 12 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,10 +22,14 @@ npm install -g @rage-against-the-pixel/unity-cli

## Usage

In general, the command structure is:

```bash
unity-cli [command] [options]
unity-cli [command] [options] <args...>
```

With options always using double dashes (`--option`) and arguments passed directly to Unity or Unity Hub commands as they normally would with single dashes (`-arg`). Each option typically has a short alias using a single dash (`-o`), except for commands where we pass through arguments, as those get confused by the command parser.

### Common Commands

#### Auth
Expand Down Expand Up @@ -61,21 +65,25 @@ unity-cli setup-unity --unity-version 2022.3.x --modules android,ios

#### Activate a Unity License

Supports personal, professional, and floating licenses (using a license server configuration).

```bash
unity-cli activate-license --email <your-email> --password <your-password> --serial <your-serial>
unity-cli activate-license --license personal --email <your-email> --password <your-password>
```

#### Create a New Project from a Template

> [!NOTE] Regex patterns are supported for the `--template` option. For example, to create a 3D project with either the standard or cross-platform template, you can use `com.unity.template.3d(-cross-platform)?`.
> [!NOTE]
> Regex patterns are supported for the `--template` option. For example, to create a 3D project with either the standard or cross-platform template, you can use `com.unity.template.3d(-cross-platform)?`.

```bash
unity-cli create-project --name "MyGame" --template com.unity.template.3d(-cross-platform)? --unity-editor <path-to-editor>
```

#### Open a project from the command line

> [!NOTE] If you run this command in the same directory as your Unity project, you can omit the `--unity-project`, `--unity-version`, and `--unity-editor` options.
> [!TIP]
> If you run this command in the same directory as your Unity project, you can omit the `--unity-project`, `--unity-version`, and `--unity-editor` options.

```bash
unity-cli open-project
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@rage-against-the-pixel/unity-cli",
"version": "1.2.1",
"version": "1.2.2",
"description": "A command line utility for the Unity Game Engine.",
"author": "RageAgainstThePixel",
"license": "MIT",
Expand Down
72 changes: 51 additions & 21 deletions src/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,15 +32,16 @@ program.command('license-version')
.action(async () => {
const client = new LicensingClient();
await client.Version();
process.exit(0);
});

program.command('activate-license')
.description('Activate a Unity license.')
.option('-l, --license <license>', 'License type (personal, professional, floating). Required.')
.option('-e, --email <email>', 'Email associated with the Unity account. Required when activating a personal or professional license.')
.option('-p, --password <password>', 'Password for the Unity account. Required when activating a personal or professional license.')
.option('-s, --serial <serial>', 'License serial number. Required when activating a professional license.')
.option('-l, --license <license>', 'License type (personal, professional, floating).')
.option('-c, --config <config>', 'Path to the configuration file. Required when activating a floating license.')
.option('-c, --config <config>', 'Path to the configuration file, or base64 encoded JSON string. Required when activating a floating license.')
.option('--verbose', 'Enable verbose logging.')
.action(async (options) => {
if (options.verbose) {
Expand All @@ -53,13 +54,15 @@ program.command('activate-license')
const licenseStr: string = options.license?.toString()?.trim();

if (!licenseStr || licenseStr.length === 0) {
throw new Error('License type is required. Use -l or --license to specify it.');
Logger.instance.error('License type is required. Use -l or --license to specify it.');
process.exit(1);
}

const licenseType: LicenseType = options.license.toLowerCase() as LicenseType;

if (![LicenseType.personal, LicenseType.professional, LicenseType.floating].includes(licenseType)) {
throw new Error(`Invalid license type: ${licenseType}`);
Logger.instance.error(`Invalid license type: ${licenseType}`);
process.exit(1);
}

if (licenseType !== LicenseType.floating) {
Expand All @@ -83,6 +86,7 @@ program.command('activate-license')
username: options.email,
password: options.password
});
process.exit(0);
});

program.command('return-license')
Expand All @@ -100,16 +104,19 @@ program.command('return-license')
const licenseStr: string = options.license?.toString()?.trim();

if (!licenseStr || licenseStr.length === 0) {
throw new Error('License type is required. Use -l or --license to specify it.');
Logger.instance.error('License type is required. Use -l or --license to specify it.');
process.exit(1);
}

const licenseType: LicenseType = licenseStr.toLowerCase() as LicenseType;

if (![LicenseType.personal, LicenseType.professional, LicenseType.floating].includes(licenseType)) {
throw new Error(`Invalid license type: ${licenseType}`);
Logger.instance.error(`Invalid license type: ${licenseType}`);
process.exit(1);
}

await client.Deactivate(licenseType);
process.exit(0);
});

program.commandsGroup('Unity Hub:');
Expand All @@ -123,6 +130,8 @@ program.command('hub-version')
process.stdout.write(`${version}\n`);
} catch (error) {
process.stdout.write(`${error}\n`);
} finally {
process.exit(0);
}
});

Expand All @@ -148,6 +157,8 @@ program.command('hub-install')
} else {
process.stdout.write(`${hubPath}\n`);
}

process.exit(0);
});

program.command('hub-path')
Expand All @@ -163,13 +174,15 @@ program.command('hub-path')
} else {
process.stdout.write(`${hub.executable}\n`);
}

process.exit(0);
});

program.command('hub')
.description('Run commands directly to the Unity Hub. (You need not to pass --headless or -- to this command).')
.allowUnknownOption(true)
.argument('<args...>', 'Arguments to pass to the Unity Hub executable.')
.option('--verbose', 'Enable verbose logging.')
.allowUnknownOption(true)
.action(async (args: string[], options) => {
if (options.verbose) {
Logger.instance.logLevel = LogLevel.DEBUG;
Expand All @@ -179,6 +192,7 @@ program.command('hub')

const unityHub = new UnityHub();
await unityHub.Exec(args, { silent: false, showCommand: Logger.instance.logLevel === LogLevel.DEBUG });
process.exit(0);
});

program.command('setup-unity')
Expand Down Expand Up @@ -206,7 +220,8 @@ program.command('setup-unity')
}

if (!options.unityVersion && !unityProject) {
throw new Error('You must specify a Unity version or project path with -u, --unity-version, -p, --unity-project.');
Logger.instance.error('You must specify a Unity version or project path with -u, --unity-version, -p, --unity-project.');
process.exit(1);
}

const unityVersion = unityProject?.version ?? new UnityVersion(options.unityVersion, options.changeset);
Expand Down Expand Up @@ -266,6 +281,8 @@ program.command('setup-unity')
}
}
}

process.exit(0);
});

program.command('uninstall-unity')
Expand All @@ -289,6 +306,7 @@ program.command('uninstall-unity')
const unityVersion = new UnityVersion(unityVersionStr, options.changeset, options.arch);
const unityHub = new UnityHub();
const installedEditors = await unityHub.ListInstalledEditors();

if (unityVersion.isLegacy()) {
const installPath = await unityHub.GetInstallPath();
unityEditor = new UnityEditor(path.join(installPath, `Unity ${unityVersion.toString()}`, 'Unity.exe'));
Expand All @@ -299,7 +317,8 @@ program.command('uninstall-unity')
const editorPath = options.unityEditor?.toString()?.trim() || process.env.UNITY_EDITOR_PATH || undefined;

if (!editorPath || editorPath.length === 0) {
throw new Error('You must specify a Unity version or editor path with -u, --unity-version, -e, --unity-editor.');
Logger.instance.error('You must specify a Unity version or editor path with -u, --unity-version, -e, --unity-editor.');
process.exit(1);
}

try {
Expand Down Expand Up @@ -332,11 +351,12 @@ program.command('open-project')
}

Logger.instance.debug(JSON.stringify(options));
const projectPath = options.unityProject?.toString()?.trim() || process.env.UNITY_PROJECT_PATH || process.cwd();
const projectPath = options.unityProject?.toString()?.trim() || process.env.UNITY_PROJECT_PATH || undefined;
const unityProject = await UnityProject.GetProject(projectPath);

if (!unityProject) {
throw new Error(`The specified path is not a valid Unity project: ${projectPath}`);
Logger.instance.error(`The specified path is not a valid Unity project: ${projectPath}`);
process.exit(1);
}

const unityVersion = unityProject?.version ?? new UnityVersion(options.unityVersion, options.changeset);
Expand Down Expand Up @@ -380,7 +400,8 @@ program.command('run')
const unityProject = await UnityProject.GetProject(projectPath);

if (!unityProject) {
throw new Error(`The specified path is not a valid Unity project: ${projectPath}`);
Logger.instance.error(`The specified path is not a valid Unity project: ${projectPath}`);
process.exit(1);
}

if (!unityEditor) {
Expand All @@ -389,7 +410,8 @@ program.command('run')
}

if (!unityEditor) {
throw new Error('The Unity Editor path was not specified. Use --unity-editor to specify it or set the UNITY_EDITOR_PATH environment variable.');
Logger.instance.error('The Unity Editor path was not specified. Use --unity-editor to specify it or set the UNITY_EDITOR_PATH environment variable.');
process.exit(1);
}

if (!args.includes('-logFile')) {
Expand All @@ -400,6 +422,7 @@ program.command('run')
await unityEditor.Run({
args: [...args]
});
process.exit(0);
});

program.command('list-project-templates')
Expand All @@ -418,7 +441,8 @@ program.command('list-project-templates')
const unityVersionStr = options.unityVersion?.toString()?.trim();

if (!unityVersionStr && !options.unityEditor) {
throw new Error('You must specify a Unity version or editor path with -u, --unity-version, -e, --unity-editor.');
Logger.instance.error('You must specify a Unity version or editor path with -u, --unity-version, -e, --unity-editor.');
process.exit(1);
}

let unityEditor: UnityEditor;
Expand All @@ -442,14 +466,13 @@ program.command('list-project-templates')
if (options.json) {
process.stdout.write(`\n${JSON.stringify({ templates })}\n`);
} else {
process.stdout.write(`Available project templates:\n`);
for (const template of templates) {
process.stdout.write(` - ${path.basename(template)}\n`);
}
process.stdout.write(`Available project templates:\n${templates.map(t => ` - ${path.basename(t)}`).join('\n')}\n`);
}
} else {
process.stdout.write('No project templates found for this Unity Editor.\n');
}

process.exit(0);
});

program.command('create-project')
Expand All @@ -471,7 +494,8 @@ program.command('create-project')
const unityVersionStr = options.unityVersion?.toString()?.trim();

if (!unityVersionStr && !options.unityEditor) {
throw new Error('You must specify a Unity version or editor path with -u, --unity-version, -e, --unity-editor.');
Logger.instance.error('You must specify a Unity version or editor path with -u, --unity-version, -e, --unity-editor.');
process.exit(1);
}

let unityEditor: UnityEditor;
Expand All @@ -483,7 +507,8 @@ program.command('create-project')
const editorPath = options.unityEditor?.toString()?.trim() || process.env.UNITY_EDITOR_PATH;

if (!editorPath || editorPath.length === 0) {
throw new Error('The Unity Editor path was not specified. Use -e or --unity-editor to specify it, or set the UNITY_EDITOR_PATH environment variable.');
Logger.instance.error('The Unity Editor path was not specified. Use -e or --unity-editor to specify it, or set the UNITY_EDITOR_PATH environment variable.');
process.exit(1);
}

unityEditor = new UnityEditor(editorPath);
Expand All @@ -506,7 +531,10 @@ program.command('create-project')

if (!unityEditor.version.isLegacy() && options.template && options.template.length > 0) {
const templatePath = unityEditor.GetTemplatePath(options.template);
args.push('-cloneFromTemplate', templatePath);

if (templatePath) {
args.push('-cloneFromTemplate', templatePath);
}
}

await unityEditor.Run({ projectPath, args });
Expand All @@ -518,6 +546,8 @@ program.command('create-project')
} else {
process.stdout.write(`Unity project created at: ${projectPath}\n`);
}

process.exit(0);
});

program.parse(process.argv);
Loading
Loading