Skip to content

Commit a509375

Browse files
unity-cli@v1.2.0
- Refactor LicenseClient.Active with a new ActivateOptions interface for input parameters - Refactor Hub installation on windows to invoke UAC when installing hub - Added command groups when running help command - Added list-project-templates command to list available templates for a given unity editor
1 parent 57dd46d commit a509375

8 files changed

Lines changed: 926 additions & 105 deletions

File tree

package-lock.json

Lines changed: 715 additions & 4 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@rage-against-the-pixel/unity-cli",
3-
"version": "1.1.3",
3+
"version": "1.2.0",
44
"description": "A command line utility for the Unity Game Engine.",
55
"author": "RageAgainstThePixel",
66
"license": "MIT",
@@ -54,12 +54,14 @@
5454
"glob": "11.0.3",
5555
"semver": "^7.7.2",
5656
"source-map-support": "^0.5.21",
57+
"update-notifier": "^7.3.1",
5758
"yaml": "^2.8.1"
5859
},
5960
"devDependencies": {
6061
"@types/jest": "^30.0.0",
6162
"@types/node": "^24.6.2",
6263
"@types/semver": "^7.7.1",
64+
"@types/update-notifier": "^6.0.8",
6365
"jest": "^30.2.0",
6466
"ts-jest": "^29.4.4",
6567
"ts-node": "^10.9.2",

src/cli.ts

Lines changed: 101 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -13,11 +13,15 @@ import { UnityVersion } from './unity-version';
1313
import { UnityProject } from './unity-project';
1414
import { CheckAndroidSdkInstalled } from './android-sdk';
1515
import { UnityEditor } from './unity-editor';
16+
import updateNotifier from "update-notifier";
1617

1718
const pkgPath = join(__dirname, '..', 'package.json');
1819
const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf-8'));
20+
updateNotifier({ pkg }).notify();
1921
const program = new Command();
2022

23+
program.commandsGroup('Auth:');
24+
2125
program.name('unity-cli')
2226
.description('A command line utility for the Unity Game Engine.')
2327
.version(pkg.version);
@@ -71,7 +75,13 @@ program.command('activate-license')
7175
}
7276
}
7377

74-
await client.Activate(licenseType, options.config, options.serial, options.email, options.password);
78+
await client.Activate({
79+
licenseType,
80+
servicesConfig: options.config,
81+
serial: options.serial,
82+
username: options.email,
83+
password: options.password
84+
});
7585
});
7686

7787
program.command('return-license')
@@ -101,6 +111,8 @@ program.command('return-license')
101111
await client.Deactivate(licenseType);
102112
});
103113

114+
program.commandsGroup('Unity Hub:');
115+
104116
program.command('hub-version')
105117
.description('Print the version of the Unity Hub.')
106118
.action(async () => {
@@ -124,7 +136,7 @@ program.command('hub-install')
124136
const unityHub = new UnityHub();
125137
const hubPath = await unityHub.Install(options.autoUpdate === true);
126138

127-
Logger.instance.setEnvironmentVariable('UNITY_HUB_PATH', hubPath);
139+
Logger.instance.CI_setEnvironmentVariable('UNITY_HUB_PATH', hubPath);
128140

129141
if (options.json) {
130142
process.stdout.write(`\n${JSON.stringify({ UNITY_HUB_PATH: hubPath })}\n`);
@@ -139,7 +151,7 @@ program.command('hub-path')
139151
.action(async (options) => {
140152
const hub = new UnityHub();
141153

142-
Logger.instance.setEnvironmentVariable('UNITY_HUB_PATH', hub.executable);
154+
Logger.instance.CI_setEnvironmentVariable('UNITY_HUB_PATH', hub.executable);
143155

144156
if (options.json) {
145157
process.stdout.write(`\n${JSON.stringify({ UNITY_HUB_PATH: hub.executable })}\n`);
@@ -234,11 +246,93 @@ program.command('setup-unity')
234246
}
235247

236248
for (const [key, value] of Object.entries(output)) {
237-
Logger.instance.setEnvironmentVariable(key, value);
249+
if (value && value.length > 0) {
250+
Logger.instance.CI_setEnvironmentVariable(key, value);
251+
}
238252
}
239253

240254
if (options.json) {
241255
process.stdout.write(`\n${JSON.stringify(output)}\n`);
256+
} else {
257+
process.stdout.write(`Unity setup complete!\n`);
258+
for (const [key, value] of Object.entries(output)) {
259+
if (value && value.length > 0) {
260+
process.stdout.write(`${key}=${value}\n`);
261+
}
262+
}
263+
}
264+
});
265+
266+
program.commandsGroup('Unity Editor:');
267+
268+
269+
program.command('run')
270+
.description('Run command line args directly to the Unity Editor.')
271+
.option('--unity-editor <unityEditorPath>', 'The path to the Unity Editor executable. If unspecified, the UNITY_EDITOR_PATH environment variable must be set.')
272+
.option('--unity-project <unityProjectPath>', 'The path to a Unity project. If unspecified, the UNITY_PROJECT_PATH environment variable or the current working directory will be used.')
273+
.option('--log-name <logName>', 'The name of the log file.')
274+
.allowUnknownOption(true)
275+
.argument('<args...>', 'Arguments to pass to the Unity Editor executable.')
276+
.option('--verbose', 'Enable verbose logging.')
277+
.action(async (args: string[], options) => {
278+
if (options.verbose) {
279+
Logger.instance.logLevel = LogLevel.DEBUG;
280+
}
281+
282+
Logger.instance.debug(JSON.stringify({ options, args }));
283+
284+
const editorPath = options.unityEditor?.toString()?.trim() || process.env.UNITY_EDITOR_PATH;
285+
286+
if (!editorPath || editorPath.length === 0) {
287+
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.');
288+
}
289+
290+
const unityEditor = new UnityEditor(editorPath);
291+
const projectPath = options.unityProject?.toString()?.trim() || process.env.UNITY_PROJECT_PATH || process.cwd();
292+
const unityProject = await UnityProject.GetProject(projectPath);
293+
294+
if (!unityProject) {
295+
throw new Error(`The specified path is not a valid Unity project: ${projectPath}`);
296+
}
297+
298+
if (!args.includes('-logFile')) {
299+
const logPath = unityEditor.GenerateLogFilePath(unityProject.projectPath, options.logName);
300+
args.push('-logFile', logPath);
301+
}
302+
303+
await unityEditor.Run({
304+
args: [...args]
305+
});
306+
});
307+
308+
program.command('list-project-templates')
309+
.description('List all available project templates for the given Unity editor.')
310+
.option('--unity-editor <unityEditorPath>', 'The path to the Unity Editor executable. If unspecified, the UNITY_EDITOR_PATH environment variable must be set.')
311+
.option('--verbose', 'Enable verbose logging.')
312+
.option('--json', 'Prints the last line of output as JSON string.')
313+
.action(async (options) => {
314+
if (options.verbose) {
315+
Logger.instance.logLevel = LogLevel.DEBUG;
316+
}
317+
318+
Logger.instance.debug(JSON.stringify(options));
319+
320+
const editorPath = options.unityEditor?.toString()?.trim() || process.env.UNITY_EDITOR_PATH;
321+
322+
if (!editorPath || editorPath.length === 0) {
323+
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.');
324+
}
325+
326+
const unityEditor = new UnityEditor(editorPath);
327+
const templates = unityEditor.GetAvailableTemplates();
328+
329+
if (options.json) {
330+
process.stdout.write(`\n{\"templates\": ${JSON.stringify(templates)}}\n`);
331+
} else {
332+
process.stdout.write(`Available project templates:\n`);
333+
for (const template of templates) {
334+
process.stdout.write(` - ${template}\n`);
335+
}
242336
}
243337
});
244338

@@ -289,50 +383,13 @@ program.command('create-project')
289383
]
290384
});
291385

292-
Logger.instance.setEnvironmentVariable('UNITY_PROJECT_PATH', projectPath);
386+
Logger.instance.CI_setEnvironmentVariable('UNITY_PROJECT_PATH', projectPath);
293387

294388
if (options.json) {
295389
process.stdout.write(`\n${JSON.stringify({ UNITY_PROJECT_PATH: projectPath })}\n`);
390+
} else {
391+
process.stdout.write(`Unity project created at: ${projectPath}\n`);
296392
}
297393
});
298394

299-
program.command('run')
300-
.description('Run command line args directly to the Unity Editor.')
301-
.option('--unity-editor <unityEditorPath>', 'The path to the Unity Editor executable. If unspecified, the UNITY_EDITOR_PATH environment variable must be set.')
302-
.option('--unity-project <unityProjectPath>', 'The path to a Unity project. If unspecified, the UNITY_PROJECT_PATH environment variable or the current working directory will be used.')
303-
.option('--log-name <logName>', 'The name of the log file.')
304-
.allowUnknownOption(true)
305-
.argument('<args...>', 'Arguments to pass to the Unity Editor executable.')
306-
.option('--verbose', 'Enable verbose logging.')
307-
.action(async (args: string[], options) => {
308-
if (options.verbose) {
309-
Logger.instance.logLevel = LogLevel.DEBUG;
310-
}
311-
312-
Logger.instance.debug(JSON.stringify({ options, args }));
313-
314-
const editorPath = options.unityEditor?.toString()?.trim() || process.env.UNITY_EDITOR_PATH;
315-
316-
if (!editorPath || editorPath.length === 0) {
317-
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.');
318-
}
319-
320-
const unityEditor = new UnityEditor(editorPath);
321-
const projectPath = options.unityProject?.toString()?.trim() || process.env.UNITY_PROJECT_PATH || process.cwd();
322-
const unityProject = await UnityProject.GetProject(projectPath);
323-
324-
if (!unityProject) {
325-
throw new Error(`The specified path is not a valid Unity project: ${projectPath}`);
326-
}
327-
328-
if (!args.includes('-logFile')) {
329-
const logPath = unityEditor.GenerateLogFilePath(unityProject.projectPath, options.logName);
330-
args.push('-logFile', logPath);
331-
}
332-
333-
await unityEditor.Run({
334-
args: [...args]
335-
});
336-
});
337-
338395
program.parse(process.argv);

src/license-client.ts

Lines changed: 30 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,19 @@ export enum LicenseType {
1212
floating = 'floating'
1313
}
1414

15+
export interface ActivateOptions {
16+
/** The type of license to activate */
17+
licenseType: LicenseType;
18+
/** Base64 encoded services configuration json */
19+
servicesConfig?: string;
20+
/** The license serial number */
21+
serial?: string;
22+
/** The Unity ID username (email address) */
23+
username?: string;
24+
/** The Unity ID password */
25+
password?: string;
26+
}
27+
1528
export class LicensingClient {
1629
private readonly unityHub: UnityHub = new UnityHub();
1730
private readonly logger: Logger = Logger.instance;
@@ -297,7 +310,7 @@ export class LicensingClient {
297310
private maskSerialInOutput(output: string): string {
298311
return output.replace(/([\w-]+-XXXX)/g, (_, serial) => {
299312
const maskedSerial = serial.slice(0, -4) + `XXXX`;
300-
this.logger.mask(maskedSerial);
313+
this.logger.CI_mask(maskedSerial);
301314
return serial;
302315
});
303316
}
@@ -311,24 +324,21 @@ export class LicensingClient {
311324

312325
/**
313326
* Activates a Unity license.
314-
* @param licenseType The type of license to activate.
315-
* @param servicesConfig The services config path for floating licenses.
316-
* @param serial The license serial number.
317-
* @param username The Unity ID username.
318-
* @param password The Unity ID password.
327+
* @param options The activation options including license type, services config, serial, username, and password.
328+
* @returns A promise that resolves when the license is activated.
319329
* @throws Error if activation fails or required parameters are missing.
320330
*/
321-
public async Activate(licenseType: LicenseType, servicesConfig: string | undefined = undefined, serial: string | undefined = undefined, username: string | undefined = undefined, password: string | undefined = undefined): Promise<void> {
331+
public async Activate(options: ActivateOptions): Promise<void> {
322332
let activeLicenses = await this.GetActiveEntitlements();
323333

324-
if (activeLicenses.includes(licenseType)) {
325-
this.logger.info(`License of type '${licenseType}' is already active, skipping activation`);
334+
if (activeLicenses.includes(options.licenseType)) {
335+
this.logger.info(`License of type '${options.licenseType}' is already active, skipping activation`);
326336
return;
327337
}
328338

329-
switch (licenseType) {
339+
switch (options.licenseType) {
330340
case LicenseType.floating: {
331-
if (!servicesConfig) {
341+
if (!options.servicesConfig) {
332342
throw new Error('Services config path is required for floating license activation');
333343
}
334344

@@ -348,41 +358,40 @@ export class LicensingClient {
348358
}
349359

350360
const servicesConfigPath = path.join(servicesPath, 'services-config.json');
351-
await fs.promises.writeFile(servicesConfigPath, Buffer.from(servicesConfig, 'base64'));
352-
return;
361+
await fs.promises.writeFile(servicesConfigPath, Buffer.from(options.servicesConfig, 'base64'));
353362
}
354363
default: { // personal and professional license activation
355-
if (!username) {
364+
if (!options.username) {
356365
const encodedUsername = process.env.UNITY_USERNAME_BASE64;
357366

358367
if (!encodedUsername) {
359368
throw Error('Username is required for Unity License Activation!');
360369
}
361370

362-
username = Buffer.from(encodedUsername, 'base64').toString('utf-8');
371+
options.username = Buffer.from(encodedUsername, 'base64').toString('utf-8');
363372
}
364373

365374
const emailRegex: RegExp = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
366375

367-
if (username.length === 0 || !emailRegex.test(username)) {
376+
if (options.username.length === 0 || !emailRegex.test(options.username)) {
368377
throw Error('Username must be your Unity ID email address!');
369378
}
370379

371-
if (!password) {
380+
if (!options.password) {
372381
const encodedPassword = process.env.UNITY_PASSWORD_BASE64;
373382

374383
if (!encodedPassword) {
375384
throw Error('Password is required for Unity License Activation!');
376385
}
377386

378-
password = Buffer.from(encodedPassword, 'base64').toString('utf-8');
387+
options.password = Buffer.from(encodedPassword, 'base64').toString('utf-8');
379388
}
380389

381-
if (password.length === 0) {
390+
if (options.password.length === 0) {
382391
throw Error('Password is required for Unity License Activation!');
383392
}
384393

385-
await this.activateLicense(licenseType, username, password, serial);
394+
await this.activateLicense(options.licenseType, options.username, options.password, options.serial);
386395
}
387396
}
388397
}
@@ -445,7 +454,7 @@ export class LicensingClient {
445454
serial = serial.trim();
446455
args.push(`--serial`, serial);
447456
const maskedSerial = serial.slice(0, -4) + `XXXX`;
448-
this.logger.mask(maskedSerial);
457+
this.logger.CI_mask(maskedSerial);
449458
}
450459

451460
if (licenseType === LicenseType.personal) {

src/logging.ts

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -141,7 +141,7 @@ export class Logger {
141141
* Masks a string in the console output in CI environments that support it.
142142
* @param message The string to mask.
143143
*/
144-
public mask(message: string): void {
144+
public CI_mask(message: string): void {
145145
switch (this._ci) {
146146
case 'GITHUB_ACTIONS': {
147147
process.stdout.write(`::add-mask::${message}\n`);
@@ -150,7 +150,12 @@ export class Logger {
150150
}
151151
}
152152

153-
public setEnvironmentVariable(name: string, value: string): void {
153+
/**
154+
* Sets an environment variable in CI environments that support it.
155+
* @param name The name of the environment variable.
156+
* @param value The value of the environment variable.
157+
*/
158+
public CI_setEnvironmentVariable(name: string, value: string): void {
154159
switch (this._ci) {
155160
case 'GITHUB_ACTIONS': {
156161
// needs to be appended to the temporary file specified in the GITHUB_ENV environment variable
@@ -164,7 +169,7 @@ export class Logger {
164169
}
165170
}
166171

167-
public setOutput(name: string, value: string): void {
172+
public CI_setOutput(name: string, value: string): void {
168173
switch (this._ci) {
169174
case 'GITHUB_ACTIONS': {
170175
// needs to be appended to the temporary file specified in the GITHUB_OUTPUT environment variable

0 commit comments

Comments
 (0)