Compare commits

..
Author SHA1 Message Date
Gabe Debes 7581efc8db Record current-main simulator validation
Edited-by: Codex
2026-10-17 00:57:46 -07:00
Gabe DebesandCodex 3d6c7f4595 Add simulator control terminal QA evidence
Edited-by: Codex

Co-authored-by: Codex <[email protected]>
2026-10-17 00:54:56 -07:00
856 changed files with 664 additions and 156460 deletions

No files matched your search

-11
View File
@@ -1,11 +0,0 @@
root = true
[*]
indent_style = space
indent_size = 2
charset = utf-8
trim_trailing_whitespace = true
insert_final_newline = true
[*.md]
trim_trailing_whitespace = false
-7
View File
@@ -1,7 +0,0 @@
**/build/**
!packages/eas-cli/src/commands/build/**
!packages/eas-cli/src/build/**
packages/eas-cli/src/graphql/generated.ts
node_modules
-50
View File
@@ -1,50 +0,0 @@
module.exports = {
root: true,
extends: ['universe/node'],
plugins: ['async-protect', 'node'],
rules: {
'no-console': 'warn',
'no-constant-condition': ['warn', { checkLoops: false }],
'sort-imports': [
'warn',
{
ignoreDeclarationSort: true,
},
],
curly: 'warn',
'import/no-cycle': 'error',
'import/no-extraneous-dependencies': [
'error',
{ devDependencies: ['**/__tests__/**/*', '**/__mocks__/**/*'] },
],
'import/no-relative-packages': 'error',
'no-restricted-imports': [
'error',
{
paths: [
{
name: 'lodash',
message: "Don't use lodash, it's heavy!",
},
],
},
],
'no-underscore-dangle': ['error', { allow: ['__typename'] }],
'@typescript-eslint/explicit-function-return-type': [
'warn',
{
allowExpressions: true,
},
],
'async-protect/async-suffix': 'error',
'node/no-sync': 'error',
},
overrides: [
{
files: ['**/__tests__/**/*.ts', '**/__mocks__/**/*.ts'],
rules: {
'async-protect/async-suffix': 'off',
},
},
],
};
-5
View File
@@ -1,5 +0,0 @@
* text eol=lf
*.png binary
*.jpg binary
**/yarn.lock linguist-generated
-58
View File
@@ -1,58 +0,0 @@
name: "\U0001F41B Bug Report in EAS CLI"
labels: 'needs review'
description: 'You want to report a reproducible bug or regression in EAS CLI.'
body:
- type: markdown
attributes:
value: "We're looking to keep questions on our forums and bug reports on the GitHub repo. For questions and help using EAS CLI, post in our [community forums](https://forums.expo.dev/c/help-expo-application-services) (log in with your Expo developer account)."
- type: markdown
attributes:
value: Found a bug in the Expo SDK? Open a bug report [here](https://github.com/expo/expo/issues/new?assignees=&labels=needs+review&template=bug_report.yml) instead.
- type: markdown
attributes:
value: Thanks for taking the time to file a bug report! Please fill out this form as completely as possible.
- type: markdown
attributes:
value: If you leave out sections there is a high likelihood your issue will be closed. If you have a question, not a bug report, please post it on our [forums](https://forums.expo.dev/) instead.
- type: input
attributes:
label: Build/Submit details page URL
description: If you're reporting an issue with EAS Build and/or EAS Submit, please provide the URL to the details page URL. The page can be only viewed by the Expo team members. Providing the URL is optional but it makes debugging the issue easier.
placeholder: https://expo.dev/accounts/ACCOUNT_NAME/projects/PROJECT_NAME/builds/BUILD_ID
- type: textarea
validations:
required: true
attributes:
label: Summary
description: Describe the issue in 1 or 2 sentences
placeholder: Clearly describe what the expected behavior is vs. what is actually happening. This should be as short as possible, while still communicating all the necessary information. If your summary is just 'X library/method isn't working', then you need to continue debugging yourself and provide more information.
- type: input
validations:
required: true
attributes:
label: Managed or bare?
description: Is your project managed or bare? Bare - Android and/or iOS native projects exist in your repo; Managed - you don't have native code (custom or 3rd party) in your project. See the [Workflows](https://docs.expo.dev/introduction/managed-vs-bare/) page for more information.
- type: textarea
validations:
required: true
attributes:
label: Environment
description: Run `npx expo-env-info` and `npx expo-doctor`, and paste the output here.
- type: textarea
validations:
required: false
attributes:
label: Error output
placeholder: If you're running into an error message, please run the same command with `EXPO_DEBUG=true <your command here>` and paste output here.
- type: textarea
validations:
required: true
attributes:
label: Reproducible demo or steps to reproduce from a blank project
description: 'This should include as little code as possible, do not simply link your entire project. Sharing a link to a [Snack](https://snack.expo.dev/) is a GREAT way to provide a reproducible demo :) If a reproducible demo, or a complete list of steps from blank project to bug, are not provided, it is very likely your issue will be closed. Read [here more guidance](https://stackoverflow.com/help/mcve).'
- type: markdown
attributes:
value: Please make sure contributors can follow the steps your provided in order to reproduce the bug.
- type: markdown
attributes:
value: "**Realize that it is up to you to debug your code and be as certain as possible that the bug is with EAS CLI, not with your own app.** [Here's an excellent guide to debugging you can follow](https://github.com/expo/fyi/blob/master/manual-debugging.md#how-to-narrow-down-the-source-of-an-error-a-step-by-step-guide-to-good-old-fashioned-manual-debugging)."
-5
View File
@@ -1,5 +0,0 @@
blank_issues_enabled: false
contact_links:
- name: Expo Developers Discord
url: https://chat.expo.dev/
about: Join other developers in discussions regarding Expo and EAS.
-14
View File
@@ -1,14 +0,0 @@
name: "\U0001F64B Enhancement"
labels: 'enhancement,needs review'
description: 'Want us to add something to EAS CLI?'
body:
- type: markdown
attributes:
value: Thanks for helping us make EAS even better! 😀 Unfortunately, this is not the right place for feature requests. Please find and upvote your feature (or create a new one) at https://expo.canny.io instead.
- type: dropdown
validations:
required: true
attributes:
label: Do you understand that any feature requests opened in this repository will be closed?
options:
- 'Yes'
-14
View File
@@ -1,14 +0,0 @@
<!-- If this PR requires a changelog entry, add it by commenting the PR with the command `/changelog-entry [breaking-change|new-feature|bug-fix|chore] [message]`. -->
<!-- You can skip the changelog check by labeling the PR with "no changelog". -->
# Why
Please describe the motivation for this PR, and link to relevant GitHub issues, forums posts, or feature requests.
# How
How did you build this feature or fix this bug and why?
# Test Plan
Please describe how you tested this change and how a reviewer could reproduce your test, especially if this PR does not include automated tests! If possible, please also provide terminal output and/or screenshots demonstrating your test/reproduction.
@@ -1,4 +0,0 @@
{
"printWidth": 100,
"singleQuote": true
}
@@ -1,53 +0,0 @@
name: notify-slack-on-fail-or-recover
inputs:
channel:
required: true
type: string
slack_webhook:
required: true
type: string
runs:
using: 'composite'
steps:
- name: Get previous workflow run status
uses: actions/github-script@v6
id: run-status
with:
script: |
const script = require('.github/internal-actions/notify-slack-on-fail-or-recover/main.js');
return await script({github, context, core});
- name: Commit subject
shell: bash
run: |
subject="$(git show --format=%s --quiet | sed 's/"/\\"/g')"
echo "${{ fromJSON(steps.run-status.outputs.result).previous }}"
echo "SUBJECT=$subject" >> "$GITHUB_ENV"
- name: Send Slack Success Notification
if: fromJSON(steps.run-status.outputs.result).previous == 'failure' && fromJSON(steps.run-status.outputs.result).current == 'success'
uses: rtCamp/action-slack-notify@v2
env:
SLACK_CHANNEL: ${{ inputs.channel }}
SLACK_COLOR: good
SLACK_ICON: https://github.com/expo-bot.png?size=48
SLACK_TITLE: 'Fixed, was failing'
SLACK_MESSAGE: ${{ env.SUBJECT }}
SLACK_USERNAME: 'EAS CLI CI'
SLACK_WEBHOOK: ${{ inputs.slack_webhook }}
SLACK_FOOTER: ''
- name: Send Slack Failure Notification
if: fromJSON(steps.run-status.outputs.result).current == 'failure'
uses: rtCamp/action-slack-notify@v2
env:
SLACK_CHANNEL: ${{ inputs.channel }}
SLACK_COLOR: danger
SLACK_ICON: https://github.com/expo-bot.png?size=48
SLACK_TITLE: 'Failed'
SLACK_MESSAGE: ${{ env.SUBJECT }}
SLACK_USERNAME: 'EAS CLI CI'
SLACK_WEBHOOK: ${{ inputs.slack_webhook }}
SLACK_FOOTER: ''
@@ -1,155 +0,0 @@
function updateOctokitInstance(github) {
updateGithubRestActions(github);
addConvenienceMethods(github);
}
function addConvenienceMethods(github) {
Object.assign(github, {
// checks if a run is a failure from the perspective of sending Slack
// notifications. Works for both runs and run attempts, which should be the
// same object shapes.
// When a run has not been completed, it fetches the jobs for the workflow
// to check if the job is "failing" instead of "failed".
async isFailedRun(run) {
console.log(`checking run: ${run.id}`);
console.log(`status: ${run.status}`);
console.log(`conclusion: ${run.conclusion}`);
console.log(`run_attempt: ${run.run_attempt}`);
if (run.status === 'completed') {
console.log('completed -- checking conclusion');
return run.conclusion === 'failure' || run.conclusion === 'timed_out';
}
const owner = run.repository.owner.login;
const repo = run.repository.name;
const jobsResp = await github.rest.actions.listJobsForWorkflowRun({
owner,
repo,
run_id: run.id,
});
console.log('checking jobs');
const jobs = jobsResp.data;
for (const job of jobs.jobs) {
if (job.conclusion === 'failure' || job.conclusion === 'timed_out') {
console.log('job failed');
return true;
} else if (job.status === 'completed') {
continue;
}
for (const step of job.steps) {
if (step.conclusion === 'failure') {
console.log('job step failed');
return true;
}
}
}
return false;
},
});
}
// github-script's Octokit/REST plugins are out of date, so we have to
// add our own custom endpoint method
// see https://octokit.github.io/rest.js/v18#custom-endpoint-methods for
// documentation
function updateGithubRestActions(github) {
Object.assign(github.rest.actions, {
getWorkflowRunAttempt: github.request.defaults({
method: 'GET',
url: '/repos/{owner}/{repo}/actions/runs/{run_id}/attempts/{attempt_number}',
params: {
owner: {
required: true,
type: 'string',
},
repo: {
required: true,
type: 'string',
},
run_id: {
required: true,
type: 'number',
},
attempt_number: {
required: true,
type: 'number',
},
},
}),
});
}
// Returns { current: 'failure' } | { current: 'success', previous: 'success' | 'failure' } to indicate the failure status
// of the current and previous runs
module.exports = async ({ github }) => {
const { GITHUB_REPOSITORY, GITHUB_RUN_ID, GITHUB_REF_NAME } = process.env;
const [owner, repo] = GITHUB_REPOSITORY.split('/');
updateOctokitInstance(github);
const workflowRunResp = await github.rest.actions.getWorkflowRun({
owner,
repo,
run_id: GITHUB_RUN_ID,
});
const currentRun = workflowRunResp.data;
const listWorkflowRunsResp = await github.rest.actions.listWorkflowRuns({
owner,
repo,
workflow_id: currentRun.workflow_id,
branch: GITHUB_REF_NAME,
});
const { workflow_runs } = listWorkflowRunsResp.data;
if (workflow_runs.length === 1 && workflow_runs[0].run_attempt === 1) {
return false;
}
const previousRuns = workflow_runs.filter(
(run) => run.status === 'completed' && run.conclusion !== 'cancelled'
);
if (await github.isFailedRun(currentRun)) {
return { current: 'failure', previous: null };
}
if (currentRun.run_attempt === 1 && previousRuns.length > 0) {
return {
current: 'success',
previous: (await github.isFailedRun(previousRuns[0])) ? 'failure' : 'success',
};
}
for (let i = currentRun.run_attempt - 1; i > 0; i--) {
const previousAttemptResp = await github.rest.actions.getWorkflowRunAttempt({
owner,
repo,
run_id: currentRun.id,
attempt_number: i,
});
const previousAttempt = previousAttemptResp.data;
if (
previousAttempt.conclusion === 'success' ||
previousAttempt.conclusion === 'failure' ||
previousAttempt.conclusion === 'timed_out'
) {
return {
current: 'success',
previous: (await github.isFailedRun(previousAttempt)) ? 'failure' : 'success',
};
}
}
if (previousRuns.length > 0) {
return {
current: 'success',
previous: (await github.isFailedRun(previousRuns[0])) ? 'failure' : 'success',
};
}
return { current: 'success', previous: 'success' };
};
@@ -1,12 +0,0 @@
{
"name": "notify-slack-on-fail-or-recover",
"version": "1.0.0",
"description": "A GitHub Action to notify Expo Slack of build failures and recovery.",
"main": "main.js",
"author": "Expo",
"license": "UNLICENSED",
"private": true,
"devDependencies": {
"prettier": "^2.5.1"
}
}
@@ -1,8 +0,0 @@
# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
# yarn lockfile v1
[email protected]:
version "2.5.1"
resolved "https://registry.yarnpkg.com/prettier/-/prettier-2.5.1.tgz#fff75fa9d519c54cf0fce328c1017d94546bc56a"
integrity sha512-vBZcPRUR5MZJwoyi3ZoyQlc1rXeEck8KgeC9AwwOn+exuxLxq5toTRDTSaVrXHxelDMHy9zlicw8u66yxoSUFg==
-48
View File
@@ -1,48 +0,0 @@
name: Changelog bot
on:
issue_comment:
types: [created]
jobs:
pr-commented:
runs-on: ubuntu-latest
if: ${{ github.event.issue.pull_request && github.event.issue.user.login == github.event.comment.user.login && startsWith(github.event.comment.body, '/changelog-entry ') }}
steps:
- uses: actions/checkout@v2
with:
token: ${{ secrets.EXPO_BOT_PAT }}
- uses: actions/setup-node@v2
with:
node-version: 20
- name: Checkout Pull Request
run: gh pr checkout ${{ github.event.issue.number }}
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Install dependencies
working-directory: ./scripts
run: yarn install --frozen-lockfile --check-files
- name: Add changelog entry
working-directory: ./scripts
run: |
PARAMS=${PR_COMMENT_BODY#/changelog-entry }
if [[ ! "$PARAMS" =~ " " ]]; then
echo "Invalid /changelog-entry command"
exit 1
fi
CATEGORY=$( cut -d ' ' -f 1 <<< "$PARAMS" | sed -e 's/^[[:space:]]*//' )
MESSAGE=$( cut -d ' ' -f 2- <<< "$PARAMS" | sed -e 's/^[[:space:]]*//' )
yarn changelog-entry "$CATEGORY" "$MESSAGE"
env:
GITHUB_PR_AUTHOR: ${{ github.event.issue.user.login }}
GITHUB_PR_NUMBER: ${{ github.event.issue.number }}
PR_COMMENT_BODY: ${{ github.event.comment.body }}
- name: Commit and push
run: |
# use the last commit author name and email
git config --global user.name "$(git --no-pager log --format=format:'%an' -n 1)"
git config --global user.email "$(git --no-pager log --format=format:'%ae' -n 1)"
git add CHANGELOG.md
git commit -m "update CHANGELOG.md"
git push
-41
View File
@@ -1,41 +0,0 @@
name: CHANGELOG.md PR check
on:
pull_request:
types:
- opened
- synchronize
- labeled
- unlabeled
concurrency:
group: changelog-${{ github.ref }}
cancel-in-progress: true
jobs:
pr-changelog-entry:
name: Check CHANGELOG.md updated
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Check if CHANGELOG.md was updated
id: changelog-updated
uses: tj-actions/changed-files@v41
with:
files: CHANGELOG.md
- name: Fail if CHANGELOG.md was not updated and the "no changelog" label is absent
if: steps.changelog-updated.outputs.any_changed == 'false' && !contains(github.event.pull_request.labels.*.name, 'no changelog')
uses: actions/github-script@v6
with:
script: |
core.setFailed('Please add a changelog entry!')
- uses: mshick/[email protected]
if: always()
with:
message-id: changelog-entry-check
refresh-message-position: true
message: ${{ contains(github.event.pull_request.labels.*.name, 'no changelog') && '⏩ The changelog entry check has been skipped since the "no changelog" label is present.' || '✅ Thank you for adding the changelog entry!' }}
message-failure: |
❌ It looks like a changelog entry is missing for this PR. You have two options: you can add it manually, or you can use the changelog bot to do it for you.
🤖 To use the bot, simply comment on this PR with the command `/changelog-entry [breaking-change|new-feature|bug-fix|chore] [message]`.
⏩ If this PR doesn't require a changelog entry, such as if it's an internal change that doesn't affect the user experience, you can add the "no changelog" label to the PR.
-146
View File
@@ -1,146 +0,0 @@
name: Issue Triage
on:
issues:
types: [labeled]
jobs:
needs-repro:
runs-on: ubuntu-20.04
if: "${{ contains(github.event.label.name, 'incomplete issue: missing or invalid repro') }}"
steps:
- uses: actions/github-script@v3
with:
github-token: ${{ secrets.EXPO_BOT_GITHUB_TOKEN }}
script: |
github.issues.createComment({
issue_number: context.issue.number,
owner: context.repo.owner,
repo: context.repo.repo,
body: `Hi there! It looks like your issue requires a minimal reproducible example, but it is invalid or absent. Please prepare such an example and share it in a new issue.
**The best way to get attention to your issue is to provide a clean and easy way for a developer to reproduce the issue on their own machine.** Please do not provide your entire project, or a project with more code than is necessary to reproduce the issue.
A side benefit of going through the process of narrowing down the minimal amount of code needed to reproduce the issue is that you may get lucky and discover that the bug is due to a mistake in your application code that you can quickly fix on your own.
### Resources
- ["How to create a Minimal, Reproducible Example"](https://stackoverflow.com/help/minimal-reproducible-example)
- ["How to narrow down the source of an error"](https://expo.fyi/manual-debugging)
### Common concerns
#### "I've only been able to reproduce it in private, proprietary code"
You may not have spent enough time narrowing down the root cause of the issue. Try out the techniques discussed in this [manual debugging guide](https://expo.fyi/manual-debugging) to learn how to isolate the problem from the rest of your codebase.
#### "I didn't have time to create one"
That's understandable, it can take some time to prepare. We ask that you hold off on filing an issue until you are able to fully complete the required fields in the issue template.
#### "You can reproduce it by yourself by creating a project and following these steps"
This is useful knowledge, but it's still valuable to have the resulting project that is produced from running the steps, where you have verified you can reproduce the issue.
`})
github.issues.update({
issue_number: context.issue.number,
owner: context.repo.owner,
repo: context.repo.repo,
state: 'closed'
})
needs-info:
runs-on: ubuntu-20.04
if: "${{ contains(github.event.label.name, 'incomplete issue: missing info') }}"
steps:
- uses: actions/github-script@v3
with:
github-token: ${{ secrets.EXPO_BOT_GITHUB_TOKEN }}
script: |
github.issues.createComment({
issue_number: context.issue.number,
owner: context.repo.owner,
repo: context.repo.repo,
body: `Hello! There isn't enough information provided in your issue description for us to be able to help you. The best way to get help is to provide information to enable other developers to understand the issue and reproduce it on their own machines.
Please create a new issue and fill out the entire issue template to the best of your ability. Refer to the following resources for more information on how to create a good issue report.
### Resources
- ["How do I ask a good question?"](https://stackoverflow.com/help/how-to-ask)
- ["How to create a Minimal, Reproducible Example"](https://stackoverflow.com/help/minimal-reproducible-example)
- ["How to narrow down the source of an error"](https://expo.fyi/manual-debugging)
`})
github.issues.update({
issue_number: context.issue.number,
owner: context.repo.owner,
repo: context.repo.repo,
state: 'closed'
})
issue-accepted:
runs-on: ubuntu-20.04
if: github.event.label.name == 'issue accepted'
steps:
- uses: actions/github-script@v3
with:
github-token: ${{ secrets.EXPO_BOT_GITHUB_TOKEN }}
script: |
github.issues.createComment({
issue_number: context.issue.number,
owner: context.repo.owner,
repo: context.repo.repo,
body: `Thank you for filing this issue!
This comment acknowledges we believe this may be a bug and there’s enough information to investigate it.
However, we can’t promise any sort of timeline for resolution. We prioritize issues based on severity, breadth of impact, and alignment with our roadmap. If you’d like to help move it more quickly, you can continue to investigate it more deeply and/or you can open a pull request that fixes the cause.
`})
question:
runs-on: ubuntu-20.04
if: "${{ contains(github.event.label.name, 'invalid issue: question') }}"
steps:
- uses: actions/github-script@v3
with:
github-token: ${{ secrets.EXPO_BOT_GITHUB_TOKEN }}
script: |
github.issues.createComment({
issue_number: context.issue.number,
owner: context.repo.owner,
repo: context.repo.repo,
body: `Hello! Our GitHub issues are reserved for bug reports.
If you have a question about Expo or related tools, please post on our forums at https://forums.expo.dev/ or join our Discord at https://chat.expo.dev/.
### Resources
- ["How do I ask a good question?"](https://stackoverflow.com/help/how-to-ask)
- ["Join the community"](https://docs.expo.dev/next-steps/community/)
`})
github.issues.update({
issue_number: context.issue.number,
owner: context.repo.owner,
repo: context.repo.repo,
state: 'closed'
})
feature-request:
runs-on: ubuntu-20.04
if: "${{ contains(github.event.label.name, 'invalid issue: feature request') }}"
steps:
- uses: actions/github-script@v3
with:
github-token: ${{ secrets.EXPO_BOT_GITHUB_TOKEN }}
script: |
github.issues.createComment({
issue_number: context.issue.number,
owner: context.repo.owner,
repo: context.repo.repo,
body: `Hello! Our GitHub issues are reserved for bug reports.
If you would like to request a feature, please post to https://expo.canny.io/feature-requests.
`})
github.issues.update({
issue_number: context.issue.number,
owner: context.repo.owner,
repo: context.repo.repo,
state: 'closed'
})
-241
View File
@@ -1,241 +0,0 @@
on:
push:
tags:
- 'v*' # Push events to matching v*, i.e. v1.0, v20.15.10
name: Release workflow
jobs:
release:
name: Create a release
runs-on: ubuntu-latest
outputs:
upload_url: ${{ steps.create_release.outputs.upload_url }}
steps:
- id: create_release
uses: actions/create-release@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
tag_name: ${{ github.ref }}
draft: true
build-linux:
name: Build for Linux
runs-on: ubuntu-latest
needs: release
steps:
- uses: actions/checkout@v2
- uses: actions/setup-node@v2
with:
node-version: 20
- run: yarn install --frozen-lockfile --check-files
- run: yarn build
- name: Build tarballs
working-directory: ${{ github.workspace }}/packages/eas-cli
run: |
yarn pretarball-ci
yarn oclif pack:tarballs --no-xz --targets linux-x64,linux-arm
- id: x64
run: echo "filename=$(ls eas-*-x64.tar.gz)" >> $GITHUB_OUTPUT
working-directory: ${{ github.workspace }}/packages/eas-cli/dist
- id: arm
run: echo "filename=$(ls eas-*-arm.tar.gz)" >> $GITHUB_OUTPUT
working-directory: ${{ github.workspace }}/packages/eas-cli/dist
- name: Upload linux-x64 tarball
uses: actions/upload-release-asset@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
upload_url: ${{ needs.release.outputs.upload_url }}
asset_path: packages/eas-cli/dist/${{ steps.x64.outputs.filename }}
asset_name: ${{ steps.x64.outputs.filename }}
asset_content_type: application/gzip
- name: Upload linux-arm tarball
uses: actions/upload-release-asset@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
upload_url: ${{ needs.release.outputs.upload_url }}
asset_path: packages/eas-cli/dist/${{ steps.arm.outputs.filename }}
asset_name: ${{ steps.arm.outputs.filename }}
asset_content_type: application/gzip
build-mac:
name: Build for macOS
# Use macOS to get the correct platform specific dependencies
runs-on: macos-latest
needs: release
steps:
- uses: actions/checkout@v2
- uses: actions/setup-node@v2
with:
node-version: 20
- run: brew install python-setuptools
env:
HOMEBREW_NO_AUTO_UPDATE: 1
- run: yarn install --frozen-lockfile --check-files
- run: yarn build
- name: Build tarballs
working-directory: ${{ github.workspace }}/packages/eas-cli
run: |
yarn pretarball-ci
yarn oclif pack:tarballs --no-xz --targets darwin-x64,darwin-arm64
- id: x64
run: echo "filename=$(ls eas-*-x64.tar.gz)" >> $GITHUB_OUTPUT
working-directory: ${{ github.workspace }}/packages/eas-cli/dist
- id: arm64
run: echo "filename=$(ls eas-*-arm64.tar.gz)" >> $GITHUB_OUTPUT
working-directory: ${{ github.workspace }}/packages/eas-cli/dist
- name: Upload darwin-x64 tarball
uses: actions/upload-release-asset@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
upload_url: ${{ needs.release.outputs.upload_url }}
asset_path: packages/eas-cli/dist/${{ steps.x64.outputs.filename }}
asset_name: ${{ steps.x64.outputs.filename }}
asset_content_type: application/gzip
- name: Upload darwin-arm64 tarball
uses: actions/upload-release-asset@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
upload_url: ${{ needs.release.outputs.upload_url }}
asset_path: packages/eas-cli/dist/${{ steps.arm64.outputs.filename }}
asset_name: ${{ steps.arm64.outputs.filename }}
asset_content_type: application/gzip
build-windows:
name: Build for Windows
runs-on: ubuntu-latest
needs: release
steps:
- uses: actions/checkout@v2
- uses: actions/setup-node@v2
with:
node-version: 20
- run: yarn install --frozen-lockfile --check-files
- run: sudo apt-get install nsis
- run: yarn build
- run: |
yarn pretarball-ci
yarn oclif pack:win
working-directory: ${{ github.workspace }}/packages/eas-cli
- id: x64
run: echo "filename=$(ls eas-*-x64.exe)" >> $GITHUB_OUTPUT
working-directory: ${{ github.workspace }}/packages/eas-cli/dist/win32
- id: x86
run: echo "filename=$(ls eas-*-x86.exe)" >> $GITHUB_OUTPUT
working-directory: ${{ github.workspace }}/packages/eas-cli/dist/win32
- name: Upload x64 installer
uses: actions/upload-release-asset@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
upload_url: ${{ needs.release.outputs.upload_url }}
asset_path: packages/eas-cli/dist/win32/${{ steps.x64.outputs.filename }}
asset_name: ${{ steps.x64.outputs.filename }}
asset_content_type: application/gzip
- name: Upload x86 installer
uses: actions/upload-release-asset@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
upload_url: ${{ needs.release.outputs.upload_url }}
asset_path: packages/eas-cli/dist/win32/${{ steps.x86.outputs.filename }}
asset_name: ${{ steps.x86.outputs.filename }}
asset_content_type: application/gzip
publish-to-npm:
name: Publish to npm
runs-on: ubuntu-latest
needs: [build-linux, build-mac, build-windows]
steps:
- uses: actions/checkout@v2
- uses: actions/setup-node@v2
with:
registry-url: 'https://registry.npmjs.org/'
scope: 'expo'
node-version: 20
- name: Install dependencies
run: yarn install --frozen-lockfile --check-files
- name: Build
run: yarn build
- name: Publish packages to npm
run: yarn lerna publish from-package --yes --no-verify-access
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_ACCESS_TOKEN }}
- name: Add latest-eas-build-staging tag
run: |
VERSION=$(cat lerna.json | jq -r .version)
echo //registry.npmjs.org/:_authToken=$NODE_AUTH_TOKEN > ~/.npmrc
npm dist-tag add eas-cli@$VERSION latest-eas-build-staging
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_ACCESS_TOKEN }}
- name: Add latest-eas-build tag
run: |
VERSION=$(cat lerna.json | jq -r .version)
echo //registry.npmjs.org/:_authToken=$NODE_AUTH_TOKEN > ~/.npmrc
npm dist-tag add eas-cli@$VERSION latest-eas-build
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_ACCESS_TOKEN }}
release-changelog:
name: Update changelog and publish release
runs-on: ubuntu-latest
needs: publish-to-npm
steps:
- uses: actions/checkout@v2
with:
token: ${{ secrets.EXPO_BOT_PAT }}
- uses: actions/setup-node@v2
with:
node-version: 20
- name: Install dependencies
working-directory: ./scripts
run: yarn install --frozen-lockfile --check-files
- name: Resolve version
run: echo "EAS_CLI_VERSION=${GITHUB_REF#refs/tags/v}" >> $GITHUB_ENV
- name: Update changelog
working-directory: ./scripts
run: yarn --silent release-changelog $EAS_CLI_VERSION > /tmp/current-changelog
- name: Publish release
run: gh release edit v$EAS_CLI_VERSION --draft=false -F /tmp/current-changelog
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Commit and push
run: |
git fetch
git checkout main
git config --global user.email "[email protected]"
git config --global user.name "Expo CI"
git add CHANGELOG.md
git commit -m "update CHANGELOG.md"
git push
- name: Prepare changelog for Slack
run: |
tail -n +3 /tmp/current-changelog > /tmp/changelog.md
npx slackmd /tmp/changelog.md
echo 'SLACK_CHANGELOG<<EOF' >> $GITHUB_ENV
cat /tmp/changelog.slack.md >> $GITHUB_ENV
echo 'EOF' >> $GITHUB_ENV
- name: Slack Notification
uses: rtCamp/action-slack-notify@v2
env:
SLACK_CHANNEL: eas-cli
SLACK_COLOR: ${{ job.status }}
SLACK_ICON: https://github.com/expo-bot.png?size=48
SLACK_TITLE: 'Released eas-cli@${{ env.EAS_CLI_VERSION }}'
SLACK_MESSAGE: ${{ env.SLACK_CHANGELOG }}
SLACK_USERNAME: 'EAS CLI CI'
SLACK_WEBHOOK: ${{ secrets.SLACK_WEBHOOK }}
SLACK_FOOTER: ''
open-pr-in-universe:
name: Open a PR with `@expo/eas-json` bump in universe
needs: publish-to-npm
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Open a PR with `@expo/eas-json` version bump in universe
uses: benc-uk/workflow-dispatch@v1
with:
repo: expo/universe
ref: main
workflow: update-eas-json.yml
token: ${{ secrets.EXPO_BOT_PAT }}
-23
View File
@@ -1,23 +0,0 @@
name: Size
on:
pull_request:
types: [synchronize, opened]
concurrency:
group: size-${{ github.ref }}
cancel-in-progress: true
jobs:
compare:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions/setup-node@v2
with:
node-version: 20
- uses: preactjs/compressed-size-action@v2
with:
build-script: 'build:tarballs:linux'
repo-token: '${{ secrets.GITHUB_TOKEN }}'
pattern: './packages/eas-cli/dist/*.tar.gz'
compression: 'none'
strip-hash: "v\\d+.\\d+.\\d+-\\w+-"
clean-script: 'clean'
-24
View File
@@ -1,24 +0,0 @@
name: Close inactive issues
on:
workflow_dispatch:
schedule:
- cron: "0 0 * * *"
jobs:
close-issues:
runs-on: ubuntu-latest
steps:
- uses: actions/stale@v4
with:
ascending: false
operations-per-run: 300
days-before-issue-stale: 30
days-before-issue-close: 7
stale-issue-label: "stale"
stale-issue-message: "This issue is stale because it has been open for 30 days with no activity. If there is no activity in the next 7 days, the issue will be closed."
close-issue-message: "This issue was closed because it has been inactive for 7 days since being marked as stale. Please open a new issue if you believe you are encountering a related problem."
days-before-pr-stale: -1
days-before-pr-close: -1
enable-statistics: true
exempt-issue-labels: "needs review"
exempt-all-assignees: true
-30
View File
@@ -1,30 +0,0 @@
name: Test scripts
defaults:
run:
working-directory: scripts
on:
push:
branches: [main]
paths:
- 'scripts/**'
pull_request:
paths:
- 'scripts/**'
concurrency:
group: test-scripts-${{ github.ref }}
cancel-in-progress: true
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Setup node
uses: actions/setup-node@v2
with:
node-version: 20
- run: yarn install --frozen-lockfile --check-files
- run: yarn typecheck
-61
View File
@@ -1,61 +0,0 @@
name: Run tests
on:
push:
branches: [main]
pull_request:
types: [opened, synchronize]
concurrency:
group: test-${{ github.ref }}
cancel-in-progress: true
jobs:
test:
runs-on: ubuntu-latest
strategy:
matrix:
node: ['18', '20', '22']
include:
- node: '20'
coverage: true
name: Test with Node ${{ matrix.node }}
steps:
- uses: actions/checkout@v2
- name: Setup node
uses: actions/setup-node@v2
with:
node-version: ${{ matrix.node }}
- run: yarn install --frozen-lockfile --check-files
- run: yarn typecheck
- run: yarn test
if: ${{ !matrix.coverage }}
env:
NODE_OPTIONS: --max-old-space-size=8192
- run: yarn test --coverage
if: ${{ matrix.coverage }}
env:
# Oclif seems to cause OOM errors when testing with coverage enabled.
# Remove this whenever we solved those OOM errors.
NODE_OPTIONS: --max-old-space-size=8192
- run: yarn lint --max-warnings=0
- run: yarn install --frozen-lockfile --check-files
working-directory: ./scripts
- uses: codecov/codecov-action@v1
if: ${{ matrix.coverage }}
with:
token: ${{ secrets.CODECOV_TOKEN }}
- run: yarn lint-changelog
notify-slack:
runs-on: ubuntu-latest
needs: test
name: Notify Slack
if: ${{ github.ref == 'refs/heads/main' && always() }}
steps:
- uses: actions/checkout@v2
- name: Notify Slack
uses: ./.github/internal-actions/notify-slack-on-fail-or-recover
with:
channel: eas-cli
slack_webhook: ${{ secrets.SLACK_WEBHOOK }}
@@ -1,81 +0,0 @@
name: Open a PR bumping eas-build modules
on: workflow_dispatch
jobs:
open-pr:
name: Open a PR bumping eas-build modules
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
with:
ref: main
token: ${{ secrets.EXPO_BOT_PAT }}
- name: Setup node
uses: actions/setup-node@v2
with:
node-version: 20
- run: yarn install --frozen-lockfile
- name: Configure git
run: |
git config --global user.email "[email protected]"
git config --global user.name "Expo Bot"
- name: Upgrade eas-build modules
run: |
for DIR in packages/eas-cli packages/eas-json; do
pushd $DIR >/dev/null 2>&1
# packages are ordered according to output of `lerna list --toposort`
# https://github.com/expo/turtle-v2/pull/568
for PACKAGE in "@expo/downloader" "@expo/eas-build-job" "@expo/logger" "@expo/template-file" "@expo/steps" "@expo/turtle-spawn" "@expo/build-tools"; do
if grep -q $PACKAGE package.json; then
echo "[$DIR] yarn --exact add $PACKAGE"
yarn add --exact $PACKAGE
fi
done
popd >/dev/null 2>&1
done
- name: Set a branch name
id: bump-branch-name
run: |
HASH="$(echo $RANDOM | md5sum | head -c 10)"
BRANCH_NAME="@expo-bot/eas-build-bump-$HASH"
echo "BRANCH_NAME=$BRANCH_NAME" >> $GITHUB_OUTPUT
- name: Create a branch, commit, and push
run: |
BRANCH_NAME="${{ steps.bump-branch-name.outputs.BRANCH_NAME }}"
git checkout -b $BRANCH_NAME
git add .
git commit -m "Upgrade eas-build dependencies"
git push origin $BRANCH_NAME
- name: Open a PR to main
uses: repo-sync/pull-request@v2
id: open-pr
with:
source_branch: '${{ steps.bump-branch-name.outputs.BRANCH_NAME }}'
destination_branch: 'main'
pr_title: 'Upgrade eas-build dependencies'
pr_body: ':arrow_up: Upgrades [`eas-build`](https://github.com/expo/eas-build) dependencies.'
pr_reviewer: 'szdziedzic,radoslawkrzemien,sjchmiela,khamilowicz'
github_token: ${{ secrets.EXPO_BOT_PAT }}
- uses: actions/github-script@v3
with:
github-token: ${{ secrets.EXPO_BOT_PAT }}
script: |
github.issues.createComment({
issue_number: ${{ steps.open-pr.outputs.pr_number }},
owner: context.repo.owner,
repo: context.repo.repo,
body: "/changelog-entry chore Upgrade [`eas-build`](https://github.com/expo/eas-build) dependencies."
})
-21
View File
@@ -1,21 +0,0 @@
# Build directories
packages/*/build
packages/eas-cli/dist
packages/eas-cli/tmp
scripts/build
# Code editors
.idea
.history
.vscode
# macOS
.DS_Store
# Node
node_modules
*-debug.log
*-error.log
# Tests
coverage
-9
View File
@@ -1,9 +0,0 @@
{
"printWidth": 100,
"tabWidth": 2,
"singleQuote": true,
"bracketSameLine": true,
"trailingComma": "es5",
"arrowParens": "avoid",
"endOfLine": "auto"
}
-10
View File
@@ -1,10 +0,0 @@
{
"eslint.enable": true,
"search.exclude": {
"**/node_modules": true,
"**/bower_components": true,
"**/*.code-search": true,
"**/src/**/build/**": false,
"**/src/build/**": false,
}
}
-1
View File
@@ -1 +0,0 @@
save-prefix ""
-2594
View File
File diff suppressed because it is too large. Load diff
-16
View File
@@ -1,16 +0,0 @@
* @szdziedzic @khamilowicz @sjchmiela
packages/eas-cli/src/build @szdziedzic @khamilowicz @sjchmiela
packages/eas-cli/src/commands/build @szdziedzic @khamilowicz @sjchmiela
packages/eas-cli/src/submit @szdziedzic @khamilowicz @sjchmiela
packages/eas-cli/src/commands/submit.ts @szdziedzic @khamilowicz @sjchmiela
packages/eas-cli/src/update @EvanBacon @byCedric @wschurman
packages/eas-cli/src/commands/update @EvanBacon @byCedric @wschurman
packages/eas-cli/schema @bycedric
packages/eas-cli/src/metadata @bycedric
packages/eas-cli/src/commands/metadata @bycedric
packages/eas-json @szdziedzic @khamilowicz @sjchmiela
-7
View File
@@ -1,7 +0,0 @@
# Community Code of Conduct
The Expo team is committed to fostering an open and welcoming environment.
**Our Community Guidelines can be found here:**
https://expo.dev/guidelines
-67
View File
@@ -1,67 +0,0 @@
# Development
Set up an alias for the EAS CLI so you can try it in projects all around your computer. The project is compiled on the fly so you don't need to run a build (watch) command.
> The only exception is when you just cloned the repository or there have been changes in `packages/eas-json`. In that case, you'll have to run `yarn build` in the root.
Open your `.zshrc` or other config file and add:
```
alias easd="/PATH/TO/eas-cli/bin/run"
```
Then use it with `easd` like `easd build`.
If you're making changes to `packages/eas-json` or prefer/need to work with production code, start the build command in watch mode with:
```
yarn start
```
If it is easier for you to work with the code locally with compiler options set to:
```json
...
"compilerOptions": {
...
"noUnusedLocals": false,
"noUnusedParameters": false,
...
}
...
```
you can consider using:
```
yarn start-allow-unused
```
## Format
- Be sure to update the [`CHANGELOG.md`](./CHANGELOG.md) with changes for every PR. There is a changelog bot that can generate the proper entry for you. The instructions on how to use it are in the PR description placeholder.
- End `async` functions with `Async` like `runAsync`. This is just how we format functions at Expo.
- Utilize the unified `Log` module instead of `console.log`.
## Using local and staging API
For development against local API:
- Set up the API server as described at https://github.com/expo/universe/tree/main/server/www.
- Set the `EXPO_LOCAL=1` env variable either on every command run (e.g. `EXPO_LOCAL=1 easd ...`) or more permamently with `export EXPO_LOCAL=1` (works in a current shell session).
For development against staging API:
- Set the `EXPO_STAGING=1` env variable either on every command run (e.g. `EXPO_STAGING=1 easd ...`) or more permamently with `export EXPO_STAGING=1` (works in a current shell session).
## Working on local builds (`eas build --local`)
See https://github.com/expo/eas-build/blob/main/DEVELOPMENT.md for how to set up your environment when making changes to [`eas-cli-local-build-plugin`](https://github.com/expo/eas-build/tree/main/packages/local-build-plugin) and/or [`build-tools`](https://github.com/expo/eas-build/tree/main/packages/build-tools).
## Testing
Run `yarn test` either in the repository root or in a package directory that you're working on.
## Releasing
See [RELEASING.md](./RELEASING.md).
-21
View File
@@ -1,21 +0,0 @@
The MIT License (MIT)
Copyright (c) 2020-present 650 Industries, Inc. (aka Expo)
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
-1
View File
@@ -1 +0,0 @@
packages/eas-cli/README.md
+19
View File
@@ -0,0 +1,19 @@
# Simulator control context and import QA
Codex recorded the real EAS CLI terminal against authenticated serve-sim in an owned Tart VM on 2026-10-17. Session lookup and account authentication use local GraphQL fixtures; the WebSocket host actions, uploads, downloads, device metadata and iOS Simulator are real. This is local native QA, not hosted EAS or API-to-worker E2E.
- Interactive recording: EAS CLI `9c5aa3860`, with the same displayed fields as final `d6396aa11`. Shows session name, project, real device name/runtime, state and IDs; searchable action picker; keyboard write/readback; screenshot save; Exit.
- Import recording: final EAS CLI `d6396aa11`. Validates a seven-action file, shows the same header and local file destinations, uploads text into the app, reads identical bytes back, launches the app, saves a screenshot, and exits without a picker. `verified-in-app.png` shows the imported text in the real UIKit fixture app.
- serve-sim source checkout: `1c41d7e0aa3f27ca9cc10f74f2c8bd2442b71620`. The 62-action catalog was fetched successfully. Earlier all-62 direct/import action QA remains linked in the PR; this run repeats the changed UI/JSON/lifecycle and file-import paths, not the whole matrix.
- `--list --json`, direct-action `--json`, import success `--json` and import failure `--json` stdout parsed as JSON. Direct/import results include safe session metadata; the list remains the action catalog. No picker or human header appears on JSON stdout.
- File upload/readback SHA-256: `b0af4ba55cc8f460d10c0583ed8bd731b090c950fb18993cf61b0a2cbb7998ee`.
- Exit, Ctrl-C (130), session stop (0), and session error (1) restored terminal canonical/echo settings. Session status transitions use the local fixture. A fresh-app stop failure also demonstrated stop-on-failure and structured error output.
- A cold device-metadata request timed out under host load and fell back to the pinned UDID. Subsequent native requests and both recordings returned the real device name/runtime.
- Final focused validation: 213 tests across seven suites; typecheck, build, changed-file type-aware lint, formatting and diff checks passed. Full suite during iteration: 356 suites, 3,523 tests passed, four skipped and 19 snapshots; the known worker teardown warning was previously reproduced on the base. Final presentation fixes were followed by the focused suite.
- Three local review lenses (Stanley, Will and security) found no blocker. Verified issues fixed: alternative upload usage, errored-session exit status, oclif JSON example formatting, and output-directory hints. Android explicitly reports unsupported; no Android or hosted EAS acceptance is claimed.
Recordings capture actual PTY output and input. Rendered videos use the captured cast; existing dependency deprecation warning DEP0040 was suppressed only in the recorder environment. Credentials and private server logs are excluded.
The published stack subsequently merged current main `0199cd933` through all four layers (top `fae8c090f`). Control/UI source is unchanged from the recordings. Post-merge validation: all 25 simulator suites / 607 tests passed, including the combined preview-access and embedding tests; typecheck, build, affected-file lint and formatting passed.
— Codex
-14
View File
@@ -1,14 +0,0 @@
# Releasing EAS CLI
1. Run `yarn release` in the repository root folder. The next version is chosen automatically based on the changelog entries. If you want to use different version, pass the version string as a positional argument to the command, e.g. `yarn release 1.2.3`.
2. That's it! GitHub Actions is going to take care of the rest. Watch the #eas-cli Slack channel for a successful release notification.
## Choosing the next version
As stated above, the next EAS CLI version is chosen automatically based on the existing changelog entries under the unreleased section in **CHANGELOG.md**.
The algorithm works as follows:
- If there are any entries in the "🛠 Breaking changes" section, bump the MAJOR version.
- Otherwise, if there are any entries in the "🎉 New features", bump the MINOR version.
- Otherwise, bump the PATCH version.
-5
View File
@@ -1,5 +0,0 @@
# Security Policy
## Reporting a Vulnerability
Instead of opening a GitHub issue for security vulnerabilities, send an email to our secure inbox: secure@expo.io.
-1
View File
@@ -1 +0,0 @@
../packages/eas-cli/bin/dev
-17
View File
@@ -1,17 +0,0 @@
coverage:
round: up
precision: 2
range: 30..100
status:
patch: false
project:
default:
informational: true
eas-cli:
informational: true
paths:
- packages/eas-cli
eas-json:
informational: true
paths:
- packages/eas-json
+51
View File
@@ -0,0 +1,51 @@
{
"version": 1,
"actions": [
{
"action": "app.stop",
"params": {
"bundleId": "dev.expo.eas-control-qa"
}
},
{
"action": "appearance.set",
"params": {
"value": "dark"
}
},
{
"action": "app.file.upload",
"params": {
"bundleId": "dev.expo.eas-control-qa",
"relativePath": "Documents/imported.txt",
"overwrite": true
},
"file": "./imported.txt"
},
{
"action": "app.file.read",
"params": {
"bundleId": "dev.expo.eas-control-qa",
"relativePath": "Documents/imported.txt"
},
"output": "./readback.txt"
},
{
"action": "app.launch",
"params": {
"bundleId": "dev.expo.eas-control-qa",
"openUrl": "easqa://verified"
}
},
{
"action": "screenshot.capture",
"output": "./verified-in-app.png"
},
{
"action": "appearance.set",
"params": {
"value": "light"
}
}
]
}
+254
View File
@@ -0,0 +1,254 @@
{"version": 2, "width": 116, "height": 51, "title": "eas simulator:control --import import-demo.json \u00b7 Tart iOS Simulator", "env": {"TERM": "xterm-256color"}}
[2.4596623750403523, "o", "\u001b[?25l"]
[2.468252541963011, "o", "\u001b[36m⠋\u001b[39m Connecting to simulator session tart-qa"]
[2.5425652919802815, "o", "\u001b[2K\u001b[1G\u001b[36m⠙\u001b[39m Connecting to simulator session tart-qa"]
[2.627630249829963, "o", "\u001b[2K\u001b[1G\u001b[36m⠹\u001b[39m Connecting to simulator session tart-qa"]
[2.707990749971941, "o", "\u001b[2K\u001b[1G\u001b[36m⠸\u001b[39m Connecting to simulator session tart-qa"]
[2.7941946669016033, "o", "\u001b[2K\u001b[1G\u001b[36m⠼\u001b[39m Connecting to simulator session tart-qa"]
[2.869631625013426, "o", "\u001b[2K\u001b[1G\u001b[36m⠴\u001b[39m Connecting to simulator session tart-qa"]
[2.9577866250183433, "o", "\u001b[2K\u001b[1G\u001b[36m⠦\u001b[39m Connecting to simulator session tart-qa"]
[3.0378237918484956, "o", "\u001b[2K\u001b[1G"]
[3.0403360000345856, "o", "\u001b[36m⠧\u001b[39m Connecting to simulator session tart-qa"]
[3.121280499966815, "o", "\u001b[2K\u001b[1G\u001b[36m⠇\u001b[39m Connecting to simulator session tart-qa"]
[3.2034899999853224, "o", "\u001b[2K\u001b[1G"]
[3.2051411669235677, "o", "\u001b[36m⠏\u001b[39m Connecting to simulator session tart-qa"]
[3.2856196670327336, "o", "\u001b[2K\u001b[1G\u001b[36m⠋\u001b[39m Connecting to simulator session tart-qa"]
[3.367519374936819, "o", "\u001b[2K\u001b[1G\u001b[36m⠙\u001b[39m Connecting to simulator session tart-qa"]
[3.4474066249094903, "o", "\u001b[2K\u001b[1G\u001b[36m⠹\u001b[39m Connecting to simulator session tart-qa"]
[3.5292347089853138, "o", "\u001b[2K\u001b[1G\u001b[36m⠸\u001b[39m Connecting to simulator session tart-qa"]
[3.613229166949168, "o", "\u001b[2K\u001b[1G\u001b[36m⠼\u001b[39m Connecting to simulator session tart-qa"]
[3.7037641669157892, "o", "\u001b[2K\u001b[1G\u001b[36m⠴\u001b[39m Connecting to simulator session tart-qa"]
[3.7836617089342326, "o", "\u001b[2K\u001b[1G\u001b[36m⠦\u001b[39m Connecting to simulator session tart-qa"]
[3.8637555418536067, "o", "\u001b[2K\u001b[1G\u001b[36m⠧\u001b[39m Connecting to simulator session tart-qa"]
[3.9463117090053856, "o", "\u001b[2K\u001b[1G\u001b[36m⠇\u001b[39m Connecting to simulator session tart-qa"]
[4.030578333884478, "o", "\u001b[2K\u001b[1G\u001b[36m⠏\u001b[39m Connecting to simulator session tart-qa"]
[4.108949000015855, "o", "\u001b[2K\u001b[1G\u001b[36m⠋\u001b[39m Connecting to simulator session tart-qa"]
[4.191971791908145, "o", "\u001b[2K\u001b[1G\u001b[36m⠙\u001b[39m Connecting to simulator session tart-qa"]
[4.274297208990902, "o", "\u001b[2K\u001b[1G\u001b[36m⠹\u001b[39m Connecting to simulator session tart-qa"]
[4.357123333960772, "o", "\u001b[2K\u001b[1G\u001b[36m⠸\u001b[39m Connecting to simulator session tart-qa"]
[4.437165708979592, "o", "\u001b[2K\u001b[1G\u001b[36m⠼\u001b[39m Connecting to simulator session tart-qa"]
[4.52031399984844, "o", "\u001b[2K\u001b[1G\u001b[36m⠴\u001b[39m Connecting to simulator session tart-qa"]
[4.6005204590037465, "o", "\u001b[2K\u001b[1G\u001b[36m⠦\u001b[39m Connecting to simulator session tart-qa"]
[4.68557466682978, "o", "\u001b[2K\u001b[1G\u001b[36m⠧\u001b[39m Connecting to simulator session tart-qa"]
[4.765125958947465, "o", "\u001b[2K\u001b[1G\u001b[36m⠇\u001b[39m Connecting to simulator session tart-qa"]
[4.846912291832268, "o", "\u001b[2K\u001b[1G\u001b[36m⠏\u001b[39m Connecting to simulator session tart-qa"]
[4.927491249982268, "o", "\u001b[2K\u001b[1G\u001b[36m⠋\u001b[39m Connecting to simulator session tart-qa"]
[5.019439166877419, "o", "\u001b[2K\u001b[1G\u001b[36m⠙\u001b[39m Connecting to simulator session tart-qa"]
[5.101469249930233, "o", "\u001b[2K\u001b[1G\u001b[36m⠹\u001b[39m Connecting to simulator session tart-qa"]
[5.182818084023893, "o", "\u001b[2K\u001b[1G\u001b[36m⠸\u001b[39m Connecting to simulator session tart-qa"]
[5.263798083877191, "o", "\u001b[2K\u001b[1G\u001b[36m⠼\u001b[39m Connecting to simulator session tart-qa"]
[5.3449329999275506, "o", "\u001b[2K\u001b[1G\u001b[36m⠴\u001b[39m Connecting to simulator session tart-qa"]
[5.425754416966811, "o", "\u001b[2K\u001b[1G\u001b[36m⠦\u001b[39m Connecting to simulator session tart-qa"]
[5.505597916897386, "o", "\u001b[2K\u001b[1G"]
[5.514800916891545, "o", "\u001b[36m⠧\u001b[39m Connecting to simulator session tart-qa"]
[5.587791417026892, "o", "\u001b[2K\u001b[1G\u001b[36m⠇\u001b[39m Connecting to simulator session tart-qa"]
[5.672002624953166, "o", "\u001b[2K\u001b[1G\u001b[36m⠏\u001b[39m Connecting to simulator session tart-qa"]
[5.751895333873108, "o", "\u001b[2K\u001b[1G\u001b[36m⠋\u001b[39m Connecting to simulator session tart-qa"]
[5.834119374863803, "o", "\u001b[2K\u001b[1G\u001b[36m⠙\u001b[39m Connecting to simulator session tart-qa"]
[5.916902625001967, "o", "\u001b[2K\u001b[1G\u001b[36m⠹\u001b[39m Connecting to simulator session tart-qa"]
[5.999366374919191, "o", "\u001b[2K\u001b[1G\u001b[36m⠸\u001b[39m Connecting to simulator session tart-qa"]
[6.080119666876271, "o", "\u001b[2K\u001b[1G\u001b[36m⠼\u001b[39m Connecting to simulator session tart-qa"]
[6.162979083834216, "o", "\u001b[2K\u001b[1G\u001b[36m⠴\u001b[39m Connecting to simulator session tart-qa"]
[6.248023624997586, "o", "\u001b[2K\u001b[1G\u001b[36m⠦\u001b[39m Connecting to simulator session tart-qa"]
[6.3269378340337425, "o", "\u001b[2K\u001b[1G\u001b[36m⠧\u001b[39m Connecting to simulator session tart-qa"]
[6.407871208852157, "o", "\u001b[2K\u001b[1G\u001b[36m⠇\u001b[39m Connecting to simulator session tart-qa"]
[6.488582749851048, "o", "\u001b[2K\u001b[1G\u001b[36m⠏\u001b[39m Connecting to simulator session tart-qa"]
[6.574364291969687, "o", "\u001b[2K\u001b[1G\u001b[36m⠋\u001b[39m Connecting to simulator session tart-qa"]
[6.656044916948304, "o", "\u001b[2K\u001b[1G\u001b[36m⠙\u001b[39m Connecting to simulator session tart-qa"]
[6.736275749979541, "o", "\u001b[2K\u001b[1G\u001b[36m⠹\u001b[39m Connecting to simulator session tart-qa"]
[6.817582749994472, "o", "\u001b[2K\u001b[1G\u001b[36m⠸\u001b[39m Connecting to simulator session tart-qa"]
[6.906716624973342, "o", "\u001b[2K\u001b[1G\u001b[36m⠼\u001b[39m Connecting to simulator session tart-qa"]
[6.986072708852589, "o", "\u001b[2K\u001b[1G\u001b[36m⠴\u001b[39m Connecting to simulator session tart-qa"]
[7.0667139168363065, "o", "\u001b[2K\u001b[1G\u001b[36m⠦\u001b[39m Connecting to simulator session tart-qa"]
[7.13800562499091, "o", "\u001b[2K\u001b[1G\u001b[?25h"]
[7.139354833867401, "o", "\u001b[32m✔\u001b[39m Connected to simulator session tart-qa\r\n"]
[7.139386833878234, "o", "\u001b[?25l"]
[7.13955358392559, "o", "\u001b[36m⠋\u001b[39m Validating import-demo.json"]
[7.222704208921641, "o", "\u001b[2K\u001b[1G\u001b[36m⠙\u001b[39m Validating import-demo.json"]
[7.245556541951373, "o", "\u001b[2K\u001b[1G\u001b[?25h"]
[7.247009333921596, "o", "\u001b[32m✔\u001b[39m Validated 7 actions\r\n"]
[7.247259750030935, "o", "\r\n"]
[7.249119208892807, "o", "\u001b[1mSimulator controls\u001b[22m\r\n\u001b[2mSession \u001b[22m Owned Tart CLI QA\r\n\u001b[2mProject \u001b[22m @local-qa/local-qa\r\n\u001b[2mDevice \u001b[22m eas-control-context-20261017 · iOS 26.4\r\n\u001b[2mState \u001b[22m in progress\r\n\u001b[2mSession ID\u001b[22m tart-qa\r\n\u001b[2mDevice ID \u001b[22m 9D46A327-1A3E-442C-8481-68A96914CF83\r\n\r\n\u001b[1mimport-demo.json\u001b[22m\r\n\u001b[90m/private/tmp/eas-control-context-20261017/import-demo.json\u001b[39m\r\n\u001b[90m Read: /private/tmp/eas-control-context-20261017/imported.txt\u001b[39m\r\n\u001b[90m Output: /private/tmp/eas-control-context-20261017/readback.txt\u001b[39m\r\n\u001b[90m Output: /private/tmp/eas-control-context-20261017/verified-in-app.png\u001b[39m\r\n\u001b[90mExplicit output files are overwritten; directory outputs use simulator filenames and fail if the file exists.\u001b[39m\r\n\r\n\u001b[?25l"]
[7.251978749874979, "o", "\u001b[36m⠋\u001b[39m [1/7] app.stop"]
[7.332890583900735, "o", "\u001b[2K\u001b[1G\u001b[36m⠙\u001b[39m [1/7] app.stop"]
[7.414331624982879, "o", "\u001b[2K\u001b[1G\u001b[36m⠹\u001b[39m [1/7] app.stop"]
[7.496396499918774, "o", "\u001b[2K\u001b[1G\u001b[36m⠸\u001b[39m [1/7] app.stop"]
[7.577906874939799, "o", "\u001b[2K"]
[7.600593291921541, "o", "\u001b[1G\u001b[36m⠼\u001b[39m [1/7] app.stop"]
[7.64307912485674, "o", "\u001b[2K\u001b[1G\u001b[?25h"]
[7.647269416833296, "o", "\u001b[32m✔\u001b[39m [1/7] app.stop \u001b[2m<1s\u001b[22m\r\n\u001b[?25l"]
[7.648644083878025, "o", "\u001b[36m⠋\u001b[39m [2/7] appearance.set"]
[7.7315515000373125, "o", "\u001b[2K\u001b[1G\u001b[36m⠙\u001b[39m [2/7] appearance.set"]
[7.811032374855131, "o", "\u001b[2K\u001b[1G\u001b[36m⠹\u001b[39m [2/7] appearance.set"]
[7.892804541857913, "o", "\u001b[2K\u001b[1G\u001b[?25h"]
[7.911673708818853, "o", "\u001b[32m✔\u001b[39m [2/7] appearance.set \u001b[2m<1s\u001b[22m\r\n\u001b[?25l"]
[7.9133055419661105, "o", "\u001b[36m⠋\u001b[39m [3/7] app.file.upload"]
[7.993380917003378, "o", "\u001b[2K\u001b[1G\u001b[36m⠙\u001b[39m [3/7] app.file.upload"]
[8.076198583934456, "o", "\u001b[2K\u001b[1G"]
[8.101115458877757, "o", "\u001b[36m⠹\u001b[39m [3/7] app.file.upload"]
[8.157890458824113, "o", "\u001b[2K\u001b[1G\u001b[36m⠸\u001b[39m [3/7] app.file.upload"]
[8.238219708902761, "o", "\u001b[2K"]
[8.251997374929488, "o", "\u001b[1G\u001b[36m⠼\u001b[39m [3/7] app.file.upload"]
[8.319125583861023, "o", "\u001b[2K\u001b[1G\u001b[36m⠴\u001b[39m [3/7] app.file.upload"]
[8.39964795904234, "o", "\u001b[2K\u001b[1G\u001b[36m⠦\u001b[39m [3/7] app.file.upload"]
[8.479942749952897, "o", "\u001b[2K\u001b[1G\u001b[36m⠧\u001b[39m [3/7] app.file.upload"]
[8.529454541858286, "o", "\u001b[2K\u001b[1G\u001b[?25h"]
[8.529603749979287, "o", "\u001b[32m✔\u001b[39m [3/7] app.file.upload \u001b[2m<1s\u001b[22m\r\n"]
[8.529682666994631, "o", " {\r\n \"relativePath\": \"Documents/imported.txt\",\r\n \"bytes\": 78\r\n }\r\n"]
[8.529694417025894, "o", "\u001b[?25l"]
[8.5298042090144, "o", "\u001b[36m⠋\u001b[39m [4/7] app.file.read"]
[8.614456083858386, "o", "\u001b[2K\u001b[1G\u001b[36m⠙\u001b[39m [4/7] app.file.read"]
[8.70874174986966, "o", "\u001b[2K\u001b[1G\u001b[36m⠹\u001b[39m [4/7] app.file.read"]
[8.785724499961361, "o", "\u001b[2K\u001b[1G\u001b[36m⠸\u001b[39m [4/7] app.file.read"]
[8.869370416970924, "o", "\u001b[2K\u001b[1G\u001b[36m⠼\u001b[39m [4/7] app.file.read"]
[8.950997791951522, "o", "\u001b[2K\u001b[1G\u001b[36m⠴\u001b[39m [4/7] app.file.read"]
[8.973261334002018, "o", "\u001b[2K\u001b[1G\u001b[?25h"]
[8.97356695891358, "o", "\u001b[32m✔\u001b[39m [4/7] app.file.read \u001b[2m<1s\u001b[22m\r\n Saved: /private/tmp/eas-control-context-20261017/readback.txt\r\n"]
[8.973581166937947, "o", "\u001b[?25l"]
[8.973720166832209, "o", "\u001b[36m⠋\u001b[39m [5/7] app.launch"]
[9.055221166927367, "o", "\u001b[2K\u001b[1G\u001b[36m⠙\u001b[39m [5/7] app.launch"]
[9.153222249820828, "o", "\u001b[2K\u001b[1G\u001b[36m⠹\u001b[39m [5/7] app.launch"]
[9.234001541975886, "o", "\u001b[2K\u001b[1G\u001b[36m⠸\u001b[39m [5/7] app.launch"]
[9.319874417036772, "o", "\u001b[2K\u001b[1G\u001b[36m⠼\u001b[39m [5/7] app.launch"]
[9.396764958975837, "o", "\u001b[2K\u001b[1G\u001b[36m⠴\u001b[39m [5/7] app.launch"]
[9.47868529194966, "o", "\u001b[2K\u001b[1G\u001b[36m⠦\u001b[39m [5/7] app.launch"]
[9.563849249854684, "o", "\u001b[2K\u001b[1G\u001b[36m⠧\u001b[39m [5/7] app.launch"]
[9.648745791986585, "o", "\u001b[2K\u001b[1G\u001b[36m⠇\u001b[39m [5/7] app.launch"]
[9.729599333833903, "o", "\u001b[2K\u001b[1G\u001b[36m⠏\u001b[39m [5/7] app.launch"]
[9.811809291830286, "o", "\u001b[2K\u001b[1G\u001b[36m⠋\u001b[39m [5/7] app.launch"]
[9.894461416872218, "o", "\u001b[2K\u001b[1G\u001b[36m⠙\u001b[39m [5/7] app.launch"]
[9.990136374952272, "o", "\u001b[2K\u001b[1G\u001b[36m⠹\u001b[39m [5/7] app.launch"]
[10.062177499989048, "o", "\u001b[2K\u001b[1G\u001b[36m⠸\u001b[39m [5/7] app.launch"]
[10.143968374934047, "o", "\u001b[2K\u001b[1G\u001b[36m⠼\u001b[39m [5/7] app.launch"]
[10.223695749882609, "o", "\u001b[2K\u001b[1G\u001b[36m⠴\u001b[39m [5/7] app.launch"]
[10.306327791884542, "o", "\u001b[2K\u001b[1G\u001b[36m⠦\u001b[39m [5/7] app.launch"]
[10.385356249986216, "o", "\u001b[2K\u001b[1G\u001b[36m⠧\u001b[39m [5/7] app.launch"]
[10.467466125031933, "o", "\u001b[2K\u001b[1G\u001b[36m⠇\u001b[39m [5/7] app.launch"]
[10.549067999934778, "o", "\u001b[2K\u001b[1G\u001b[36m⠏\u001b[39m [5/7] app.launch"]
[10.628886791877449, "o", "\u001b[2K\u001b[1G\u001b[36m⠋\u001b[39m [5/7] app.launch"]
[10.712016124976799, "o", "\u001b[2K\u001b[1G\u001b[36m⠙\u001b[39m [5/7] app.launch"]
[10.79425449995324, "o", "\u001b[2K\u001b[1G\u001b[36m⠹\u001b[39m [5/7] app.launch"]
[10.8756537088193, "o", "\u001b[2K\u001b[1G\u001b[36m⠸\u001b[39m [5/7] app.launch"]
[10.9653779999353, "o", "\u001b[2K\u001b[1G\u001b[36m⠼\u001b[39m [5/7] app.launch"]
[11.047358250012621, "o", "\u001b[2K\u001b[1G\u001b[36m⠴\u001b[39m [5/7] app.launch"]
[11.128127499949187, "o", "\u001b[2K\u001b[1G\u001b[36m⠦\u001b[39m [5/7] app.launch"]
[11.209073917008936, "o", "\u001b[2K\u001b[1G\u001b[36m⠧\u001b[39m [5/7] app.launch"]
[11.2902685420122, "o", "\u001b[2K"]
[11.315825999947265, "o", "\u001b[1G\u001b[36m⠇\u001b[39m [5/7] app.launch"]
[11.372839167015627, "o", "\u001b[2K\u001b[1G\u001b[36m⠏\u001b[39m [5/7] app.launch"]
[11.45364262489602, "o", "\u001b[2K\u001b[1G\u001b[36m⠋\u001b[39m [5/7] app.launch"]
[11.534794374834746, "o", "\u001b[2K\u001b[1G\u001b[36m⠙\u001b[39m [5/7] app.launch"]
[11.615886125015095, "o", "\u001b[2K\u001b[1G\u001b[36m⠹\u001b[39m [5/7] app.launch"]
[11.696463708998635, "o", "\u001b[2K\u001b[1G\u001b[36m⠸\u001b[39m [5/7] app.launch"]
[11.777258083922789, "o", "\u001b[2K\u001b[1G\u001b[36m⠼\u001b[39m [5/7] app.launch"]
[11.857859459007159, "o", "\u001b[2K\u001b[1G\u001b[36m⠴\u001b[39m [5/7] app.launch"]
[11.93836258398369, "o", "\u001b[2K\u001b[1G\u001b[36m⠦\u001b[39m [5/7] app.launch"]
[12.021289666881785, "o", "\u001b[2K\u001b[1G\u001b[36m⠧\u001b[39m [5/7] app.launch"]
[12.100873833987862, "o", "\u001b[2K\u001b[1G\u001b[36m⠇\u001b[39m [5/7] app.launch"]
[12.183285541832447, "o", "\u001b[2K\u001b[1G\u001b[36m⠏\u001b[39m [5/7] app.launch"]
[12.266338624991477, "o", "\u001b[2K\u001b[1G\u001b[36m⠋\u001b[39m [5/7] app.launch"]
[12.34678270900622, "o", "\u001b[2K\u001b[1G\u001b[36m⠙\u001b[39m [5/7] app.launch"]
[12.42967874999158, "o", "\u001b[2K\u001b[1G\u001b[36m⠹\u001b[39m [5/7] app.launch"]
[12.512262333882973, "o", "\u001b[2K\u001b[1G\u001b[36m⠸\u001b[39m [5/7] app.launch"]
[12.596894291928038, "o", "\u001b[2K\u001b[1G\u001b[36m⠼\u001b[39m [5/7] app.launch"]
[12.679858542047441, "o", "\u001b[2K"]
[12.696894541848451, "o", "\u001b[1G\u001b[36m⠴\u001b[39m [5/7] app.launch"]
[12.762672459008172, "o", "\u001b[2K\u001b[1G\u001b[36m⠦\u001b[39m [5/7] app.launch"]
[12.845901541877538, "o", "\u001b[2K\u001b[1G\u001b[36m⠧\u001b[39m [5/7] app.launch"]
[12.931836667004973, "o", "\u001b[2K\u001b[1G\u001b[36m⠇\u001b[39m [5/7] app.launch"]
[13.011527041904628, "o", "\u001b[2K\u001b[1G\u001b[36m⠏\u001b[39m [5/7] app.launch"]
[13.092334958957508, "o", "\u001b[2K\u001b[1G\u001b[36m⠋\u001b[39m [5/7] app.launch"]
[13.175904083997011, "o", "\u001b[2K\u001b[1G\u001b[36m⠙\u001b[39m [5/7] app.launch"]
[13.255637249909341, "o", "\u001b[2K\u001b[1G\u001b[36m⠹\u001b[39m [5/7] app.launch"]
[13.338237916817889, "o", "\u001b[2K\u001b[1G\u001b[36m⠸\u001b[39m [5/7] app.launch"]
[13.418565792031586, "o", "\u001b[2K\u001b[1G\u001b[36m⠼\u001b[39m [5/7] app.launch"]
[13.501273917034268, "o", "\u001b[2K\u001b[1G\u001b[36m⠴\u001b[39m [5/7] app.launch"]
[13.58228962495923, "o", "\u001b[2K\u001b[1G\u001b[36m⠦\u001b[39m [5/7] app.launch"]
[13.66202741698362, "o", "\u001b[2K\u001b[1G"]
[13.662081749876961, "o", "\u001b[36m⠧\u001b[39m [5/7] app.launch"]
[13.746383916819468, "o", "\u001b[2K\u001b[1G\u001b[36m⠇\u001b[39m [5/7] app.launch"]
[13.827674958854914, "o", "\u001b[2K\u001b[1G\u001b[36m⠏\u001b[39m [5/7] app.launch"]
[13.911160541931167, "o", "\u001b[2K\u001b[1G\u001b[36m⠋\u001b[39m [5/7] app.launch"]
[13.990865166997537, "o", "\u001b[2K\u001b[1G\u001b[36m⠙\u001b[39m [5/7] app.launch"]
[14.070741500006989, "o", "\u001b[2K\u001b[1G"]
[14.101837833877653, "o", "\u001b[36m⠹\u001b[39m [5/7] app.launch"]
[14.154566916869953, "o", "\u001b[2K\u001b[1G\u001b[36m⠸\u001b[39m [5/7] app.launch"]
[14.233832208905369, "o", "\u001b[2K\u001b[1G\u001b[36m⠼\u001b[39m [5/7] app.launch"]
[14.31584154185839, "o", "\u001b[2K\u001b[1G\u001b[36m⠴\u001b[39m [5/7] app.launch"]
[14.395961541915312, "o", "\u001b[2K\u001b[1G\u001b[36m⠦\u001b[39m [5/7] app.launch"]
[14.478099291911349, "o", "\u001b[2K\u001b[1G\u001b[36m⠧\u001b[39m [5/7] app.launch"]
[14.55978100001812, "o", "\u001b[2K\u001b[1G\u001b[36m⠇\u001b[39m [5/7] app.launch"]
[14.640580541919917, "o", "\u001b[2K\u001b[1G\u001b[36m⠏\u001b[39m [5/7] app.launch"]
[14.7218332500197, "o", "\u001b[2K\u001b[1G\u001b[36m⠋\u001b[39m [5/7] app.launch"]
[14.801585417008027, "o", "\u001b[2K\u001b[1G\u001b[36m⠙\u001b[39m [5/7] app.launch"]
[14.886103000026196, "o", "\u001b[2K\u001b[1G\u001b[36m⠹\u001b[39m [5/7] app.launch"]
[14.96513899997808, "o", "\u001b[2K\u001b[1G\u001b[36m⠸\u001b[39m [5/7] app.launch"]
[15.046817208873108, "o", "\u001b[2K\u001b[1G\u001b[36m⠼\u001b[39m [5/7] app.launch"]
[15.133719417033717, "o", "\u001b[2K\u001b[1G\u001b[36m⠴\u001b[39m [5/7] app.launch"]
[15.212149958824739, "o", "\u001b[2K\u001b[1G\u001b[36m⠦\u001b[39m [5/7] app.launch"]
[15.293170124990866, "o", "\u001b[2K\u001b[1G\u001b[36m⠧\u001b[39m [5/7] app.launch"]
[15.373921166872606, "o", "\u001b[2K\u001b[1G\u001b[36m⠇\u001b[39m [5/7] app.launch"]
[15.455129917012528, "o", "\u001b[2K"]
[15.48460595891811, "o", "\u001b[1G\u001b[36m⠏\u001b[39m [5/7] app.launch"]
[15.536852458957583, "o", "\u001b[2K\u001b[1G\u001b[36m⠋\u001b[39m [5/7] app.launch"]
[15.618369749980047, "o", "\u001b[2K\u001b[1G\u001b[36m⠙\u001b[39m [5/7] app.launch"]
[15.701708458829671, "o", "\u001b[2K\u001b[1G\u001b[36m⠹\u001b[39m [5/7] app.launch"]
[15.783410208998248, "o", "\u001b[2K\u001b[1G\u001b[36m⠸\u001b[39m [5/7] app.launch"]
[15.86273004184477, "o", "\u001b[2K\u001b[1G\u001b[36m⠼\u001b[39m [5/7] app.launch"]
[15.945583750028163, "o", "\u001b[2K\u001b[1G\u001b[36m⠴\u001b[39m [5/7] app.launch"]
[16.02597966697067, "o", "\u001b[2K\u001b[1G\u001b[36m⠦\u001b[39m [5/7] app.launch"]
[16.106997292023152, "o", "\u001b[2K\u001b[1G\u001b[36m⠧\u001b[39m [5/7] app.launch"]
[16.18756379187107, "o", "\u001b[2K\u001b[1G\u001b[36m⠇\u001b[39m [5/7] app.launch"]
[16.26842249999754, "o", "\u001b[2K\u001b[1G"]
[16.288151334039867, "o", "\u001b[36m⠏\u001b[39m [5/7] app.launch"]
[16.351446041837335, "o", "\u001b[2K\u001b[1G\u001b[36m⠋\u001b[39m [5/7] app.launch"]
[16.431490374961868, "o", "\u001b[2K\u001b[1G\u001b[36m⠙\u001b[39m [5/7] app.launch"]
[16.513916791882366, "o", "\u001b[2K\u001b[1G\u001b[36m⠹\u001b[39m [5/7] app.launch"]
[16.596952041843906, "o", "\u001b[2K\u001b[1G\u001b[36m⠸\u001b[39m [5/7] app.launch"]
[16.679810999892652, "o", "\u001b[2K\u001b[1G\u001b[36m⠼\u001b[39m [5/7] app.launch"]
[16.7592509589158, "o", "\u001b[2K\u001b[1G\u001b[36m⠴\u001b[39m [5/7] app.launch"]
[16.840195999946445, "o", "\u001b[2K\u001b[1G\u001b[36m⠦\u001b[39m [5/7] app.launch"]
[16.924074250040576, "o", "\u001b[2K\u001b[1G\u001b[36m⠧\u001b[39m [5/7] app.launch"]
[17.003622124902904, "o", "\u001b[2K\u001b[1G\u001b[36m⠇\u001b[39m [5/7] app.launch"]
[17.083756458945572, "o", "\u001b[2K\u001b[1G\u001b[36m⠏\u001b[39m [5/7] app.launch"]
[17.16398183396086, "o", "\u001b[2K\u001b[1G\u001b[36m⠋\u001b[39m [5/7] app.launch"]
[17.249549666885287, "o", "\u001b[2K\u001b[1G\u001b[36m⠙\u001b[39m [5/7] app.launch"]
[17.330221708863974, "o", "\u001b[2K\u001b[1G\u001b[36m⠹\u001b[39m [5/7] app.launch"]
[17.411569374846295, "o", "\u001b[2K\u001b[1G\u001b[36m⠸\u001b[39m [5/7] app.launch"]
[17.493097458966076, "o", "\u001b[2K\u001b[1G\u001b[36m⠼\u001b[39m [5/7] app.launch"]
[17.573670709040016, "o", "\u001b[2K\u001b[1G\u001b[36m⠴\u001b[39m [5/7] app.launch"]
[17.653915375005454, "o", "\u001b[2K\u001b[1G\u001b[36m⠦\u001b[39m [5/7] app.launch"]
[17.73624649993144, "o", "\u001b[2K\u001b[1G\u001b[36m⠧\u001b[39m [5/7] app.launch"]
[17.821237416937947, "o", "\u001b[2K\u001b[1G\u001b[36m⠇\u001b[39m [5/7] app.launch"]
[17.904998041922227, "o", "\u001b[2K\u001b[1G\u001b[36m⠏\u001b[39m [5/7] app.launch"]
[17.982112291967496, "o", "\u001b[2K\u001b[1G\u001b[36m⠋\u001b[39m [5/7] app.launch"]
[18.063618124928325, "o", "\u001b[2K\u001b[1G\u001b[36m⠙\u001b[39m [5/7] app.launch"]
[18.147133250022307, "o", "\u001b[2K\u001b[1G\u001b[36m⠹\u001b[39m [5/7] app.launch"]
[18.22886733384803, "o", "\u001b[2K\u001b[1G\u001b[36m⠸\u001b[39m [5/7] app.launch"]
[18.310311584034935, "o", "\u001b[2K\u001b[1G\u001b[36m⠼\u001b[39m [5/7] app.launch"]
[18.390233208891004, "o", "\u001b[2K\u001b[1G\u001b[36m⠴\u001b[39m [5/7] app.launch"]
[18.470859499881044, "o", "\u001b[2K"]
[18.488847916945815, "o", "\u001b[1G\u001b[36m⠦\u001b[39m [5/7] app.launch"]
[18.552624417003244, "o", "\u001b[2K\u001b[1G\u001b[36m⠧\u001b[39m [5/7] app.launch"]
[18.63566791685298, "o", "\u001b[2K\u001b[1G\u001b[36m⠇\u001b[39m [5/7] app.launch"]
[18.715130124939606, "o", "\u001b[2K\u001b[1G\u001b[36m⠏\u001b[39m [5/7] app.launch"]
[18.796341166831553, "o", "\u001b[2K\u001b[1G\u001b[36m⠋\u001b[39m [5/7] app.launch"]
[18.877436874900013, "o", "\u001b[2K\u001b[1G\u001b[36m⠙\u001b[39m [5/7] app.launch"]
[18.960103541845456, "o", "\u001b[2K\u001b[1G\u001b[36m⠹\u001b[39m [5/7] app.launch"]
[19.0397401668597, "o", "\u001b[2K\u001b[1G\u001b[36m⠸\u001b[39m [5/7] app.launch"]
[19.094444334041327, "o", "\u001b[2K\u001b[1G\u001b[?25h"]
[19.112441333942115, "o", "\u001b[32m✔\u001b[39m [5/7] app.launch \u001b[2m10s\u001b[22m\r\n\u001b[?25l"]
[19.11301204189658, "o", "\u001b[36m⠋\u001b[39m [6/7] screenshot.capture"]
[19.193671667017043, "o", "\u001b[2K\u001b[1G\u001b[36m⠙\u001b[39m [6/7] screenshot.capture"]
[19.274385625030845, "o", "\u001b[2K\u001b[1G"]
[19.292101874947548, "o", "\u001b[36m⠹\u001b[39m [6/7] screenshot.capture"]
[19.355722583830357, "o", "\u001b[2K\u001b[1G\u001b[36m⠸\u001b[39m [6/7] screenshot.capture"]
[19.425292499829084, "o", "\u001b[2K\u001b[1G\u001b[?25h"]
[19.42563349986449, "o", "\u001b[32m✔\u001b[39m [6/7] screenshot.capture \u001b[2m<1s\u001b[22m\r\n Saved: /private/tmp/eas-control-context-20261017/verified-in-app.png\r\n"]
[19.425654374994338, "o", "\u001b[?25l"]
[19.426498875021935, "o", "\u001b[36m⠋\u001b[39m [7/7] appearance.set"]
[19.507328958949074, "o", "\u001b[2K\u001b[1G\u001b[36m⠙\u001b[39m [7/7] appearance.set"]
[19.588522125035524, "o", "\u001b[2K\u001b[1G\u001b[36m⠹\u001b[39m [7/7] appearance.set"]
[19.66303824982606, "o", "\u001b[2K\u001b[1G\u001b[?25h"]
[19.663655458949506, "o", "\u001b[32m✔\u001b[39m [7/7] appearance.set \u001b[2m<1s\u001b[22m\r\n\r\n\u001b[32m\u001b[32m✔\u001b[39m\u001b[32m\u001b[39m Import complete · 7 actions · 12s\r\n"]
[19.663874958874658, "o", "\u001b[?25h"]
BIN
View File
Binary file not shown.
BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 92 KiB

+340
View File
@@ -0,0 +1,340 @@
{"version": 2, "width": 116, "height": 42, "title": "eas simulator:control \u00b7 Interactive controls \u00b7 Tart iOS Simulator", "env": {"TERM": "xterm-256color"}}
[0.9949184998404235, "o", "\u001b[?25l"]
[0.999221249949187, "o", "\u001b[36m⠋\u001b[39m Connecting to simulator session tart-qa"]
[1.0777833338361233, "o", "\u001b[2K"]
[1.0805074588861316, "o", "\u001b[1G\u001b[36m⠙\u001b[39m Connecting to simulator session tart-qa"]
[1.1594961248338223, "o", "\u001b[2K"]
[1.1622414169833064, "o", "\u001b[1G\u001b[36m⠹\u001b[39m Connecting to simulator session tart-qa"]
[1.2395955419633538, "o", "\u001b[2K\u001b[1G\u001b[36m⠸\u001b[39m Connecting to simulator session tart-qa"]
[1.3192388338502496, "o", "\u001b[2K"]
[1.320422959048301, "o", "\u001b[1G\u001b[36m⠼\u001b[39m Connecting to simulator session tart-qa"]
[1.4008017498999834, "o", "\u001b[2K\u001b[1G\u001b[36m⠴\u001b[39m Connecting to simulator session tart-qa"]
[1.4813550838734955, "o", "\u001b[2K\u001b[1G"]
[1.485504416981712, "o", "\u001b[36m⠦\u001b[39m Connecting to simulator session tart-qa"]
[1.5633219999726862, "o", "\u001b[2K\u001b[1G\u001b[36m⠧\u001b[39m Connecting to simulator session tart-qa"]
[1.6429577919188887, "o", "\u001b[2K\u001b[1G\u001b[36m⠇\u001b[39m Connecting to simulator session tart-qa"]
[1.7246339588891715, "o", "\u001b[2K"]
[1.7272536668460816, "o", "\u001b[1G"]
[1.7284052919130772, "o", "\u001b[36m⠏\u001b[39m Connecting to simulator session tart-qa"]
[1.8048345418646932, "o", "\u001b[2K\u001b[1G"]
[1.8074167920276523, "o", "\u001b[36m⠋\u001b[39m Connecting to simulator session tart-qa"]
[1.8859623749740422, "o", "\u001b[2K\u001b[1G"]
[1.888438749825582, "o", "\u001b[36m⠙\u001b[39m Connecting to simulator session tart-qa"]
[1.9672075419221073, "o", "\u001b[2K\u001b[1G\u001b[36m⠹\u001b[39m Connecting to simulator session tart-qa"]
[2.0476795418653637, "o", "\u001b[2K\u001b[1G"]
[2.050236125010997, "o", "\u001b[36m⠸\u001b[39m Connecting to simulator session tart-qa"]
[2.128850542008877, "o", "\u001b[2K\u001b[1G"]
[2.131375083932653, "o", "\u001b[36m⠼\u001b[39m Connecting to simulator session tart-qa"]
[2.2095625000074506, "o", "\u001b[2K\u001b[1G\u001b[36m⠴\u001b[39m Connecting to simulator session tart-qa"]
[2.289741375017911, "o", "\u001b[2K\u001b[1G\u001b[36m⠦\u001b[39m Connecting to simulator session tart-qa"]
[2.3707614589948207, "o", "\u001b[2K\u001b[1G\u001b[36m⠧\u001b[39m Connecting to simulator session tart-qa"]
[2.4524517918471247, "o", "\u001b[2K\u001b[1G\u001b[36m⠇\u001b[39m Connecting to simulator session tart-qa"]
[2.5330557089764625, "o", "\u001b[2K\u001b[1G"]
[2.5364182088524103, "o", "\u001b[36m⠏\u001b[39m Connecting to simulator session tart-qa"]
[2.6145420419052243, "o", "\u001b[2K\u001b[1G\u001b[36m⠋\u001b[39m Connecting to simulator session tart-qa"]
[2.619030667003244, "o", "\u001b[2K\u001b[1G"]
[2.619997458998114, "o", "\u001b[?25h"]
[2.6212660840246826, "o", "\u001b[32m✔\u001b[39m Connected to simulator session tart-qa\r\n\r\n\u001b[1mSimulator controls\u001b[22m\r\n\u001b[2mSession \u001b[22m Owned Tart CLI QA\r\n\u001b[2mProject \u001b[22m @local-qa/local-qa\r\n\u001b[2mDevice \u001b[22m eas-control-context-20261017 · iOS 26.4\r\n\u001b[2mState \u001b[22m in progress\r\n\u001b[2mSession ID\u001b[22m tart-qa\r\n\u001b[2mDevice ID \u001b[22m 9D46A327-1A3E-442C-8481-68A96914CF83\r\n\r\n"]
[2.6223270839545876, "o", "\u001b[90m62 actions · Type to search · ↑/↓ to navigate · Enter to select · Ctrl-C to exit\u001b[39m\r\n"]
[2.6237614168785512, "o", "\u001b[?25l\u001b[2K\u001b[1G\u001b[36m?\u001b[39m \u001b[1mChoose a simulator action\u001b[22m \u001b[90m…\u001b[39m \r\n\u001b[90mno matches found\u001b[39m\u001b[2K\u001b[1A\u001b[2K\u001b[G\u001b[2K\u001b[1G\u001b[36m?\u001b[39m \u001b[1mChoose a simulator action\u001b[22m \u001b[90m›\u001b[39m \r\n\u001b[36m❯\u001b[39m \u001b[36m\u001b[4mExit\u001b[39m\u001b[24m\u001b[90m - Close simulator controls\u001b[39m\r\n App › Container\u001b[90m - app.container\u001b[39m\r\n App › File › List\u001b[90m - app.file.list\u001b[39m\r\n App › File › Read\u001b[90m - app.file.read\u001b[39m\r\n App › File › Remove\u001b[90m - app.file.remove\u001b[39m\r\n App › File › Upload\u001b[90m - app.file.upload\u001b[39m\r\n App › Info plist\u001b[90m - app.infoPlist\u001b[39m\r\n ↓ App › Install\u001b[90m - app.install\u001b[39m"]
[3.663351541850716, "i", "hardware keyboard set"]
[3.6640302918385714, "o", "\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[G"]
[3.6710308748297393, "o", "\u001b[2K\u001b[1G\u001b[36m?\u001b[39m \u001b[1mChoose a simulator action\u001b[22m \u001b[90m…\u001b[39m h\r\n\u001b[36m❯\u001b[39m \u001b[36m\u001b[4mExit\u001b[39m\u001b[24m\u001b[90m - Close simulator controls\u001b[39m\r\n App › Container\u001b[90m - app.container\u001b[39m\r\n App › File › List\u001b[90m - app.file.list\u001b[39m\r\n App › File › Read\u001b[90m - app.file.read\u001b[39m\r\n App › File › Remove\u001b[90m - app.file.remove\u001b[39m\r\n App › File › Upload\u001b[90m - app.file.upload\u001b[39m\r\n App › Info plist\u001b[90m - app.infoPlist\u001b[39m\r\n ↓ App › Install\u001b[90m - app.install\u001b[39m\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[G\u001b[2K\u001b[1G\u001b[36m?\u001b[39m \u001b[1mChoose a simulator action\u001b[22m \u001b[90m…\u001b[39m ha\r\n\u001b[36m❯\u001b[39m \u001b[36m\u001b[4mExit\u001b[39m\u001b[24m\u001b[90m - Close simulator controls\u001b[39m\r\n App › Container\u001b[90m - app.container\u001b[39m\r\n App › File › List\u001b[90m - app.file.list\u001b[39m\r\n App › File › Read\u001b[90m - app.file.read\u001b[39m\r\n App › File › Remove\u001b[90m - app.file.remove\u001b[39m\r\n App › File › Upload\u001b[90m - app.file.upload\u001b[39m\r\n App › Info pl"]
[3.6715248748660088, "o", "ist\u001b[90m - app.infoPlist\u001b[39m\r\n ↓ App › Install\u001b[90m - app.install\u001b[39m\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[G\u001b[2K\u001b[1G\u001b[36m?\u001b[39m \u001b[1mChoose a simulator action\u001b[22m \u001b[90m…\u001b[39m har\r\n\u001b[36m❯\u001b[39m \u001b[36m\u001b[4mExit\u001b[39m\u001b[24m\u001b[90m - Close simulator controls\u001b[39m\r\n App › Container\u001b[90m - app.container\u001b[39m\r\n App › File › List\u001b[90m - app.file.list\u001b[39m\r\n App › File › Read\u001b[90m - app.file.read\u001b[39m\r\n App › File › Remove\u001b[90m - app.file.remove\u001b[39m\r\n App › File › Upload\u001b[90m - app.file.upload\u001b[39m\r\n App › Info plist\u001b[90m - app.infoPlist\u001b[39m\r\n ↓ App › Install\u001b[90m - app.install\u001b[39m\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[G\u001b[2K\u001b[1G\u001b[36m?\u001b[39m \u001b[1mChoose a simulator action\u001b[22m \u001b[90m…\u001b[39m hard\r\n\u001b[36m❯\u001b[39m \u001b[36m\u001b[4mExit\u001b[39m\u001b[24m\u001b[90m - Close simulator controls\u001b[39m\r\n App › Container\u001b[90m - app.container\u001b[39m\r\n App › File › List\u001b[90m - app.file.list\u001b[39m\r\n App › File › Read\u001b[90m - ap"]
[3.6721878750249743, "o", "p.file.read\u001b[39m\r\n App › File › Remove\u001b[90m - app.file.remove\u001b[39m\r\n App › File › Upload\u001b[90m - app.file.upload\u001b[39m\r\n App › Info plist\u001b[90m - app.infoPlist\u001b[39m\r\n ↓ App › Install\u001b[90m - app.install\u001b[39m\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[G\u001b[2K\u001b[1G\u001b[36m?\u001b[39m \u001b[1mChoose a simulator action\u001b[22m \u001b[90m…\u001b[39m hardw\r\n\u001b[36m❯\u001b[39m \u001b[36m\u001b[4mExit\u001b[39m\u001b[24m\u001b[90m - Close simulator controls\u001b[39m\r\n App › Container\u001b[90m - app.container\u001b[39m\r\n App › File › List\u001b[90m - app.file.list\u001b[39m\r\n App › File › Read\u001b[90m - app.file.read\u001b[39m\r\n App › File › Remove\u001b[90m - app.file.remove\u001b[39m\r\n App › File › Upload\u001b[90m - app.file.upload\u001b[39m\r\n App › Info plist\u001b[90m - app.infoPlist\u001b[39m\r\n ↓ App › Install\u001b[90m - app.install\u001b[39m\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[G\u001b[2K\u001b[1G\u001b[36m?\u001b[39m \u001b[1mChoose a simulator action\u001b[22m \u001b[90m…\u001b[39m hardwa\r\n\u001b[36m❯\u001b[39m \u001b[36m\u001b[4mExit\u001b[39m\u001b[24m\u001b[90m - Close simulat"]
[3.6726767090149224, "o", "or controls\u001b[39m\r\n App › Container\u001b[90m - app.container\u001b[39m\r\n App › File › List\u001b[90m - app.file.list\u001b[39m\r\n App › File › Read\u001b[90m - app.file.read\u001b[39m\r\n App › File › Remove\u001b[90m - app.file.remove\u001b[39m\r\n App › File › Upload\u001b[90m - app.file.upload\u001b[39m\r\n App › Info plist\u001b[90m - app.infoPlist\u001b[39m\r\n ↓ App › Install\u001b[90m - app.install\u001b[39m\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[G\u001b[2K\u001b[1G\u001b[36m?\u001b[39m \u001b[1mChoose a simulator action\u001b[22m \u001b[90m…\u001b[39m hardwar\r\n\u001b[36m❯\u001b[39m \u001b[36m\u001b[4mExit\u001b[39m\u001b[24m\u001b[90m - Close simulator controls\u001b[39m\r\n App › Container\u001b[90m - app.container\u001b[39m\r\n App › File › List\u001b[90m - app.file.list\u001b[39m\r\n App › File › Read\u001b[90m - app.file.read\u001b[39m\r\n App › File › Remove\u001b[90m - app.file.remove\u001b[39m\r\n App › File › Upload\u001b[90m - app.file.upload\u001b[39m\r\n App › Info plist\u001b[90m - app.infoPlist\u001b[39m\r\n ↓ App › Install\u001b[90m - app.install\u001b[39m\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2"]
[3.673131708987057, "o", "K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[G\u001b[2K\u001b[1G\u001b[36m?\u001b[39m \u001b[1mChoose a simulator action\u001b[22m \u001b[90m…\u001b[39m hardware\r\n\u001b[36m❯\u001b[39m \u001b[36m\u001b[4mExit\u001b[39m\u001b[24m\u001b[90m - Close simulator controls\u001b[39m\r\n App › Container\u001b[90m - app.container\u001b[39m\r\n App › File › List\u001b[90m - app.file.list\u001b[39m\r\n App › File › Read\u001b[90m - app.file.read\u001b[39m\r\n App › File › Remove\u001b[90m - app.file.remove\u001b[39m\r\n App › File › Upload\u001b[90m - app.file.upload\u001b[39m\r\n App › Info plist\u001b[90m - app.infoPlist\u001b[39m\r\n ↓ App › Install\u001b[90m - app.install\u001b[39m\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[G\u001b[2K\u001b[1G\u001b[36m?\u001b[39m \u001b[1mChoose a simulator action\u001b[22m \u001b[90m…\u001b[39m hardware \r\n\u001b[36m❯\u001b[39m \u001b[36m\u001b[4mExit\u001b[39m\u001b[24m\u001b[90m - Close simulator controls\u001b[39m\r\n App › Container\u001b[90m - app.container\u001b[39m\r\n App › File › List\u001b[90m - app.file.list\u001b[39m\r\n App › File › Read\u001b[90m - app.file.read\u001b[39m\r\n App › File › Remove\u001b[90m - app.file.remove\u001b[39m\r\n App › File › Upload\u001b[90m - app.fil"]
[3.6734852918889374, "o", "e.upload\u001b[39m\r\n App › Info plist\u001b[90m - app.infoPlist\u001b[39m\r\n ↓ App › Install\u001b[90m - app.install\u001b[39m\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[G\u001b[2K\u001b[1G\u001b[36m?\u001b[39m \u001b[1mChoose a simulator action\u001b[22m \u001b[90m…\u001b[39m hardware k\r\n\u001b[36m❯\u001b[39m \u001b[36m\u001b[4mExit\u001b[39m\u001b[24m\u001b[90m - Close simulator controls\u001b[39m\r\n App › Container\u001b[90m - app.container\u001b[39m\r\n App › File › List\u001b[90m - app.file.list\u001b[39m\r\n App › File › Read\u001b[90m - app.file.read\u001b[39m\r\n App › File › Remove\u001b[90m - app.file.remove\u001b[39m\r\n App › File › Upload\u001b[90m - app.file.upload\u001b[39m\r\n App › Info plist\u001b[90m - app.infoPlist\u001b[39m\r\n ↓ App › Install\u001b[90m - app.install\u001b[39m\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[G"]
[3.6739264589268714, "o", "\u001b[2K\u001b[1G\u001b[36m?\u001b[39m \u001b[1mChoose a simulator action\u001b[22m \u001b[90m…\u001b[39m hardware ke\r\n\u001b[36m❯\u001b[39m \u001b[36m\u001b[4mExit\u001b[39m\u001b[24m\u001b[90m - Close simulator controls\u001b[39m\r\n App › Container\u001b[90m - app.container\u001b[39m\r\n App › File › List\u001b[90m - app.file.list\u001b[39m\r\n App › File › Read\u001b[90m - app.file.read\u001b[39m\r\n App › File › Remove\u001b[90m - app.file.remove\u001b[39m\r\n App › File › Upload\u001b[90m - app.file.upload\u001b[39m\r\n App › Info plist\u001b[90m - app.infoPlist\u001b[39m\r\n ↓ App › Install\u001b[90m - app.install\u001b[39m\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[G\u001b[2K\u001b[1G\u001b[36m?\u001b[39m \u001b[1mChoose a simulator action\u001b[22m \u001b[90m…\u001b[39m hardware key\r\n\u001b[36m❯\u001b[39m \u001b[36m\u001b[4mExit\u001b[39m\u001b[24m\u001b[90m - Close simulator controls\u001b[39m\r\n App › Container\u001b[90m - app.container\u001b[39m\r\n App › File › List\u001b[90m - app.file.list\u001b[39m\r\n App › File › Read\u001b[90m - app.file.read\u001b[39m\r\n App › File › Remove\u001b[90m - app.file.remove\u001b[39m\r\n App › File › Upload\u001b[90m - app.file.upload\u001b[39m\r"]
[3.6743680420331657, "o", "\r\n App › Info plist\u001b[90m - app.infoPlist\u001b[39m\r\n ↓ App › Install\u001b[90m - app.install\u001b[39m\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[G\u001b[2K\u001b[1G\u001b[36m?\u001b[39m \u001b[1mChoose a simulator action\u001b[22m \u001b[90m…\u001b[39m hardware keyb\r\n\u001b[36m❯\u001b[39m \u001b[36m\u001b[4mExit\u001b[39m\u001b[24m\u001b[90m - Close simulator controls\u001b[39m\r\n App › Container\u001b[90m - app.container\u001b[39m\r\n App › File › List\u001b[90m - app.file.list\u001b[39m\r\n App › File › Read\u001b[90m - app.file.read\u001b[39m\r\n App › File › Remove\u001b[90m - app.file.remove\u001b[39m\r\n App › File › Upload\u001b[90m - app.file.upload\u001b[39m\r\n App › Info plist\u001b[90m - app.infoPlist\u001b[39m\r\n ↓ App › Install\u001b[90m - app.install\u001b[39m\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[G\u001b[2K\u001b[1G\u001b[36m?\u001b[39m \u001b[1mChoose a simulator action\u001b[22m \u001b[90m…\u001b[39m hardware keybo\r\n\u001b[36m❯\u001b[39m \u001b[36m\u001b[4mExit\u001b[39m\u001b[24m\u001b[90m - Close simulator controls\u001b[39m\r\n App › Container\u001b[90m - app.container\u001b[39m\r\n App › File › List\u001b[90m - app.file.list\u001b"]
[3.6748090418986976, "o", "[39m\r\n App › File › Read\u001b[90m - app.file.read\u001b[39m\r\n App › File › Remove\u001b[90m - app.file.remove\u001b[39m\r\n App › File › Upload\u001b[90m - app.file.upload\u001b[39m\r\n App › Info plist\u001b[90m - app.infoPlist\u001b[39m\r\n ↓ App › Install\u001b[90m - app.install\u001b[39m\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[G\u001b[2K\u001b[1G\u001b[36m?\u001b[39m \u001b[1mChoose a simulator action\u001b[22m \u001b[90m…\u001b[39m hardware keyboa\r\n\u001b[36m❯\u001b[39m \u001b[36m\u001b[4mExit\u001b[39m\u001b[24m\u001b[90m - Close simulator controls\u001b[39m\r\n App › Container\u001b[90m - app.container\u001b[39m\r\n App › File › List\u001b[90m - app.file.list\u001b[39m\r\n App › File › Read\u001b[90m - app.file.read\u001b[39m\r\n App › File › Remove\u001b[90m - app.file.remove\u001b[39m\r\n App › File › Upload\u001b[90m - app.file.upload\u001b[39m\r\n App › Info plist\u001b[90m - app.infoPlist\u001b[39m\r\n ↓ App › Install\u001b[90m - app.install\u001b[39m\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[G\u001b[2K\u001b[1G\u001b[36m?\u001b[39m \u001b[1mChoose a simulator action\u001b[22m \u001b[90m…\u001b[39m hardware keyboar\r"]
[3.6752360840328038, "o", "\r\n\u001b[36m❯\u001b[39m \u001b[36m\u001b[4mExit\u001b[39m\u001b[24m\u001b[90m - Close simulator controls\u001b[39m\r\n App › Container\u001b[90m - app.container\u001b[39m\r\n App › File › List\u001b[90m - app.file.list\u001b[39m\r\n App › File › Read\u001b[90m - app.file.read\u001b[39m\r\n App › File › Remove\u001b[90m - app.file.remove\u001b[39m\r\n App › File › Upload\u001b[90m - app.file.upload\u001b[39m\r\n App › Info plist\u001b[90m - app.infoPlist\u001b[39m\r\n ↓ App › Install\u001b[90m - app.install\u001b[39m\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[G\u001b[2K\u001b[1G\u001b[36m?\u001b[39m \u001b[1mChoose a simulator action\u001b[22m \u001b[90m…\u001b[39m hardware keyboard\r\n\u001b[36m❯\u001b[39m \u001b[36m\u001b[4mExit\u001b[39m\u001b[24m\u001b[90m - Close simulator controls\u001b[39m\r\n App › Container\u001b[90m - app.container\u001b[39m\r\n App › File › List\u001b[90m - app.file.list\u001b[39m\r\n App › File › Read\u001b[90m - app.file.read\u001b[39m\r\n App › File › Remove\u001b[90m - app.file.remove\u001b[39m\r\n App › File › Upload\u001b[90m - app.file.upload\u001b[39m\r\n App › Info plist\u001b[90m - app.infoPlist\u001b[39m\r\n ↓ App › Install\u001b[9"]
[3.675679791951552, "o", "0m - app.install\u001b[39m\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[G\u001b[2K\u001b[1G\u001b[36m?\u001b[39m \u001b[1mChoose a simulator action\u001b[22m \u001b[90m…\u001b[39m hardware keyboard \r\n\u001b[36m❯\u001b[39m \u001b[36m\u001b[4mExit\u001b[39m\u001b[24m\u001b[90m - Close simulator controls\u001b[39m\r\n App › Container\u001b[90m - app.container\u001b[39m\r\n App › File › List\u001b[90m - app.file.list\u001b[39m\r\n App › File › Read\u001b[90m - app.file.read\u001b[39m\r\n App › File › Remove\u001b[90m - app.file.remove\u001b[39m\r\n App › File › Upload\u001b[90m - app.file.upload\u001b[39m\r\n App › Info plist\u001b[90m - app.infoPlist\u001b[39m\r\n ↓ App › Install\u001b[90m - app.install\u001b[39m\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[G\u001b[2K\u001b[1G\u001b[36m?\u001b[39m \u001b[1mChoose a simulator action\u001b[22m \u001b[90m…\u001b[39m hardware keyboard s\r\n\u001b[36m❯\u001b[39m \u001b[36m\u001b[4mExit\u001b[39m\u001b[24m\u001b[90m - Close simulator controls\u001b[39m\r\n App › Container\u001b[90m - app.container\u001b[39m\r\n App › File › List\u001b[90m - app.file.list\u001b[39m\r\n App › File › Read\u001b[90m - app.file.read\u001b[39m\r\n App"]
[3.6765553338918835, "o", " › File › Remove\u001b[90m - app.file.remove\u001b[39m\r\n App › File › Upload\u001b[90m - app.file.upload\u001b[39m\r\n App › Info plist\u001b[90m - app.infoPlist\u001b[39m\r\n ↓ App › Install\u001b[90m - app.install\u001b[39m\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[G\u001b[2K\u001b[1G\u001b[36m?\u001b[39m \u001b[1mChoose a simulator action\u001b[22m \u001b[90m…\u001b[39m hardware keyboard se\r\n\u001b[36m❯\u001b[39m \u001b[36m\u001b[4mExit\u001b[39m\u001b[24m\u001b[90m - Close simulator controls\u001b[39m\r\n App › Container\u001b[90m - app.container\u001b[39m\r\n App › File › List\u001b[90m - app.file.list\u001b[39m\r\n App › File › Read\u001b[90m - app.file.read\u001b[39m\r\n App › File › Remove\u001b[90m - app.file.remove\u001b[39m\r\n App › File › Upload\u001b[90m - app.file.upload\u001b[39m\r\n App › Info plist\u001b[90m - app.infoPlist\u001b[39m\r\n ↓ App › Install\u001b[90m - app.install\u001b[39m\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[G\u001b[2K\u001b[1G\u001b[36m?\u001b[39m \u001b[1mChoose a simulator action\u001b[22m \u001b[90m…\u001b[39m hardware keyboard set\r\n\u001b[36m❯\u001b[39m \u001b[36m\u001b[4mExit\u001b[39m\u001b[24m\u001b[90m - Close si"]
[3.677136000012979, "o", "mulator controls\u001b[39m\r\n App › Container\u001b[90m - app.container\u001b[39m\r\n App › File › List\u001b[90m - app.file.list\u001b[39m\r\n App › File › Read\u001b[90m - app.file.read\u001b[39m\r\n App › File › Remove\u001b[90m - app.file.remove\u001b[39m\r\n App › File › Upload\u001b[90m - app.file.upload\u001b[39m\r\n App › Info plist\u001b[90m - app.infoPlist\u001b[39m\r\n ↓ App › Install\u001b[90m - app.install\u001b[39m\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[G\u001b[2K\u001b[1G\u001b[36m?\u001b[39m \u001b[1mChoose a simulator action\u001b[22m \u001b[90m›\u001b[39m hardware keyboard set\r\n\u001b[36m❯\u001b[39m \u001b[36m\u001b[4mHardware keyboard › Set\u001b[39m\u001b[24m\u001b[90m - hardwareKeyboard.set\u001b[39m"]
[4.417503458913416, "i", "\r"]
[4.418454791884869, "o", "\u001b[2K\u001b[1A\u001b[2K\u001b[G\u001b[2K\u001b[1G\u001b[32m✔\u001b[39m \u001b[1mChoose a simulator action\u001b[22m \u001b[90m›\u001b[39m Hardware keyboard › Set\r\n\u001b[?25h"]
[4.430651125032455, "o", "\r\n\u001b[1mhardwareKeyboard.set\u001b[22m\r\n\u001b[90mConnect or disconnect the hardware keyboard.\u001b[39m\r\n"]
[4.431940333917737, "o", "\u001b[?25l\u001b[36m?\u001b[39m \u001b[1mvalue\u001b[22m \u001b[90m›\u001b[39m \u001b[90m- Use arrow-keys. Return to submit.\u001b[39m\r\n\u001b[36m❯\u001b[39m \u001b[36m\u001b[4mon\u001b[39m\u001b[24m\u001b[90m\u001b[39m\r\n off\u001b[90m\u001b[39m\r\n\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[G\u001b[36m?\u001b[39m \u001b[1mvalue\u001b[22m \u001b[90m›\u001b[39m \u001b[90m- Use arrow-keys. Return to submit.\u001b[39m\r\n\u001b[36m❯\u001b[39m \u001b[36m\u001b[4mon\u001b[39m\u001b[24m\u001b[90m\u001b[39m\r\n off\u001b[90m\u001b[39m\r\n"]
[4.937215500045568, "i", "\u001b[B"]
[4.937736874911934, "o", "\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[G"]
[4.9490849589928985, "o", "\u001b[36m?\u001b[39m \u001b[1mvalue\u001b[22m \u001b[90m›\u001b[39m \u001b[90m- Use arrow-keys. Return to submit.\u001b[39m\r\n on\u001b[90m\u001b[39m\r\n\u001b[36m❯\u001b[39m \u001b[36m\u001b[4moff\u001b[39m\u001b[24m\u001b[90m\u001b[39m\r\n"]
[5.141326208831742, "i", "\r"]
[5.141661624889821, "o", "\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[G"]
[5.152061458909884, "o", "\u001b[32m✔\u001b[39m \u001b[1mvalue\u001b[22m \u001b[90m›\u001b[39m off\r\n\u001b[?25h"]
[5.153068374842405, "o", "\u001b[?25l"]
[5.15408283402212, "o", "\u001b[36m⠋\u001b[39m Running hardwareKeyboard.set"]
[5.234223125036806, "o", "\u001b[2K\u001b[1G\u001b[36m⠙\u001b[39m Running hardwareKeyboard.set"]
[5.315617833985016, "o", "\u001b[2K\u001b[1G\u001b[36m⠹\u001b[39m Running hardwareKeyboard.set"]
[5.39660175004974, "o", "\u001b[2K\u001b[1G\u001b[36m⠸\u001b[39m Running hardwareKeyboard.set"]
[5.477243333822116, "o", "\u001b[2K\u001b[1G\u001b[36m⠼\u001b[39m Running hardwareKeyboard.set"]
[5.558316791895777, "o", "\u001b[2K\u001b[1G\u001b[36m⠴\u001b[39m Running hardwareKeyboard.set"]
[5.643088249955326, "o", "\u001b[2K\u001b[1G\u001b[36m⠦\u001b[39m Running hardwareKeyboard.set"]
[5.719202667009085, "o", "\u001b[2K\u001b[1G\u001b[36m⠧\u001b[39m Running hardwareKeyboard.set"]
[5.800747249973938, "o", "\u001b[2K\u001b[1G\u001b[36m⠇\u001b[39m Running hardwareKeyboard.set"]
[5.881897208979353, "o", "\u001b[2K\u001b[1G\u001b[36m⠏\u001b[39m Running hardwareKeyboard.set"]
[5.962820874992758, "o", "\u001b[2K\u001b[1G\u001b[36m⠋\u001b[39m Running hardwareKeyboard.set"]
[6.043177708983421, "o", "\u001b[2K\u001b[1G\u001b[36m⠙\u001b[39m Running hardwareKeyboard.set"]
[6.124171667033806, "o", "\u001b[2K\u001b[1G\u001b[36m⠹\u001b[39m Running hardwareKeyboard.set"]
[6.204708749894053, "o", "\u001b[2K\u001b[1G\u001b[36m⠸\u001b[39m Running hardwareKeyboard.set"]
[6.285866333870217, "o", "\u001b[2K\u001b[1G\u001b[36m⠼\u001b[39m Running hardwareKeyboard.set"]
[6.366973249940202, "o", "\u001b[2K\u001b[1G\u001b[36m⠴\u001b[39m Running hardwareKeyboard.set"]
[6.447533041937277, "o", "\u001b[2K\u001b[1G\u001b[36m⠦\u001b[39m Running hardwareKeyboard.set"]
[6.5278557918500155, "o", "\u001b[2K\u001b[1G\u001b[36m⠧\u001b[39m Running hardwareKeyboard.set"]
[6.609075124841183, "o", "\u001b[2K\u001b[1G\u001b[36m⠇\u001b[39m Running hardwareKeyboard.set"]
[6.690196666866541, "o", "\u001b[2K\u001b[1G\u001b[36m⠏\u001b[39m Running hardwareKeyboard.set"]
[6.770408999873325, "o", "\u001b[2K\u001b[1G\u001b[36m⠋\u001b[39m Running hardwareKeyboard.set"]
[6.863829708890989, "o", "\u001b[2K\u001b[1G\u001b[36m⠙\u001b[39m Running hardwareKeyboard.set"]
[6.932893624994904, "o", "\u001b[2K\u001b[1G\u001b[36m⠹\u001b[39m Running hardwareKeyboard.set"]
[7.0137738338671625, "o", "\u001b[2K\u001b[1G\u001b[36m⠸\u001b[39m Running hardwareKeyboard.set"]
[7.094446124974638, "o", "\u001b[2K\u001b[1G\u001b[36m⠼\u001b[39m Running hardwareKeyboard.set"]
[7.1755909589119256, "o", "\u001b[2K\u001b[1G\u001b[36m⠴\u001b[39m Running hardwareKeyboard.set"]
[7.25669858395122, "o", "\u001b[2K\u001b[1G\u001b[36m⠦\u001b[39m Running hardwareKeyboard.set"]
[7.336990833980963, "o", "\u001b[2K\u001b[1G\u001b[36m⠧\u001b[39m Running hardwareKeyboard.set"]
[7.417977791978046, "o", "\u001b[2K\u001b[1G\u001b[36m⠇\u001b[39m Running hardwareKeyboard.set"]
[7.499123333953321, "o", "\u001b[2K\u001b[1G\u001b[36m⠏\u001b[39m Running hardwareKeyboard.set"]
[7.580276624998078, "o", "\u001b[2K\u001b[1G\u001b[36m⠋\u001b[39m Running hardwareKeyboard.set"]
[7.661377375014126, "o", "\u001b[2K\u001b[1G\u001b[36m⠙\u001b[39m Running hardwareKeyboard.set"]
[7.74246891704388, "o", "\u001b[2K\u001b[1G"]
[7.753087041899562, "o", "\u001b[36m⠹\u001b[39m Running hardwareKeyboard.set"]
[7.823712624842301, "o", "\u001b[2K\u001b[1G\u001b[36m⠸\u001b[39m Running hardwareKeyboard.set"]
[7.905104625038803, "o", "\u001b[2K\u001b[1G\u001b[36m⠼\u001b[39m Running hardwareKeyboard.set"]
[7.9842255839612335, "o", "\u001b[2K\u001b[1G\u001b[36m⠴\u001b[39m Running hardwareKeyboard.set"]
[8.06492020888254, "o", "\u001b[2K\u001b[1G\u001b[36m⠦\u001b[39m Running hardwareKeyboard.set"]
[8.14666899992153, "o", "\u001b[2K\u001b[1G\u001b[36m⠧\u001b[39m Running hardwareKeyboard.set"]
[8.226771083893254, "o", "\u001b[2K\u001b[1G\u001b[36m⠇\u001b[39m Running hardwareKeyboard.set"]
[8.307853000005707, "o", "\u001b[2K\u001b[1G\u001b[36m⠏\u001b[39m Running hardwareKeyboard.set"]
[8.388951709028333, "o", "\u001b[2K\u001b[1G\u001b[36m⠋\u001b[39m Running hardwareKeyboard.set"]
[8.47035741689615, "o", "\u001b[2K\u001b[1G\u001b[36m⠙\u001b[39m Running hardwareKeyboard.set"]
[8.551293749827892, "o", "\u001b[2K\u001b[1G\u001b[36m⠹\u001b[39m Running hardwareKeyboard.set"]
[8.632182874949649, "o", "\u001b[2K\u001b[1G\u001b[36m⠸\u001b[39m Running hardwareKeyboard.set"]
[8.713446334004402, "o", "\u001b[2K\u001b[1G\u001b[36m⠼\u001b[39m Running hardwareKeyboard.set"]
[8.794543083989993, "o", "\u001b[2K\u001b[1G\u001b[36m⠴\u001b[39m Running hardwareKeyboard.set"]
[8.875676124822348, "o", "\u001b[2K\u001b[1G\u001b[36m⠦\u001b[39m Running hardwareKeyboard.set"]
[8.955817458918318, "o", "\u001b[2K\u001b[1G\u001b[36m⠧\u001b[39m Running hardwareKeyboard.set"]
[9.036027499940246, "o", "\u001b[2K\u001b[1G\u001b[36m⠇\u001b[39m Running hardwareKeyboard.set"]
[9.117111916886643, "o", "\u001b[2K\u001b[1G\u001b[36m⠏\u001b[39m Running hardwareKeyboard.set"]
[9.197752249892801, "o", "\u001b[2K\u001b[1G\u001b[36m⠋\u001b[39m Running hardwareKeyboard.set"]
[9.27869987487793, "o", "\u001b[2K\u001b[1G\u001b[36m⠙\u001b[39m Running hardwareKeyboard.set"]
[9.360502166906372, "o", "\u001b[2K\u001b[1G\u001b[36m⠹\u001b[39m Running hardwareKeyboard.set"]
[9.440405708970502, "o", "\u001b[2K\u001b[1G\u001b[36m⠸\u001b[39m Running hardwareKeyboard.set"]
[9.521599999861792, "o", "\u001b[2K\u001b[1G\u001b[36m⠼\u001b[39m Running hardwareKeyboard.set"]
[9.602678666822612, "o", "\u001b[2K\u001b[1G\u001b[36m⠴\u001b[39m Running hardwareKeyboard.set"]
[9.682837625034153, "o", "\u001b[2K\u001b[1G\u001b[36m⠦\u001b[39m Running hardwareKeyboard.set"]
[9.763320666970685, "o", "\u001b[2K\u001b[1G\u001b[36m⠧\u001b[39m Running hardwareKeyboard.set"]
[9.844133666949347, "o", "\u001b[2K\u001b[1G\u001b[36m⠇\u001b[39m Running hardwareKeyboard.set"]
[9.92507433402352, "o", "\u001b[2K\u001b[1G"]
[9.934899542015046, "o", "\u001b[36m⠏\u001b[39m Running hardwareKeyboard.set"]
[10.006281750043854, "o", "\u001b[2K\u001b[1G\u001b[36m⠋\u001b[39m Running hardwareKeyboard.set"]
[10.087457333924249, "o", "\u001b[2K\u001b[1G\u001b[36m⠙\u001b[39m Running hardwareKeyboard.set"]
[10.168584999861196, "o", "\u001b[2K\u001b[1G\u001b[36m⠹\u001b[39m Running hardwareKeyboard.set"]
[10.24872879195027, "o", "\u001b[2K\u001b[1G\u001b[36m⠸\u001b[39m Running hardwareKeyboard.set"]
[10.330310459015891, "o", "\u001b[2K\u001b[1G\u001b[36m⠼\u001b[39m Running hardwareKeyboard.set"]
[10.413896208861843, "o", "\u001b[2K\u001b[1G\u001b[36m⠴\u001b[39m Running hardwareKeyboard.set"]
[10.493850291939452, "o", "\u001b[2K\u001b[1G\u001b[36m⠦\u001b[39m Running hardwareKeyboard.set"]
[10.575927083846182, "o", "\u001b[2K\u001b[1G\u001b[36m⠧\u001b[39m Running hardwareKeyboard.set"]
[10.657552708871663, "o", "\u001b[2K\u001b[1G\u001b[36m⠇\u001b[39m Running hardwareKeyboard.set"]
[10.73866083403118, "o", "\u001b[2K\u001b[1G\u001b[36m⠏\u001b[39m Running hardwareKeyboard.set"]
[10.818548333831131, "o", "\u001b[2K\u001b[1G\u001b[36m⠋\u001b[39m Running hardwareKeyboard.set"]
[10.900174041977152, "o", "\u001b[2K\u001b[1G\u001b[36m⠙\u001b[39m Running hardwareKeyboard.set"]
[10.980310125043616, "o", "\u001b[2K\u001b[1G\u001b[36m⠹\u001b[39m Running hardwareKeyboard.set"]
[11.061436292016879, "o", "\u001b[2K\u001b[1G\u001b[36m⠸\u001b[39m Running hardwareKeyboard.set"]
[11.142963167047128, "o", "\u001b[2K\u001b[1G\u001b[36m⠼\u001b[39m Running hardwareKeyboard.set"]
[11.223574291914701, "o", "\u001b[2K\u001b[1G\u001b[36m⠴\u001b[39m Running hardwareKeyboard.set"]
[11.304173083975911, "o", "\u001b[2K\u001b[1G\u001b[36m⠦\u001b[39m Running hardwareKeyboard.set"]
[11.385375999845564, "o", "\u001b[2K\u001b[1G\u001b[36m⠧\u001b[39m Running hardwareKeyboard.set"]
[11.467151416931301, "o", "\u001b[2K\u001b[1G\u001b[36m⠇\u001b[39m Running hardwareKeyboard.set"]
[11.548500709002838, "o", "\u001b[2K\u001b[1G\u001b[36m⠏\u001b[39m Running hardwareKeyboard.set"]
[11.628865459002554, "o", "\u001b[2K\u001b[1G\u001b[36m⠋\u001b[39m Running hardwareKeyboard.set"]
[11.709941959008574, "o", "\u001b[2K\u001b[1G\u001b[36m⠙\u001b[39m Running hardwareKeyboard.set"]
[11.79197454196401, "o", "\u001b[2K\u001b[1G\u001b[36m⠹\u001b[39m Running hardwareKeyboard.set"]
[11.872829291969538, "o", "\u001b[2K\u001b[1G\u001b[36m⠸\u001b[39m Running hardwareKeyboard.set"]
[11.953969708876684, "o", "\u001b[2K\u001b[1G\u001b[36m⠼\u001b[39m Running hardwareKeyboard.set"]
[12.034893875010312, "o", "\u001b[2K\u001b[1G\u001b[36m⠴\u001b[39m Running hardwareKeyboard.set"]
[12.114915000041947, "o", "\u001b[2K\u001b[1G\u001b[36m⠦\u001b[39m Running hardwareKeyboard.set"]
[12.196066916920245, "o", "\u001b[2K\u001b[1G\u001b[36m⠧\u001b[39m Running hardwareKeyboard.set"]
[12.277282833820209, "o", "\u001b[2K\u001b[1G\u001b[36m⠇\u001b[39m Running hardwareKeyboard.set"]
[12.358847375027835, "o", "\u001b[2K\u001b[1G\u001b[36m⠏\u001b[39m Running hardwareKeyboard.set"]
[12.439420541981235, "o", "\u001b[2K\u001b[1G\u001b[36m⠋\u001b[39m Running hardwareKeyboard.set"]
[12.520563874859363, "o", "\u001b[2K\u001b[1G\u001b[36m⠙\u001b[39m Running hardwareKeyboard.set"]
[12.601696541998535, "o", "\u001b[2K\u001b[1G\u001b[36m⠹\u001b[39m Running hardwareKeyboard.set"]
[12.68290274986066, "o", "\u001b[2K\u001b[1G\u001b[36m⠸\u001b[39m Running hardwareKeyboard.set"]
[12.763091083848849, "o", "\u001b[2K\u001b[1G"]
[12.77269387501292, "o", "\u001b[36m⠼\u001b[39m Running hardwareKeyboard.set"]
[12.844318500021473, "o", "\u001b[2K\u001b[1G\u001b[36m⠴\u001b[39m Running hardwareKeyboard.set"]
[12.925441792001948, "o", "\u001b[2K\u001b[1G\u001b[36m⠦\u001b[39m Running hardwareKeyboard.set"]
[13.006504375021905, "o", "\u001b[2K\u001b[1G\u001b[36m⠧\u001b[39m Running hardwareKeyboard.set"]
[13.091790959006175, "o", "\u001b[2K\u001b[1G\u001b[36m⠇\u001b[39m Running hardwareKeyboard.set"]
[13.169457041891292, "o", "\u001b[2K\u001b[1G\u001b[36m⠏\u001b[39m Running hardwareKeyboard.set"]
[13.250556709012017, "o", "\u001b[2K\u001b[1G\u001b[36m⠋\u001b[39m Running hardwareKeyboard.set"]
[13.331479375017807, "o", "\u001b[2K\u001b[1G\u001b[36m⠙\u001b[39m Running hardwareKeyboard.set"]
[13.412635833956301, "o", "\u001b[2K\u001b[1G\u001b[36m⠹\u001b[39m Running hardwareKeyboard.set"]
[13.492869833949953, "o", "\u001b[2K\u001b[1G\u001b[36m⠸\u001b[39m Running hardwareKeyboard.set"]
[13.57397941686213, "o", "\u001b[2K\u001b[1G\u001b[36m⠼\u001b[39m Running hardwareKeyboard.set"]
[13.655002417042851, "o", "\u001b[2K\u001b[1G\u001b[36m⠴\u001b[39m Running hardwareKeyboard.set"]
[13.73585404199548, "o", "\u001b[2K\u001b[1G\u001b[36m⠦\u001b[39m Running hardwareKeyboard.set"]
[13.815101583953947, "o", "\u001b[2K\u001b[1G\u001b[36m⠧\u001b[39m Running hardwareKeyboard.set"]
[13.895353624830022, "o", "\u001b[2K\u001b[1G\u001b[36m⠇\u001b[39m Running hardwareKeyboard.set"]
[13.976550000021234, "o", "\u001b[2K\u001b[1G\u001b[36m⠏\u001b[39m Running hardwareKeyboard.set"]
[14.05732783395797, "o", "\u001b[2K\u001b[1G"]
[14.067912084050477, "o", "\u001b[36m⠋\u001b[39m Running hardwareKeyboard.set"]
[14.138545833993703, "o", "\u001b[2K\u001b[1G\u001b[36m⠙\u001b[39m Running hardwareKeyboard.set"]
[14.218941541854292, "o", "\u001b[2K\u001b[1G\u001b[36m⠹\u001b[39m Running hardwareKeyboard.set"]
[14.300092958845198, "o", "\u001b[2K\u001b[1G\u001b[36m⠸\u001b[39m Running hardwareKeyboard.set"]
[14.383424374973401, "o", "\u001b[2K\u001b[1G\u001b[36m⠼\u001b[39m Running hardwareKeyboard.set"]
[14.462565749883652, "o", "\u001b[2K\u001b[1G\u001b[36m⠴\u001b[39m Running hardwareKeyboard.set"]
[14.5485108748544, "o", "\u001b[2K\u001b[1G\u001b[36m⠦\u001b[39m Running hardwareKeyboard.set"]
[14.628651791950688, "o", "\u001b[2K\u001b[1G\u001b[36m⠧\u001b[39m Running hardwareKeyboard.set"]
[14.708914167014882, "o", "\u001b[2K\u001b[1G\u001b[36m⠇\u001b[39m Running hardwareKeyboard.set"]
[14.790110958972946, "o", "\u001b[2K\u001b[1G\u001b[36m⠏\u001b[39m Running hardwareKeyboard.set"]
[14.875117166899145, "o", "\u001b[2K\u001b[1G\u001b[36m⠋\u001b[39m Running hardwareKeyboard.set"]
[14.969444166868925, "o", "\u001b[2K\u001b[1G\u001b[36m⠙\u001b[39m Running hardwareKeyboard.set"]
[15.035175124881789, "o", "\u001b[2K\u001b[1G\u001b[36m⠹\u001b[39m Running hardwareKeyboard.set"]
[15.116620291955769, "o", "\u001b[2K\u001b[1G\u001b[36m⠸\u001b[39m Running hardwareKeyboard.set"]
[15.197228458942845, "o", "\u001b[2K\u001b[1G\u001b[36m⠼\u001b[39m Running hardwareKeyboard.set"]
[15.277551708975807, "o", "\u001b[2K\u001b[1G\u001b[36m⠴\u001b[39m Running hardwareKeyboard.set"]
[15.358189541846514, "o", "\u001b[2K\u001b[1G"]
[15.367686084005982, "o", "\u001b[36m⠦\u001b[39m Running hardwareKeyboard.set"]
[15.439325749874115, "o", "\u001b[2K\u001b[1G\u001b[36m⠧\u001b[39m Running hardwareKeyboard.set"]
[15.519865833921358, "o", "\u001b[2K\u001b[1G"]
[15.529336416861042, "o", "\u001b[36m⠇\u001b[39m Running hardwareKeyboard.set"]
[15.601249916944653, "o", "\u001b[2K\u001b[1G\u001b[36m⠏\u001b[39m Running hardwareKeyboard.set"]
[15.68264108384028, "o", "\u001b[2K\u001b[1G\u001b[36m⠋\u001b[39m Running hardwareKeyboard.set"]
[15.73954520886764, "o", "\u001b[2K\u001b[1G\u001b[?25h"]
[15.740707041928545, "o", "\u001b[32m✔\u001b[39m hardwareKeyboard.set complete · 10s\r\n\r\n"]
[15.741831374820322, "o", "\u001b[?25l\u001b[2K\u001b[1G\u001b[36m?\u001b[39m \u001b[1mChoose a simulator action\u001b[22m \u001b[90m…\u001b[39m \r\n\u001b[90mno matches found\u001b[39m\u001b[2K\u001b[1A\u001b[2K\u001b[G\u001b[2K\u001b[1G\u001b[36m?\u001b[39m \u001b[1mChoose a simulator action\u001b[22m \u001b[90m›\u001b[39m \r\n\u001b[36m❯\u001b[39m \u001b[36m\u001b[4mExit\u001b[39m\u001b[24m\u001b[90m - Close simulator controls\u001b[39m\r\n App › Container\u001b[90m - app.container\u001b[39m\r\n App › File › List\u001b[90m - app.file.list\u001b[39m\r\n App › File › Read\u001b[90m - app.file.read\u001b[39m\r\n App › File › Remove\u001b[90m - app.file.remove\u001b[39m\r\n App › File › Upload\u001b[90m - app.file.upload\u001b[39m\r\n App › Info plist\u001b[90m - app.infoPlist\u001b[39m\r\n ↓ App › Install\u001b[90m - app.install\u001b[39m"]
[16.974447791930288, "i", "hardwareKeyboard.get"]
[16.97557087498717, "o", "\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[G\u001b[2K\u001b[1G\u001b[36m?\u001b[39m \u001b[1mChoose a simulator action\u001b[22m \u001b[90m…\u001b[39m h\r\n\u001b[36m❯\u001b[39m \u001b[36m\u001b[4mExit\u001b[39m\u001b[24m\u001b[90m - Close simulator controls\u001b[39m\r\n App › Container\u001b[90m - app.container\u001b[39m\r\n App › File › List\u001b[90m - app.file.list\u001b[39m\r\n App › File › Read\u001b[90m - app.file.read\u001b[39m\r\n App › File › Remove\u001b[90m - app.file.remove\u001b[39m\r\n App › File › Upload\u001b[90m - app.file.upload\u001b[39m\r\n App › Info plist\u001b[90m - app.infoPlist\u001b[39m\r\n ↓ App › Install\u001b[90m - app.install\u001b[39m\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[G\u001b[2K\u001b[1G\u001b[36m?\u001b[39m \u001b[1mChoose a simulator action\u001b[22m \u001b[90m…\u001b[39m ha\r\n\u001b[36m❯\u001b[39m \u001b[36m\u001b[4mExit\u001b[39m\u001b[24m\u001b[90m - Close simulator controls\u001b[39m\r\n App › Container\u001b[90m - app.container\u001b[39m\r\n App › File › List\u001b[90m - app.file.list\u001b[39m\r\n App › File › Read\u001b[90m - app.file.read\u001b[39m\r\n App › File › Remove\u001b[90m - app.file.remove\u001b[39m\r\n A"]
[16.991120459046215, "o", "pp › File › Upload\u001b[90m - app.file.upload\u001b[39m\r\n App › Info plist\u001b[90m - app.infoPlist\u001b[39m\r\n ↓ App › Install\u001b[90m - app.install\u001b[39m\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[G\u001b[2K\u001b[1G\u001b[36m?\u001b[39m \u001b[1mChoose a simulator action\u001b[22m \u001b[90m…\u001b[39m har\r\n\u001b[36m❯\u001b[39m \u001b[36m\u001b[4mExit\u001b[39m\u001b[24m\u001b[90m - Close simulator controls\u001b[39m\r\n App › Container\u001b[90m - app.container\u001b[39m\r\n App › File › List\u001b[90m - app.file.list\u001b[39m\r\n App › File › Read\u001b[90m - app.file.read\u001b[39m\r\n App › File › Remove\u001b[90m - app.file.remove\u001b[39m\r\n App › File › Upload\u001b[90m - app.file.upload\u001b[39m\r\n App › Info plist\u001b[90m - app.infoPlist\u001b[39m\r\n ↓ App › Install\u001b[90m - app.install\u001b[39m\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[G\u001b[2K\u001b[1G\u001b[36m?\u001b[39m \u001b[1mChoose a simulator action\u001b[22m \u001b[90m…\u001b[39m hard\r\n\u001b[36m❯\u001b[39m \u001b[36m\u001b[4mExit\u001b[39m\u001b[24m\u001b[90m - Close simulator controls\u001b[39m\r\n App › Container\u001b[90m - app.container\u001b[39m\r\n App › File "]
[16.99204633384943, "o", "› List\u001b[90m - app.file.list\u001b[39m\r\n App › File › Read\u001b[90m - app.file.read\u001b[39m\r\n App › File › Remove\u001b[90m - app.file.remove\u001b[39m\r\n App › File › Upload\u001b[90m - app.file.upload\u001b[39m\r\n App › Info plist\u001b[90m - app.infoPlist\u001b[39m\r\n ↓ App › Install\u001b[90m - app.install\u001b[39m\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[G\u001b[2K\u001b[1G\u001b[36m?\u001b[39m \u001b[1mChoose a simulator action\u001b[22m \u001b[90m…\u001b[39m hardw\r\n\u001b[36m❯\u001b[39m \u001b[36m\u001b[4mExit\u001b[39m\u001b[24m\u001b[90m - Close simulator controls\u001b[39m\r\n App › Container\u001b[90m - app.container\u001b[39m\r\n App › File › List\u001b[90m - app.file.list\u001b[39m\r\n App › File › Read\u001b[90m - app.file.read\u001b[39m\r\n App › File › Remove\u001b[90m - app.file.remove\u001b[39m\r\n App › File › Upload\u001b[90m - app.file.upload\u001b[39m\r\n App › Info plist\u001b[90m - app.infoPlist\u001b[39m\r\n ↓ App › Install\u001b[90m - app.install\u001b[39m\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[G\u001b[2K\u001b[1G\u001b[36m?\u001b[39m \u001b[1mChoose a simulator action\u001b[22m \u001b[90m…\u001b[3"]
[16.993183041922748, "o", "9m hardwa\r\n\u001b[36m❯\u001b[39m \u001b[36m\u001b[4mExit\u001b[39m\u001b[24m\u001b[90m - Close simulator controls\u001b[39m\r\n App › Container\u001b[90m - app.container\u001b[39m\r\n App › File › List\u001b[90m - app.file.list\u001b[39m\r\n App › File › Read\u001b[90m - app.file.read\u001b[39m\r\n App › File › Remove\u001b[90m - app.file.remove\u001b[39m\r\n App › File › Upload\u001b[90m - app.file.upload\u001b[39m\r\n App › Info plist\u001b[90m - app.infoPlist\u001b[39m\r\n ↓ App › Install\u001b[90m - app.install\u001b[39m\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[G\u001b[2K\u001b[1G\u001b[36m?\u001b[39m \u001b[1mChoose a simulator action\u001b[22m \u001b[90m…\u001b[39m hardwar\r\n\u001b[36m❯\u001b[39m \u001b[36m\u001b[4mExit\u001b[39m\u001b[24m\u001b[90m - Close simulator controls\u001b[39m\r\n App › Container\u001b[90m - app.container\u001b[39m\r\n App › File › List\u001b[90m - app.file.list\u001b[39m\r\n App › File › Read\u001b[90m - app.file.read\u001b[39m\r\n App › File › Remove\u001b[90m - app.file.remove\u001b[39m\r\n App › File › Upload\u001b[90m - app.file.upload\u001b[39m\r\n App › Info plist\u001b[90m - app.infoPlist\u001b[39m\r\n ↓ App › Install\u001b[90"]
[16.99374737497419, "o", "m - app.install\u001b[39m\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[G\u001b[2K\u001b[1G\u001b[36m?\u001b[39m \u001b[1mChoose a simulator action\u001b[22m \u001b[90m…\u001b[39m hardware\r\n\u001b[36m❯\u001b[39m \u001b[36m\u001b[4mExit\u001b[39m\u001b[24m\u001b[90m - Close simulator controls\u001b[39m\r\n App › Container\u001b[90m - app.container\u001b[39m\r\n App › File › List\u001b[90m - app.file.list\u001b[39m\r\n App › File › Read\u001b[90m - app.file.read\u001b[39m\r\n App › File › Remove\u001b[90m - app.file.remove\u001b[39m\r\n App › File › Upload\u001b[90m - app.file.upload\u001b[39m\r\n App › Info plist\u001b[90m - app.infoPlist\u001b[39m\r\n ↓ App › Install\u001b[90m - app.install\u001b[39m\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[G\u001b[2K\u001b[1G\u001b[36m?\u001b[39m \u001b[1mChoose a simulator action\u001b[22m \u001b[90m…\u001b[39m hardwareK\r\n\u001b[36m❯\u001b[39m \u001b[36m\u001b[4mExit\u001b[39m\u001b[24m\u001b[90m - Close simulator controls\u001b[39m\r\n App › Container\u001b[90m - app.container\u001b[39m\r\n App › File › List\u001b[90m - app.file.list\u001b[39m\r\n App › File › Read\u001b[90m - app.file.read\u001b[39m\r\n App › File › Remove\u001b"]
[16.994273792020977, "o", "[90m - app.file.remove\u001b[39m\r\n App › File › Upload\u001b[90m - app.file.upload\u001b[39m\r\n App › Info plist\u001b[90m - app.infoPlist\u001b[39m\r\n ↓ App › Install\u001b[90m - app.install\u001b[39m\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[G\u001b[2K\u001b[1G\u001b[36m?\u001b[39m \u001b[1mChoose a simulator action\u001b[22m \u001b[90m…\u001b[39m hardwareKe\r\n\u001b[36m❯\u001b[39m \u001b[36m\u001b[4mExit\u001b[39m\u001b[24m\u001b[90m - Close simulator controls\u001b[39m\r\n App › Container\u001b[90m - app.container\u001b[39m\r\n App › File › List\u001b[90m - app.file.list\u001b[39m\r\n App › File › Read\u001b[90m - app.file.read\u001b[39m\r\n App › File › Remove\u001b[90m - app.file.remove\u001b[39m\r\n App › File › Upload\u001b[90m - app.file.upload\u001b[39m\r\n App › Info plist\u001b[90m - app.infoPlist\u001b[39m\r\n ↓ App › Install\u001b[90m - app.install\u001b[39m\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[G\u001b[2K\u001b[1G\u001b[36m?\u001b[39m \u001b[1mChoose a simulator action\u001b[22m \u001b[90m…\u001b[39m hardwareKey\r\n\u001b[36m❯\u001b[39m \u001b[36m\u001b[4mExit\u001b[39m\u001b[24m\u001b[90m - Close simulator controls\u001b[39m\r\n App › Contai"]
[16.994811666896567, "o", "ner\u001b[90m - app.container\u001b[39m\r\n App › File › List\u001b[90m - app.file.list\u001b[39m\r\n App › File › Read\u001b[90m - app.file.read\u001b[39m\r\n App › File › Remove\u001b[90m - app.file.remove\u001b[39m\r\n App › File › Upload\u001b[90m - app.file.upload\u001b[39m\r\n App › Info plist\u001b[90m - app.infoPlist\u001b[39m\r\n ↓ App › Install\u001b[90m - app.install\u001b[39m\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[G\u001b[2K\u001b[1G\u001b[36m?\u001b[39m \u001b[1mChoose a simulator action\u001b[22m \u001b[90m…\u001b[39m hardwareKeyb\r\n\u001b[36m❯\u001b[39m \u001b[36m\u001b[4mExit\u001b[39m\u001b[24m\u001b[90m - Close simulator controls\u001b[39m\r\n App › Container\u001b[90m - app.container\u001b[39m\r\n App › File › List\u001b[90m - app.file.list\u001b[39m\r\n App › File › Read\u001b[90m - app.file.read\u001b[39m\r\n App › File › Remove\u001b[90m - app.file.remove\u001b[39m\r\n App › File › Upload\u001b[90m - app.file.upload\u001b[39m\r\n App › Info plist\u001b[90m - app.infoPlist\u001b[39m\r\n ↓ App › Install\u001b[90m - app.install\u001b[39m\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[G\u001b[2K\u001b[1G\u001b[3"]
[16.99532295903191, "o", "6m?\u001b[39m \u001b[1mChoose a simulator action\u001b[22m \u001b[90m…\u001b[39m hardwareKeybo\r\n\u001b[36m❯\u001b[39m \u001b[36m\u001b[4mExit\u001b[39m\u001b[24m\u001b[90m - Close simulator controls\u001b[39m\r\n App › Container\u001b[90m - app.container\u001b[39m\r\n App › File › List\u001b[90m - app.file.list\u001b[39m\r\n App › File › Read\u001b[90m - app.file.read\u001b[39m\r\n App › File › Remove\u001b[90m - app.file.remove\u001b[39m\r\n App › File › Upload\u001b[90m - app.file.upload\u001b[39m\r\n App › Info plist\u001b[90m - app.infoPlist\u001b[39m\r\n ↓ App › Install\u001b[90m - app.install\u001b[39m\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[G\u001b[2K\u001b[1G\u001b[36m?\u001b[39m \u001b[1mChoose a simulator action\u001b[22m \u001b[90m…\u001b[39m hardwareKeyboa\r\n\u001b[36m❯\u001b[39m \u001b[36m\u001b[4mExit\u001b[39m\u001b[24m\u001b[90m - Close simulator controls\u001b[39m\r\n App › Container\u001b[90m - app.container\u001b[39m\r\n App › File › List\u001b[90m - app.file.list\u001b[39m\r\n App › File › Read\u001b[90m - app.file.read\u001b[39m\r\n App › File › Remove\u001b[90m - app.file.remove\u001b[39m\r\n App › File › Upload\u001b[90m - app.file.upload\u001b[39m\r\n Ap"]
[16.995842000003904, "o", "p › Info plist\u001b[90m - app.infoPlist\u001b[39m\r\n ↓ App › Install\u001b[90m - app.install\u001b[39m\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[G\u001b[2K\u001b[1G\u001b[36m?\u001b[39m \u001b[1mChoose a simulator action\u001b[22m \u001b[90m…\u001b[39m hardwareKeyboar\r\n\u001b[36m❯\u001b[39m \u001b[36m\u001b[4mExit\u001b[39m\u001b[24m\u001b[90m - Close simulator controls\u001b[39m\r\n App › Container\u001b[90m - app.container\u001b[39m\r\n App › File › List\u001b[90m - app.file.list\u001b[39m\r\n App › File › Read\u001b[90m - app.file.read\u001b[39m\r\n App › File › Remove\u001b[90m - app.file.remove\u001b[39m\r\n App › File › Upload\u001b[90m - app.file.upload\u001b[39m\r\n App › Info plist\u001b[90m - app.infoPlist\u001b[39m\r\n ↓ App › Install\u001b[90m - app.install\u001b[39m\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[G\u001b[2K\u001b[1G\u001b[36m?\u001b[39m \u001b[1mChoose a simulator action\u001b[22m \u001b[90m…\u001b[39m hardwareKeyboard\r\n\u001b[36m❯\u001b[39m \u001b[36m\u001b[4mExit\u001b[39m\u001b[24m\u001b[90m - Close simulator controls\u001b[39m\r\n App › Container\u001b[90m - app.container\u001b[39m\r\n App › File › List\u001b[90m - app.file.list\u001b[39m"]
[16.996364542050287, "o", "\r\n App › File › Read\u001b[90m - app.file.read\u001b[39m\r\n App › File › Remove\u001b[90m - app.file.remove\u001b[39m\r\n App › File › Upload\u001b[90m - app.file.upload\u001b[39m\r\n App › Info plist\u001b[90m - app.infoPlist\u001b[39m\r\n ↓ App › Install\u001b[90m - app.install\u001b[39m\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[G\u001b[2K\u001b[1G\u001b[36m?\u001b[39m \u001b[1mChoose a simulator action\u001b[22m \u001b[90m…\u001b[39m hardwareKeyboard.\r\n\u001b[36m❯\u001b[39m \u001b[36m\u001b[4mExit\u001b[39m\u001b[24m\u001b[90m - Close simulator controls\u001b[39m\r\n App › Container\u001b[90m - app.container\u001b[39m\r\n App › File › List\u001b[90m - app.file.list\u001b[39m\r\n App › File › Read\u001b[90m - app.file.read\u001b[39m\r\n App › File › Remove\u001b[90m - app.file.remove\u001b[39m\r\n App › File › Upload\u001b[90m - app.file.upload\u001b[39m\r\n App › Info plist\u001b[90m - app.infoPlist\u001b[39m\r\n ↓ App › Install\u001b[90m - app.install\u001b[39m\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[G\u001b[2K\u001b[1G\u001b[36m?\u001b[39m \u001b[1mChoose a simulator action\u001b[22m \u001b[90m…\u001b[39m hardwareKeyboard.g\r"]
[16.996960541931912, "o", "\r\n\u001b[36m❯\u001b[39m \u001b[36m\u001b[4mExit\u001b[39m\u001b[24m\u001b[90m - Close simulator controls\u001b[39m\r\n App › Container\u001b[90m - app.container\u001b[39m\r\n App › File › List\u001b[90m - app.file.list\u001b[39m\r\n App › File › Read\u001b[90m - app.file.read\u001b[39m\r\n App › File › Remove\u001b[90m - app.file.remove\u001b[39m\r\n App › File › Upload\u001b[90m - app.file.upload\u001b[39m\r\n App › Info plist\u001b[90m - app.infoPlist\u001b[39m\r\n ↓ App › Install\u001b[90m - app.install\u001b[39m\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[G\u001b[2K\u001b[1G\u001b[36m?\u001b[39m \u001b[1mChoose a simulator action\u001b[22m \u001b[90m…\u001b[39m hardwareKeyboard.ge\r\n\u001b[36m❯\u001b[39m \u001b[36m\u001b[4mExit\u001b[39m\u001b[24m\u001b[90m - Close simulator controls\u001b[39m\r\n App › Container\u001b[90m - app.container\u001b[39m\r\n App › File › List\u001b[90m - app.file.list\u001b[39m\r\n App › File › Read\u001b[90m - app.file.read\u001b[39m\r\n App › File › Remove\u001b[90m - app.file.remove\u001b[39m\r\n App › File › Upload\u001b[90m - app.file.upload\u001b[39m\r\n App › Info plist\u001b[90m - app.infoPlist\u001b[39m\r\n ↓ App › Install\u001b"]
[16.997391166863963, "o", "[90m - app.install\u001b[39m\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[G\u001b[2K\u001b[1G\u001b[36m?\u001b[39m \u001b[1mChoose a simulator action\u001b[22m \u001b[90m…\u001b[39m hardwareKeyboard.get\r\n\u001b[36m❯\u001b[39m \u001b[36m\u001b[4mExit\u001b[39m\u001b[24m\u001b[90m - Close simulator controls\u001b[39m\r\n App › Container\u001b[90m - app.container\u001b[39m\r\n App › File › List\u001b[90m - app.file.list\u001b[39m\r\n App › File › Read\u001b[90m - app.file.read\u001b[39m\r\n App › File › Remove\u001b[90m - app.file.remove\u001b[39m\r\n App › File › Upload\u001b[90m - app.file.upload\u001b[39m\r\n App › Info plist\u001b[90m - app.infoPlist\u001b[39m\r\n ↓ App › Install\u001b[90m - app.install\u001b[39m\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[G\u001b[2K\u001b[1G\u001b[36m?\u001b[39m \u001b[1mChoose a simulator action\u001b[22m \u001b[90m›\u001b[39m hardwareKeyboard.get\r\n\u001b[36m❯\u001b[39m \u001b[36m\u001b[4mHardware keyboard › Get\u001b[39m\u001b[24m\u001b[90m - hardwareKeyboard.get\u001b[39m"]
[17.479496124899015, "i", "\r"]
[17.479708291823044, "o", "\u001b[2K\u001b[1A\u001b[2K\u001b[G"]
[17.490146916825324, "o", "\u001b[2K\u001b[1G\u001b[32m✔\u001b[39m \u001b[1mChoose a simulator action\u001b[22m \u001b[90m›\u001b[39m Hardware keyboard › Get\r\n\u001b[?25h"]
[17.493270416976884, "o", "\r\n\u001b[1mhardwareKeyboard.get\u001b[22m\r\n\u001b[90mRead whether the hardware keyboard is connected.\u001b[39m\r\n\u001b[?25l"]
[17.494699166854843, "o", "\u001b[36m⠋\u001b[39m Running hardwareKeyboard.get"]
[17.57369454205036, "o", "\u001b[2K\u001b[1G\u001b[36m⠙\u001b[39m Running hardwareKeyboard.get"]
[17.654387874994427, "o", "\u001b[2K\u001b[1G\u001b[36m⠹\u001b[39m Running hardwareKeyboard.get"]
[17.735302208922803, "o", "\u001b[2K\u001b[1G\u001b[36m⠸\u001b[39m Running hardwareKeyboard.get"]
[17.81632991693914, "o", "\u001b[2K\u001b[1G\u001b[36m⠼\u001b[39m Running hardwareKeyboard.get"]
[17.89701258391142, "o", "\u001b[2K\u001b[1G\u001b[36m⠴\u001b[39m Running hardwareKeyboard.get"]
[17.981051541864872, "o", "\u001b[2K\u001b[1G\u001b[36m⠦\u001b[39m Running hardwareKeyboard.get"]
[18.060075209010392, "o", "\u001b[2K\u001b[1G\u001b[36m⠧\u001b[39m Running hardwareKeyboard.get"]
[18.142794041894376, "o", "\u001b[2K\u001b[1G\u001b[36m⠇\u001b[39m Running hardwareKeyboard.get"]
[18.22327116690576, "o", "\u001b[2K\u001b[1G\u001b[36m⠏\u001b[39m Running hardwareKeyboard.get"]
[18.304872083943337, "o", "\u001b[2K\u001b[1G\u001b[36m⠋\u001b[39m Running hardwareKeyboard.get"]
[18.385799624957144, "o", "\u001b[2K\u001b[1G\u001b[36m⠙\u001b[39m Running hardwareKeyboard.get"]
[18.465768791968003, "o", "\u001b[2K\u001b[1G\u001b[36m⠹\u001b[39m Running hardwareKeyboard.get"]
[18.54689925001003, "o", "\u001b[2K\u001b[1G\u001b[36m⠸\u001b[39m Running hardwareKeyboard.get"]
[18.62797366688028, "o", "\u001b[2K\u001b[1G"]
[18.64218600001186, "o", "\u001b[36m⠼\u001b[39m Running hardwareKeyboard.get"]
[18.708716291934252, "o", "\u001b[2K\u001b[1G\u001b[36m⠴\u001b[39m Running hardwareKeyboard.get"]
[18.789846417028457, "o", "\u001b[2K\u001b[1G\u001b[36m⠦\u001b[39m Running hardwareKeyboard.get"]
[18.870477583957836, "o", "\u001b[2K\u001b[1G\u001b[36m⠧\u001b[39m Running hardwareKeyboard.get"]
[18.951340917032212, "o", "\u001b[2K\u001b[1G\u001b[36m⠇\u001b[39m Running hardwareKeyboard.get"]
[19.03248695889488, "o", "\u001b[2K\u001b[1G\u001b[36m⠏\u001b[39m Running hardwareKeyboard.get"]
[19.113611334003508, "o", "\u001b[2K\u001b[1G\u001b[36m⠋\u001b[39m Running hardwareKeyboard.get"]
[19.193742083851248, "o", "\u001b[2K\u001b[1G\u001b[36m⠙\u001b[39m Running hardwareKeyboard.get"]
[19.273996291914955, "o", "\u001b[2K\u001b[1G\u001b[36m⠹\u001b[39m Running hardwareKeyboard.get"]
[19.355182624887675, "o", "\u001b[2K\u001b[1G\u001b[36m⠸\u001b[39m Running hardwareKeyboard.get"]
[19.43636312498711, "o", "\u001b[2K\u001b[1G\u001b[36m⠼\u001b[39m Running hardwareKeyboard.get"]
[19.517340916907415, "o", "\u001b[2K\u001b[1G"]
[19.530037167016417, "o", "\u001b[36m⠴\u001b[39m Running hardwareKeyboard.get"]
[19.60103858401999, "o", "\u001b[2K\u001b[1G\u001b[36m⠦\u001b[39m Running hardwareKeyboard.get"]
[19.68140899995342, "o", "\u001b[2K\u001b[1G\u001b[36m⠧\u001b[39m Running hardwareKeyboard.get"]
[19.7624026669655, "o", "\u001b[2K\u001b[1G\u001b[36m⠇\u001b[39m Running hardwareKeyboard.get"]
[19.843559250002727, "o", "\u001b[2K\u001b[1G\u001b[36m⠏\u001b[39m Running hardwareKeyboard.get"]
[19.9246554588899, "o", "\u001b[2K\u001b[1G\u001b[36m⠋\u001b[39m Running hardwareKeyboard.get"]
[20.00712487497367, "o", "\u001b[2K\u001b[1G\u001b[36m⠙\u001b[39m Running hardwareKeyboard.get"]
[20.089441583957523, "o", "\u001b[2K\u001b[1G\u001b[36m⠹\u001b[39m Running hardwareKeyboard.get"]
[20.16806745901704, "o", "\u001b[2K\u001b[1G\u001b[36m⠸\u001b[39m Running hardwareKeyboard.get"]
[20.248506624950096, "o", "\u001b[2K\u001b[1G\u001b[36m⠼\u001b[39m Running hardwareKeyboard.get"]
[20.329670666949823, "o", "\u001b[2K\u001b[1G\u001b[36m⠴\u001b[39m Running hardwareKeyboard.get"]
[20.40979920886457, "o", "\u001b[2K\u001b[1G\u001b[36m⠦\u001b[39m Running hardwareKeyboard.get"]
[20.49076074990444, "o", "\u001b[2K\u001b[1G\u001b[36m⠧\u001b[39m Running hardwareKeyboard.get"]
[20.571516958996654, "o", "\u001b[2K\u001b[1G\u001b[36m⠇\u001b[39m Running hardwareKeyboard.get"]
[20.65349991689436, "o", "\u001b[2K\u001b[1G\u001b[36m⠏\u001b[39m Running hardwareKeyboard.get"]
[20.73352124984376, "o", "\u001b[2K\u001b[1G\u001b[36m⠋\u001b[39m Running hardwareKeyboard.get"]
[20.814831958850846, "o", "\u001b[2K\u001b[1G\u001b[36m⠙\u001b[39m Running hardwareKeyboard.get"]
[20.894782125018537, "o", "\u001b[2K\u001b[1G\u001b[36m⠹\u001b[39m Running hardwareKeyboard.get"]
[20.976087791845202, "o", "\u001b[2K\u001b[1G\u001b[36m⠸\u001b[39m Running hardwareKeyboard.get"]
[21.057208916870877, "o", "\u001b[2K\u001b[1G\u001b[36m⠼\u001b[39m Running hardwareKeyboard.get"]
[21.138354334048927, "o", "\u001b[2K\u001b[1G\u001b[36m⠴\u001b[39m Running hardwareKeyboard.get"]
[21.230020666960627, "o", "\u001b[2K\u001b[1G\u001b[36m⠦\u001b[39m Running hardwareKeyboard.get"]
[21.306734750047326, "o", "\u001b[2K\u001b[1G\u001b[36m⠧\u001b[39m Running hardwareKeyboard.get"]
[21.385315833846107, "o", "\u001b[2K\u001b[1G\u001b[36m⠇\u001b[39m Running hardwareKeyboard.get"]
[21.467253083828837, "o", "\u001b[2K\u001b[1G\u001b[36m⠏\u001b[39m Running hardwareKeyboard.get"]
[21.54833179200068, "o", "\u001b[2K\u001b[1G\u001b[36m⠋\u001b[39m Running hardwareKeyboard.get"]
[21.629590624943376, "o", "\u001b[2K\u001b[1G\u001b[36m⠙\u001b[39m Running hardwareKeyboard.get"]
[21.711231124820188, "o", "\u001b[2K\u001b[1G\u001b[36m⠹\u001b[39m Running hardwareKeyboard.get"]
[21.791989875026047, "o", "\u001b[2K\u001b[1G\u001b[36m⠸\u001b[39m Running hardwareKeyboard.get"]
[21.8728033748921, "o", "\u001b[2K\u001b[1G\u001b[36m⠼\u001b[39m Running hardwareKeyboard.get"]
[21.95395441702567, "o", "\u001b[2K\u001b[1G\u001b[36m⠴\u001b[39m Running hardwareKeyboard.get"]
[22.037675166968256, "o", "\u001b[2K\u001b[1G\u001b[36m⠦\u001b[39m Running hardwareKeyboard.get"]
[22.11712154187262, "o", "\u001b[2K\u001b[1G\u001b[36m⠧\u001b[39m Running hardwareKeyboard.get"]
[22.19586787489243, "o", "\u001b[2K\u001b[1G\u001b[36m⠇\u001b[39m Running hardwareKeyboard.get"]
[22.27701458404772, "o", "\u001b[2K\u001b[1G\u001b[36m⠏\u001b[39m Running hardwareKeyboard.get"]
[22.35822316701524, "o", "\u001b[2K\u001b[1G\u001b[36m⠋\u001b[39m Running hardwareKeyboard.get"]
[22.439326041843742, "o", "\u001b[2K\u001b[1G\u001b[36m⠙\u001b[39m Running hardwareKeyboard.get"]
[22.52054104185663, "o", "\u001b[2K\u001b[1G\u001b[36m⠹\u001b[39m Running hardwareKeyboard.get"]
[22.60164845897816, "o", "\u001b[2K\u001b[1G\u001b[36m⠸\u001b[39m Running hardwareKeyboard.get"]
[22.65924987499602, "o", "\u001b[2K\u001b[1G\u001b[?25h"]
[22.66018395894207, "o", "\u001b[32m✔\u001b[39m hardwareKeyboard.get complete · 5s\r\noff\r\n\r\n"]
[22.66132316691801, "o", "\u001b[?25l\u001b[2K\u001b[1G\u001b[36m?\u001b[39m \u001b[1mChoose a simulator action\u001b[22m \u001b[90m…\u001b[39m \r\n\u001b[90mno matches found\u001b[39m\u001b[2K\u001b[1A\u001b[2K\u001b[G\u001b[2K\u001b[1G\u001b[36m?\u001b[39m \u001b[1mChoose a simulator action\u001b[22m \u001b[90m›\u001b[39m \r\n\u001b[36m❯\u001b[39m \u001b[36m\u001b[4mExit\u001b[39m\u001b[24m\u001b[90m - Close simulator controls\u001b[39m\r\n App › Container\u001b[90m - app.container\u001b[39m\r\n App › File › List\u001b[90m - app.file.list\u001b[39m\r\n App › File › Read\u001b[90m - app.file.read\u001b[39m\r\n App › File › Remove\u001b[90m - app.file.remove\u001b[39m\r\n App › File › Upload\u001b[90m - app.file.upload\u001b[39m\r\n App › Info plist\u001b[90m - app.infoPlist\u001b[39m\r\n ↓ App › Install\u001b[90m - app.install\u001b[39m"]
[23.69174512499012, "i", "screenshot.capture"]
[23.69253662484698, "o", "\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[G\u001b[2K\u001b[1G\u001b[36m?\u001b[39m \u001b[1mChoose a simulator action\u001b[22m \u001b[90m…\u001b[39m s\r\n\u001b[36m❯\u001b[39m \u001b[36m\u001b[4mExit\u001b[39m\u001b[24m\u001b[90m - Close simulator controls\u001b[39m\r\n App › Container\u001b[90m - app.container\u001b[39m\r\n App › File › List\u001b[90m - app.file.list\u001b[39m\r\n App › File › Read\u001b[90m - app.file.read\u001b[39m\r\n App › File › Remove\u001b[90m - app.file.remove\u001b[39m\r\n App › File › Upload\u001b[90m - app.file.upload\u001b[39m\r\n App › Info plist\u001b[90m - app.infoPlist\u001b[39m\r\n ↓ App › Install\u001b[90m - app.install\u001b[39m"]
[23.711195959011093, "o", "\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[G\u001b[2K\u001b[1G\u001b[36m?\u001b[39m \u001b[1mChoose a simulator action\u001b[22m \u001b[90m…\u001b[39m sc\r\n\u001b[36m❯\u001b[39m \u001b[36m\u001b[4mExit\u001b[39m\u001b[24m\u001b[90m - Close simulator controls\u001b[39m\r\n App › Container\u001b[90m - app.container\u001b[39m\r\n App › File › List\u001b[90m - app.file.list\u001b[39m\r\n App › File › Read\u001b[90m - app.file.read\u001b[39m\r\n App › File › Remove\u001b[90m - app.file.remove\u001b[39m\r\n App › File › Upload\u001b[90m - app.file.upload\u001b[39m\r\n App › Info plist\u001b[90m - app.infoPlist\u001b[39m\r\n ↓ App › Install\u001b[90m - app.install\u001b[39m\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[G\u001b[2K\u001b[1G\u001b[36m?\u001b[39m \u001b[1mChoose a simulator action\u001b[22m \u001b[90m…\u001b[39m scr\r\n\u001b[36m❯\u001b[39m \u001b[36m\u001b[4mExit\u001b[39m\u001b[24m\u001b[90m - Close simulator controls\u001b[39m\r\n App › Container\u001b[90m - app.container\u001b[39m\r\n App › File › List\u001b[90m - app.file.list\u001b[39m\r\n App › File › Read\u001b[90m - app.file.read\u001b[39m\r\n App › File › Remove\u001b[90m - app.file.remove\u001b[39m\r\n "]
[23.711681874934584, "o", " App › File › Upload\u001b[90m - app.file.upload\u001b[39m\r\n App › Info plist\u001b[90m - app.infoPlist\u001b[39m\r\n ↓ App › Install\u001b[90m - app.install\u001b[39m\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[G\u001b[2K\u001b[1G\u001b[36m?\u001b[39m \u001b[1mChoose a simulator action\u001b[22m \u001b[90m…\u001b[39m scre\r\n\u001b[36m❯\u001b[39m \u001b[36m\u001b[4mExit\u001b[39m\u001b[24m\u001b[90m - Close simulator controls\u001b[39m\r\n App › Container\u001b[90m - app.container\u001b[39m\r\n App › File › List\u001b[90m - app.file.list\u001b[39m\r\n App › File › Read\u001b[90m - app.file.read\u001b[39m\r\n App › File › Remove\u001b[90m - app.file.remove\u001b[39m\r\n App › File › Upload\u001b[90m - app.file.upload\u001b[39m\r\n App › Info plist\u001b[90m - app.infoPlist\u001b[39m\r\n ↓ App › Install\u001b[90m - app.install\u001b[39m\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[G\u001b[2K\u001b[1G\u001b[36m?\u001b[39m \u001b[1mChoose a simulator action\u001b[22m \u001b[90m…\u001b[39m scree\r\n\u001b[36m❯\u001b[39m \u001b[36m\u001b[4mExit\u001b[39m\u001b[24m\u001b[90m - Close simulator controls\u001b[39m\r\n App › Container\u001b[90m - app.container\u001b[39m\r\n App › F"]
[23.712159999879077, "o", "ile › List\u001b[90m - app.file.list\u001b[39m\r\n App › File › Read\u001b[90m - app.file.read\u001b[39m\r\n App › File › Remove\u001b[90m - app.file.remove\u001b[39m\r\n App › File › Upload\u001b[90m - app.file.upload\u001b[39m\r\n App › Info plist\u001b[90m - app.infoPlist\u001b[39m\r\n ↓ App › Install\u001b[90m - app.install\u001b[39m\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[G\u001b[2K\u001b[1G\u001b[36m?\u001b[39m \u001b[1mChoose a simulator action\u001b[22m \u001b[90m…\u001b[39m screen\r\n\u001b[36m❯\u001b[39m \u001b[36m\u001b[4mExit\u001b[39m\u001b[24m\u001b[90m - Close simulator controls\u001b[39m\r\n App › Container\u001b[90m - app.container\u001b[39m\r\n App › File › List\u001b[90m - app.file.list\u001b[39m\r\n App › File › Read\u001b[90m - app.file.read\u001b[39m\r\n App › File › Remove\u001b[90m - app.file.remove\u001b[39m\r\n App › File › Upload\u001b[90m - app.file.upload\u001b[39m\r\n App › Info plist\u001b[90m - app.infoPlist\u001b[39m\r\n ↓ App › Install\u001b[90m - app.install\u001b[39m\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[G\u001b[2K\u001b[1G\u001b[36m?\u001b[39m \u001b[1mChoose a simulator action\u001b[22m \u001b[90m"]
[23.712624334031716, "o", "…\u001b[39m screens\r\n\u001b[36m❯\u001b[39m \u001b[36m\u001b[4mExit\u001b[39m\u001b[24m\u001b[90m - Close simulator controls\u001b[39m\r\n App › Container\u001b[90m - app.container\u001b[39m\r\n App › File › List\u001b[90m - app.file.list\u001b[39m\r\n App › File › Read\u001b[90m - app.file.read\u001b[39m\r\n App › File › Remove\u001b[90m - app.file.remove\u001b[39m\r\n App › File › Upload\u001b[90m - app.file.upload\u001b[39m\r\n App › Info plist\u001b[90m - app.infoPlist\u001b[39m\r\n ↓ App › Install\u001b[90m - app.install\u001b[39m\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[G\u001b[2K\u001b[1G\u001b[36m?\u001b[39m \u001b[1mChoose a simulator action\u001b[22m \u001b[90m…\u001b[39m screensh\r\n\u001b[36m❯\u001b[39m \u001b[36m\u001b[4mExit\u001b[39m\u001b[24m\u001b[90m - Close simulator controls\u001b[39m\r\n App › Container\u001b[90m - app.container\u001b[39m\r\n App › File › List\u001b[90m - app.file.list\u001b[39m\r\n App › File › Read\u001b[90m - app.file.read\u001b[39m\r\n App › File › Remove\u001b[90m - app.file.remove\u001b[39m\r\n App › File › Upload\u001b[90m - app.file.upload\u001b[39m\r\n App › Info plist\u001b[90m - app.infoPlist\u001b[39m\r\n ↓ App › Inst"]
[23.713105666916817, "o", "all\u001b[90m - app.install\u001b[39m\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[G\u001b[2K\u001b[1G\u001b[36m?\u001b[39m \u001b[1mChoose a simulator action\u001b[22m \u001b[90m…\u001b[39m screensho\r\n\u001b[36m❯\u001b[39m \u001b[36m\u001b[4mExit\u001b[39m\u001b[24m\u001b[90m - Close simulator controls\u001b[39m\r\n App › Container\u001b[90m - app.container\u001b[39m\r\n App › File › List\u001b[90m - app.file.list\u001b[39m\r\n App › File › Read\u001b[90m - app.file.read\u001b[39m\r\n App › File › Remove\u001b[90m - app.file.remove\u001b[39m\r\n App › File › Upload\u001b[90m - app.file.upload\u001b[39m\r\n App › Info plist\u001b[90m - app.infoPlist\u001b[39m\r\n ↓ App › Install\u001b[90m - app.install\u001b[39m\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[G\u001b[2K\u001b[1G\u001b[36m?\u001b[39m \u001b[1mChoose a simulator action\u001b[22m \u001b[90m…\u001b[39m screenshot\r\n\u001b[36m❯\u001b[39m \u001b[36m\u001b[4mExit\u001b[39m\u001b[24m\u001b[90m - Close simulator controls\u001b[39m\r\n App › Container\u001b[90m - app.container\u001b[39m\r\n App › File › List\u001b[90m - app.file.list\u001b[39m\r\n App › File › Read\u001b[90m - app.file.read\u001b[39m\r\n App › File "]
[23.71380254183896, "o", "› Remove\u001b[90m - app.file.remove\u001b[39m\r\n App › File › Upload\u001b[90m - app.file.upload\u001b[39m\r\n App › Info plist\u001b[90m - app.infoPlist\u001b[39m\r\n ↓ App › Install\u001b[90m - app.install\u001b[39m\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[G\u001b[2K\u001b[1G\u001b[36m?\u001b[39m \u001b[1mChoose a simulator action\u001b[22m \u001b[90m…\u001b[39m screenshot.\r\n\u001b[36m❯\u001b[39m \u001b[36m\u001b[4mExit\u001b[39m\u001b[24m\u001b[90m - Close simulator controls\u001b[39m\r\n App › Container\u001b[90m - app.container\u001b[39m\r\n App › File › List\u001b[90m - app.file.list\u001b[39m\r\n App › File › Read\u001b[90m - app.file.read\u001b[39m\r\n App › File › Remove\u001b[90m - app.file.remove\u001b[39m\r\n App › File › Upload\u001b[90m - app.file.upload\u001b[39m\r\n App › Info plist\u001b[90m - app.infoPlist\u001b[39m\r\n ↓ App › Install\u001b[90m - app.install\u001b[39m\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[G\u001b[2K\u001b[1G\u001b[36m?\u001b[39m \u001b[1mChoose a simulator action\u001b[22m \u001b[90m…\u001b[39m screenshot.c\r\n\u001b[36m❯\u001b[39m \u001b[36m\u001b[4mExit\u001b[39m\u001b[24m\u001b[90m - Close simulator controls\u001b[39m\r\n App"]
[23.714382166974247, "o", " › Container\u001b[90m - app.container\u001b[39m\r\n App › File › List\u001b[90m - app.file.list\u001b[39m\r\n App › File › Read\u001b[90m - app.file.read\u001b[39m\r\n App › File › Remove\u001b[90m - app.file.remove\u001b[39m\r\n App › File › Upload\u001b[90m - app.file.upload\u001b[39m\r\n App › Info plist\u001b[90m - app.infoPlist\u001b[39m\r\n ↓ App › Install\u001b[90m - app.install\u001b[39m\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[G\u001b[2K\u001b[1G\u001b[36m?\u001b[39m \u001b[1mChoose a simulator action\u001b[22m \u001b[90m…\u001b[39m screenshot.ca\r\n\u001b[36m❯\u001b[39m \u001b[36m\u001b[4mExit\u001b[39m\u001b[24m\u001b[90m - Close simulator controls\u001b[39m\r\n App › Container\u001b[90m - app.container\u001b[39m\r\n App › File › List\u001b[90m - app.file.list\u001b[39m\r\n App › File › Read\u001b[90m - app.file.read\u001b[39m\r\n App › File › Remove\u001b[90m - app.file.remove\u001b[39m\r\n App › File › Upload\u001b[90m - app.file.upload\u001b[39m\r\n App › Info plist\u001b[90m - app.infoPlist\u001b[39m\r\n ↓ App › Install\u001b[90m - app.install\u001b[39m\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b["]
[23.71490141702816, "o", "G\u001b[2K\u001b[1G\u001b[36m?\u001b[39m \u001b[1mChoose a simulator action\u001b[22m \u001b[90m…\u001b[39m screenshot.cap\r\n\u001b[36m❯\u001b[39m \u001b[36m\u001b[4mExit\u001b[39m\u001b[24m\u001b[90m - Close simulator controls\u001b[39m\r\n App › Container\u001b[90m - app.container\u001b[39m\r\n App › File › List\u001b[90m - app.file.list\u001b[39m\r\n App › File › Read\u001b[90m - app.file.read\u001b[39m\r\n App › File › Remove\u001b[90m - app.file.remove\u001b[39m\r\n App › File › Upload\u001b[90m - app.file.upload\u001b[39m\r\n App › Info plist\u001b[90m - app.infoPlist\u001b[39m\r\n ↓ App › Install\u001b[90m - app.install\u001b[39m\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[G\u001b[2K\u001b[1G\u001b[36m?\u001b[39m \u001b[1mChoose a simulator action\u001b[22m \u001b[90m…\u001b[39m screenshot.capt\r\n\u001b[36m❯\u001b[39m \u001b[36m\u001b[4mExit\u001b[39m\u001b[24m\u001b[90m - Close simulator controls\u001b[39m\r\n App › Container\u001b[90m - app.container\u001b[39m\r\n App › File › List\u001b[90m - app.file.list\u001b[39m\r\n App › File › Read\u001b[90m - app.file.read\u001b[39m\r\n App › File › Remove\u001b[90m - app.file.remove\u001b[39m\r\n App › File › Upload\u001b[90m - app.file.uploa"]
[23.715422624954954, "o", "d\u001b[39m\r\n App › Info plist\u001b[90m - app.infoPlist\u001b[39m\r\n ↓ App › Install\u001b[90m - app.install\u001b[39m\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[G\u001b[2K\u001b[1G\u001b[36m?\u001b[39m \u001b[1mChoose a simulator action\u001b[22m \u001b[90m…\u001b[39m screenshot.captu\r\n\u001b[36m❯\u001b[39m \u001b[36m\u001b[4mExit\u001b[39m\u001b[24m\u001b[90m - Close simulator controls\u001b[39m\r\n App › Container\u001b[90m - app.container\u001b[39m\r\n App › File › List\u001b[90m - app.file.list\u001b[39m\r\n App › File › Read\u001b[90m - app.file.read\u001b[39m\r\n App › File › Remove\u001b[90m - app.file.remove\u001b[39m\r\n App › File › Upload\u001b[90m - app.file.upload\u001b[39m\r\n App › Info plist\u001b[90m - app.infoPlist\u001b[39m\r\n ↓ App › Install\u001b[90m - app.install\u001b[39m\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[G\u001b[2K\u001b[1G\u001b[36m?\u001b[39m \u001b[1mChoose a simulator action\u001b[22m \u001b[90m…\u001b[39m screenshot.captur\r\n\u001b[36m❯\u001b[39m \u001b[36m\u001b[4mExit\u001b[39m\u001b[24m\u001b[90m - Close simulator controls\u001b[39m\r\n App › Container\u001b[90m - app.container\u001b[39m\r\n App › File › List\u001b[90m - ap"]
[23.715938458917663, "o", "p.file.list\u001b[39m\r\n App › File › Read\u001b[90m - app.file.read\u001b[39m\r\n App › File › Remove\u001b[90m - app.file.remove\u001b[39m\r\n App › File › Upload\u001b[90m - app.file.upload\u001b[39m\r\n App › Info plist\u001b[90m - app.infoPlist\u001b[39m\r\n ↓ App › Install\u001b[90m - app.install\u001b[39m\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[G\u001b[2K\u001b[1G\u001b[36m?\u001b[39m \u001b[1mChoose a simulator action\u001b[22m \u001b[90m…\u001b[39m screenshot.capture\r\n\u001b[36m❯\u001b[39m \u001b[36m\u001b[4mExit\u001b[39m\u001b[24m\u001b[90m - Close simulator controls\u001b[39m\r\n App › Container\u001b[90m - app.container\u001b[39m\r\n App › File › List\u001b[90m - app.file.list\u001b[39m\r\n App › File › Read\u001b[90m - app.file.read\u001b[39m\r\n App › File › Remove\u001b[90m - app.file.remove\u001b[39m\r\n App › File › Upload\u001b[90m - app.file.upload\u001b[39m\r\n App › Info plist\u001b[90m - app.infoPlist\u001b[39m\r\n ↓ App › Install\u001b[90m - app.install\u001b[39m\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[G\u001b[2K\u001b[1G\u001b[36m?\u001b[39m \u001b[1mChoose a simulator action\u001b[22m \u001b[90m›\u001b[39m sc"]
[23.716004208894446, "o", "reenshot.capture\r\n\u001b[36m❯\u001b[39m \u001b[36m\u001b[4mScreenshot › Capture\u001b[39m\u001b[24m\u001b[90m - screenshot.capture\u001b[39m"]
[24.195908499881625, "i", "\r"]
[24.19655083399266, "o", "\u001b[2K\u001b[1A\u001b[2K\u001b[G\u001b[2K\u001b[1G\u001b[32m✔\u001b[39m \u001b[1mChoose a simulator action\u001b[22m \u001b[90m›\u001b[39m Screenshot › Capture\r\n\u001b[?25h"]
[24.21195320901461, "o", "\r\n\u001b[1mscreenshot.capture\u001b[22m\r\n\u001b[90mTake a screenshot.\u001b[39m\r\n"]
[24.212831625016406, "o", "\u001b[?25l\u001b[2K\u001b[1G\u001b[36m?\u001b[39m \u001b[1mSet optional fileName?\u001b[22m \u001b[90m›\u001b[39m \u001b[90m(y/N)\u001b[39m"]
[24.213640624890104, "i", "n\r"]
[24.213908708887175, "o", "\u001b[2K\u001b[G"]
[24.21608449984342, "o", "\u001b[2K\u001b[1G\u001b[32m✔\u001b[39m \u001b[1mSet optional fileName?\u001b[22m \u001b[90m…\u001b[39m no\r\n\u001b[?25h"]
[24.21728516696021, "o", "\u001b[2K\u001b[1G\u001b[36m?\u001b[39m \u001b[1mSave file to\u001b[22m \u001b[90m›\u001b[39m \u001b7\u001b8"]
[24.218149584019557, "i", "/private/tmp/eas-control-context-20261017/interactive-screen.png\r"]
[24.218676666961983, "o", "\u001b[2K\u001b[G\u001b[2K\u001b[1G\u001b[36m?\u001b[39m \u001b[1mSave file to\u001b[22m \u001b[90m›\u001b[39m /\u001b7\u001b8"]
[24.22036658390425, "o", "\u001b[2K\u001b[G\u001b[2K\u001b[1G\u001b[36m?\u001b[39m \u001b[1mSave file to\u001b[22m \u001b[90m›\u001b[39m /p\u001b7\u001b8\u001b[2K\u001b[G\u001b[2K\u001b[1G\u001b[36m?\u001b[39m \u001b[1mSave file to\u001b[22m \u001b[90m›\u001b[39m /pr\u001b7\u001b8\u001b[2K\u001b[G\u001b[2K\u001b[1G\u001b[36m?\u001b[39m \u001b[1mSave file to\u001b[22m \u001b[90m›\u001b[39m /pri\u001b7\u001b8\u001b[2K\u001b[G\u001b[2K\u001b[1G\u001b[36m?\u001b[39m \u001b[1mSave file to\u001b[22m \u001b[90m›\u001b[39m /priv\u001b7\u001b8\u001b[2K\u001b[G\u001b[2K\u001b[1G\u001b[36m?\u001b[39m \u001b[1mSave file to\u001b[22m \u001b[90m›\u001b[39m /priva\u001b7\u001b8\u001b[2K\u001b[G\u001b[2K\u001b[1G\u001b[36m?\u001b[39m \u001b[1mSave file to\u001b[22m \u001b[90m›\u001b[39m /privat\u001b7\u001b8\u001b[2K\u001b[G\u001b[2K\u001b[1G\u001b[36m?\u001b[39m \u001b[1mSave file to\u001b[22m \u001b[90m›\u001b[39m /private\u001b7\u001b8\u001b[2K\u001b[G\u001b[2K\u001b[1G\u001b[36m?\u001b[39m \u001b[1mSave file to\u001b[22m \u001b[90m›\u001b[39m /private/\u001b7\u001b8\u001b[2K\u001b[G\u001b[2K\u001b[1G\u001b[36m?\u001b[39m \u001b[1mSave file to\u001b[22m \u001b[90m›\u001b[39m /private/t\u001b7\u001b8\u001b[2K\u001b[G\u001b[2K\u001b[1G\u001b[36m?\u001b[39m \u001b[1mSave file to\u001b[22m \u001b[90m›\u001b[39m /private/tm\u001b7\u001b8\u001b[2K\u001b[G\u001b[2K\u001b[1G\u001b[36m?\u001b[39m \u001b[1mSave file to\u001b[22m \u001b[90m›\u001b[39m /private/tmp\u001b7\u001b8\u001b[2K\u001b[G\u001b[2K\u001b[1G\u001b[36m?\u001b[39m \u001b[1mSave file to\u001b[22m \u001b[90m›\u001b[39m /private/tmp/\u001b7\u001b8\u001b[2K\u001b[G\u001b[2K\u001b[1G\u001b[36m?\u001b[39m \u001b[1mSave file to\u001b[22m \u001b[90m›\u001b[39m /private/tmp/e\u001b7\u001b8\u001b[2K\u001b[G\u001b[2K\u001b[1G\u001b[36m?\u001b[39m \u001b[1mSave file to\u001b[22m "]
[24.22196008404717, "o", "\u001b[90m›\u001b[39m /private/tmp/ea\u001b7\u001b8\u001b[2K\u001b[G\u001b[2K\u001b[1G\u001b[36m?\u001b[39m \u001b[1mSave file to\u001b[22m \u001b[90m›\u001b[39m /private/tmp/eas\u001b7\u001b8\u001b[2K\u001b[G\u001b[2K\u001b[1G\u001b[36m?\u001b[39m \u001b[1mSave file to\u001b[22m \u001b[90m›\u001b[39m /private/tmp/eas-\u001b7\u001b8\u001b[2K\u001b[G\u001b[2K\u001b[1G\u001b[36m?\u001b[39m \u001b[1mSave file to\u001b[22m \u001b[90m›\u001b[39m /private/tmp/eas-c\u001b7\u001b8\u001b[2K\u001b[G\u001b[2K\u001b[1G\u001b[36m?\u001b[39m \u001b[1mSave file to\u001b[22m \u001b[90m›\u001b[39m /private/tmp/eas-co\u001b7\u001b8\u001b[2K\u001b[G\u001b[2K\u001b[1G\u001b[36m?\u001b[39m \u001b[1mSave file to\u001b[22m \u001b[90m›\u001b[39m /private/tmp/eas-con\u001b7\u001b8\u001b[2K\u001b[G\u001b[2K\u001b[1G\u001b[36m?\u001b[39m \u001b[1mSave file to\u001b[22m \u001b[90m›\u001b[39m /private/tmp/eas-cont\u001b7\u001b8\u001b[2K\u001b[G\u001b[2K\u001b[1G\u001b[36m?\u001b[39m \u001b[1mSave file to\u001b[22m \u001b[90m›\u001b[39m /private/tmp/eas-contr\u001b7\u001b8\u001b[2K\u001b[G\u001b[2K\u001b[1G\u001b[36m?\u001b[39m \u001b[1mSave file to\u001b[22m \u001b[90m›\u001b[39m /private/tmp/eas-contro\u001b7\u001b8\u001b[2K\u001b[G\u001b[2K\u001b[1G\u001b[36m?\u001b[39m \u001b[1mSave file to\u001b[22m \u001b[90m›\u001b[39m /private/tmp/eas-control\u001b7\u001b8\u001b[2K\u001b[G\u001b[2K\u001b[1G\u001b[36m?\u001b[39m \u001b[1mSave file to\u001b[22m \u001b[90m›\u001b[39m /private/tmp/eas-control-\u001b7\u001b8\u001b[2K\u001b[G\u001b[2K\u001b[1G\u001b[36m?\u001b[39m \u001b[1mSave file to\u001b[22m \u001b[90m›\u001b[39m /private/tmp/eas-control-c\u001b7\u001b8\u001b[2K\u001b[G\u001b[2K\u001b[1G\u001b[36m?\u001b["]
[24.223494875011966, "o", "39m \u001b[1mSave file to\u001b[22m \u001b[90m›\u001b[39m /private/tmp/eas-control-co\u001b7\u001b8\u001b[2K\u001b[G\u001b[2K\u001b[1G\u001b[36m?\u001b[39m \u001b[1mSave file to\u001b[22m \u001b[90m›\u001b[39m /private/tmp/eas-control-con\u001b7\u001b8\u001b[2K\u001b[G\u001b[2K\u001b[1G\u001b[36m?\u001b[39m \u001b[1mSave file to\u001b[22m \u001b[90m›\u001b[39m /private/tmp/eas-control-cont\u001b7\u001b8\u001b[2K\u001b[G\u001b[2K\u001b[1G\u001b[36m?\u001b[39m \u001b[1mSave file to\u001b[22m \u001b[90m›\u001b[39m /private/tmp/eas-control-conte\u001b7\u001b8\u001b[2K\u001b[G\u001b[2K\u001b[1G\u001b[36m?\u001b[39m \u001b[1mSave file to\u001b[22m \u001b[90m›\u001b[39m /private/tmp/eas-control-contex\u001b7\u001b8\u001b[2K\u001b[G\u001b[2K\u001b[1G\u001b[36m?\u001b[39m \u001b[1mSave file to\u001b[22m \u001b[90m›\u001b[39m /private/tmp/eas-control-context\u001b7\u001b8\u001b[2K\u001b[G\u001b[2K\u001b[1G\u001b[36m?\u001b[39m \u001b[1mSave file to\u001b[22m \u001b[90m›\u001b[39m /private/tmp/eas-control-context-\u001b7\u001b8\u001b[2K\u001b[G\u001b[2K\u001b[1G\u001b[36m?\u001b[39m \u001b[1mSave file to\u001b[22m \u001b[90m›\u001b[39m /private/tmp/eas-control-context-2\u001b7\u001b8\u001b[2K\u001b[G\u001b[2K\u001b[1G\u001b[36m?\u001b[39m \u001b[1mSave file to\u001b[22m \u001b[90m›\u001b[39m /private/tmp/eas-control-context-20\u001b7\u001b8\u001b[2K\u001b[G\u001b[2K\u001b[1G\u001b[36m?\u001b[39m \u001b[1mSave file to\u001b[22m \u001b[90m›\u001b[39m /private/tmp/eas-control-context-202\u001b7\u001b8\u001b[2K\u001b[G\u001b[2K\u001b[1G\u001b[36m?\u001b[39m \u001b[1mSave file to\u001b[22m \u001b[90m›\u001b[39m"]
[24.224985542008653, "o", " /private/tmp/eas-control-context-2026\u001b7\u001b8\u001b[2K\u001b[G\u001b[2K\u001b[1G\u001b[36m?\u001b[39m \u001b[1mSave file to\u001b[22m \u001b[90m›\u001b[39m /private/tmp/eas-control-context-20261\u001b7\u001b8\u001b[2K\u001b[G\u001b[2K\u001b[1G\u001b[36m?\u001b[39m \u001b[1mSave file to\u001b[22m \u001b[90m›\u001b[39m /private/tmp/eas-control-context-202610\u001b7\u001b8\u001b[2K\u001b[G\u001b[2K\u001b[1G\u001b[36m?\u001b[39m \u001b[1mSave file to\u001b[22m \u001b[90m›\u001b[39m /private/tmp/eas-control-context-2026101\u001b7\u001b8\u001b[2K\u001b[G\u001b[2K\u001b[1G\u001b[36m?\u001b[39m \u001b[1mSave file to\u001b[22m \u001b[90m›\u001b[39m /private/tmp/eas-control-context-20261017\u001b7\u001b8\u001b[2K\u001b[G\u001b[2K\u001b[1G\u001b[36m?\u001b[39m \u001b[1mSave file to\u001b[22m \u001b[90m›\u001b[39m /private/tmp/eas-control-context-20261017/\u001b7\u001b8\u001b[2K\u001b[G\u001b[2K\u001b[1G\u001b[36m?\u001b[39m \u001b[1mSave file to\u001b[22m \u001b[90m›\u001b[39m /private/tmp/eas-control-context-20261017/i\u001b7\u001b8\u001b[2K\u001b[G\u001b[2K\u001b[1G\u001b[36m?\u001b[39m \u001b[1mSave file to\u001b[22m \u001b[90m›\u001b[39m /private/tmp/eas-control-context-20261017/in\u001b7\u001b8\u001b[2K\u001b[G\u001b[2K\u001b[1G\u001b[36m?\u001b[39m \u001b[1mSave file to\u001b[22m \u001b[90m›\u001b[39m /private/tmp/eas-control-context-20261017/int\u001b7\u001b8\u001b[2K\u001b[G\u001b[2K\u001b[1G\u001b[36m?\u001b[39m \u001b[1mSave file to\u001b[22m \u001b[90m›\u001b[39m /private/tmp/eas-control-context-20261017/inte\u001b7\u001b8\u001b"]
[24.226475458825007, "o", "[2K\u001b[G\u001b[2K\u001b[1G\u001b[36m?\u001b[39m \u001b[1mSave file to\u001b[22m \u001b[90m›\u001b[39m /private/tmp/eas-control-context-20261017/inter\u001b7\u001b8\u001b[2K\u001b[G\u001b[2K\u001b[1G\u001b[36m?\u001b[39m \u001b[1mSave file to\u001b[22m \u001b[90m›\u001b[39m /private/tmp/eas-control-context-20261017/intera\u001b7\u001b8\u001b[2K\u001b[G\u001b[2K\u001b[1G\u001b[36m?\u001b[39m \u001b[1mSave file to\u001b[22m \u001b[90m›\u001b[39m /private/tmp/eas-control-context-20261017/interac\u001b7\u001b8\u001b[2K\u001b[G\u001b[2K\u001b[1G\u001b[36m?\u001b[39m \u001b[1mSave file to\u001b[22m \u001b[90m›\u001b[39m /private/tmp/eas-control-context-20261017/interact\u001b7\u001b8\u001b[2K\u001b[G\u001b[2K\u001b[1G\u001b[36m?\u001b[39m \u001b[1mSave file to\u001b[22m \u001b[90m›\u001b[39m /private/tmp/eas-control-context-20261017/interacti\u001b7\u001b8\u001b[2K\u001b[G\u001b[2K\u001b[1G\u001b[36m?\u001b[39m \u001b[1mSave file to\u001b[22m \u001b[90m›\u001b[39m /private/tmp/eas-control-context-20261017/interactiv\u001b7\u001b8\u001b[2K\u001b[G\u001b[2K\u001b[1G\u001b[36m?\u001b[39m \u001b[1mSave file to\u001b[22m \u001b[90m›\u001b[39m /private/tmp/eas-control-context-20261017/interactive\u001b7\u001b8\u001b[2K\u001b[G\u001b[2K\u001b[1G\u001b[36m?\u001b[39m \u001b[1mSave file to\u001b[22m \u001b[90m›\u001b[39m /private/tmp/eas-control-context-20261017/interactive-\u001b7\u001b8\u001b[2K\u001b[G\u001b[2K\u001b[1G\u001b[36m?\u001b[39m \u001b[1mSave file to\u001b[22m \u001b[90m›\u001b[39m /private/tmp/eas-contr"]
[24.227959000039846, "o", "ol-context-20261017/interactive-s\u001b7\u001b8\u001b[2K\u001b[G\u001b[2K\u001b[1G\u001b[36m?\u001b[39m \u001b[1mSave file to\u001b[22m \u001b[90m›\u001b[39m /private/tmp/eas-control-context-20261017/interactive-sc\u001b7\u001b8\u001b[2K\u001b[G\u001b[2K\u001b[1G\u001b[36m?\u001b[39m \u001b[1mSave file to\u001b[22m \u001b[90m›\u001b[39m /private/tmp/eas-control-context-20261017/interactive-scr\u001b7\u001b8\u001b[2K\u001b[G\u001b[2K\u001b[1G\u001b[36m?\u001b[39m \u001b[1mSave file to\u001b[22m \u001b[90m›\u001b[39m /private/tmp/eas-control-context-20261017/interactive-scre\u001b7\u001b8\u001b[2K\u001b[G\u001b[2K\u001b[1G\u001b[36m?\u001b[39m \u001b[1mSave file to\u001b[22m \u001b[90m›\u001b[39m /private/tmp/eas-control-context-20261017/interactive-scree\u001b7\u001b8\u001b[2K\u001b[G\u001b[2K\u001b[1G\u001b[36m?\u001b[39m \u001b[1mSave file to\u001b[22m \u001b[90m›\u001b[39m /private/tmp/eas-control-context-20261017/interactive-screen\u001b7\u001b8\u001b[2K\u001b[G\u001b[2K\u001b[1G\u001b[36m?\u001b[39m \u001b[1mSave file to\u001b[22m \u001b[90m›\u001b[39m /private/tmp/eas-control-context-20261017/interactive-screen.\u001b7\u001b8\u001b[2K\u001b[G\u001b[2K\u001b[1G\u001b[36m?\u001b[39m \u001b[1mSave file to\u001b[22m \u001b[90m›\u001b[39m /private/tmp/eas-control-context-20261017/interactive-screen.p\u001b7\u001b8\u001b[2K\u001b[G\u001b[2K\u001b[1G\u001b[36m?\u001b[39m \u001b[1mSave file to\u001b[22m \u001b[90m›\u001b[39m /private/tmp/eas-control-context-20261017/"]
[24.22921920893714, "o", "interactive-screen.pn\u001b7\u001b8\u001b[2K\u001b[G\u001b[2K\u001b[1G\u001b[36m?\u001b[39m \u001b[1mSave file to\u001b[22m \u001b[90m›\u001b[39m /private/tmp/eas-control-context-20261017/interactive-screen.png\u001b7\u001b8\u001b[2K\u001b[G\u001b[2K\u001b[1G\u001b[32m✔\u001b[39m \u001b[1mSave file to\u001b[22m \u001b[90m…\u001b[39m /private/tmp/eas-control-context-20261017/interactive-screen.png\u001b7\u001b8\r\n\u001b[?25h"]
[24.230243084020913, "o", "\u001b[?25l"]
[24.231167499907315, "o", "\u001b[36m⠋\u001b[39m Running screenshot.capture"]
[24.310396749991924, "o", "\u001b[2K\u001b[1G\u001b[36m⠙\u001b[39m Running screenshot.capture"]
[24.391523167025298, "o", "\u001b[2K\u001b[1G\u001b[36m⠹\u001b[39m Running screenshot.capture"]
[24.472665374865755, "o", "\u001b[2K\u001b[1G\u001b[36m⠸\u001b[39m Running screenshot.capture"]
[24.55382329202257, "o", "\u001b[2K\u001b[1G\u001b[36m⠼\u001b[39m Running screenshot.capture"]
[24.635289500001818, "o", "\u001b[2K\u001b[1G\u001b[36m⠴\u001b[39m Running screenshot.capture"]
[24.716552374884486, "o", "\u001b[2K\u001b[1G\u001b[36m⠦\u001b[39m Running screenshot.capture"]
[24.797744541894644, "o", "\u001b[2K\u001b[1G\u001b[36m⠧\u001b[39m Running screenshot.capture"]
[24.877844041911885, "o", "\u001b[2K\u001b[1G\u001b[36m⠇\u001b[39m Running screenshot.capture"]
[24.959062083857134, "o", "\u001b[2K\u001b[1G\u001b[36m⠏\u001b[39m Running screenshot.capture"]
[25.039228208828717, "o", "\u001b[2K\u001b[1G\u001b[36m⠋\u001b[39m Running screenshot.capture"]
[25.119210624834523, "o", "\u001b[2K\u001b[1G\u001b[36m⠙\u001b[39m Running screenshot.capture"]
[25.135079249972478, "o", "\u001b[2K\u001b[1G\u001b[?25h"]
[25.15396633394994, "o", "\u001b[32m✔\u001b[39m screenshot.capture complete · <1s\r\nSaved /private/tmp/eas-control-context-20261017/interactive-screen.png\r\n\r\n"]
[25.155244124820456, "o", "\u001b[?25l\u001b[2K\u001b[1G\u001b[36m?\u001b[39m \u001b[1mChoose a simulator action\u001b[22m \u001b[90m…\u001b[39m \r\n\u001b[90mno matches found\u001b[39m\u001b[2K\u001b[1A\u001b[2K\u001b[G\u001b[2K\u001b[1G\u001b[36m?\u001b[39m \u001b[1mChoose a simulator action\u001b[22m \u001b[90m›\u001b[39m \r\n\u001b[36m❯\u001b[39m \u001b[36m\u001b[4mExit\u001b[39m\u001b[24m\u001b[90m - Close simulator controls\u001b[39m\r\n App › Container\u001b[90m - app.container\u001b[39m\r\n App › File › List\u001b[90m - app.file.list\u001b[39m\r\n App › File › Read\u001b[90m - app.file.read\u001b[39m\r\n App › File › Remove\u001b[90m - app.file.remove\u001b[39m\r\n App › File › Upload\u001b[90m - app.file.upload\u001b[39m\r\n App › Info plist\u001b[90m - app.infoPlist\u001b[39m\r\n ↓ App › Install\u001b[90m - app.install\u001b[39m"]
[26.19365404197015, "i", "\r"]
[26.194113416830078, "o", "\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[G\u001b[2K\u001b[1G\u001b[32m✔\u001b[39m \u001b[1mChoose a simulator action\u001b[22m \u001b[90m›\u001b[39m Exit\r\n\u001b[?25h"]
[26.211439333856106, "o", "\u001b[90mSimulator controls closed.\u001b[39m\r\n\u001b[?25h"]
BIN
View File
Binary file not shown.
BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 61 KiB

-15
View File
@@ -1,15 +0,0 @@
import type { Config } from 'jest';
// eslint-disable-next-line import/no-relative-packages
import easCliConfig from './packages/eas-cli/jest.config';
// eslint-disable-next-line import/no-relative-packages
import easJsonConfig from './packages/eas-json/jest.config';
const config: Config = {
projects: [easCliConfig, easJsonConfig],
testPathIgnorePatterns: ['.*'],
watchPlugins: ['jest-watch-typeahead/filename', 'jest-watch-typeahead/testname'],
collectCoverageFrom: ['<rootDir>/src/**/*.{ts,tsx,js,jsx}'],
};
export default config;
-9
View File
@@ -1,9 +0,0 @@
import type { Config } from 'jest';
const config: Config = {
preset: 'ts-jest',
testEnvironment: 'node',
testRegex: '/__tests__/.*(test|spec)\\.[jt]sx?$',
};
export default config;
-5
View File
@@ -1,5 +0,0 @@
{
"npmClient": "yarn",
"version": "10.0.2",
"$schema": "node_modules/lerna/schemas/lerna-schema.json"
}
-41
View File
@@ -1,41 +0,0 @@
{
"name": "eas-cli-root",
"private": true,
"workspaces": [
"packages/*"
],
"scripts": {
"build": "lerna run build",
"build:tarballs:linux": "yarn build-for-size-check",
"build-for-size-check": "yarn build && yarn workspace eas-cli pretarball-ci && CLI_SIZE_CHECK=1 yarn workspace eas-cli oclif pack:tarballs --no-xz --targets linux-x64",
"typecheck": "lerna run typecheck",
"start": "lerna run watch --stream --parallel",
"start-allow-unused": "lerna run watch-allow-unused --stream --parallel",
"watch": "yarn start",
"eas": "packages/eas-cli/bin/run",
"lint": "eslint .",
"lint-changelog": "./scripts/bin/run lint-changelog",
"release": "lerna run rebuild && ./scripts/bin/run release",
"test": "jest",
"clean": "lerna run clean && rm -rf node_modules coverage"
},
"devDependencies": {
"@types/jest": "29.5.11",
"@types/node": "20.11.0",
"eslint": "8.56.0",
"eslint-config-universe": "12.0.0",
"eslint-plugin-async-protect": "3.1.0",
"eslint-plugin-node": "11.1.0",
"jest": "29.7.0",
"jest-watch-typeahead": "2.2.2",
"lerna": "7.4.2",
"oclif": "^3.9.0",
"prettier": "3.1.1",
"ts-jest": "29.1.1",
"typescript": "5.3.3"
},
"volta": {
"node": "20.11.0",
"yarn": "1.22.21"
}
}
-64
View File
@@ -1,64 +0,0 @@
const rootEslintrc = require('../../.eslintrc.js');
module.exports = {
rules: {
'no-restricted-properties': [
'warn',
{
object: 'it',
property: 'only',
message: 'it.only should not be committed to main.',
},
{
object: 'test',
property: 'only',
message: 'test.only should not be committed to main.',
},
{
object: 'describe',
property: 'only',
message: 'describe.only should not be committed to main.',
},
],
'no-restricted-imports': [
'error',
{
paths: [
...rootEslintrc.rules['no-restricted-imports'][1].paths,
{
name: 'ora',
message: 'Import ora from src/ora.ts instead.',
},
],
},
],
'graphql/template-strings': [
'error',
{
env: 'apollo',
schemaJson: require('./graphql.schema.json'),
},
],
'graphql/named-operations': [
'error',
{
schemaJson: require('./graphql.schema.json'),
},
],
'graphql/required-fields': [
'error',
{
env: 'apollo',
schemaJson: require('./graphql.schema.json'),
requiredFields: ['id'],
},
],
'graphql/capitalized-type-name': [
'error',
{
schemaJson: require('./graphql.schema.json'),
},
],
},
plugins: ['graphql'],
};
-2
View File
@@ -1,2 +0,0 @@
graphql.schema.json linguist-generated
src/graphql/generated.ts linguist-generated
File diff suppressed because it is too large. Load diff
-14
View File
@@ -1,14 +0,0 @@
import { fs } from 'memfs';
// needed because of a weird bug with tempy (dependency of @expo/config-plugins)
// eslint-disable-next-line node/no-sync
fs.mkdirSync('/tmp');
if (process.env.TMPDIR) {
// eslint-disable-next-line node/no-sync
fs.mkdirSync(process.env.TMPDIR, { recursive: true });
}
// fs-extra@10 is not compatible with memfs, the following line fixes tests
(fs.realpath as any).native = jest.fn();
module.exports = fs;
-1
View File
@@ -1 +0,0 @@
export default jest.fn(() => ({}));
-17
View File
@@ -1,17 +0,0 @@
#!/usr/bin/env node
const oclif = require('@oclif/core');
const path = require('path');
const project = path.join(__dirname, '..', 'tsconfig.json');
// In dev mode -> use ts-node and dev plugins
process.env.NODE_ENV = 'development';
require('ts-node').register({ project });
// In dev mode, always show stack traces
oclif.settings.disableJsonFlag = true
// Start the CLI
oclif.run().then(oclif.flush).catch(oclif.Errors.handle);
-3
View File
@@ -1,3 +0,0 @@
@echo off
node "%~dp0\dev" %*
-6
View File
@@ -1,6 +0,0 @@
#!/usr/bin/env node
require('@oclif/core')
.run()
.then(require('@oclif/core/flush'))
.catch(require('@oclif/core/handle'));
-3
View File
@@ -1,3 +0,0 @@
@echo off
node "%~dp0\run" %*
-22
View File
@@ -1,22 +0,0 @@
overwrite: true
schema: 'https://staging-api.expo.dev/graphql'
documents:
- 'src/graphql/**/!(*.d).{ts,tsx}'
- 'src/credentials/ios/api/graphql/**/!(*.d).{ts,tsx}'
- 'src/credentials/android/api/graphql/**/!(*.d).{ts,tsx}'
- 'src/commands/**/*.ts'
- 'src/branch/**/*.ts'
- 'src/channel/**/*.ts'
generates:
src/graphql/generated.ts:
plugins:
- 'typescript'
- 'typescript-operations'
config:
dedupeOperationSuffix: true
hooks:
afterOneFileWrite:
- 'node ./scripts/annotate-graphql-codegen.js'
./graphql.schema.json:
plugins:
- 'introspection'
File diff suppressed because it is too large. Load diff
-14
View File
@@ -1,14 +0,0 @@
import type { Config } from 'jest';
// eslint-disable-next-line import/no-relative-packages
import baseConfig from '../../jest/jest.config';
const config: Config = {
...baseConfig,
rootDir: __dirname,
roots: ['src', '__mocks__'],
globalSetup: './jest/global-setup.ts',
setupFilesAfterEnv: ['./jest/setup-after-env.ts'],
};
export default config;
-6
View File
@@ -1,6 +0,0 @@
module.exports = async () => {
process.env.TZ = 'UTC';
process.env.EAS_NO_VCS = '1';
process.env.EAS_PROJECT_ROOT = '/app';
process.env.FORCE_COLOR = '1';
};
-6
View File
@@ -1,6 +0,0 @@
/* eslint-disable no-console */
beforeAll(() => {
console.log = jest.fn();
console.warn = jest.fn();
console.error = jest.fn();
});
-226
View File
@@ -1,226 +0,0 @@
{
"name": "eas-cli",
"description": "EAS command line tool",
"version": "10.0.2",
"author": "Expo <[email protected]>",
"bin": {
"eas": "./bin/run"
},
"bugs": "https://github.com/expo/eas-cli/issues",
"dependencies": {
"@expo/apple-utils": "1.7.0",
"@expo/code-signing-certificates": "0.0.5",
"@expo/config": "8.5.4",
"@expo/config-plugins": "7.8.4",
"@expo/config-types": "50.0.0",
"@expo/eas-build-job": "1.0.117",
"@expo/eas-json": "10.0.0",
"@expo/json-file": "8.2.37",
"@expo/logger": "1.0.117",
"@expo/multipart-body-parser": "1.1.0",
"@expo/osascript": "2.0.33",
"@expo/package-manager": "1.1.2",
"@expo/pkcs12": "0.0.8",
"@expo/plist": "0.0.20",
"@expo/plugin-help": "5.1.23",
"@expo/plugin-warn-if-update-available": "2.5.1",
"@expo/prebuild-config": "6.7.3",
"@expo/results": "1.0.0",
"@expo/rudder-sdk-node": "1.1.1",
"@expo/spawn-async": "1.7.0",
"@expo/steps": "1.0.118",
"@expo/timeago.js": "1.0.0",
"@oclif/core": "^1.26.2",
"@oclif/plugin-autocomplete": "^2.3.10",
"@segment/ajv-human-errors": "^2.1.2",
"@urql/core": "4.0.11",
"@urql/exchange-retry": "1.2.0",
"ajv": "8.11.0",
"ajv-formats": "2.1.1",
"better-opn": "3.0.2",
"chalk": "4.1.2",
"cli-progress": "3.12.0",
"dateformat": "4.6.3",
"dotenv": "16.3.1",
"env-paths": "2.2.0",
"envinfo": "7.11.0",
"fast-deep-equal": "3.1.3",
"fast-glob": "3.3.2",
"figures": "3.2.0",
"form-data": "4.0.0",
"fs-extra": "11.2.0",
"getenv": "1.0.0",
"gradle-to-js": "2.0.1",
"graphql": "16.8.1",
"graphql-tag": "2.12.6",
"https-proxy-agent": "5.0.1",
"ignore": "5.3.0",
"indent-string": "4.0.0",
"jks-js": "1.1.0",
"joi": "17.11.0",
"jsonwebtoken": "9.0.0",
"keychain": "1.5.0",
"log-symbols": "4.1.0",
"mime": "3.0.0",
"minimatch": "5.1.2",
"nanoid": "3.3.4",
"node-fetch": "2.6.7",
"node-forge": "1.3.1",
"nullthrows": "1.1.1",
"ora": "5.1.0",
"pkg-dir": "4.2.0",
"pngjs": "7.0.0",
"promise-limit": "2.7.0",
"promise-retry": "2.0.1",
"prompts": "2.4.2",
"qrcode-terminal": "0.12.0",
"resolve-from": "5.0.0",
"semver": "7.5.4",
"slash": "3.0.0",
"tar": "6.2.1",
"terminal-link": "2.1.1",
"tslib": "2.6.2",
"turndown": "7.1.2",
"untildify": "4.0.0",
"uuid": "9.0.1",
"wrap-ansi": "7.0.0"
},
"devDependencies": {
"@graphql-codegen/cli": "5.0.0",
"@graphql-codegen/introspection": "4.0.0",
"@graphql-codegen/typescript": "4.0.1",
"@graphql-codegen/typescript-operations": "4.0.1",
"@types/cli-progress": "3.11.5",
"@types/dateformat": "3.0.1",
"@types/envinfo": "7.8.3",
"@types/express": "4.17.15",
"@types/fs-extra": "11.0.4",
"@types/getenv": "^1.0.0",
"@types/jsonwebtoken": "8.5.1",
"@types/mime": "3.0.1",
"@types/node-fetch": "2.6.2",
"@types/node-forge": "1.3.1",
"@types/pngjs": "6.0.4",
"@types/promise-retry": "1.1.3",
"@types/prompts": "2.4.2",
"@types/semver": "7.5.6",
"@types/tar": "6.1.10",
"@types/tough-cookie": "4.0.2",
"@types/uuid": "9.0.7",
"@types/wrap-ansi": "3.0.0",
"axios": "0.27.2",
"eslint-plugin-graphql": "4.0.0",
"express": "4.19.2",
"memfs": "3.4.13",
"mockdate": "3.0.5",
"nock": "13.4.0",
"rimraf": "3.0.2",
"ts-deepmerge": "6.2.0",
"ts-mockito": "2.6.1",
"ts-node": "10.9.2",
"typescript": "5.3.3"
},
"engines": {
"node": ">=18.0.0"
},
"files": [
"/bin",
"/build",
"/oclif.manifest.json",
"/schema"
],
"homepage": "https://github.com/expo/eas-cli",
"keywords": [
"expo",
"eas",
"cli"
],
"license": "MIT",
"oclif": {
"bin": "eas",
"commands": "./build/commands",
"plugins": [
"@oclif/plugin-autocomplete",
"@expo/plugin-help",
"@expo/plugin-warn-if-update-available"
],
"topics": {
"account": {
"description": "manage account"
},
"branch": {
"description": "manage update branches"
},
"build": {
"description": "build app binaries"
},
"channel": {
"description": "manage update channels"
},
"device": {
"description": "manage Apple devices for Internal Distribution"
},
"metadata": {
"description": "manage store configuration"
},
"project": {
"description": "manage project"
},
"release": {
"description": "manage update releases"
},
"secret": {
"description": "manage project and account secrets"
},
"update": {
"description": "manage individual updates"
},
"webhook": {
"description": "manage webhooks"
}
},
"warn-if-update-available": {
"timeoutInDays": 0.5,
"message": "<%= chalk('★') %> <%= chalk.bold(config.name + '@' + latest) %> <%= chalk.bold('is now available.') %>\n<%= chalk.dim('To upgrade, run ') %><%= chalk.dim(chalk.bold('npm install -g eas-cli')) %><%= chalk.dim('.') %>\n<%= chalk.dim('Proceeding with outdated version.') %>\n"
},
"update": {
"node": {
"version": "18.6.0"
},
"s3": {
"templates": {
"target": {
"unversioned": "<%- bin %>-<%- platform %>-<%- arch %><%- ext %>",
"versioned": "<%- bin %>-<%- platform %>-<%- arch %><%- ext %>"
}
}
}
},
"additionalHelpFlags": [
"-h"
],
"additionalVersionFlags": [
"-v"
]
},
"repository": "expo/eas-cli",
"scripts": {
"postpack": "rimraf oclif.manifest.json",
"prepack": "yarn rebuild && node ./scripts/prepack.js",
"rebuild": "rimraf build && yarn build",
"pretarball-ci": "./scripts/pretarball-ci.sh",
"build": "tsc --project tsconfig.build.json",
"build-allow-unused": "tsc --project tsconfig.allowUnused.json",
"watch": "yarn build --watch --preserveWatchOutput",
"watch-allow-unused": "yarn build-allow-unused --watch --preserveWatchOutput",
"typecheck": "tsc",
"test": "jest",
"version": "yarn oclif readme && node scripts/patch-readme && git add README.md",
"generate-graphql-code": "graphql-codegen --config graphql-codegen.yml",
"clean": "rimraf dist build tmp node_modules yarn-error.log"
},
"volta": {
"node": "20.11.0",
"yarn": "1.22.21"
}
}
-793
View File
@@ -1,793 +0,0 @@
{
"$schema": "http://json-schema.org/draft-06/schema",
"type": "object",
"definitions": {
"apple": {
"AppleKidsAge": {
"enum": [
"FIVE_AND_UNDER",
"SIX_TO_EIGHT",
"NINE_TO_ELEVEN"
]
},
"AppleRating": {
"enum": [
"NONE",
"INFREQUENT_OR_MILD",
"FREQUENT_OR_INTENSE"
]
},
"AppleCategory": {
"type": "array",
"description": "App Store categories for the app, can add up to two categories. The first item in this list is the primary category, the second is the secondary and optional category. `STICKERS` and `GAMES` categories can add up to two additional subcategories, written as nested array.",
"markdownDescription": "App Store categories for the app, can add up to two categories. The first item in this list is the primary category, the second is the secondary and optional category. `STICKERS` and `GAMES` categories can add up to two additional subcategories, written as nested array. E.g.\n\n- `[\"SPORTS\"]`\n- `[\"SPORTS\", \"LIFESTYLE\"]`\n- `[[\"GAMES\", \"GAMES_CARD\"], \"ENTERTAINMENT\"]`\n- `[\"GAMES\", \"WEATHER\"]`",
"minItems": 1,
"maxItems": 2,
"uniqueItems": true,
"defaultSnippets": [
{
"label": "Basic categories",
"description": "Add two simple categories",
"body": [
"$1",
"$2"
]
},
{
"label": "Category with subcategories",
"description": "Add a category containing subcategories",
"body": [
[
"${1|GAMES,STICKERS|}",
"$3",
"$4"
]
]
}
],
"items": {
"oneOf": [
{
"$ref": "#/definitions/apple/AppleCategory/categories/root"
},
{
"$ref": "#/definitions/apple/AppleCategory/categories/parent"
},
{
"type": "array",
"description": "The GAMES category can have additional subcategories",
"minItems": 1,
"maxItems": 3,
"uniqueItems": true,
"items": [
{
"const": "GAMES"
},
{
"$ref": "#/definitions/apple/AppleCategory/categories/games"
},
{
"$ref": "#/definitions/apple/AppleCategory/categories/games"
}
]
},
{
"type": "array",
"description": "The STICKERS category can have additional subcategories",
"minItems": 1,
"maxItems": 3,
"uniqueItems": true,
"items": [
{
"const": "STICKERS"
},
{
"$ref": "#/definitions/apple/AppleCategory/categories/stickers"
},
{
"$ref": "#/definitions/apple/AppleCategory/categories/stickers"
}
]
}
]
},
"categories": {
"root": {
"enum": [
"BUSINESS",
"SOCIAL_NETWORKING",
"MEDICAL",
"FOOD_AND_DRINK",
"EDUCATION",
"BOOKS",
"SPORTS",
"FINANCE",
"REFERENCE",
"GRAPHICS_AND_DESIGN",
"DEVELOPER_TOOLS",
"HEALTH_AND_FITNESS",
"MUSIC",
"WEATHER",
"TRAVEL",
"ENTERTAINMENT",
"LIFESTYLE",
"MAGAZINES_AND_NEWSPAPERS",
"UTILITIES",
"SHOPPING",
"PRODUCTIVITY",
"NEWS",
"PHOTO_AND_VIDEO",
"NAVIGATION"
]
},
"parent": {
"enum": [
"GAMES",
"STICKERS"
]
},
"games": {
"enum": [
"GAMES_SPORTS",
"GAMES_WORD",
"GAMES_MUSIC",
"GAMES_ADVENTURE",
"GAMES_ACTION",
"GAMES_ROLE_PLAYING",
"GAMES_CASUAL",
"GAMES_BOARD",
"GAMES_TRIVIA",
"GAMES_CARD",
"GAMES_PUZZLE",
"GAMES_CASINO",
"GAMES_STRATEGY",
"GAMES_SIMULATION",
"GAMES_RACING",
"GAMES_FAMILY"
]
},
"stickers": {
"enum": [
"STICKERS_PLACES_AND_OBJECTS",
"STICKERS_EMOJI_AND_EXPRESSIONS",
"STICKERS_CELEBRATIONS",
"STICKERS_CELEBRITIES",
"STICKERS_MOVIES_AND_TV",
"STICKERS_SPORTS_AND_ACTIVITIES",
"STICKERS_EATING_AND_DRINKING",
"STICKERS_CHARACTERS",
"STICKERS_ANIMALS",
"STICKERS_FASHION",
"STICKERS_ART",
"STICKERS_GAMING",
"STICKERS_KIDS_AND_FAMILY",
"STICKERS_PEOPLE",
"STICKERS_MUSIC"
]
}
}
},
"AppleInfo": {
"type": "object",
"additionalProperties": false,
"required": [
"title",
"privacyPolicyUrl"
],
"defaultSnippets": [
{
"label": "Basic",
"description": "Create the minimum required locale properties",
"body": {
"title": "${1:App title}",
"privacyPolicyUrl": "https://${3:www.}"
}
},
{
"label": "Full",
"description": "Create all available locale properties",
"body": {
"title": "${1:App title}",
"subtitle": "${2:Subtitle for your app}",
"description": "${3:A longer description of what your app does}",
"keywords": [
"${4:keyword}"
],
"releaseNotes": "${5:Bug fixes and improved stability}",
"promoText": "${6:Short tagline for your app}",
"marketingUrl": "https://${7:www.}",
"supportUrl": "https://${8:www.}",
"privacyPolicyUrl": "https://${9:www.}",
"privacyChoicesUrl": "https://${10:www.}"
}
}
],
"properties": {
"title": {
"type": "string",
"description": "Name of the app in the store. This should be similar to the installed app name.",
"minLength": 2,
"maxLength": 30,
"meta": {
"storeInfo": "The name will be reviewed before it is made available on the App Store."
}
},
"subtitle": {
"type": "string",
"description": "Subtext for the app. Example: 'A Fun Game For Friends'",
"maxLength": 30,
"meta": {
"storeInfo": "The subtitle will be reviewed before it is made available on the App Store."
}
},
"description": {
"type": "string",
"description": "Main description of what the app does",
"minLength": 10,
"maxLength": 4000,
"meta": {
"versioned": true,
"liveEdits": true
}
},
"keywords": {
"type": "array",
"description": "List of keywords to help users find the app in the App Store",
"uniqueItems": true,
"items": {
"maxLength": 100,
"type": "string"
},
"meta": {
"versioned": true
}
},
"releaseNotes": {
"type": "string",
"description": "Changes since the last public version",
"maxLength": 4000,
"meta": {
"versioned": true,
"liveEdits": true
}
},
"promoText": {
"type": "string",
"description": "Short tagline for the app",
"maxLength": 170,
"meta": {
"versioned": true,
"liveEdits": true,
"aso": false
}
},
"marketingUrl": {
"type": "string",
"description": "URL to the app marketing page",
"maxLength": 255,
"meta": {
"versioned": true,
"liveEdits": true
}
},
"supportUrl": {
"type": "string",
"description": "URL to the app support page",
"maxLength": 255,
"meta": {
"versioned": true,
"liveEdits": true
}
},
"privacyPolicyText": {
"type": "string",
"description": "Privacy policy for Apple TV"
},
"privacyPolicyUrl": {
"type": "string",
"description": "A URL that links to your privacy policy. A privacy policy is required for all apps.",
"maxLength": 255,
"meta": {
"versioned": true,
"liveEdits": true
}
},
"privacyChoicesUrl": {
"type": "string",
"description": "A URL where users can modify and delete the data collected from the app, or decide how their data is used and shared.",
"maxLength": 255,
"meta": {
"versioned": true,
"liveEdits": true,
"optional": true
}
}
}
},
"AppleAdvisory": {
"type": "object",
"additionalProperties": false,
"required": [
"alcoholTobaccoOrDrugUseOrReferences",
"contests",
"gamblingSimulated",
"horrorOrFearThemes",
"matureOrSuggestiveThemes",
"medicalOrTreatmentInformation",
"profanityOrCrudeHumor",
"sexualContentGraphicAndNudity",
"sexualContentOrNudity",
"violenceCartoonOrFantasy",
"violenceRealistic",
"violenceRealisticProlongedGraphicOrSadistic",
"gambling",
"unrestrictedWebAccess",
"kidsAgeBand",
"seventeenPlus"
],
"defaultSnippets": [
{
"label": "Basic",
"description": "Create an advisory that disables all age-rated properties, for when your app does nothing",
"body": {
"alcoholTobaccoOrDrugUseOrReferences": "NONE",
"contests": "NONE",
"gamblingSimulated": "NONE",
"horrorOrFearThemes": "NONE",
"matureOrSuggestiveThemes": "NONE",
"medicalOrTreatmentInformation": "NONE",
"profanityOrCrudeHumor": "NONE",
"sexualContentGraphicAndNudity": "NONE",
"sexualContentOrNudity": "NONE",
"violenceCartoonOrFantasy": "NONE",
"violenceRealistic": "NONE",
"violenceRealisticProlongedGraphicOrSadistic": "NONE",
"gambling": false,
"unrestrictedWebAccess": false,
"kidsAgeBand": null,
"seventeenPlus": false
}
},
{
"label": "Full",
"description": "Create an advisory that enables some of the age-rated properties",
"body": {
"alcoholTobaccoOrDrugUseOrReferences": "${1|NONE,INFREQUENT_OR_MILD,FREQUENT_OR_INTENSE|}",
"contests": "${2|NONE,INFREQUENT_OR_MILD,FREQUENT_OR_INTENSE|}",
"gamblingSimulated": "${3|NONE,INFREQUENT_OR_MILD,FREQUENT_OR_INTENSE|}",
"horrorOrFearThemes": "${4|NONE,INFREQUENT_OR_MILD,FREQUENT_OR_INTENSE|}",
"matureOrSuggestiveThemes": "${5|NONE,INFREQUENT_OR_MILD,FREQUENT_OR_INTENSE|}",
"medicalOrTreatmentInformation": "${6|NONE,INFREQUENT_OR_MILD,FREQUENT_OR_INTENSE|}",
"profanityOrCrudeHumor": "${7|NONE,INFREQUENT_OR_MILD,FREQUENT_OR_INTENSE|}",
"sexualContentGraphicAndNudity": "${8|NONE,INFREQUENT_OR_MILD,FREQUENT_OR_INTENSE|}",
"sexualContentOrNudity": "${9|NONE,INFREQUENT_OR_MILD,FREQUENT_OR_INTENSE|}",
"violenceCartoonOrFantasy": "${10|NONE,INFREQUENT_OR_MILD,FREQUENT_OR_INTENSE|}",
"violenceRealistic": "${11|NONE,INFREQUENT_OR_MILD,FREQUENT_OR_INTENSE|}",
"violenceRealisticProlongedGraphicOrSadistic": "${12|NONE,INFREQUENT_OR_MILD,FREQUENT_OR_INTENSE|}",
"unrestrictedWebAccess": false,
"gambling": false,
"kidsAgeBand": null,
"seventeenPlus": false
}
}
],
"properties": {
"alcoholTobaccoOrDrugUseOrReferences": {
"$ref": "#/definitions/apple/AppleRating",
"description": "Does the app contain alcohol, tobacco, or drug use or references?"
},
"contests": {
"$ref": "#/definitions/apple/AppleRating",
"description": "Does the app contain contests?"
},
"gamblingSimulated": {
"$ref": "#/definitions/apple/AppleRating",
"description": "Does the app contain simulated gambling?"
},
"medicalOrTreatmentInformation": {
"$ref": "#/definitions/apple/AppleRating",
"description": "Does the app contain medical or treatment information?"
},
"profanityOrCrudeHumor": {
"$ref": "#/definitions/apple/AppleRating",
"description": "Does the app contain profanity or crude humor?"
},
"sexualContentGraphicAndNudity": {
"$ref": "#/definitions/apple/AppleRating",
"description": "Does the app contain graphic sexual content and nudity?"
},
"sexualContentOrNudity": {
"$ref": "#/definitions/apple/AppleRating",
"description": "Does the app contain sexual content or nudity?"
},
"horrorOrFearThemes": {
"$ref": "#/definitions/apple/AppleRating",
"description": "Does the app contain horror or fear themes?"
},
"matureOrSuggestiveThemes": {
"$ref": "#/definitions/apple/AppleRating",
"description": "Does the app contain mature or suggestive themes?"
},
"violenceCartoonOrFantasy": {
"$ref": "#/definitions/apple/AppleRating",
"description": "Does the app contain cartoon or fantasy violence?"
},
"violenceRealisticProlongedGraphicOrSadistic": {
"$ref": "#/definitions/apple/AppleRating",
"description": "Does the app contain prolonged graphic or sadistic realistic violence?"
},
"violenceRealistic": {
"$ref": "#/definitions/apple/AppleRating",
"description": "Does the app contain realistic violence?"
},
"unrestrictedWebAccess": {
"type": "boolean",
"description": "Does your app contain unrestricted web access, such as with an embedded browser?"
},
"gambling": {
"type": "boolean",
"description": "Does your app contain gambling?"
},
"kidsAgeBand": {
"oneOf": [
{
"type": "null"
},
{
"$ref": "#/definitions/apple/AppleKidsAge"
}
],
"description": "When parents visit the Kids category on the App Store, they expect the apps they find will protect their children’s data, provide only age-appropriate content, and require a parental gate in order to link out of the app, request permissions, or present purchasing opportunities. It’s critical that no personally identifiable information or device information be transmitted to third parties, and that advertisements are human-reviewed for age appropriateness in order to be displayed.\n@see https://developer-mdn.apple.com/news/?id=091202019a",
"markdownDescription": "When parents visit the Kids category on the App Store, they expect the apps they find will protect their children’s data, provide only age-appropriate content, and require a parental gate in order to link out of the app, request permissions, or present purchasing opportunities. It’s critical that no personally identifiable information or device information be transmitted to third parties, and that advertisements are human-reviewed for age appropriateness in order to be displayed.\n\n[Learn more](https://developer-mdn.apple.com/news/?id=091202019a)"
},
"seventeenPlus": {
"type": "boolean",
"description": "If your app rates 12+ or lower, and you believe its content may not be suitable for children under 17, you can manually set the age rating to 17+.\n@see https://help.apple.com/app-store-connect/#/dev599d50efb",
"markdownDescription": "If your app rates 12+ or lower, and you believe its content may not be suitable for children under 17, you can manually set the age rating to 17+.\n\n[Learn more](https://help.apple.com/app-store-connect/#/dev599d50efb)"
}
}
},
"AppleRelease": {
"type": "object",
"description": "Configure how the app is released in the app store.",
"additionalProperties": false,
"properties": {
"automaticRelease": {
"description": "If and how the app should automatically be released when approved by App Store review.",
"markdownDescription": "If and how the app should automatically be released when approved by App Store review.\n- `false` - Manually release the app after store approval. (default behavior)\n- `true` - Automatically release after store approval.\n- `Date` - Automatically schedule release on this date, after store approval.\n\nApple does not guarantee that your app is available at the chosen scheduled release date.",
"oneOf": [
{
"type": "string",
"format": "date-time"
},
{
"type": "boolean"
}
],
"defaultSnippets": [
{
"label": "Manual release",
"description": "Manually release the app after store approval",
"body": false
},
{
"label": "Automatic release",
"description": "Automatically release the app after store approval",
"body": true
},
{
"label": "Scheduled release",
"description": "Automatically release the app on this date, and store approval",
"body": "$CURRENT_YEAR-${1:$CURRENT_MONTH}-${2:$CURRENT_DATE}T${3:$CURRENT_HOUR}:00:00+00:00"
}
]
},
"phasedRelease": {
"type": "boolean",
"description": "Phased release for automatic updates lets you gradually release this update over a 7-day period to users who have turned on automatic updates. Keep in mind that this version will still be available to all users as a manual update from the App Store. You can pause the phased release for up to 30 days or release this update to all users at any time.\n@see https://help.apple.com/app-store-connect/#/dev3d65fcee1",
"markdownDescription": "Phased release for automatic updates lets you gradually release this update over a 7-day period to users who have turned on automatic updates.\n\nKeep in mind that this version will still be available to all users as a manual update from the App Store.\n\nYou can pause the phased release for up to 30 days or release this update to all users at any time.\n\n[Learn More](https://help.apple.com/app-store-connect/#/dev3d65fcee1)"
}
}
},
"AppleReview": {
"type": "object",
"description": "Info provided to the App Store review team to help them understand how to use your app.",
"required": [
"firstName",
"lastName",
"email",
"phone"
],
"defaultSnippets": [
{
"label": "Basic",
"description": "Only define the contact details in case communcation is needed with the App Store review team.",
"body": {
"firstName": "$1",
"lastName": "$2",
"email": "$3",
"phone": "+$4"
}
},
{
"label": "Full",
"description": "Define the demo account credentials, additional info, and contact details for the App Store review team.",
"body": {
"firstName": "$1",
"lastName": "$2",
"email": "$3",
"phone": "+$4",
"demoUsername": "$5",
"demoPassword": "$6",
"demoRequired": true,
"notes": "$7"
}
}
],
"properties": {
"firstName": {
"type": "string",
"description": "First name of contact in case communication is needed with the App Store review team.",
"minLength": 1
},
"lastName": {
"type": "string",
"description": "Last name of contact in case communication is needed with the App Store review team.",
"minLength": 1
},
"email": {
"type": "string",
"description": "Email address of contact in case communication is needed with the App Store review team.",
"format": "email"
},
"phone": {
"type": "string",
"description": "Phone number of contact in case communication is needed with the App Store review team. Preface the phone number with ‘+’ followed by the country code (for example, +44 844 209 0611)"
},
"demoUsername": {
"type": "string",
"description": "The user name to sign in to your app to review its features."
},
"demoPassword": {
"type": "string",
"description": "The password to sign in to your app to review its features."
},
"demoRequired": {
"type": "boolean",
"description": "A Boolean value that indicates if sign-in information is required to review all the features of your app. If users sign in using social media, provide information for an account for review. Credentials must be valid and active for duration of review.",
"markdownDescription": "A Boolean value that indicates if sign-in information is required to review all the features of your app. If users sign in using social media, provide information for an account for review.\n\n**Credentials must be valid and active for duration of review.**"
},
"notes": {
"type": "string",
"description": "Additional information about your app that can help during the review process. Do not include demo account details.",
"markdownDescription": "Additional information about your app that can help during the review process.\n\n**Do not include demo account details.**",
"minLength": 2,
"maxLength": 4000
}
}
}
},
"AppleConfig": {
"type": "object",
"description": "Configuration that is specific to the Apple App Store.",
"additionalProperties": false,
"properties": {
"version": {
"type": "string",
"description": "The version string to edit, or create, if it doesn't exist. All the metadata will be stored on this app version.",
"minLength": 1,
"defaultSnippets": [
{
"body": "${1:1}.${2:0}"
}
]
},
"copyright": {
"type": "string",
"description": "The updated company copyright. Example: 2021 Evan Bacon",
"defaultSnippets": [
{
"body": "$CURRENT_YEAR ${1:name}"
}
],
"meta": {
"versioned": true,
"liveEdits": true,
"storeInfo": "The name of the person or entity that owns the exclusive rights to your app, preceded by the year the rights were obtained (for example, \"2008 Acme Inc.\"). Do not provide a URL."
}
},
"advisory": {
"$ref": "#/definitions/apple/AppleAdvisory"
},
"categories": {
"$ref": "#/definitions/apple/AppleCategory"
},
"info": {
"description": "Localized core app info",
"type": "object",
"additionalProperties": false,
"properties": {
"ar-SA": {
"$ref": "#/definitions/apple/AppleInfo",
"description": "Arabic"
},
"ca": {
"$ref": "#/definitions/apple/AppleInfo",
"description": "Catalan"
},
"zh-Hans": {
"$ref": "#/definitions/apple/AppleInfo",
"description": "Chinese (Simplified)"
},
"zh-Hant": {
"$ref": "#/definitions/apple/AppleInfo",
"description": "Chinese (Traditional)"
},
"hr": {
"$ref": "#/definitions/apple/AppleInfo",
"description": "Croatian"
},
"cs": {
"$ref": "#/definitions/apple/AppleInfo",
"description": "Czech"
},
"da": {
"$ref": "#/definitions/apple/AppleInfo",
"description": "Danish"
},
"nl-NL": {
"$ref": "#/definitions/apple/AppleInfo",
"description": "Dutch"
},
"en-AU": {
"$ref": "#/definitions/apple/AppleInfo",
"description": "English (Australia)"
},
"en-CA": {
"$ref": "#/definitions/apple/AppleInfo",
"description": "English (Canada)"
},
"en-GB": {
"$ref": "#/definitions/apple/AppleInfo",
"description": "English (U.K.)"
},
"en-US": {
"$ref": "#/definitions/apple/AppleInfo",
"description": "English (U.S.)"
},
"fi": {
"$ref": "#/definitions/apple/AppleInfo",
"description": "Finnish"
},
"fr-FR": {
"$ref": "#/definitions/apple/AppleInfo",
"description": "French"
},
"fr-CA": {
"$ref": "#/definitions/apple/AppleInfo",
"description": "French (Canada)"
},
"de-DE": {
"$ref": "#/definitions/apple/AppleInfo",
"description": "German"
},
"el": {
"$ref": "#/definitions/apple/AppleInfo",
"description": "Greek"
},
"he": {
"$ref": "#/definitions/apple/AppleInfo",
"description": "Hebrew"
},
"hi": {
"$ref": "#/definitions/apple/AppleInfo",
"description": "Hindi"
},
"hu": {
"$ref": "#/definitions/apple/AppleInfo",
"description": "Hungarian"
},
"id": {
"$ref": "#/definitions/apple/AppleInfo",
"description": "Indonesian"
},
"it": {
"$ref": "#/definitions/apple/AppleInfo",
"description": "Italian"
},
"ja": {
"$ref": "#/definitions/apple/AppleInfo",
"description": "Japanese"
},
"ko": {
"$ref": "#/definitions/apple/AppleInfo",
"description": "Korean"
},
"ms": {
"$ref": "#/definitions/apple/AppleInfo",
"description": "Malay"
},
"no": {
"$ref": "#/definitions/apple/AppleInfo",
"description": "Norwegian"
},
"pl": {
"$ref": "#/definitions/apple/AppleInfo",
"description": "Polish"
},
"pt-BR": {
"$ref": "#/definitions/apple/AppleInfo",
"description": "Portuguese (Brazil)"
},
"pt-PT": {
"$ref": "#/definitions/apple/AppleInfo",
"description": "Portuguese (Portugal)"
},
"ro": {
"$ref": "#/definitions/apple/AppleInfo",
"description": "Romanian"
},
"ru": {
"$ref": "#/definitions/apple/AppleInfo",
"description": "Russian"
},
"sk": {
"$ref": "#/definitions/apple/AppleInfo",
"description": "Slovak"
},
"es-MX": {
"$ref": "#/definitions/apple/AppleInfo",
"description": "Spanish (Mexico)"
},
"es-ES": {
"$ref": "#/definitions/apple/AppleInfo",
"description": "Spanish (Spain)"
},
"sv": {
"$ref": "#/definitions/apple/AppleInfo",
"description": "Swedish"
},
"th": {
"$ref": "#/definitions/apple/AppleInfo",
"description": "Thai"
},
"tr": {
"$ref": "#/definitions/apple/AppleInfo",
"description": "Turkish"
},
"uk": {
"$ref": "#/definitions/apple/AppleInfo",
"description": "Ukrainian"
},
"vi": {
"$ref": "#/definitions/apple/AppleInfo",
"description": "Vietnamese"
}
}
},
"release": {
"$ref": "#/definitions/apple/AppleRelease",
"description": "Release strategy"
},
"review": {
"$ref": "#/definitions/apple/AppleReview",
"description": "Info for the App Store reviewer"
}
}
}
},
"properties": {
"configVersion": {
"enum": [0],
"description": "The EAS metadata store configuration version"
},
"apple": {
"$ref": "#/definitions/AppleConfig"
}
},
"additionalProperties": false,
"required": [
"configVersion",
"apple"
]
}
@@ -1,15 +0,0 @@
const fs = require('fs/promises');
const path = './src/graphql/generated.ts';
(async () => {
const generatedCode = await fs.readFile(path, 'utf8');
const comment = `/**
* This file was generated using GraphQL Codegen
* Command: yarn generate-graphql-code
* Run this during development for automatic type generation when editing GraphQL documents
* For more info and docs, visit https://graphql-code-generator.com/
*/\n\n`;
await fs.writeFile(path, comment + generatedCode);
})();
-11
View File
@@ -1,11 +0,0 @@
const fs = require('fs/promises');
(async () => {
// Patch `oclif readme` path and link generation
let readmeContent = await fs.readFile('README.md', 'utf8');
readmeContent = readmeContent.replace(/src\/commands/g, 'packages/eas-cli/src/commands');
await fs.writeFile('README.md', readmeContent);
// eslint-disable-next-line no-console
console.log('Patched README path generation');
})();
-7
View File
@@ -1,7 +0,0 @@
const spawn = require('@expo/spawn-async');
(async () => {
if (!process.env.CLI_SIZE_CHECK) {
await spawn('yarn', ['oclif', 'manifest'], { stdio: 'inherit' });
}
})();
-12
View File
@@ -1,12 +0,0 @@
#!/usr/bin/env bash
set -eo pipefail
EAS_CLI_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )"/.. && pwd )"
PACKAGE_JSON_PATH="$EAS_CLI_DIR/package.json"
if [[ "$OSTYPE" == "darwin"* ]]; then
sed -i '' "s/\\\"@expo\/eas-json\\\": \\\".*\\\"/\\\"@expo\/eas-json\\\": \\\"file:..\/..\/..\/eas-json\\\"/g" $PACKAGE_JSON_PATH
else
sed -i "s/\\\"@expo\/eas-json\\\": \\\".*\\\"/\\\"@expo\/eas-json\\\": \\\"file:..\/..\/..\/eas-json\\\"/g" $PACKAGE_JSON_PATH
fi
-23
View File
@@ -1,23 +0,0 @@
import { JSONValue } from '@expo/json-file';
export class ApiV2Error extends Error {
override readonly name = 'ApiV2Error';
readonly expoApiV2ErrorCode: string;
readonly expoApiV2ErrorDetails?: JSONValue;
readonly expoApiV2ErrorServerStack?: string;
readonly expoApiV2ErrorMetadata?: object;
constructor(response: {
message: string;
code: string;
stack?: string;
details?: JSONValue;
metadata?: object;
}) {
super(response.message);
this.expoApiV2ErrorCode = response.code;
this.expoApiV2ErrorDetails = response.details;
this.expoApiV2ErrorServerStack = response.stack;
this.expoApiV2ErrorMetadata = response.metadata;
}
}
-9
View File
@@ -1,9 +0,0 @@
export const ora = jest.fn().mockReturnValue({
start: jest.fn().mockReturnValue({
fail: jest.fn(),
succeed: jest.fn(),
}),
stop: jest.fn(),
stopAndPersist: jest.fn(),
succeed: jest.fn(),
});
@@ -1,56 +0,0 @@
import assert from 'assert';
import nock from 'nock';
import { ApiV2Error } from '../ApiV2Error';
import { ApiV2Client, getExpoApiBaseUrl } from '../api';
describe(ApiV2Client, () => {
it('converts Expo APIv2 error to ApiV2Error', async () => {
nock(getExpoApiBaseUrl())
.post('/v2/test')
.reply(400, {
errors: [
{
message: 'hellomessage',
code: 'TEST_CODE',
stack: 'line 1: hello',
details: { who: 'world' },
metadata: { an: 'object' },
},
],
});
let error: Error | null = null;
try {
await new ApiV2Client({ accessToken: null, sessionSecret: null }).postAsync('test', {
body: {},
});
} catch (e: any) {
error = e;
}
expect(error).toBeInstanceOf(ApiV2Error);
assert(error instanceof ApiV2Error);
expect(error.message).toEqual('hellomessage');
expect(error.expoApiV2ErrorCode).toEqual('TEST_CODE');
expect(error.expoApiV2ErrorDetails).toEqual({ who: 'world' });
expect(error.expoApiV2ErrorMetadata).toEqual({ an: 'object' });
expect(error.expoApiV2ErrorServerStack).toEqual('line 1: hello');
});
it('does not convert non-APIv2 error to ApiV2Error', async () => {
nock(getExpoApiBaseUrl()).post('/v2/test').reply(500, 'Something went wrong');
let error: Error | null = null;
try {
await new ApiV2Client({ accessToken: null, sessionSecret: null }).postAsync('test', {
body: {},
});
} catch (e: any) {
error = e;
}
expect(error).not.toBeNull();
expect(error).not.toBeInstanceOf(ApiV2Error);
});
});
@@ -1,3 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`BuildVersionGetView reading version when appVersionSource is set to local 1`] = `"This project is not configured for using remote version source. Add {"cli": { "appVersionSource": "remote" }} in eas.json or re-run this command without "--non-interactive" flag."`;
@@ -1,161 +0,0 @@
import { AppVersionSource, EasJson } from '@expo/eas-json';
import chalk from 'chalk';
import { getMockEasJson, mockCommandContext, mockProjectId, mockTestCommand } from './utils';
import BuildVersionGetView from '../../commands/build/version/get';
import { AppVersionQuery } from '../../graphql/queries/AppVersionQuery';
import Log from '../../log';
import { enableJsonOutput, printJsonOnlyOutput } from '../../utils/json';
jest.mock('../../project/applicationIdentifier');
jest.mock('fs');
jest.mock('../../log');
jest.mock('../../utils/json');
jest.mock('../../graphql/queries/AppVersionQuery');
function withRemoteVersionSource(easJson: EasJson): EasJson {
return {
...easJson,
cli: {
...easJson.cli,
appVersionSource: AppVersionSource.REMOTE,
},
};
}
describe(BuildVersionGetView, () => {
afterEach(() => {
jest.clearAllMocks();
});
test('reading version for platform android', async () => {
const ctx = mockCommandContext(BuildVersionGetView, {
easJson: withRemoteVersionSource(getMockEasJson()),
});
jest.mocked(AppVersionQuery.latestVersionAsync).mockImplementation(async () => ({
buildVersion: '100',
storeVersion: '1.0.0',
}));
const cmd = mockTestCommand(BuildVersionGetView, ['--platform=android'], ctx);
await cmd.run();
expect(AppVersionQuery.latestVersionAsync).toHaveBeenCalledWith(
ctx.loggedIn.graphqlClient,
mockProjectId,
'ANDROID',
'eas.test.com'
);
expect(Log.log).toHaveBeenCalledWith(`Android versionCode - ${chalk.bold('100')}`);
expect(enableJsonOutput).not.toHaveBeenCalled();
expect(printJsonOnlyOutput).not.toHaveBeenCalled();
});
test('reading version for platform android when no remote version is set', async () => {
const ctx = mockCommandContext(BuildVersionGetView, {
easJson: withRemoteVersionSource(getMockEasJson()),
});
jest.mocked(AppVersionQuery.latestVersionAsync).mockImplementation(async () => null);
const cmd = mockTestCommand(BuildVersionGetView, ['--platform=android'], ctx);
await cmd.run();
expect(AppVersionQuery.latestVersionAsync).toHaveBeenCalledWith(
ctx.loggedIn.graphqlClient,
mockProjectId,
'ANDROID',
'eas.test.com'
);
expect(Log.log).toHaveBeenCalledWith(`No remote versions are configured for this project.`);
expect(enableJsonOutput).not.toHaveBeenCalled();
expect(printJsonOnlyOutput).not.toHaveBeenCalled();
});
test('reading version with --json flag for platform android', async () => {
const ctx = mockCommandContext(BuildVersionGetView, {
easJson: withRemoteVersionSource(getMockEasJson()),
});
jest.mocked(AppVersionQuery.latestVersionAsync).mockImplementation(async () => ({
buildVersion: '100',
storeVersion: '1.0.0',
}));
const cmd = mockTestCommand(
BuildVersionGetView,
['--non-interactive', '--json', '--platform=android'],
ctx
);
await cmd.run();
expect(AppVersionQuery.latestVersionAsync).toHaveBeenCalledWith(
ctx.loggedIn.graphqlClient,
mockProjectId,
'ANDROID',
'eas.test.com'
);
expect(enableJsonOutput).toHaveBeenCalled();
expect(printJsonOnlyOutput).toHaveBeenCalledWith({
versionCode: '100',
});
});
test('reading version with --json flag for platform ios', async () => {
const ctx = mockCommandContext(BuildVersionGetView, {
easJson: withRemoteVersionSource(getMockEasJson()),
});
jest.mocked(AppVersionQuery.latestVersionAsync).mockImplementation(async () => ({
buildVersion: '100',
storeVersion: '1.0.0',
}));
const cmd = mockTestCommand(
BuildVersionGetView,
['--non-interactive', '--json', '--platform=ios'],
ctx
);
await cmd.run();
expect(AppVersionQuery.latestVersionAsync).toHaveBeenCalledWith(
ctx.loggedIn.graphqlClient,
mockProjectId,
'IOS',
'eas.test.com'
);
expect(enableJsonOutput).toHaveBeenCalled();
expect(printJsonOnlyOutput).toHaveBeenCalledWith({
buildNumber: '100',
});
});
test('reading version with --json flag when no remote version is set', async () => {
const ctx = mockCommandContext(BuildVersionGetView, {
easJson: withRemoteVersionSource(getMockEasJson()),
});
jest.mocked(AppVersionQuery.latestVersionAsync).mockImplementation(async () => null);
const cmd = mockTestCommand(
BuildVersionGetView,
['--non-interactive', '--json', '--platform=android'],
ctx
);
await cmd.run();
expect(AppVersionQuery.latestVersionAsync).toHaveBeenCalledWith(
ctx.loggedIn.graphqlClient,
mockProjectId,
'ANDROID',
'eas.test.com'
);
expect(enableJsonOutput).toHaveBeenCalled();
expect(printJsonOnlyOutput).toHaveBeenCalledWith({});
});
test('reading version when appVersionSource is set to local ', async () => {
const ctx = mockCommandContext(BuildVersionGetView, {});
jest.mocked(AppVersionQuery.latestVersionAsync).mockImplementation(async () => ({
buildVersion: '100',
storeVersion: '1.0.0',
}));
const cmd = mockTestCommand(
BuildVersionGetView,
['--non-interactive', '--json', '--platform=android'],
ctx
);
await expect(cmd.run()).rejects.toThrowErrorMatchingSnapshot();
});
});
@@ -1,140 +0,0 @@
import { AppVersionSource, EasJson } from '@expo/eas-json';
import fs from 'fs-extra';
import path from 'path';
import {
getMockAppFragment,
getMockEasJson,
mockCommandContext,
mockProjectId,
mockTestCommand,
} from './utils';
import BuildVersionSetView from '../../commands/build/version/set';
import { AppVersionMutation } from '../../graphql/mutations/AppVersionMutation';
import { AppQuery } from '../../graphql/queries/AppQuery';
import { AppVersionQuery } from '../../graphql/queries/AppVersionQuery';
import Log from '../../log';
import * as prompts from '../../prompts';
jest.mock('../../project/applicationIdentifier');
jest.mock('../../graphql/queries/AppVersionQuery');
jest.mock('../../graphql/queries/AppQuery');
jest.mock('../../graphql/mutations/AppVersionMutation');
jest.mock('fs');
jest.mock('../../log');
jest.mock('../../prompts');
jest.mock('../../utils/json');
function withRemoteVersionSource(easJson: EasJson): EasJson {
return {
...easJson,
cli: {
...easJson.cli,
appVersionSource: AppVersionSource.REMOTE,
},
};
}
describe(BuildVersionSetView, () => {
afterEach(() => {
jest.clearAllMocks();
});
test('setting version for platform android', async () => {
const ctx = mockCommandContext(BuildVersionSetView, {
easJson: withRemoteVersionSource(getMockEasJson()),
});
jest.mocked(AppQuery.byIdAsync).mockImplementation(async () => getMockAppFragment());
jest.mocked(AppVersionQuery.latestVersionAsync).mockImplementation(async () => null);
jest.mocked(prompts.promptAsync).mockImplementation(async () => ({
version: '1000',
}));
const cmd = mockTestCommand(BuildVersionSetView, ['--platform=android'], ctx);
await cmd.run();
expect(AppVersionQuery.latestVersionAsync).toHaveBeenCalledWith(
ctx.loggedIn.graphqlClient,
mockProjectId,
'ANDROID',
'eas.test.com'
);
expect(AppVersionMutation.createAppVersionAsync).toHaveBeenCalledWith(
ctx.loggedIn.graphqlClient,
expect.objectContaining({
buildVersion: '1000',
storeVersion: '1.0.0',
})
);
});
test('printing current remote version before prompting for a new one', async () => {
const ctx = mockCommandContext(BuildVersionSetView, {
easJson: withRemoteVersionSource(getMockEasJson()),
});
jest.mocked(AppQuery.byIdAsync).mockImplementation(async () => getMockAppFragment());
jest.mocked(AppVersionQuery.latestVersionAsync).mockImplementation(async () => ({
buildVersion: '100',
storeVersion: '1.0.0',
}));
jest.mocked(prompts.promptAsync).mockImplementation(async () => ({
version: '1000',
}));
const cmd = mockTestCommand(BuildVersionSetView, ['--platform=android'], ctx);
await cmd.run();
expect(Log.log).toHaveBeenCalledWith(expect.stringMatching('configured with versionCode 100'));
expect(AppVersionQuery.latestVersionAsync).toHaveBeenCalledWith(
ctx.loggedIn.graphqlClient,
mockProjectId,
'ANDROID',
'eas.test.com'
);
});
test('printing info that there is no remote version configured', async () => {
const ctx = mockCommandContext(BuildVersionSetView, {
easJson: withRemoteVersionSource(getMockEasJson()),
});
jest.mocked(AppQuery.byIdAsync).mockImplementation(async () => getMockAppFragment());
jest.mocked(AppVersionQuery.latestVersionAsync).mockImplementation(async () => null);
jest.mocked(prompts.promptAsync).mockImplementation(async () => ({
version: '1000',
}));
const cmd = mockTestCommand(BuildVersionSetView, ['--platform=android'], ctx);
await cmd.run();
expect(Log.log).toHaveBeenCalledWith(
expect.stringMatching('does not have any versionCode configured')
);
expect(AppVersionQuery.latestVersionAsync).toHaveBeenCalledWith(
ctx.loggedIn.graphqlClient,
mockProjectId,
'ANDROID',
'eas.test.com'
);
});
test('setting version aborts when appVersionSource is set to local and users refuse auto configuration', async () => {
const ctx = mockCommandContext(BuildVersionSetView, {});
jest.mocked(AppVersionQuery.latestVersionAsync).mockImplementation(async () => null);
jest.mocked(AppQuery.byIdAsync).mockImplementation(async () => getMockAppFragment());
jest.mocked(prompts.confirmAsync).mockImplementation(async () => false);
const cmd = mockTestCommand(BuildVersionSetView, ['--platform=android'], ctx);
await expect(cmd.run()).rejects.toThrowError('Aborting...');
expect(AppVersionMutation.createAppVersionAsync).not.toHaveBeenCalledWith();
});
test('setting version when appVersionSource is set to local and user allows auto configuration', async () => {
const ctx = mockCommandContext(BuildVersionSetView, {});
jest.mocked(AppVersionQuery.latestVersionAsync).mockImplementation(async () => null);
jest.mocked(AppQuery.byIdAsync).mockImplementation(async () => getMockAppFragment());
jest.mocked(prompts.confirmAsync).mockImplementation(async () => true);
const cmd = mockTestCommand(BuildVersionSetView, ['--platform=android'], ctx);
await cmd.run();
const easJsonAfterCmd = await fs.readJson(path.join(ctx.projectDir, 'eas.json'));
expect(easJsonAfterCmd.cli.appVersionSource).toBe('remote');
});
});
@@ -1,204 +0,0 @@
import { Workflow } from '@expo/eas-build-job';
import { AppVersionSource, EasJson } from '@expo/eas-json';
import fs from 'fs-extra';
import path from 'path';
import {
getMockAppFragment,
getMockEasJson,
mockCommandContext,
mockProjectId,
mockTestCommand,
} from './utils';
import { updateNativeVersionsAsync as updateAndroidNativeVersionsAsync } from '../../build/android/version';
import { updateNativeVersionsAsync as updateIosNativeVersionsAsync } from '../../build/ios/version';
import BuildVersionSyncView from '../../commands/build/version/sync';
import { Target } from '../../credentials/ios/types';
import { AppVersionMutation } from '../../graphql/mutations/AppVersionMutation';
import { AppQuery } from '../../graphql/queries/AppQuery';
import { AppVersionQuery } from '../../graphql/queries/AppVersionQuery';
import { getAppBuildGradleAsync } from '../../project/android/gradleUtils';
import { resolveTargetsAsync } from '../../project/ios/target';
import { resolveWorkflowAsync } from '../../project/workflow';
import * as prompts from '../../prompts';
jest.mock('../../build/android/version');
jest.mock('../../build/ios/version');
jest.mock('../../project/applicationIdentifier');
jest.mock('../../graphql/queries/AppVersionQuery');
jest.mock('../../graphql/queries/AppQuery');
jest.mock('../../graphql/mutations/AppVersionMutation');
jest.mock('../../project/workflow');
jest.mock('../../project/android/gradleUtils');
jest.mock('../../project/ios/target');
jest.mock('../../project/ios/scheme');
jest.mock('fs');
jest.mock('../../log');
jest.mock('../../prompts');
jest.mock('../../utils/json');
function withRemoteVersionSource(easJson: EasJson): EasJson {
return {
...easJson,
cli: {
...easJson.cli,
appVersionSource: AppVersionSource.REMOTE,
},
};
}
describe(BuildVersionSyncView, () => {
afterEach(() => {
jest.clearAllMocks();
});
test('syncing version for managed project on platform android', async () => {
const ctx = mockCommandContext(BuildVersionSyncView, {
easJson: withRemoteVersionSource(getMockEasJson()),
});
jest.mocked(AppQuery.byIdAsync).mockImplementation(async () => getMockAppFragment());
jest.mocked(AppVersionQuery.latestVersionAsync).mockImplementation(async () => ({
buildVersion: '1000',
storeVersion: '0.0.1',
}));
jest.mocked(prompts.promptAsync).mockImplementation(async () => ({
version: '1000',
}));
jest.mocked(resolveWorkflowAsync).mockImplementation(async () => Workflow.MANAGED);
const cmd = mockTestCommand(BuildVersionSyncView, ['--platform=android'], ctx);
const syncAndroidAsync = jest.spyOn(cmd, 'syncAndroidAsync' as any);
await cmd.run();
expect(AppVersionQuery.latestVersionAsync).toHaveBeenCalledWith(
ctx.loggedIn.graphqlClient,
mockProjectId,
'ANDROID',
'eas.test.com'
);
expect(syncAndroidAsync).not.toHaveBeenCalled();
});
test('syncing version for bare project on platform android', async () => {
const ctx = mockCommandContext(BuildVersionSyncView, {
easJson: withRemoteVersionSource(getMockEasJson()),
});
jest.mocked(AppQuery.byIdAsync).mockImplementation(async () => getMockAppFragment());
jest.mocked(AppVersionQuery.latestVersionAsync).mockImplementation(async () => ({
buildVersion: '1000',
storeVersion: '0.0.1',
}));
jest.mocked(prompts.promptAsync).mockImplementation(async () => ({
version: '1000',
}));
jest.mocked(resolveWorkflowAsync).mockImplementation(async () => Workflow.GENERIC);
jest.mocked(getAppBuildGradleAsync).mockImplementation(async () => ({
android: {},
}));
const cmd = mockTestCommand(BuildVersionSyncView, ['--platform=android'], ctx);
const syncAndroidAsync = jest.spyOn(cmd, 'syncAndroidAsync' as any);
await cmd.run();
expect(AppVersionQuery.latestVersionAsync).toHaveBeenCalledWith(
ctx.loggedIn.graphqlClient,
mockProjectId,
'ANDROID',
'eas.test.com'
);
expect(syncAndroidAsync).toHaveBeenCalled();
expect(updateAndroidNativeVersionsAsync).toHaveBeenCalledWith({
projectDir: ctx.projectDir,
versionCode: 1000,
});
});
test('syncing version for managed project on platform ios', async () => {
const ctx = mockCommandContext(BuildVersionSyncView, {
easJson: withRemoteVersionSource(getMockEasJson()),
});
jest.mocked(AppQuery.byIdAsync).mockImplementation(async () => getMockAppFragment());
jest.mocked(AppVersionQuery.latestVersionAsync).mockImplementation(async () => ({
buildVersion: '1000',
storeVersion: '0.0.1',
}));
jest.mocked(prompts.promptAsync).mockImplementation(async () => ({
version: '1000',
}));
jest.mocked(resolveWorkflowAsync).mockImplementation(async () => Workflow.MANAGED);
const cmd = mockTestCommand(BuildVersionSyncView, ['--platform=ios'], ctx);
const syncIosAsync = jest.spyOn(cmd, 'syncIosAsync' as any);
await cmd.run();
expect(AppVersionQuery.latestVersionAsync).toHaveBeenCalledWith(
ctx.loggedIn.graphqlClient,
mockProjectId,
'IOS',
'eas.test.com'
);
expect(syncIosAsync).not.toHaveBeenCalled();
});
test('syncing version for bare project on platform ios', async () => {
const ctx = mockCommandContext(BuildVersionSyncView, {
easJson: withRemoteVersionSource(getMockEasJson()),
});
const fakeTarget: Target = {
targetName: 'testapp',
bundleIdentifier: 'eas.test.com',
entitlements: {},
};
jest.mocked(AppQuery.byIdAsync).mockImplementation(async () => getMockAppFragment());
jest.mocked(AppVersionQuery.latestVersionAsync).mockImplementation(async () => ({
buildVersion: '1000',
storeVersion: '0.0.1',
}));
jest.mocked(prompts.promptAsync).mockImplementation(async () => ({
version: '1000',
}));
jest.mocked(resolveWorkflowAsync).mockImplementation(async () => Workflow.GENERIC);
jest.mocked(resolveTargetsAsync).mockImplementation(async () => [fakeTarget]);
const cmd = mockTestCommand(BuildVersionSyncView, ['--platform=ios'], ctx);
const syncIosAsync = jest.spyOn(cmd, 'syncIosAsync' as any);
await cmd.run();
expect(AppVersionQuery.latestVersionAsync).toHaveBeenCalledWith(
ctx.loggedIn.graphqlClient,
mockProjectId,
'IOS',
'eas.test.com'
);
expect(syncIosAsync).toHaveBeenCalled();
expect(updateIosNativeVersionsAsync).toHaveBeenCalledWith({
buildNumber: '1000',
projectDir: ctx.projectDir,
targets: [fakeTarget],
});
});
test('syncing version aborts when appVersionSource is set to local and users refuse auto configuration', async () => {
const ctx = mockCommandContext(BuildVersionSyncView, {});
jest.mocked(AppVersionQuery.latestVersionAsync).mockImplementation(async () => null);
jest.mocked(AppQuery.byIdAsync).mockImplementation(async () => getMockAppFragment());
jest.mocked(prompts.confirmAsync).mockImplementation(async () => false);
const cmd = mockTestCommand(BuildVersionSyncView, ['--platform=android'], ctx);
await expect(cmd.run()).rejects.toThrowError('Aborting...');
expect(AppVersionMutation.createAppVersionAsync).not.toHaveBeenCalledWith();
});
test('syncing version when appVersionSource is set to local and user allows auto configuration', async () => {
const ctx = mockCommandContext(BuildVersionSyncView, {});
jest.mocked(AppVersionQuery.latestVersionAsync).mockImplementation(async () => null);
jest.mocked(AppQuery.byIdAsync).mockImplementation(async () => getMockAppFragment());
jest.mocked(prompts.confirmAsync).mockImplementation(async () => true);
const cmd = mockTestCommand(BuildVersionSyncView, ['--platform=android'], ctx);
await cmd.run();
const easJsonAfterCmd = await fs.readJson(path.join(ctx.projectDir, 'eas.json'));
expect(easJsonAfterCmd.cli.appVersionSource).toBe('remote');
});
});
@@ -1,150 +0,0 @@
import { ExpoConfig } from '@expo/config-types';
import { EasJson } from '@expo/eas-json';
import { Command, Config } from '@oclif/core';
import { vol } from 'memfs';
import path from 'path';
import { v4 as uuidv4 } from 'uuid';
import { ContextInput, ContextOutput } from '../../commandUtils/EasCommand';
import { DynamicConfigContextFn } from '../../commandUtils/context/DynamicProjectConfigContextField';
import { AppFragment, Role } from '../../graphql/generated';
export function getMockEasJson(): EasJson {
return {
cli: {},
build: {
production: {},
},
};
}
export const mockProjectId = 'fake-project-id';
export function getMockAppFragment(): AppFragment {
return {
id: mockProjectId,
slug: 'testapp',
name: 'testapp',
fullName: '@testuser/testpp',
ownerAccount: {
id: 'test-account-id',
name: 'testuser',
users: [
{
role: Role.Owner,
actor: {
id: 'test-user-id',
},
},
],
},
};
}
export function getMockExpoConfig(): ExpoConfig {
return {
name: 'testapp',
slug: 'tetsapp',
extra: {
projectId: mockProjectId,
},
};
}
export interface LocalProjectContext {
projectDir: string;
exp?: ExpoConfig;
appId?: string;
}
export function mockCommandContext<
C extends {
[name: string]: any;
} = object,
>(
commandClass: { contextDefinition: ContextInput<C> },
overrides: {
easJson?: EasJson;
exp?: ExpoConfig;
projectId?: string;
getDynamicPrivateProjectConfigAsync?: DynamicConfigContextFn;
getDynamicPublicProjectConfigAsync?: DynamicConfigContextFn;
}
): ContextOutput<C> {
const projectDir = path.join('/test', uuidv4());
vol.reset();
vol.fromJSON({ 'eas.json': JSON.stringify(overrides.easJson ?? getMockEasJson()) }, projectDir);
const contextDefinition = commandClass.contextDefinition;
const result: any = {};
for (const [contextKey] of Object.entries(contextDefinition)) {
if (contextKey === 'loggedIn') {
result.loggedIn = {};
}
if (contextKey === 'projectDir') {
result.projectDir = projectDir;
}
if (contextKey === 'maybeLoggedIn') {
result.maybeLoggedIn = {};
}
if (contextKey === 'sessionManager') {
result.sessionManager = {};
}
if (contextKey === 'projectConfig') {
result.projectConfig = {
exp: overrides.exp ?? getMockExpoConfig(),
projectId: overrides.projectId ?? mockProjectId,
projectDir,
};
}
if (contextKey === 'getDynamicPrivateProjectConfigAsync') {
result.getDynamicPrivateProjectConfigAsync = () => ({
exp: overrides.exp ?? getMockExpoConfig(),
projectId: overrides.projectId ?? mockProjectId,
projectDir,
});
}
if (contextKey === 'getDynamicPublicProjectConfigAsync') {
result.getDynamicPublicProjectConfigAsync = () => ({
exp: overrides.exp ?? getMockExpoConfig(),
projectId: overrides.projectId ?? mockProjectId,
projectDir,
});
}
}
return result;
}
export function mockTestCommand<T extends Command>(
commandConstructor: new (argv: string[], config: Config) => T,
argv: string[],
ctx: any
): T {
const cmd = new commandConstructor(argv, new Config({ root: __dirname }));
(cmd as any).getContextAsync = jest.fn(() => ctx);
return cmd;
}
export class NoErrorThrownError extends Error {}
export const getErrorAsync = async <TError = any>(
call: () => unknown
): Promise<TError | NoErrorThrownError> => {
try {
await call();
throw new NoErrorThrownError();
} catch (error: unknown) {
return error as TError;
}
};
export const getError = <TError = any>(call: () => unknown): TError | NoErrorThrownError => {
try {
call();
throw new NoErrorThrownError();
} catch (error: unknown) {
return error as TError;
}
};
@@ -1,234 +0,0 @@
import RudderAnalytics from '@expo/rudder-sdk-node';
import os from 'os';
import { URL } from 'url';
import { v4 as uuidv4 } from 'uuid';
import { Actor, getActorDisplayName } from '../user/User';
import UserSettings from '../user/UserSettings';
import { easCliVersion } from '../utils/easCli';
const PLATFORM_TO_ANALYTICS_PLATFORM: Partial<Record<NodeJS.Platform, string>> = {
darwin: 'Mac',
win32: 'Windows',
linux: 'Linux',
};
export type AnalyticsEvent = CommandEvent | BuildEvent | SubmissionEvent | MetadataEvent;
export enum CommandEvent {
ACTION = 'action', // generic event type which is used to determine the 'daily active user' stat, include an `action: eas ${subcommand}` property inside of the event properties object
}
export enum SubmissionEvent {
SUBMIT_COMMAND = 'submit cli submit command',
SUBMIT_COMMAND_ATTEMPT = 'submit cli attempt',
SUBMIT_COMMAND_SUCCESS = 'submit cli success',
SUBMIT_COMMAND_FAIL = 'submit cli fail',
GATHER_CREDENTIALS_ATTEMPT = 'submit cli gather credentials attempt',
GATHER_CREDENTIALS_SUCCESS = 'submit cli gather credentials success',
GATHER_CREDENTIALS_FAIL = 'submit cli gather credentials fail',
GATHER_ARCHIVE_ATTEMPT = 'submit cli gather archive attempt',
GATHER_ARCHIVE_SUCCESS = 'submit cli gather archive success',
GATHER_ARCHIVE_FAIL = 'submit cli gather archive fail',
SUBMIT_REQUEST_ATTEMPT = 'submit cli request attempt',
SUBMIT_REQUEST_SUCCESS = 'submit cli request success',
SUBMIT_REQUEST_FAIL = 'submit cli request fail',
API_KEY_DOWNLOAD_FAIL = 'submit cli credentials api key download fail',
API_KEY_DOWNLOAD_RETRY = 'submit cli credentials api key download fail temporary',
API_KEY_DOWNLOAD_SUCCESS = 'submit cli credentials api key download succeed',
}
export enum BuildEvent {
BUILD_COMMAND = 'build cli build command',
PROJECT_UPLOAD_ATTEMPT = 'build cli project upload attempt',
PROJECT_UPLOAD_SUCCESS = 'build cli project upload success',
PROJECT_UPLOAD_FAIL = 'build cli project upload fail',
GATHER_CREDENTIALS_ATTEMPT = 'build cli gather credentials attempt',
GATHER_CREDENTIALS_SUCCESS = 'build cli gather credentials success',
GATHER_CREDENTIALS_FAIL = 'build cli gather credentials fail',
CONFIGURE_PROJECT_ATTEMPT = 'build cli configure project attempt',
CONFIGURE_PROJECT_SUCCESS = 'build cli configure project success',
CONFIGURE_PROJECT_FAIL = 'build cli configure project fail',
BUILD_REQUEST_ATTEMPT = 'build cli build request attempt',
BUILD_REQUEST_SUCCESS = 'build cli build request success',
BUILD_REQUEST_FAIL = 'build cli build request fail',
BUILD_STATUS_COMMAND = 'build cli build status',
CREDENTIALS_SYNC_COMMAND = 'build cli credentials sync command',
CREDENTIALS_SYNC_UPDATE_LOCAL_ATTEMPT = 'build cli credentials sync update local attempt',
CREDENTIALS_SYNC_UPDATE_LOCAL_SUCCESS = 'build cli credentials sync update local success',
CREDENTIALS_SYNC_UPDATE_LOCAL_FAIL = 'build cli credentials sync update local fail',
CREDENTIALS_SYNC_UPDATE_REMOTE_ATTEMPT = 'build cli credentials sync update remote attempt',
CREDENTIALS_SYNC_UPDATE_REMOTE_SUCCESS = 'build cli credentials sync update remote success',
CREDENTIALS_SYNC_UPDATE_REMOTE_FAIL = 'build cli credentials sync update remote fail',
ANDROID_KEYSTORE_CREATE = 'build cli credentials keystore create',
}
export enum MetadataEvent {
APPLE_METADATA_DOWNLOAD = 'metadata cli download apple response',
APPLE_METADATA_UPLOAD = 'metadata cli upload apple response',
}
export type AnalyticsEventProperties = Record<string, string | number | boolean>;
/**
* The interface for commands to use to log events to analytics.
*/
export interface Analytics {
logEvent(name: AnalyticsEvent, properties: AnalyticsEventProperties): void;
}
/**
* The interface for commands to use to orchestrate the analytics system. Should only be necessary to use
* this within EASCommand.
*/
export interface AnalyticsWithOrchestration extends Analytics {
setActor(actor: Actor): void;
flushAsync(): Promise<void>;
}
const USER_SETTINGS_KEY_AMPLITUDE_ENABLED = 'amplitudeEnabled';
const USER_SETTINGS_KEY_AMPLITUDE_DEVICE_ID = 'amplitudeDeviceId';
const USER_SETTINGS_KEY_ANALYTICS_ENABLED = 'analyticsEnabled';
const USER_SETTINGS_KEY_ANALYTICS_DEVICE_ID = 'analyticsDeviceId';
/**
* Sets the user's analytics enabled preference. Note that this will only take effect
* upon the next run of the CLI.
*/
export async function setAnalyticsEnabledAsync(enabled: boolean): Promise<void> {
await UserSettings.setAsync(USER_SETTINGS_KEY_ANALYTICS_ENABLED, enabled);
}
/**
* Returns the user's analytics enabled preference.
*/
export async function getAnalyticsEnabledAsync(): Promise<boolean> {
const analyticsEnabled = await UserSettings.getAsync(USER_SETTINGS_KEY_ANALYTICS_ENABLED, null);
return !!analyticsEnabled;
}
/**
* Create an instance of Analytics based on the user's analytics enabled preferences.
*/
export async function createAnalyticsAsync(): Promise<AnalyticsWithOrchestration> {
// TODO: remove after some time
const amplitudeEnabled = await UserSettings.getAsync(USER_SETTINGS_KEY_AMPLITUDE_ENABLED, null);
if (amplitudeEnabled !== null) {
await UserSettings.setAsync(USER_SETTINGS_KEY_ANALYTICS_ENABLED, amplitudeEnabled);
await UserSettings.deleteKeyAsync(USER_SETTINGS_KEY_AMPLITUDE_ENABLED);
}
const amplitudeDeviceId = await UserSettings.getAsync(
USER_SETTINGS_KEY_AMPLITUDE_DEVICE_ID,
null
);
if (amplitudeDeviceId !== null) {
await UserSettings.setAsync(USER_SETTINGS_KEY_ANALYTICS_DEVICE_ID, amplitudeDeviceId);
await UserSettings.deleteKeyAsync(USER_SETTINGS_KEY_AMPLITUDE_DEVICE_ID);
}
if (process.env.DISABLE_EAS_ANALYTICS) {
await UserSettings.setAsync(USER_SETTINGS_KEY_ANALYTICS_ENABLED, false);
}
const analyticsEnabled =
!process.env.https_proxy && // disable analytics if running behind proxy
(await UserSettings.getAsync(USER_SETTINGS_KEY_ANALYTICS_ENABLED, true));
if (!analyticsEnabled) {
return new NoOpAnalytics();
}
const persistedDeviceId = await UserSettings.getAsync(
USER_SETTINGS_KEY_ANALYTICS_DEVICE_ID,
null
);
const deviceId = persistedDeviceId ?? uuidv4();
if (!persistedDeviceId) {
await UserSettings.setAsync(USER_SETTINGS_KEY_ANALYTICS_DEVICE_ID, deviceId);
}
return new RudderstackAnalytics(deviceId);
}
class NoOpAnalytics implements AnalyticsWithOrchestration {
logEvent(): void {}
setActor(): void {}
async flushAsync(): Promise<void> {}
}
const RudderstackAnalyticsConfig =
process.env.EXPO_STAGING || process.env.EXPO_LOCAL
? {
// staging environment
rudderstackWriteKey: '1wpX20Da4ltFGSXbPFYUL00Chb7',
rudderstackDataPlaneURL: 'https://cdp.expo.dev',
}
: {
// prod environment
rudderstackWriteKey: '1wpXLFxmujq86etH6G6cc90hPcC',
rudderstackDataPlaneURL: 'https://cdp.expo.dev',
};
class RudderstackAnalytics implements AnalyticsWithOrchestration {
private readonly rudderstackClient = new RudderAnalytics(
RudderstackAnalyticsConfig.rudderstackWriteKey,
new URL('/v1/batch', RudderstackAnalyticsConfig.rudderstackDataPlaneURL).toString(),
{
flushInterval: 300,
}
);
private identifiedActor: Actor | null = null;
constructor(private readonly persistentDeviceId: string) {
// identify once with just anonymous ID. Once the actor is fetched, re-indentify with
// both so that they can be associated
this.rudderstackClient.identify({
anonymousId: persistentDeviceId,
});
}
public setActor(actor: Actor): void {
if (this.identifiedActor) {
return;
}
this.rudderstackClient.identify({
userId: actor.id,
anonymousId: this.persistentDeviceId,
traits: {
username: getActorDisplayName(actor),
user_id: actor.id,
user_type: actor.__typename,
},
});
this.identifiedActor = actor;
}
public logEvent(name: AnalyticsEvent, properties: AnalyticsEventProperties): void {
const userId = this.identifiedActor?.id;
const deviceId = this.persistentDeviceId;
const commonEventProperties = { source_version: easCliVersion, source: 'eas cli' };
const identity = { userId: userId ?? undefined, anonymousId: deviceId ?? uuidv4() };
this.rudderstackClient.track({
event: name,
properties: { ...properties, ...commonEventProperties },
...identity,
context: this.getRudderStackContext(),
});
}
public async flushAsync(): Promise<void> {
await this.rudderstackClient.flush();
}
private getRudderStackContext(): Record<string, any> {
const platform = PLATFORM_TO_ANALYTICS_PLATFORM[os.platform()] || os.platform();
return {
os: { name: platform, version: os.release() },
device: { type: platform, model: platform },
app: { name: 'eas cli', version: easCliVersion ?? undefined },
};
}
}
-30
View File
@@ -1,30 +0,0 @@
import { Analytics, AnalyticsEvent, AnalyticsEventProperties } from './AnalyticsManager';
export async function withAnalyticsAsync<Result>(
analytics: Analytics,
fn: () => Promise<Result>,
{
attemptEvent,
successEvent,
failureEvent,
properties,
}: {
attemptEvent: AnalyticsEvent;
successEvent: AnalyticsEvent;
failureEvent: AnalyticsEvent;
properties: AnalyticsEventProperties;
}
): Promise<Result> {
try {
analytics.logEvent(attemptEvent, properties);
const result = await fn();
analytics.logEvent(successEvent, properties);
return result;
} catch (error: any) {
analytics.logEvent(failureEvent, {
...properties,
reason: error.message,
});
throw error;
}
}
-106
View File
@@ -1,106 +0,0 @@
import { JSONValue } from '@expo/json-file';
import { ApiV2Error } from './ApiV2Error';
import fetch, { RequestError, RequestInit } from './fetch';
interface RequestOptions {
body: JSONValue;
}
export class ApiV2Client {
constructor(
private readonly authInfo: {
accessToken: string | null;
sessionSecret: string | null;
}
) {}
public async putAsync(path: string, options: RequestOptions): Promise<any> {
return await this.requestAsync(path, { method: 'PUT', body: JSON.stringify(options.body) });
}
public async postAsync(path: string, options: RequestOptions): Promise<any> {
return await this.requestAsync(path, { method: 'POST', body: JSON.stringify(options.body) });
}
public async deleteAsync(path: string): Promise<any> {
return await this.requestAsync(path, { method: 'DELETE' });
}
public async getAsync(path: string): Promise<any> {
return await this.requestAsync(path, { method: 'GET' });
}
private getAuthHeaders(): Record<string, string> {
const token = this.authInfo.accessToken;
if (token) {
return { authorization: `Bearer ${token}` };
}
const sessionSecret = this.authInfo.sessionSecret;
if (sessionSecret) {
return { 'expo-session': sessionSecret };
}
return {};
}
private async requestAsync(path: string, options: RequestInit): Promise<any> {
try {
const response = await fetch(`${getExpoApiBaseUrl()}/v2/${path}`, {
...options,
headers: {
'Content-Type': 'application/json',
...this.getAuthHeaders(),
},
});
return await response.json();
} catch (err) {
await ApiV2Client.handleApiErrorAsync(err);
}
}
private static async handleApiErrorAsync(err: any): Promise<void> {
if (err instanceof RequestError) {
let result: { [key: string]: any };
try {
result = (await err.response.json()) as { [key: string]: any };
} catch {
throw new Error(`Malformed api response: ${await err.response.text()}`);
}
if (result.errors?.length) {
throw new ApiV2Error(result.errors[0]);
}
} else {
throw err;
}
}
}
export function getExpoApiBaseUrl(): string {
if (process.env.EXPO_STAGING) {
return `https://staging-api.expo.dev`;
} else if (process.env.EXPO_LOCAL) {
return `http://127.0.0.1:3000`;
} else {
return `https://api.expo.dev`;
}
}
export function getExpoWebsiteBaseUrl(): string {
if (process.env.EXPO_STAGING) {
return `https://staging.expo.dev`;
} else if (process.env.EXPO_LOCAL) {
return `http://expo.test`;
} else {
return `https://expo.dev`;
}
}
export function getEASUpdateURL(projectId: string): string {
if (process.env.EXPO_STAGING) {
return new URL(projectId, `https://staging-u.expo.dev`).href;
} else if (process.env.EXPO_LOCAL) {
return new URL(`expo-updates/${projectId}`, `http://127.0.0.1:3000`).href;
} else {
return new URL(projectId, `https://u.expo.dev`).href;
}
}
@@ -1,12 +0,0 @@
import { UpdateBranchBasicInfoFragment } from '../../graphql/generated';
export const testBasicBranchInfo: UpdateBranchBasicInfoFragment = {
id: 'c4fec95d-7ab1-4d35-bfe6-fe7ff05beda9',
name: 'development',
__typename: 'UpdateBranch',
};
export const testBasicBranchInfo2: UpdateBranchBasicInfoFragment = {
id: '43ccb8a4-8aaa-47f0-9790-dcc80933441b',
name: 'preview',
__typename: 'UpdateBranch',
};
@@ -1,107 +0,0 @@
import { EASUpdateAction, EASUpdateContext, NonInteractiveError } from '../../eas-update/utils';
import { UpdateBranchBasicInfoFragment } from '../../graphql/generated';
import { BranchQuery } from '../../graphql/queries/BranchQuery';
import {
Connection,
Edge,
FilterPagination,
QueryParams,
selectPaginatedAsync,
} from '../../utils/relay';
/**
* Select a branch for the project.
*
* @constructor
* @param {function} options.filterPredicate - A predicate to filter the branches that are shown to the user. It takes a branchInfo object as a parameter and returns a boolean.
* @param {string} options.printedType - The type of branch printed to the user. Defaults to 'branch'.
* @param {number} options.pageSize - The number of branches to show per page. Defaults to 100.
* @param {function} options.beforeEachFilterQuery Optional. If a filter predicate was specified, this callback function will be called before each query.
* @args externalQueryParams The query params for the pagination.
* @args totalNodesFetched The total number of nodes fetched so far.
* @args dataset The dataset so far.
* @param {function} options.afterEachFilterQuery Optional. If a filter predicate was specified, this callback function will be called after each query.
* @args externalQueryParams The query params for the pagination.
* @args totalNodesFetched The total number of nodes fetched so far.
* @args dataset The dataset so far.
* @args willFetchAgain If the query will fetch again to get a complete page.
*/
export class SelectBranch implements EASUpdateAction<UpdateBranchBasicInfoFragment | null> {
constructor(
private options: {
filterPredicate?: (branchInfo: UpdateBranchBasicInfoFragment) => boolean;
printedType?: string;
pageSize?: number;
beforeEachFilterQuery?: (
externalQueryParams: QueryParams,
totalNodesFetched: number,
dataset: Edge<UpdateBranchBasicInfoFragment>[]
) => void;
afterEachFilterQuery?: (
externalQueryParams: QueryParams,
totalNodesFetched: number,
dataset: Edge<UpdateBranchBasicInfoFragment>[],
willFetchAgain: boolean
) => void;
} = {}
) {}
async queryAsync(
ctx: EASUpdateContext,
queryParams: QueryParams
): Promise<Connection<UpdateBranchBasicInfoFragment>> {
const { graphqlClient, app } = ctx;
const { projectId } = app;
return await BranchQuery.listBranchesBasicInfoPaginatedOnAppAsync(graphqlClient, {
appId: projectId,
...queryParams,
});
}
async filterQueryAsync(
ctx: EASUpdateContext,
queryParams: QueryParams,
filterPredicate: (branchInfo: UpdateBranchBasicInfoFragment) => boolean
): Promise<Connection<UpdateBranchBasicInfoFragment>> {
const queryAsync = async (
queryParams: QueryParams
): Promise<Connection<UpdateBranchBasicInfoFragment>> =>
await this.queryAsync(ctx, queryParams);
return await FilterPagination.getPageAsync({
queryParams,
queryAsync,
filterPredicate,
beforeEachQuery: this.options.beforeEachFilterQuery,
afterEachQuery: this.options.afterEachFilterQuery,
});
}
public async runAsync(ctx: EASUpdateContext): Promise<UpdateBranchBasicInfoFragment | null> {
const { nonInteractive } = ctx;
const { filterPredicate } = this.options;
const printedType = this.options.printedType ?? 'branch';
const pageSize = this.options.pageSize ?? 100;
if (nonInteractive) {
throw new NonInteractiveError(
`${printedType} selection cannot be run in non-interactive mode.`
);
}
const queryAsync = async (
queryParams: QueryParams
): Promise<Connection<UpdateBranchBasicInfoFragment>> =>
filterPredicate
? this.filterQueryAsync(ctx, queryParams, filterPredicate)
: this.queryAsync(ctx, queryParams);
const getTitleAsync = async (branchInfo: UpdateBranchBasicInfoFragment): Promise<string> =>
branchInfo.name;
return await selectPaginatedAsync({
queryAsync,
getTitleAsync,
printedType,
pageSize,
});
}
}
@@ -1,92 +0,0 @@
import { createCtxMock } from '../../../eas-update/__tests__/fixtures';
import { NonInteractiveError } from '../../../eas-update/utils';
import { UpdateBranchBasicInfoFragment } from '../../../graphql/generated';
import { BranchQuery } from '../../../graphql/queries/BranchQuery';
import { promptAsync } from '../../../prompts';
import { testBasicBranchInfo, testBasicBranchInfo2 } from '../../__tests__/fixtures';
import { SelectBranch } from '../SelectBranch';
jest.mock('../../../prompts');
jest.mock('../../../graphql/queries/BranchQuery');
describe(SelectBranch, () => {
afterEach(() => {
jest.clearAllMocks();
});
it('returns null if project has no branchs', async () => {
const ctx = createCtxMock();
jest.mocked(BranchQuery.listBranchesBasicInfoPaginatedOnAppAsync).mockResolvedValue({
edges: [],
pageInfo: {
hasPreviousPage: false,
hasNextPage: false,
},
});
const selectBranch = new SelectBranch();
const branch = await selectBranch.runAsync(ctx);
expect(branch).toBeNull();
});
it('still prompts if project has only one branch', async () => {
const ctx = createCtxMock();
jest.mocked(BranchQuery.listBranchesBasicInfoPaginatedOnAppAsync).mockResolvedValue({
edges: [{ node: testBasicBranchInfo, cursor: 'cursor' }],
pageInfo: {
hasPreviousPage: false,
hasNextPage: false,
},
});
jest.mocked(promptAsync).mockImplementation(async () => ({
item: testBasicBranchInfo,
}));
const selectBranch = new SelectBranch();
const branch = await selectBranch.runAsync(ctx);
expect(branch).toBe(testBasicBranchInfo);
expect(promptAsync).toHaveBeenCalledTimes(1);
});
it('returns the selected branch', async () => {
jest.mocked(BranchQuery.listBranchesBasicInfoPaginatedOnAppAsync).mockResolvedValue({
edges: [
{ node: testBasicBranchInfo, cursor: 'cursor' },
{ node: testBasicBranchInfo2, cursor: 'cursor' },
],
pageInfo: {
hasPreviousPage: false,
hasNextPage: false,
},
});
jest.mocked(promptAsync).mockImplementation(async () => ({
item: testBasicBranchInfo,
}));
const ctx = createCtxMock();
const selectBranch = new SelectBranch();
const branch = await selectBranch.runAsync(ctx);
expect(branch).toBe(testBasicBranchInfo);
});
it('returns the only available branch with a filter', async () => {
jest.mocked(BranchQuery.listBranchesBasicInfoPaginatedOnAppAsync).mockResolvedValue({
edges: [
{ node: testBasicBranchInfo, cursor: 'cursor' },
{ node: testBasicBranchInfo2, cursor: 'cursor' },
],
pageInfo: {
hasPreviousPage: false,
hasNextPage: false,
},
});
const ctx = createCtxMock();
const selectBranch = new SelectBranch({
filterPredicate: (branchInfo: UpdateBranchBasicInfoFragment) =>
branchInfo === testBasicBranchInfo,
});
const branch = await selectBranch.runAsync(ctx);
expect(branch).toBe(testBasicBranchInfo);
});
it('throws an error in Non-Interactive Mode', async () => {
const ctx = createCtxMock({
nonInteractive: true,
});
const selectBranch = new SelectBranch();
// dont fail if users are running in non-interactive mode
await expect(selectBranch.runAsync(ctx)).rejects.toThrowError(NonInteractiveError);
});
});
-184
View File
@@ -1,184 +0,0 @@
import chalk from 'chalk';
import gql from 'graphql-tag';
import { BranchNotFoundError } from './utils';
import { ExpoGraphqlClient } from '../commandUtils/context/contextUtils/createGraphqlClient';
import { PaginatedQueryOptions } from '../commandUtils/pagination';
import { withErrorHandlingAsync } from '../graphql/client';
import {
CreateUpdateBranchForAppMutation,
CreateUpdateBranchForAppMutationVariables,
UpdateBranch,
UpdateBranchFragment,
} from '../graphql/generated';
import { BranchQuery } from '../graphql/queries/BranchQuery';
import Log from '../log';
import { formatBranch, getBranchDescription } from '../update/utils';
import { printJsonOnlyOutput } from '../utils/json';
import {
SelectPromptEntry,
paginatedQueryWithConfirmPromptAsync,
paginatedQueryWithSelectPromptAsync,
} from '../utils/queries';
export const BRANCHES_LIMIT = 50;
export async function selectBranchOnAppAsync(
graphqlClient: ExpoGraphqlClient,
{
projectId,
promptTitle,
displayTextForListItem,
paginatedQueryOptions,
}: {
projectId: string;
displayTextForListItem: (queryItem: UpdateBranchFragment) => SelectPromptEntry;
promptTitle: string;
paginatedQueryOptions: PaginatedQueryOptions;
}
): Promise<UpdateBranchFragment> {
if (paginatedQueryOptions.nonInteractive) {
throw new Error('Unable to select a branch in non-interactive mode.');
}
const selectedBranch = await paginatedQueryWithSelectPromptAsync({
limit: paginatedQueryOptions.limit ?? BRANCHES_LIMIT,
offset: paginatedQueryOptions.offset,
queryToPerform: (limit, offset) =>
queryBranchesOnProjectAsync(graphqlClient, limit, offset, projectId),
promptOptions: {
title: promptTitle,
getIdentifierForQueryItem: updateBranchFragment => updateBranchFragment.id,
makePartialChoiceObject: displayTextForListItem,
},
});
if (!selectedBranch) {
throw new Error(`Could not find any branches for project "${projectId}"`);
}
return selectedBranch;
}
export async function listAndRenderBranchesOnAppAsync(
graphqlClient: ExpoGraphqlClient,
{
projectId,
paginatedQueryOptions,
}: {
projectId: string;
paginatedQueryOptions: PaginatedQueryOptions;
}
): Promise<void> {
if (paginatedQueryOptions.nonInteractive) {
const branches = await queryBranchesOnProjectAsync(
graphqlClient,
paginatedQueryOptions.limit ?? BRANCHES_LIMIT,
paginatedQueryOptions.offset,
projectId
);
renderPageOfBranches(branches, paginatedQueryOptions);
} else {
await paginatedQueryWithConfirmPromptAsync({
limit: paginatedQueryOptions.limit ?? BRANCHES_LIMIT,
offset: paginatedQueryOptions.offset,
queryToPerform: (limit, offset) =>
queryBranchesOnProjectAsync(graphqlClient, limit, offset, projectId),
promptOptions: {
title: 'Load more branches?',
renderListItems: branches => renderPageOfBranches(branches, paginatedQueryOptions),
},
});
}
}
async function queryBranchesOnProjectAsync(
graphqlClient: ExpoGraphqlClient,
limit: number,
offset: number,
projectId: string
): Promise<UpdateBranchFragment[]> {
return await BranchQuery.listBranchesOnAppAsync(graphqlClient, {
appId: projectId,
limit,
offset,
});
}
function renderPageOfBranches(
currentPage: UpdateBranchFragment[],
{ json }: PaginatedQueryOptions
): void {
if (json) {
printJsonOnlyOutput(currentPage);
} else {
Log.addNewLineIfNone();
Log.log(chalk.bold('Branches:'));
Log.addNewLineIfNone();
Log.log(
currentPage
.map(branch => formatBranch(getBranchDescription(branch)))
.join(`\n\n${chalk.dim('———')}\n\n`)
);
}
}
export async function createUpdateBranchOnAppAsync(
graphqlClient: ExpoGraphqlClient,
{ appId, name }: CreateUpdateBranchForAppMutationVariables
): Promise<Pick<UpdateBranch, 'id' | 'name'>> {
const result = await withErrorHandlingAsync(
graphqlClient
.mutation<CreateUpdateBranchForAppMutation, CreateUpdateBranchForAppMutationVariables>(
gql`
mutation CreateUpdateBranchForApp($appId: ID!, $name: String!) {
updateBranch {
createUpdateBranchForApp(appId: $appId, name: $name) {
id
name
}
}
}
`,
{
appId,
name,
}
)
.toPromise()
);
const newBranch = result.updateBranch.createUpdateBranchForApp;
if (!newBranch) {
throw new Error(`Could not create branch ${name}.`);
}
return newBranch;
}
export async function ensureBranchExistsAsync(
graphqlClient: ExpoGraphqlClient,
{
appId,
branchName,
}: {
appId: string;
branchName: string;
}
): Promise<{ branchId: string; createdBranch: boolean }> {
try {
const updateBranch = await BranchQuery.getBranchByNameAsync(graphqlClient, {
appId,
name: branchName,
});
const { id } = updateBranch;
return { branchId: id, createdBranch: false };
} catch (error) {
if (error instanceof BranchNotFoundError) {
const newUpdateBranch = await createUpdateBranchOnAppAsync(graphqlClient, {
appId,
name: branchName,
});
return { branchId: newUpdateBranch.id, createdBranch: true };
} else {
throw error;
}
}
}
-13
View File
@@ -1,13 +0,0 @@
import { Client } from '../vcs/vcs';
export async function getDefaultBranchNameAsync(vcsClient: Client): Promise<string> {
return (
(await vcsClient.getBranchNameAsync()) || `branch-${Math.random().toString(36).substring(2, 4)}`
);
}
export class BranchNotFoundError extends Error {
constructor(message?: string) {
super(message ?? 'Branch not found.');
}
}
@@ -1,90 +0,0 @@
import { instance, mock } from 'ts-mockito';
import { v4 as uuid } from 'uuid';
import { ExpoGraphqlClient } from '../../commandUtils/context/contextUtils/createGraphqlClient';
import { selectBuildToCancelAsync } from '../../commands/build/cancel';
import { AppPlatform, BuildFragment, BuildPriority, BuildStatus } from '../../graphql/generated';
import { BuildQuery } from '../../graphql/queries/BuildQuery';
import { confirmAsync, selectAsync } from '../../prompts';
jest.mock('../../ora', () => ({
ora: jest.fn().mockImplementation(() => ({
start: jest.fn().mockImplementation(() => ({
stop: jest.fn(),
fail: jest.fn(),
})),
})),
}));
jest.mock('../../prompts', () => {
return {
selectAsync: jest.fn(),
confirmAsync: jest.fn(),
};
});
jest.mock('../../graphql/queries/BuildQuery', () => {
const actual = jest.requireActual('../../graphql/queries/BuildQuery');
return {
BuildQuery: {
...actual.BuildQuery,
viewBuildsOnAppAsync: jest.fn(),
},
};
});
describe(selectBuildToCancelAsync.name, () => {
const selectedBuildId = uuid();
const projectId = uuid();
beforeEach(() => {
jest
.mocked(BuildQuery.viewBuildsOnAppAsync)
.mockImplementation(async () => [
createMockBuildFragment({ projectId, buildId: selectedBuildId }),
]);
});
it('does not return build id when confirmation is rejected', async () => {
const graphqlClient = instance(mock<ExpoGraphqlClient>());
jest.mocked(confirmAsync).mockResolvedValueOnce(false);
expect(selectBuildToCancelAsync(graphqlClient, projectId, 'blah')).resolves.toEqual(null);
});
it('returns build id when confirmation is confirmed', async () => {
const graphqlClient = instance(mock<ExpoGraphqlClient>());
jest.mocked(selectAsync).mockResolvedValueOnce(selectedBuildId);
jest.mocked(confirmAsync).mockResolvedValueOnce(true);
expect(selectBuildToCancelAsync(graphqlClient, projectId, 'blah')).resolves.toEqual(
selectedBuildId
);
});
});
function createMockBuildFragment({
projectId,
buildId,
}: {
projectId: string;
buildId?: string;
}): BuildFragment {
return {
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
platform: AppPlatform.Android,
id: buildId ?? uuid(),
priority: BuildPriority.Normal,
project: {
__typename: 'App',
slug: 'test-project',
id: projectId,
name: 'test-project',
ownerAccount: {
__typename: 'Account',
id: uuid(),
name: 'test-account',
},
},
status: BuildStatus.InQueue,
isForIosSimulator: false,
};
}
@@ -1,90 +0,0 @@
import { instance, mock } from 'ts-mockito';
import { v4 as uuid } from 'uuid';
import { ExpoGraphqlClient } from '../../commandUtils/context/contextUtils/createGraphqlClient';
import { selectBuildToDeleteAsync } from '../../commands/build/delete';
import { AppPlatform, BuildFragment, BuildPriority, BuildStatus } from '../../graphql/generated';
import { BuildQuery } from '../../graphql/queries/BuildQuery';
import { confirmAsync, selectAsync } from '../../prompts';
jest.mock('../../ora', () => ({
ora: jest.fn().mockImplementation(() => ({
start: jest.fn().mockImplementation(() => ({
stop: jest.fn(),
fail: jest.fn(),
})),
})),
}));
jest.mock('../../prompts', () => {
return {
selectAsync: jest.fn(),
confirmAsync: jest.fn(),
};
});
jest.mock('../../graphql/queries/BuildQuery', () => {
const actual = jest.requireActual('../../graphql/queries/BuildQuery');
return {
BuildQuery: {
...actual.BuildQuery,
viewBuildsOnAppAsync: jest.fn(),
},
};
});
describe(selectBuildToDeleteAsync.name, () => {
const selectedBuildId = uuid();
const projectId = uuid();
beforeEach(() => {
jest
.mocked(BuildQuery.viewBuildsOnAppAsync)
.mockImplementation(async () => [
createMockBuildFragment({ projectId, buildId: selectedBuildId }),
]);
});
it('does not return build id when confirmation is rejected', async () => {
const graphqlClient = instance(mock<ExpoGraphqlClient>());
jest.mocked(confirmAsync).mockResolvedValueOnce(false);
expect(selectBuildToDeleteAsync(graphqlClient, projectId, 'blah')).resolves.toEqual(null);
});
it('returns build id when confirmation is confirmed', async () => {
const graphqlClient = instance(mock<ExpoGraphqlClient>());
jest.mocked(selectAsync).mockResolvedValueOnce(selectedBuildId);
jest.mocked(confirmAsync).mockResolvedValueOnce(true);
expect(selectBuildToDeleteAsync(graphqlClient, projectId, 'blah')).resolves.toEqual(
selectedBuildId
);
});
});
function createMockBuildFragment({
projectId,
buildId,
}: {
projectId: string;
buildId?: string;
}): BuildFragment {
return {
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
platform: AppPlatform.Android,
id: buildId ?? uuid(),
priority: BuildPriority.Normal,
project: {
__typename: 'App',
slug: 'test-project',
id: projectId,
name: 'test-project',
ownerAccount: {
__typename: 'Account',
id: uuid(),
name: 'test-account',
},
},
status: BuildStatus.InQueue,
isForIosSimulator: false,
};
}
Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.0 KiB

@@ -1,597 +0,0 @@
import { Platform } from '@expo/eas-build-job';
import { CombinedError } from '@urql/core';
import { GraphQLError } from 'graphql/error';
import { v4 as uuidv4 } from 'uuid';
import { getError } from '../../__tests__/commands/utils';
import { EasCommandError } from '../../commandUtils/errors';
import Build from '../../commands/build';
import Log, { link } from '../../log';
import { handleBuildRequestError } from '../build';
import {
EasBuildDownForMaintenanceError,
EasBuildFreeTierDisabledAndroidError,
EasBuildFreeTierDisabledError,
EasBuildFreeTierDisabledIOSError,
EasBuildFreeTierIosLimitExceededError,
EasBuildFreeTierLimitExceededError,
EasBuildResourceClassNotAvailableInFreeTierError,
EasBuildTooManyPendingBuildsError,
RequestValidationError,
TurtleDeprecatedJobFormatError,
} from '../errors';
beforeEach(() => {
jest.resetAllMocks();
});
const EXPECTED_STRINGIFIED_GRAPHQL_ERROR_JSON = `[
{
"name": "GraphQLError",
"extensions": {},
"message": "Error 1"
},
{
"name": "GraphQLError",
"extensions": {},
"message": "Error 2"
},
{
"name": "GraphQLError",
"extensions": {},
"message": "Error 3"
}
]`;
const EXPECTED_EAS_BUILD_DOWN_MESSAGE = `EAS Build is down for maintenance. Try again later. Check ${link(
'https://status.expo.dev/'
)} for updates.`;
const EXPECTED_MAX_BUILD_COUNT_MESSAGE_ANDROID = `You have already reached the maximum number of pending Android builds for your account. Try again later.`;
const EXPECTED_MAX_BUILD_COUNT_MESSAGE_IOS = `You have already reached the maximum number of pending iOS builds for your account. Try again later.`;
const EXPECTED_GENERIC_MESSAGE =
'Build request failed. Make sure you are using the latest eas-cli version. If the problem persists, report the issue.';
const mockRequestId = uuidv4();
const getGraphQLError = (message: string, errorCode: string): GraphQLError => {
return new GraphQLError(message, null, null, null, null, null, {
errorCode,
requestId: mockRequestId,
});
};
const assertReThrownError = (
caughtError: CombinedError | Error,
expectedType: typeof EasCommandError,
expectedMessage: string = 'Error 1'
): void => {
expect(caughtError).toBeInstanceOf(expectedType);
expect((caughtError as Error).message).toEqual(expectedMessage);
};
describe(Build.name, () => {
describe('handle build request errors', () => {
describe('logs graphQL errors to debug if present', () => {
it('does it with Android', async () => {
const platform = Platform.ANDROID;
const logDebugSpy = jest.spyOn(Log, 'debug');
const graphQLErrors = ['Error 1', 'Error 2', 'Error 3'];
const error = new CombinedError({ graphQLErrors });
try {
handleBuildRequestError(error, platform);
} catch {}
expect(logDebugSpy).toBeCalledWith(EXPECTED_STRINGIFIED_GRAPHQL_ERROR_JSON);
});
it('does it with iOS', async () => {
const platform = Platform.IOS;
const logDebugSpy = jest.spyOn(Log, 'debug');
const graphQLErrors = ['Error 1', 'Error 2', 'Error 3'];
const error = new CombinedError({ graphQLErrors });
try {
handleBuildRequestError(error, platform);
} catch {}
expect(logDebugSpy).toBeCalledWith(EXPECTED_STRINGIFIED_GRAPHQL_ERROR_JSON);
});
});
describe('logs nothing to debug if graphQLErrors not present', () => {
it('does it with Android', async () => {
const platform = Platform.ANDROID;
const logDebugSpy = jest.spyOn(Log, 'debug');
const error = new Error('Generic error');
try {
handleBuildRequestError(error, platform);
} catch {}
expect(logDebugSpy).toBeCalledWith(undefined);
});
it('does it with iOS', async () => {
const platform = Platform.IOS;
const logDebugSpy = jest.spyOn(Log, 'debug');
const error = new Error('Generic error');
try {
handleBuildRequestError(error, platform);
} catch {}
expect(logDebugSpy).toBeCalledWith(undefined);
});
});
describe('error is server-side defined error', () => {
describe('for TURTLE_DEPRECATED_JOB_FORMAT', () => {
describe('with Android', () => {
it('throws correct EasCommandError subclass', async () => {
const platform = Platform.ANDROID;
const graphQLError = getGraphQLError('Error 1', 'TURTLE_DEPRECATED_JOB_FORMAT');
const graphQLErrors = [graphQLError];
const error = new CombinedError({ graphQLErrors });
const handleBuildRequestErrorThrownError = getError<TurtleDeprecatedJobFormatError>(
() => {
handleBuildRequestError(error, platform);
}
);
assertReThrownError(
handleBuildRequestErrorThrownError,
TurtleDeprecatedJobFormatError,
'Error 1'
);
});
it('throws correct EasBuildResourceClassNotAvailableInFreeTierError', async () => {
const platform = Platform.ANDROID;
const graphQLError = getGraphQLError(
'Error 1',
'EAS_BUILD_RESOURCE_CLASS_NOT_AVAILABLE_IN_FREE_TIER'
);
const graphQLErrors = [graphQLError];
const error = new CombinedError({ graphQLErrors });
const handleBuildRequestErrorThrownError =
getError<EasBuildResourceClassNotAvailableInFreeTierError>(() => {
handleBuildRequestError(error, platform);
});
assertReThrownError(
handleBuildRequestErrorThrownError,
EasBuildResourceClassNotAvailableInFreeTierError,
'Error 1'
);
});
});
describe('with iOS', () => {
it('throws correct EasCommandError subclass', async () => {
const platform = Platform.IOS;
const graphQLError = getGraphQLError('Error 1', 'TURTLE_DEPRECATED_JOB_FORMAT');
const graphQLErrors = [graphQLError];
const error = new CombinedError({ graphQLErrors });
const handleBuildRequestErrorThrownError = getError<TurtleDeprecatedJobFormatError>(
() => {
handleBuildRequestError(error, platform);
}
);
assertReThrownError(
handleBuildRequestErrorThrownError,
TurtleDeprecatedJobFormatError,
'Error 1'
);
});
it('throws correct EasBuildResourceClassNotAvailableInFreeTierError', async () => {
const platform = Platform.IOS;
const graphQLError = getGraphQLError(
'Error 1',
'EAS_BUILD_RESOURCE_CLASS_NOT_AVAILABLE_IN_FREE_TIER'
);
const graphQLErrors = [graphQLError];
const error = new CombinedError({ graphQLErrors });
const handleBuildRequestErrorThrownError =
getError<EasBuildResourceClassNotAvailableInFreeTierError>(() => {
handleBuildRequestError(error, platform);
});
assertReThrownError(
handleBuildRequestErrorThrownError,
EasBuildResourceClassNotAvailableInFreeTierError,
'Error 1'
);
});
});
});
describe('for EAS_BUILD_FREE_TIER_DISABLED', () => {
describe('with Android', () => {
it('throws correct EasCommandError subclass', async () => {
const platform = Platform.ANDROID;
const graphQLError = getGraphQLError('Error 1', 'EAS_BUILD_FREE_TIER_DISABLED');
const graphQLErrors = [graphQLError];
const error = new CombinedError({ graphQLErrors });
const handleBuildRequestErrorThrownError = getError<EasBuildFreeTierDisabledError>(
() => {
handleBuildRequestError(error, platform);
}
);
assertReThrownError(
handleBuildRequestErrorThrownError,
EasBuildFreeTierDisabledError,
'Error 1'
);
});
});
describe('with iOS', () => {
it('throws correct EasCommandError subclass', async () => {
const platform = Platform.IOS;
const graphQLError = getGraphQLError('Error 1', 'EAS_BUILD_FREE_TIER_DISABLED');
const graphQLErrors = [graphQLError];
const error = new CombinedError({ graphQLErrors });
const handleBuildRequestErrorThrownError = getError<EasBuildFreeTierDisabledError>(
() => {
handleBuildRequestError(error, platform);
}
);
assertReThrownError(
handleBuildRequestErrorThrownError,
EasBuildFreeTierDisabledError,
'Error 1'
);
});
});
});
describe('for EAS_BUILD_FREE_TIER_DISABLED_IOS', () => {
it('throws correct EasCommandError subclass', async () => {
const platform = Platform.IOS;
const graphQLError = getGraphQLError('Error 1', 'EAS_BUILD_FREE_TIER_DISABLED_IOS');
const graphQLErrors = [graphQLError];
const error = new CombinedError({ graphQLErrors });
try {
handleBuildRequestError(error, platform);
} catch (caughtError) {
assertReThrownError(caughtError as Error, EasBuildFreeTierDisabledIOSError, 'Error 1');
}
});
});
describe('for EAS_BUILD_FREE_TIER_DISABLED_ANDROID', () => {
it('throws correct EasCommandError subclass', async () => {
const platform = Platform.ANDROID;
const graphQLError = getGraphQLError('Error 1', 'EAS_BUILD_FREE_TIER_DISABLED_ANDROID');
const graphQLErrors = [graphQLError];
const error = new CombinedError({ graphQLErrors });
const handleBuildRequestErrorThrownError = getError<EasBuildFreeTierDisabledAndroidError>(
() => {
handleBuildRequestError(error, platform);
}
);
assertReThrownError(
handleBuildRequestErrorThrownError,
EasBuildFreeTierDisabledAndroidError,
'Error 1'
);
});
});
describe('for EAS_BUILD_FREE_TIER_LIMIT_EXCEEDED', () => {
it('throws correct EasCommandError subclass', async () => {
const platform = Platform.ANDROID;
const graphQLError = getGraphQLError('Error 1', 'EAS_BUILD_FREE_TIER_LIMIT_EXCEEDED');
const graphQLErrors = [graphQLError];
const error = new CombinedError({ graphQLErrors });
const handleBuildRequestErrorThrownError = getError<EasBuildFreeTierLimitExceededError>(
() => {
handleBuildRequestError(error, platform);
}
);
assertReThrownError(
handleBuildRequestErrorThrownError,
EasBuildFreeTierLimitExceededError,
'Error 1'
);
});
});
describe('for EAS_BUILD_FREE_TIER_IOS_LIMIT_EXCEEDED', () => {
it('throws correct EasCommandError subclass', async () => {
const platform = Platform.ANDROID;
const graphQLError = getGraphQLError('Error 1', 'EAS_BUILD_FREE_TIER_IOS_LIMIT_EXCEEDED');
const graphQLErrors = [graphQLError];
const error = new CombinedError({ graphQLErrors });
const handleBuildRequestErrorThrownError =
getError<EasBuildFreeTierIosLimitExceededError>(() => {
handleBuildRequestError(error, platform);
});
assertReThrownError(
handleBuildRequestErrorThrownError,
EasBuildFreeTierIosLimitExceededError,
'Error 1'
);
});
});
describe('for VALIDATION_ERROR', () => {
describe('with Android', () => {
it('throws correct EasCommandError subclass', async () => {
const platform = Platform.ANDROID;
const graphQLError = getGraphQLError('Error 1', 'VALIDATION_ERROR');
const graphQLErrors = [graphQLError];
const error = new CombinedError({ graphQLErrors });
const handleBuildRequestErrorThrownError = getError<RequestValidationError>(() => {
handleBuildRequestError(error, platform);
});
assertReThrownError(
handleBuildRequestErrorThrownError,
RequestValidationError,
'Error 1'
);
});
});
describe('with iOS', () => {
it('throws correct EasCommandError subclass', async () => {
const platform = Platform.IOS;
const graphQLError = getGraphQLError('Error 1', 'VALIDATION_ERROR');
const graphQLErrors = [graphQLError];
const error = new CombinedError({ graphQLErrors });
const handleBuildRequestErrorThrownError = getError<RequestValidationError>(() => {
handleBuildRequestError(error, platform);
});
assertReThrownError(
handleBuildRequestErrorThrownError,
RequestValidationError,
'Error 1'
);
});
});
});
});
describe('error is other expected graphQL-related error', () => {
describe('for EAS_BUILD_DOWN_FOR_MAINTENANCE', () => {
describe('with Android', () => {
it('throws correct EasCommandError subclass', async () => {
const platform = Platform.ANDROID;
const graphQLError = getGraphQLError('Error 1', 'EAS_BUILD_DOWN_FOR_MAINTENANCE');
const graphQLErrors = [graphQLError];
const error = new CombinedError({ graphQLErrors });
const handleBuildRequestErrorThrownError = getError<EasBuildDownForMaintenanceError>(
() => {
handleBuildRequestError(error, platform);
}
);
assertReThrownError(
handleBuildRequestErrorThrownError,
EasBuildDownForMaintenanceError,
EXPECTED_EAS_BUILD_DOWN_MESSAGE
);
});
});
describe('with iOS', () => {
it('throws correct EasCommandError subclass', async () => {
const platform = Platform.IOS;
const graphQLError = getGraphQLError('Error 1', 'EAS_BUILD_DOWN_FOR_MAINTENANCE');
const graphQLErrors = [graphQLError];
const error = new CombinedError({ graphQLErrors });
const handleBuildRequestErrorThrownError = getError<EasBuildDownForMaintenanceError>(
() => {
handleBuildRequestError(error, platform);
}
);
assertReThrownError(
handleBuildRequestErrorThrownError,
EasBuildDownForMaintenanceError,
EXPECTED_EAS_BUILD_DOWN_MESSAGE
);
});
});
});
describe('for EAS_BUILD_TOO_MANY_PENDING_BUILDS', () => {
describe('with Android', () => {
it('throws correct EasCommandError subclass', async () => {
const platform = Platform.ANDROID;
const graphQLError = getGraphQLError('Error 1', 'EAS_BUILD_TOO_MANY_PENDING_BUILDS');
const graphQLErrors = [graphQLError];
const error = new CombinedError({ graphQLErrors });
const handleBuildRequestErrorThrownError = getError<EasBuildTooManyPendingBuildsError>(
() => {
handleBuildRequestError(error, platform);
}
);
assertReThrownError(
handleBuildRequestErrorThrownError,
EasBuildTooManyPendingBuildsError,
EXPECTED_MAX_BUILD_COUNT_MESSAGE_ANDROID
);
});
});
describe('with iOS', () => {
it('throws correct EasCommandError subclass', async () => {
const platform = Platform.IOS;
const graphQLError = getGraphQLError('Error 1', 'EAS_BUILD_TOO_MANY_PENDING_BUILDS');
const graphQLErrors = [graphQLError];
const error = new CombinedError({ graphQLErrors });
const handleBuildRequestErrorThrownError = getError<EasBuildTooManyPendingBuildsError>(
() => {
handleBuildRequestError(error, platform);
}
);
assertReThrownError(
handleBuildRequestErrorThrownError,
EasBuildTooManyPendingBuildsError,
EXPECTED_MAX_BUILD_COUNT_MESSAGE_IOS
);
});
});
});
});
describe('error is unexpected graphQL-related error', () => {
describe('with Android', () => {
it('throws base Error class with custom message', async () => {
const platform = Platform.ANDROID;
const graphQLError = getGraphQLError('Error 1', 'UNKNOWN_GRAPHQL_ERROR');
const graphQLErrors = [graphQLError];
const error = new CombinedError({ graphQLErrors });
const expectedMessage = `${EXPECTED_GENERIC_MESSAGE}\nRequest ID: ${mockRequestId}\nError message: Error 1`;
const handleBuildRequestErrorThrownError = getError<Error>(() => {
handleBuildRequestError(error, platform);
});
assertReThrownError(handleBuildRequestErrorThrownError, Error, expectedMessage);
});
it('throws base Error class with message including all error messages if combined error has multiple GraphQL errors', async () => {
const platform = Platform.ANDROID;
const graphQLErrors = [
getGraphQLError('Error 1', 'UNKNOWN_GRAPHQL_ERROR'),
getGraphQLError('Error 2', 'OTHER_GRAPHQL_ERROR'),
getGraphQLError('Error 3', 'YET_ANOTHER_GRAPHQL_ERROR'),
];
const error = new CombinedError({ graphQLErrors });
const expectedMessage =
`${EXPECTED_GENERIC_MESSAGE}\n` +
`Request ID: ${mockRequestId}\n` +
'Error message: Error 1\n' +
`Request ID: ${mockRequestId}\n` +
'Error message: Error 2\n' +
`Request ID: ${mockRequestId}\n` +
'Error message: Error 3';
const handleBuildRequestErrorThrownError = getError<Error>(() => {
handleBuildRequestError(error, platform);
});
assertReThrownError(handleBuildRequestErrorThrownError, Error, expectedMessage);
});
describe('without request ID', () => {
it('throws base Error class with custom message without request ID line', async () => {
const platform = Platform.ANDROID;
const graphQLError = getGraphQLError('Error 1', 'UNKNOWN_GRAPHQL_ERROR');
delete graphQLError.extensions.requestId;
const graphQLErrors = [graphQLError];
const error = new CombinedError({ graphQLErrors });
const expectedMessage = `${EXPECTED_GENERIC_MESSAGE}\nError message: Error 1`;
const handleBuildRequestErrorThrownError = getError<Error>(() => {
handleBuildRequestError(error, platform);
});
assertReThrownError(handleBuildRequestErrorThrownError, Error, expectedMessage);
});
});
});
describe('with iOS', () => {
it('throws base Error class with custom message', async () => {
const platform = Platform.IOS;
const graphQLError = getGraphQLError('Error 1', 'UNKNOWN_GRAPHQL_ERROR');
const graphQLErrors = [graphQLError];
const error = new CombinedError({ graphQLErrors });
const expectedMessage = `${EXPECTED_GENERIC_MESSAGE}\nRequest ID: ${mockRequestId}\nError message: Error 1`;
const handleBuildRequestErrorThrownError = getError<Error>(() => {
handleBuildRequestError(error, platform);
});
assertReThrownError(handleBuildRequestErrorThrownError, Error, expectedMessage);
});
it('throws base Error class with message including all error messages if combined error has multiple GraphQL errors', async () => {
const platform = Platform.IOS;
const graphQLErrors = [
getGraphQLError('Error 1', 'UNKNOWN_GRAPHQL_ERROR'),
getGraphQLError('Error 2', 'OTHER_GRAPHQL_ERROR'),
getGraphQLError('Error 3', 'YET_ANOTHER_GRAPHQL_ERROR'),
];
const error = new CombinedError({ graphQLErrors });
const expectedMessage =
`${EXPECTED_GENERIC_MESSAGE}\n` +
`Request ID: ${mockRequestId}\n` +
'Error message: Error 1\n' +
`Request ID: ${mockRequestId}\n` +
'Error message: Error 2\n' +
`Request ID: ${mockRequestId}\n` +
'Error message: Error 3';
const handleBuildRequestErrorThrownError = getError<Error>(() => {
handleBuildRequestError(error, platform);
});
assertReThrownError(handleBuildRequestErrorThrownError, Error, expectedMessage);
});
describe('without request ID', () => {
it('throws base Error class with custom message without request ID line', async () => {
const platform = Platform.IOS;
const graphQLError = getGraphQLError('Error 1', 'UNKNOWN_GRAPHQL_ERROR');
delete graphQLError.extensions.requestId;
const graphQLErrors = [graphQLError];
const error = new CombinedError({ graphQLErrors });
const expectedMessage = `${EXPECTED_GENERIC_MESSAGE}\nError message: Error 1`;
const handleBuildRequestErrorThrownError = getError<Error>(() => {
handleBuildRequestError(error, platform);
});
assertReThrownError(handleBuildRequestErrorThrownError, Error, expectedMessage);
});
});
});
});
describe('error is unexpected non-graphQL-related error', () => {
describe('with Android', () => {
it('throws base Error class with propagated message', async () => {
const platform = Platform.ANDROID;
const error = new Error('Non-graphQL-related error');
const handleBuildRequestErrorThrownError = getError<Error>(() => {
handleBuildRequestError(error, platform);
});
expect(handleBuildRequestErrorThrownError).toStrictEqual(error);
});
});
describe('with iOS', () => {
it('throws base Error class with propagated message', async () => {
const platform = Platform.IOS;
const error = new Error('Non-graphQL-related error');
const handleBuildRequestErrorThrownError = getError<Error>(() => {
handleBuildRequestError(error, platform);
});
expect(handleBuildRequestErrorThrownError).toStrictEqual(error);
});
});
});
});
});
@@ -1,14 +0,0 @@
import { truncateGitCommitMessage } from '../metadata';
describe(truncateGitCommitMessage, () => {
it('returns undefined if no message was passed', () => {
expect(truncateGitCommitMessage(undefined)).toBeUndefined();
});
it('returns commit message', () => {
expect(truncateGitCommitMessage('a'.repeat(10))).toBe('a'.repeat(10));
});
it('truncates long commit messages', () => {
expect(truncateGitCommitMessage('a'.repeat(5000))).toBe(`${'a'.repeat(4093)}...`);
});
});
@@ -1,142 +0,0 @@
import { Platform, Workflow } from '@expo/eas-build-job';
import { ExitError } from '@oclif/core/lib/errors';
import path from 'path';
import { instance, mock, when } from 'ts-mockito';
import { CommonContext } from '../context';
import { validatePNGsForManagedProjectAsync } from '../validate';
const fixturesPath = path.join(__dirname, 'fixtures');
describe(validatePNGsForManagedProjectAsync, () => {
it('does not validate PNGs for generic projects', async () => {
const ctxMock = mock<CommonContext<Platform.ANDROID>>();
when(ctxMock.workflow).thenReturn(Workflow.GENERIC);
const ctx = instance(ctxMock);
await expect(validatePNGsForManagedProjectAsync(ctx)).resolves.not.toThrow();
});
it('does not validate PNGs for projects with SDK >= 47', async () => {
const ctxMock = mock<CommonContext<Platform.ANDROID>>();
when(ctxMock.workflow).thenReturn(Workflow.GENERIC);
when(ctxMock.exp).thenReturn({
name: 'blah',
slug: 'blah',
android: { adaptiveIcon: { foregroundImage: 'assets/icon.jpg' } },
sdkVersion: '47.0.0',
});
const ctx = instance(ctxMock);
await expect(validatePNGsForManagedProjectAsync(ctx)).resolves.not.toThrow();
});
describe(Platform.ANDROID, () => {
it('exits if foregroundImage is not a file with .png extension', async () => {
const ctxMock = mock<CommonContext<Platform.ANDROID>>();
when(ctxMock.workflow).thenReturn(Workflow.MANAGED);
when(ctxMock.platform).thenReturn(Platform.ANDROID);
when(ctxMock.exp).thenReturn({
name: 'blah',
slug: 'blah',
android: { adaptiveIcon: { foregroundImage: 'assets/icon.jpg' } },
sdkVersion: '46.0.0',
});
const ctx = instance(ctxMock);
await expect(validatePNGsForManagedProjectAsync(ctx)).rejects.toThrow(ExitError);
});
it('exits if foregroundImage is not a png file', async () => {
const ctxMock = mock<CommonContext<Platform.ANDROID>>();
when(ctxMock.workflow).thenReturn(Workflow.MANAGED);
when(ctxMock.platform).thenReturn(Platform.ANDROID);
when(ctxMock.exp).thenReturn({
name: 'blah',
slug: 'blah',
android: { adaptiveIcon: { foregroundImage: path.join(fixturesPath, 'icon-jpg.png') } },
sdkVersion: '46.0.0',
});
const ctx = instance(ctxMock);
await expect(validatePNGsForManagedProjectAsync(ctx)).rejects.toThrow(ExitError);
});
it('does not throw if foregroundImage is a png file', async () => {
const ctxMock = mock<CommonContext<Platform.ANDROID>>();
when(ctxMock.workflow).thenReturn(Workflow.MANAGED);
when(ctxMock.platform).thenReturn(Platform.ANDROID);
when(ctxMock.exp).thenReturn({
name: 'blah',
slug: 'blah',
android: { adaptiveIcon: { foregroundImage: path.join(fixturesPath, 'icon-alpha.png') } },
sdkVersion: '46.0.0',
});
const ctx = instance(ctxMock);
await expect(validatePNGsForManagedProjectAsync(ctx)).resolves.not.toThrow();
});
});
// Validating iOS PNGs is currently disabled
// See https://github.com/expo/eas-cli/pull/1477 for context
xdescribe(Platform.IOS, () => {
it('exits if icon is not a file with .png extension', async () => {
const ctxMock = mock<CommonContext<Platform.IOS>>();
when(ctxMock.workflow).thenReturn(Workflow.MANAGED);
when(ctxMock.platform).thenReturn(Platform.IOS);
when(ctxMock.exp).thenReturn({
name: 'blah',
slug: 'blah',
ios: { icon: path.join(fixturesPath, 'assets/icon.jpg') },
sdkVersion: '46.0.0',
});
const ctx = instance(ctxMock);
await expect(validatePNGsForManagedProjectAsync(ctx)).rejects.toThrow(ExitError);
});
it('exits if icon is not a png file', async () => {
const ctxMock = mock<CommonContext<Platform.IOS>>();
when(ctxMock.workflow).thenReturn(Workflow.MANAGED);
when(ctxMock.platform).thenReturn(Platform.IOS);
when(ctxMock.exp).thenReturn({
name: 'blah',
slug: 'blah',
ios: { icon: path.join(fixturesPath, 'icon-jpg.png') },
sdkVersion: '46.0.0',
});
const ctx = instance(ctxMock);
await expect(validatePNGsForManagedProjectAsync(ctx)).rejects.toThrow(ExitError);
});
it('exits if icon has alpha channel (transparency)', async () => {
const ctxMock = mock<CommonContext<Platform.IOS>>();
when(ctxMock.workflow).thenReturn(Workflow.MANAGED);
when(ctxMock.platform).thenReturn(Platform.IOS);
when(ctxMock.exp).thenReturn({
name: 'blah',
slug: 'blah',
ios: { icon: path.join(fixturesPath, 'icon-alpha.png') },
sdkVersion: '46.0.0',
});
const ctx = instance(ctxMock);
await expect(validatePNGsForManagedProjectAsync(ctx)).rejects.toThrow(ExitError);
});
it('does not throw if icon is a png file and does not have alpha channel', async () => {
const ctxMock = mock<CommonContext<Platform.IOS>>();
when(ctxMock.workflow).thenReturn(Workflow.MANAGED);
when(ctxMock.platform).thenReturn(Platform.IOS);
when(ctxMock.exp).thenReturn({
name: 'blah',
slug: 'blah',
ios: { icon: path.join(fixturesPath, 'icon-no-alpha.png') },
sdkVersion: '46.0.0',
});
const ctx = instance(ctxMock);
await expect(validatePNGsForManagedProjectAsync(ctx)).resolves.not.toThrow();
});
});
});
@@ -1,285 +0,0 @@
import { ExpoConfig } from '@expo/config';
import { Platform } from '@expo/eas-build-job';
import { BuildProfile } from '@expo/eas-json';
import assert from 'assert';
import fs from 'fs-extra';
import { vol } from 'memfs';
import os from 'os';
import path from 'path';
import { getAppBuildGradleAsync, resolveConfigValue } from '../../../project/android/gradleUtils';
import { resolveVcsClient } from '../../../vcs';
import {
BumpStrategy,
bumpVersionAsync,
bumpVersionInAppJsonAsync,
maybeResolveVersionsAsync,
} from '../version';
const fsReal = jest.requireActual('fs').promises as typeof fs;
jest.mock('fs');
const vcsClient = resolveVcsClient();
afterAll(async () => {
// do not remove the following line
// this fixes a weird error with tempy in @expo/image-utils
await fs.remove(os.tmpdir());
});
beforeEach(async () => {
vol.reset();
// do not remove the following line
// this fixes a weird error with tempy in @expo/image-utils
await fs.mkdirp(os.tmpdir());
});
// bare workflow
describe(bumpVersionAsync, () => {
it('throws an informative error when multiple flavors are detected in the android project', async () => {
await initProjectWithGradleFileAsync(
/*buildGradlePath*/ path.join(
__dirname,
'../../../project/android/__tests__/fixtures/multiflavor-build.gradle'
)
);
await expect(
bumpVersionAsync({
bumpStrategy: BumpStrategy.VERSION_CODE,
projectDir: '/multiflavor',
exp: {} as any,
})
).rejects.toThrow(
'Automatic version bumping is not supported for multi-flavor Android projects.'
);
});
it('throws an informative error when multiple flavor dimensions are defined in an android project', async () => {
await initProjectWithGradleFileAsync(
/*buildGradlePath*/ path.join(
__dirname,
'../../../project/android/__tests__/fixtures/multiflavor-with-dimensions-build.gradle'
)
);
await expect(
bumpVersionAsync({
bumpStrategy: BumpStrategy.VERSION_CODE,
projectDir: '/multiflavor',
exp: {} as any,
})
).rejects.toThrow(
'Automatic version bumping is not supported for multi-flavor Android projects.'
);
});
it('bumps expo.android.versionCode and buildGradle versionCode when strategy = BumpStrategy.VERSION_CODE', async () => {
const nativeVersionCode = 100; // this should be overwritten by bumpVersionAsync
const fakeExp = initBareWorkflowProject({ versionCode: nativeVersionCode });
await bumpVersionAsync({
bumpStrategy: BumpStrategy.VERSION_CODE,
projectDir: '/app',
exp: fakeExp,
});
const appJSON = await fs.readJSON('/app/app.json');
expect(fakeExp.version).toBe('3.0.0');
expect(fakeExp.android?.versionCode).toBe(124);
expect(appJSON.expo.version).toBe('3.0.0');
expect(appJSON.expo.android.versionCode).toBe(124);
const buildGradle = await getAppBuildGradleAsync('/app');
assert(buildGradle);
expect(resolveConfigValue(buildGradle, 'versionCode')).toBe('124');
expect(resolveConfigValue(buildGradle, 'versionName')).toBe('3.0.0');
});
it('bumps expo.version and gradle versionCode when strategy = BumpStrategy.APP_VERSION', async () => {
const nativeVersionName = '1.0.0'; // this should be overwritten by bumpVersionAsync
const fakeExp = initBareWorkflowProject({ versionName: nativeVersionName });
await bumpVersionAsync({
bumpStrategy: BumpStrategy.APP_VERSION,
projectDir: '/app',
exp: fakeExp,
});
const appJSON = await fs.readJSON('/app/app.json');
expect(fakeExp.version).toBe('3.0.1');
expect(fakeExp.android?.versionCode).toBe(123);
expect(appJSON.expo.version).toBe('3.0.1');
expect(appJSON.expo.android.versionCode).toBe(123);
const buildGradle = await getAppBuildGradleAsync('/app');
assert(buildGradle);
expect(resolveConfigValue(buildGradle, 'versionCode')).toBe('123');
expect(resolveConfigValue(buildGradle, 'versionName')).toBe('3.0.1');
});
it('does not bump any version when strategy = BumpStrategy.NOOP', async () => {
const fakeExp = initBareWorkflowProject();
await bumpVersionAsync({
bumpStrategy: BumpStrategy.NOOP,
projectDir: '/app',
exp: fakeExp,
});
const appJSON = await fs.readJSON('/app/app.json');
expect(fakeExp.version).toBe('3.0.0');
expect(fakeExp.android?.versionCode).toBe(123);
expect(appJSON.expo.version).toBe('3.0.0');
expect(appJSON.expo.android.versionCode).toBe(123);
const buildGradle = await getAppBuildGradleAsync('/app');
assert(buildGradle);
expect(resolveConfigValue(buildGradle, 'versionCode')).toBe('123');
expect(resolveConfigValue(buildGradle, 'versionName')).toBe('3.0.0');
});
});
// managed workflow
describe(bumpVersionInAppJsonAsync, () => {
it('bumps expo.android.versionCode when strategy = BumpStrategy.VERSION_CODE', async () => {
const fakeExp = initManagedProject();
await bumpVersionInAppJsonAsync({
bumpStrategy: BumpStrategy.VERSION_CODE,
projectDir: '/app',
exp: fakeExp,
});
const appJSON = await fs.readJSON('/app/app.json');
expect(fakeExp.version).toBe('5.0.0');
expect(fakeExp.android?.versionCode).toBe(127);
expect(appJSON.expo.version).toBe('5.0.0');
expect(appJSON.expo.android.versionCode).toBe(127);
});
it('bumps expo.version when strategy = BumpStrategy.SHORT_VERSION', async () => {
const fakeExp = initManagedProject();
await bumpVersionInAppJsonAsync({
bumpStrategy: BumpStrategy.APP_VERSION,
projectDir: '/app',
exp: fakeExp,
});
const appJSON = await fs.readJSON('/app/app.json');
expect(fakeExp.version).toBe('5.0.1');
expect(fakeExp.android?.versionCode).toBe(126);
expect(appJSON.expo.version).toBe('5.0.1');
expect(appJSON.expo.android.versionCode).toBe(126);
});
it('does not bump any version when strategy = BumpStrategy.NOOP', async () => {
const fakeExp = initManagedProject();
await bumpVersionInAppJsonAsync({
bumpStrategy: BumpStrategy.NOOP,
projectDir: '/app',
exp: fakeExp,
});
const appJSON = await fs.readJSON('/app/app.json');
expect(fakeExp.version).toBe('5.0.0');
expect(fakeExp.android?.versionCode).toBe(126);
expect(appJSON.expo.version).toBe('5.0.0');
expect(appJSON.expo.android.versionCode).toBe(126);
});
});
describe(maybeResolveVersionsAsync, () => {
describe('bare project', () => {
it('reads the versions from native code', async () => {
const exp = initBareWorkflowProject();
const { appVersion, appBuildVersion } = await maybeResolveVersionsAsync(
'/app',
exp,
{} as BuildProfile<Platform.ANDROID>,
vcsClient
);
expect(appVersion).toBe('3.0.0');
expect(appBuildVersion).toBe('123');
});
});
describe('managed project', () => {
it('reads the versions from expo config', async () => {
const exp = initManagedProject();
const { appVersion, appBuildVersion } = await maybeResolveVersionsAsync(
'/app',
exp,
{} as BuildProfile<Platform.ANDROID>,
vcsClient
);
expect(appVersion).toBe('5.0.0');
expect(appBuildVersion).toBe('126');
});
});
});
function initBareWorkflowProject({
versionCode = 123,
versionName = '3.0.0',
}: {
versionCode?: number;
versionName?: string;
} = {}): ExpoConfig {
const fakeExp: ExpoConfig = {
name: 'myproject',
slug: 'myproject',
version: '3.0.0',
android: {
versionCode: 123,
},
};
vol.fromJSON(
{
'./app.json': JSON.stringify({
expo: fakeExp,
}),
'./android/app/build.gradle': `android {
defaultConfig {
applicationId "com.expo.testapp"
versionCode ${versionCode}
versionName "${versionName}"
}
}`,
'./android/app/src/main/AndroidManifest.xml': 'fake',
},
'/app'
);
return fakeExp;
}
function initManagedProject(): ExpoConfig {
const fakeExp: ExpoConfig = {
name: 'myproject',
slug: 'myproject',
version: '5.0.0',
android: {
versionCode: 126,
},
};
vol.fromJSON(
{
'./app.json': JSON.stringify({
expo: fakeExp,
}),
},
'/app'
);
return fakeExp;
}
async function initProjectWithGradleFileAsync(gradleFilePath: string): Promise<void> {
vol.fromJSON(
{
'android/app/build.gradle': await fsReal.readFile(gradleFilePath, 'utf-8'),
'./app.json': JSON.stringify({
expo: {} as any,
}),
},
'/multiflavor'
);
}
-169
View File
@@ -1,169 +0,0 @@
import { Android, Metadata, Platform, Workflow } from '@expo/eas-build-job';
import { AppVersionSource } from '@expo/eas-json';
import chalk from 'chalk';
import nullthrows from 'nullthrows';
import { transformJob } from './graphql';
import { prepareJobAsync } from './prepareJob';
import { syncProjectConfigurationAsync } from './syncProjectConfiguration';
import { resolveRemoteVersionCodeAsync } from './version';
import AndroidCredentialsProvider, {
AndroidCredentials,
} from '../../credentials/android/AndroidCredentialsProvider';
import { BuildParamsInput } from '../../graphql/generated';
import { BuildMutation, BuildResult } from '../../graphql/mutations/BuildMutation';
import Log from '../../log';
import {
ensureApplicationIdIsDefinedForManagedProjectAsync,
getApplicationIdAsync,
} from '../../project/android/applicationId';
import { resolveGradleBuildContextAsync } from '../../project/android/gradle';
import { toggleConfirmAsync } from '../../prompts';
import {
BuildRequestSender,
CredentialsResult,
JobData,
prepareBuildRequestForPlatformAsync,
} from '../build';
import { AndroidBuildContext, BuildContext, CommonContext } from '../context';
import { transformMetadata } from '../graphql';
import { logCredentialsSource } from '../utils/credentials';
import {
checkGoogleServicesFileAsync,
checkNodeEnvVariable,
validatePNGsForManagedProjectAsync,
} from '../validate';
export async function createAndroidContextAsync(
ctx: CommonContext<Platform.ANDROID>
): Promise<AndroidBuildContext> {
const { buildProfile } = ctx;
if (buildProfile.distribution === 'internal' && buildProfile.gradleCommand?.match(/bundle/)) {
Log.addNewLineIfNone();
Log.warn(
`You're building your Android app for internal distribution. However, we've detected that the Gradle command you defined (${chalk.underline(
buildProfile.gradleCommand
)}) includes string 'bundle'.
This means that it will most likely produce an AAB and you will not be able to install it on your Android devices straight from the Expo website.`
);
Log.newLine();
const confirmed = await toggleConfirmAsync({ message: 'Would you like to proceed?' });
if (!confirmed) {
Log.error('Update eas.json and come back again.');
process.exit(1);
}
}
checkNodeEnvVariable(ctx);
await checkGoogleServicesFileAsync(ctx);
await validatePNGsForManagedProjectAsync(ctx);
const gradleContext = await resolveGradleBuildContextAsync(
ctx.projectDir,
buildProfile,
ctx.vcsClient
);
if (ctx.workflow === Workflow.MANAGED) {
await ensureApplicationIdIsDefinedForManagedProjectAsync(ctx);
}
const applicationId = await getApplicationIdAsync(
ctx.projectDir,
ctx.exp,
ctx.vcsClient,
gradleContext
);
const versionCodeOverride =
ctx.easJsonCliConfig?.appVersionSource === AppVersionSource.REMOTE
? await resolveRemoteVersionCodeAsync(ctx.graphqlClient, {
projectDir: ctx.projectDir,
projectId: ctx.projectId,
exp: ctx.exp,
applicationId,
buildProfile,
vcsClient: ctx.vcsClient,
})
: undefined;
return { applicationId, gradleContext, versionCodeOverride };
}
export async function prepareAndroidBuildAsync(
ctx: BuildContext<Platform.ANDROID>
): Promise<BuildRequestSender> {
return await prepareBuildRequestForPlatformAsync({
ctx,
ensureCredentialsAsync: async (ctx: BuildContext<Platform.ANDROID>) => {
return await ensureAndroidCredentialsAsync(ctx);
},
syncProjectConfigurationAsync: async () => {
await syncProjectConfigurationAsync({
projectDir: ctx.projectDir,
exp: ctx.exp,
localAutoIncrement:
ctx.easJsonCliConfig?.appVersionSource === AppVersionSource.REMOTE
? false
: ctx.buildProfile.autoIncrement,
vcsClient: ctx.vcsClient,
env: ctx.buildProfile.env,
});
},
prepareJobAsync: async (
ctx: BuildContext<Platform.ANDROID>,
jobData: JobData<AndroidCredentials>
): Promise<Android.Job> => {
return await prepareJobAsync(ctx, jobData);
},
sendBuildRequestAsync: async (
appId: string,
job: Android.Job,
metadata: Metadata,
buildParams: BuildParamsInput
): Promise<BuildResult> => {
const graphqlMetadata = transformMetadata(metadata);
const graphqlJob = transformJob(job);
return await BuildMutation.createAndroidBuildAsync(ctx.graphqlClient, {
appId,
job: graphqlJob,
metadata: graphqlMetadata,
buildParams,
});
},
});
}
function shouldProvideCredentials(ctx: BuildContext<Platform.ANDROID>): boolean {
return !ctx.buildProfile.withoutCredentials;
}
async function ensureAndroidCredentialsAsync(
ctx: BuildContext<Platform.ANDROID>
): Promise<CredentialsResult<AndroidCredentials> | undefined> {
if (!shouldProvideCredentials(ctx)) {
return;
}
const androidApplicationIdentifier = await getApplicationIdAsync(
ctx.projectDir,
ctx.exp,
ctx.vcsClient,
ctx.android.gradleContext
);
const provider = new AndroidCredentialsProvider(ctx.credentialsCtx, {
app: {
account: nullthrows(
ctx.user.accounts.find(a => a.name === ctx.accountName),
`You do not have access to account: ${ctx.accountName}`
),
projectName: ctx.projectName,
androidApplicationIdentifier,
},
});
const { credentialsSource } = ctx.buildProfile;
logCredentialsSource(credentialsSource, Platform.ANDROID);
return {
credentials: await provider.getCredentialsAsync(credentialsSource),
source: credentialsSource,
};
}
@@ -1,44 +0,0 @@
import { Android } from '@expo/eas-build-job';
import { AndroidBuildType, AndroidJobInput } from '../../graphql/generated';
import {
loggerLevelToGraphQLWorkerLoggerLevel,
transformBuildMode,
transformBuildTrigger,
transformProjectArchive,
transformWorkflow,
} from '../graphql';
export function transformJob(job: Android.Job): AndroidJobInput {
return {
type: transformWorkflow(job.type),
triggeredBy: transformBuildTrigger(job.triggeredBy),
projectArchive: transformProjectArchive(job.projectArchive),
projectRootDirectory: job.projectRootDirectory,
updates: job.updates,
secrets: job.secrets,
builderEnvironment: job.builderEnvironment,
cache: job.cache,
version: job.version?.versionCode ? { versionCode: job.version.versionCode } : undefined,
gradleCommand: job.gradleCommand,
applicationArchivePath: job.applicationArchivePath,
buildArtifactPaths: job.buildArtifactPaths,
username: job.username,
buildType: job.buildType && transformBuildType(job.buildType),
developmentClient: job.developmentClient,
experimental: job.experimental,
mode: transformBuildMode(job.mode),
customBuildConfig: job.customBuildConfig,
loggerLevel: job.loggerLevel
? loggerLevelToGraphQLWorkerLoggerLevel[job.loggerLevel]
: undefined,
};
}
function transformBuildType(buildType: Android.BuildType): AndroidBuildType {
if (buildType === Android.BuildType.APK) {
return AndroidBuildType.Apk;
} else {
return AndroidBuildType.AppBundle;
}
}
@@ -1,121 +0,0 @@
import {
Android,
ArchiveSource,
BuildMode,
BuildTrigger,
Platform,
sanitizeBuildJob,
} from '@expo/eas-build-job';
import { BuildProfile } from '@expo/eas-json';
import path from 'path';
import slash from 'slash';
import { AndroidCredentials } from '../../credentials/android/AndroidCredentialsProvider';
import { getCustomBuildConfigPathForJob } from '../../project/customBuildConfig';
import { getUsername } from '../../project/projectUtils';
import { BuildContext } from '../context';
interface JobData {
projectArchive: ArchiveSource;
credentials?: AndroidCredentials;
}
const cacheDefaults = {
disabled: false,
paths: [],
};
export async function prepareJobAsync(
ctx: BuildContext<Platform.ANDROID>,
jobData: JobData
): Promise<Android.Job> {
const username = getUsername(ctx.exp, ctx.user);
const buildProfile: BuildProfile<Platform.ANDROID> = ctx.buildProfile;
const projectRootDirectory =
slash(path.relative(await ctx.vcsClient.getRootPathAsync(), ctx.projectDir)) || '.';
const { credentials } = jobData;
const buildCredentials = credentials
? {
buildCredentials: {
keystore: {
dataBase64: credentials.keystore.keystore,
keystorePassword: credentials.keystore.keystorePassword,
keyAlias: credentials.keystore.keyAlias,
keyPassword: credentials.keystore.keyPassword,
},
},
}
: {};
let buildType = buildProfile.buildType;
if (!buildType && !buildProfile.gradleCommand && buildProfile.distribution === 'internal') {
buildType = Android.BuildType.APK;
}
const maybeCustomBuildConfigPath = buildProfile.config
? getCustomBuildConfigPathForJob(buildProfile.config)
: undefined;
let buildMode;
if (ctx.repack) {
buildMode = BuildMode.REPACK;
} else if (buildProfile.config) {
buildMode = BuildMode.CUSTOM;
} else {
buildMode = BuildMode.BUILD;
}
const job: Android.Job = {
type: ctx.workflow,
platform: Platform.ANDROID,
projectRootDirectory,
projectArchive: jobData.projectArchive,
builderEnvironment: {
image: buildProfile.image,
node: buildProfile.node,
pnpm: buildProfile.pnpm,
bun: buildProfile.bun,
yarn: buildProfile.yarn,
ndk: buildProfile.ndk,
env: buildProfile.env,
},
cache: {
...cacheDefaults,
...buildProfile.cache,
clear: ctx.clearCache,
},
secrets: {
...buildCredentials,
},
updates: { channel: buildProfile.channel },
developmentClient: buildProfile.developmentClient,
gradleCommand: buildProfile.gradleCommand,
applicationArchivePath: buildProfile.applicationArchivePath ?? buildProfile.artifactPath,
buildArtifactPaths: buildProfile.buildArtifactPaths,
buildType,
username,
...(ctx.android.versionCodeOverride && {
version: {
versionCode: ctx.android.versionCodeOverride,
},
}),
experimental: {
prebuildCommand: buildProfile.prebuildCommand,
},
mode: buildMode,
triggeredBy: BuildTrigger.EAS_CLI,
...(maybeCustomBuildConfigPath && {
customBuildConfig: {
path: maybeCustomBuildConfigPath,
},
}),
...(ctx.repack && {
customBuildConfig: {
path: '__eas/repack.yml',
},
}),
loggerLevel: ctx.loggerLevel,
};
return sanitizeBuildJob(job) as Android.Job;
}
@@ -1,72 +0,0 @@
import { ExpoConfig } from '@expo/config';
import { AndroidConfig } from '@expo/config-plugins';
import { Env, Platform, Workflow } from '@expo/eas-build-job';
import { AndroidVersionAutoIncrement } from '@expo/eas-json';
import chalk from 'chalk';
import fs from 'fs-extra';
import path from 'path';
import { BumpStrategy, bumpVersionAsync, bumpVersionInAppJsonAsync } from './version';
import Log from '../../log';
import { isExpoUpdatesInstalled } from '../../project/projectUtils';
import { resolveWorkflowAsync } from '../../project/workflow';
import { syncUpdatesConfigurationAsync } from '../../update/android/UpdatesModule';
import { Client } from '../../vcs/vcs';
export async function syncProjectConfigurationAsync({
projectDir,
exp,
localAutoIncrement,
vcsClient,
env,
}: {
projectDir: string;
exp: ExpoConfig;
localAutoIncrement?: AndroidVersionAutoIncrement;
vcsClient: Client;
env: Env | undefined;
}): Promise<void> {
const workflow = await resolveWorkflowAsync(projectDir, Platform.ANDROID, vcsClient);
const versionBumpStrategy = resolveVersionBumpStrategy(localAutoIncrement ?? false);
if (workflow === Workflow.GENERIC) {
await cleanUpOldEasBuildGradleScriptAsync(projectDir);
if (isExpoUpdatesInstalled(projectDir)) {
await syncUpdatesConfigurationAsync({ projectDir, exp, workflow, env });
}
await bumpVersionAsync({ projectDir, exp, bumpStrategy: versionBumpStrategy });
} else {
await bumpVersionInAppJsonAsync({ projectDir, exp, bumpStrategy: versionBumpStrategy });
}
}
function resolveVersionBumpStrategy(autoIncrement: AndroidVersionAutoIncrement): BumpStrategy {
if (autoIncrement === true) {
return BumpStrategy.VERSION_CODE;
} else if (autoIncrement === false) {
return BumpStrategy.NOOP;
} else if (autoIncrement === 'versionCode') {
return BumpStrategy.VERSION_CODE;
} else {
return BumpStrategy.APP_VERSION;
}
}
// TODO: remove this after a few months
export async function cleanUpOldEasBuildGradleScriptAsync(projectDir: string): Promise<void> {
const easBuildGradlePath = path.join(projectDir, 'android', 'app', 'eas-build.gradle');
if (await fs.pathExists(easBuildGradlePath)) {
Log.withTick(`Removing ${chalk.bold('eas-build.gradle')} as it's not longer necessary`);
await fs.remove(easBuildGradlePath);
const buildGradlePath = AndroidConfig.Paths.getAppBuildGradleFilePath(projectDir);
const buildGradleContents = await fs.readFile(buildGradlePath, 'utf-8');
const buildGradleContentsWithoutApply = buildGradleContents.replace(
/apply from: ["'].\/eas-build.gradle["']\n/,
''
);
if (buildGradleContentsWithoutApply !== buildGradleContents) {
await fs.writeFile(buildGradlePath, buildGradleContentsWithoutApply);
}
}
}
@@ -1,278 +0,0 @@
import { ExpoConfig } from '@expo/config';
import { AndroidConfig, Updates } from '@expo/config-plugins';
import { Platform, Workflow } from '@expo/eas-build-job';
import { BuildProfile } from '@expo/eas-json';
import chalk from 'chalk';
import fs from 'fs-extra';
import { ExpoGraphqlClient } from '../../commandUtils/context/contextUtils/createGraphqlClient';
import { AppPlatform } from '../../graphql/generated';
import { AppVersionMutation } from '../../graphql/mutations/AppVersionMutation';
import { AppVersionQuery } from '../../graphql/queries/AppVersionQuery';
import Log from '../../log';
import { ora } from '../../ora';
import {
getAppBuildGradleAsync,
parseGradleCommand,
resolveConfigValue,
} from '../../project/android/gradleUtils';
import { getNextVersionCode } from '../../project/android/versions';
import { resolveWorkflowAsync } from '../../project/workflow';
import { Client } from '../../vcs/vcs';
import { updateAppJsonConfigAsync } from '../utils/appJson';
import { bumpAppVersionAsync, ensureStaticConfigExists } from '../utils/version';
export enum BumpStrategy {
APP_VERSION,
VERSION_CODE,
NOOP,
}
export async function bumpVersionAsync({
bumpStrategy,
projectDir,
exp,
}: {
projectDir: string;
exp: ExpoConfig;
bumpStrategy: BumpStrategy;
}): Promise<void> {
if (bumpStrategy === BumpStrategy.NOOP) {
return;
}
ensureStaticConfigExists(projectDir);
const buildGradle = await getAppBuildGradleAsync(projectDir);
const isMultiFlavor =
buildGradle.android?.productFlavors || buildGradle.android?.flavorDimensions;
if (isMultiFlavor) {
throw new Error(
'Automatic version bumping is not supported for multi-flavor Android projects.'
);
}
await bumpVersionInAppJsonAsync({ bumpStrategy, projectDir, exp });
Log.log('Updated versions in app.json');
await updateNativeVersionsAsync({
projectDir,
version: exp.version,
versionCode: exp.android?.versionCode,
});
Log.log('Synchronized versions with build gradle');
}
export async function bumpVersionInAppJsonAsync({
bumpStrategy,
projectDir,
exp,
}: {
bumpStrategy: BumpStrategy;
projectDir: string;
exp: ExpoConfig;
}): Promise<void> {
if (bumpStrategy === BumpStrategy.NOOP) {
return;
}
ensureStaticConfigExists(projectDir);
Log.addNewLineIfNone();
if (bumpStrategy === BumpStrategy.APP_VERSION) {
const appVersion = AndroidConfig.Version.getVersionName(exp) ?? '1.0.0';
await bumpAppVersionAsync({ appVersion, projectDir, exp });
} else {
const versionCode = AndroidConfig.Version.getVersionCode(exp);
const bumpedVersionCode = getNextVersionCode(versionCode);
Log.log(
`Bumping ${chalk.bold('expo.android.versionCode')} from ${chalk.bold(
versionCode
)} to ${chalk.bold(bumpedVersionCode)}`
);
await updateAppJsonConfigAsync({ projectDir, exp }, config => {
config.android = { ...config.android, versionCode: bumpedVersionCode };
});
}
}
export async function maybeResolveVersionsAsync(
projectDir: string,
exp: ExpoConfig,
buildProfile: BuildProfile<Platform.ANDROID>,
vcsClient: Client
): Promise<{ appVersion?: string; appBuildVersion?: string }> {
const workflow = await resolveWorkflowAsync(projectDir, Platform.ANDROID, vcsClient);
if (workflow === Workflow.GENERIC) {
const buildGradle = await getAppBuildGradleAsync(projectDir);
try {
const parsedGradleCommand = buildProfile.gradleCommand
? parseGradleCommand(buildProfile.gradleCommand, buildGradle)
: undefined;
return {
appVersion:
resolveConfigValue(buildGradle, 'versionName', parsedGradleCommand?.flavor) ?? '1.0.0',
appBuildVersion:
resolveConfigValue(buildGradle, 'versionCode', parsedGradleCommand?.flavor) ?? '1',
};
} catch {
return {};
}
} else {
return {
appBuildVersion: String(AndroidConfig.Version.getVersionCode(exp)),
appVersion: exp.version,
};
}
}
export async function updateNativeVersionsAsync({
projectDir,
version,
versionCode,
}: {
projectDir: string;
version?: string;
versionCode?: number;
}): Promise<void> {
const buildGradle = await readBuildGradleAsync(projectDir);
if (!buildGradle) {
throw new Error('This project is missing a build.gradle file.');
}
let updatedBuildGradle = buildGradle;
if (version !== undefined) {
updatedBuildGradle = updatedBuildGradle.replace(
new RegExp(`versionName ".*"`),
`versionName "${version}"`
);
}
if (versionCode !== undefined) {
updatedBuildGradle = updatedBuildGradle.replace(
new RegExp(`versionCode.*`),
`versionCode ${versionCode}`
);
}
await writeBuildGradleAsync({ projectDir, buildGradle: updatedBuildGradle });
}
async function readBuildGradleAsync(projectDir: string): Promise<string | undefined> {
const buildGradlePath = AndroidConfig.Paths.getAppBuildGradleFilePath(projectDir);
if (!(await fs.pathExists(buildGradlePath))) {
return undefined;
}
return await fs.readFile(buildGradlePath, 'utf8');
}
async function writeBuildGradleAsync({
projectDir,
buildGradle,
}: {
projectDir: string;
buildGradle: string;
}): Promise<void> {
const buildGradlePath = AndroidConfig.Paths.getAppBuildGradleFilePath(projectDir);
await fs.writeFile(buildGradlePath, buildGradle);
}
/**
* Returns buildNumber that will be used for the next build. If current build profile
* has an 'autoIncrement' option set, it increments the version on server.
*/
export async function resolveRemoteVersionCodeAsync(
graphqlClient: ExpoGraphqlClient,
{
projectDir,
projectId,
exp,
applicationId,
buildProfile,
vcsClient,
}: {
projectDir: string;
projectId: string;
exp: ExpoConfig;
applicationId: string;
buildProfile: BuildProfile<Platform.ANDROID>;
vcsClient: Client;
}
): Promise<string> {
const remoteVersions = await AppVersionQuery.latestVersionAsync(
graphqlClient,
projectId,
AppPlatform.Android,
applicationId
);
const localVersions = await maybeResolveVersionsAsync(projectDir, exp, buildProfile, vcsClient);
let currentBuildVersion: string;
if (remoteVersions?.buildVersion) {
currentBuildVersion = remoteVersions.buildVersion;
} else {
if (localVersions.appBuildVersion) {
Log.warn(
'No remote versions are configured for this project, versionCode will be initialized based on the value from the local project.'
);
currentBuildVersion = localVersions.appBuildVersion;
} else {
Log.error(
`Remote versions are not configured and EAS CLI was not able to read the current version from your project. Use "eas build:version:set" to initialize remote versions.`
);
throw new Error('Remote versions are not configured.');
}
}
if (!buildProfile.autoIncrement && remoteVersions?.buildVersion) {
return currentBuildVersion;
} else if (!buildProfile.autoIncrement && !remoteVersions?.buildVersion) {
const spinner = ora(
`Initializing versionCode with ${chalk.bold(currentBuildVersion)}.`
).start();
try {
await AppVersionMutation.createAppVersionAsync(graphqlClient, {
appId: projectId,
platform: AppPlatform.Android,
applicationIdentifier: applicationId,
storeVersion: localVersions.appVersion ?? exp.version ?? '1.0.0',
buildVersion: currentBuildVersion,
runtimeVersion:
(await Updates.getRuntimeVersionNullableAsync(projectDir, exp, Platform.ANDROID)) ??
undefined,
});
spinner.succeed(`Initialized versionCode with ${chalk.bold(currentBuildVersion)}.`);
} catch (err) {
spinner.fail(`Failed to initialize versionCode with ${chalk.bold(currentBuildVersion)}.`);
throw err;
}
return currentBuildVersion;
} else {
const nextBuildVersion = getNextVersionCode(currentBuildVersion);
const spinner = ora(
`Incrementing versionCode from ${chalk.bold(currentBuildVersion)} to ${chalk.bold(
nextBuildVersion
)}.`
).start();
try {
await AppVersionMutation.createAppVersionAsync(graphqlClient, {
appId: projectId,
platform: AppPlatform.Android,
applicationIdentifier: applicationId,
storeVersion: localVersions.appVersion ?? exp.version ?? '1.0.0',
buildVersion: String(nextBuildVersion),
runtimeVersion:
(await Updates.getRuntimeVersionNullableAsync(projectDir, exp, Platform.ANDROID)) ??
undefined,
});
spinner.succeed(
`Incremented versionCode from ${chalk.bold(currentBuildVersion)} to ${chalk.bold(
nextBuildVersion
)}.`
);
} catch (err) {
spinner.fail(
`Failed to increment versionCode from ${chalk.bold(currentBuildVersion)} to ${chalk.bold(
nextBuildVersion
)}.`
);
throw err;
}
return String(nextBuildVersion);
}
}
-651
View File
@@ -1,651 +0,0 @@
import {
ArchiveSource,
ArchiveSourceType,
BuildJob,
Metadata,
Platform,
} from '@expo/eas-build-job';
import { CredentialsSource } from '@expo/eas-json';
import assert from 'assert';
import chalk from 'chalk';
import cliProgress from 'cli-progress';
import fs from 'fs-extra';
import { GraphQLError } from 'graphql/error';
import nullthrows from 'nullthrows';
import { BuildContext } from './context';
import {
EasBuildDownForMaintenanceError,
EasBuildFreeTierDisabledAndroidError,
EasBuildFreeTierDisabledError,
EasBuildFreeTierDisabledIOSError,
EasBuildFreeTierIosLimitExceededError,
EasBuildFreeTierLimitExceededError,
EasBuildLegacyResourceClassNotAvailableError,
EasBuildProjectArchiveUploadError,
EasBuildResourceClassNotAvailableInFreeTierError,
EasBuildTooManyPendingBuildsError,
RequestValidationError,
TurtleDeprecatedJobFormatError,
} from './errors';
import { transformMetadata } from './graphql';
import { LocalBuildMode, runLocalBuildAsync } from './local';
import { collectMetadataAsync } from './metadata';
import { printDeprecationWarnings } from './utils/printBuildInfo';
import {
LocalFile,
makeProjectMetadataFileAsync,
makeProjectTarballAsync,
reviewAndCommitChangesAsync,
} from './utils/repository';
import { BuildEvent } from '../analytics/AnalyticsManager';
import { withAnalyticsAsync } from '../analytics/common';
import { getExpoWebsiteBaseUrl } from '../api';
import { ExpoGraphqlClient } from '../commandUtils/context/contextUtils/createGraphqlClient';
import { EasCommandError } from '../commandUtils/errors';
import {
AppPlatform,
BuildFragment,
BuildParamsInput,
BuildPriority,
BuildStatus,
UploadSessionType,
} from '../graphql/generated';
import { BuildMutation, BuildResult } from '../graphql/mutations/BuildMutation';
import { BuildQuery } from '../graphql/queries/BuildQuery';
import Log, { learnMore, link } from '../log';
import { Ora, ora } from '../ora';
import {
appPlatformDisplayNames,
appPlatformEmojis,
requestedPlatformDisplayNames,
} from '../platform';
import { uploadFileAtPathToGCSAsync } from '../uploads';
import { formatBytes } from '../utils/files';
import { printJsonOnlyOutput } from '../utils/json';
import { createProgressTracker } from '../utils/progress';
import { sleepAsync } from '../utils/promise';
export interface CredentialsResult<Credentials> {
source: CredentialsSource.LOCAL | CredentialsSource.REMOTE;
credentials: Credentials;
}
export interface JobData<Credentials> {
credentials?: Credentials;
projectArchive: ArchiveSource;
}
interface Builder<TPlatform extends Platform, Credentials, TJob extends BuildJob> {
ctx: BuildContext<TPlatform>;
ensureCredentialsAsync(
ctx: BuildContext<TPlatform>
): Promise<CredentialsResult<Credentials> | undefined>;
syncProjectConfigurationAsync(ctx: BuildContext<TPlatform>): Promise<void>;
prepareJobAsync(ctx: BuildContext<TPlatform>, jobData: JobData<Credentials>): Promise<BuildJob>;
sendBuildRequestAsync(
appId: string,
job: TJob,
metadata: Metadata,
buildParams: BuildParamsInput
): Promise<BuildResult>;
}
export type BuildRequestSender = () => Promise<BuildFragment | undefined>;
function resolveBuildParamsInput<T extends Platform>(
ctx: BuildContext<T>,
metadata: Metadata
): BuildParamsInput {
return {
resourceClass: ctx.resourceClass,
sdkVersion: metadata.sdkVersion,
reactNativeVersion: metadata.reactNativeVersion,
};
}
export async function prepareBuildRequestForPlatformAsync<
TPlatform extends Platform,
Credentials,
TJob extends BuildJob,
>(builder: Builder<TPlatform, Credentials, TJob>): Promise<BuildRequestSender> {
const { ctx } = builder;
const credentialsResult = await withAnalyticsAsync(
ctx.analytics,
async () => await builder.ensureCredentialsAsync(ctx),
{
attemptEvent: BuildEvent.GATHER_CREDENTIALS_ATTEMPT,
successEvent: BuildEvent.GATHER_CREDENTIALS_SUCCESS,
failureEvent: BuildEvent.GATHER_CREDENTIALS_FAIL,
properties: ctx.analyticsEventProperties,
}
);
await withAnalyticsAsync(
ctx.analytics,
async () => await builder.syncProjectConfigurationAsync(ctx),
{
attemptEvent: BuildEvent.CONFIGURE_PROJECT_ATTEMPT,
successEvent: BuildEvent.CONFIGURE_PROJECT_SUCCESS,
failureEvent: BuildEvent.CONFIGURE_PROJECT_FAIL,
properties: ctx.analyticsEventProperties,
}
);
if (await ctx.vcsClient.isCommitRequiredAsync()) {
Log.addNewLineIfNone();
await reviewAndCommitChangesAsync(
ctx.vcsClient,
`[EAS Build] Run EAS Build for ${requestedPlatformDisplayNames[ctx.platform as Platform]}`,
{ nonInteractive: ctx.nonInteractive }
);
}
let projectArchive: ArchiveSource | undefined;
if (ctx.localBuildOptions.localBuildMode === LocalBuildMode.LOCAL_BUILD_PLUGIN) {
const projectPath = (await makeProjectTarballAsync(ctx.vcsClient)).path;
projectArchive = {
type: ArchiveSourceType.PATH,
path: projectPath,
};
} else if (ctx.localBuildOptions.localBuildMode === LocalBuildMode.INTERNAL) {
projectArchive = {
type: ArchiveSourceType.PATH,
path: process.cwd(),
};
} else if (!ctx.localBuildOptions.localBuildMode) {
projectArchive = {
type: ArchiveSourceType.GCS,
...(await uploadProjectAsync(ctx)),
};
}
assert(projectArchive);
const metadata = await collectMetadataAsync(ctx);
const buildParams = resolveBuildParamsInput(ctx, metadata);
const job = await builder.prepareJobAsync(ctx, {
projectArchive,
credentials: credentialsResult?.credentials,
});
return async () => {
if (ctx.localBuildOptions.localBuildMode === LocalBuildMode.LOCAL_BUILD_PLUGIN) {
await runLocalBuildAsync(job, metadata, ctx.localBuildOptions);
return undefined;
} else if (ctx.localBuildOptions.localBuildMode === LocalBuildMode.INTERNAL) {
await BuildMutation.updateBuildMetadataAsync(ctx.graphqlClient, {
buildId: nullthrows(process.env.EAS_BUILD_ID),
metadata: transformMetadata(metadata),
});
printJsonOnlyOutput({ job, metadata });
return undefined;
} else if (!ctx.localBuildOptions.localBuildMode) {
try {
return await sendBuildRequestAsync(builder, job, metadata, buildParams);
} catch (error: any) {
handleBuildRequestError(error, job.platform);
}
} else {
throw new Error('Unknown localBuildMode.');
}
};
}
const SERVER_SIDE_DEFINED_ERRORS: Record<string, typeof EasCommandError> = {
TURTLE_DEPRECATED_JOB_FORMAT: TurtleDeprecatedJobFormatError,
EAS_BUILD_FREE_TIER_DISABLED: EasBuildFreeTierDisabledError,
EAS_BUILD_FREE_TIER_DISABLED_IOS: EasBuildFreeTierDisabledIOSError,
EAS_BUILD_FREE_TIER_DISABLED_ANDROID: EasBuildFreeTierDisabledAndroidError,
EAS_BUILD_FREE_TIER_LIMIT_EXCEEDED: EasBuildFreeTierLimitExceededError,
EAS_BUILD_FREE_TIER_IOS_LIMIT_EXCEEDED: EasBuildFreeTierIosLimitExceededError,
EAS_BUILD_RESOURCE_CLASS_NOT_AVAILABLE_IN_FREE_TIER:
EasBuildResourceClassNotAvailableInFreeTierError,
EAS_BUILD_LEGACY_RESOURCE_CLASS_NOT_AVAILABLE: EasBuildLegacyResourceClassNotAvailableError,
VALIDATION_ERROR: RequestValidationError,
};
export function handleBuildRequestError(error: any, platform: Platform): never {
Log.debug(JSON.stringify(error.graphQLErrors, null, 2));
const graphQLErrorCode: string = error?.graphQLErrors?.[0]?.extensions?.errorCode;
if (graphQLErrorCode in SERVER_SIDE_DEFINED_ERRORS) {
const ErrorClass: typeof EasCommandError = SERVER_SIDE_DEFINED_ERRORS[graphQLErrorCode];
throw new ErrorClass(error?.graphQLErrors?.[0]?.message);
} else if (graphQLErrorCode === 'EAS_BUILD_DOWN_FOR_MAINTENANCE') {
throw new EasBuildDownForMaintenanceError(
`EAS Build is down for maintenance. Try again later. Check ${link(
'https://status.expo.dev/'
)} for updates.`
);
} else if (graphQLErrorCode === 'EAS_BUILD_TOO_MANY_PENDING_BUILDS') {
throw new EasBuildTooManyPendingBuildsError(
`You have already reached the maximum number of pending ${requestedPlatformDisplayNames[platform]} builds for your account. Try again later.`
);
} else if (error?.graphQLErrors) {
const errorMessage = error.graphQLErrors
.map((graphQLError: GraphQLError) => {
const requestIdLine = graphQLError?.extensions?.requestId
? `\nRequest ID: ${graphQLError.extensions.requestId}`
: '';
const errorMessageLine = graphQLError?.message
? `\nError message: ${graphQLError.message}`
: '';
return `${requestIdLine}${errorMessageLine}`;
})
.join('');
throw new Error(
`Build request failed. Make sure you are using the latest eas-cli version. If the problem persists, report the issue.${errorMessage}`
);
}
throw error;
}
async function uploadProjectAsync<TPlatform extends Platform>(
ctx: BuildContext<TPlatform>
): Promise<{
bucketKey: string;
metadataLocation?: string;
}> {
let projectTarballPath;
try {
return await withAnalyticsAsync(
ctx.analytics,
async () => {
Log.newLine();
Log.log(
`Compressing project files and uploading to EAS Build. ${learnMore(
'https://expo.fyi/eas-build-archive'
)}`
);
const projectTarball = await makeProjectTarballAsync(ctx.vcsClient);
if (projectTarball.size > 1024 * 1024 * 100) {
Log.warn(
`Your project archive is ${formatBytes(
projectTarball.size
)}. You can reduce its size and the time it takes to upload by excluding files that are unnecessary for the build process in ${chalk.bold(
'.easignore'
)} file. ${learnMore('https://expo.fyi/eas-build-archive')}`
);
}
if (projectTarball.size > 2 * 1024 * 1024 * 1024) {
throw new Error('Project archive is too big. Maximum allowed size is 2GB.');
}
projectTarballPath = projectTarball.path;
const [bucketKey, { metadataLocation }] = await Promise.all([
uploadFileAtPathToGCSAsync(
ctx.graphqlClient,
UploadSessionType.EasBuildGcsProjectSources,
projectTarball.path,
createProgressTracker({
total: projectTarball.size,
message: ratio =>
`Uploading to EAS Build (${formatBytes(
projectTarball.size * ratio
)} / ${formatBytes(projectTarball.size)})`,
completedMessage: (duration: string) => `Uploaded to EAS ${chalk.dim(duration)}`,
})
),
uploadMetadataFileAsync<TPlatform>(projectTarball, ctx),
]);
if (metadataLocation) {
return { bucketKey, metadataLocation };
}
return { bucketKey };
},
{
attemptEvent: BuildEvent.PROJECT_UPLOAD_ATTEMPT,
successEvent: BuildEvent.PROJECT_UPLOAD_SUCCESS,
failureEvent: BuildEvent.PROJECT_UPLOAD_FAIL,
properties: ctx.analyticsEventProperties,
}
);
} catch (err: any) {
let errMessage = 'Failed to upload the project tarball to EAS Build';
if (err.message) {
errMessage += `\n\nReason: ${err.message}`;
}
throw new EasBuildProjectArchiveUploadError(errMessage);
} finally {
if (projectTarballPath) {
await fs.remove(projectTarballPath);
}
}
}
async function uploadMetadataFileAsync<TPlatform extends Platform>(
projectTarball: LocalFile,
ctx: BuildContext<TPlatform>
): Promise<{ metadataLocation: string | null }> {
let projectMetadataFile: LocalFile | null = null;
try {
projectMetadataFile = await makeProjectMetadataFileAsync(projectTarball.path);
const metadataLocation = await uploadFileAtPathToGCSAsync(
ctx.graphqlClient,
UploadSessionType.EasBuildGcsProjectMetadata,
projectMetadataFile.path
);
return { metadataLocation };
} catch (err: any) {
let errMessage = 'Failed to upload metadata to EAS Build';
if (err.message) {
errMessage += `\n\nReason: ${err.message}`;
}
Log.warn(errMessage);
return { metadataLocation: null };
} finally {
if (projectMetadataFile) {
await fs.remove(projectMetadataFile.path);
}
}
}
async function sendBuildRequestAsync<
TPlatform extends Platform,
Credentials,
TJob extends BuildJob,
>(
builder: Builder<TPlatform, Credentials, TJob>,
job: TJob,
metadata: Metadata,
buildParams: BuildParamsInput
): Promise<BuildFragment> {
const { ctx } = builder;
return await withAnalyticsAsync(
ctx.analytics,
async () => {
if (Log.isDebug) {
Log.log(`Starting ${requestedPlatformDisplayNames[job.platform]} build`);
}
const { build, deprecationInfo } = await builder.sendBuildRequestAsync(
ctx.projectId,
job,
metadata,
buildParams
);
printDeprecationWarnings(deprecationInfo);
return build;
},
{
attemptEvent: BuildEvent.BUILD_REQUEST_ATTEMPT,
successEvent: BuildEvent.BUILD_REQUEST_SUCCESS,
failureEvent: BuildEvent.BUILD_REQUEST_FAIL,
properties: ctx.analyticsEventProperties,
}
);
}
export type MaybeBuildFragment = BuildFragment | null;
export async function waitForBuildEndAsync(
graphqlClient: ExpoGraphqlClient,
{ buildIds, accountName }: { buildIds: string[]; accountName: string },
{ intervalSec = 10 } = {}
): Promise<MaybeBuildFragment[]> {
let spinner;
let originalSpinnerText;
if (buildIds.length === 1) {
Log.log('Waiting for build to complete. You can press Ctrl+C to exit.');
originalSpinnerText = 'Waiting for build to complete.';
spinner = ora(originalSpinnerText).start();
} else {
originalSpinnerText = 'Waiting for builds to complete. You can press Ctrl+C to exit.';
spinner = ora('Waiting for builds to complete. You can press Ctrl+C to exit.').start();
}
while (true) {
const builds = await getBuildsSafelyAsync(graphqlClient, buildIds);
const { refetch } =
builds.length === 1
? await handleSingleBuildProgressAsync({ build: builds[0], accountName }, { spinner })
: await handleMultipleBuildsProgressAsync({ builds }, { spinner, originalSpinnerText });
if (!refetch) {
return builds;
}
await sleepAsync(intervalSec * 1000);
}
}
async function getBuildsSafelyAsync(
graphqlClient: ExpoGraphqlClient,
buildIds: string[]
): Promise<MaybeBuildFragment[]> {
const promises = buildIds.map(async buildId => {
try {
return await BuildQuery.byIdAsync(graphqlClient, buildId, { useCache: false });
} catch (err) {
Log.debug('Failed to fetch the build status', err);
return null;
}
});
return await Promise.all(promises);
}
interface BuildProgressResult {
refetch: boolean;
}
let queueProgressBarStarted = false;
const queueProgressBar = new cliProgress.SingleBar(
{ format: '|{bar}| {estimatedWaitTime}' },
cliProgress.Presets.rect
);
async function handleSingleBuildProgressAsync(
{
build,
accountName,
}: {
build: MaybeBuildFragment;
accountName: string;
},
{ spinner }: { spinner: Ora }
): Promise<BuildProgressResult> {
if (build === null) {
spinner.text = 'Could not fetch the build status. Check your network connection.';
return { refetch: true };
}
if (queueProgressBarStarted && build?.status && build.status !== BuildStatus.InQueue) {
if (build.status === BuildStatus.InProgress) {
queueProgressBar.update(queueProgressBar.getTotal(), {
estimatedWaitTime: '',
});
}
queueProgressBar.stop();
Log.newLine();
queueProgressBarStarted = false;
spinner.start('Build is about to start');
}
switch (build.status) {
case BuildStatus.Finished:
spinner.succeed('Build finished');
return { refetch: false };
case BuildStatus.New:
spinner.text = `Build is waiting to enter the queue. Check your concurrency limit at ${link(
formatAccountSubscriptionsUrl(accountName)
)}.`;
break;
case BuildStatus.InQueue: {
spinner.text = 'Build queued...';
const progressBarPayload =
typeof build.estimatedWaitTimeLeftSeconds === 'number'
? { estimatedWaitTime: formatEstimatedWaitTime(build.estimatedWaitTimeLeftSeconds) }
: { estimatedWaitTime: '' };
if (
!queueProgressBarStarted &&
typeof build.initialQueuePosition === 'number' &&
typeof build.queuePosition === 'number' &&
typeof build.estimatedWaitTimeLeftSeconds === 'number'
) {
spinner.stopAndPersist();
if (build.priority !== BuildPriority.High) {
Log.newLine();
Log.log('Start builds sooner in the priority queue.');
Log.log(
`Sign up for EAS Production or Enterprise at ${link(
formatAccountSubscriptionsUrl(accountName)
)}`
);
}
Log.newLine();
Log.log(`Waiting in ${priorityToQueueDisplayName[build.priority]}`);
queueProgressBar.start(
build.initialQueuePosition + 1,
build.initialQueuePosition - build.queuePosition + 1,
progressBarPayload
);
queueProgressBarStarted = true;
}
if (typeof build.queuePosition === 'number') {
queueProgressBar.update(build.queuePosition, progressBarPayload);
}
break;
}
case BuildStatus.PendingCancel:
case BuildStatus.Canceled:
spinner.fail('Build canceled');
return { refetch: false };
case BuildStatus.InProgress:
spinner.text = 'Build in progress...';
break;
case BuildStatus.Errored:
spinner.fail('Build failed');
if (build.error) {
return { refetch: false };
} else {
throw new Error('Standalone build failed!');
}
default:
spinner.warn('Unknown status');
throw new Error(`Unknown build status: ${build.status} - aborting!`);
}
return { refetch: true };
}
const priorityToQueueDisplayName: Record<BuildPriority, string> = {
[BuildPriority.Normal]: 'Free tier queue',
[BuildPriority.NormalPlus]: 'Free tier queue',
[BuildPriority.High]: 'priority queue',
};
const statusToDisplayName: Record<BuildStatus, string> = {
[BuildStatus.New]: 'waiting to enter the queue (concurrency limit reached)',
[BuildStatus.InQueue]: 'in queue',
[BuildStatus.InProgress]: 'in progress',
[BuildStatus.PendingCancel]: 'canceled',
[BuildStatus.Canceled]: 'canceled',
[BuildStatus.Finished]: 'finished',
[BuildStatus.Errored]: 'failed',
};
const platforms = [AppPlatform.Android, AppPlatform.Ios];
async function handleMultipleBuildsProgressAsync(
{ builds: maybeBuilds }: { builds: MaybeBuildFragment[] },
{ spinner, originalSpinnerText }: { spinner: Ora; originalSpinnerText: string }
): Promise<BuildProgressResult> {
const buildCount = maybeBuilds.length;
const builds = maybeBuilds.filter<BuildFragment>(isBuildFragment);
const allFinished =
builds.filter(build => build.status === BuildStatus.Finished).length === buildCount;
const allSettled =
builds.filter(build =>
[
BuildStatus.Finished,
BuildStatus.Errored,
BuildStatus.Canceled,
BuildStatus.PendingCancel,
].includes(build.status)
).length === buildCount;
if (allSettled) {
if (allFinished) {
spinner.succeed(formatSettledBuildsText(builds));
} else {
spinner.fail(formatSettledBuildsText(builds));
}
return { refetch: false };
} else {
spinner.text = formatPendingBuildsText(originalSpinnerText, builds);
return { refetch: true };
}
}
function formatSettledBuildsText(builds: BuildFragment[]): string {
return platforms
.map(platform => {
const build = nullthrows(
builds.find(build => build.platform === platform),
`Build for platform ${platform} must be defined in this context`
);
return `${appPlatformEmojis[platform]} ${
appPlatformDisplayNames[platform]
} build - status: ${chalk.bold(statusToDisplayName[build.status])}`;
})
.join('\n ');
}
function formatPendingBuildsText(originalSpinnerText: string, builds: BuildFragment[]): string {
return [
originalSpinnerText,
...platforms.map(platform => {
const build = builds.find(build => build.platform === platform);
const status = build ? statusToDisplayName[build.status] : 'unknown';
let extraInfo = '';
if (
build?.status === BuildStatus.InQueue &&
typeof build.initialQueuePosition === 'number' &&
typeof build.queuePosition === 'number'
) {
const percent = Math.floor(
((build.initialQueuePosition - build.queuePosition + 1) /
(build.initialQueuePosition + 1)) *
100
);
const estimatedWaitTime =
typeof build.estimatedWaitTimeLeftSeconds === 'number'
? ` - ${formatEstimatedWaitTime(build.estimatedWaitTimeLeftSeconds)}`
: '';
extraInfo = ` - queue progress: ${chalk.bold(`${percent}%`)}${estimatedWaitTime}`;
}
return `${appPlatformEmojis[platform]} ${
appPlatformDisplayNames[platform]
} build - status: ${chalk.bold(status)}${extraInfo}`;
}),
].join('\n ');
}
function isBuildFragment(maybeBuild: MaybeBuildFragment): maybeBuild is BuildFragment {
return maybeBuild !== null;
}
function formatEstimatedWaitTime(estimatedWaitTimeLeftSeconds: number): string {
if (estimatedWaitTimeLeftSeconds < 5 * 60) {
return 'starting soon...';
} else {
const n = Math.floor(estimatedWaitTimeLeftSeconds / (10 * 60)) + 1;
return `starting in about ${n}0 minutes...`;
}
}
function formatAccountSubscriptionsUrl(accountName: string): string {
return new URL(
`/accounts/${accountName}/settings/subscriptions`,
getExpoWebsiteBaseUrl()
).toString();
}
-84
View File
@@ -1,84 +0,0 @@
import { EasJson, EasJsonAccessor } from '@expo/eas-json';
import chalk from 'chalk';
import fs from 'fs-extra';
import { maybeBailOnRepoStatusAsync, reviewAndCommitChangesAsync } from './utils/repository';
import Log, { learnMore } from '../log';
import { easCliVersion } from '../utils/easCli';
import { Client } from '../vcs/vcs';
interface ConfigureParams {
projectDir: string;
nonInteractive: boolean;
vcsClient: Client;
}
export async function easJsonExistsAsync(projectDir: string): Promise<boolean> {
return await fs.pathExists(EasJsonAccessor.formatEasJsonPath(projectDir));
}
/**
* Creates eas.json if it does not exist.
*
* Returns:
* - false - if eas.json already exists
* - true - if eas.json was created by the function
*/
export async function ensureProjectConfiguredAsync(
configureParams: ConfigureParams
): Promise<boolean> {
if (await easJsonExistsAsync(configureParams.projectDir)) {
return false;
}
await configureAsync(configureParams);
return true;
}
async function configureAsync({
projectDir,
nonInteractive,
vcsClient,
}: ConfigureParams): Promise<void> {
await maybeBailOnRepoStatusAsync(vcsClient, nonInteractive);
await createEasJsonAsync(projectDir, vcsClient);
if (await vcsClient.isCommitRequiredAsync()) {
Log.newLine();
await reviewAndCommitChangesAsync(vcsClient, 'Configure EAS Build', {
nonInteractive,
});
}
}
const EAS_JSON_DEFAULT: EasJson = {
cli: {
version: `>= ${easCliVersion}`,
},
build: {
development: {
developmentClient: true,
distribution: 'internal',
},
preview: {
distribution: 'internal',
},
production: {},
},
submit: {
production: {},
},
};
async function createEasJsonAsync(projectDir: string, vcsClient: Client): Promise<void> {
const easJsonPath = EasJsonAccessor.formatEasJsonPath(projectDir);
await fs.writeFile(easJsonPath, `${JSON.stringify(EAS_JSON_DEFAULT, null, 2)}\n`);
await vcsClient.trackFileAsync(easJsonPath);
Log.withTick(
`Generated ${chalk.bold('eas.json')}. ${learnMore(
'https://docs.expo.dev/build-reference/eas-json/'
)}`
);
}
Loaded 100 of 856 files, more files were not shown because too many files have changed in this diff. Show more