@makefully/adaptfully 3.6.1 → 3.8.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 CHANGED
@@ -2,6 +2,27 @@
2
2
 
3
3
  All notable changes to this project are documented in this file.
4
4
 
5
+ ## 3.8.0 — 2026-07-10
6
+
7
+ ### Changed
8
+
9
+ - **`steam-auth` Steamworks app ID** moves from top-level `config.steamId` to **`platforms.<name>.steamId`**. Steam **upload** app IDs belong in each deployment’s `manifest.json` (Wrapfully), not in Adaptfully client config.
10
+
11
+ ## 3.7.0 — 2026-07-09
12
+
13
+ ### Added
14
+
15
+ - **`adaptfully release <platform>`** stage — single Wrapfully call for build + configured deployments.
16
+ - **`resolveBuildSpec()`** — returns `{ family, targets, platformKey, steamworks, deployments }` for multi-target platforms.
17
+ - **`createSourceArchive`**, **`createDeployArchive`**, **`createReleaseArchive`** — split zip profiles for build/deploy/release.
18
+ - **`--artifact <path>`** on deploy for promoting a prior build artifact.
19
+
20
+ ### Changed
21
+
22
+ - **`send()`** targets Wrapfully v2 URLs: `/{family}/build`, `/{family}/release`, `/deploy/{key}`.
23
+ - Default **`deployments`** when omitted is `["zip"]` instead of the platform key.
24
+ - **`builder`** in platform config may be a string or array (e.g. `["win","mac","linux"]`).
25
+
5
26
  ## 3.6.1 — 2026-07-06
6
27
 
7
28
  ### Changed
package/README.md CHANGED
@@ -21,22 +21,44 @@ Games register platform services before load and retrieve them in-game. Adaptful
21
21
  ### Pipeline stages
22
22
 
23
23
  ```bash
24
- adaptfully prebuild web # deploy/ → output/web-prebuild/
25
- adaptfully build steam # prebuild + zip and send to Wrapfully
26
- adaptfully deploy web # prebuild + POST to Wrapfully webapp builder (SFTP when sftp.json is in the zip)
27
- adaptfully deploy steam # prebuild + POST to Wrapfully steam builder
28
- adaptfully steam-auth # one-time: log in with steamcmd assets/meta/publish/steam.json
24
+ adaptfully prebuild web # deploy/ → output/web-prebuild/
25
+ adaptfully build steam # prebuild + zip and send to Wrapfully (default deployment)
26
+ adaptfully deploy web # prebuild + POST to Wrapfully for each of web's deployments
27
+ adaptfully deploy web --deployment web-prod # deploy to a single named deployment
28
+ adaptfully deploy steam # prebuild + POST to Wrapfully steam builder
29
+ adaptfully steam-auth # one-time: log in with steamcmd → assets/meta/deployments/steam/steam.json
29
30
  ```
30
31
 
31
32
  | Stage | What it does |
32
33
  |-------|----------------|
33
34
  | `prebuild` | Copy `deploy/` to `output/<platform>-prebuild/` and inject registrations into `config.htmlInjections` |
34
- | `build` | Prebuild, then POST the result to Wrapfully |
35
- | `deploy` | Prebuild, zip, POST to Wrapfully; platform release when credentials are in `assets/meta/publish/` |
36
- | `steam-auth` | One-time local setup: install steamcmd, interactive login, write `assets/meta/publish/steam.json` |
35
+ | `build` | Prebuild, then POST the result to Wrapfully (artifact zip only) |
36
+ | `deploy` | POST the prior build artifact from `./output/` to each configured deployment (or `--deployment`) |
37
+ | `steam-auth` | One-time local setup: install steamcmd, interactive login, write the `steam` deployment's `steam.json` |
37
38
 
38
39
  `wrapfully-deploy` is a compatibility alias for `adaptfully deploy` when invoked with a Wrapfully builder name (`steam`, `win`, `android`, etc.).
39
40
 
41
+ ### Platforms vs. deployments
42
+
43
+ A **platform** (`config.platforms.<key>`) describes *how* to build — registrations, packager, and the Wrapfully builder. A **deployment** describes *where* a build is sent, along with the credentials for that target. This lets a single build fan out to several targets of the same platform type (for example, a staging and a production SFTP host).
44
+
45
+ - A platform may declare `"deployments": ["<key>", ...]`. Each key maps to a credential folder at `assets/meta/deployments/<key>/`.
46
+ - If a platform has no `deployments` array, the platform key itself is the single default deployment key (so `steam` → `assets/meta/deployments/steam/`).
47
+ - `adaptfully deploy <platform>` prebuilds once, then sends to Wrapfully once per deployment. `--deployment <key>` narrows it to one.
48
+ - Only the selected deployment's folder is shipped in each zip (as `meta/publish/`), so other targets' credentials never travel with a build.
49
+
50
+ ```json
51
+ "config": {
52
+ "platforms": {
53
+ "web": { "registrations": { "auth": "google-auth" }, "deployments": ["web-testing", "web-prod"] },
54
+ "steam": { "packager": "electron", "registrations": { "auth": "steam-auth" } },
55
+ "steam-dev": { "packager": "electron", "registrations": { "auth": "steam-auth" }, "deployments": ["steam"] }
56
+ }
57
+ }
58
+ ```
59
+
60
+ > **Back-compat:** projects with no `assets/meta/deployments/` directory keep the legacy behavior — the entire `assets/meta/` tree (including `assets/meta/publish/`) is shipped as `meta/`, and a single set of credentials is read from `meta/publish/`.
61
+
40
62
  Place `<!-- adaptfully -->` / `<!-- /adaptfully -->` markers in your HTML templates where registrations should be injected (typically between split bundle scripts, before `account.js` runs).
41
63
 
42
64
  ```javascript
@@ -113,12 +135,11 @@ Each platform entry may set a **`packager`** (`web`, `electron`, `cordova`, or `
113
135
  | `cordova` | `cordova.js` stub, `game-config.js`, CSP/viewport HTML extras |
114
136
  | `capacitor` | `game-config.js` (more Capacitor-specific output planned) |
115
137
 
116
- **`steam-auth` requires `packager: "electron"`** and `config.steamId`.
138
+ **`steam-auth` requires `packager: "electron"`** and `platforms.<name>.steamId` (Steamworks app ID for the client preload). Steam **upload** app IDs live separately in each Steam deployment’s `manifest.json` — see Wrapfully docs.
117
139
 
118
140
  ```json
119
141
  {
120
142
  "config": {
121
- "steamId": 719140,
122
143
  "platforms": {
123
144
  "web": {
124
145
  "packager": "web",
@@ -126,6 +147,7 @@ Each platform entry may set a **`packager`** (`web`, `electron`, `cordova`, or `
126
147
  },
127
148
  "steam": {
128
149
  "packager": "electron",
150
+ "steamId": 1234567,
129
151
  "registrations": { "auth": "steam-auth" }
130
152
  }
131
153
  }
@@ -138,7 +160,7 @@ Each platform entry may set a **`packager`** (`web`, `electron`, `cordova`, or `
138
160
  When `steam-auth` is registered on an **`electron`** platform, Adaptfully prebuild writes:
139
161
 
140
162
  - **`main.js`** — Electron shell with Steam overlay enabled and a preload script wired in
141
- - **`preload.js`** — initializes [steamworks.js](https://github.com/ceifa/steamworks.js) with `config.steamId` and exposes `window.__ADAPTFULLY_STEAMWORKS__`
163
+ - **`preload.js`** — initializes [steamworks.js](https://github.com/ceifa/steamworks.js) with `platforms.<name>.steamId` and exposes `window.__ADAPTFULLY_STEAMWORKS__`
142
164
 
143
165
  The renderer `steam-auth` plugin reads the Steam ID from that bridge. When Steam is available, `autoLogin()` succeeds immediately with `{ id: steamId64, email: '' }`. Optional config keys:
144
166
 
@@ -297,16 +319,20 @@ mygame/
297
319
  │ ├── web-prebuild/ # after adaptfully prebuild web
298
320
  │ └── steam-prebuild/ # after adaptfully prebuild steam
299
321
  └── assets/
300
- └── meta/ # packaged as meta/ in the zip
322
+ └── meta/ # shared files packaged as meta/ in the zip
301
323
  ├── icon-foreground.png
302
324
  ├── icon-background.png
303
- └── publish/ # platform signing & deploy credentials
304
- ├── build.json
305
- ├── android/
306
- ├── apple.json
307
- └── ...
325
+ └── deployments/ # one folder per deployment target (creds)
326
+ ├── web-testing/
327
+ │ └── sftp.json
328
+ ├── web-prod/
329
+ └── sftp.json
330
+ └── steam/ # default deployment for the `steam` platform
331
+ └── steam.json
308
332
  ```
309
333
 
334
+ 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>/`.
335
+
310
336
  Icons (`icon-foreground.png`, `icon-background.png`) are required for mobile, desktop, and Steam builds.
311
337
 
312
338
  ### Icons
@@ -345,7 +371,6 @@ Standard npm fields (`name`, `version`, `description`) are used directly. Add a
345
371
  "scope": "https://example.com/games/",
346
372
  "themeColor": "#1a1a2e",
347
373
  "twitterId": "@examplegames",
348
- "steamId": 1234567,
349
374
  "deployFolder": "deploy",
350
375
  "platforms": {
351
376
  "web": {
@@ -356,6 +381,7 @@ Standard npm fields (`name`, `version`, `description`) are used directly. Add a
356
381
  },
357
382
  "steam": {
358
383
  "packager": "electron",
384
+ "steamId": 1234567,
359
385
  "registrations": {
360
386
  "auth": "steam-auth",
361
387
  "storage": "javascript/adaptfully-bridge.js"
@@ -381,7 +407,6 @@ Standard npm fields (`name`, `version`, `description`) are used directly. Add a
381
407
  | `scope` | Web/PWA | Base URL scope for the web app |
382
408
  | `themeColor` | Cordova, Electron, UWP, web | Loading screen / theme color |
383
409
  | `twitterId` | Web | Twitter handle for meta tags |
384
- | `steamId` | Steam | Steam app ID |
385
410
  | `deployFolder` | Client | Neutral deploy directory staged before prebuild (default: `deploy`) |
386
411
  | `htmlInjections` | Prebuild | Deploy-relative HTML paths to inject (default: `["index.html"]`) |
387
412
  | `outputFolder` | Client | Prebuild output root (default: `output`) |
@@ -389,6 +414,7 @@ Standard npm fields (`name`, `version`, `description`) are used directly. Add a
389
414
  | `platforms.<name>.builder` | Build/deploy | Override Wrapfully builder for a platform (default: `web` → `webapp`, others match platform key) |
390
415
  | `platforms.<name>.builders` | wrapfully-deploy | Map additional Wrapfully builder names to a platform |
391
416
  | `platforms.<name>.packager` | Prebuild | `web` (default), `electron`, `cordova`, or `capacitor` — selects the packager class that adds platform-specific files during prebuild |
417
+ | `platforms.<name>.steamId` | Electron + steam-auth | Steamworks app ID baked into `preload.js` (client). Upload app IDs belong in the Steam deployment `manifest.json` |
392
418
  | `properties` | Cordova | Cordova config.xml entries (plugins, allow-navigation, etc.) |
393
419
 
394
420
  ### `wrapfully.json`
@@ -436,11 +462,11 @@ For a single platform, pass the specific builder name rather than a composite.
436
462
 
437
463
  ### Platform package requirements
438
464
 
439
- Signing keys, provisioning profiles, and store credentials go in `./assets/meta/publish/` on disk (sent as `meta/publish/` in the zip). **These files contain secrets** — add them to `.gitignore` and never commit them to a public repository.
465
+ Signing keys, provisioning profiles, and store credentials go in the deployment folder `./assets/meta/deployments/<deployment>/` on disk (the selected deployment is sent as `meta/publish/` in the zip). The examples below use `<deployment>` as a placeholder for the target key (e.g. `steam`, `web-prod`). **These files contain secrets** — add them to `.gitignore` and never commit them to a public repository. (Projects that have not adopted the `deployments/` layout may still place these under the legacy `./assets/meta/publish/`.)
440
466
 
441
467
  #### Android (`android`, `android-dev`)
442
468
 
443
- Place keystore files in `assets/meta/publish/android/`. Include `assets/meta/publish/build.json`:
469
+ Place keystore files in `assets/meta/deployments/<deployment>/android/`. Include `assets/meta/deployments/<deployment>/build.json`:
444
470
 
445
471
  ```json
446
472
  {
@@ -465,7 +491,7 @@ Place keystore files in `assets/meta/publish/android/`. Include `assets/meta/pub
465
491
  }
466
492
  ```
467
493
 
468
- To deploy to Google Play, also include `assets/meta/publish/google.json`:
494
+ To deploy to Google Play, also include `assets/meta/deployments/<deployment>/google.json`:
469
495
 
470
496
  ```json
471
497
  {
@@ -484,7 +510,7 @@ To deploy to Google Play, also include `assets/meta/publish/google.json`:
484
510
 
485
511
  #### Apple (`ios`, `ios-dev`, `ios-sim`, `mac`, `apple`, `apple-dev`)
486
512
 
487
- Include `assets/meta/publish/build.json` with iOS signing settings:
513
+ Include `assets/meta/deployments/<deployment>/build.json` with iOS signing settings:
488
514
 
489
515
  ```json
490
516
  {
@@ -507,7 +533,7 @@ Include `assets/meta/publish/build.json` with iOS signing settings:
507
533
  }
508
534
  ```
509
535
 
510
- To deploy to the App Store, include `assets/meta/publish/apple.json`:
536
+ To deploy to the App Store, include `assets/meta/deployments/<deployment>/apple.json`:
511
537
 
512
538
  ```json
513
539
  {
@@ -526,7 +552,7 @@ Requires the Android and Apple package requirements above.
526
552
 
527
553
  `steam-dev` builds debug Electron binaries for Windows, Mac, and Linux without uploading to Steam. No `steam.json` credentials are required.
528
554
 
529
- For release uploads, run **`adaptfully steam-auth`** once on your machine. It installs steamcmd if needed, logs you in interactively (enter a Steam Guard code if prompted), and writes `assets/meta/publish/steam.json`:
555
+ For release uploads, run **`adaptfully steam-auth`** 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`:
530
556
 
531
557
  ```json
532
558
  {
@@ -542,7 +568,7 @@ Accounts **without** Steam Guard only need `username`, `password`, and `configVd
542
568
 
543
569
  Re-run `adaptfully steam-auth` if Steam invalidates the token (new machine, password change, or expired guard).
544
570
 
545
- Also set `steamId` in your `config` block.
571
+ 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.
546
572
 
547
573
  Steam builds can run on either the Windows or Mac server. The server that receives the request builds its own platforms and requests the rest from the other server (Windows builds `win` and requests `mac`/`linux`; Mac builds `mac`/`linux` and requests `win`). Install the Steamworks SDK ContentBuilder on any server that will upload to Steam.
548
574
 
@@ -552,11 +578,11 @@ When builds relay between servers, `meta/publish/` credentials travel in the zip
552
578
 
553
579
  `-dev` builders produce debug Electron apps with DevTools enabled and the application menu visible. Dev builds skip code signing, notarization, and Steam upload. No publish credentials are required for dev builds.
554
580
 
555
- Release `win` builds can be signed with `assets/meta/publish/ms.json` (see Windows below). Release `mac` builds can use `assets/meta/publish/apple.json` for signing and notarization (see Apple above).
581
+ Release `win` builds can be signed with `assets/meta/deployments/<deployment>/ms.json` (see Windows below). Release `mac` builds can use `assets/meta/deployments/<deployment>/apple.json` for signing and notarization (see Apple above).
556
582
 
557
583
  #### Web (`web` / `webapp`)
558
584
 
559
- `adaptfully deploy web` prebuilds and POSTs to the Wrapfully **`webapp`** builder (same conduit as Steam). Include `assets/meta/publish/sftp.json` in the project so it is zipped as `meta/publish/sftp.json` for Wrapfully to deploy via SFTP.
585
+ `adaptfully deploy web` prebuilds and POSTs to the Wrapfully **`webapp`** builder (same conduit as Steam). Put an `sftp.json` in each web deployment folder (e.g. `assets/meta/deployments/web-prod/sftp.json`); the selected deployment is zipped as `meta/publish/sftp.json` for Wrapfully to deploy via SFTP. The inner key stays the builder name (`webapp`).
560
586
 
561
587
  ```json
562
588
  {
@@ -578,7 +604,7 @@ See [Wrapfully README](https://github.com/Makefully-Studios/wrapfully-client) fo
578
604
 
579
605
  #### Windows (`win`, `win-dev`, `uwp`)
580
606
 
581
- To sign the app, place your certificate at `assets/meta/publish/ms/packcert.pfx` and include `assets/meta/publish/ms.json`:
607
+ To sign the app, place your certificate at `assets/meta/deployments/<deployment>/ms/packcert.pfx` and include `assets/meta/deployments/<deployment>/ms.json`:
582
608
 
583
609
  ```json
584
610
  {
@@ -1,12 +1,57 @@
1
1
  import archiver from 'archiver';
2
2
  import fs from 'node:fs';
3
+ import path from 'node:path';
4
+
5
+ const META_DIR = 'assets/meta';
6
+ const DEPLOYMENTS_DIRNAME = 'deployments';
7
+ const PUBLISH_DIRNAME = 'publish';
3
8
 
4
9
  /**
5
- * @param {string} deployFolder
6
- * @param {string} contents - serialized package.json for the zip
7
- * @param {{ indexHtml?: string }} [options]
10
+ * @param {import('archiver').Archiver} zip
11
+ * @param {string} [publishDir] Single deployment folder
12
+ * @param {string[]} [deploymentDirs] Multiple deployment folders for release
8
13
  */
9
- export function createArchive(deployFolder, contents, options = {}) {
14
+ function appendMeta(zip, publishDir, deploymentDirs = []) {
15
+ if (!fs.existsSync(META_DIR)) {
16
+ return;
17
+ }
18
+
19
+ const deploymentsRoot = path.join(META_DIR, DEPLOYMENTS_DIRNAME);
20
+
21
+ if (!fs.existsSync(deploymentsRoot)) {
22
+ zip.directory(`${META_DIR}/`, 'meta');
23
+ return;
24
+ }
25
+
26
+ for (const entry of fs.readdirSync(META_DIR, { withFileTypes: true })) {
27
+ if (entry.name === DEPLOYMENTS_DIRNAME || entry.name === PUBLISH_DIRNAME) {
28
+ continue;
29
+ }
30
+
31
+ const fullPath = path.join(META_DIR, entry.name);
32
+ if (entry.isDirectory()) {
33
+ zip.directory(`${fullPath}/`, `meta/${entry.name}`);
34
+ } else {
35
+ zip.file(fullPath, { name: `meta/${entry.name}` });
36
+ }
37
+ }
38
+
39
+ if (publishDir && fs.existsSync(publishDir)) {
40
+ zip.directory(`${publishDir}/`, `meta/${PUBLISH_DIRNAME}`);
41
+ return;
42
+ }
43
+
44
+ for (const deploymentDir of deploymentDirs) {
45
+ if (!fs.existsSync(deploymentDir)) {
46
+ continue;
47
+ }
48
+
49
+ const deploymentKey = path.basename(deploymentDir);
50
+ zip.directory(`${deploymentDir}/`, `meta/${DEPLOYMENTS_DIRNAME}/${deploymentKey}`);
51
+ }
52
+ }
53
+
54
+ function createZipStream(deployFolder, contents, options = {}) {
10
55
  const zip = archiver('zip', { zlib: { level: 0 } });
11
56
 
12
57
  zip.on('warning', (err) => {
@@ -33,11 +78,64 @@ export function createArchive(deployFolder, contents, options = {}) {
33
78
  zip.directory(`${deployFolder}/`, 'deploy');
34
79
  zip.file(`${deployFolder}/index.html`, { name: 'deploy/index.html' });
35
80
  }
36
- if (fs.existsSync('assets/meta/')) {
37
- zip.directory('assets/meta/', 'meta');
38
- }
81
+
82
+ appendMeta(zip, options.publishDir, options.deploymentDirs);
39
83
  zip.append(contents, { name: 'package.json' });
40
84
  zip.finalize();
41
85
 
42
86
  return zip;
43
87
  }
88
+
89
+ /**
90
+ * Game source zip for /build and /release (no credentials unless release).
91
+ *
92
+ * @param {string} deployFolder
93
+ * @param {string} contents
94
+ * @param {{ indexHtml?: string, publishDir?: string, deploymentDirs?: string[] }} [options]
95
+ */
96
+ export function createSourceArchive(deployFolder, contents, options = {}) {
97
+ return createZipStream(deployFolder, contents, options);
98
+ }
99
+
100
+ /**
101
+ * @deprecated Use createSourceArchive, createDeployArchive, or createReleaseArchive.
102
+ */
103
+ export function createArchive(deployFolder, contents, options = {}) {
104
+ return createSourceArchive(deployFolder, contents, options);
105
+ }
106
+
107
+ /**
108
+ * Prior artifact zip plus one deployment folder for /deploy.
109
+ *
110
+ * @param {string} artifactPath
111
+ * @param {string} publishDir
112
+ * @param {string} [contents]
113
+ */
114
+ export function createDeployArchive(artifactPath, publishDir, contents) {
115
+ const zip = archiver('zip', { zlib: { level: 0 } });
116
+
117
+ zip.directory(`${artifactPath}/`, false);
118
+ appendMeta(zip, publishDir);
119
+
120
+ if (contents) {
121
+ zip.append(contents, { name: 'package.json' });
122
+ }
123
+
124
+ zip.finalize();
125
+ return zip;
126
+ }
127
+
128
+ /**
129
+ * Game source zip with all non-zip deployment credential folders for /release.
130
+ *
131
+ * @param {string} deployFolder
132
+ * @param {string} contents
133
+ * @param {string[]} deploymentDirs
134
+ * @param {{ indexHtml?: string }} [options]
135
+ */
136
+ export function createReleaseArchive(deployFolder, contents, deploymentDirs, options = {}) {
137
+ return createZipStream(deployFolder, contents, {
138
+ ...options,
139
+ deploymentDirs,
140
+ });
141
+ }
@@ -0,0 +1,58 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+
4
+ const MANIFEST_NAME = 'wrapfully-build.json';
5
+
6
+ /**
7
+ * Directory where build responses are extracted (contains wrapfully-build.json).
8
+ *
9
+ * @param {{ config?: { outputFolder?: string } }} pkg
10
+ * @param {string} [outputRoot='output']
11
+ */
12
+ export function buildOutputDir(pkg, outputRoot = 'output') {
13
+ const outputFolder = pkg.config?.outputFolder || outputRoot;
14
+ return path.resolve(outputFolder);
15
+ }
16
+
17
+ /**
18
+ * Resolve the artifact directory from a prior `adaptfully build` (or compatible zip extract).
19
+ *
20
+ * @param {{ name: string, version: string, config?: { outputFolder?: string } }} pkg
21
+ * @param {{ outputRoot?: string, platformKey?: string }} [options]
22
+ * @returns {string}
23
+ */
24
+ export function resolveBuildArtifactDir(pkg, options = {}) {
25
+ const artifactDir = buildOutputDir(pkg, options.outputRoot);
26
+ const manifestPath = path.join(artifactDir, MANIFEST_NAME);
27
+
28
+ if (!fs.existsSync(manifestPath)) {
29
+ throw new Error(
30
+ 'No prior build artifact found in '
31
+ + `${artifactDir}. Run \`adaptfully build <platform>\` first or pass --artifact <path>.`,
32
+ );
33
+ }
34
+
35
+ let manifest;
36
+ try {
37
+ manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf8'));
38
+ } catch {
39
+ throw new Error(`Unable to read ${manifestPath}`);
40
+ }
41
+
42
+ const gameId = `${pkg.name}-${pkg.version}`;
43
+ if (manifest.gameId && manifest.gameId !== gameId) {
44
+ throw new Error(
45
+ `Build artifact is for "${manifest.gameId}" but package.json is ${gameId}. `
46
+ + 'Run a fresh build or pass --artifact <path>.',
47
+ );
48
+ }
49
+
50
+ if (options.platformKey && manifest.platformKey && manifest.platformKey !== options.platformKey) {
51
+ throw new Error(
52
+ `Build artifact is for platform "${manifest.platformKey}" but deploy targets "${options.platformKey}". `
53
+ + 'Run a fresh build or pass --artifact <path>.',
54
+ );
55
+ }
56
+
57
+ return artifactDir;
58
+ }
@@ -3,31 +3,79 @@ import fs from 'node:fs';
3
3
  import path from 'node:path';
4
4
  import { pipeline } from 'node:stream/promises';
5
5
  import unzipper from 'unzip-stream';
6
- import { createArchive } from './archive.js';
6
+ import { createDeployArchive, createReleaseArchive, createSourceArchive } from './archive.js';
7
7
  import { listHtmlFilesRecursive } from './fs-utils.js';
8
8
  import { printBuildReport } from './report.js';
9
9
 
10
+ /**
11
+ * @param {string} gameId
12
+ * @param {string} platformKey
13
+ */
14
+ function buildInfoParam(gameId, platformKey) {
15
+ return platformKey && platformKey !== gameId ? `${gameId}_${platformKey}` : gameId;
16
+ }
17
+
18
+ /**
19
+ * @param {string} server
20
+ * @param {'build' | 'deploy' | 'release'} stage
21
+ * @param {string} family
22
+ * @param {string} gameId
23
+ * @param {string} platformKey
24
+ * @param {string} [deploymentKey]
25
+ */
26
+ function resolveWrapfullyUrl(server, stage, family, gameId, platformKey, deploymentKey) {
27
+ const info = buildInfoParam(gameId, platformKey);
28
+
29
+ if (stage === 'deploy') {
30
+ return `${server}deploy/${deploymentKey}/${info}`;
31
+ }
32
+
33
+ return `${server}${family}/${stage}/${info}`;
34
+ }
35
+
10
36
  /**
11
37
  * @param {string} gameId
12
38
  * @param {string} contents
13
39
  * @param {string} server
14
- * @param {string} builder
15
- * @param {string} deployFolder - prebuilt deploy directory
40
+ * @param {'build' | 'deploy' | 'release'} stage
41
+ * @param {string} family
42
+ * @param {string} deployFolder
16
43
  * @param {{ name: string, version: string }} pkg
17
44
  * @param {'extract' | string} mode
18
- * @param {{ log?: (message: string) => void }} [options]
45
+ * @param {{ log?: (message: string) => void, publishDir?: string, deploymentDirs?: string[], platformKey?: string, deploymentKey?: string, artifactPath?: string }} [options]
19
46
  */
20
- export async function send(gameId, contents, server, builder, deployFolder, pkg, mode = 'extract', options = {}) {
47
+ export async function send(gameId, contents, server, stage, family, deployFolder, pkg, mode = 'extract', options = {}) {
21
48
  const log = options.log ?? console.log;
49
+ const platformKey = options.platformKey ?? family;
22
50
  const destination = mode === 'extract'
23
51
  ? unzipper.Extract({ path: './output/', concurrency: 1 })
24
- : fs.createWriteStream(`./output/${pkg.name}-${pkg.version}-${builder}.zip`);
52
+ : fs.createWriteStream(`./output/${pkg.name}-${pkg.version}-${stage}-${family}.zip`);
53
+
54
+ let archiveStream;
55
+
56
+ if (stage === 'deploy') {
57
+ if (!options.artifactPath) {
58
+ throw new Error('deploy stage requires options.artifactPath to the prior build artifact directory');
59
+ }
60
+ if (!options.deploymentKey) {
61
+ throw new Error('deploy stage requires options.deploymentKey');
62
+ }
63
+ archiveStream = createDeployArchive(options.artifactPath, options.publishDir, contents);
64
+ } else if (stage === 'release') {
65
+ archiveStream = createReleaseArchive(deployFolder, contents, options.deploymentDirs ?? []);
66
+ } else {
67
+ archiveStream = createSourceArchive(deployFolder, contents);
68
+ }
25
69
 
26
- const htmlFiles = listHtmlFilesRecursive(deployFolder);
27
- log(`adaptfully: sending ${htmlFiles.length} HTML file(s) from ${path.resolve(deployFolder)}`);
70
+ const url = resolveWrapfullyUrl(server, stage, family, gameId, platformKey, options.deploymentKey);
71
+ log(`adaptfully: POST ${url}`);
72
+
73
+ if (stage !== 'deploy') {
74
+ const htmlFiles = listHtmlFilesRecursive(deployFolder);
75
+ log(`adaptfully: sending ${htmlFiles.length} HTML file(s) from ${path.resolve(deployFolder)}`);
76
+ }
28
77
 
29
- const archiveStream = createArchive(deployFolder, contents);
30
- const { data } = await axios.post(`${server}${builder}/${gameId}`, archiveStream, {
78
+ const { data } = await axios.post(url, archiveStream, {
31
79
  maxRedirects: 0,
32
80
  responseType: 'stream',
33
81
  });
@@ -47,6 +95,6 @@ export async function send(gameId, contents, server, builder, deployFolder, pkg,
47
95
  }
48
96
 
49
97
  if (mode === 'extract') {
50
- printBuildReport(builder, pkg);
98
+ printBuildReport(`${stage}-${family}`, pkg);
51
99
  }
52
100
  }
package/lib/node/index.js CHANGED
@@ -1,7 +1,8 @@
1
- export { createArchive } from './archive.js';
1
+ export { buildOutputDir, resolveBuildArtifactDir } from './artifacts.js';
2
+ export { createArchive, createDeployArchive, createReleaseArchive, createSourceArchive } from './archive.js';
2
3
  export { loadProjectConfig, resolveServerUrl } from './config.js';
3
4
  export { send } from './deploy.js';
4
- export { adaptfullyFromCli, runAdaptfullyStage } from './pipeline.js';
5
+ export { adaptfullyFromCli, parseStageArgs, runAdaptfullyStage } from './pipeline.js';
5
6
  export { prebuildPlatform, prebuildOutputDir, resolveHtmlInjections } from './prebuild.js';
6
7
  export { getPackageRoot, getRuntimeDir, resolveRuntimeScript } from './paths.js';
7
8
  export {
@@ -28,9 +29,14 @@ export {
28
29
  collectRegistrationParts,
29
30
  injectAdaptfullyRegistrations,
30
31
  resolveBuilderForPlatform,
32
+ resolveBuildSpec,
31
33
  resolveCliPlatformAndBuilder,
34
+ resolveDeploymentsForPlatform,
35
+ resolveFamily,
36
+ resolveFamilyFromTarget,
32
37
  resolvePlatformKey,
33
38
  resolvePlatformRegistrationsByKey,
39
+ resolvePublishDir,
34
40
  resolveRegistrationAssets,
35
41
  } from './registrations.js';
36
42
  export { printBuildReport } from './report.js';
@@ -178,6 +178,19 @@ export function buildElectronPreload(steamAppId) {
178
178
  return ELECTRON_PRELOAD_HEAD + steamAppId + ELECTRON_PRELOAD_TAIL;
179
179
  }
180
180
 
181
+ /**
182
+ * Steamworks app ID for steam-auth preload, from the active platform config.
183
+ * @param {string | null | undefined} platformKey
184
+ * @param {{ config?: { platforms?: Record<string, { steamId?: number | string }> } }} pkg
185
+ * @returns {number | string | undefined}
186
+ */
187
+ export function resolvePlatformSteamId(platformKey, pkg) {
188
+ if (!platformKey) {
189
+ return undefined;
190
+ }
191
+ return pkg.config?.platforms?.[platformKey]?.steamId;
192
+ }
193
+
181
194
  export class ElectronPackager extends Packager {
182
195
  /** @type {'electron'} */
183
196
  static id = 'electron';
@@ -188,10 +201,10 @@ export class ElectronPackager extends Packager {
188
201
  validate() {
189
202
  super.validate();
190
203
 
191
- if (this.usesPlugin('steam-auth') && !this.pkg.config?.steamId) {
204
+ if (this.usesPlugin('steam-auth') && !resolvePlatformSteamId(this.platformKey, this.pkg)) {
192
205
  const platformLabel = this.platformKey ?? this.platforms.join(', ');
193
206
  throw new Error(
194
- `Platform "${platformLabel}" uses steam-auth but config.steamId is not set.`,
207
+ `Platform "${platformLabel}" uses steam-auth but platforms.${platformLabel}.steamId is not set.`,
195
208
  );
196
209
  }
197
210
  }
@@ -212,9 +225,10 @@ export class ElectronPackager extends Packager {
212
225
  const mainContent = buildElectronMain(withSteam, this.pkg.config?.themeColor);
213
226
 
214
227
  if (withSteam) {
228
+ const steamId = resolvePlatformSteamId(this.platformKey, this.pkg);
215
229
  fs.writeFileSync(
216
230
  path.join(dest, 'preload.js'),
217
- buildElectronPreload(this.pkg.config.steamId),
231
+ buildElectronPreload(steamId),
218
232
  );
219
233
  this.log('adaptfully: write preload.js (steam-auth)');
220
234
  }
@@ -6,7 +6,12 @@ import { WebPackager } from './web.js';
6
6
 
7
7
  export { Packager, VALID_PACKAGERS } from './base.js';
8
8
  export { WebPackager } from './web.js';
9
- export { ElectronPackager, buildElectronMain, buildElectronPreload } from './electron.js';
9
+ export {
10
+ ElectronPackager,
11
+ buildElectronMain,
12
+ buildElectronPreload,
13
+ resolvePlatformSteamId,
14
+ } from './electron.js';
10
15
  export { CordovaPackager } from './cordova.js';
11
16
  export { CapacitorPackager } from './capacitor.js';
12
17
 
@@ -22,7 +27,7 @@ const PACKAGER_REGISTRY = {
22
27
 
23
28
  /**
24
29
  * @param {string} platformKey
25
- * @param {{ config?: { platforms?: Record<string, { packager?: string, registrations?: Record<string, string> }>, steamId?: number } }} pkg
30
+ * @param {{ config?: { platforms?: Record<string, { packager?: string, registrations?: Record<string, string>, steamId?: number | string }> } }} pkg
26
31
  * @param {{ log?: (message: string) => void, platforms?: string[] }} [options]
27
32
  * @returns {Packager}
28
33
  */
@@ -54,7 +59,7 @@ export function resolvePlatformPackager(platformKey, pkg) {
54
59
 
55
60
  /**
56
61
  * @param {string} platformKey
57
- * @param {{ config?: { platforms?: Record<string, { packager?: string, registrations?: Record<string, string> }>, steamId?: number } }} pkg
62
+ * @param {{ config?: { platforms?: Record<string, { packager?: string, registrations?: Record<string, string>, steamId?: number | string }> } }} pkg
58
63
  */
59
64
  export function validatePlatformPackager(platformKey, pkg) {
60
65
  createPackagerForPlatform(platformKey, pkg).validate();
@@ -70,7 +75,7 @@ export function usesSteamAuth(registrations) {
70
75
  /**
71
76
  * @param {string} dest
72
77
  * @param {string} platformKey
73
- * @param {{ config?: { platforms?: Record<string, { packager?: string, registrations?: Record<string, string> }>, steamId?: number } }} pkg
78
+ * @param {{ config?: { platforms?: Record<string, { packager?: string, registrations?: Record<string, string>, steamId?: number | string }> } }} pkg
74
79
  * @param {{ log?: (message: string) => void, platforms?: string[] }} [options]
75
80
  */
76
81
  export function applyPackagerTemplates(dest, platformKey, pkg, options = {}) {
@@ -1,50 +1,130 @@
1
1
  import { loadProjectConfig, resolveServerUrl } from './config.js';
2
2
  import { send } from './deploy.js';
3
+ import { resolveBuildArtifactDir } from './artifacts.js';
3
4
  import { prebuildPlatform } from './prebuild.js';
4
- import { resolveBuilderForPlatform } from './registrations.js';
5
+ import {
6
+ resolveBuildSpec,
7
+ resolveDeploymentsForPlatform,
8
+ resolvePublishDir,
9
+ } from './registrations.js';
5
10
  import { steamAuthFromCli } from './steam-auth.js';
6
11
 
7
- /** @typedef {'prebuild' | 'build' | 'deploy'} AdaptfullyStage */
12
+ /** @typedef {'prebuild' | 'build' | 'deploy' | 'release'} AdaptfullyStage */
8
13
 
9
- const VALID_STAGES = new Set(['prebuild', 'build', 'deploy']);
14
+ const VALID_STAGES = new Set(['prebuild', 'build', 'deploy', 'release']);
10
15
 
11
16
  /**
12
17
  * @param {AdaptfullyStage} stage
13
18
  * @param {string} platformKey
14
- * @param {{ pkg: object, deployFolder: string, server?: string, mode?: string, builder?: string, log?: (message: string) => void, outputRoot?: string }} options
19
+ * @param {{ pkg: object, deployFolder: string, server?: string, mode?: string, deployment?: string, artifactPath?: string, log?: (message: string) => void, outputRoot?: string }} options
15
20
  */
16
21
  export async function runAdaptfullyStage(stage, platformKey, options) {
17
22
  if (!VALID_STAGES.has(stage)) {
18
- throw new Error(`Unknown adaptfully stage "${stage}". Expected: prebuild, build, or deploy.`);
23
+ throw new Error(`Unknown adaptfully stage "${stage}". Expected: prebuild, build, deploy, or release.`);
19
24
  }
20
25
 
21
26
  const log = options.log ?? console.log;
22
27
  const { pkg, deployFolder } = options;
28
+ const buildSpec = resolveBuildSpec(platformKey, pkg);
29
+ let deployments = resolveDeploymentsForPlatform(platformKey, pkg);
23
30
 
24
- const prebuiltDir = prebuildPlatform(deployFolder, platformKey, pkg, {
25
- log,
26
- outputRoot: options.outputRoot,
27
- });
31
+ if (options.deployment) {
32
+ if (!deployments.includes(options.deployment)) {
33
+ throw new Error(
34
+ `Deployment "${options.deployment}" is not configured for platform "${platformKey}". `
35
+ + `Available deployments: ${deployments.join(', ')}`,
36
+ );
37
+ }
38
+ deployments = [options.deployment];
39
+ } else if (stage === 'build') {
40
+ deployments = ['zip'];
41
+ } else if (stage === 'deploy') {
42
+ deployments = deployments.filter((key) => key !== 'zip');
43
+ } else if (stage === 'release') {
44
+ deployments = deployments.filter((key) => key !== 'zip');
45
+ }
46
+
47
+ let artifactPath = options.artifactPath;
48
+ if (stage === 'deploy' && !artifactPath) {
49
+ artifactPath = resolveBuildArtifactDir(pkg, {
50
+ outputRoot: options.outputRoot,
51
+ platformKey,
52
+ });
53
+ }
54
+
55
+ const prebuiltDir = stage === 'deploy'
56
+ ? artifactPath
57
+ : prebuildPlatform(deployFolder, platformKey, pkg, {
58
+ log,
59
+ outputRoot: options.outputRoot,
60
+ });
28
61
 
29
62
  if (stage === 'prebuild') {
30
- return { prebuiltDir, platformKey };
63
+ return { prebuiltDir, platformKey, buildSpec };
31
64
  }
32
65
 
33
- const builder = options.builder ?? resolveBuilderForPlatform(platformKey, pkg);
34
- log(`adaptfully: ${stage} ${platformKey} via Wrapfully builder "${builder}"`);
66
+ const gameId = `${pkg.name}-${pkg.version}`;
67
+ const contents = JSON.stringify(pkg);
68
+ /** @type {{ deploymentKey: string, stage: string, family: string }[]} */
69
+ const sent = [];
35
70
 
36
- await send(
37
- `${pkg.name}-${pkg.version}`,
38
- JSON.stringify(pkg),
39
- options.server,
40
- builder,
41
- prebuiltDir,
42
- pkg,
43
- options.mode ?? 'extract',
44
- { log },
45
- );
71
+ if (stage === 'release') {
72
+ const deploymentDirs = deployments.map((key) => resolvePublishDir(key));
73
+
74
+ log(`adaptfully: ${stage} ${platformKey} → ${buildSpec.family} (${buildSpec.targets.join(', ')})`);
75
+
76
+ await send(
77
+ gameId,
78
+ contents,
79
+ options.server,
80
+ 'release',
81
+ buildSpec.family,
82
+ prebuiltDir,
83
+ pkg,
84
+ options.mode ?? 'extract',
85
+ {
86
+ log,
87
+ platformKey,
88
+ deploymentDirs,
89
+ },
90
+ );
91
+
92
+ sent.push({ deploymentKey: 'release', stage, family: buildSpec.family });
93
+ return { prebuiltDir, platformKey, buildSpec, deployments: sent, artifactPath };
94
+ }
95
+
96
+ for (const deploymentKey of deployments) {
97
+ const publishDir = deploymentKey === 'zip' ? undefined : resolvePublishDir(deploymentKey);
98
+ const wrapStage = stage === 'deploy' ? 'deploy' : 'build';
99
+
100
+ log(
101
+ `adaptfully: ${stage} ${platformKey} → `
102
+ + `${deploymentKey === 'zip' ? 'artifact zip' : `deployment "${deploymentKey}"`} `
103
+ + `via Wrapfully ${buildSpec.family}`,
104
+ );
46
105
 
47
- return { prebuiltDir, platformKey, builder };
106
+ await send(
107
+ gameId,
108
+ contents,
109
+ options.server,
110
+ wrapStage,
111
+ buildSpec.family,
112
+ prebuiltDir,
113
+ pkg,
114
+ options.mode ?? 'extract',
115
+ {
116
+ log,
117
+ publishDir,
118
+ platformKey,
119
+ deploymentKey: wrapStage === 'deploy' ? deploymentKey : undefined,
120
+ artifactPath,
121
+ },
122
+ );
123
+
124
+ sent.push({ deploymentKey, stage: wrapStage, family: buildSpec.family });
125
+ }
126
+
127
+ return { prebuiltDir, platformKey, buildSpec, deployments: sent, artifactPath };
48
128
  }
49
129
 
50
130
  /**
@@ -58,22 +138,27 @@ export async function adaptfullyFromCli(argv = process.argv) {
58
138
  }
59
139
 
60
140
  const stage = command;
61
- const platformKey = argv[3];
62
- const cliServer = argv[4];
63
- const mode = argv[5] ?? 'extract';
141
+ const { positionals, deployment, artifactPath } = parseStageArgs(argv.slice(3));
142
+ const platformKey = positionals[0];
143
+ const cliServer = positionals[1];
144
+ const mode = positionals[2] ?? 'extract';
64
145
 
65
146
  if (!stage || !platformKey) {
66
147
  throw new Error(
67
148
  'Usage:\n'
68
- + ' adaptfully <prebuild|build|deploy> <platform> [server] [mode]\n'
149
+ + ' adaptfully <prebuild|build|deploy|release> <platform> [server] [mode] [--deployment <key>] [--artifact <path>]\n'
69
150
  + ' adaptfully steam-auth [--username U] [--password P] [--output path]\n'
70
151
  + '\n'
71
152
  + 'Stages:\n'
72
153
  + ' prebuild Copy deploy/ and apply platform registrations → output/<platform>-prebuild/\n'
73
- + ' build prebuild + zip and send to Wrapfully\n'
74
- + ' deploy prebuild + zip and send to Wrapfully (platform release when credentials are in the zip)\n'
154
+ + ' build prebuild + POST /{family}/build (zip artifact only)\n'
155
+ + ' deploy POST /deploy/{key} with prior artifact + deployment credentials\n'
156
+ + ' release prebuild + POST /{family}/release (build + configured deployments)\n'
75
157
  + '\n'
76
- + ' steam-auth Install steamcmd if needed, log in interactively, write assets/meta/publish/steam.json',
158
+ + ' --deployment <key> Target a single named deployment.\n'
159
+ + ' --artifact <path> Prior build artifact directory (deploy only; default: ./output/ after build).\n'
160
+ + '\n'
161
+ + ' steam-auth Install steamcmd if needed, log in interactively, write steam deployment steam.json',
77
162
  );
78
163
  }
79
164
 
@@ -86,5 +171,36 @@ export async function adaptfullyFromCli(argv = process.argv) {
86
171
  deployFolder,
87
172
  server,
88
173
  mode,
174
+ deployment,
175
+ artifactPath,
89
176
  });
90
177
  }
178
+
179
+ /**
180
+ * @param {string[]} args
181
+ * @returns {{ positionals: string[], deployment?: string, artifactPath?: string }}
182
+ */
183
+ export function parseStageArgs(args) {
184
+ /** @type {string[]} */
185
+ const positionals = [];
186
+ let deployment;
187
+ let artifactPath;
188
+
189
+ for (let i = 0; i < args.length; i++) {
190
+ const arg = args[i];
191
+
192
+ if (arg === '--deployment' && args[i + 1]) {
193
+ deployment = args[++i];
194
+ } else if (arg.startsWith('--deployment=')) {
195
+ deployment = arg.slice('--deployment='.length);
196
+ } else if (arg === '--artifact' && args[i + 1]) {
197
+ artifactPath = args[++i];
198
+ } else if (arg.startsWith('--artifact=')) {
199
+ artifactPath = arg.slice('--artifact='.length);
200
+ } else {
201
+ positionals.push(arg);
202
+ }
203
+ }
204
+
205
+ return { positionals, deployment, artifactPath };
206
+ }
@@ -1,4 +1,5 @@
1
1
  import fs from 'node:fs';
2
+ import path from 'node:path';
2
3
  import { resolveRuntimeScript } from './paths.js';
3
4
 
4
5
  /** @typedef {Record<string, string>} RegistrationMap */
@@ -299,20 +300,119 @@ export function adaptfullyInjectionForPlatform(platformKey, pkg, options = {}) {
299
300
  }
300
301
 
301
302
  /**
303
+ * Resolve the deployment target keys for a platform. A platform may declare a
304
+ * `deployments` array (each entry maps to assets/meta/deployments/<key>/), which
305
+ * lets one build fan out to multiple targets of the same platform type. When no
306
+ * `deployments` array is present, the default is `["zip"]` (artifact download only).
307
+ *
302
308
  * @param {string} platformKey
303
- * @param {{ config?: { platforms?: Record<string, { builder?: string }> } }} pkg
309
+ * @param {{ config?: { platforms?: Record<string, { deployments?: string[] }> } }} pkg
310
+ * @returns {string[]}
304
311
  */
305
- export function resolveBuilderForPlatform(platformKey, pkg) {
306
- const platform = pkg.config?.platforms?.[platformKey];
307
- if (platform?.builder) {
308
- return platform.builder;
312
+ export function resolveDeploymentsForPlatform(platformKey, pkg) {
313
+ const deployments = pkg.config?.platforms?.[platformKey]?.deployments;
314
+
315
+ if (Array.isArray(deployments) && deployments.length > 0) {
316
+ for (const key of deployments) {
317
+ if (typeof key !== 'string' || !key.length) {
318
+ throw new Error(
319
+ `config.platforms.${platformKey}.deployments entries must be non-empty strings`,
320
+ );
321
+ }
322
+ }
323
+ return [...deployments];
324
+ }
325
+
326
+ return ['zip'];
327
+ }
328
+
329
+ /**
330
+ * Absolute-or-cwd-relative path to a deployment's credential/publish folder.
331
+ *
332
+ * @param {string} deploymentKey
333
+ * @param {string} [metaDir='assets/meta']
334
+ * @returns {string}
335
+ */
336
+ export function resolvePublishDir(deploymentKey, metaDir = 'assets/meta') {
337
+ return path.join(metaDir, 'deployments', deploymentKey);
338
+ }
339
+
340
+ const ELECTRON_TARGETS = new Set([
341
+ 'win', 'win-dev', 'mac', 'mac-dev', 'linux', 'linux-dev',
342
+ ]);
343
+ const CORDOVA_TARGETS = new Set([
344
+ 'android', 'android-dev', 'ios', 'ios-dev', 'ios-sim',
345
+ ]);
346
+
347
+ /**
348
+ * @param {string} target
349
+ * @returns {string | null}
350
+ */
351
+ export function resolveFamilyFromTarget(target) {
352
+ if (ELECTRON_TARGETS.has(target)) {
353
+ return 'electron';
354
+ }
355
+ if (CORDOVA_TARGETS.has(target)) {
356
+ return 'cordova';
357
+ }
358
+ if (target === 'webapp' || target === 'web') {
359
+ return 'webapp';
360
+ }
361
+ if (target === 'uwp') {
362
+ return 'uwp';
363
+ }
364
+ return null;
365
+ }
366
+
367
+ /**
368
+ * @param {string[]} targets
369
+ * @param {string} [packager]
370
+ */
371
+ export function resolveFamily(targets, packager) {
372
+ if (packager === 'electron') {
373
+ return 'electron';
374
+ }
375
+ if (packager === 'cordova' || packager === 'capacitor') {
376
+ return 'cordova';
377
+ }
378
+
379
+ const families = [...new Set(targets.map((target) => resolveFamilyFromTarget(target)).filter(Boolean))];
380
+
381
+ if (families.length === 1) {
382
+ return families[0];
309
383
  }
310
384
 
311
- if (platformKey === 'web') {
385
+ if (targets.length === 1 && targets[0] === 'webapp') {
312
386
  return 'webapp';
313
387
  }
314
388
 
315
- return platformKey;
389
+ throw new Error(`Cannot resolve builder family for targets [${targets.join(', ')}]`);
390
+ }
391
+
392
+ /**
393
+ * @param {string} platformKey
394
+ * @param {{ config?: { platforms?: Record<string, { builder?: string | string[], packager?: string, steamworks?: boolean, deployments?: string[] }> } }} pkg
395
+ */
396
+ export function resolveBuildSpec(platformKey, pkg) {
397
+ const platform = pkg.config?.platforms?.[platformKey];
398
+ const builder = platform?.builder ?? (platformKey === 'web' ? 'webapp' : platformKey);
399
+ const targets = Array.isArray(builder) ? [...builder] : [builder];
400
+
401
+ return {
402
+ family: resolveFamily(targets, platform?.packager),
403
+ targets,
404
+ platformKey,
405
+ steamworks: platform?.steamworks === true,
406
+ deployments: resolveDeploymentsForPlatform(platformKey, pkg),
407
+ };
408
+ }
409
+
410
+ /**
411
+ * @param {string} platformKey
412
+ * @param {{ config?: { platforms?: Record<string, { builder?: string | string[] }> } }} pkg
413
+ */
414
+ export function resolveBuilderForPlatform(platformKey, pkg) {
415
+ return resolveBuildSpec(platformKey, pkg).family;
316
416
  }
317
417
 
318
418
  /**
@@ -323,9 +423,11 @@ export function resolveCliPlatformAndBuilder(arg, pkg) {
323
423
  const platforms = pkg.config?.platforms ?? {};
324
424
 
325
425
  if (platforms[arg]) {
426
+ const spec = resolveBuildSpec(arg, pkg);
326
427
  return {
327
428
  platformKey: arg,
328
- builder: resolveBuilderForPlatform(arg, pkg),
429
+ builder: spec.family,
430
+ buildSpec: spec,
329
431
  };
330
432
  }
331
433
 
@@ -26,7 +26,7 @@ const STEAMCMD_INSTALL = {
26
26
  * @param {string} [projectRoot]
27
27
  */
28
28
  export function defaultSteamPublishPath(projectRoot = '.') {
29
- return path.resolve(projectRoot, 'assets', 'meta', 'publish', 'steam.json');
29
+ return path.resolve(projectRoot, 'assets', 'meta', 'deployments', 'steam', 'steam.json');
30
30
  }
31
31
 
32
32
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@makefully/adaptfully",
3
- "version": "3.6.1",
3
+ "version": "3.8.0",
4
4
  "description": "Platform abstraction and Wrapfully deploy client for Makefully games",
5
5
  "type": "module",
6
6
  "main": "./lib/node/index.js",