@makefully/adaptfully 3.11.0 → 3.12.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +13 -0
- package/README.md +28 -8
- package/lib/node/apple-publish.js +156 -0
- package/lib/node/google-publish.js +112 -0
- package/lib/node/index.js +24 -1
- package/lib/node/pipeline.js +21 -4
- package/lib/node/publish-credentials.js +220 -0
- package/lib/node/{steam-auth.js → steam-publish.js} +38 -36
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,19 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to this project are documented in this file.
|
|
4
4
|
|
|
5
|
+
## 3.12.0 — 2026-07-23
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
|
|
9
|
+
- **`adaptfully google-publish`** — import a Google Play service-account JSON into `assets/meta/deployments/android/google.json` (or `--deployment` / `--output`) and ensure `manifest.json` has `"type": "google"`.
|
|
10
|
+
- **`adaptfully apple-publish`** — prompt for (or accept) App Store Connect credentials and write `assets/meta/deployments/ios/apple.json` with `"type": "apple"` on the deployment manifest.
|
|
11
|
+
- **Publish helpers update `.gitignore`** — `steam-publish` / `google-publish` / `apple-publish` append standard `assets/meta/deployments/**` credential ignore patterns (and any custom `--output` under the project) so secrets are less likely to be committed.
|
|
12
|
+
|
|
13
|
+
### Changed
|
|
14
|
+
|
|
15
|
+
- **`adaptfully steam-publish`** replaces the credential CLI formerly named `steam-auth` (runtime plugin `steam-auth` is unchanged). `adaptfully steam-auth` still works as a deprecated alias.
|
|
16
|
+
- Steam publish also ensures the deployment `manifest.json` has `"type": "steam"`.
|
|
17
|
+
|
|
5
18
|
## 3.11.0 — 2026-07-23
|
|
6
19
|
|
|
7
20
|
### Added
|
package/README.md
CHANGED
|
@@ -26,7 +26,9 @@ adaptfully build steam # prebuild + zip and send to Wrapf
|
|
|
26
26
|
adaptfully deploy web # prebuild + POST to Wrapfully for each of web's deployments
|
|
27
27
|
adaptfully deploy web --deployment web-prod # deploy to a single named deployment
|
|
28
28
|
adaptfully deploy steam # prebuild + POST to Wrapfully steam builder
|
|
29
|
-
adaptfully steam-
|
|
29
|
+
adaptfully steam-publish # one-time: log in with steamcmd → assets/meta/deployments/steam/steam.json
|
|
30
|
+
adaptfully google-publish --from ./sa.json # one-time: import Play service account → deployments/android/google.json
|
|
31
|
+
adaptfully apple-publish # one-time: write App Store Connect creds → deployments/ios/apple.json
|
|
30
32
|
```
|
|
31
33
|
|
|
32
34
|
| Stage | What it does |
|
|
@@ -34,7 +36,9 @@ adaptfully steam-auth # one-time: log in with steamcmd
|
|
|
34
36
|
| `prebuild` | Copy `deploy/` to `output/<platform>-prebuild/` and inject registrations into `config.htmlInjections` |
|
|
35
37
|
| `build` | Prebuild, then POST the result to Wrapfully (artifact zip only) |
|
|
36
38
|
| `deploy` | POST the prior build artifact from `./output/` to each configured deployment (or `--deployment`) |
|
|
37
|
-
| `steam-
|
|
39
|
+
| `steam-publish` | One-time local setup: install steamcmd, interactive login, write the `steam` deployment's `steam.json` (also updates `.gitignore`) |
|
|
40
|
+
| `google-publish` | One-time local setup: import a Play Console service-account JSON into the `android` deployment folder (also updates `.gitignore`) |
|
|
41
|
+
| `apple-publish` | One-time local setup: write App Store Connect credentials into the `ios` deployment folder (also updates `.gitignore`) |
|
|
38
42
|
|
|
39
43
|
`wrapfully-deploy` is a compatibility alias for `adaptfully deploy` when invoked with a Wrapfully builder name (`steam`, `win`, `android`, etc.).
|
|
40
44
|
|
|
@@ -253,8 +257,10 @@ For web-only hosting (no Wrapfully), stop after prebuild and upload `output/web-
|
|
|
253
257
|
Run from your project root:
|
|
254
258
|
|
|
255
259
|
```bash
|
|
256
|
-
npx adaptfully <prebuild|build|deploy> <platform> [server] [mode]
|
|
257
|
-
npx adaptfully steam-
|
|
260
|
+
npx adaptfully <prebuild|build|deploy|release> <platform> [server] [mode]
|
|
261
|
+
npx adaptfully steam-publish [--username U] [--password P] [--deployment steam] [--output path]
|
|
262
|
+
npx adaptfully google-publish [--from service-account.json] [--deployment android] [--output path]
|
|
263
|
+
npx adaptfully apple-publish [--deployment ios] [--category C] [--identity I] [--username U] [--password P]
|
|
258
264
|
```
|
|
259
265
|
|
|
260
266
|
| Stage | Description |
|
|
@@ -520,7 +526,14 @@ Place keystore files in `assets/meta/deployments/<deployment>/android/`. Include
|
|
|
520
526
|
}
|
|
521
527
|
```
|
|
522
528
|
|
|
523
|
-
To deploy to Google Play, also include `assets/meta/deployments/<deployment>/google.json
|
|
529
|
+
To deploy to Google Play, also include `assets/meta/deployments/<deployment>/google.json`. The easiest path is:
|
|
530
|
+
|
|
531
|
+
```bash
|
|
532
|
+
npx adaptfully google-publish --from ./path/to/play-service-account.json
|
|
533
|
+
# defaults to assets/meta/deployments/android/google.json (+ manifest type "google")
|
|
534
|
+
```
|
|
535
|
+
|
|
536
|
+
The file looks like:
|
|
524
537
|
|
|
525
538
|
```json
|
|
526
539
|
{
|
|
@@ -562,7 +575,14 @@ Include `assets/meta/deployments/<deployment>/build.json` with iOS signing setti
|
|
|
562
575
|
}
|
|
563
576
|
```
|
|
564
577
|
|
|
565
|
-
To deploy to the App Store, include `assets/meta/deployments/<deployment>/apple.json
|
|
578
|
+
To deploy to the App Store, include `assets/meta/deployments/<deployment>/apple.json`. The easiest path is:
|
|
579
|
+
|
|
580
|
+
```bash
|
|
581
|
+
npx adaptfully apple-publish
|
|
582
|
+
# defaults to assets/meta/deployments/ios/apple.json (+ manifest type "apple")
|
|
583
|
+
```
|
|
584
|
+
|
|
585
|
+
The file looks like:
|
|
566
586
|
|
|
567
587
|
```json
|
|
568
588
|
{
|
|
@@ -581,7 +601,7 @@ Set `packager: "cordova"` on an `android` or `ios` platform to use Cordova inste
|
|
|
581
601
|
|
|
582
602
|
`steam-dev` builds debug Electron binaries for Windows, Mac, and Linux without uploading to Steam. No `steam.json` credentials are required.
|
|
583
603
|
|
|
584
|
-
For release uploads, run **`adaptfully steam-
|
|
604
|
+
For release uploads, run **`adaptfully steam-publish`** once on your machine. It installs steamcmd if needed, logs you in interactively (enter a Steam Guard code if prompted), and writes `assets/meta/deployments/steam/steam.json`:
|
|
585
605
|
|
|
586
606
|
```json
|
|
587
607
|
{
|
|
@@ -595,7 +615,7 @@ For release uploads, run **`adaptfully steam-auth`** once on your machine. It in
|
|
|
595
615
|
|
|
596
616
|
Accounts **without** Steam Guard only need `username`, `password`, and `configVdf`. Accounts **with** Steam Guard also include the `ssfn` sentry file so the Wrapfully server can log in without a live 2FA prompt.
|
|
597
617
|
|
|
598
|
-
Re-run `adaptfully steam-
|
|
618
|
+
Re-run `adaptfully steam-publish` if Steam invalidates the token (new machine, password change, or expired guard).
|
|
599
619
|
|
|
600
620
|
For Steamworks / `steam-auth` client builds, set `platforms.<name>.steamId`. For Steam **uploads**, set `steamId` on the deployment’s `manifest.json` (see Wrapfully). Those are independent: you can deploy without steam-auth, or use steam-auth without uploading.
|
|
601
621
|
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
import fsp from 'node:fs/promises';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import readline from 'node:readline/promises';
|
|
4
|
+
import { stdin as input, stdout as output } from 'node:process';
|
|
5
|
+
import {
|
|
6
|
+
defaultDeploymentCredentialPath,
|
|
7
|
+
ensureDeploymentManifest,
|
|
8
|
+
ensurePublishCredentialsGitignore,
|
|
9
|
+
parsePublishCredentialArgv,
|
|
10
|
+
} from './publish-credentials.js';
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* @param {string} [projectRoot]
|
|
14
|
+
* @param {string} [deploymentKey='ios']
|
|
15
|
+
*/
|
|
16
|
+
export function defaultApplePublishPath(projectRoot = '.', deploymentKey = 'ios') {
|
|
17
|
+
return defaultDeploymentCredentialPath(projectRoot, deploymentKey, 'apple.json');
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* @param {{ category: string, identity: string, username: string, password: string }} auth
|
|
22
|
+
*/
|
|
23
|
+
export function buildApplePublishJson(auth) {
|
|
24
|
+
return {
|
|
25
|
+
category: String(auth.category).trim(),
|
|
26
|
+
identity: String(auth.identity).trim(),
|
|
27
|
+
username: String(auth.username).trim(),
|
|
28
|
+
password: String(auth.password),
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* @param {unknown} raw
|
|
34
|
+
* @returns {{ category: string, identity: string, username: string, password: string }}
|
|
35
|
+
*/
|
|
36
|
+
export function validateApplePublishJson(raw) {
|
|
37
|
+
if (raw == null || typeof raw !== 'object' || Array.isArray(raw)) {
|
|
38
|
+
throw new Error('Apple credentials must be a JSON object.');
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const json = /** @type {Record<string, unknown>} */ (raw);
|
|
42
|
+
const required = ['category', 'identity', 'username', 'password'];
|
|
43
|
+
const missing = required.filter((key) => {
|
|
44
|
+
const value = json[key];
|
|
45
|
+
return value == null || String(value).trim() === '';
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
if (missing.length) {
|
|
49
|
+
throw new Error(`Apple credentials JSON is missing required field(s): ${missing.join(', ')}.`);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
return buildApplePublishJson({
|
|
53
|
+
category: /** @type {string} */ (json.category),
|
|
54
|
+
identity: /** @type {string} */ (json.identity),
|
|
55
|
+
username: /** @type {string} */ (json.username),
|
|
56
|
+
password: /** @type {string} */ (json.password),
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* @param {{
|
|
62
|
+
* category?: string,
|
|
63
|
+
* identity?: string,
|
|
64
|
+
* username?: string,
|
|
65
|
+
* password?: string,
|
|
66
|
+
* from?: string,
|
|
67
|
+
* output?: string,
|
|
68
|
+
* deployment?: string,
|
|
69
|
+
* projectRoot?: string,
|
|
70
|
+
* log?: (message: string) => void,
|
|
71
|
+
* }} [options]
|
|
72
|
+
*/
|
|
73
|
+
export async function runApplePublish(options = {}) {
|
|
74
|
+
const log = options.log ?? console.log;
|
|
75
|
+
const projectRoot = options.projectRoot ?? '.';
|
|
76
|
+
const deploymentKey = options.deployment ?? 'ios';
|
|
77
|
+
const outputPath = path.resolve(
|
|
78
|
+
options.output ?? defaultApplePublishPath(projectRoot, deploymentKey),
|
|
79
|
+
);
|
|
80
|
+
|
|
81
|
+
/** @type {{ category: string, identity: string, username: string, password: string }} */
|
|
82
|
+
let appleJson;
|
|
83
|
+
|
|
84
|
+
if (options.from) {
|
|
85
|
+
const fromPath = path.resolve(options.from);
|
|
86
|
+
let raw;
|
|
87
|
+
|
|
88
|
+
try {
|
|
89
|
+
raw = JSON.parse(await fsp.readFile(fromPath, 'utf8'));
|
|
90
|
+
} catch (err) {
|
|
91
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
92
|
+
throw new Error(`Unable to read Apple credentials JSON at "${fromPath}": ${message}`);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
appleJson = validateApplePublishJson(raw);
|
|
96
|
+
} else {
|
|
97
|
+
let category = options.category ?? process.env.APPLE_CATEGORY;
|
|
98
|
+
let identity = options.identity ?? process.env.APPLE_IDENTITY;
|
|
99
|
+
let username = options.username
|
|
100
|
+
?? process.env.APPLE_ID
|
|
101
|
+
?? process.env.APPLE_USERNAME;
|
|
102
|
+
let password = options.password
|
|
103
|
+
?? process.env.APPLE_APP_SPECIFIC_PASSWORD
|
|
104
|
+
?? process.env.APPLE_PASSWORD;
|
|
105
|
+
|
|
106
|
+
if (!category || !identity || !username || !password) {
|
|
107
|
+
const rl = readline.createInterface({ input, output });
|
|
108
|
+
|
|
109
|
+
try {
|
|
110
|
+
if (!category) {
|
|
111
|
+
category = String(await rl.question('App Store category: ')).trim();
|
|
112
|
+
}
|
|
113
|
+
if (!identity) {
|
|
114
|
+
identity = String(await rl.question('Apple team identity (e.g. TEAMID): ')).trim();
|
|
115
|
+
}
|
|
116
|
+
if (!username) {
|
|
117
|
+
username = String(await rl.question('Apple ID username: ')).trim();
|
|
118
|
+
}
|
|
119
|
+
if (!password) {
|
|
120
|
+
password = String(
|
|
121
|
+
await rl.question('Apple app-specific password: '),
|
|
122
|
+
);
|
|
123
|
+
}
|
|
124
|
+
} finally {
|
|
125
|
+
rl.close();
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
appleJson = validateApplePublishJson({
|
|
130
|
+
category,
|
|
131
|
+
identity,
|
|
132
|
+
username,
|
|
133
|
+
password,
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
await fsp.mkdir(path.dirname(outputPath), { recursive: true });
|
|
138
|
+
await fsp.writeFile(outputPath, `${JSON.stringify(appleJson, null, 4)}\n`);
|
|
139
|
+
await ensureDeploymentManifest(path.dirname(outputPath), 'apple', log);
|
|
140
|
+
await ensurePublishCredentialsGitignore(projectRoot, {
|
|
141
|
+
extraPaths: [outputPath],
|
|
142
|
+
log,
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
log(`adaptfully: wrote ${outputPath}`);
|
|
146
|
+
log(`adaptfully: Apple ID ${appleJson.username} (identity ${appleJson.identity})`);
|
|
147
|
+
|
|
148
|
+
return { outputPath, appleJson, deploymentKey };
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* @param {string[]} [argv]
|
|
153
|
+
*/
|
|
154
|
+
export async function applePublishFromCli(argv = process.argv) {
|
|
155
|
+
return runApplePublish(parsePublishCredentialArgv(argv));
|
|
156
|
+
}
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import fsp from 'node:fs/promises';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import {
|
|
4
|
+
defaultDeploymentCredentialPath,
|
|
5
|
+
ensureDeploymentManifest,
|
|
6
|
+
ensurePublishCredentialsGitignore,
|
|
7
|
+
parsePublishCredentialArgv,
|
|
8
|
+
promptRequired,
|
|
9
|
+
} from './publish-credentials.js';
|
|
10
|
+
|
|
11
|
+
const GOOGLE_REQUIRED_FIELDS = [
|
|
12
|
+
'type',
|
|
13
|
+
'project_id',
|
|
14
|
+
'private_key',
|
|
15
|
+
'client_email',
|
|
16
|
+
];
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* @param {string} [projectRoot]
|
|
20
|
+
* @param {string} [deploymentKey='android']
|
|
21
|
+
*/
|
|
22
|
+
export function defaultGooglePublishPath(projectRoot = '.', deploymentKey = 'android') {
|
|
23
|
+
return defaultDeploymentCredentialPath(projectRoot, deploymentKey, 'google.json');
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* @param {unknown} raw
|
|
28
|
+
* @returns {Record<string, unknown>}
|
|
29
|
+
*/
|
|
30
|
+
export function validateGoogleServiceAccount(raw) {
|
|
31
|
+
if (raw == null || typeof raw !== 'object' || Array.isArray(raw)) {
|
|
32
|
+
throw new Error('Google Play credentials must be a JSON object (service account key).');
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** @type {Record<string, unknown>} */
|
|
36
|
+
const json = { .../** @type {Record<string, unknown>} */ (raw) };
|
|
37
|
+
|
|
38
|
+
if (json.type != null && json.type !== 'service_account') {
|
|
39
|
+
throw new Error(
|
|
40
|
+
`Google service account JSON type must be "service_account" (got ${JSON.stringify(json.type)}).`,
|
|
41
|
+
);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const missing = GOOGLE_REQUIRED_FIELDS.filter((key) => {
|
|
45
|
+
const value = json[key];
|
|
46
|
+
return value == null || String(value).trim() === '';
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
if (missing.length) {
|
|
50
|
+
throw new Error(
|
|
51
|
+
`Google service account JSON is missing required field(s): ${missing.join(', ')}.`,
|
|
52
|
+
);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
return json;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* @param {{
|
|
60
|
+
* from?: string,
|
|
61
|
+
* output?: string,
|
|
62
|
+
* deployment?: string,
|
|
63
|
+
* projectRoot?: string,
|
|
64
|
+
* log?: (message: string) => void,
|
|
65
|
+
* }} [options]
|
|
66
|
+
*/
|
|
67
|
+
export async function runGooglePublish(options = {}) {
|
|
68
|
+
const log = options.log ?? console.log;
|
|
69
|
+
const projectRoot = options.projectRoot ?? '.';
|
|
70
|
+
const deploymentKey = options.deployment ?? 'android';
|
|
71
|
+
const outputPath = path.resolve(
|
|
72
|
+
options.output ?? defaultGooglePublishPath(projectRoot, deploymentKey),
|
|
73
|
+
);
|
|
74
|
+
|
|
75
|
+
const fromPath = path.resolve(
|
|
76
|
+
await promptRequired(
|
|
77
|
+
'Path to Google Play service account JSON: ',
|
|
78
|
+
options.from ?? process.env.GOOGLE_APPLICATION_CREDENTIALS,
|
|
79
|
+
),
|
|
80
|
+
);
|
|
81
|
+
|
|
82
|
+
let raw;
|
|
83
|
+
|
|
84
|
+
try {
|
|
85
|
+
raw = JSON.parse(await fsp.readFile(fromPath, 'utf8'));
|
|
86
|
+
} catch (err) {
|
|
87
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
88
|
+
throw new Error(`Unable to read Google service account JSON at "${fromPath}": ${message}`);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const googleJson = validateGoogleServiceAccount(raw);
|
|
92
|
+
|
|
93
|
+
await fsp.mkdir(path.dirname(outputPath), { recursive: true });
|
|
94
|
+
await fsp.writeFile(outputPath, `${JSON.stringify(googleJson, null, 4)}\n`);
|
|
95
|
+
await ensureDeploymentManifest(path.dirname(outputPath), 'google', log);
|
|
96
|
+
await ensurePublishCredentialsGitignore(projectRoot, {
|
|
97
|
+
extraPaths: [outputPath],
|
|
98
|
+
log,
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
log(`adaptfully: wrote ${outputPath}`);
|
|
102
|
+
log(`adaptfully: imported service account ${googleJson.client_email}`);
|
|
103
|
+
|
|
104
|
+
return { outputPath, googleJson, deploymentKey };
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* @param {string[]} [argv]
|
|
109
|
+
*/
|
|
110
|
+
export async function googlePublishFromCli(argv = process.argv) {
|
|
111
|
+
return runGooglePublish(parsePublishCredentialArgv(argv));
|
|
112
|
+
}
|
package/lib/node/index.js
CHANGED
|
@@ -50,12 +50,35 @@ export {
|
|
|
50
50
|
resolveRegistrationAssets,
|
|
51
51
|
} from './registrations.js';
|
|
52
52
|
export { printBuildReport } from './report.js';
|
|
53
|
+
export {
|
|
54
|
+
defaultDeploymentCredentialPath,
|
|
55
|
+
ensureDeploymentManifest,
|
|
56
|
+
ensurePublishCredentialsGitignore,
|
|
57
|
+
parsePublishCredentialArgv,
|
|
58
|
+
promptRequired,
|
|
59
|
+
PUBLISH_CREDENTIAL_GITIGNORE_PATTERNS,
|
|
60
|
+
} from './publish-credentials.js';
|
|
53
61
|
export {
|
|
54
62
|
buildSteamPublishJson,
|
|
55
63
|
collectSteamAuthFiles,
|
|
56
64
|
defaultSteamPublishPath,
|
|
57
65
|
defaultSteamcmdCacheDir,
|
|
58
66
|
ensureSteamcmd,
|
|
67
|
+
runSteamPublish,
|
|
68
|
+
steamPublishFromCli,
|
|
59
69
|
runSteamAuth,
|
|
60
70
|
steamAuthFromCli,
|
|
61
|
-
} from './steam-
|
|
71
|
+
} from './steam-publish.js';
|
|
72
|
+
export {
|
|
73
|
+
defaultGooglePublishPath,
|
|
74
|
+
googlePublishFromCli,
|
|
75
|
+
runGooglePublish,
|
|
76
|
+
validateGoogleServiceAccount,
|
|
77
|
+
} from './google-publish.js';
|
|
78
|
+
export {
|
|
79
|
+
applePublishFromCli,
|
|
80
|
+
buildApplePublishJson,
|
|
81
|
+
defaultApplePublishPath,
|
|
82
|
+
runApplePublish,
|
|
83
|
+
validateApplePublishJson,
|
|
84
|
+
} from './apple-publish.js';
|
package/lib/node/pipeline.js
CHANGED
|
@@ -8,11 +8,18 @@ import {
|
|
|
8
8
|
resolveDeploymentsForPlatform,
|
|
9
9
|
resolvePublishDir,
|
|
10
10
|
} from './registrations.js';
|
|
11
|
-
import {
|
|
11
|
+
import { steamPublishFromCli } from './steam-publish.js';
|
|
12
|
+
import { googlePublishFromCli } from './google-publish.js';
|
|
13
|
+
import { applePublishFromCli } from './apple-publish.js';
|
|
12
14
|
|
|
13
15
|
/** @typedef {'prebuild' | 'build' | 'deploy' | 'release'} AdaptfullyStage */
|
|
14
16
|
|
|
15
17
|
const VALID_STAGES = new Set(['prebuild', 'build', 'deploy', 'release']);
|
|
18
|
+
const PUBLISH_COMMANDS = {
|
|
19
|
+
'steam-publish': steamPublishFromCli,
|
|
20
|
+
'google-publish': googlePublishFromCli,
|
|
21
|
+
'apple-publish': applePublishFromCli,
|
|
22
|
+
};
|
|
16
23
|
|
|
17
24
|
/**
|
|
18
25
|
* Wrapfully URL path segment for build/release.
|
|
@@ -161,7 +168,12 @@ export async function adaptfullyFromCli(argv = process.argv) {
|
|
|
161
168
|
const command = argv[2];
|
|
162
169
|
|
|
163
170
|
if (command === 'steam-auth') {
|
|
164
|
-
|
|
171
|
+
console.warn('adaptfully: "steam-auth" CLI is deprecated; use "steam-publish" instead.');
|
|
172
|
+
return steamPublishFromCli(argv);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
if (PUBLISH_COMMANDS[command]) {
|
|
176
|
+
return PUBLISH_COMMANDS[command](argv);
|
|
165
177
|
}
|
|
166
178
|
|
|
167
179
|
const stage = command;
|
|
@@ -174,7 +186,9 @@ export async function adaptfullyFromCli(argv = process.argv) {
|
|
|
174
186
|
throw new Error(
|
|
175
187
|
'Usage:\n'
|
|
176
188
|
+ ' adaptfully <prebuild|build|deploy|release> <platform> [server] [mode] [--deployment <key>] [--artifact <path>]\n'
|
|
177
|
-
+ ' adaptfully steam-
|
|
189
|
+
+ ' adaptfully steam-publish [--username U] [--password P] [--deployment steam] [--output path]\n'
|
|
190
|
+
+ ' adaptfully google-publish [--from service-account.json] [--deployment android] [--output path]\n'
|
|
191
|
+
+ ' adaptfully apple-publish [--deployment ios] [--category C] [--identity I] [--username U] [--password P]\n'
|
|
178
192
|
+ '\n'
|
|
179
193
|
+ 'Stages:\n'
|
|
180
194
|
+ ' prebuild Copy deploy/ and apply platform registrations → output/<platform>-prebuild/\n'
|
|
@@ -185,7 +199,10 @@ export async function adaptfullyFromCli(argv = process.argv) {
|
|
|
185
199
|
+ ' --deployment <key> Target a single named deployment.\n'
|
|
186
200
|
+ ' --artifact <path> Prior build artifact directory (deploy only; default: ./output/ after build).\n'
|
|
187
201
|
+ '\n'
|
|
188
|
-
+ '
|
|
202
|
+
+ 'Publish credential helpers (one-time local setup):\n'
|
|
203
|
+
+ ' steam-publish Install steamcmd if needed, log in, write steam deployment steam.json\n'
|
|
204
|
+
+ ' google-publish Import a Play service-account JSON into the android deployment folder\n'
|
|
205
|
+
+ ' apple-publish Write App Store Connect credentials into the ios deployment folder',
|
|
189
206
|
);
|
|
190
207
|
}
|
|
191
208
|
|
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
import fsp from 'node:fs/promises';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import readline from 'node:readline/promises';
|
|
4
|
+
import { stdin as input, stdout as output } from 'node:process';
|
|
5
|
+
|
|
6
|
+
/** Patterns that should never be committed from deployment credential folders. */
|
|
7
|
+
export const PUBLISH_CREDENTIAL_GITIGNORE_PATTERNS = [
|
|
8
|
+
'assets/meta/deployments/**/sftp.json',
|
|
9
|
+
'assets/meta/deployments/**/steam.json',
|
|
10
|
+
'assets/meta/deployments/**/build.json',
|
|
11
|
+
'assets/meta/deployments/**/google.json',
|
|
12
|
+
'assets/meta/deployments/**/apple.json',
|
|
13
|
+
'assets/meta/deployments/**/ms.json',
|
|
14
|
+
'assets/meta/deployments/**/android/',
|
|
15
|
+
'assets/meta/deployments/**/ms/',
|
|
16
|
+
];
|
|
17
|
+
|
|
18
|
+
const GITIGNORE_SECTION_HEADER = '# Adaptfully publish credentials — do not commit';
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* @param {string} [projectRoot]
|
|
22
|
+
* @param {string} deploymentKey
|
|
23
|
+
* @param {string} filename
|
|
24
|
+
*/
|
|
25
|
+
export function defaultDeploymentCredentialPath(projectRoot = '.', deploymentKey, filename) {
|
|
26
|
+
return path.resolve(
|
|
27
|
+
projectRoot,
|
|
28
|
+
'assets',
|
|
29
|
+
'meta',
|
|
30
|
+
'deployments',
|
|
31
|
+
deploymentKey,
|
|
32
|
+
filename,
|
|
33
|
+
);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* @param {string} content
|
|
38
|
+
* @returns {Set<string>}
|
|
39
|
+
*/
|
|
40
|
+
function gitignoreLineSet(content) {
|
|
41
|
+
return new Set(
|
|
42
|
+
content
|
|
43
|
+
.split(/\r?\n/)
|
|
44
|
+
.map((line) => line.trim())
|
|
45
|
+
.filter((line) => line && !line.startsWith('#')),
|
|
46
|
+
);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Ensure project `.gitignore` ignores Adaptfully deployment credential files.
|
|
51
|
+
* Idempotent: only appends missing patterns. Optionally also ignores a specific
|
|
52
|
+
* credential file path (e.g. custom `--output`).
|
|
53
|
+
*
|
|
54
|
+
* @param {string} [projectRoot='.']
|
|
55
|
+
* @param {{
|
|
56
|
+
* extraPaths?: string[],
|
|
57
|
+
* log?: (message: string) => void,
|
|
58
|
+
* }} [options]
|
|
59
|
+
*/
|
|
60
|
+
export async function ensurePublishCredentialsGitignore(projectRoot = '.', options = {}) {
|
|
61
|
+
const log = options.log ?? console.log;
|
|
62
|
+
const root = path.resolve(projectRoot);
|
|
63
|
+
const gitignorePath = path.join(root, '.gitignore');
|
|
64
|
+
|
|
65
|
+
let content = '';
|
|
66
|
+
let existed = true;
|
|
67
|
+
|
|
68
|
+
try {
|
|
69
|
+
content = await fsp.readFile(gitignorePath, 'utf8');
|
|
70
|
+
} catch {
|
|
71
|
+
existed = false;
|
|
72
|
+
content = '';
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const existing = gitignoreLineSet(content);
|
|
76
|
+
/** @type {string[]} */
|
|
77
|
+
const toAdd = [];
|
|
78
|
+
|
|
79
|
+
for (const pattern of PUBLISH_CREDENTIAL_GITIGNORE_PATTERNS) {
|
|
80
|
+
if (!existing.has(pattern)) {
|
|
81
|
+
toAdd.push(pattern);
|
|
82
|
+
existing.add(pattern);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
for (const extra of options.extraPaths ?? []) {
|
|
87
|
+
const absolute = path.resolve(extra);
|
|
88
|
+
const relative = path.relative(root, absolute).replace(/\\/g, '/');
|
|
89
|
+
|
|
90
|
+
if (!relative || relative.startsWith('..')) {
|
|
91
|
+
// Outside the project — skip; can't usefully gitignore it here.
|
|
92
|
+
continue;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
if (!existing.has(relative)) {
|
|
96
|
+
toAdd.push(relative);
|
|
97
|
+
existing.add(relative);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
if (toAdd.length === 0) {
|
|
102
|
+
return { gitignorePath, added: [], created: false, updated: false };
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const needsHeader = !content.includes(GITIGNORE_SECTION_HEADER);
|
|
106
|
+
const prefix = content.length === 0 || content.endsWith('\n') ? '' : '\n';
|
|
107
|
+
const section = [
|
|
108
|
+
needsHeader ? GITIGNORE_SECTION_HEADER : null,
|
|
109
|
+
...toAdd,
|
|
110
|
+
'',
|
|
111
|
+
].filter((line) => line != null).join('\n');
|
|
112
|
+
|
|
113
|
+
await fsp.writeFile(gitignorePath, `${content}${prefix}${section}`);
|
|
114
|
+
|
|
115
|
+
if (!existed) {
|
|
116
|
+
log(`adaptfully: created ${gitignorePath} with publish credential ignores`);
|
|
117
|
+
} else {
|
|
118
|
+
log(`adaptfully: updated ${gitignorePath} (+${toAdd.length} credential ignore${toAdd.length === 1 ? '' : 's'})`);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
return { gitignorePath, added: toAdd, created: !existed, updated: true };
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Ensure deployment folder has a manifest.json with the given deployer type.
|
|
126
|
+
* Creates the file when missing; updates `type` when wrong; leaves other fields intact.
|
|
127
|
+
*
|
|
128
|
+
* @param {string} deploymentDir
|
|
129
|
+
* @param {string} type
|
|
130
|
+
* @param {(message: string) => void} [log]
|
|
131
|
+
*/
|
|
132
|
+
export async function ensureDeploymentManifest(deploymentDir, type, log = console.log) {
|
|
133
|
+
const manifestPath = path.join(deploymentDir, 'manifest.json');
|
|
134
|
+
/** @type {Record<string, unknown>} */
|
|
135
|
+
let manifest = {};
|
|
136
|
+
|
|
137
|
+
try {
|
|
138
|
+
manifest = JSON.parse(await fsp.readFile(manifestPath, 'utf8'));
|
|
139
|
+
} catch {
|
|
140
|
+
// create below
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
if (manifest.type === type) {
|
|
144
|
+
return { manifestPath, created: false, updated: false };
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
const previous = manifest.type;
|
|
148
|
+
manifest.type = type;
|
|
149
|
+
await fsp.mkdir(deploymentDir, { recursive: true });
|
|
150
|
+
await fsp.writeFile(manifestPath, `${JSON.stringify(manifest, null, 4)}\n`);
|
|
151
|
+
|
|
152
|
+
if (previous == null) {
|
|
153
|
+
log(`adaptfully: wrote ${manifestPath} with type "${type}"`);
|
|
154
|
+
return { manifestPath, created: true, updated: false };
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
log(`adaptfully: updated ${manifestPath} type "${previous}" → "${type}"`);
|
|
158
|
+
return { manifestPath, created: false, updated: true };
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* @param {string} prompt
|
|
163
|
+
* @param {string} [existing]
|
|
164
|
+
* @returns {Promise<string>}
|
|
165
|
+
*/
|
|
166
|
+
export async function promptRequired(prompt, existing) {
|
|
167
|
+
if (existing != null && String(existing).trim() !== '') {
|
|
168
|
+
return String(existing).trim();
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
const rl = readline.createInterface({ input, output });
|
|
172
|
+
|
|
173
|
+
try {
|
|
174
|
+
const value = String(await rl.question(prompt)).trim();
|
|
175
|
+
if (!value) {
|
|
176
|
+
throw new Error(`${prompt.replace(/:\s*$/, '')} is required.`);
|
|
177
|
+
}
|
|
178
|
+
return value;
|
|
179
|
+
} finally {
|
|
180
|
+
rl.close();
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* Parse shared CLI flags used by publish credential helpers.
|
|
186
|
+
* @param {string[]} argv
|
|
187
|
+
* @param {number} [startIndex=3]
|
|
188
|
+
*/
|
|
189
|
+
export function parsePublishCredentialArgv(argv, startIndex = 3) {
|
|
190
|
+
/** @type {Record<string, string>} */
|
|
191
|
+
const options = {
|
|
192
|
+
projectRoot: '.',
|
|
193
|
+
};
|
|
194
|
+
|
|
195
|
+
for (let i = startIndex; i < argv.length; i++) {
|
|
196
|
+
const arg = argv[i];
|
|
197
|
+
|
|
198
|
+
if (arg === '--from' && argv[i + 1]) {
|
|
199
|
+
options.from = argv[++i];
|
|
200
|
+
} else if (arg === '--output' && argv[i + 1]) {
|
|
201
|
+
options.output = argv[++i];
|
|
202
|
+
} else if (arg === '--deployment' && argv[i + 1]) {
|
|
203
|
+
options.deployment = argv[++i];
|
|
204
|
+
} else if (arg === '--project-root' && argv[i + 1]) {
|
|
205
|
+
options.projectRoot = argv[++i];
|
|
206
|
+
} else if (arg === '--steamcmd-dir' && argv[i + 1]) {
|
|
207
|
+
options.steamcmdDir = argv[++i];
|
|
208
|
+
} else if (arg === '--category' && argv[i + 1]) {
|
|
209
|
+
options.category = argv[++i];
|
|
210
|
+
} else if (arg === '--identity' && argv[i + 1]) {
|
|
211
|
+
options.identity = argv[++i];
|
|
212
|
+
} else if (arg === '--username' && argv[i + 1]) {
|
|
213
|
+
options.username = argv[++i];
|
|
214
|
+
} else if (arg === '--password' && argv[i + 1]) {
|
|
215
|
+
options.password = argv[++i];
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
return options;
|
|
220
|
+
}
|
|
@@ -6,6 +6,12 @@ import path from 'node:path';
|
|
|
6
6
|
import { pipeline } from 'node:stream/promises';
|
|
7
7
|
import readline from 'node:readline/promises';
|
|
8
8
|
import { stdin as input, stdout as output } from 'node:process';
|
|
9
|
+
import {
|
|
10
|
+
defaultDeploymentCredentialPath,
|
|
11
|
+
ensureDeploymentManifest,
|
|
12
|
+
ensurePublishCredentialsGitignore,
|
|
13
|
+
parsePublishCredentialArgv,
|
|
14
|
+
} from './publish-credentials.js';
|
|
9
15
|
|
|
10
16
|
const STEAMCMD_INSTALL = {
|
|
11
17
|
win32: {
|
|
@@ -24,9 +30,10 @@ const STEAMCMD_INSTALL = {
|
|
|
24
30
|
|
|
25
31
|
/**
|
|
26
32
|
* @param {string} [projectRoot]
|
|
33
|
+
* @param {string} [deploymentKey='steam']
|
|
27
34
|
*/
|
|
28
|
-
export function defaultSteamPublishPath(projectRoot = '.') {
|
|
29
|
-
return
|
|
35
|
+
export function defaultSteamPublishPath(projectRoot = '.', deploymentKey = 'steam') {
|
|
36
|
+
return defaultDeploymentCredentialPath(projectRoot, deploymentKey, 'steam.json');
|
|
30
37
|
}
|
|
31
38
|
|
|
32
39
|
/**
|
|
@@ -245,45 +252,29 @@ export function runSteamcmdLogin(cacheDir, username, password) {
|
|
|
245
252
|
}
|
|
246
253
|
|
|
247
254
|
/**
|
|
248
|
-
* @param {
|
|
255
|
+
* @param {{
|
|
256
|
+
* username?: string,
|
|
257
|
+
* password?: string,
|
|
258
|
+
* output?: string,
|
|
259
|
+
* deployment?: string,
|
|
260
|
+
* steamcmdDir?: string,
|
|
261
|
+
* projectRoot?: string,
|
|
262
|
+
* log?: (message: string) => void,
|
|
263
|
+
* }} [options]
|
|
249
264
|
*/
|
|
250
|
-
function
|
|
251
|
-
/** @type {Record<string, string>} */
|
|
252
|
-
const options = {
|
|
253
|
-
projectRoot: '.',
|
|
254
|
-
};
|
|
255
|
-
|
|
256
|
-
for (let i = 3; i < argv.length; i++) {
|
|
257
|
-
const arg = argv[i];
|
|
258
|
-
|
|
259
|
-
if (arg === '--username' && argv[i + 1]) {
|
|
260
|
-
options.username = argv[++i];
|
|
261
|
-
} else if (arg === '--password' && argv[i + 1]) {
|
|
262
|
-
options.password = argv[++i];
|
|
263
|
-
} else if (arg === '--output' && argv[i + 1]) {
|
|
264
|
-
options.output = argv[++i];
|
|
265
|
-
} else if (arg === '--steamcmd-dir' && argv[i + 1]) {
|
|
266
|
-
options.steamcmdDir = argv[++i];
|
|
267
|
-
} else if (arg === '--project-root' && argv[i + 1]) {
|
|
268
|
-
options.projectRoot = argv[++i];
|
|
269
|
-
}
|
|
270
|
-
}
|
|
271
|
-
|
|
272
|
-
return options;
|
|
273
|
-
}
|
|
274
|
-
|
|
275
|
-
/**
|
|
276
|
-
* @param {{ username?: string, password?: string, output?: string, steamcmdDir?: string, projectRoot?: string, log?: (message: string) => void }} [options]
|
|
277
|
-
*/
|
|
278
|
-
export async function runSteamAuth(options = {}) {
|
|
265
|
+
export async function runSteamPublish(options = {}) {
|
|
279
266
|
const log = options.log ?? console.log;
|
|
267
|
+
const projectRoot = options.projectRoot ?? '.';
|
|
268
|
+
const deploymentKey = options.deployment ?? 'steam';
|
|
280
269
|
const username = options.username
|
|
281
270
|
?? process.env.STEAM_USERNAME
|
|
282
271
|
?? process.env.STEAM_BUILD_USERNAME;
|
|
283
272
|
const password = options.password
|
|
284
273
|
?? process.env.STEAM_PASSWORD
|
|
285
274
|
?? process.env.STEAM_BUILD_PASSWORD;
|
|
286
|
-
const outputPath = path.resolve(
|
|
275
|
+
const outputPath = path.resolve(
|
|
276
|
+
options.output ?? defaultSteamPublishPath(projectRoot, deploymentKey),
|
|
277
|
+
);
|
|
287
278
|
const cacheDir = options.steamcmdDir ?? defaultSteamcmdCacheDir();
|
|
288
279
|
|
|
289
280
|
let resolvedUsername = username;
|
|
@@ -324,6 +315,11 @@ export async function runSteamAuth(options = {}) {
|
|
|
324
315
|
|
|
325
316
|
await fsp.mkdir(path.dirname(outputPath), { recursive: true });
|
|
326
317
|
await fsp.writeFile(outputPath, `${JSON.stringify(steamJson, null, 4)}\n`);
|
|
318
|
+
await ensureDeploymentManifest(path.dirname(outputPath), 'steam', log);
|
|
319
|
+
await ensurePublishCredentialsGitignore(projectRoot, {
|
|
320
|
+
extraPaths: [outputPath],
|
|
321
|
+
log,
|
|
322
|
+
});
|
|
327
323
|
|
|
328
324
|
log(`adaptfully: wrote ${outputPath}`);
|
|
329
325
|
if (authFiles.sentryFileName) {
|
|
@@ -332,12 +328,18 @@ export async function runSteamAuth(options = {}) {
|
|
|
332
328
|
log('adaptfully: no sentry file was created (Steam Guard may be disabled on this account)');
|
|
333
329
|
}
|
|
334
330
|
|
|
335
|
-
return { outputPath, steamJson };
|
|
331
|
+
return { outputPath, steamJson, deploymentKey };
|
|
336
332
|
}
|
|
337
333
|
|
|
338
334
|
/**
|
|
339
335
|
* @param {string[]} [argv]
|
|
340
336
|
*/
|
|
341
|
-
export async function
|
|
342
|
-
return
|
|
337
|
+
export async function steamPublishFromCli(argv = process.argv) {
|
|
338
|
+
return runSteamPublish(parsePublishCredentialArgv(argv));
|
|
343
339
|
}
|
|
340
|
+
|
|
341
|
+
/** @deprecated Use {@link runSteamPublish} */
|
|
342
|
+
export const runSteamAuth = runSteamPublish;
|
|
343
|
+
|
|
344
|
+
/** @deprecated Use {@link steamPublishFromCli} */
|
|
345
|
+
export const steamAuthFromCli = steamPublishFromCli;
|