@makefully/adaptfully 3.10.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 +25 -0
- package/README.md +31 -9
- package/lib/assets/icon-background.png +0 -0
- package/lib/assets/icon-foreground.png +0 -0
- package/lib/node/apple-publish.js +156 -0
- package/lib/node/archive.js +18 -2
- package/lib/node/google-publish.js +112 -0
- package/lib/node/icons.js +97 -0
- package/lib/node/index.js +35 -2
- package/lib/node/pipeline.js +47 -10
- 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,30 @@
|
|
|
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
|
+
|
|
18
|
+
## 3.11.0 — 2026-07-23
|
|
19
|
+
|
|
20
|
+
### Added
|
|
21
|
+
|
|
22
|
+
- **Layered icon validation** — build/deploy/release zips always include `meta/icon-foreground.png` and `meta/icon-background.png`. When project icons are missing under `assets/meta/`, Adaptfully **warns** (does not fail) and ships packaged placeholder icons from `lib/assets/`.
|
|
23
|
+
- **`resolveMetaIcons` / `appendMetaIcons`** — exported helpers for resolving project vs placeholder icon paths.
|
|
24
|
+
|
|
25
|
+
### Changed
|
|
26
|
+
|
|
27
|
+
- Legacy meta packaging skips re-adding icon filenames so placeholders and project icons are not duplicated in the zip.
|
|
28
|
+
|
|
5
29
|
## 3.10.0 — 2026-07-17
|
|
6
30
|
|
|
7
31
|
### Added
|
|
@@ -13,6 +37,7 @@ All notable changes to this project are documented in this file.
|
|
|
13
37
|
|
|
14
38
|
- **Builder family defaults** — `android` / `ios` / `ios-sim` (and `-dev`) resolve to Wrapfully family **`capacitor`**. Explicit `packager: "cordova"` still selects Cordova for that platform. Composite `cordova` / `apple` routes are not used.
|
|
15
39
|
- **`resolveBuildSpec`** includes `socialLogin: boolean`.
|
|
40
|
+
- **Single-target Wrapfully POSTs** go to the target route (`/android/build`, `/ios/release`, …) instead of the family route (`/capacitor/...`), so OS relay and existing target routes keep working.
|
|
16
41
|
|
|
17
42
|
## 3.9.0 — 2026-07-15
|
|
18
43
|
|
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 |
|
|
@@ -363,7 +369,7 @@ mygame/
|
|
|
363
369
|
|
|
364
370
|
The selected deployment's folder is shipped as `meta/publish/` in the zip, so per-platform credential file names (`build.json`, `sftp.json`, `steam.json`, `apple.json`, `google.json`, `ms.json`, `android/`, `ms/`) live inside `assets/meta/deployments/<key>/`.
|
|
365
371
|
|
|
366
|
-
Icons (`icon-foreground.png`, `icon-background.png`) are required for mobile, desktop, and Steam builds.
|
|
372
|
+
Icons (`icon-foreground.png`, `icon-background.png`) are required for mobile, desktop, and Steam builds. If either file is missing from `assets/meta/`, Adaptfully **warns** and ships packaged placeholder icons for that build so Wrapfully can still run.
|
|
367
373
|
|
|
368
374
|
### Icons
|
|
369
375
|
|
|
@@ -378,6 +384,8 @@ The build server composites the foreground over the background, applies a bindin
|
|
|
378
384
|
|
|
379
385
|
**Recommended format:** 1536×1536 pixel square PNGs for both files. Images with other dimensions are scaled to 1536×1536 automatically, but matching the target size produces the sharpest results.
|
|
380
386
|
|
|
387
|
+
Missing icons do not fail the Adaptfully client: check the `WARNING missing layered icon(s)` log line and replace the placeholders before a store release.
|
|
388
|
+
|
|
381
389
|
## Configuration
|
|
382
390
|
|
|
383
391
|
Build settings are read from `package.json`. The client merges any `wrapfully.json` fields into `package.json`'s `config` object before sending.
|
|
@@ -518,7 +526,14 @@ Place keystore files in `assets/meta/deployments/<deployment>/android/`. Include
|
|
|
518
526
|
}
|
|
519
527
|
```
|
|
520
528
|
|
|
521
|
-
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:
|
|
522
537
|
|
|
523
538
|
```json
|
|
524
539
|
{
|
|
@@ -560,7 +575,14 @@ Include `assets/meta/deployments/<deployment>/build.json` with iOS signing setti
|
|
|
560
575
|
}
|
|
561
576
|
```
|
|
562
577
|
|
|
563
|
-
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:
|
|
564
586
|
|
|
565
587
|
```json
|
|
566
588
|
{
|
|
@@ -579,7 +601,7 @@ Set `packager: "cordova"` on an `android` or `ios` platform to use Cordova inste
|
|
|
579
601
|
|
|
580
602
|
`steam-dev` builds debug Electron binaries for Windows, Mac, and Linux without uploading to Steam. No `steam.json` credentials are required.
|
|
581
603
|
|
|
582
|
-
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`:
|
|
583
605
|
|
|
584
606
|
```json
|
|
585
607
|
{
|
|
@@ -593,7 +615,7 @@ For release uploads, run **`adaptfully steam-auth`** once on your machine. It in
|
|
|
593
615
|
|
|
594
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.
|
|
595
617
|
|
|
596
|
-
Re-run `adaptfully steam-
|
|
618
|
+
Re-run `adaptfully steam-publish` if Steam invalidates the token (new machine, password change, or expired guard).
|
|
597
619
|
|
|
598
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.
|
|
599
621
|
|
|
Binary file
|
|
Binary file
|
|
@@ -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
|
+
}
|
package/lib/node/archive.js
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import archiver from 'archiver';
|
|
2
2
|
import fs from 'node:fs';
|
|
3
3
|
import path from 'node:path';
|
|
4
|
+
import { appendMetaIcons, isMetaIconFilename, META_DIR } from './icons.js';
|
|
4
5
|
|
|
5
6
|
const
|
|
6
|
-
META_DIR = 'assets/meta',
|
|
7
7
|
DEPLOYMENTS_DIRNAME = 'deployments',
|
|
8
8
|
PUBLISH_DIRNAME = 'publish';
|
|
9
9
|
|
|
@@ -38,6 +38,9 @@ function appendMeta(zip, publishDir, deploymentDirs = [], options = {}) {
|
|
|
38
38
|
log = options.log ?? (() => {}),
|
|
39
39
|
metaDir = path.resolve(META_DIR);
|
|
40
40
|
|
|
41
|
+
// Always ship layered icons (project files or Adaptfully placeholders).
|
|
42
|
+
appendMetaIcons(zip, { metaDir, log });
|
|
43
|
+
|
|
41
44
|
if (!fs.existsSync(metaDir)) {
|
|
42
45
|
log(`adaptfully: meta dir missing at ${metaDir}; skipping credentials`);
|
|
43
46
|
return;
|
|
@@ -46,7 +49,17 @@ function appendMeta(zip, publishDir, deploymentDirs = [], options = {}) {
|
|
|
46
49
|
const deploymentsRoot = path.join(metaDir, DEPLOYMENTS_DIRNAME);
|
|
47
50
|
|
|
48
51
|
if (!fs.existsSync(deploymentsRoot)) {
|
|
49
|
-
|
|
52
|
+
for (const entry of fs.readdirSync(metaDir, { withFileTypes: true })) {
|
|
53
|
+
if (isMetaIconFilename(entry.name)) {
|
|
54
|
+
continue;
|
|
55
|
+
}
|
|
56
|
+
const fullPath = path.join(metaDir, entry.name);
|
|
57
|
+
if (entry.isDirectory()) {
|
|
58
|
+
appendDirectory(zip, fullPath, `meta/${entry.name}`);
|
|
59
|
+
} else {
|
|
60
|
+
zip.file(fullPath, { name: `meta/${entry.name}` });
|
|
61
|
+
}
|
|
62
|
+
}
|
|
50
63
|
return;
|
|
51
64
|
}
|
|
52
65
|
|
|
@@ -54,6 +67,9 @@ function appendMeta(zip, publishDir, deploymentDirs = [], options = {}) {
|
|
|
54
67
|
if (entry.name === DEPLOYMENTS_DIRNAME || entry.name === PUBLISH_DIRNAME) {
|
|
55
68
|
continue;
|
|
56
69
|
}
|
|
70
|
+
if (isMetaIconFilename(entry.name)) {
|
|
71
|
+
continue;
|
|
72
|
+
}
|
|
57
73
|
|
|
58
74
|
const fullPath = path.join(metaDir, entry.name);
|
|
59
75
|
if (entry.isDirectory()) {
|
|
@@ -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
|
+
}
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { getPackageRoot } from './paths.js';
|
|
4
|
+
|
|
5
|
+
export const META_DIR = 'assets/meta';
|
|
6
|
+
export const ICON_FOREGROUND = 'icon-foreground.png';
|
|
7
|
+
export const ICON_BACKGROUND = 'icon-background.png';
|
|
8
|
+
|
|
9
|
+
const ICON_FILES = [ICON_FOREGROUND, ICON_BACKGROUND];
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Absolute path to a packaged Adaptfully placeholder icon.
|
|
13
|
+
* @param {string} filename
|
|
14
|
+
*/
|
|
15
|
+
export function resolvePlaceholderIconPath(filename) {
|
|
16
|
+
return path.join(getPackageRoot(), 'lib', 'assets', filename);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* @param {string} [metaDir=META_DIR]
|
|
21
|
+
*/
|
|
22
|
+
export function resolveMetaIconPaths(metaDir = META_DIR) {
|
|
23
|
+
const root = path.resolve(metaDir);
|
|
24
|
+
return {
|
|
25
|
+
metaDir: root,
|
|
26
|
+
foreground: path.join(root, ICON_FOREGROUND),
|
|
27
|
+
background: path.join(root, ICON_BACKGROUND),
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Resolve layered icon paths for Wrapfully packaging. Warns (does not throw) when
|
|
33
|
+
* project icons are missing and falls back to Adaptfully placeholders.
|
|
34
|
+
*
|
|
35
|
+
* @param {{ metaDir?: string, log?: (message: string) => void }} [options]
|
|
36
|
+
* @returns {{
|
|
37
|
+
* metaDir: string,
|
|
38
|
+
* foreground: string,
|
|
39
|
+
* background: string,
|
|
40
|
+
* missing: string[],
|
|
41
|
+
* usedPlaceholders: boolean,
|
|
42
|
+
* }}
|
|
43
|
+
*/
|
|
44
|
+
export function resolveMetaIcons(options = {}) {
|
|
45
|
+
const log = options.log ?? console.log;
|
|
46
|
+
const { metaDir, foreground, background } = resolveMetaIconPaths(options.metaDir);
|
|
47
|
+
/** @type {string[]} */
|
|
48
|
+
const missing = [];
|
|
49
|
+
let resolvedForeground = foreground;
|
|
50
|
+
let resolvedBackground = background;
|
|
51
|
+
|
|
52
|
+
if (!fs.existsSync(foreground)) {
|
|
53
|
+
missing.push(ICON_FOREGROUND);
|
|
54
|
+
resolvedForeground = resolvePlaceholderIconPath(ICON_FOREGROUND);
|
|
55
|
+
}
|
|
56
|
+
if (!fs.existsSync(background)) {
|
|
57
|
+
missing.push(ICON_BACKGROUND);
|
|
58
|
+
resolvedBackground = resolvePlaceholderIconPath(ICON_BACKGROUND);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
if (missing.length > 0) {
|
|
62
|
+
log(
|
|
63
|
+
`adaptfully: WARNING missing layered icon(s) in ${metaDir}: ${missing.join(', ')}. `
|
|
64
|
+
+ 'Using Adaptfully placeholder icons for this build.',
|
|
65
|
+
);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
return {
|
|
69
|
+
metaDir,
|
|
70
|
+
foreground: resolvedForeground,
|
|
71
|
+
background: resolvedBackground,
|
|
72
|
+
missing,
|
|
73
|
+
usedPlaceholders: missing.length > 0,
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Append layered icons to a zip under meta/. Uses project icons when present;
|
|
79
|
+
* otherwise warns and ships Adaptfully placeholders.
|
|
80
|
+
*
|
|
81
|
+
* @param {import('archiver').Archiver} zip
|
|
82
|
+
* @param {{ metaDir?: string, log?: (message: string) => void }} [options]
|
|
83
|
+
* @returns {ReturnType<typeof resolveMetaIcons>}
|
|
84
|
+
*/
|
|
85
|
+
export function appendMetaIcons(zip, options = {}) {
|
|
86
|
+
const resolved = resolveMetaIcons(options);
|
|
87
|
+
zip.file(resolved.foreground, { name: `meta/${ICON_FOREGROUND}` });
|
|
88
|
+
zip.file(resolved.background, { name: `meta/${ICON_BACKGROUND}` });
|
|
89
|
+
return resolved;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* @param {string} name
|
|
94
|
+
*/
|
|
95
|
+
export function isMetaIconFilename(name) {
|
|
96
|
+
return ICON_FILES.includes(name);
|
|
97
|
+
}
|
package/lib/node/index.js
CHANGED
|
@@ -2,7 +2,17 @@ export { buildOutputDir, clearStaleBuildExtract, resolveBuildArtifactDir } from
|
|
|
2
2
|
export { createArchive, createDeployArchive, createReleaseArchive, createSourceArchive } from './archive.js';
|
|
3
3
|
export { loadProjectConfig, resolveServerUrl } from './config.js';
|
|
4
4
|
export { send } from './deploy.js';
|
|
5
|
-
export {
|
|
5
|
+
export {
|
|
6
|
+
appendMetaIcons,
|
|
7
|
+
ICON_BACKGROUND,
|
|
8
|
+
ICON_FOREGROUND,
|
|
9
|
+
isMetaIconFilename,
|
|
10
|
+
META_DIR,
|
|
11
|
+
resolveMetaIconPaths,
|
|
12
|
+
resolveMetaIcons,
|
|
13
|
+
resolvePlaceholderIconPath,
|
|
14
|
+
} from './icons.js';
|
|
15
|
+
export { adaptfullyFromCli, parseStageArgs, resolveWrapfullyRoute, runAdaptfullyStage } from './pipeline.js';
|
|
6
16
|
export { prebuildPlatform, prebuildOutputDir, resolveHtmlInjections } from './prebuild.js';
|
|
7
17
|
export { getPackageRoot, getRuntimeDir, resolveRuntimeScript } from './paths.js';
|
|
8
18
|
export {
|
|
@@ -40,12 +50,35 @@ export {
|
|
|
40
50
|
resolveRegistrationAssets,
|
|
41
51
|
} from './registrations.js';
|
|
42
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';
|
|
43
61
|
export {
|
|
44
62
|
buildSteamPublishJson,
|
|
45
63
|
collectSteamAuthFiles,
|
|
46
64
|
defaultSteamPublishPath,
|
|
47
65
|
defaultSteamcmdCacheDir,
|
|
48
66
|
ensureSteamcmd,
|
|
67
|
+
runSteamPublish,
|
|
68
|
+
steamPublishFromCli,
|
|
49
69
|
runSteamAuth,
|
|
50
70
|
steamAuthFromCli,
|
|
51
|
-
} 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,33 @@ 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
|
+
};
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Wrapfully URL path segment for build/release.
|
|
26
|
+
* Single-target platforms post to the target route (`android`, `ios`, …) so OS relay
|
|
27
|
+
* and long-standing target routes work; multi-target platforms post to the family
|
|
28
|
+
* (`electron`, …).
|
|
29
|
+
*
|
|
30
|
+
* @param {{ family: string, targets: string[] }} buildSpec
|
|
31
|
+
*/
|
|
32
|
+
export function resolveWrapfullyRoute(buildSpec) {
|
|
33
|
+
if (buildSpec.targets.length === 1) {
|
|
34
|
+
return buildSpec.targets[0];
|
|
35
|
+
}
|
|
36
|
+
return buildSpec.family;
|
|
37
|
+
}
|
|
16
38
|
|
|
17
39
|
/**
|
|
18
40
|
* @param {AdaptfullyStage} stage
|
|
@@ -69,10 +91,15 @@ export async function runAdaptfullyStage(stage, platformKey, options) {
|
|
|
69
91
|
/** @type {{ deploymentKey: string, stage: string, family: string }[]} */
|
|
70
92
|
const sent = [];
|
|
71
93
|
|
|
94
|
+
const wrapfullyRoute = resolveWrapfullyRoute(buildSpec);
|
|
95
|
+
|
|
72
96
|
if (stage === 'release') {
|
|
73
97
|
const deploymentDirs = deployments.map((key) => resolvePublishDir(key));
|
|
74
98
|
|
|
75
|
-
log(
|
|
99
|
+
log(
|
|
100
|
+
`adaptfully: ${stage} ${platformKey} → ${buildSpec.family} `
|
|
101
|
+
+ `(${buildSpec.targets.join(', ')}) via /${wrapfullyRoute}/release`,
|
|
102
|
+
);
|
|
76
103
|
|
|
77
104
|
log(
|
|
78
105
|
`adaptfully: release deployments → ${
|
|
@@ -85,7 +112,7 @@ export async function runAdaptfullyStage(stage, platformKey, options) {
|
|
|
85
112
|
contents,
|
|
86
113
|
options.server,
|
|
87
114
|
'release',
|
|
88
|
-
|
|
115
|
+
wrapfullyRoute,
|
|
89
116
|
prebuiltDir,
|
|
90
117
|
pkg,
|
|
91
118
|
options.mode ?? 'extract',
|
|
@@ -107,7 +134,7 @@ export async function runAdaptfullyStage(stage, platformKey, options) {
|
|
|
107
134
|
log(
|
|
108
135
|
`adaptfully: ${stage} ${platformKey} → `
|
|
109
136
|
+ `${deploymentKey === 'zip' ? 'artifact zip' : `deployment "${deploymentKey}"`} `
|
|
110
|
-
+ `via Wrapfully ${buildSpec.family}`,
|
|
137
|
+
+ `via Wrapfully /${wrapfullyRoute}/${wrapStage} (${buildSpec.family})`,
|
|
111
138
|
);
|
|
112
139
|
|
|
113
140
|
await send(
|
|
@@ -115,7 +142,7 @@ export async function runAdaptfullyStage(stage, platformKey, options) {
|
|
|
115
142
|
contents,
|
|
116
143
|
options.server,
|
|
117
144
|
wrapStage,
|
|
118
|
-
|
|
145
|
+
wrapfullyRoute,
|
|
119
146
|
prebuiltDir,
|
|
120
147
|
pkg,
|
|
121
148
|
options.mode ?? 'extract',
|
|
@@ -141,7 +168,12 @@ export async function adaptfullyFromCli(argv = process.argv) {
|
|
|
141
168
|
const command = argv[2];
|
|
142
169
|
|
|
143
170
|
if (command === 'steam-auth') {
|
|
144
|
-
|
|
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);
|
|
145
177
|
}
|
|
146
178
|
|
|
147
179
|
const stage = command;
|
|
@@ -154,18 +186,23 @@ export async function adaptfullyFromCli(argv = process.argv) {
|
|
|
154
186
|
throw new Error(
|
|
155
187
|
'Usage:\n'
|
|
156
188
|
+ ' adaptfully <prebuild|build|deploy|release> <platform> [server] [mode] [--deployment <key>] [--artifact <path>]\n'
|
|
157
|
-
+ ' 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'
|
|
158
192
|
+ '\n'
|
|
159
193
|
+ 'Stages:\n'
|
|
160
194
|
+ ' prebuild Copy deploy/ and apply platform registrations → output/<platform>-prebuild/\n'
|
|
161
|
-
+ ' build prebuild + POST /{family}/build (zip artifact only)\n'
|
|
195
|
+
+ ' build prebuild + POST /{target|family}/build (zip artifact only)\n'
|
|
162
196
|
+ ' deploy POST /deploy/{key} with prior artifact + deployment credentials\n'
|
|
163
|
-
+ ' release prebuild + POST /{family}/release (build + configured deployments)\n'
|
|
197
|
+
+ ' release prebuild + POST /{target|family}/release (build + configured deployments)\n'
|
|
164
198
|
+ '\n'
|
|
165
199
|
+ ' --deployment <key> Target a single named deployment.\n'
|
|
166
200
|
+ ' --artifact <path> Prior build artifact directory (deploy only; default: ./output/ after build).\n'
|
|
167
201
|
+ '\n'
|
|
168
|
-
+ '
|
|
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',
|
|
169
206
|
);
|
|
170
207
|
}
|
|
171
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;
|