@google-cloud/cloud-run-mcp 1.8.0 → 1.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -32,15 +32,16 @@ import {
32
32
  } from './helpers.js';
33
33
  import {
34
34
  prepareSourceDirectory,
35
- installDependencies,
36
35
  cleanupTempDirectory,
37
36
  } from './source-processor.js';
37
+ import { runUniversalMaker } from './universal-maker.js';
38
38
 
39
39
  import {
40
40
  DEPLOYMENT_CONFIG,
41
41
  DEPLOYMENT_TYPES,
42
42
  MAX_ALLOWED_DIRECT_SOURCE_SIZE_BYTES,
43
43
  REQUIRED_APIS,
44
+ RUNTIMES,
44
45
  } from './constants.js';
45
46
 
46
47
  /**
@@ -66,10 +67,11 @@ async function deployToCloudRun(
66
67
  progressCallback,
67
68
  skipIamCheck,
68
69
  deploymentType,
70
+ accessToken,
69
71
  zippedSourceContainer,
70
72
  runtime
71
73
  ) {
72
- const runClient = await getRunClient(projectId);
74
+ const runClient = await getRunClient(projectId, accessToken);
73
75
  const parent = runClient.locationPath(projectId, location);
74
76
  const servicePath = runClient.servicePath(projectId, location, serviceId);
75
77
  const revisionName = `${serviceId}-${Date.now()}`; // Generate a unique revision name
@@ -101,6 +103,7 @@ async function deployToCloudRun(
101
103
  projectId,
102
104
  location,
103
105
  serviceId,
106
+ accessToken,
104
107
  progressCallback
105
108
  );
106
109
 
@@ -245,7 +248,8 @@ async function deployWithZip(
245
248
  bucketName,
246
249
  deploymentAttrs,
247
250
  progressCallback,
248
- skipIamCheck
251
+ skipIamCheck,
252
+ accessToken
249
253
  ) {
250
254
  await logAndProgress(
251
255
  `Attempting direct source deployment...`,
@@ -256,7 +260,18 @@ async function deployWithZip(
256
260
 
257
261
  try {
258
262
  tempDir = await prepareSourceDirectory(files);
259
- await installDependencies(tempDir, deploymentAttrs);
263
+
264
+ if (
265
+ deploymentAttrs.runtime === RUNTIMES.NODEJS ||
266
+ deploymentAttrs.runtime === RUNTIMES.PYTHON
267
+ ) {
268
+ deploymentAttrs = await updateDeploymentAttrsWithUniversalMaker(
269
+ tempDir,
270
+ deploymentAttrs,
271
+ accessToken,
272
+ progressCallback
273
+ );
274
+ }
260
275
 
261
276
  const archiveBuffer = await zipFiles([tempDir], true, progressCallback);
262
277
  const archiveName = DEPLOYMENT_CONFIG.TARGZ_FILE_NAME;
@@ -276,6 +291,7 @@ async function deployWithZip(
276
291
  projectId,
277
292
  bucketName,
278
293
  region,
294
+ accessToken,
279
295
  progressCallback
280
296
  );
281
297
 
@@ -301,6 +317,7 @@ async function deployWithZip(
301
317
  progressCallback,
302
318
  skipIamCheck,
303
319
  DEPLOYMENT_TYPES.NO_BUILD,
320
+ accessToken,
304
321
  container,
305
322
  deploymentAttrs.runtime
306
323
  );
@@ -311,6 +328,43 @@ async function deployWithZip(
311
328
  }
312
329
  }
313
330
 
331
+ /**
332
+ * Updates deployment attributes using result from Universal Maker.
333
+ *
334
+ * @async
335
+ * @param {string} tempDir - Path to the temporary source directory.
336
+ * @param {object} deploymentAttrs - The initial deployment attributes.
337
+ * @param {function(object): void} [progressCallback] - Optional callback for progress updates.
338
+ * @returns {Promise<object>} A promise that resolves with the updated deployment attributes.
339
+ */
340
+ async function updateDeploymentAttrsWithUniversalMaker(
341
+ tempDir,
342
+ deploymentAttrs,
343
+ accessToken,
344
+ progressCallback
345
+ ) {
346
+ const umResult = await runUniversalMaker(
347
+ tempDir,
348
+ accessToken,
349
+ progressCallback
350
+ );
351
+ if (umResult) {
352
+ await logAndProgress(
353
+ 'Updating deployment attributes using Universal Maker results.',
354
+ progressCallback
355
+ );
356
+ return {
357
+ ...deploymentAttrs,
358
+ baseImage: umResult.runtime || deploymentAttrs.baseImage,
359
+ runtime: umResult.runtime || deploymentAttrs.runtime,
360
+ cmd: umResult.command ? [umResult.command] : deploymentAttrs.cmd,
361
+ args: umResult.args || deploymentAttrs.args,
362
+ envVars: umResult.envVars || {},
363
+ };
364
+ }
365
+ return deploymentAttrs;
366
+ }
367
+
314
368
  /**
315
369
  * Handles deployment using Google Cloud Build.
316
370
  * This function zips the source code, uploads it to Google Cloud Storage,
@@ -337,7 +391,8 @@ async function deployWithBuild(
337
391
  hasDockerfile,
338
392
  bucketName,
339
393
  progressCallback,
340
- skipIamCheck
394
+ skipIamCheck,
395
+ accessToken
341
396
  ) {
342
397
  const imageUrl = `${region}-docker.pkg.dev/${projectId}/${DEPLOYMENT_CONFIG.REPO_NAME}/${serviceName}:${DEPLOYMENT_CONFIG.IMAGE_TAG}`;
343
398
 
@@ -345,6 +400,7 @@ async function deployWithBuild(
345
400
  projectId,
346
401
  bucketName,
347
402
  region,
403
+ accessToken,
348
404
  progressCallback
349
405
  );
350
406
 
@@ -361,6 +417,7 @@ async function deployWithBuild(
361
417
 
362
418
  await ensureArtifactRegistryRepoExists(
363
419
  projectId,
420
+ accessToken,
364
421
  region,
365
422
  DEPLOYMENT_CONFIG.REPO_NAME,
366
423
  'DOCKER',
@@ -377,6 +434,7 @@ async function deployWithBuild(
377
434
  DEPLOYMENT_CONFIG.REPO_NAME,
378
435
  imageUrl,
379
436
  hasDockerfile,
437
+ accessToken,
380
438
  progressCallback
381
439
  );
382
440
 
@@ -389,7 +447,8 @@ async function deployWithBuild(
389
447
  builtImageUrl,
390
448
  progressCallback,
391
449
  skipIamCheck,
392
- DEPLOYMENT_TYPES.WITH_BUILD
450
+ DEPLOYMENT_TYPES.WITH_BUILD,
451
+ accessToken
393
452
  );
394
453
 
395
454
  await logAndProgress(`Deployment completed successfully`, progressCallback);
@@ -414,6 +473,7 @@ export async function deploy({
414
473
  files,
415
474
  progressCallback,
416
475
  skipIamCheck,
476
+ accessToken,
417
477
  }) {
418
478
  if (!projectId) {
419
479
  const errorMsg =
@@ -444,6 +504,7 @@ export async function deploy({
444
504
  await ensureApisEnabled(
445
505
  projectId,
446
506
  REQUIRED_APIS.SOURCE_DEPLOY,
507
+ accessToken,
447
508
  progressCallback
448
509
  );
449
510
 
@@ -470,7 +531,8 @@ export async function deploy({
470
531
  bucketName,
471
532
  deploymentAttrs,
472
533
  progressCallback,
473
- skipIamCheck
534
+ skipIamCheck,
535
+ accessToken
474
536
  );
475
537
  } catch (error) {
476
538
  await logAndProgress(
@@ -489,7 +551,8 @@ export async function deploy({
489
551
  hasDockerfile,
490
552
  bucketName,
491
553
  progressCallback,
492
- skipIamCheck
554
+ skipIamCheck,
555
+ accessToken
493
556
  );
494
557
  } catch (error) {
495
558
  const deployFailedMessage = `Deployment Failed: ${error.message}`;
@@ -518,6 +581,7 @@ export async function deployImage({
518
581
  imageUrl,
519
582
  progressCallback,
520
583
  skipIamCheck,
584
+ accessToken,
521
585
  }) {
522
586
  if (!projectId) {
523
587
  const errorMsg =
@@ -547,6 +611,7 @@ export async function deployImage({
547
611
  await ensureApisEnabled(
548
612
  projectId,
549
613
  REQUIRED_APIS.IMAGE_DEPLOY,
614
+ accessToken,
550
615
  progressCallback
551
616
  );
552
617
 
@@ -562,7 +627,8 @@ export async function deployImage({
562
627
  imageUrl,
563
628
  progressCallback,
564
629
  skipIamCheck,
565
- DEPLOYMENT_TYPES.IMAGE
630
+ DEPLOYMENT_TYPES.IMAGE,
631
+ accessToken
566
632
  );
567
633
 
568
634
  await logAndProgress(`Deployment completed successfully`, progressCallback);
@@ -72,6 +72,18 @@ function checkIfNodeJsRuntime(files) {
72
72
  return fs.existsSync(packageJsonPath);
73
73
  }
74
74
 
75
+ /**
76
+ * Checks if the provided folder appears to be a Python project.
77
+ * It looks for a `requirements.txt` or `pyproject.toml` file in the root of the first path in the `files` array.
78
+ * @param {string[]} files - Array of file paths
79
+ * @returns {boolean}
80
+ */
81
+ function checkIfPythonRuntime(files) {
82
+ const requirementsTxtPath = path.join(files[0], 'requirements.txt');
83
+ const pyprojectTomlPath = path.join(files[0], 'pyproject.toml');
84
+ return fs.existsSync(requirementsTxtPath) || fs.existsSync(pyprojectTomlPath);
85
+ }
86
+
75
87
  /**
76
88
  * Returns an empty deployment attributes object.
77
89
  * This is used as a default or when attributes cannot be determined.
@@ -86,55 +98,30 @@ function getEmptyDeploymentAttrs() {
86
98
  };
87
99
  }
88
100
 
89
- /**
90
- * Extracts deployment attributes (runtime, command, args, baseImage) from a Node.js project's `package.json`.
91
- * This function is used for direct source deployments, bypassing Cloud Build.
92
- * It returns empty attributes if a `start` script is missing, invalid, or if `engines.node` is specified (indicating a need for Cloud Build).
93
- * @param {string[]} files - Array of file paths
94
- * @returns {{runtime: string|undefined, cmd: string[]|undefined, args: string[]|undefined, baseImage: string|undefined}} Deployment attributes object.
95
- */
96
- function getNodeJsDeploymentAttrs(files) {
97
- const packageJsonPath = path.join(files[0], 'package.json');
98
- let packageJson;
99
-
100
- try {
101
- packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf-8'));
102
- } catch (error) {
103
- // If package.json exists but is invalid, we can't extract attributes.
104
- return getEmptyDeploymentAttrs();
105
- }
106
-
107
- const startScript = packageJson?.scripts?.start;
108
- const hasNodeEngine = packageJson?.engines?.node;
109
-
110
- if (typeof startScript !== 'string' || !startScript.trim() || hasNodeEngine) {
111
- return getEmptyDeploymentAttrs();
112
- }
113
-
114
- // Handle multiple spaces properly
115
- const parts = startScript.trim().split(/\s+/);
116
-
117
- return {
118
- runtime: RUNTIMES.NODEJS,
119
- cmd: [parts[0]],
120
- args: parts.slice(1),
121
- baseImage: DEPLOYMENT_CONFIG.DEFAULT_NODE_BASE_IMAGE, // latest default base image
122
- };
123
- }
124
-
125
101
  /**
126
102
  * Determines deployment attributes based on the file list/folder content.
127
103
  * @param {Array<string|Object>} files - Array of file paths or file objects.
128
- * @returns {{runtime: string|undefined, cmd: string[]|undefined, args: string[]|undefined, baseImage: string|undefined}} Deployment attributes object.
104
+ * @returns {{runtime: string|undefined, baseImage: string|undefined}} Deployment attributes object.
129
105
  */
130
106
  function getDeploymentAttrs(files) {
107
+ let deploymentAttrs = getEmptyDeploymentAttrs();
131
108
  // Currently only support detection of NodeJs runtime
132
- if (!isFolder(files) || !checkIfNodeJsRuntime(files)) {
109
+ if (
110
+ !isFolder(files) ||
111
+ !(checkIfNodeJsRuntime(files) || checkIfPythonRuntime(files))
112
+ ) {
133
113
  // TODO: support file list deployment runtime detection
134
- return getEmptyDeploymentAttrs();
114
+ return deploymentAttrs;
135
115
  }
136
116
 
137
- return getNodeJsDeploymentAttrs(files);
117
+ if (checkIfPythonRuntime(files)) {
118
+ deploymentAttrs.runtime = RUNTIMES.PYTHON;
119
+ deploymentAttrs.baseImage = DEPLOYMENT_CONFIG.DEFAULT_PYTHON_BASE_IMAGE;
120
+ } else {
121
+ deploymentAttrs.runtime = RUNTIMES.NODEJS;
122
+ deploymentAttrs.baseImage = DEPLOYMENT_CONFIG.DEFAULT_NODE_BASE_IMAGE;
123
+ }
124
+ return deploymentAttrs;
138
125
  }
139
126
 
140
127
  /**
@@ -157,13 +144,7 @@ export function makeFileDeploymentMetadata(files) {
157
144
  * @returns {boolean} True if zip deployment is feasible.
158
145
  */
159
146
  export function canDeployWithoutBuild({ hasDockerfile, deploymentAttrs }) {
160
- return !!(
161
- !hasDockerfile &&
162
- deploymentAttrs &&
163
- deploymentAttrs.runtime &&
164
- deploymentAttrs.cmd &&
165
- deploymentAttrs.args
166
- );
147
+ return !!(!hasDockerfile && deploymentAttrs && deploymentAttrs.runtime);
167
148
  }
168
149
 
169
150
  /**
@@ -179,7 +160,7 @@ export function createDirectSourceDeploymentContainer({
179
160
  fileName,
180
161
  deploymentAttrs,
181
162
  }) {
182
- return {
163
+ const container = {
183
164
  image: DEPLOYMENT_CONFIG.NO_BUILD_IMAGE_TYPE,
184
165
  baseImageUri: deploymentAttrs.baseImage,
185
166
  sourceCode: {
@@ -191,4 +172,16 @@ export function createDirectSourceDeploymentContainer({
191
172
  command: deploymentAttrs.cmd,
192
173
  args: deploymentAttrs.args,
193
174
  };
175
+
176
+ // Add environment variables if provided
177
+ if (deploymentAttrs.envVars && typeof deploymentAttrs.envVars === 'object') {
178
+ container.env = Object.entries(deploymentAttrs.envVars).map(
179
+ ([name, value]) => ({
180
+ name,
181
+ value: String(value),
182
+ })
183
+ );
184
+ }
185
+
186
+ return container;
194
187
  }
@@ -58,54 +58,6 @@ export async function prepareSourceDirectory(files, progressCallback) {
58
58
  return tempDir;
59
59
  }
60
60
 
61
- /**
62
- * Installs Node.js dependencies in the target directory if a package.json exists.
63
- * Skips if node_modules already exists (assumed pre-installed or copied).
64
- *
65
- * @param {string} targetDir - The directory containing the source code.
66
- */
67
- async function installNodeDependencies(targetDir) {
68
- const nodeModulesPath = path.join(targetDir, 'node_modules');
69
-
70
- if (fs.existsSync(nodeModulesPath)) {
71
- console.log(
72
- `Existing node_modules detected. Skipping dependency installation.`
73
- );
74
- return;
75
- }
76
-
77
- // Check for lock file to decide between install and ci
78
- const hasLockFile = fs.existsSync(path.join(targetDir, 'package-lock.json'));
79
- const npmCommand = hasLockFile
80
- ? 'npm ci --omit=dev'
81
- : 'npm install --omit=dev';
82
-
83
- try {
84
- await execAsync(npmCommand, { cwd: targetDir });
85
- console.log(`${npmCommand} completed successfully`);
86
- } catch (e) {
87
- console.log(`Dependency installation failed: ${e.message}`);
88
- }
89
- }
90
-
91
- /**
92
- * Generic wrapper to install dependencies for the detected runtime.
93
- *
94
- * @param {string} targetDir - The directory containing the source code.
95
- * @param {Object} deploymentAttrs - Deployment attributes containing runtime info.
96
- */
97
- export async function installDependencies(targetDir, deploymentAttrs) {
98
- switch (deploymentAttrs?.runtime) {
99
- case RUNTIMES.NODEJS:
100
- await installNodeDependencies(targetDir);
101
- break;
102
- default:
103
- console.log(
104
- `No specific dependency installation logic for runtime: ${deploymentAttrs?.runtime}`
105
- );
106
- }
107
- }
108
-
109
61
  /**
110
62
  * Cleans up the temporary directory.
111
63
  *
@@ -0,0 +1,274 @@
1
+ /*
2
+ Copyright 2025 Google LLC
3
+
4
+ Licensed under the Apache License, Version 2.0 (the "License");
5
+ you may not use this file except in compliance with the License.
6
+ You may obtain a copy of the License at
7
+
8
+ https://www.apache.org/licenses/LICENSE-2.0
9
+
10
+ Unless required by applicable law or agreed to in writing, software
11
+ distributed under the License is distributed on an "AS IS" BASIS,
12
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ See the License for the specific language governing permissions and
14
+ limitations under the License.
15
+ */
16
+
17
+ import { exec } from 'child_process';
18
+ import fs from 'fs';
19
+ import path from 'path';
20
+ import os from 'os';
21
+ import util from 'util';
22
+ import crypto from 'crypto';
23
+ import { logAndProgress } from '../util/helpers.js';
24
+ import { TEMP_PATHS } from './constants.js';
25
+ import { getArtifactRegistryClient } from '../clients.js';
26
+
27
+ const execAsync = util.promisify(exec);
28
+
29
+ const UNIVERSAL_MAKER_BIN = 'universal_maker';
30
+ const UM_VERSION = '1.0.0';
31
+
32
+ const AR_HOST = 'https://artifactregistry.googleapis.com';
33
+ const AR_PROJECT = 'serverless-runtimes';
34
+ const AR_LOCATION = 'us-central1';
35
+ const AR_REPOSITORY = 'universal-maker';
36
+ const AR_DOWNLOAD_PATH_PREFIX = '/download/v1/';
37
+ const AR_DOWNLOAD_URL_SUFFIX = ':download?alt=media';
38
+
39
+ const ARCH_MAPPING = {
40
+ linux_x64: 'x86-64',
41
+ darwin_arm64: 'darwin-arm64',
42
+ };
43
+
44
+ /**
45
+ * Gets the architecture key for the current platform.
46
+ * @returns {string|null} The architecture key or null if not supported.
47
+ */
48
+ function getUniversalMakerArchitectureKey() {
49
+ const platform = process.platform;
50
+ const arch = process.arch;
51
+
52
+ if (platform === 'linux' && arch === 'x64') {
53
+ return 'linux_x64';
54
+ } else if (platform === 'darwin' && arch === 'arm64') {
55
+ return 'darwin_arm64';
56
+ }
57
+ return null;
58
+ }
59
+
60
+ /**
61
+ * Checks if the existing binary is up to date by comparing its SHA256 mash with the remote metadata.
62
+ * If the binary is out of date, it removes it.
63
+ * @param {string} binPath - Path to the local binary.
64
+ * @param {string} arch - Current architecture.
65
+ * @param {string} accessToken - Access token for authentication.
66
+ * @param {function} progressCallback - Progress callback.
67
+ * @returns {Promise<boolean>} True if the binary is up to date (or if updating fails), false otherwise.
68
+ */
69
+ async function isBinaryUpToDate(binPath, arch, accessToken, progressCallback) {
70
+ if (!fs.existsSync(binPath)) {
71
+ return false;
72
+ }
73
+
74
+ try {
75
+ await logAndProgress(
76
+ 'Checking for Universal Maker updates...',
77
+ progressCallback,
78
+ 'debug'
79
+ );
80
+
81
+ const artifactRegistryClient = await getArtifactRegistryClient(
82
+ AR_PROJECT,
83
+ accessToken
84
+ );
85
+ const resourceName = artifactRegistryClient.filePath(
86
+ AR_PROJECT,
87
+ AR_LOCATION,
88
+ AR_REPOSITORY,
89
+ `${arch}:${UM_VERSION}:${UNIVERSAL_MAKER_BIN}`
90
+ );
91
+
92
+ const [file] = await artifactRegistryClient.getFile({ name: resourceName });
93
+ const remoteSha256Base64 = file.hashes?.find(
94
+ (h) => h.type === 'SHA256'
95
+ )?.value;
96
+
97
+ if (!remoteSha256Base64) {
98
+ return false; // Cannot verify, download new binary
99
+ }
100
+
101
+ const remoteSha256Hex = Buffer.from(remoteSha256Base64, 'base64').toString(
102
+ 'hex'
103
+ );
104
+ const localFileContent = fs.readFileSync(binPath);
105
+ const localSha256Hex = crypto
106
+ .createHash('sha256')
107
+ .update(localFileContent)
108
+ .digest('hex');
109
+
110
+ if (remoteSha256Hex === localSha256Hex) {
111
+ await logAndProgress(
112
+ 'Universal Maker binary is up to date.',
113
+ progressCallback,
114
+ 'debug'
115
+ );
116
+ return true;
117
+ }
118
+
119
+ await logAndProgress(
120
+ 'Universal Maker binary is out of date. Removing existing binary.',
121
+ progressCallback,
122
+ 'debug'
123
+ );
124
+ fs.unlinkSync(binPath);
125
+ return false;
126
+ } catch (error) {
127
+ await logAndProgress(
128
+ `Could not check for Universal Maker updates: ${error.message}. Downloading the new binary.`,
129
+ progressCallback,
130
+ 'debug'
131
+ );
132
+ return false; // On error, fallback to downloading the new binary
133
+ }
134
+ }
135
+
136
+ /**
137
+ * Downloads the universal maker binary if it doesn't exist.
138
+ * @param {string} binDir - Directory to store the binary.
139
+ * @param {string} accessToken - Access token for authentication.
140
+ * @param {function} progressCallback - Progress callback.
141
+ * @returns {Promise<string|null>} Path to the binary or null if not supported.
142
+ */
143
+ async function ensureUniversalMaker(binDir, accessToken, progressCallback) {
144
+ const key = getUniversalMakerArchitectureKey();
145
+
146
+ if (!key) {
147
+ await logAndProgress(
148
+ `Universal Maker is not supported on ${process.platform} ${process.arch}.`,
149
+ progressCallback,
150
+ 'debug'
151
+ );
152
+ return null;
153
+ }
154
+
155
+ const binPath = path.join(binDir, UNIVERSAL_MAKER_BIN);
156
+ const arch = ARCH_MAPPING[key];
157
+
158
+ if (await isBinaryUpToDate(binPath, arch, accessToken, progressCallback)) {
159
+ return binPath;
160
+ }
161
+
162
+ await logAndProgress(`Downloading Universal Maker...`, progressCallback);
163
+
164
+ try {
165
+ if (!fs.existsSync(binDir)) {
166
+ fs.mkdirSync(binDir, { recursive: true });
167
+ }
168
+
169
+ const artifactRegistryClient = await getArtifactRegistryClient(
170
+ AR_PROJECT,
171
+ accessToken
172
+ );
173
+ const resourceName = artifactRegistryClient.filePath(
174
+ AR_PROJECT,
175
+ AR_LOCATION,
176
+ AR_REPOSITORY,
177
+ `${arch}:${UM_VERSION}:${UNIVERSAL_MAKER_BIN}`
178
+ );
179
+
180
+ const url = `${AR_HOST}${AR_DOWNLOAD_PATH_PREFIX}${resourceName}${AR_DOWNLOAD_URL_SUFFIX}`;
181
+ const response = await artifactRegistryClient.auth.request({
182
+ url,
183
+ responseType: 'stream',
184
+ });
185
+
186
+ const dest = fs.createWriteStream(binPath);
187
+ await new Promise((resolve, reject) => {
188
+ response.data.pipe(dest).on('finish', resolve).on('error', reject);
189
+ });
190
+
191
+ fs.chmodSync(binPath, '755');
192
+ return binPath;
193
+ } catch (error) {
194
+ await logAndProgress(
195
+ `Failed to download Universal Maker: ${error.message}`,
196
+ progressCallback,
197
+ 'debug'
198
+ );
199
+ return null;
200
+ }
201
+ }
202
+
203
+ /**
204
+ * Runs Universal Maker on the given application directory.
205
+ * @param {string} appDir - Directory containing the application source.
206
+ * @param {function} progressCallback - Progress callback.
207
+ * @param {string} accessToken - Access token for authentication.
208
+ * @returns {Promise<object|null>} The parsed build_output.json or null.
209
+ */
210
+ export async function runUniversalMaker(appDir, accessToken, progressCallback) {
211
+ const binDir = path.join(
212
+ os.homedir(),
213
+ TEMP_PATHS.BASE,
214
+ TEMP_PATHS.BIN_SUBDIR
215
+ );
216
+ const binPath = await ensureUniversalMaker(
217
+ binDir,
218
+ accessToken,
219
+ progressCallback
220
+ );
221
+
222
+ if (!binPath) {
223
+ return null;
224
+ }
225
+
226
+ const outputDir = path.join(os.tmpdir(), `um-output-${Date.now()}`);
227
+ if (!fs.existsSync(outputDir)) {
228
+ fs.mkdirSync(outputDir, { recursive: true });
229
+ }
230
+
231
+ try {
232
+ // ./universal_maker -application_dir <source_code_path> -output_dir <output_results_path> -output_format json
233
+ await logAndProgress(
234
+ `Running Universal Maker: ${binPath} -application_dir ${appDir} -output_dir ${outputDir} -output_format json`,
235
+ progressCallback,
236
+ 'debug'
237
+ );
238
+ const command = `"${binPath}" -application_dir "${appDir}" -output_dir "${outputDir}" -output_format json`;
239
+ await execAsync(command);
240
+
241
+ const outputPath = path.join(outputDir, 'build_output.json');
242
+ if (fs.existsSync(outputPath)) {
243
+ const output = JSON.parse(fs.readFileSync(outputPath, 'utf-8'));
244
+ await logAndProgress(
245
+ 'Universal Maker completed successfully.',
246
+ progressCallback
247
+ );
248
+ return output;
249
+ } else {
250
+ await logAndProgress(
251
+ 'Universal Maker did not produce build_output.json',
252
+ progressCallback,
253
+ 'debug'
254
+ );
255
+ return null;
256
+ }
257
+ } catch (error) {
258
+ await logAndProgress(
259
+ `Universal Maker failed: ${error.message}`,
260
+ progressCallback,
261
+ 'debug'
262
+ );
263
+ return null;
264
+ } finally {
265
+ // Cleanup output dir
266
+ try {
267
+ if (fs.existsSync(outputDir)) {
268
+ fs.rmSync(outputDir, { recursive: true, force: true });
269
+ }
270
+ } catch (e) {
271
+ // ignore cleanup errors
272
+ }
273
+ }
274
+ }