@invarn/cibuild 1.4.0 → 1.4.2

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 (39) hide show
  1. package/dist/cli.cjs +1 -1
  2. package/dist/src/cli.d.ts.map +1 -1
  3. package/dist/src/cli.js +56 -678
  4. package/dist/src/commands/detect-platform.d.ts +13 -0
  5. package/dist/src/commands/detect-platform.d.ts.map +1 -0
  6. package/dist/src/commands/detect-platform.js +51 -0
  7. package/dist/src/commands/detect-project.d.ts +7 -0
  8. package/dist/src/commands/detect-project.d.ts.map +1 -0
  9. package/dist/src/commands/detect-project.js +12 -0
  10. package/dist/src/commands/index.d.ts +26 -0
  11. package/dist/src/commands/index.d.ts.map +1 -0
  12. package/dist/src/commands/index.js +22 -0
  13. package/dist/src/commands/init.d.ts +13 -0
  14. package/dist/src/commands/init.d.ts.map +1 -0
  15. package/dist/src/commands/init.js +262 -0
  16. package/dist/src/commands/run.d.ts +22 -0
  17. package/dist/src/commands/run.d.ts.map +1 -0
  18. package/dist/src/commands/run.js +131 -0
  19. package/dist/src/commands/validate.d.ts +10 -0
  20. package/dist/src/commands/validate.d.ts.map +1 -0
  21. package/dist/src/commands/validate.js +46 -0
  22. package/dist/src/shared/detect-project.d.ts +11 -0
  23. package/dist/src/shared/detect-project.d.ts.map +1 -0
  24. package/dist/src/shared/detect-project.js +53 -0
  25. package/dist/src/shared/gitignore.d.ts +6 -0
  26. package/dist/src/shared/gitignore.d.ts.map +1 -0
  27. package/dist/src/shared/gitignore.js +26 -0
  28. package/dist/src/shared/prompts.d.ts +18 -0
  29. package/dist/src/shared/prompts.d.ts.map +1 -0
  30. package/dist/src/shared/prompts.js +96 -0
  31. package/dist/src/yaml/converter.d.ts.map +1 -1
  32. package/dist/src/yaml/converter.js +25 -1
  33. package/dist/src/yaml/converter.test.js +149 -0
  34. package/dist/src/yaml/steps/cache.d.ts +2 -0
  35. package/dist/src/yaml/steps/cache.d.ts.map +1 -1
  36. package/dist/src/yaml/steps/cache.js +84 -23
  37. package/dist/src/yaml/types.d.ts +1 -0
  38. package/dist/src/yaml/types.d.ts.map +1 -1
  39. package/package.json +7 -3
@@ -0,0 +1,53 @@
1
+ import { existsSync, readdirSync, statSync } from "node:fs";
2
+ import { resolve } from "node:path";
3
+ /**
4
+ * Detects whether the given directory is the root of an Android, iOS, or
5
+ * KMM project. Returns the detected project type, or null if none match.
6
+ *
7
+ * KMM is checked before Android because both have Gradle files; KMM is
8
+ * distinguished by the presence of an `iosApp/`, `shared/`, or
9
+ * `composeApp/` directory alongside the Gradle files.
10
+ */
11
+ export function detectMobileProjectRoot(dir) {
12
+ const androidIndicators = [
13
+ "build.gradle",
14
+ "build.gradle.kts",
15
+ "settings.gradle",
16
+ "settings.gradle.kts",
17
+ "gradlew",
18
+ ];
19
+ const hasAndroidIndicator = androidIndicators.some((f) => existsSync(resolve(dir, f)));
20
+ if (hasAndroidIndicator) {
21
+ const kmmIndicators = ["iosApp", "shared", "composeApp"];
22
+ const hasKmmIndicator = kmmIndicators.some((d) => {
23
+ const fullPath = resolve(dir, d);
24
+ try {
25
+ return existsSync(fullPath) && statSync(fullPath).isDirectory();
26
+ }
27
+ catch {
28
+ return false;
29
+ }
30
+ });
31
+ if (hasKmmIndicator)
32
+ return "kmm";
33
+ return "android";
34
+ }
35
+ const iosFileIndicators = ["Podfile"];
36
+ const hasIosFile = iosFileIndicators.some((f) => existsSync(resolve(dir, f)));
37
+ if (!hasIosFile) {
38
+ try {
39
+ const entries = readdirSync(dir);
40
+ const hasXcodeDir = entries.some((e) => e.endsWith(".xcodeproj") || e.endsWith(".xcworkspace"));
41
+ if (hasXcodeDir)
42
+ return "ios";
43
+ }
44
+ catch {
45
+ // Unreadable directory — fall through.
46
+ }
47
+ }
48
+ else {
49
+ return "ios";
50
+ }
51
+ return null;
52
+ }
53
+ //# sourceMappingURL=detect-project.js.map
@@ -0,0 +1,6 @@
1
+ /**
2
+ * Ensures CI Build runtime files are listed in .gitignore. Safe to call
3
+ * repeatedly — only appends entries that are missing.
4
+ */
5
+ export declare function ensureCiBuildGitignoreEntries(cwd: string): void;
6
+ //# sourceMappingURL=gitignore.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"gitignore.d.ts","sourceRoot":"","sources":["../../../src/shared/gitignore.ts"],"names":[],"mappings":"AAGA;;;GAGG;AACH,wBAAgB,6BAA6B,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI,CAiB/D"}
@@ -0,0 +1,26 @@
1
+ import { appendFileSync, existsSync, readFileSync, writeFileSync } from "node:fs";
2
+ import { resolve } from "node:path";
3
+ /**
4
+ * Ensures CI Build runtime files are listed in .gitignore. Safe to call
5
+ * repeatedly — only appends entries that are missing.
6
+ */
7
+ export function ensureCiBuildGitignoreEntries(cwd) {
8
+ const gitignorePath = resolve(cwd, ".gitignore");
9
+ const gitignoreEntries = [".cibuild-secrets.json", ".ci/.envstore.json", ".ci/keys/", "build/"];
10
+ if (existsSync(gitignorePath)) {
11
+ const contents = readFileSync(gitignorePath, "utf-8");
12
+ const lines = contents.split("\n").map((l) => l.trim());
13
+ const toAdd = gitignoreEntries.filter((e) => !lines.includes(e));
14
+ if (toAdd.length > 0) {
15
+ appendFileSync(gitignorePath, `\n${toAdd.join("\n")}\n`);
16
+ for (const entry of toAdd) {
17
+ console.log(`✅ Added ${entry} to .gitignore`);
18
+ }
19
+ }
20
+ }
21
+ else {
22
+ writeFileSync(gitignorePath, `${gitignoreEntries.join("\n")}\n`);
23
+ console.log(`✅ Created .gitignore with CI Build entries`);
24
+ }
25
+ }
26
+ //# sourceMappingURL=gitignore.js.map
@@ -0,0 +1,18 @@
1
+ import type { YAMLPipeline } from "../yaml/types.js";
2
+ import type { CIConfig } from "../types.js";
3
+ /**
4
+ * Shows an interactive workflow picker. If exactly one workflow exists,
5
+ * returns it without prompting. Returns `undefined` when the user cancels
6
+ * or no workflows are defined.
7
+ */
8
+ export declare function promptForWorkflow(workflows: string[]): Promise<string | undefined>;
9
+ /**
10
+ * Runs pre-execution validation, separates issues into user-fillable
11
+ * (missing env vars) vs hard-blocking, and collects all missing values
12
+ * upfront in a single form-like pass before execution begins.
13
+ *
14
+ * @returns true if it's safe to proceed, false if the user cancelled or
15
+ * hard errors exist.
16
+ */
17
+ export declare function promptForMissingVariables(yamlPipeline: YAMLPipeline, workflowName: string, config: CIConfig, yamlFilePath: string): Promise<boolean>;
18
+ //# sourceMappingURL=prompts.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"prompts.d.ts","sourceRoot":"","sources":["../../../src/shared/prompts.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,kBAAkB,CAAC;AACrD,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,aAAa,CAAC;AAE5C;;;;GAIG;AACH,wBAAsB,iBAAiB,CAAC,SAAS,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,MAAM,GAAG,SAAS,CAAC,CA0BxF;AAED;;;;;;;GAOG;AACH,wBAAsB,yBAAyB,CAC7C,YAAY,EAAE,YAAY,EAC1B,YAAY,EAAE,MAAM,EACpB,MAAM,EAAE,QAAQ,EAChB,YAAY,EAAE,MAAM,GACnB,OAAO,CAAC,OAAO,CAAC,CAgFlB"}
@@ -0,0 +1,96 @@
1
+ import prompts from "prompts";
2
+ import { StepValidator, formatValidationResult } from "../yaml/step-validator.js";
3
+ import { MissingEnvHandler } from "../yaml/missing-env-handler.js";
4
+ import { MissingEnvironmentVariableError } from "../yaml/env-resolver.js";
5
+ /**
6
+ * Shows an interactive workflow picker. If exactly one workflow exists,
7
+ * returns it without prompting. Returns `undefined` when the user cancels
8
+ * or no workflows are defined.
9
+ */
10
+ export async function promptForWorkflow(workflows) {
11
+ if (workflows.length === 0) {
12
+ console.error("Error: No workflows found in pipeline file");
13
+ return undefined;
14
+ }
15
+ if (workflows.length === 1) {
16
+ console.log(`Using workflow: ${workflows[0]}\n`);
17
+ return workflows[0];
18
+ }
19
+ const response = await prompts({
20
+ type: "select",
21
+ name: "workflow",
22
+ message: "Select a workflow to run:",
23
+ choices: workflows.map((name) => ({ title: name, value: name })),
24
+ initial: 0,
25
+ });
26
+ if (!response.workflow) {
27
+ console.log("\nWorkflow selection cancelled");
28
+ return undefined;
29
+ }
30
+ console.log();
31
+ return response.workflow;
32
+ }
33
+ /**
34
+ * Runs pre-execution validation, separates issues into user-fillable
35
+ * (missing env vars) vs hard-blocking, and collects all missing values
36
+ * upfront in a single form-like pass before execution begins.
37
+ *
38
+ * @returns true if it's safe to proceed, false if the user cancelled or
39
+ * hard errors exist.
40
+ */
41
+ export async function promptForMissingVariables(yamlPipeline, workflowName, config, yamlFilePath) {
42
+ console.log(`\n🔍 Running pre-execution validation...`);
43
+ const validator = new StepValidator(yamlPipeline, workflowName, config, yamlFilePath);
44
+ const result = await validator.validateWorkflow();
45
+ const errorIssues = result.issues.filter((i) => i.requirement.severity === "error" && i.result && !i.result.passed);
46
+ const fillable = errorIssues.filter((i) => i.requirement.timing === "pre-execution" &&
47
+ (i.requirement.category === "environment" || i.requirement.category === "input"));
48
+ const blocking = errorIssues.filter((i) => !(i.requirement.timing === "pre-execution" &&
49
+ (i.requirement.category === "environment" || i.requirement.category === "input")));
50
+ if (blocking.length > 0) {
51
+ console.log(formatValidationResult(result));
52
+ return false;
53
+ }
54
+ if (fillable.length === 0) {
55
+ if (result.counts.warnings > 0 || result.counts.info > 0) {
56
+ console.log(formatValidationResult(result));
57
+ }
58
+ else {
59
+ console.log("✅ Pre-execution validation passed\n");
60
+ }
61
+ return true;
62
+ }
63
+ console.log(`\n${fillable.length} required value(s) missing — provide them to continue:\n`);
64
+ for (const issue of fillable) {
65
+ const stepInfo = issue.stepName ? ` needed by: ${issue.stepName}` : "";
66
+ console.log(` • ${issue.requirement.name.padEnd(30)}${stepInfo}`);
67
+ }
68
+ console.log();
69
+ const handler = new MissingEnvHandler({
70
+ interactive: true,
71
+ workflow: workflowName,
72
+ });
73
+ for (let i = 0; i < fillable.length; i++) {
74
+ const issue = fillable[i];
75
+ const error = new MissingEnvironmentVariableError(issue.requirement.name, issue.stepName, issue.requirement.hint);
76
+ const original = console.log;
77
+ const label = `(${i + 1} of ${fillable.length})`;
78
+ let patched = false;
79
+ console.log = (...args) => {
80
+ if (!patched && typeof args[0] === "string" && args[0].includes("MISSING REQUIRED")) {
81
+ original(`║ MISSING REQUIRED ENVIRONMENT VARIABLE ${label.padEnd(28)}║`);
82
+ patched = true;
83
+ return;
84
+ }
85
+ original(...args);
86
+ };
87
+ const handleResult = await handler.handleMissingVariable(error);
88
+ console.log = original;
89
+ if (handleResult.cancelled) {
90
+ return false;
91
+ }
92
+ }
93
+ handler.close();
94
+ return true;
95
+ }
96
+ //# sourceMappingURL=prompts.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"converter.d.ts","sourceRoot":"","sources":["../../../src/yaml/converter.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,KAAK,EAAE,WAAW,EAAW,QAAQ,EAAE,MAAM,aAAa,CAAC;AAClE,OAAO,KAAK,EAAE,YAAY,EAA4B,MAAM,YAAY,CAAC;AAKzE,qBAAa,mBAAoB,SAAQ,KAAK;gBAChC,OAAO,EAAE,MAAM;CAI5B;AAED;;GAEG;AACH,MAAM,WAAW,gBAAgB;IAC/B,QAAQ,EAAE,WAAW,CAAC;IACtB,QAAQ,EAAE,MAAM,EAAE,CAAC;IACnB,YAAY,EAAE,MAAM,CAAC;CACtB;AAkCD;;;;;;;GAOG;AACH,wBAAsB,wBAAwB,CAC5C,QAAQ,EAAE,YAAY,EACtB,MAAM,EAAE,QAAQ,EAChB,YAAY,CAAC,EAAE,MAAM,EACrB,YAAY,CAAC,EAAE,MAAM,GACpB,OAAO,CAAC,gBAAgB,CAAC,CAgH3B;AAwGD;;;;GAIG;AACH,wBAAgB,qBAAqB,CAAC,QAAQ,EAAE,YAAY,GAAG,MAAM,EAAE,CAEtE"}
1
+ {"version":3,"file":"converter.d.ts","sourceRoot":"","sources":["../../../src/yaml/converter.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,KAAK,EAAE,WAAW,EAAW,QAAQ,EAAE,MAAM,aAAa,CAAC;AAClE,OAAO,KAAK,EAAE,YAAY,EAA4B,MAAM,YAAY,CAAC;AAKzE,qBAAa,mBAAoB,SAAQ,KAAK;gBAChC,OAAO,EAAE,MAAM;CAI5B;AAED;;GAEG;AACH,MAAM,WAAW,gBAAgB;IAC/B,QAAQ,EAAE,WAAW,CAAC;IACtB,QAAQ,EAAE,MAAM,EAAE,CAAC;IACnB,YAAY,EAAE,MAAM,CAAC;CACtB;AAkCD;;;;;;;GAOG;AACH,wBAAsB,wBAAwB,CAC5C,QAAQ,EAAE,YAAY,EACtB,MAAM,EAAE,QAAQ,EAChB,YAAY,CAAC,EAAE,MAAM,EACrB,YAAY,CAAC,EAAE,MAAM,GACpB,OAAO,CAAC,gBAAgB,CAAC,CA0I3B;AAwGD;;;;GAIG;AACH,wBAAgB,qBAAqB,CAAC,QAAQ,EAAE,YAAY,GAAG,MAAM,EAAE,CAEtE"}
@@ -60,11 +60,35 @@ export async function convertYAMLToPipelineDef(pipeline, config, workflowName, y
60
60
  const platformInfo = detectPlatformInfo(pipeline, selectedWorkflowName);
61
61
  // Step 3: Environment variable resolution
62
62
  const envResolver = new EnvResolver(pipeline, selectedWorkflow, selectedWorkflowName, platformInfo.platform, platformInfo.stack, yamlFilePath);
63
+ // Step 3.5: Auto-inject cache steps if cachedBuild is enabled
64
+ const cibuildMeta = pipeline.meta?.['cibuild.io'];
65
+ const cachedBuild = cibuildMeta?.cachedBuild === true;
66
+ let workflowSteps = [...selectedWorkflow.steps];
67
+ if (cachedBuild && cibuildMeta?.platform) {
68
+ const platformToCacheTechnology = {
69
+ ios: 'ios',
70
+ android: 'gradle',
71
+ kmm: 'kmm',
72
+ };
73
+ const technology = platformToCacheTechnology[cibuildMeta.platform];
74
+ if (technology) {
75
+ // Remove any manually written cache-pull/cache-push steps
76
+ workflowSteps = workflowSteps.filter(step => {
77
+ const stepKey = Object.keys(step)[0];
78
+ const stepName = stepKey.split('@')[0];
79
+ return stepName !== 'cache-pull' && stepName !== 'cache-push';
80
+ });
81
+ const cachePullStep = { 'cache-pull@1.0.0': { inputs: { technology } } };
82
+ const cachePushStep = { 'cache-push@1.0.0': { inputs: { technology } } };
83
+ workflowSteps.unshift(cachePullStep);
84
+ workflowSteps.push(cachePushStep);
85
+ }
86
+ }
63
87
  // Step 4: Parse and convert steps
64
88
  const steps = [];
65
89
  const stepsOrder = [];
66
90
  let stepCounter = 0;
67
- for (const yamlStep of selectedWorkflow.steps) {
91
+ for (const yamlStep of workflowSteps) {
68
92
  // Parse step (sub-task 3.8)
69
93
  const parsedStep = parseStep(yamlStep, stepCounter);
70
94
  stepCounter++;
@@ -6,6 +6,7 @@ import { convertYAMLToPipelineDef, YAMLConversionError } from './converter.js';
6
6
  import { clearRegistry, registerStep } from './steps/registry.js';
7
7
  import { ScriptStepExecutor } from './steps/script.js';
8
8
  import { CocoapodsInstallStepExecutor } from './steps/ios-deps.js';
9
+ import { CachePullStepExecutor, CachePushStepExecutor } from './steps/cache.js';
9
10
  describe('convertYAMLToPipelineDef - Warning System', () => {
10
11
  let mockConfig;
11
12
  beforeEach(() => {
@@ -344,5 +345,153 @@ describe('convertYAMLToPipelineDef - Warning System', () => {
344
345
  expect(result.skippedSteps).toBeGreaterThan(0);
345
346
  });
346
347
  });
348
+ describe('cachedBuild auto-injection', () => {
349
+ beforeEach(() => {
350
+ registerStep('cache-pull', new CachePullStepExecutor());
351
+ registerStep('cache-push', new CachePushStepExecutor());
352
+ });
353
+ test('should inject cache-pull and cache-push when cachedBuild is true', async () => {
354
+ const pipeline = {
355
+ format_version: '1',
356
+ meta: {
357
+ 'cibuild.io': {
358
+ stack: 'macos-xcode-26.4',
359
+ platform: 'kmm',
360
+ cachedBuild: true,
361
+ },
362
+ },
363
+ workflows: {
364
+ build: {
365
+ steps: [
366
+ { 'script@1.0.0': { inputs: { content: 'echo "building"' } } },
367
+ ],
368
+ },
369
+ },
370
+ };
371
+ const result = await convertYAMLToPipelineDef(pipeline, mockConfig, 'build');
372
+ expect(result.pipeline.steps.length).toBe(3);
373
+ expect(result.pipeline.steps[0].name).toBe('cache-pull');
374
+ expect(result.pipeline.steps[0].script).toContain('kmm-');
375
+ expect(result.pipeline.steps[1].name).toBe('script');
376
+ expect(result.pipeline.steps[2].name).toBe('cache-push');
377
+ expect(result.pipeline.steps[2].script).toContain('kmm-');
378
+ });
379
+ test('should not inject cache steps when cachedBuild is false', async () => {
380
+ const pipeline = {
381
+ format_version: '1',
382
+ meta: {
383
+ 'cibuild.io': {
384
+ stack: 'macos-xcode-26.4',
385
+ platform: 'kmm',
386
+ cachedBuild: false,
387
+ },
388
+ },
389
+ workflows: {
390
+ build: {
391
+ steps: [
392
+ { 'script@1.0.0': { inputs: { content: 'echo "building"' } } },
393
+ ],
394
+ },
395
+ },
396
+ };
397
+ const result = await convertYAMLToPipelineDef(pipeline, mockConfig, 'build');
398
+ expect(result.pipeline.steps.length).toBe(1);
399
+ expect(result.pipeline.steps[0].name).toBe('script');
400
+ });
401
+ test('should not inject cache steps when cachedBuild is absent', async () => {
402
+ const pipeline = {
403
+ format_version: '1',
404
+ meta: {
405
+ 'cibuild.io': {
406
+ stack: 'macos-xcode-26.4',
407
+ platform: 'kmm',
408
+ },
409
+ },
410
+ workflows: {
411
+ build: {
412
+ steps: [
413
+ { 'script@1.0.0': { inputs: { content: 'echo "building"' } } },
414
+ ],
415
+ },
416
+ },
417
+ };
418
+ const result = await convertYAMLToPipelineDef(pipeline, mockConfig, 'build');
419
+ expect(result.pipeline.steps.length).toBe(1);
420
+ });
421
+ test('should use correct technology for android platform', async () => {
422
+ const pipeline = {
423
+ format_version: '1',
424
+ meta: {
425
+ 'cibuild.io': {
426
+ stack: 'linux-docker-android-22.04',
427
+ platform: 'android',
428
+ cachedBuild: true,
429
+ },
430
+ },
431
+ workflows: {
432
+ build: {
433
+ steps: [
434
+ { 'script@1.0.0': { inputs: { content: 'echo "building"' } } },
435
+ ],
436
+ },
437
+ },
438
+ };
439
+ const result = await convertYAMLToPipelineDef(pipeline, mockConfig, 'build');
440
+ expect(result.pipeline.steps[0].script).toContain('gradle-');
441
+ expect(result.pipeline.steps[2].script).toContain('gradle-');
442
+ });
443
+ test('should use cocoapods technology for ios platform', async () => {
444
+ const pipeline = {
445
+ format_version: '1',
446
+ meta: {
447
+ 'cibuild.io': {
448
+ stack: 'macos-xcode-26.4',
449
+ platform: 'ios',
450
+ cachedBuild: true,
451
+ },
452
+ },
453
+ workflows: {
454
+ build: {
455
+ steps: [
456
+ { 'script@1.0.0': { inputs: { content: 'echo "building"' } } },
457
+ ],
458
+ },
459
+ },
460
+ };
461
+ const result = await convertYAMLToPipelineDef(pipeline, mockConfig, 'build');
462
+ expect(result.pipeline.steps[0].script).toContain('pods-');
463
+ expect(result.pipeline.steps[2].script).toContain('pods-');
464
+ });
465
+ test('should strip manual cache steps when cachedBuild is true', async () => {
466
+ const pipeline = {
467
+ format_version: '1',
468
+ meta: {
469
+ 'cibuild.io': {
470
+ stack: 'macos-xcode-26.4',
471
+ platform: 'kmm',
472
+ cachedBuild: true,
473
+ },
474
+ },
475
+ workflows: {
476
+ build: {
477
+ steps: [
478
+ { 'cache-pull@1.0.0': { inputs: { technology: 'gradle' } } },
479
+ { 'script@1.0.0': { inputs: { content: 'echo "building"' } } },
480
+ { 'cache-push@1.0.0': { inputs: { technology: 'gradle' } } },
481
+ ],
482
+ },
483
+ },
484
+ };
485
+ const result = await convertYAMLToPipelineDef(pipeline, mockConfig, 'build');
486
+ // Should have 3 steps: auto-injected pull, script, auto-injected push
487
+ // The manual cache steps should be replaced, not duplicated
488
+ expect(result.pipeline.steps.length).toBe(3);
489
+ expect(result.pipeline.steps[0].name).toBe('cache-pull');
490
+ expect(result.pipeline.steps[0].script).toContain('kmm-'); // auto uses kmm, not gradle
491
+ expect(result.pipeline.steps[1].name).toBe('script');
492
+ expect(result.pipeline.steps[2].name).toBe('cache-push');
493
+ expect(result.pipeline.steps[2].script).toContain('kmm-');
494
+ });
495
+ });
347
496
  });
348
497
  //# sourceMappingURL=converter.test.js.map
@@ -12,6 +12,8 @@ export interface CachePreset {
12
12
  lockfile: string;
13
13
  keyPrefix: string;
14
14
  paths: string[];
15
+ /** Optional fallback preset name: if primary lockfile not found, use this preset's lockfile/key/paths */
16
+ fallback?: string;
15
17
  }
16
18
  export declare const CACHE_PRESETS: Record<string, CachePreset>;
17
19
  /**
@@ -1 +1 @@
1
- {"version":3,"file":"cache.d.ts","sourceRoot":"","sources":["../../../../src/yaml/steps/cache.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,EAAE,gBAAgB,EAAE,MAAM,WAAW,CAAC;AAC7C,OAAO,KAAK,EAAE,OAAO,EAAE,QAAQ,EAAE,MAAM,gBAAgB,CAAC;AAExD;;;;GAIG;AACH,MAAM,WAAW,WAAW;IAC1B,QAAQ,EAAE,MAAM,CAAC;IACjB,SAAS,EAAE,MAAM,CAAC;IAClB,KAAK,EAAE,MAAM,EAAE,CAAC;CACjB;AAED,eAAO,MAAM,aAAa,EAAE,MAAM,CAAC,MAAM,EAAE,WAAW,CASrD,CAAC;AAEF;;GAEG;AACH,MAAM,WAAW,eAAe;IAC9B,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,WAAW,CAAC,EAAE,MAAM,EAAE,CAAC;IACvB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,aAAa,CAAC,EAAE,OAAO,CAAC;CACzB;AAED;;GAEG;AACH,MAAM,WAAW,eAAe;IAC9B,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,WAAW,CAAC,EAAE,MAAM,EAAE,CAAC;IACvB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,aAAa,CAAC,EAAE,OAAO,CAAC;IACxB,qBAAqB,CAAC,EAAE,OAAO,CAAC;CACjC;AAED;;;GAGG;AACH,qBAAa,qBAAsB,SAAQ,gBAAgB;IACnD,OAAO,CAAC,MAAM,EAAE,eAAe,EAAE,GAAG,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,EAAE,MAAM,EAAE,QAAQ,GAAG,OAAO,CAAC,OAAO,CAAC;YAoGzF,iBAAiB;CAoEhC;AAED;;;GAGG;AACH,qBAAa,qBAAsB,SAAQ,gBAAgB;IACnD,OAAO,CAAC,MAAM,EAAE,eAAe,EAAE,GAAG,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,EAAE,MAAM,EAAE,QAAQ,GAAG,OAAO,CAAC,OAAO,CAAC;YAwJzF,iBAAiB;CA0EhC"}
1
+ {"version":3,"file":"cache.d.ts","sourceRoot":"","sources":["../../../../src/yaml/steps/cache.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,EAAE,gBAAgB,EAAE,MAAM,WAAW,CAAC;AAC7C,OAAO,KAAK,EAAE,OAAO,EAAE,QAAQ,EAAE,MAAM,gBAAgB,CAAC;AAExD;;;;GAIG;AACH,MAAM,WAAW,WAAW;IAC1B,QAAQ,EAAE,MAAM,CAAC;IACjB,SAAS,EAAE,MAAM,CAAC;IAClB,KAAK,EAAE,MAAM,EAAE,CAAC;IAChB,yGAAyG;IACzG,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB;AAED,eAAO,MAAM,aAAa,EAAE,MAAM,CAAC,MAAM,EAAE,WAAW,CAUrD,CAAC;AAoBF;;GAEG;AACH,MAAM,WAAW,eAAe;IAC9B,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,WAAW,CAAC,EAAE,MAAM,EAAE,CAAC;IACvB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,aAAa,CAAC,EAAE,OAAO,CAAC;CACzB;AAED;;GAEG;AACH,MAAM,WAAW,eAAe;IAC9B,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,WAAW,CAAC,EAAE,MAAM,EAAE,CAAC;IACvB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,aAAa,CAAC,EAAE,OAAO,CAAC;IACxB,qBAAqB,CAAC,EAAE,OAAO,CAAC;CACjC;AAED;;;GAGG;AACH,qBAAa,qBAAsB,SAAQ,gBAAgB;IACnD,OAAO,CAAC,MAAM,EAAE,eAAe,EAAE,GAAG,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,EAAE,MAAM,EAAE,QAAQ,GAAG,OAAO,CAAC,OAAO,CAAC;YAoGzF,iBAAiB;CA2FhC;AAED;;;GAGG;AACH,qBAAa,qBAAsB,SAAQ,gBAAgB;IACnD,OAAO,CAAC,MAAM,EAAE,eAAe,EAAE,GAAG,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,EAAE,MAAM,EAAE,QAAQ,GAAG,OAAO,CAAC,OAAO,CAAC;YAwJzF,iBAAiB;CA6FhC"}
@@ -3,6 +3,7 @@
3
3
  */
4
4
  import { BaseStepExecutor } from './base.js';
5
5
  export const CACHE_PRESETS = {
6
+ ios: { lockfile: 'Podfile.lock', keyPrefix: 'pods', paths: ['Pods'], fallback: 'spm' },
6
7
  cocoapods: { lockfile: 'Podfile.lock', keyPrefix: 'pods', paths: ['Pods'] },
7
8
  carthage: { lockfile: 'Cartfile.resolved', keyPrefix: 'carthage', paths: ['Carthage'] },
8
9
  spm: { lockfile: 'Package.resolved', keyPrefix: 'spm', paths: ['~/Library/Developer/Xcode/DerivedData/*/SourcePackages'] },
@@ -12,6 +13,24 @@ export const CACHE_PRESETS = {
12
13
  yarn: { lockfile: 'yarn.lock', keyPrefix: 'yarn', paths: ['node_modules'] },
13
14
  dart: { lockfile: 'pubspec.lock', keyPrefix: 'dart', paths: ['.dart_tool', '.pub-cache'] },
14
15
  };
16
+ /**
17
+ * Resolves a preset with its fallback chain into a flat list of
18
+ * {lockfile, keyPrefix, paths} entries to try in order at runtime.
19
+ */
20
+ function resolvePresetChain(technology) {
21
+ const chain = [];
22
+ let current = technology;
23
+ const visited = new Set();
24
+ while (current && !visited.has(current)) {
25
+ visited.add(current);
26
+ const preset = CACHE_PRESETS[current];
27
+ if (!preset)
28
+ break;
29
+ chain.push(preset);
30
+ current = preset.fallback;
31
+ }
32
+ return chain;
33
+ }
15
34
  /**
16
35
  * Cache pull step executor
17
36
  * Restores cached files from cache directory
@@ -97,8 +116,8 @@ export class CachePullStepExecutor extends BaseStepExecutor {
97
116
  }
98
117
  async executeWithPreset(technology, inputs, config) {
99
118
  const stepName = 'cache-pull';
100
- const preset = CACHE_PRESETS[technology];
101
- if (!preset) {
119
+ const chain = resolvePresetChain(technology);
120
+ if (chain.length === 0) {
102
121
  const supported = Object.keys(CACHE_PRESETS).join(', ');
103
122
  throw new Error(`Unknown cache technology '${technology}'. Supported: ${supported}`);
104
123
  }
@@ -108,17 +127,39 @@ export class CachePullStepExecutor extends BaseStepExecutor {
108
127
  commands.push(`echo "Restoring ${technology} cache..."`);
109
128
  commands.push('');
110
129
  // Compute cache key from lockfile checksum at runtime
130
+ // If technology has a fallback chain, try each lockfile in order
111
131
  commands.push(`CACHE_DIR="${this.escapeBash(config.paths.cacheDir)}"`);
112
132
  commands.push('mkdir -p "$CACHE_DIR"');
113
133
  commands.push('');
114
- commands.push(`LOCKFILE="${this.escapeBash(preset.lockfile)}"`);
115
- commands.push('if [ -f "$LOCKFILE" ]; then');
116
- commands.push(` CHECKSUM=$(shasum -a 256 "$LOCKFILE" | cut -d ' ' -f1 | head -c 16)`);
117
- commands.push(` CACHE_KEY="${this.escapeBash(preset.keyPrefix)}-\${CHECKSUM}"`);
118
- commands.push('else');
119
- commands.push(` echo "Warning: $LOCKFILE not found, using fallback cache key"`);
120
- commands.push(` CACHE_KEY="${this.escapeBash(preset.keyPrefix)}-no-lockfile"`);
121
- commands.push('fi');
134
+ if (chain.length === 1) {
135
+ const preset = chain[0];
136
+ commands.push(`LOCKFILE="${this.escapeBash(preset.lockfile)}"`);
137
+ commands.push('if [ -f "$LOCKFILE" ]; then');
138
+ commands.push(` CHECKSUM=$(shasum -a 256 "$LOCKFILE" | cut -d ' ' -f1 | head -c 16)`);
139
+ commands.push(` CACHE_KEY="${this.escapeBash(preset.keyPrefix)}-\${CHECKSUM}"`);
140
+ commands.push('else');
141
+ commands.push(` echo "Warning: $LOCKFILE not found, using fallback cache key"`);
142
+ commands.push(` CACHE_KEY="${this.escapeBash(preset.keyPrefix)}-no-lockfile"`);
143
+ commands.push('fi');
144
+ }
145
+ else {
146
+ // Fallback chain: try each lockfile in order, use the first one found
147
+ for (let i = 0; i < chain.length; i++) {
148
+ const preset = chain[i];
149
+ const cond = i === 0 ? 'if' : 'elif';
150
+ commands.push(`${cond} [ -f "${this.escapeBash(preset.lockfile)}" ]; then`);
151
+ commands.push(` LOCKFILE="${this.escapeBash(preset.lockfile)}"`);
152
+ commands.push(` CHECKSUM=$(shasum -a 256 "$LOCKFILE" | cut -d ' ' -f1 | head -c 16)`);
153
+ commands.push(` CACHE_KEY="${this.escapeBash(preset.keyPrefix)}-\${CHECKSUM}"`);
154
+ if (isDebugMode) {
155
+ commands.push(` echo "Detected: ${this.escapeBash(preset.keyPrefix)} (lockfile: $LOCKFILE)"`);
156
+ }
157
+ }
158
+ commands.push('else');
159
+ commands.push(` echo "Warning: no lockfile found, using fallback cache key"`);
160
+ commands.push(` CACHE_KEY="${this.escapeBash(chain[0].keyPrefix)}-no-lockfile"`);
161
+ commands.push('fi');
162
+ }
122
163
  commands.push('');
123
164
  if (isDebugMode) {
124
165
  commands.push('echo "Lockfile: $LOCKFILE"');
@@ -135,7 +176,8 @@ export class CachePullStepExecutor extends BaseStepExecutor {
135
176
  commands.push(' echo "Cache restored successfully"');
136
177
  if (isDebugMode) {
137
178
  commands.push(' echo "Restored paths:"');
138
- for (const p of preset.paths) {
179
+ const allPaths = [...new Set(chain.flatMap(p => p.paths))];
180
+ for (const p of allPaths) {
139
181
  commands.push(` EXPANDED="${this.escapeBash(p)}"`);
140
182
  commands.push(' EXPANDED="${EXPANDED/#~/${CIBUILD_USER_HOME:-$HOME}}"');
141
183
  commands.push(' if [ -e "$EXPANDED" ]; then');
@@ -285,8 +327,8 @@ export class CachePushStepExecutor extends BaseStepExecutor {
285
327
  }
286
328
  async executeWithPreset(technology, inputs, config) {
287
329
  const stepName = 'cache-push';
288
- const preset = CACHE_PRESETS[technology];
289
- if (!preset) {
330
+ const chain = resolvePresetChain(technology);
331
+ if (chain.length === 0) {
290
332
  const supported = Object.keys(CACHE_PRESETS).join(', ');
291
333
  throw new Error(`Unknown cache technology '${technology}'. Supported: ${supported}`);
292
334
  }
@@ -299,14 +341,31 @@ export class CachePushStepExecutor extends BaseStepExecutor {
299
341
  commands.push(`CACHE_DIR="${this.escapeBash(config.paths.cacheDir)}"`);
300
342
  commands.push('mkdir -p "$CACHE_DIR"');
301
343
  commands.push('');
302
- commands.push(`LOCKFILE="${this.escapeBash(preset.lockfile)}"`);
303
- commands.push('if [ -f "$LOCKFILE" ]; then');
304
- commands.push(` CHECKSUM=$(shasum -a 256 "$LOCKFILE" | cut -d ' ' -f1 | head -c 16)`);
305
- commands.push(` CACHE_KEY="${this.escapeBash(preset.keyPrefix)}-\${CHECKSUM}"`);
306
- commands.push('else');
307
- commands.push(` echo "Warning: $LOCKFILE not found, using fallback cache key"`);
308
- commands.push(` CACHE_KEY="${this.escapeBash(preset.keyPrefix)}-no-lockfile"`);
309
- commands.push('fi');
344
+ if (chain.length === 1) {
345
+ const preset = chain[0];
346
+ commands.push(`LOCKFILE="${this.escapeBash(preset.lockfile)}"`);
347
+ commands.push('if [ -f "$LOCKFILE" ]; then');
348
+ commands.push(` CHECKSUM=$(shasum -a 256 "$LOCKFILE" | cut -d ' ' -f1 | head -c 16)`);
349
+ commands.push(` CACHE_KEY="${this.escapeBash(preset.keyPrefix)}-\${CHECKSUM}"`);
350
+ commands.push('else');
351
+ commands.push(` echo "Warning: $LOCKFILE not found, using fallback cache key"`);
352
+ commands.push(` CACHE_KEY="${this.escapeBash(preset.keyPrefix)}-no-lockfile"`);
353
+ commands.push('fi');
354
+ }
355
+ else {
356
+ for (let i = 0; i < chain.length; i++) {
357
+ const preset = chain[i];
358
+ const cond = i === 0 ? 'if' : 'elif';
359
+ commands.push(`${cond} [ -f "${this.escapeBash(preset.lockfile)}" ]; then`);
360
+ commands.push(` LOCKFILE="${this.escapeBash(preset.lockfile)}"`);
361
+ commands.push(` CHECKSUM=$(shasum -a 256 "$LOCKFILE" | cut -d ' ' -f1 | head -c 16)`);
362
+ commands.push(` CACHE_KEY="${this.escapeBash(preset.keyPrefix)}-\${CHECKSUM}"`);
363
+ }
364
+ commands.push('else');
365
+ commands.push(` echo "Warning: no lockfile found, using fallback cache key"`);
366
+ commands.push(` CACHE_KEY="${this.escapeBash(chain[0].keyPrefix)}-no-lockfile"`);
367
+ commands.push('fi');
368
+ }
310
369
  commands.push('');
311
370
  if (isDebugMode) {
312
371
  commands.push('echo "Lockfile: $LOCKFILE"');
@@ -314,10 +373,12 @@ export class CachePushStepExecutor extends BaseStepExecutor {
314
373
  }
315
374
  commands.push('CACHE_FILE="$CACHE_DIR/$CACHE_KEY.tar.zst"');
316
375
  commands.push('');
317
- // Collect paths to cache
376
+ // Collect paths to cache — use all paths from all presets in the chain,
377
+ // only archiving the ones that actually exist on disk
318
378
  commands.push('# Collect paths to cache');
319
379
  commands.push('PATHS_TO_CACHE=()');
320
- for (const p of preset.paths) {
380
+ const allPaths = [...new Set(chain.flatMap(p => p.paths))];
381
+ for (const p of allPaths) {
321
382
  commands.push(`EXPANDED="${this.escapeBash(p)}"`);
322
383
  commands.push('EXPANDED="${EXPANDED/#~/${CIBUILD_USER_HOME:-$HOME}}"');
323
384
  commands.push('if [ -e "$EXPANDED" ]; then');
@@ -11,6 +11,7 @@ export interface YAMLCIBuildMeta {
11
11
  machine_type?: MachineType;
12
12
  machine_type_id?: string;
13
13
  platform?: 'ios' | 'android' | 'kmm';
14
+ cachedBuild?: boolean;
14
15
  }
15
16
  export interface YAMLMeta {
16
17
  'cibuild.io'?: YAMLCIBuildMeta;
@@ -1 +1 @@
1
- {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../../src/yaml/types.ts"],"names":[],"mappings":"AAAA;;GAEG;AAGH,MAAM,MAAM,QAAQ,GAAG,OAAO,GAAG,OAAO,CAAC;AACzC,MAAM,MAAM,WAAW,GAAG,UAAU,GAAG,aAAa,CAAC;AAGrD,MAAM,WAAW,UAAU;IACzB,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAAC;CACvB;AAGD,MAAM,WAAW,eAAe;IAC9B,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,YAAY,CAAC,EAAE,WAAW,CAAC;IAC3B,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,EAAE,KAAK,GAAG,SAAS,GAAG,KAAK,CAAC;CACtC;AAED,MAAM,WAAW,QAAQ;IACvB,YAAY,CAAC,EAAE,eAAe,CAAC;IAC/B,YAAY,CAAC,EAAE,eAAe,CAAC;IAC/B,QAAQ,CAAC,EAAE,QAAQ,CAAC;CACrB;AAGD,MAAM,WAAW,cAAc;IAC7B,CAAC,GAAG,EAAE,MAAM,GAAG,GAAG,CAAC;CACpB;AAGD,MAAM,WAAW,QAAQ;IACvB,CAAC,mBAAmB,EAAE,MAAM,GAAG;QAC7B,KAAK,CAAC,EAAE,MAAM,CAAC;QACf,MAAM,CAAC,EAAE,cAAc,CAAC;QACxB,MAAM,CAAC,EAAE,MAAM,CAAC;QAChB,aAAa,CAAC,EAAE,OAAO,CAAC;QACxB,YAAY,CAAC,EAAE,OAAO,CAAC;KACxB,CAAC;CACH;AAGD,MAAM,WAAW,YAAY;IAC3B,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,IAAI,CAAC,EAAE,UAAU,EAAE,CAAC;IACpB,KAAK,EAAE,QAAQ,EAAE,CAAC;CACnB;AAGD,MAAM,WAAW,OAAO;IACtB,IAAI,CAAC,EAAE,UAAU,EAAE,CAAC;CACrB;AAGD,MAAM,WAAW,WAAW;IAC1B,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,0BAA0B,CAAC,EAAE,MAAM,CAAC;IACpC,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB;AAGD,MAAM,WAAW,YAAY;IAC3B,cAAc,EAAE,MAAM,CAAC;IACvB,IAAI,CAAC,EAAE,QAAQ,CAAC;IAChB,GAAG,CAAC,EAAE,OAAO,CAAC;IACd,SAAS,EAAE;QAAE,CAAC,IAAI,EAAE,MAAM,GAAG,YAAY,CAAA;KAAE,CAAC;IAC5C,WAAW,CAAC,EAAE,WAAW,EAAE,CAAC;CAC7B;AAGD,MAAM,WAAW,UAAU;IACzB,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,cAAc,CAAC;IACvB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,aAAa,CAAC,EAAE,OAAO,CAAC;IACxB,YAAY,CAAC,EAAE,OAAO,CAAC;CACxB;AAGD,MAAM,WAAW,YAAY;IAC3B,QAAQ,EAAE,QAAQ,CAAC;IACnB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,WAAW,CAAC,EAAE,WAAW,CAAC;CAC3B"}
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../../src/yaml/types.ts"],"names":[],"mappings":"AAAA;;GAEG;AAGH,MAAM,MAAM,QAAQ,GAAG,OAAO,GAAG,OAAO,CAAC;AACzC,MAAM,MAAM,WAAW,GAAG,UAAU,GAAG,aAAa,CAAC;AAGrD,MAAM,WAAW,UAAU;IACzB,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAAC;CACvB;AAGD,MAAM,WAAW,eAAe;IAC9B,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,YAAY,CAAC,EAAE,WAAW,CAAC;IAC3B,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,EAAE,KAAK,GAAG,SAAS,GAAG,KAAK,CAAC;IACrC,WAAW,CAAC,EAAE,OAAO,CAAC;CACvB;AAED,MAAM,WAAW,QAAQ;IACvB,YAAY,CAAC,EAAE,eAAe,CAAC;IAC/B,YAAY,CAAC,EAAE,eAAe,CAAC;IAC/B,QAAQ,CAAC,EAAE,QAAQ,CAAC;CACrB;AAGD,MAAM,WAAW,cAAc;IAC7B,CAAC,GAAG,EAAE,MAAM,GAAG,GAAG,CAAC;CACpB;AAGD,MAAM,WAAW,QAAQ;IACvB,CAAC,mBAAmB,EAAE,MAAM,GAAG;QAC7B,KAAK,CAAC,EAAE,MAAM,CAAC;QACf,MAAM,CAAC,EAAE,cAAc,CAAC;QACxB,MAAM,CAAC,EAAE,MAAM,CAAC;QAChB,aAAa,CAAC,EAAE,OAAO,CAAC;QACxB,YAAY,CAAC,EAAE,OAAO,CAAC;KACxB,CAAC;CACH;AAGD,MAAM,WAAW,YAAY;IAC3B,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,IAAI,CAAC,EAAE,UAAU,EAAE,CAAC;IACpB,KAAK,EAAE,QAAQ,EAAE,CAAC;CACnB;AAGD,MAAM,WAAW,OAAO;IACtB,IAAI,CAAC,EAAE,UAAU,EAAE,CAAC;CACrB;AAGD,MAAM,WAAW,WAAW;IAC1B,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,0BAA0B,CAAC,EAAE,MAAM,CAAC;IACpC,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB;AAGD,MAAM,WAAW,YAAY;IAC3B,cAAc,EAAE,MAAM,CAAC;IACvB,IAAI,CAAC,EAAE,QAAQ,CAAC;IAChB,GAAG,CAAC,EAAE,OAAO,CAAC;IACd,SAAS,EAAE;QAAE,CAAC,IAAI,EAAE,MAAM,GAAG,YAAY,CAAA;KAAE,CAAC;IAC5C,WAAW,CAAC,EAAE,WAAW,EAAE,CAAC;CAC7B;AAGD,MAAM,WAAW,UAAU;IACzB,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,cAAc,CAAC;IACvB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,aAAa,CAAC,EAAE,OAAO,CAAC;IACxB,YAAY,CAAC,EAAE,OAAO,CAAC;CACxB;AAGD,MAAM,WAAW,YAAY;IAC3B,QAAQ,EAAE,QAAQ,CAAC;IACnB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,WAAW,CAAC,EAAE,WAAW,CAAC;CAC3B"}