@limrun/api 0.41.1 → 0.42.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.
Files changed (86) hide show
  1. package/CHANGELOG.md +8 -0
  2. package/client.d.mts +4 -0
  3. package/client.d.mts.map +1 -1
  4. package/client.d.ts +4 -0
  5. package/client.d.ts.map +1 -1
  6. package/client.js +3 -0
  7. package/client.js.map +1 -1
  8. package/client.mjs +3 -0
  9. package/client.mjs.map +1 -1
  10. package/exec-client.d.mts +34 -1
  11. package/exec-client.d.mts.map +1 -1
  12. package/exec-client.d.ts +34 -1
  13. package/exec-client.d.ts.map +1 -1
  14. package/exec-client.js +18 -4
  15. package/exec-client.js.map +1 -1
  16. package/exec-client.mjs +18 -4
  17. package/exec-client.mjs.map +1 -1
  18. package/folder-sync-ignore.d.mts +8 -0
  19. package/folder-sync-ignore.d.mts.map +1 -1
  20. package/folder-sync-ignore.d.ts +8 -0
  21. package/folder-sync-ignore.d.ts.map +1 -1
  22. package/folder-sync-ignore.js +12 -4
  23. package/folder-sync-ignore.js.map +1 -1
  24. package/folder-sync-ignore.mjs +12 -4
  25. package/folder-sync-ignore.mjs.map +1 -1
  26. package/index.d.mts +2 -1
  27. package/index.d.mts.map +1 -1
  28. package/index.d.ts +2 -1
  29. package/index.d.ts.map +1 -1
  30. package/index.js.map +1 -1
  31. package/index.mjs.map +1 -1
  32. package/package.json +1 -1
  33. package/resources/daemon-client-shared.d.mts +37 -0
  34. package/resources/daemon-client-shared.d.mts.map +1 -0
  35. package/resources/daemon-client-shared.d.ts +37 -0
  36. package/resources/daemon-client-shared.d.ts.map +1 -0
  37. package/resources/daemon-client-shared.js +51 -0
  38. package/resources/daemon-client-shared.js.map +1 -0
  39. package/resources/daemon-client-shared.mjs +45 -0
  40. package/resources/daemon-client-shared.mjs.map +1 -0
  41. package/resources/gradle-instances-helpers.d.mts +54 -0
  42. package/resources/gradle-instances-helpers.d.mts.map +1 -0
  43. package/resources/gradle-instances-helpers.d.ts +54 -0
  44. package/resources/gradle-instances-helpers.d.ts.map +1 -0
  45. package/resources/gradle-instances-helpers.js +98 -0
  46. package/resources/gradle-instances-helpers.js.map +1 -0
  47. package/resources/gradle-instances-helpers.mjs +94 -0
  48. package/resources/gradle-instances-helpers.mjs.map +1 -0
  49. package/resources/gradle-instances.d.mts +149 -0
  50. package/resources/gradle-instances.d.mts.map +1 -0
  51. package/resources/gradle-instances.d.ts +149 -0
  52. package/resources/gradle-instances.d.ts.map +1 -0
  53. package/resources/gradle-instances.js +41 -0
  54. package/resources/gradle-instances.js.map +1 -0
  55. package/resources/gradle-instances.mjs +37 -0
  56. package/resources/gradle-instances.mjs.map +1 -0
  57. package/resources/index.d.mts +2 -0
  58. package/resources/index.d.mts.map +1 -1
  59. package/resources/index.d.ts +2 -0
  60. package/resources/index.d.ts.map +1 -1
  61. package/resources/index.js +3 -1
  62. package/resources/index.js.map +1 -1
  63. package/resources/index.mjs +1 -0
  64. package/resources/index.mjs.map +1 -1
  65. package/resources/xcode-instances-helpers.d.mts +2 -11
  66. package/resources/xcode-instances-helpers.d.mts.map +1 -1
  67. package/resources/xcode-instances-helpers.d.ts +2 -11
  68. package/resources/xcode-instances-helpers.d.ts.map +1 -1
  69. package/resources/xcode-instances-helpers.js +5 -41
  70. package/resources/xcode-instances-helpers.js.map +1 -1
  71. package/resources/xcode-instances-helpers.mjs +3 -38
  72. package/resources/xcode-instances-helpers.mjs.map +1 -1
  73. package/src/client.ts +27 -0
  74. package/src/exec-client.ts +55 -5
  75. package/src/folder-sync-ignore.ts +22 -6
  76. package/src/index.ts +10 -0
  77. package/src/resources/daemon-client-shared.ts +71 -0
  78. package/src/resources/gradle-instances-helpers.ts +155 -0
  79. package/src/resources/gradle-instances.ts +206 -0
  80. package/src/resources/index.ts +13 -0
  81. package/src/resources/xcode-instances-helpers.ts +13 -59
  82. package/src/version.ts +1 -1
  83. package/version.d.mts +1 -1
  84. package/version.d.ts +1 -1
  85. package/version.js +1 -1
  86. package/version.mjs +1 -1
package/src/client.ts CHANGED
@@ -54,6 +54,19 @@ import {
54
54
  XcodeInstanceListParams,
55
55
  XcodeInstancesItems,
56
56
  } from './resources/xcode-instances';
57
+ import {
58
+ GradleInstance,
59
+ GradleInstanceCreateParams,
60
+ GradleInstanceListParams,
61
+ GradleInstancesItems,
62
+ } from './resources/gradle-instances';
63
+ import {
64
+ GradleInstances,
65
+ GradleCreateClientParams,
66
+ GradleClient,
67
+ GradleSyncOptions,
68
+ GradleBuildOptions,
69
+ } from './resources/gradle-instances-helpers';
57
70
  import {
58
71
  XcodeInstances,
59
72
  XcodeCreateClientParams,
@@ -822,6 +835,7 @@ export class Limrun {
822
835
  assets: API.Assets = new API.Assets(this);
823
836
  iosInstances: API.IosInstances = new API.IosInstances(this);
824
837
  xcodeInstances: API.XcodeInstances = new API.XcodeInstances(this);
838
+ gradleInstances: API.GradleInstances = new API.GradleInstances(this);
825
839
  analytics: API.Analytics = new API.Analytics(this);
826
840
  }
827
841
 
@@ -829,6 +843,7 @@ Limrun.AndroidInstances = AndroidInstances;
829
843
  Limrun.Assets = Assets;
830
844
  Limrun.IosInstances = IosInstances;
831
845
  Limrun.XcodeInstances = XcodeInstances;
846
+ Limrun.GradleInstances = GradleInstances;
832
847
  Limrun.Analytics = Analytics;
833
848
 
834
849
  export declare namespace Limrun {
@@ -883,6 +898,18 @@ export declare namespace Limrun {
883
898
  type SimulatorInstallState as SimulatorInstallState,
884
899
  };
885
900
 
901
+ export {
902
+ GradleInstances as GradleInstances,
903
+ type GradleInstance as GradleInstance,
904
+ type GradleInstancesItems as GradleInstancesItems,
905
+ type GradleInstanceCreateParams as GradleInstanceCreateParams,
906
+ type GradleInstanceListParams as GradleInstanceListParams,
907
+ type GradleCreateClientParams as GradleCreateClientParams,
908
+ type GradleClient as GradleClient,
909
+ type GradleSyncOptions as GradleSyncOptions,
910
+ type GradleBuildOptions as GradleBuildOptions,
911
+ };
912
+
886
913
  export {
887
914
  Analytics as Analytics,
888
915
  type AnalyticsInstancesResponse as AnalyticsInstancesResponse,
@@ -12,7 +12,9 @@ import { nodeProxyTransport } from './internal/proxy-transport';
12
12
  // Types
13
13
  // =============================================================================
14
14
 
15
- export type ExecRequest = {
15
+ export type ExecRequest = XcodeBuildExecRequest | GradleBuildExecRequest;
16
+
17
+ export type XcodeBuildExecRequest = {
16
18
  command: 'xcodebuild';
17
19
  xcodebuild?: {
18
20
  workspace?: string;
@@ -43,6 +45,41 @@ export type ExecRequest = {
43
45
  };
44
46
  };
45
47
 
48
+ /** Android ABIs the gradle daemon accepts; 'all' keeps the project's own configuration. */
49
+ export type GradleAndroidABI = 'armeabi-v7a' | 'arm64-v8a' | 'x86' | 'x86_64' | 'all';
50
+
51
+ /**
52
+ * React Native / Expo tuning for gradle builds. The server detects Expo
53
+ * managed-workflow projects automatically when the workspace has no
54
+ * Gradle root; setting this forces the React Native pipeline (dependency
55
+ * install, expo prebuild) and is an error for projects with no detected
56
+ * Expo app.
57
+ */
58
+ export type GradleReactNativeConfig = {
59
+ /** Relative path to the Expo app directory in a monorepo. Omit to auto-detect. */
60
+ expoAppDir?: string;
61
+ /**
62
+ * Android ABIs to build. The server defaults to x86_64 (what Limrun
63
+ * Android instances run) except for release and bundle tasks, which
64
+ * keep the project's own ABI configuration; pass ['all'] to always
65
+ * keep it.
66
+ */
67
+ architectures?: GradleAndroidABI[];
68
+ };
69
+
70
+ export type GradleBuildExecRequest = {
71
+ command: 'gradlebuild';
72
+ /** Gradle tasks to run. Omit for the server default (assembleDebug). */
73
+ tasks?: string[];
74
+ /** Relative path to the Gradle root when auto-discovery is ambiguous. */
75
+ projectPath?: string;
76
+ reactNative?: GradleReactNativeConfig;
77
+ signedUploadUrl?: string;
78
+ additionalMetadata?: {
79
+ signedDownloadUrl?: string;
80
+ };
81
+ };
82
+
46
83
  export type ExecOptions = {
47
84
  apiUrl: string;
48
85
  token: string;
@@ -232,7 +269,12 @@ export class ExecChildProcess implements PromiseLike<ExecResult> {
232
269
  const { log } = this;
233
270
  const { apiUrl, token } = this.options;
234
271
 
235
- // 1. Trigger the build via POST /exec
272
+ // 1. Trigger the build via POST /exec.
273
+ // additionalMetadata is a client-only carrier (no daemon reads it; it is
274
+ // spread into ExecResult below so callers can surface the download URL), so
275
+ // it is stripped from the wire body: the daemon OpenAPI schemas do not
276
+ // declare it, and sending it would 400 under strict request validation.
277
+ const { additionalMetadata: _clientOnly, ...wireRequest } = request;
236
278
  let execRes: Response;
237
279
  try {
238
280
  execRes = await nodeProxyTransport.fetch(`${apiUrl}/exec`, {
@@ -241,7 +283,7 @@ export class ExecChildProcess implements PromiseLike<ExecResult> {
241
283
  'Content-Type': 'application/json',
242
284
  Authorization: `Bearer ${token}`,
243
285
  },
244
- body: JSON.stringify(request),
286
+ body: JSON.stringify(wireRequest),
245
287
  signal: this.abortController.signal,
246
288
  });
247
289
  } catch (err) {
@@ -259,7 +301,15 @@ export class ExecChildProcess implements PromiseLike<ExecResult> {
259
301
 
260
302
  if (!execRes.ok) {
261
303
  const text = await execRes.text();
262
- throw new Error(`exec failed: ${execRes.status} ${text}`);
304
+ let message = text;
305
+ try {
306
+ // The daemon returns an APIError JSON body; surface its message
307
+ // instead of the raw escaped JSON.
308
+ message = (JSON.parse(text) as { message?: string }).message || text;
309
+ } catch {
310
+ // Not JSON; keep the raw body.
311
+ }
312
+ throw new Error(`exec failed: ${execRes.status} ${message}`);
263
313
  }
264
314
 
265
315
  const execData = (await execRes.json()) as { execId: string };
@@ -273,7 +323,7 @@ export class ExecChildProcess implements PromiseLike<ExecResult> {
273
323
  // budget by its server-side verdict watch plus upload headroom so a long
274
324
  // build is not force-failed client-side while the server still succeeds.
275
325
  let timeoutMs = 3600 * 1000;
276
- if (request.testflight) {
326
+ if (request.command === 'xcodebuild' && request.testflight) {
277
327
  timeoutMs += (Math.max(0, request.testflight.waitTimeoutSeconds ?? 120) + 900) * 1000;
278
328
  }
279
329
  let exitCode: number;
@@ -31,6 +31,14 @@ export type IgnoreFnOptions = {
31
31
  * in it.
32
32
  */
33
33
  xcodeDefaults?: boolean;
34
+ /**
35
+ * Honor .gitignore files in nested directories, not just the project root.
36
+ * Defaults to whatever `xcodeDefaults` is, so existing callers are unchanged;
37
+ * a caller can enable it independently (e.g. gradle sync, whose nested module
38
+ * build/ dirs are excluded by their own app/.gitignore) without opting into
39
+ * the Xcode-specific default excludes and force-includes.
40
+ */
41
+ nestedGitignore?: boolean;
34
42
  basisCacheDir: string;
35
43
  log?: IgnoreLogger;
36
44
  };
@@ -43,6 +51,14 @@ function normalizeRelativePath(relativePath: string): string {
43
51
  .replace(/\/+/g, '/');
44
52
  }
45
53
 
54
+ // Linear-time replacement for `.replace(/\/+$/, '')`: that regex backtracks
55
+ // quadratically on long runs of '/' (flagged by CodeQL as polynomial ReDoS).
56
+ function stripTrailingSlashes(s: string): string {
57
+ let end = s.length;
58
+ while (end > 0 && s[end - 1] === '/') end--;
59
+ return end === s.length ? s : s.slice(0, end);
60
+ }
61
+
46
62
  type GitTrackedSets = {
47
63
  tracked: Set<string>;
48
64
  prefixes: Set<string>;
@@ -138,7 +154,7 @@ export async function createIgnoreFn(rootDir: string, options: IgnoreFnOptions):
138
154
  // install sync) keep the legacy behavior: root-only .gitignore and no
139
155
  // project force-includes, so a build artifact isn't reshaped by gitignore
140
156
  // files or Xcode-specific rules that happen to sit inside it.
141
- const nestedGitignore = !!options.xcodeDefaults;
157
+ const nestedGitignore = options.nestedGitignore ?? !!options.xcodeDefaults;
142
158
 
143
159
  // Evaluates the .gitignore chain for a path (trailing slash preserved so
144
160
  // dir-only rules like `build/` match the directory itself). Files are
@@ -146,7 +162,7 @@ export async function createIgnoreFn(rootDir: string, options: IgnoreFnOptions):
146
162
  // files naturally override shallower ones. When nestedGitignore is off,
147
163
  // only the root .gitignore is consulted.
148
164
  const gitignoreDecision = (testPath: string): { ignored: boolean; rule?: string } | undefined => {
149
- const withoutTrailingSlash = testPath.replace(/\/+$/, '');
165
+ const withoutTrailingSlash = stripTrailingSlashes(testPath);
150
166
  const trailingSlash = testPath.endsWith('/') ? '/' : '';
151
167
  const parts = withoutTrailingSlash.split('/');
152
168
  let decision: { ignored: boolean; rule?: string } | undefined;
@@ -164,9 +180,9 @@ export async function createIgnoreFn(rootDir: string, options: IgnoreFnOptions):
164
180
  return decision;
165
181
  };
166
182
 
167
- const basisCacheRelative = normalizeRelativePath(
168
- path.relative(rootResolved, options.basisCacheDir),
169
- ).replace(/\/+$/, '');
183
+ const basisCacheRelative = stripTrailingSlashes(
184
+ normalizeRelativePath(path.relative(rootResolved, options.basisCacheDir)),
185
+ );
170
186
  const shouldIgnoreBasisCache =
171
187
  basisCacheRelative &&
172
188
  basisCacheRelative !== '.' &&
@@ -181,7 +197,7 @@ export async function createIgnoreFn(rootDir: string, options: IgnoreFnOptions):
181
197
  return (relativePath: string) => {
182
198
  const normalized = normalizeRelativePath(relativePath);
183
199
  if (!normalized) return false;
184
- const withoutTrailingSlash = normalized.replace(/\/+$/, '');
200
+ const withoutTrailingSlash = stripTrailingSlashes(normalized);
185
201
 
186
202
  // 1. The basis cache must not sync itself, even when --include matches.
187
203
  if (
package/src/index.ts CHANGED
@@ -13,6 +13,10 @@ export { buildSettingKeyPattern, parseBuildSettingEntries, validateBuildSettings
13
13
  export {
14
14
  exec,
15
15
  type ExecRequest,
16
+ type XcodeBuildExecRequest,
17
+ type GradleAndroidABI,
18
+ type GradleBuildExecRequest,
19
+ type GradleReactNativeConfig,
16
20
  type ExecOptions,
17
21
  type ExecResult,
18
22
  type ExecChildProcess,
@@ -46,6 +50,12 @@ export {
46
50
  type SimulatorDeviceInfo,
47
51
  type SimulatorInstallState,
48
52
  } from './resources/xcode-instances-helpers';
53
+ export {
54
+ type GradleCreateClientParams,
55
+ type GradleClient,
56
+ type GradleSyncOptions,
57
+ type GradleBuildOptions,
58
+ } from './resources/gradle-instances-helpers';
49
59
  export {
50
60
  LIMRUN_DIR,
51
61
  TRY_IMPORT_LINE,
@@ -0,0 +1,71 @@
1
+ // Helpers and platform-neutral types shared by the per-instance build-daemon
2
+ // clients (xcode, gradle).
3
+
4
+ import os from 'os';
5
+ import path from 'path';
6
+ import crypto from 'crypto';
7
+
8
+ export type LogLevel = 'none' | 'error' | 'warn' | 'info' | 'debug';
9
+
10
+ /**
11
+ * Derives the client-side folder-sync cache location for a local project.
12
+ * The key format is a compatibility contract: changing it orphans every
13
+ * user's existing basis cache, so both daemon clients must share this one
14
+ * derivation.
15
+ */
16
+ export function deriveBasisCache(
17
+ localCodePath: string,
18
+ override?: string,
19
+ ): { cacheKey: string; basisCacheDir: string } {
20
+ const resolvedPath = path.resolve(localCodePath);
21
+ const folderName = path.basename(resolvedPath);
22
+ const hash = crypto.createHash('sha1').update(resolvedPath).digest('hex').slice(0, 8);
23
+ const cacheKey = `limsync-cache-${folderName}-${hash}`;
24
+ return { cacheKey, basisCacheDir: override ?? path.join(os.tmpdir(), cacheKey) };
25
+ }
26
+
27
+ export type SyncResult = {
28
+ /**
29
+ * Number of bytes transmitted to the server for this sync (full uploads plus
30
+ * delta patches, before transport compression). In watch mode, this reflects
31
+ * the initial sync only.
32
+ */
33
+ bytesSent?: number;
34
+ /** Present only when watch=true; call to stop watching */
35
+ stopWatching?: () => Promise<void>;
36
+ };
37
+
38
+ export type AssetUploadUrls = { signedUploadUrl: string; signedDownloadUrl: string };
39
+
40
+ /**
41
+ * Mints presigned upload/download URLs for a named asset via assets.getOrCreate,
42
+ * wrapping failures with the asset name (and the original error as cause).
43
+ */
44
+ export function mintAssetUploadUrls(
45
+ assets: { getOrCreate: (body: { name: string; ttl?: string }) => Promise<AssetUploadUrls> },
46
+ name: string,
47
+ ttl?: string,
48
+ ): Promise<AssetUploadUrls> {
49
+ return assets.getOrCreate({ name, ...(ttl && { ttl }) }).catch((err) => {
50
+ const message = `Failed to create upload URL for asset '${name}': ${
51
+ err instanceof Error ? err.message : err
52
+ }`;
53
+ // @ts-ignore - not all envs have native support for cause yet
54
+ throw new Error(message, { cause: err });
55
+ });
56
+ }
57
+
58
+ const logLevels: LogLevel[] = ['none', 'error', 'warn', 'info', 'debug'];
59
+
60
+ export function createDaemonLogger(prefix: string, logLevel: LogLevel) {
61
+ const threshold = logLevels.indexOf(logLevel);
62
+ const shouldLog = (level: LogLevel) => threshold >= logLevels.indexOf(level);
63
+ return (level: 'debug' | 'info' | 'warn' | 'error', msg: string) => {
64
+ if (!shouldLog(level)) return;
65
+ if (level === 'error' || level === 'warn') {
66
+ console[level](prefix, msg);
67
+ } else {
68
+ console.log(prefix, msg);
69
+ }
70
+ };
71
+ }
@@ -0,0 +1,155 @@
1
+ import { GradleInstances as GeneratedGradleInstances, type GradleInstance } from './gradle-instances';
2
+ import {
3
+ exec,
4
+ type ExecChildProcess,
5
+ type GradleBuildExecRequest,
6
+ type GradleReactNativeConfig,
7
+ } from '../exec-client';
8
+ import { syncFolder as syncFolderImpl, type FolderSyncOptions } from '../folder-sync';
9
+ import { createIgnoreFn } from '../folder-sync-ignore';
10
+ import {
11
+ createDaemonLogger,
12
+ deriveBasisCache,
13
+ mintAssetUploadUrls,
14
+ type LogLevel,
15
+ type SyncResult,
16
+ } from './daemon-client-shared';
17
+
18
+ export type GradleCreateClientParams = { logLevel?: LogLevel } & (
19
+ | { instance: GradleInstance }
20
+ | { apiUrl: string; token: string }
21
+ );
22
+
23
+ export type GradleSyncOptions = {
24
+ /**
25
+ * Directory for the client-side folder-sync cache.
26
+ * Defaults to a temporary directory under the OS temp directory.
27
+ */
28
+ basisCacheDir?: string;
29
+ /**
30
+ * Optional predicate for ignoring files and directories during sync.
31
+ * Called with the relative path from the sync root (using forward slashes).
32
+ * For directories, the path ends with '/'.
33
+ * Return true to ignore, false to keep.
34
+ */
35
+ ignore?: (relativePath: string) => boolean;
36
+ /**
37
+ * Optional predicate for force-including paths that would otherwise be
38
+ * dropped. Same calling convention as `ignore`; return true to force-sync.
39
+ */
40
+ include?: (relativePath: string) => boolean;
41
+ };
42
+
43
+ export type GradleBuildOptions = {
44
+ /** Gradle tasks to run. Omit for the server default (assembleDebug). */
45
+ tasks?: string[];
46
+ /** Relative path to the Gradle root when auto-discovery is ambiguous. */
47
+ projectPath?: string;
48
+ /** Upload the built APK as a named org asset, or to a presigned URL. */
49
+ upload?: { assetName: string; signedUploadUrl?: never } | { signedUploadUrl: string; assetName?: never };
50
+ /** React Native / Expo tuning. */
51
+ reactNative?: GradleReactNativeConfig;
52
+ };
53
+
54
+ export type GradleClient = {
55
+ sync(localCodePath: string, opts?: GradleSyncOptions): Promise<SyncResult>;
56
+ gradlebuild(options?: GradleBuildOptions): ExecChildProcess;
57
+ };
58
+
59
+ // Machine-local or regenerable files that must never reach the build
60
+ // sandbox: local.properties points at the developer's own SDK and would
61
+ // shadow the image's. Only ROOT-level cache/output dirs are defaulted;
62
+ // nested module build/ dirs are the project .gitignore's job (dropping any
63
+ // 'build' segment would silently exclude legitimate packages named build).
64
+ function gradleDefaultIgnore(relativePath: string): boolean {
65
+ const trimmed = relativePath.endsWith('/') ? relativePath.slice(0, -1) : relativePath;
66
+ const segments = trimmed.split('/');
67
+ if (segments[segments.length - 1] === 'local.properties') {
68
+ return true;
69
+ }
70
+ return segments.length === 1 && (trimmed === '.gradle' || trimmed === '.kotlin' || trimmed === 'build');
71
+ }
72
+
73
+ export class GradleInstances extends GeneratedGradleInstances {
74
+ async createClient(params: GradleCreateClientParams): Promise<GradleClient> {
75
+ let apiUrl: string;
76
+ let token: string;
77
+ if ('instance' in params) {
78
+ if (!params.instance.status.apiUrl) {
79
+ throw new Error('Instance not ready: apiUrl is not available');
80
+ }
81
+ apiUrl = params.instance.status.apiUrl;
82
+ token = params.instance.status.token;
83
+ } else {
84
+ apiUrl = params.apiUrl;
85
+ token = params.token;
86
+ }
87
+
88
+ const log = createDaemonLogger('[GradleInstance]', params.logLevel ?? 'info');
89
+ const client = this._client;
90
+
91
+ return {
92
+ async sync(localCodePath: string, opts?: GradleSyncOptions): Promise<SyncResult> {
93
+ const { cacheKey, basisCacheDir } = deriveBasisCache(localCodePath, opts?.basisCacheDir);
94
+ const userIgnore = opts?.ignore;
95
+ const codeSyncOpts: FolderSyncOptions = {
96
+ apiUrl,
97
+ token,
98
+ udid: cacheKey,
99
+ install: false,
100
+ ignoreFn: await createIgnoreFn(localCodePath, {
101
+ basisCacheDir,
102
+ log,
103
+ xcodeDefaults: false,
104
+ // Honor nested module .gitignore files (app/.gitignore's /build,
105
+ // etc.); gradleDefaultIgnore only covers root-level build/.gradle/
106
+ // .kotlin, so without this a locally-built project would sync its
107
+ // nested build/ artifact trees.
108
+ nestedGitignore: true,
109
+ additional: userIgnore ? (p) => gradleDefaultIgnore(p) || userIgnore(p) : gradleDefaultIgnore,
110
+ ...(opts?.include ? { include: opts.include } : {}),
111
+ }),
112
+ basisCacheDir,
113
+ // Gradle builds are one-shot; no dev-loop watch like xcode.
114
+ watch: false,
115
+ launchMode: 'ForegroundIfRunning',
116
+ log,
117
+ syncSymlinks: true,
118
+ };
119
+
120
+ const result = await syncFolderImpl(localCodePath, codeSyncOpts);
121
+ const out: SyncResult = {};
122
+ if (result.bytesSent !== undefined) {
123
+ out.bytesSent = result.bytesSent;
124
+ }
125
+ return out;
126
+ },
127
+
128
+ gradlebuild(options?: GradleBuildOptions): ExecChildProcess {
129
+ const request: GradleBuildExecRequest = {
130
+ command: 'gradlebuild',
131
+ ...(options?.tasks?.length && { tasks: options.tasks }),
132
+ ...(options?.projectPath && { projectPath: options.projectPath }),
133
+ ...(options?.reactNative && { reactNative: options.reactNative }),
134
+ };
135
+
136
+ if (options?.upload && 'assetName' in options.upload) {
137
+ const requestPromise = mintAssetUploadUrls(client.assets, options.upload.assetName).then(
138
+ (asset) => {
139
+ request.signedUploadUrl = asset.signedUploadUrl;
140
+ request.additionalMetadata = { signedDownloadUrl: asset.signedDownloadUrl };
141
+ return request;
142
+ },
143
+ );
144
+ return exec(requestPromise, { apiUrl, token, log });
145
+ }
146
+
147
+ if (options?.upload && 'signedUploadUrl' in options.upload) {
148
+ request.signedUploadUrl = options.upload.signedUploadUrl;
149
+ }
150
+
151
+ return exec(request, { apiUrl, token, log });
152
+ },
153
+ };
154
+ }
155
+ }
@@ -0,0 +1,206 @@
1
+ // Hand-written following the Stainless resource pattern; a future generation
2
+ // from the OpenAPI spec reconciles with this file.
3
+
4
+ import { APIResource } from '../core/resource';
5
+ import { APIPromise } from '../core/api-promise';
6
+ import { Items, type ItemsParams, PagePromise } from '../core/pagination';
7
+ import { buildHeaders } from '../internal/headers';
8
+ import { RequestOptions } from '../internal/request-options';
9
+ import { path } from '../internal/utils/path';
10
+
11
+ export class GradleInstances extends APIResource {
12
+ /**
13
+ * Create a gradle instance
14
+ */
15
+ create(params: GradleInstanceCreateParams, options?: RequestOptions): APIPromise<GradleInstance> {
16
+ const { reuseIfExists, wait, ...body } = params;
17
+ return this._client.post('/v1/gradle_instances', { query: { reuseIfExists, wait }, body, ...options });
18
+ }
19
+
20
+ /**
21
+ * List gradle instances
22
+ */
23
+ list(
24
+ query: GradleInstanceListParams | null | undefined = {},
25
+ options?: RequestOptions,
26
+ ): PagePromise<GradleInstancesItems, GradleInstance> {
27
+ return this._client.getAPIList('/v1/gradle_instances', Items<GradleInstance>, { query, ...options });
28
+ }
29
+
30
+ /**
31
+ * Delete gradle instance with given name
32
+ */
33
+ delete(id: string, options?: RequestOptions): APIPromise<void> {
34
+ return this._client.delete(path`/v1/gradle_instances/${id}`, {
35
+ ...options,
36
+ headers: buildHeaders([{ Accept: '*/*' }, options?.headers]),
37
+ });
38
+ }
39
+
40
+ /**
41
+ * Get gradle instance with given ID
42
+ */
43
+ get(id: string, options?: RequestOptions): APIPromise<GradleInstance> {
44
+ return this._client.get(path`/v1/gradle_instances/${id}`, options);
45
+ }
46
+ }
47
+
48
+ export type GradleInstancesItems = Items<GradleInstance>;
49
+
50
+ export interface GradleInstance {
51
+ metadata: GradleInstance.Metadata;
52
+
53
+ spec: GradleInstance.Spec;
54
+
55
+ status: GradleInstance.Status;
56
+ }
57
+
58
+ export namespace GradleInstance {
59
+ export interface Metadata {
60
+ id: string;
61
+
62
+ createdAt: string;
63
+
64
+ organizationId: string;
65
+
66
+ displayName?: string;
67
+
68
+ labels?: { [key: string]: string };
69
+
70
+ terminatedAt?: string;
71
+ }
72
+
73
+ export interface Spec {
74
+ inactivityTimeout: string;
75
+
76
+ region: string;
77
+
78
+ hardTimeout?: string;
79
+ }
80
+
81
+ export interface Status {
82
+ token: string;
83
+
84
+ state: 'unknown' | 'creating' | 'assigned' | 'ready' | 'terminated';
85
+
86
+ apiUrl?: string;
87
+
88
+ errorMessage?: string;
89
+
90
+ /**
91
+ * Machine-readable reason the instance was terminated. Always present once state
92
+ * is "terminated", never present before that. New values may be added over time,
93
+ * so treat any unrecognized value as "Unknown". Known values:
94
+ *
95
+ * - "UserRequested": terminated by a delete request to the API.
96
+ * - "InactivityTimeout": the timeout given in spec.inactivityTimeout elapsed.
97
+ * - "HardTimeout": the timeout given in spec.hardTimeout elapsed.
98
+ * - "Unknown": terminated for a cause the platform did not attribute, including
99
+ * instances that failed to get ready during creation. See errorMessage for
100
+ * details when available.
101
+ */
102
+ terminationReason?: string;
103
+ }
104
+ }
105
+
106
+ export interface GradleInstanceCreateParams {
107
+ /**
108
+ * Query param: If there is another instance with given labels and region, return
109
+ * that one instead of creating a new instance.
110
+ */
111
+ reuseIfExists?: boolean;
112
+
113
+ /**
114
+ * Query param: Return after the instance is ready to connect.
115
+ */
116
+ wait?: boolean;
117
+
118
+ /**
119
+ * Body param
120
+ */
121
+ metadata?: GradleInstanceCreateParams.Metadata;
122
+
123
+ /**
124
+ * Body param
125
+ */
126
+ spec?: GradleInstanceCreateParams.Spec;
127
+ }
128
+
129
+ export namespace GradleInstanceCreateParams {
130
+ export interface Metadata {
131
+ displayName?: string;
132
+
133
+ labels?: { [key: string]: string };
134
+ }
135
+
136
+ export interface Spec {
137
+ clues?: Array<Spec.Clue>;
138
+
139
+ /**
140
+ * After how many minutes should the instance be terminated. Example values 1m,
141
+ * 10m, 3h. Default is "0" which means no hard timeout.
142
+ */
143
+ hardTimeout?: string;
144
+
145
+ /**
146
+ * After how many minutes of inactivity should the instance be terminated. Example
147
+ * values 1m, 10m, 3h. Default is 5m.
148
+ */
149
+ inactivityTimeout?: string;
150
+
151
+ /**
152
+ * Where the instance will be created. If not given, the region is decided based on
153
+ * scheduling clues (client IP) and availability.
154
+ *
155
+ * A region is a preference, not a hard pin: the request always overflows to every
156
+ * other available region, ordered by proximity, when the preferred ones are full.
157
+ *
158
+ * Accepted values:
159
+ *
160
+ * - A specific region name (e.g. "us-west1"). It is tried first, then the
161
+ * remaining regions in order of proximity to it. Scheduling clues (client IP)
162
+ * are ignored when a region is given.
163
+ * - A region group name (e.g. "us", "eu"). Its member regions are tried first in
164
+ * their listed order, then the remaining regions by proximity to the first
165
+ * member.
166
+ * - A pipe-separated, ordered list of regions (e.g. "us-east1|us-west1"). Those
167
+ * are tried first in the given order, then the remaining regions by proximity to
168
+ * the first.
169
+ */
170
+ region?: string;
171
+ }
172
+
173
+ export namespace Spec {
174
+ export interface Clue {
175
+ kind: 'ClientIP';
176
+
177
+ clientIp?: string;
178
+ }
179
+ }
180
+ }
181
+
182
+ export interface GradleInstanceListParams extends ItemsParams {
183
+ /**
184
+ * Labels filter to apply to instances to return. Expects a comma-separated list of
185
+ * key=value pairs (e.g., env=prod,region=us-west).
186
+ */
187
+ labelSelector?: string;
188
+
189
+ /**
190
+ * State filter to apply to gradle instances to return. Each comma-separated state
191
+ * will be used as part of an OR clause, e.g. "assigned,ready" will return all
192
+ * instances that are either assigned or ready.
193
+ *
194
+ * Valid states: creating, assigned, ready, terminated, unknown
195
+ */
196
+ state?: string;
197
+ }
198
+
199
+ export declare namespace GradleInstances {
200
+ export {
201
+ type GradleInstance as GradleInstance,
202
+ type GradleInstancesItems as GradleInstancesItems,
203
+ type GradleInstanceCreateParams as GradleInstanceCreateParams,
204
+ type GradleInstanceListParams as GradleInstanceListParams,
205
+ };
206
+ }