@stim-cli/ci 1.19.0 → 1.20.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.
package/README.md CHANGED
@@ -10,6 +10,39 @@ its own checkout. The run stops that checkout's workspace, including resources
10
10
  started before a build failure; an explicit API `run.slot` limits cleanup to
11
11
  that slot. Do not point CI at a developer's active checkout.
12
12
 
13
+ ## Build only
14
+
15
+ ```sh
16
+ npx --yes --package @stim-cli/ci stim-ci build --platform ios --project ./app --artifacts ./build-results
17
+ ```
18
+
19
+ This compiles or restores an artifact without starting a simulator, emulator,
20
+ Metro server, or app. It does not stop an existing workspace session. iOS builds
21
+ target the simulator; archives, device distribution and web compilation are
22
+ not supported. By default, iOS and Android builds use the same native artifact
23
+ cache as `run`. A later `run` in the same job validates that cache normally; its
24
+ `run.json` reports `cacheKey` and `cacheHit`, which show whether it reused the
25
+ build. Android reuse requires the run's emulator system image to have the host's
26
+ ABI; a configured `android.systemImage` with another ABI, or a physical device,
27
+ misses. An iOS Debug build with `--arch all` is keyed without an architecture,
28
+ so a later Debug `run` misses. macOS builds are not cached, so a later macOS
29
+ `run` builds again.
30
+
31
+ The result has `stage: "build"`, `build`, `buildPath`, and `artifactPath`.
32
+ The results directory contains `build.json`, `result.json`, `diagnostics.json`,
33
+ and `app.apk` for Android or `app.tar.gz` for iOS/macOS. iOS and macOS also
34
+ keep the archive command's output in `artifact.stdout.log` and
35
+ `artifact.stderr.log`. The archive preserves app
36
+ executable permissions and symlinks when uploaded through an artifact service.
37
+ Extract it with `tar -xzf app.tar.gz`. Importing a downloaded artifact into a
38
+ Stim run is not part of this command.
39
+
40
+ The library exports `buildCI({ projectRoot, build: { platform: "ios" }, ... })`.
41
+ It accepts the same home/cache, timeout, cancellation and progress options as
42
+ `runCI`. Build cancellation waits for the owned build operation to finish
43
+ cleanup and retains diagnostics; it never calls workspace `stop()`. A cancel or timeout after the artifact is exported does not fail the build
44
+ stage.
45
+
13
46
  ## Command line
14
47
 
15
48
  Without installing:
@@ -27,8 +60,22 @@ stim-ci run --platform android --project ./app --artifacts ./test-results --time
27
60
  `--platform` accepts `ios`, `android`, `macos`, or `web`. `--project` defaults
28
61
  to the current directory. `--home` and `--build-cache` explicitly select the
29
62
  Stim home and native artifact cache; leaving them unset preserves normal Stim
30
- configuration. The CLI uses each project's normal build settings. The library
31
- also accepts the public API's platform-specific run options.
63
+ configuration.
64
+
65
+ Without selectors, the CLI uses each project's normal build settings. `build`
66
+ and `run` accept the same selectors as the public API, so a run can match a
67
+ build:
68
+
69
+ | Option | Platform | Stage | Selects |
70
+ | ------------------------ | -------- | ---------- | ------------------------------------------------------- |
71
+ | `--scheme <name>` | iOS | build, run | Xcode scheme |
72
+ | `--configuration <name>` | iOS | build, run | Xcode configuration |
73
+ | `--variant <name>` | Android | build, run | Gradle variant |
74
+ | `--arch <name>` | iOS | build | Architecture: `arm64`, `x86_64`, `all` |
75
+ | `--abi <name>` | Android | build | ABI: `arm64-v8a`, `armeabi-v7a`, `x86`, `x86_64`, `all` |
76
+
77
+ The library also accepts the public API's other platform-specific build and run
78
+ options.
32
79
 
33
80
  Everything after `--` is an argument vector. No shell is inferred. To use shell
34
81
  syntax, pass a shell explicitly: `-- bash -e -o pipefail -c 'pnpm test:e2e'`.
@@ -0,0 +1,155 @@
1
+ import { n as buildCI, t as runCI } from "./src-q52NuL5J.mjs";
2
+ import { parseArgs } from "node:util";
3
+ //#region src/cli.ts
4
+ const HELP = `Usage: stim-ci build --platform <ios|android|macos> [options]
5
+ stim-ci run --platform <ios|android|macos|web> [options] -- <command> [args...]
6
+
7
+ Build exports an artifact without starting or stopping an app.
8
+ Run builds and launches an app, runs a test command, collects diagnostics, and stops it.
9
+ The final result is JSON on stdout; progress and test output go to stderr.
10
+
11
+ Options:
12
+ --project <path> Project directory (default: current directory)
13
+ --artifacts <path> Results directory (default: a fresh temporary directory)
14
+ --home <path> Explicit Stim home (default: job-local on GitHub-hosted runners)
15
+ --build-cache <path> Native artifact cache directory
16
+ --timeout <seconds> Setup and test timeout (cleanup has its own deadline)
17
+ --help Show this help
18
+ --version Show the package version
19
+
20
+ Build selectors (build and run):
21
+ --scheme <name> iOS scheme
22
+ --configuration <name> iOS configuration
23
+ --variant <name> Android variant
24
+
25
+ Build-only selectors:
26
+ --arch <name> iOS architecture: arm64, x86_64, all
27
+ --abi <name> Android ABI: arm64-v8a, armeabi-v7a, x86, x86_64, all
28
+ `;
29
+ function parse(argv) {
30
+ const separator = argv.indexOf("--");
31
+ const command = separator < 0 ? [] : argv.slice(separator + 1);
32
+ const { values, positionals } = parseArgs({
33
+ args: separator < 0 ? argv : argv.slice(0, separator),
34
+ allowPositionals: true,
35
+ options: {
36
+ platform: { type: "string" },
37
+ project: { type: "string" },
38
+ artifacts: { type: "string" },
39
+ home: { type: "string" },
40
+ "build-cache": { type: "string" },
41
+ timeout: { type: "string" },
42
+ scheme: { type: "string" },
43
+ configuration: { type: "string" },
44
+ variant: { type: "string" },
45
+ arch: { type: "string" },
46
+ abi: { type: "string" }
47
+ }
48
+ });
49
+ const stage = positionals[0];
50
+ if (positionals.length !== 1 || stage !== "run" && stage !== "build") throw new Error("Expected stim-ci build or run.");
51
+ const platform = values.platform;
52
+ if (platform !== "ios" && platform !== "android" && platform !== "macos" && platform !== "web") throw new Error("--platform must be ios, android, macos or web.");
53
+ const timeoutMs = values.timeout === void 0 ? void 0 : Number(values.timeout) * 1e3;
54
+ const common = {
55
+ projectRoot: values.project ?? process.cwd(),
56
+ artifactsDir: values.artifacts,
57
+ home: values.home,
58
+ buildCache: values["build-cache"],
59
+ timeoutMs
60
+ };
61
+ if (stage === "run" && (values.arch !== void 0 || values.abi !== void 0)) throw new Error("--arch and --abi only apply to build-only.");
62
+ if (platform !== "ios" && (values.scheme !== void 0 || values.configuration !== void 0 || values.arch !== void 0)) throw new Error("--scheme, --configuration and --arch only apply to iOS.");
63
+ if (platform !== "android" && (values.variant !== void 0 || values.abi !== void 0)) throw new Error("--variant and --abi only apply to Android.");
64
+ if (stage === "build") {
65
+ if (separator >= 0) throw new Error("Build-only does not take a test command. Use stim-ci run to launch and test.");
66
+ if (platform === "web") throw new Error("Web projects have no native build artifact. Use stim-ci run --platform web.");
67
+ if (values.arch !== void 0 && ![
68
+ "arm64",
69
+ "x86_64",
70
+ "all"
71
+ ].includes(values.arch)) throw new Error("--arch must be arm64, x86_64 or all.");
72
+ if (values.abi !== void 0 && ![
73
+ "arm64-v8a",
74
+ "armeabi-v7a",
75
+ "x86",
76
+ "x86_64",
77
+ "all"
78
+ ].includes(values.abi)) throw new Error("--abi must be arm64-v8a, armeabi-v7a, x86, x86_64 or all.");
79
+ const build = platform === "ios" ? {
80
+ platform,
81
+ scheme: values.scheme,
82
+ configuration: values.configuration,
83
+ arch: values.arch
84
+ } : platform === "android" ? {
85
+ platform,
86
+ variant: values.variant,
87
+ abi: values.abi
88
+ } : { platform };
89
+ return {
90
+ stage,
91
+ options: {
92
+ ...common,
93
+ build
94
+ }
95
+ };
96
+ }
97
+ if (separator < 0) throw new Error("Separate the test command with --.");
98
+ if (!command[0]) throw new Error("A test command is required after --.");
99
+ const run = platform === "ios" ? {
100
+ platform,
101
+ scheme: values.scheme,
102
+ configuration: values.configuration
103
+ } : platform === "android" ? {
104
+ platform,
105
+ variant: values.variant
106
+ } : { platform };
107
+ return {
108
+ stage,
109
+ options: {
110
+ ...common,
111
+ run,
112
+ command
113
+ }
114
+ };
115
+ }
116
+ async function main(argv = process.argv.slice(2)) {
117
+ if (argv.length === 1 && argv[0] === "--version") {
118
+ process.stdout.write(`1.20.0\n`);
119
+ return;
120
+ }
121
+ if (argv.length === 0 || argv[0] === "--help" || (argv[0] === "run" || argv[0] === "build") && argv[1] === "--help") {
122
+ process.stdout.write(HELP);
123
+ return;
124
+ }
125
+ const controller = new AbortController();
126
+ const interrupt = () => controller.abort();
127
+ try {
128
+ const invocation = parse(argv);
129
+ process.on("SIGINT", interrupt);
130
+ process.on("SIGTERM", interrupt);
131
+ const callbacks = {
132
+ signal: controller.signal,
133
+ onProgress: ({ message }) => {
134
+ process.stderr.write(message);
135
+ }
136
+ };
137
+ const result = invocation.stage === "build" ? await buildCI({
138
+ ...invocation.options,
139
+ ...callbacks
140
+ }) : await runCI({
141
+ ...invocation.options,
142
+ ...callbacks
143
+ });
144
+ process.stdout.write(`${JSON.stringify(result)}\n`);
145
+ process.exitCode = result.exitCode;
146
+ } catch (error) {
147
+ process.stderr.write(`stim-ci: ${error.message}\n`);
148
+ process.exitCode = 1;
149
+ } finally {
150
+ process.off("SIGINT", interrupt);
151
+ process.off("SIGTERM", interrupt);
152
+ }
153
+ }
154
+ //#endregion
155
+ export { main };
package/dist/index.d.mts CHANGED
@@ -1,4 +1,15 @@
1
- import { StimOptions, StimRunOptions, StimRunResult, StimStopResult } from "stim";
1
+ import { StimBuildOptions, StimBuildResult, StimOptions, StimRunOptions, StimRunResult, StimStopResult } from "stim";
2
+ //#region src/context.d.ts
3
+ interface CIContextOptions {
4
+ projectRoot: string;
5
+ artifactsDir?: string;
6
+ home?: string;
7
+ buildCache?: string;
8
+ timeoutMs?: number;
9
+ signal?: AbortSignal;
10
+ onProgress?: StimOptions["onProgress"];
11
+ }
12
+ //#endregion
2
13
  //#region src/command.d.ts
3
14
  interface CommandResult {
4
15
  exitCode: number | null;
@@ -9,18 +20,40 @@ interface CommandResult {
9
20
  error?: string;
10
21
  }
11
22
  //#endregion
23
+ //#region src/build.d.ts
24
+ type WithoutSignal$1<T> = T extends unknown ? Omit<T, "signal"> : never;
25
+ interface CIBuildOptions extends CIContextOptions {
26
+ build: WithoutSignal$1<StimBuildOptions>;
27
+ }
28
+ interface CIBuildResult {
29
+ version: 1;
30
+ stage: "build";
31
+ projectRoot: string;
32
+ platform: StimBuildOptions["platform"];
33
+ artifactsDir: string;
34
+ resultPath: string;
35
+ buildPath: string;
36
+ /** Portable APK or tar.gz bundle preserving executable permissions and symlinks. */
37
+ artifactPath: string | null;
38
+ startedAt: string;
39
+ durationMs: number;
40
+ exitCode: number;
41
+ build: StimBuildResult | null;
42
+ failure?: CIFailure;
43
+ reportingError?: CIFailure;
44
+ diagnostics: {
45
+ path: string | null;
46
+ error?: CIFailure;
47
+ };
48
+ }
49
+ /** Builds and exports an artifact without starting or stopping a workspace runtime. */
50
+ declare function buildCI(input: CIBuildOptions): Promise<CIBuildResult>;
51
+ //#endregion
12
52
  //#region src/index.d.ts
13
53
  type WithoutSignal<T> = T extends unknown ? Omit<T, "signal"> : never;
14
- interface CIOptions {
15
- projectRoot: string;
54
+ interface CIOptions extends CIContextOptions {
16
55
  run: WithoutSignal<StimRunOptions>;
17
56
  command: readonly [string, ...string[]];
18
- artifactsDir?: string;
19
- home?: string;
20
- buildCache?: string;
21
- timeoutMs?: number;
22
- signal?: AbortSignal;
23
- onProgress?: StimOptions["onProgress"];
24
57
  }
25
58
  interface CIFailure {
26
59
  code: string;
@@ -54,4 +87,4 @@ interface CIResult {
54
87
  /** Runs an app and argv-based test command, stops its workspace or explicit slot, and retains diagnostics. */
55
88
  declare function runCI(options: CIOptions): Promise<CIResult>;
56
89
  //#endregion
57
- export { CIFailure, CIOptions, CIResult, type CommandResult, runCI };
90
+ export { type CIBuildOptions, type CIBuildResult, CIFailure, CIOptions, CIResult, type CommandResult, buildCI, runCI };
package/dist/index.mjs CHANGED
@@ -1,2 +1,2 @@
1
- import { t as runCI } from "./src-BdobdbxB.mjs";
2
- export { runCI };
1
+ import { n as buildCI, t as runCI } from "./src-q52NuL5J.mjs";
2
+ export { buildCI, runCI };
@@ -1,16 +1,56 @@
1
1
  import { copyDiagnosticLogs } from "./artifacts.mjs";
2
- import { closeSync, mkdirSync, mkdtempSync, openSync, readdirSync, realpathSync, renameSync, writeFileSync, writeSync } from "node:fs";
2
+ import { closeSync, copyFileSync, mkdirSync, mkdtempSync, openSync, readdirSync, realpathSync, renameSync, rmSync, writeFileSync, writeSync } from "node:fs";
3
3
  import { tmpdir } from "node:os";
4
- import { join, resolve } from "node:path";
4
+ import { basename, dirname, join, resolve } from "node:path";
5
5
  import { createStim } from "stim";
6
6
  import { setTimeout as setTimeout$1 } from "node:timers/promises";
7
7
  import spawn from "cross-spawn";
8
+ //#region src/context.ts
9
+ function failure(error, code) {
10
+ const caught = error;
11
+ return {
12
+ code: typeof caught?.code === "string" ? caught.code : code,
13
+ message: typeof caught?.message === "string" ? caught.message : String(error),
14
+ ...typeof caught?.remedy === "string" ? { remedy: caught.remedy } : {}
15
+ };
16
+ }
17
+ function writeJson(path, value) {
18
+ const pending = `${path}.tmp`;
19
+ writeFileSync(pending, `${JSON.stringify(value, null, 2)}\n`);
20
+ renameSync(pending, path);
21
+ }
22
+ function prepareCI(options) {
23
+ if (options.timeoutMs !== void 0 && (!Number.isSafeInteger(options.timeoutMs) || options.timeoutMs <= 0)) throw new Error("timeoutMs must be a positive integer.");
24
+ if (process.env.GITHUB_ACTIONS === "true" && process.env.RUNNER_ENVIRONMENT === "github-hosted" && process.env.RUNNER_TEMP && !options.home && !process.env.STIM_HOME) {
25
+ const directory = join(resolve(process.env.RUNNER_TEMP), "stim-ci");
26
+ options = {
27
+ ...options,
28
+ home: join(directory, "home"),
29
+ buildCache: options.buildCache || (process.env.STIM_BUILD_CACHE ? void 0 : join(directory, "build-cache"))
30
+ };
31
+ }
32
+ const projectRoot = realpathSync(options.projectRoot);
33
+ const artifactsDir = options.artifactsDir ? resolve(options.artifactsDir) : mkdtempSync(join(tmpdir(), "stim-ci-"));
34
+ mkdirSync(artifactsDir, { recursive: true });
35
+ if (readdirSync(artifactsDir).length > 0) throw new Error(`Artifacts directory must be empty: ${artifactsDir}. Choose a new directory for this run.`);
36
+ const timeout = options.timeoutMs === void 0 ? void 0 : AbortSignal.timeout(options.timeoutMs);
37
+ const signals = [options.signal, timeout].filter((value) => value !== void 0);
38
+ const signal = signals.length ? AbortSignal.any(signals) : void 0;
39
+ return {
40
+ options,
41
+ projectRoot,
42
+ artifactsDir,
43
+ timeout,
44
+ signal
45
+ };
46
+ }
47
+ //#endregion
8
48
  //#region src/command.ts
9
- async function runCommand({ command, cwd, env, artifactsDir, signal, onOutput }) {
49
+ async function runCommand({ command, cwd, env, artifactsDir, logName = "test", signal, onOutput }) {
10
50
  signal?.throwIfAborted();
11
51
  const started = Date.now();
12
- const stdout = join(artifactsDir, "test.stdout.log");
13
- const stderr = join(artifactsDir, "test.stderr.log");
52
+ const stdout = join(artifactsDir, `${logName}.stdout.log`);
53
+ const stderr = join(artifactsDir, `${logName}.stderr.log`);
14
54
  const out = openSync(stdout, "w");
15
55
  let err;
16
56
  try {
@@ -95,7 +135,7 @@ async function runCommand({ command, cwd, env, artifactsDir, signal, onOutput })
95
135
  groupError ??= String(caught);
96
136
  }
97
137
  if (Date.now() >= deadline) {
98
- error ??= groupError ?? `Test process group ${child.pid} did not exit after termination`;
138
+ error ??= groupError ?? `${logName === "artifact" ? "Artifact export" : "Test"} process group ${child.pid} did not exit after termination`;
99
139
  break;
100
140
  }
101
141
  await setTimeout$1(20);
@@ -118,20 +158,130 @@ async function runCommand({ command, cwd, env, artifactsDir, signal, onOutput })
118
158
  }
119
159
  }
120
160
  //#endregion
121
- //#region src/index.ts
122
- function failure(error, code) {
123
- const caught = error;
124
- return {
125
- code: typeof caught?.code === "string" ? caught.code : code,
126
- message: typeof caught?.message === "string" ? caught.message : String(error),
127
- ...typeof caught?.remedy === "string" ? { remedy: caught.remedy } : {}
161
+ //#region src/build.ts
162
+ /** Builds and exports an artifact without starting or stopping a workspace runtime. */
163
+ async function buildCI(input) {
164
+ const { options, projectRoot, artifactsDir, timeout, signal } = prepareCI(input);
165
+ const started = Date.now();
166
+ const result = {
167
+ version: 1,
168
+ stage: "build",
169
+ projectRoot,
170
+ platform: options.build.platform,
171
+ artifactsDir,
172
+ resultPath: join(artifactsDir, "result.json"),
173
+ buildPath: join(artifactsDir, "build.json"),
174
+ artifactPath: null,
175
+ startedAt: new Date(started).toISOString(),
176
+ durationMs: 0,
177
+ exitCode: 1,
178
+ build: null,
179
+ diagnostics: { path: null }
128
180
  };
181
+ let reporting = false;
182
+ const onProgress = options.onProgress ? (event) => {
183
+ try {
184
+ options.onProgress(event);
185
+ } catch (error) {
186
+ result.reportingError ??= failure(error, "STIM_CI_REPORT_FAILED");
187
+ if (!reporting) throw error;
188
+ }
189
+ } : void 0;
190
+ const stim = createStim({
191
+ projectRoot,
192
+ home: options.home ? resolve(options.home) : void 0,
193
+ buildCache: options.buildCache ? resolve(options.buildCache) : void 0,
194
+ onProgress
195
+ });
196
+ let attempted = false;
197
+ let exporting;
198
+ try {
199
+ signal?.throwIfAborted();
200
+ attempted = true;
201
+ result.build = await stim.build({
202
+ ...options.build,
203
+ signal
204
+ });
205
+ signal?.throwIfAborted();
206
+ writeJson(result.buildPath, result.build);
207
+ if (result.build.platform === "android") {
208
+ exporting = join(artifactsDir, "app.apk");
209
+ copyFileSync(result.build.facts.apkPath, exporting);
210
+ } else {
211
+ const bundle = result.build.platform === "ios" ? result.build.facts.appPath : result.build.facts.bundle;
212
+ exporting = join(artifactsDir, "app.tar.gz");
213
+ const packed = await runCommand({
214
+ command: [
215
+ "tar",
216
+ "-czf",
217
+ exporting,
218
+ "-C",
219
+ dirname(bundle),
220
+ "--",
221
+ basename(bundle)
222
+ ],
223
+ cwd: projectRoot,
224
+ env: {
225
+ ...process.env,
226
+ COPYFILE_DISABLE: "1"
227
+ },
228
+ artifactsDir,
229
+ logName: "artifact",
230
+ signal,
231
+ onOutput: onProgress
232
+ });
233
+ if (packed.exitCode !== 0 || packed.error) throw Object.assign(new Error(packed.error ?? `Artifact export exited with ${packed.exitCode}.`), { code: "STIM_CI_ARTIFACT_FAILED" });
234
+ }
235
+ signal?.throwIfAborted();
236
+ result.artifactPath = exporting;
237
+ result.exitCode = 0;
238
+ } catch (error) {
239
+ result.failure = failure(error, result.build ? "STIM_CI_ARTIFACT_FAILED" : "STIM_CI_BUILD_FAILED");
240
+ if (result.build && !result.failure.code.startsWith("STIM_")) result.failure.code = "STIM_CI_ARTIFACT_FAILED";
241
+ } finally {
242
+ reporting = true;
243
+ if (signal?.aborted) {
244
+ const timedOut = timeout?.aborted && !options.signal?.aborted;
245
+ result.exitCode = timedOut ? 124 : 130;
246
+ result.failure = {
247
+ code: timedOut ? "STIM_CI_TIMEOUT" : "STIM_CI_CANCELLED",
248
+ message: timedOut ? `CI build exceeded ${options.timeoutMs}ms.` : "CI build was cancelled."
249
+ };
250
+ }
251
+ if (exporting && !result.artifactPath) try {
252
+ rmSync(exporting, { force: true });
253
+ } catch (error) {
254
+ result.reportingError ??= failure(error, "STIM_CI_REPORT_FAILED");
255
+ }
256
+ if (attempted) try {
257
+ const diagnostics = await stim.diagnostics({
258
+ tail: 1e3,
259
+ signal: AbortSignal.timeout(1e4)
260
+ });
261
+ result.diagnostics.path = join(artifactsDir, "diagnostics.json");
262
+ writeJson(result.diagnostics.path, diagnostics);
263
+ } catch (error) {
264
+ result.diagnostics.error = failure(error, "STIM_CI_DIAGNOSTICS_FAILED");
265
+ }
266
+ if (result.reportingError && result.exitCode === 0) {
267
+ result.exitCode = 1;
268
+ result.failure = result.reportingError;
269
+ }
270
+ result.durationMs = Date.now() - started;
271
+ try {
272
+ writeJson(result.resultPath, result);
273
+ } catch (error) {
274
+ result.reportingError = failure(error, "STIM_CI_REPORT_FAILED");
275
+ if (result.exitCode === 0) {
276
+ result.exitCode = 1;
277
+ result.failure = result.reportingError;
278
+ }
279
+ }
280
+ }
281
+ return result;
129
282
  }
130
- function writeJson(path, value) {
131
- const pending = `${path}.tmp`;
132
- writeFileSync(pending, `${JSON.stringify(value, null, 2)}\n`);
133
- renameSync(pending, path);
134
- }
283
+ //#endregion
284
+ //#region src/index.ts
135
285
  function commandEnvironment(result, options) {
136
286
  const facts = result.run.facts;
137
287
  return {
@@ -150,22 +300,9 @@ function commandEnvironment(result, options) {
150
300
  async function runCI(options) {
151
301
  if (!options.command[0]) throw new Error("A non-empty test command is required.");
152
302
  for (const argument of options.command) if (typeof argument !== "string" || argument.includes("\0")) throw new Error("Test command arguments must be strings without null bytes.");
153
- if (options.timeoutMs !== void 0 && (!Number.isSafeInteger(options.timeoutMs) || options.timeoutMs <= 0)) throw new Error("timeoutMs must be a positive integer.");
154
- if (process.env.GITHUB_ACTIONS === "true" && process.env.RUNNER_ENVIRONMENT === "github-hosted" && process.env.RUNNER_TEMP && !options.home && !process.env.STIM_HOME) {
155
- const directory = join(resolve(process.env.RUNNER_TEMP), "stim-ci");
156
- options = {
157
- ...options,
158
- home: join(directory, "home"),
159
- buildCache: options.buildCache || (process.env.STIM_BUILD_CACHE ? void 0 : join(directory, "build-cache"))
160
- };
161
- }
162
- const projectRoot = realpathSync(options.projectRoot);
163
- const artifactsDir = options.artifactsDir ? resolve(options.artifactsDir) : mkdtempSync(join(tmpdir(), "stim-ci-"));
164
- mkdirSync(artifactsDir, { recursive: true });
165
- if (readdirSync(artifactsDir).length > 0) throw new Error(`Artifacts directory must be empty: ${artifactsDir}. Choose a new directory for this run.`);
166
- const timeout = options.timeoutMs === void 0 ? void 0 : AbortSignal.timeout(options.timeoutMs);
167
- const signals = [options.signal, timeout].filter((value) => value !== void 0);
168
- const signal = signals.length ? AbortSignal.any(signals) : void 0;
303
+ const prepared = prepareCI(options);
304
+ options = prepared.options;
305
+ const { projectRoot, artifactsDir, timeout, signal } = prepared;
169
306
  const started = Date.now();
170
307
  const result = {
171
308
  version: 1,
@@ -288,4 +425,4 @@ async function runCI(options) {
288
425
  return result;
289
426
  }
290
427
  //#endregion
291
- export { runCI as t };
428
+ export { buildCI as n, runCI as t };
package/dist/stim-ci.mjs CHANGED
@@ -6,6 +6,6 @@ const minor = parts[1] ?? 0;
6
6
  if (major < 22 || major === 22 && minor < 12) {
7
7
  process.stderr.write(`stim-ci requires Node.js 22.12.0 or later; this is ${process.versions.node}.\n`);
8
8
  process.exitCode = 1;
9
- } else import("./cli-RvPAkat7.mjs").then(({ main }) => main());
9
+ } else import("./cli-oSziKUiL.mjs").then(({ main }) => main());
10
10
  //#endregion
11
11
  export {};
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@stim-cli/ci",
3
- "version": "1.19.0",
4
- "description": "Run Stim apps and test commands in CI with diagnostics and scoped cleanup.",
3
+ "version": "1.20.0",
4
+ "description": "Build Stim app artifacts or run Stim apps and test commands in CI with diagnostics and scoped cleanup.",
5
5
  "license": "MIT",
6
6
  "repository": {
7
7
  "type": "git",
@@ -33,7 +33,7 @@
33
33
  },
34
34
  "dependencies": {
35
35
  "cross-spawn": "^7.0.6",
36
- "stim": "^1.19.0"
36
+ "stim": "^1.20.0"
37
37
  },
38
38
  "devDependencies": {
39
39
  "@types/cross-spawn": "^6.0.6"
@@ -1,80 +0,0 @@
1
- import { t as runCI } from "./src-BdobdbxB.mjs";
2
- import { parseArgs } from "node:util";
3
- //#region src/cli.ts
4
- const HELP = `Usage: stim-ci run --platform <ios|android|macos|web> [options] -- <command> [args...]
5
-
6
- Build and launch an app, run a test command, collect diagnostics, and stop it.
7
- The final result is JSON on stdout; progress and test output go to stderr.
8
-
9
- Options:
10
- --project <path> Project directory (default: current directory)
11
- --artifacts <path> Results directory (default: a fresh temporary directory)
12
- --home <path> Explicit Stim home (default: job-local on GitHub-hosted runners)
13
- --build-cache <path> Native artifact cache directory
14
- --timeout <seconds> Setup and test timeout (cleanup has its own deadline)
15
- --help Show this help
16
- --version Show the package version
17
- `;
18
- function parse(argv) {
19
- const separator = argv.indexOf("--");
20
- if (separator < 0) throw new Error("Separate the test command with --.");
21
- const command = argv.slice(separator + 1);
22
- if (!command[0]) throw new Error("A test command is required after --.");
23
- const { values, positionals } = parseArgs({
24
- args: argv.slice(0, separator),
25
- allowPositionals: true,
26
- options: {
27
- platform: { type: "string" },
28
- project: { type: "string" },
29
- artifacts: { type: "string" },
30
- home: { type: "string" },
31
- "build-cache": { type: "string" },
32
- timeout: { type: "string" }
33
- }
34
- });
35
- if (positionals.length !== 1 || positionals[0] !== "run") throw new Error("Expected stim-ci run.");
36
- const platform = values.platform;
37
- if (platform !== "ios" && platform !== "android" && platform !== "macos" && platform !== "web") throw new Error("--platform must be ios, android, macos or web.");
38
- const timeoutMs = values.timeout === void 0 ? void 0 : Number(values.timeout) * 1e3;
39
- return {
40
- projectRoot: values.project ?? process.cwd(),
41
- run: { platform },
42
- command,
43
- artifactsDir: values.artifacts,
44
- home: values.home,
45
- buildCache: values["build-cache"],
46
- timeoutMs
47
- };
48
- }
49
- async function main(argv = process.argv.slice(2)) {
50
- if (argv.length === 1 && argv[0] === "--version") {
51
- process.stdout.write(`1.19.0\n`);
52
- return;
53
- }
54
- if (argv.length === 0 || argv[0] === "--help" || argv[0] === "run" && argv[1] === "--help") {
55
- process.stdout.write(HELP);
56
- return;
57
- }
58
- const controller = new AbortController();
59
- const interrupt = () => controller.abort();
60
- try {
61
- const options = parse(argv);
62
- process.on("SIGINT", interrupt);
63
- process.on("SIGTERM", interrupt);
64
- const result = await runCI({
65
- ...options,
66
- signal: controller.signal,
67
- onProgress: ({ message }) => process.stderr.write(message)
68
- });
69
- process.stdout.write(`${JSON.stringify(result)}\n`);
70
- process.exitCode = result.exitCode;
71
- } catch (error) {
72
- process.stderr.write(`stim-ci: ${error.message}\n`);
73
- process.exitCode = 1;
74
- } finally {
75
- process.off("SIGINT", interrupt);
76
- process.off("SIGTERM", interrupt);
77
- }
78
- }
79
- //#endregion
80
- export { main };