ant-go 0.1.22
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +242 -0
- package/bin/ant-go.js +157 -0
- package/docs/add-device-flow.md +207 -0
- package/package.json +24 -0
- package/src/api.js +64 -0
- package/src/apple-creds.js +687 -0
- package/src/commands/auth.js +293 -0
- package/src/commands/build.js +516 -0
- package/src/commands/configure.js +31 -0
- package/src/commands/status.js +50 -0
- package/src/config.js +68 -0
- package/src/i18n.js +584 -0
- package/src/logger.js +14 -0
- package/src/update-check.js +106 -0
package/README.md
ADDED
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
# ant-go CLI
|
|
2
|
+
|
|
3
|
+
Build iOS and Android apps with a single command — no complex CI/CD setup required.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install -g ant-go
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Upgrading
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
npm install -g ant-go@latest
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
To check your current version:
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
ant --version
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
> The CLI automatically notifies you when a newer version is available after each command.
|
|
24
|
+
|
|
25
|
+
## Getting Started
|
|
26
|
+
|
|
27
|
+
### 1. Login
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
# Login with email/password
|
|
31
|
+
ant auth login
|
|
32
|
+
|
|
33
|
+
# Or login with Google via browser
|
|
34
|
+
ant auth login --browser
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
### 2. Add `projectId` to `app.json`
|
|
38
|
+
|
|
39
|
+
Get your Project ID from [antgo.work](https://antgo.work) after creating a project.
|
|
40
|
+
|
|
41
|
+
```json
|
|
42
|
+
{
|
|
43
|
+
"expo": {
|
|
44
|
+
"extra": {
|
|
45
|
+
"ant": {
|
|
46
|
+
"projectId": "your-project-id"
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
> The CLI automatically detects `bundleId`, `schemeName`, and `xcworkspace` from your project files. No extra configuration needed.
|
|
54
|
+
|
|
55
|
+
### 3. Build
|
|
56
|
+
|
|
57
|
+
```bash
|
|
58
|
+
ant build --platform ios
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
---
|
|
62
|
+
|
|
63
|
+
## Commands
|
|
64
|
+
|
|
65
|
+
### `ant build`
|
|
66
|
+
|
|
67
|
+
Packages your project, uploads it to the build server, and tracks progress.
|
|
68
|
+
|
|
69
|
+
```bash
|
|
70
|
+
ant build --platform ios
|
|
71
|
+
ant build --platform ios --profile development
|
|
72
|
+
ant build --platform ios --auto-submit # auto-submit to TestFlight after build
|
|
73
|
+
ant build --platform ios --reauth # re-login to Apple Developer, clear cache
|
|
74
|
+
ant build --platform ios --refresh-profile # recreate Provisioning Profile
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
| Option | Description |
|
|
78
|
+
|---|---|
|
|
79
|
+
| `--platform <platform>` | `ios` or `android` |
|
|
80
|
+
| `--profile <profile>` | Build profile from `ant.json` (default: `production`) |
|
|
81
|
+
| `--project <path>` | Path to project directory (default: current directory) |
|
|
82
|
+
| `--reauth` | Clear Apple Developer session cache and re-login |
|
|
83
|
+
| `--refresh-profile` | Recreate Provisioning Profile (use when Capabilities change) |
|
|
84
|
+
| `--auto-submit` | Automatically submit IPA to TestFlight after a successful build |
|
|
85
|
+
|
|
86
|
+
---
|
|
87
|
+
|
|
88
|
+
### `ant status <jobId>`
|
|
89
|
+
|
|
90
|
+
Check the status of a build job.
|
|
91
|
+
|
|
92
|
+
```bash
|
|
93
|
+
ant status abc123xyz
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
```
|
|
97
|
+
Job ID: abc123xyz
|
|
98
|
+
Status: SUCCESS
|
|
99
|
+
Created: 4/27/2026, 10:30:00 AM
|
|
100
|
+
Updated: 4/27/2026, 10:45:12 AM
|
|
101
|
+
IPA: https://storage.googleapis.com/.../MyApp.ipa
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
---
|
|
105
|
+
|
|
106
|
+
### `ant auth login`
|
|
107
|
+
|
|
108
|
+
Login to your antgo.work account. Token is stored at `~/.ant-go/config.json` and valid for 24 hours.
|
|
109
|
+
|
|
110
|
+
```bash
|
|
111
|
+
ant auth login
|
|
112
|
+
ant auth login --browser # Google OAuth
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
### `ant auth logout`
|
|
116
|
+
|
|
117
|
+
Logout and revoke the current token.
|
|
118
|
+
|
|
119
|
+
```bash
|
|
120
|
+
ant auth logout
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
### `ant auth whoami`
|
|
124
|
+
|
|
125
|
+
Display information about the currently logged-in account.
|
|
126
|
+
|
|
127
|
+
```bash
|
|
128
|
+
ant auth whoami
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
```
|
|
132
|
+
Name: John Doe
|
|
133
|
+
Email: dev@example.com
|
|
134
|
+
Plan: Pro
|
|
135
|
+
Credits: 12 / 15
|
|
136
|
+
Expires: 2026-05-01 10:30:00
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
---
|
|
140
|
+
|
|
141
|
+
## Build Profiles (`ant.json`)
|
|
142
|
+
|
|
143
|
+
Place `ant.json` at the root of your project (next to `app.json`). If it doesn't exist, the CLI will create it with default profiles on first run.
|
|
144
|
+
|
|
145
|
+
```json
|
|
146
|
+
{
|
|
147
|
+
"build": {
|
|
148
|
+
"production": {
|
|
149
|
+
"distribution": "store"
|
|
150
|
+
},
|
|
151
|
+
"development": {
|
|
152
|
+
"developmentClient": true,
|
|
153
|
+
"distribution": "internal"
|
|
154
|
+
},
|
|
155
|
+
"preview": {
|
|
156
|
+
"distribution": "internal"
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
| Profile | `distribution` | Use when |
|
|
163
|
+
|---|---|---|
|
|
164
|
+
| `production` | `store` | Releasing to App Store or distributing via TestFlight |
|
|
165
|
+
| `development` | `internal` | Debugging on a real device, connecting to Metro bundler |
|
|
166
|
+
| `preview` | `internal` | Sharing internal test builds before going to the store |
|
|
167
|
+
|
|
168
|
+
> **`distribution: internal`** requires registering device UDIDs beforehand. The CLI guides you through this automatically via QR code.
|
|
169
|
+
|
|
170
|
+
---
|
|
171
|
+
|
|
172
|
+
## Add Device (iOS internal build)
|
|
173
|
+
|
|
174
|
+
When building with `distribution: internal`, the CLI will ask which devices to include. To register a new device:
|
|
175
|
+
|
|
176
|
+
1. CLI displays a **QR code** in the terminal.
|
|
177
|
+
2. Scan the QR code with the **Camera app** on your iPhone (no separate app needed).
|
|
178
|
+
3. iPhone downloads `.mobileconfig` → automatically sends UDID to the server.
|
|
179
|
+
4. CLI receives the UDID, registers it on **Apple Developer Portal**, and creates a new Provisioning Profile.
|
|
180
|
+
|
|
181
|
+
```bash
|
|
182
|
+
ant build --platform ios --profile development
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
---
|
|
186
|
+
|
|
187
|
+
## Auto Submit to TestFlight
|
|
188
|
+
|
|
189
|
+
Add `--auto-submit` to automatically upload your IPA to TestFlight after a successful build (only works with `distribution: store`).
|
|
190
|
+
|
|
191
|
+
```bash
|
|
192
|
+
ant build --platform ios --auto-submit
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
The CLI will automatically:
|
|
196
|
+
1. Generate and cache an **App Store Connect API Key** from your Apple Developer Portal session.
|
|
197
|
+
2. Upload the key to the server for use during submission.
|
|
198
|
+
3. After the build completes, the server triggers a TestFlight submission job.
|
|
199
|
+
|
|
200
|
+
---
|
|
201
|
+
|
|
202
|
+
## How It Works
|
|
203
|
+
|
|
204
|
+
```
|
|
205
|
+
ant build --platform ios
|
|
206
|
+
│
|
|
207
|
+
├── Read app.json + ant.json
|
|
208
|
+
├── Login to Apple Developer (cached 24h)
|
|
209
|
+
├── Create/reuse Distribution Certificate (.p12)
|
|
210
|
+
├── Create/reuse Provisioning Profile
|
|
211
|
+
├── POST /api/builds → receive jobId + signed upload URLs
|
|
212
|
+
├── Upload project.tar.gz → GCS
|
|
213
|
+
├── Upload credentials.json → GCS
|
|
214
|
+
├── POST /api/builds/:id/start
|
|
215
|
+
│
|
|
216
|
+
└── Track at: https://antgo.work/account/app/MyApp/builds/abc123xyz
|
|
217
|
+
|
|
218
|
+
Mac Build Server (automatic):
|
|
219
|
+
├── Download + extract project
|
|
220
|
+
├── npm install + Fastlane + Xcode build
|
|
221
|
+
├── Upload IPA → GCS
|
|
222
|
+
└── Update status + deduct credit → realtime dashboard
|
|
223
|
+
```
|
|
224
|
+
|
|
225
|
+
---
|
|
226
|
+
|
|
227
|
+
## Credential Caching
|
|
228
|
+
|
|
229
|
+
The CLI caches Apple credentials at `~/.ant-go/creds-<profileName>.json` (TTL: 24h):
|
|
230
|
+
|
|
231
|
+
- Apple Developer session
|
|
232
|
+
- Distribution Certificate (`.p12`)
|
|
233
|
+
- Provisioning Profile
|
|
234
|
+
- App Store Connect API Key (no expiry)
|
|
235
|
+
|
|
236
|
+
Use `--reauth` to clear the cache and re-login from scratch.
|
|
237
|
+
|
|
238
|
+
---
|
|
239
|
+
|
|
240
|
+
## Full Documentation
|
|
241
|
+
|
|
242
|
+
👉 **[antgo.work/docs](https://antgo.work/docs)**
|
package/bin/ant-go.js
ADDED
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
const { program } = require('commander');
|
|
4
|
+
const { version } = require('../package.json');
|
|
5
|
+
const { startUpdateCheck } = require('../src/update-check');
|
|
6
|
+
const { setLang, isFirstRun, markFirstRunDone } = require('../src/config');
|
|
7
|
+
const { t, getLang } = require('../src/i18n');
|
|
8
|
+
const chalk = require('chalk');
|
|
9
|
+
|
|
10
|
+
// ── First-run welcome ─────────────────────────────────────────────────────────
|
|
11
|
+
if (isFirstRun()) {
|
|
12
|
+
markFirstRunDone();
|
|
13
|
+
console.log('');
|
|
14
|
+
console.log(chalk.bold.cyan(` 🐜 ant-go CLI v${version}`));
|
|
15
|
+
console.log(chalk.gray(' ─────────────────────────────────'));
|
|
16
|
+
console.log(` ${t('firstRunWelcome')}`);
|
|
17
|
+
console.log('');
|
|
18
|
+
console.log(` ${t('firstRunGetStarted')}`);
|
|
19
|
+
console.log(` ${chalk.cyan('ant auth login')}`);
|
|
20
|
+
console.log('');
|
|
21
|
+
console.log(` ${t('firstRunDocs')} ${chalk.cyan('https://antgo.work/docs')}`);
|
|
22
|
+
console.log('');
|
|
23
|
+
console.log(chalk.yellow(` ${t('firstRunLangHint')}`));
|
|
24
|
+
console.log('');
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// Kick off update check immediately (non-blocking)
|
|
28
|
+
const showUpdateHint = startUpdateCheck();
|
|
29
|
+
|
|
30
|
+
program
|
|
31
|
+
.name('ant')
|
|
32
|
+
.description('CLI to trigger iOS and Android builds')
|
|
33
|
+
.version(version);
|
|
34
|
+
|
|
35
|
+
// ── auth ──────────────────────────────────────────────────────────────────────
|
|
36
|
+
const auth = program
|
|
37
|
+
.command('auth')
|
|
38
|
+
.description('Manage account authentication');
|
|
39
|
+
|
|
40
|
+
auth
|
|
41
|
+
.command('login')
|
|
42
|
+
.description('Login to your ant-go account')
|
|
43
|
+
.option('--browser', 'Login with Google via browser')
|
|
44
|
+
.action(async (options) => {
|
|
45
|
+
const { loginCommand } = require('../src/commands/auth');
|
|
46
|
+
await loginCommand({ browser: !!options.browser });
|
|
47
|
+
await showUpdateHint();
|
|
48
|
+
process.exit(0);
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
auth
|
|
52
|
+
.command('logout')
|
|
53
|
+
.description('Logout from current account')
|
|
54
|
+
.action(async () => {
|
|
55
|
+
const { logoutCommand } = require('../src/commands/auth');
|
|
56
|
+
await logoutCommand();
|
|
57
|
+
await showUpdateHint();
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
auth
|
|
61
|
+
.command('whoami')
|
|
62
|
+
.description('Show current logged-in account info')
|
|
63
|
+
.action(async () => {
|
|
64
|
+
const { whoamiCommand } = require('../src/commands/auth');
|
|
65
|
+
await whoamiCommand();
|
|
66
|
+
await showUpdateHint();
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
// ── build ─────────────────────────────────────────────────────────────────────
|
|
70
|
+
program
|
|
71
|
+
.command('build')
|
|
72
|
+
.description('Trigger a build (ios or android)')
|
|
73
|
+
.option('--platform <platform>', 'Build platform: ios or android')
|
|
74
|
+
.option('--profile <profile>', 'Build profile from ant.json (default: production)', 'production')
|
|
75
|
+
.option('--project <path>', 'Path to project directory (default: current directory)')
|
|
76
|
+
.option('--reauth', 'Clear Apple Developer session cache and re-login')
|
|
77
|
+
.option('--refresh-profile', 'Recreate Provisioning Profile')
|
|
78
|
+
.option('--auto-submit', 'Auto submit IPA to TestFlight after successful build')
|
|
79
|
+
.action(async (options) => {
|
|
80
|
+
const { runBuild } = require('../src/commands/build');
|
|
81
|
+
await runBuild(options);
|
|
82
|
+
await showUpdateHint();
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
// ── status ────────────────────────────────────────────────────────────────────
|
|
86
|
+
program
|
|
87
|
+
.command('status <jobId>')
|
|
88
|
+
.description('Check build job status')
|
|
89
|
+
.action(async (jobId) => {
|
|
90
|
+
const { checkStatus } = require('../src/commands/status');
|
|
91
|
+
await checkStatus(jobId);
|
|
92
|
+
await showUpdateHint();
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
// ── set ───────────────────────────────────────────────────────────────────────
|
|
96
|
+
const set = program
|
|
97
|
+
.command('set')
|
|
98
|
+
.description('Set CLI preferences');
|
|
99
|
+
|
|
100
|
+
set
|
|
101
|
+
.command('lang <lang>')
|
|
102
|
+
.description('Set language: vi (Vietnamese) or en (English)')
|
|
103
|
+
.action((lang) => {
|
|
104
|
+
const valid = ['vi', 'en'];
|
|
105
|
+
if (!valid.includes(lang)) {
|
|
106
|
+
console.log('');
|
|
107
|
+
console.log(chalk.red(` ${t('langInvalid')}`));
|
|
108
|
+
console.log('');
|
|
109
|
+
process.exit(1);
|
|
110
|
+
}
|
|
111
|
+
setLang(lang);
|
|
112
|
+
console.log('');
|
|
113
|
+
console.log(chalk.green(` ${t('langSet', lang)}`));
|
|
114
|
+
console.log(chalk.gray(` ${t('firstRunLangHint')}`));
|
|
115
|
+
console.log('');
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
// ── uninstall ─────────────────────────────────────────────────────────────────
|
|
119
|
+
program
|
|
120
|
+
.command('uninstall')
|
|
121
|
+
.description('Uninstall ant-go CLI from this machine')
|
|
122
|
+
.action(async () => {
|
|
123
|
+
const inquirer = require('inquirer');
|
|
124
|
+
const { execSync } = require('child_process');
|
|
125
|
+
const ora = require('ora');
|
|
126
|
+
|
|
127
|
+
console.log('');
|
|
128
|
+
const { confirm } = await inquirer.prompt([{
|
|
129
|
+
type: 'confirm',
|
|
130
|
+
name: 'confirm',
|
|
131
|
+
message: t('uninstallConfirm'),
|
|
132
|
+
default: false,
|
|
133
|
+
}]);
|
|
134
|
+
|
|
135
|
+
if (!confirm) {
|
|
136
|
+
console.log(chalk.gray(` ${t('uninstallCancelled')}`));
|
|
137
|
+
console.log('');
|
|
138
|
+
process.exit(0);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
const spinner = ora(t('uninstallRunning')).start();
|
|
142
|
+
try {
|
|
143
|
+
execSync('npm uninstall -g ant-go', { stdio: 'ignore' });
|
|
144
|
+
spinner.succeed(chalk.green(t('uninstallDone')));
|
|
145
|
+
console.log('');
|
|
146
|
+
} catch (err) {
|
|
147
|
+
spinner.fail(chalk.red(t('uninstallFailed', err.message)));
|
|
148
|
+
console.log('');
|
|
149
|
+
process.exit(1);
|
|
150
|
+
}
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
program.parse(process.argv);
|
|
154
|
+
|
|
155
|
+
if (!process.argv.slice(2).length) {
|
|
156
|
+
program.outputHelp();
|
|
157
|
+
}
|
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
# Add Device Flow — CLI
|
|
2
|
+
|
|
3
|
+
Luồng đăng ký UDID thiết bị iOS từ CLI, được kích hoạt tự động khi chạy `ant build` với profile `distribution: internal`.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## Tổng quan
|
|
8
|
+
|
|
9
|
+
```
|
|
10
|
+
Developer Machine (CLI) antgo.work API iPhone
|
|
11
|
+
│ │ │
|
|
12
|
+
├── ant build --profile development │
|
|
13
|
+
├── ensureToken() │ │
|
|
14
|
+
│ │ │
|
|
15
|
+
├── GET /api/user/me ─────────────►│ trả plan, quota, devices │
|
|
16
|
+
│◄── { plan, freeBuildsRemaining, │ │
|
|
17
|
+
│ devices: [...] } ───────────┤ │
|
|
18
|
+
├── check quota (free builds?) │ │
|
|
19
|
+
│ │ │
|
|
20
|
+
├── distribution: internal? │ │
|
|
21
|
+
│ └─ selectDevices() │ │
|
|
22
|
+
│ ├─ multi-select UI │ │
|
|
23
|
+
│ └─ "Thêm device mới": │ │
|
|
24
|
+
│ POST /device-enroll/create │
|
|
25
|
+
│◄── { token, enrollUrl } ─────────┤ │
|
|
26
|
+
│ hiện QR code │ │
|
|
27
|
+
│ poll status 3s │◄── iPhone quét QR ────────┤
|
|
28
|
+
│ │ iOS gửi UDID │
|
|
29
|
+
│◄── { status: registered, udid } ◄┤ │
|
|
30
|
+
│ POST /api/devices ►│ lưu Firestore │
|
|
31
|
+
│ loop lại chọn │ │
|
|
32
|
+
│ │ │
|
|
33
|
+
├── Apple login (cache / fresh) │ │
|
|
34
|
+
├── sync UDIDs → Apple Developer (Device.getAsync / createAsync)
|
|
35
|
+
├── Certificate (reuse / tạo mới) │ │
|
|
36
|
+
├── Provisioning Profile({ devices: [id1, id2, ...] }) │
|
|
37
|
+
└── createBuild → upload → start │ │
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
---
|
|
41
|
+
|
|
42
|
+
## Điều kiện kích hoạt
|
|
43
|
+
|
|
44
|
+
Flow này chỉ chạy khi cả hai điều kiện đều đúng:
|
|
45
|
+
|
|
46
|
+
1. `--platform ios`
|
|
47
|
+
2. Profile được chọn có `distribution: "internal"` trong `ant.json`
|
|
48
|
+
|
|
49
|
+
```json
|
|
50
|
+
{
|
|
51
|
+
"build": {
|
|
52
|
+
"development": {
|
|
53
|
+
"distribution": "internal",
|
|
54
|
+
"developmentClient": true
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
Lệnh kích hoạt:
|
|
61
|
+
```bash
|
|
62
|
+
ant build --platform ios --profile development
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
---
|
|
66
|
+
|
|
67
|
+
## Các file liên quan
|
|
68
|
+
|
|
69
|
+
| File | Vai trò |
|
|
70
|
+
|---|---|
|
|
71
|
+
| `cli/src/commands/build.js` | Entry point — fetch user info, check quota, gọi `ensureAppleCreds()` |
|
|
72
|
+
| `cli/src/api.js` | `fetchUserInfo()`, `saveDevice()` |
|
|
73
|
+
| `cli/src/apple-creds.js` | `enrollDevice()`, `selectDevices()`, `ensureAppleCreds()` |
|
|
74
|
+
| `app/api/user/me/route.ts` | Trả user info + danh sách devices (fresh từ Firestore) |
|
|
75
|
+
| `app/api/device-enroll/create/route.ts` | Tạo enrollment session |
|
|
76
|
+
| `app/api/device-enroll/[token]/profile/route.ts` | Trả `.mobileconfig` cho iPhone |
|
|
77
|
+
| `app/api/device-enroll/[token]/complete/route.ts` | Nhận UDID từ iOS |
|
|
78
|
+
| `app/api/device-enroll/[token]/status/route.ts` | CLI poll để biết UDID về chưa |
|
|
79
|
+
| `app/api/devices/route.ts` | Lưu / đọc devices — chấp nhận cả Firebase ID token lẫn CLI token |
|
|
80
|
+
|
|
81
|
+
---
|
|
82
|
+
|
|
83
|
+
## Flow chi tiết
|
|
84
|
+
|
|
85
|
+
### Bước 1 — Fetch user info
|
|
86
|
+
|
|
87
|
+
Ngay sau `ensureToken()`, CLI gọi `GET /api/user/me` với CLI token:
|
|
88
|
+
|
|
89
|
+
```
|
|
90
|
+
Response: {
|
|
91
|
+
plan: "free|starter|pro|team",
|
|
92
|
+
planStatus: "active|past_due|canceled|null",
|
|
93
|
+
freeBuildsRemaining: 5,
|
|
94
|
+
devices: [
|
|
95
|
+
{ udid, name, deviceProduct, deviceSerial, addedAt }
|
|
96
|
+
]
|
|
97
|
+
}
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
Kiểm tra quota:
|
|
101
|
+
- `plan === "free"` và `freeBuildsRemaining <= 0` → báo lỗi, thoát.
|
|
102
|
+
- `planStatus === "past_due"` → hiện cảnh báo thanh toán, tiếp tục.
|
|
103
|
+
|
|
104
|
+
---
|
|
105
|
+
|
|
106
|
+
### Bước 2 — Multi-select device (`selectDevices`)
|
|
107
|
+
|
|
108
|
+
Khi `distribution: internal`, CLI gọi `selectDevices(userDevices, projectId, apiClient)`.
|
|
109
|
+
|
|
110
|
+
**Trường hợp chưa có device nào:** đi thẳng vào enrollment mới.
|
|
111
|
+
|
|
112
|
+
**Trường hợp đã có device:** hiển thị checkbox multi-select:
|
|
113
|
+
|
|
114
|
+
```
|
|
115
|
+
📱 Chọn device để build (Space = chọn/bỏ, Enter = xác nhận):
|
|
116
|
+
|
|
117
|
+
◉ My iPhone iPhone17,1 (00008110-001234...)
|
|
118
|
+
◯ iPad Pro 11" iPad8,1 (00008130-000ABC...)
|
|
119
|
+
─────────────────────────────────────────────────────
|
|
120
|
+
◯ + Thêm device mới
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
**Nếu user chọn "+ Thêm device mới":**
|
|
124
|
+
|
|
125
|
+
1. Chạy `enrollDevice(projectId)`:
|
|
126
|
+
- `POST /api/device-enroll/create` → nhận `token`, `enrollUrl`
|
|
127
|
+
- Hiện QR code trong terminal
|
|
128
|
+
- Poll `GET /api/device-enroll/{token}/status` mỗi 3 giây (max 10 phút)
|
|
129
|
+
- Nhận `{ udid, deviceProduct, deviceSerial }`
|
|
130
|
+
2. Prompt tên device.
|
|
131
|
+
3. `POST /api/devices` → lưu vào Firestore `users/{uid}/devices/{udid}`.
|
|
132
|
+
4. Thêm device mới vào danh sách, **quay lại màn hình chọn**.
|
|
133
|
+
|
|
134
|
+
Return: `string[]` — mảng các UDID được chọn.
|
|
135
|
+
|
|
136
|
+
---
|
|
137
|
+
|
|
138
|
+
### Bước 3 — Đăng nhập Apple Developer
|
|
139
|
+
|
|
140
|
+
`ensureAppleCreds()` xử lý cache (TTL 24h) → login → 2FA → chọn team.
|
|
141
|
+
|
|
142
|
+
---
|
|
143
|
+
|
|
144
|
+
### Bước 4 — Đồng bộ UDIDs lên Apple Developer Portal
|
|
145
|
+
|
|
146
|
+
Với mỗi UDID trong `selectedUdids`:
|
|
147
|
+
|
|
148
|
+
```
|
|
149
|
+
Device.getAsync(authCtx)
|
|
150
|
+
├── UDID đã có trên Apple? → lấy deviceId
|
|
151
|
+
└── Chưa có → Device.createAsync({ name, udid, platform: 'IOS' }) → lấy deviceId mới
|
|
152
|
+
|
|
153
|
+
deviceIds = [id1, id2, ...]
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
---
|
|
157
|
+
|
|
158
|
+
### Bước 5 — Certificate + Provisioning Profile
|
|
159
|
+
|
|
160
|
+
- **Certificate** (Development): reuse nếu còn hợp lệ, tạo mới nếu không.
|
|
161
|
+
- **Provisioning Profile** (Development): tạo với **toàn bộ** `deviceIds`:
|
|
162
|
+
```js
|
|
163
|
+
Profile.createAsync(authCtx, {
|
|
164
|
+
bundleId: bundleIdObj.id,
|
|
165
|
+
certificates: [certId],
|
|
166
|
+
devices: deviceIds, // tất cả device đã chọn
|
|
167
|
+
profileType: ProfileType.IOS_APP_DEVELOPMENT,
|
|
168
|
+
})
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
---
|
|
172
|
+
|
|
173
|
+
### Bước 6 — Build
|
|
174
|
+
|
|
175
|
+
CLI đóng gói project, upload lên GCS và gửi build job — giống hệt flow build bình thường (xem [../../docs/build-flow.md](../../docs/build-flow.md)).
|
|
176
|
+
|
|
177
|
+
---
|
|
178
|
+
|
|
179
|
+
## Cache credentials
|
|
180
|
+
|
|
181
|
+
File: `~/.ant-go/creds-<profileName>.json` (TTL 24h, mode 0600)
|
|
182
|
+
|
|
183
|
+
```json
|
|
184
|
+
{
|
|
185
|
+
"appleId": "dev@example.com",
|
|
186
|
+
"p12Base64": "...",
|
|
187
|
+
"p12Password": "...",
|
|
188
|
+
"mobileprovisionBase64": "...",
|
|
189
|
+
"teamId": "TEAMID123",
|
|
190
|
+
"udids": ["00008110-001234ABCDEF", "00008130-000ABC123"],
|
|
191
|
+
"_savedAt": 1714512345678
|
|
192
|
+
}
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
**Backward compat:** cache cũ có field `udid` (string) vẫn được đọc, convert thành `udids: [udid]`.
|
|
196
|
+
|
|
197
|
+
---
|
|
198
|
+
|
|
199
|
+
## Lỗi thường gặp
|
|
200
|
+
|
|
201
|
+
| Lỗi | Nguyên nhân | Cách xử lý |
|
|
202
|
+
|---|---|---|
|
|
203
|
+
| `Bạn đã hết lượt build miễn phí` | `freeBuildsRemaining <= 0` | Nâng cấp plan tại antgo.work/account/billing |
|
|
204
|
+
| `Device enrollment timeout` | iPhone không quét QR trong 10 phút | Chạy lại lệnh, quét QR nhanh hơn |
|
|
205
|
+
| `Không tạo được enrollment session` | Mất kết nối đến antgo.work | Kiểm tra mạng, thử lại |
|
|
206
|
+
| `App ID không tồn tại trên Apple Developer` | `bundleId` chưa được tạo trên portal | Tạo App ID thủ công tại developer.apple.com |
|
|
207
|
+
| `Đăng nhập thất bại` | Sai Apple ID / password | Kiểm tra App-Specific Password tại appleid.apple.com |
|
package/package.json
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "ant-go",
|
|
3
|
+
"version": "0.1.22",
|
|
4
|
+
"description": "CLI to trigger iOS builds via ant-go build server",
|
|
5
|
+
"main": "src/index.js",
|
|
6
|
+
"bin": {
|
|
7
|
+
"ant": "bin/ant-go.js"
|
|
8
|
+
},
|
|
9
|
+
"scripts": {
|
|
10
|
+
"start": "node bin/ant-go.js"
|
|
11
|
+
},
|
|
12
|
+
"dependencies": {
|
|
13
|
+
"@expo/apple-utils": "^2.1.21",
|
|
14
|
+
"axios": "^1.7.2",
|
|
15
|
+
"chalk": "^4.1.2",
|
|
16
|
+
"commander": "^12.1.0",
|
|
17
|
+
"inquirer": "^8.2.7",
|
|
18
|
+
"ora": "^5.4.1",
|
|
19
|
+
"qrcode-terminal": "^0.12.0"
|
|
20
|
+
},
|
|
21
|
+
"engines": {
|
|
22
|
+
"node": ">=18.0.0"
|
|
23
|
+
}
|
|
24
|
+
}
|
package/src/api.js
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* api.js — HTTP client gọi tới ant Next.js API
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
const axios = require('axios');
|
|
6
|
+
|
|
7
|
+
function createClient(apiUrl, authToken) {
|
|
8
|
+
const headers = { 'Content-Type': 'application/json' };
|
|
9
|
+
if (authToken) headers['Authorization'] = `Bearer ${authToken}`;
|
|
10
|
+
return axios.create({
|
|
11
|
+
baseURL: apiUrl.replace(/\/$/, ''),
|
|
12
|
+
headers,
|
|
13
|
+
timeout: 30_000,
|
|
14
|
+
});
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
// POST /api/builds — tạo build job, nhận signed URL
|
|
18
|
+
async function createBuild(client, payload = {}) {
|
|
19
|
+
const { data } = await client.post('/api/builds', payload);
|
|
20
|
+
return data; // { jobId, uploadUrl }
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// GET /api/builds/:id — lấy status
|
|
24
|
+
async function getBuildStatus(client, jobId) {
|
|
25
|
+
const { data } = await client.get(`/api/builds/${jobId}`);
|
|
26
|
+
return data;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// Lấy user info + devices list (fresh từ Firestore)
|
|
30
|
+
async function fetchUserInfo(client) {
|
|
31
|
+
const { data } = await client.get('/api/user/me');
|
|
32
|
+
return data;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// Lưu device mới vào Firestore sau khi enroll thành công
|
|
36
|
+
async function saveDevice(client, { udid, name, deviceProduct, deviceSerial }) {
|
|
37
|
+
const { data } = await client.post('/api/devices', {
|
|
38
|
+
udid,
|
|
39
|
+
name,
|
|
40
|
+
deviceProduct,
|
|
41
|
+
deviceSerial,
|
|
42
|
+
source: 'cli',
|
|
43
|
+
});
|
|
44
|
+
return data;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// Upload ASC API Key lên dashboard (per-user, per-team)
|
|
48
|
+
async function uploadAscKey(client, { teamId, keyId, issuerId, privateKeyP8 }) {
|
|
49
|
+
const { data } = await client.post('/api/user/asc-key', {
|
|
50
|
+
teamId,
|
|
51
|
+
keyId,
|
|
52
|
+
issuerId,
|
|
53
|
+
privateKeyP8,
|
|
54
|
+
});
|
|
55
|
+
return data;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// Lưu danh sách capabilities đang enabled vào dashboard (per-app)
|
|
59
|
+
async function uploadCapabilities(client, { projectId, capabilities }) {
|
|
60
|
+
const { data } = await client.post(`/api/apps/${projectId}/capabilities`, { capabilities });
|
|
61
|
+
return data;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
module.exports = { createClient, createBuild, getBuildStatus, fetchUserInfo, saveDevice, uploadAscKey, uploadCapabilities };
|