@makefully/adaptfully 3.6.1 → 3.7.1

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,21 @@
2
2
 
3
3
  All notable changes to this project are documented in this file.
4
4
 
5
+ ## 3.7.0 — 2026-07-09
6
+
7
+ ### Added
8
+
9
+ - **`adaptfully release <platform>`** stage — single Wrapfully call for build + configured deployments.
10
+ - **`resolveBuildSpec()`** — returns `{ family, targets, platformKey, steamworks, deployments }` for multi-target platforms.
11
+ - **`createSourceArchive`**, **`createDeployArchive`**, **`createReleaseArchive`** — split zip profiles for build/deploy/release.
12
+ - **`--artifact <path>`** on deploy for promoting a prior build artifact.
13
+
14
+ ### Changed
15
+
16
+ - **`send()`** targets Wrapfully v2 URLs: `/{family}/build`, `/{family}/release`, `/deploy/{key}`.
17
+ - Default **`deployments`** when omitted is `["zip"]` instead of the platform key.
18
+ - **`builder`** in platform config may be a string or array (e.g. `["win","mac","linux"]`).
19
+
5
20
  ## 3.6.1 — 2026-07-06
6
21
 
7
22
  ### 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 (the platform's default/first deployment only) |
36
+ | `deploy` | Prebuild, zip, POST to Wrapfully once per deployment; platform release when credentials are in the deployment folder |
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
@@ -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
@@ -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
  {
@@ -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
+ }
@@ -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,7 @@
1
- export { createArchive } from './archive.js';
1
+ export { createArchive, createDeployArchive, createReleaseArchive, createSourceArchive } from './archive.js';
2
2
  export { loadProjectConfig, resolveServerUrl } from './config.js';
3
3
  export { send } from './deploy.js';
4
- export { adaptfullyFromCli, runAdaptfullyStage } from './pipeline.js';
4
+ export { adaptfullyFromCli, parseStageArgs, runAdaptfullyStage } from './pipeline.js';
5
5
  export { prebuildPlatform, prebuildOutputDir, resolveHtmlInjections } from './prebuild.js';
6
6
  export { getPackageRoot, getRuntimeDir, resolveRuntimeScript } from './paths.js';
7
7
  export {
@@ -28,9 +28,14 @@ export {
28
28
  collectRegistrationParts,
29
29
  injectAdaptfullyRegistrations,
30
30
  resolveBuilderForPlatform,
31
+ resolveBuildSpec,
31
32
  resolveCliPlatformAndBuilder,
33
+ resolveDeploymentsForPlatform,
34
+ resolveFamily,
35
+ resolveFamilyFromTarget,
32
36
  resolvePlatformKey,
33
37
  resolvePlatformRegistrationsByKey,
38
+ resolvePublishDir,
34
39
  resolveRegistrationAssets,
35
40
  } from './registrations.js';
36
41
  export { printBuildReport } from './report.js';
@@ -1,50 +1,121 @@
1
1
  import { loadProjectConfig, resolveServerUrl } from './config.js';
2
2
  import { send } from './deploy.js';
3
3
  import { prebuildPlatform } from './prebuild.js';
4
- import { resolveBuilderForPlatform } from './registrations.js';
4
+ import {
5
+ resolveBuildSpec,
6
+ resolveDeploymentsForPlatform,
7
+ resolvePublishDir,
8
+ } from './registrations.js';
5
9
  import { steamAuthFromCli } from './steam-auth.js';
6
10
 
7
- /** @typedef {'prebuild' | 'build' | 'deploy'} AdaptfullyStage */
11
+ /** @typedef {'prebuild' | 'build' | 'deploy' | 'release'} AdaptfullyStage */
8
12
 
9
- const VALID_STAGES = new Set(['prebuild', 'build', 'deploy']);
13
+ const VALID_STAGES = new Set(['prebuild', 'build', 'deploy', 'release']);
10
14
 
11
15
  /**
12
16
  * @param {AdaptfullyStage} stage
13
17
  * @param {string} platformKey
14
- * @param {{ pkg: object, deployFolder: string, server?: string, mode?: string, builder?: string, log?: (message: string) => void, outputRoot?: string }} options
18
+ * @param {{ pkg: object, deployFolder: string, server?: string, mode?: string, deployment?: string, artifactPath?: string, log?: (message: string) => void, outputRoot?: string }} options
15
19
  */
16
20
  export async function runAdaptfullyStage(stage, platformKey, options) {
17
21
  if (!VALID_STAGES.has(stage)) {
18
- throw new Error(`Unknown adaptfully stage "${stage}". Expected: prebuild, build, or deploy.`);
22
+ throw new Error(`Unknown adaptfully stage "${stage}". Expected: prebuild, build, deploy, or release.`);
19
23
  }
20
24
 
21
25
  const log = options.log ?? console.log;
22
26
  const { pkg, deployFolder } = options;
27
+ const buildSpec = resolveBuildSpec(platformKey, pkg);
28
+ let deployments = resolveDeploymentsForPlatform(platformKey, pkg);
23
29
 
24
- const prebuiltDir = prebuildPlatform(deployFolder, platformKey, pkg, {
25
- log,
26
- outputRoot: options.outputRoot,
27
- });
30
+ if (options.deployment) {
31
+ if (!deployments.includes(options.deployment)) {
32
+ throw new Error(
33
+ `Deployment "${options.deployment}" is not configured for platform "${platformKey}". `
34
+ + `Available deployments: ${deployments.join(', ')}`,
35
+ );
36
+ }
37
+ deployments = [options.deployment];
38
+ } else if (stage === 'build') {
39
+ deployments = ['zip'];
40
+ } else if (stage === 'deploy') {
41
+ deployments = deployments.filter((key) => key !== 'zip');
42
+ } else if (stage === 'release') {
43
+ deployments = deployments.filter((key) => key !== 'zip');
44
+ }
45
+
46
+ const prebuiltDir = stage === 'deploy' && options.artifactPath
47
+ ? options.artifactPath
48
+ : prebuildPlatform(deployFolder, platformKey, pkg, {
49
+ log,
50
+ outputRoot: options.outputRoot,
51
+ });
28
52
 
29
53
  if (stage === 'prebuild') {
30
- return { prebuiltDir, platformKey };
54
+ return { prebuiltDir, platformKey, buildSpec };
31
55
  }
32
56
 
33
- const builder = options.builder ?? resolveBuilderForPlatform(platformKey, pkg);
34
- log(`adaptfully: ${stage} ${platformKey} via Wrapfully builder "${builder}"`);
57
+ const gameId = `${pkg.name}-${pkg.version}`;
58
+ const contents = JSON.stringify(pkg);
59
+ /** @type {{ deploymentKey: string, stage: string, family: string }[]} */
60
+ const sent = [];
35
61
 
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
- );
62
+ if (stage === 'release') {
63
+ const deploymentDirs = deployments.map((key) => resolvePublishDir(key));
64
+
65
+ log(`adaptfully: ${stage} ${platformKey} → ${buildSpec.family} (${buildSpec.targets.join(', ')})`);
66
+
67
+ await send(
68
+ gameId,
69
+ contents,
70
+ options.server,
71
+ 'release',
72
+ buildSpec.family,
73
+ prebuiltDir,
74
+ pkg,
75
+ options.mode ?? 'extract',
76
+ {
77
+ log,
78
+ platformKey,
79
+ deploymentDirs,
80
+ },
81
+ );
46
82
 
47
- return { prebuiltDir, platformKey, builder };
83
+ sent.push({ deploymentKey: 'release', stage, family: buildSpec.family });
84
+ return { prebuiltDir, platformKey, buildSpec, deployments: sent };
85
+ }
86
+
87
+ for (const deploymentKey of deployments) {
88
+ const publishDir = deploymentKey === 'zip' ? undefined : resolvePublishDir(deploymentKey);
89
+ const wrapStage = stage === 'deploy' ? 'deploy' : 'build';
90
+
91
+ log(
92
+ `adaptfully: ${stage} ${platformKey} → `
93
+ + `${deploymentKey === 'zip' ? 'artifact zip' : `deployment "${deploymentKey}"`} `
94
+ + `via Wrapfully ${buildSpec.family}`,
95
+ );
96
+
97
+ await send(
98
+ gameId,
99
+ contents,
100
+ options.server,
101
+ wrapStage,
102
+ buildSpec.family,
103
+ prebuiltDir,
104
+ pkg,
105
+ options.mode ?? 'extract',
106
+ {
107
+ log,
108
+ publishDir,
109
+ platformKey,
110
+ deploymentKey: wrapStage === 'deploy' ? deploymentKey : undefined,
111
+ artifactPath: options.artifactPath,
112
+ },
113
+ );
114
+
115
+ sent.push({ deploymentKey, stage: wrapStage, family: buildSpec.family });
116
+ }
117
+
118
+ return { prebuiltDir, platformKey, buildSpec, deployments: sent };
48
119
  }
49
120
 
50
121
  /**
@@ -58,22 +129,27 @@ export async function adaptfullyFromCli(argv = process.argv) {
58
129
  }
59
130
 
60
131
  const stage = command;
61
- const platformKey = argv[3];
62
- const cliServer = argv[4];
63
- const mode = argv[5] ?? 'extract';
132
+ const { positionals, deployment, artifactPath } = parseStageArgs(argv.slice(3));
133
+ const platformKey = positionals[0];
134
+ const cliServer = positionals[1];
135
+ const mode = positionals[2] ?? 'extract';
64
136
 
65
137
  if (!stage || !platformKey) {
66
138
  throw new Error(
67
139
  'Usage:\n'
68
- + ' adaptfully <prebuild|build|deploy> <platform> [server] [mode]\n'
140
+ + ' adaptfully <prebuild|build|deploy|release> <platform> [server] [mode] [--deployment <key>] [--artifact <path>]\n'
69
141
  + ' adaptfully steam-auth [--username U] [--password P] [--output path]\n'
70
142
  + '\n'
71
143
  + 'Stages:\n'
72
144
  + ' 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'
145
+ + ' build prebuild + POST /{family}/build (zip artifact only)\n'
146
+ + ' deploy POST /deploy/{key} with prior artifact + deployment credentials\n'
147
+ + ' release prebuild + POST /{family}/release (build + configured deployments)\n'
75
148
  + '\n'
76
- + ' steam-auth Install steamcmd if needed, log in interactively, write assets/meta/publish/steam.json',
149
+ + ' --deployment <key> Target a single named deployment.\n'
150
+ + ' --artifact <path> Prior build artifact directory (deploy stage).\n'
151
+ + '\n'
152
+ + ' steam-auth Install steamcmd if needed, log in interactively, write steam deployment steam.json',
77
153
  );
78
154
  }
79
155
 
@@ -86,5 +162,36 @@ export async function adaptfullyFromCli(argv = process.argv) {
86
162
  deployFolder,
87
163
  server,
88
164
  mode,
165
+ deployment,
166
+ artifactPath,
89
167
  });
90
168
  }
169
+
170
+ /**
171
+ * @param {string[]} args
172
+ * @returns {{ positionals: string[], deployment?: string, artifactPath?: string }}
173
+ */
174
+ export function parseStageArgs(args) {
175
+ /** @type {string[]} */
176
+ const positionals = [];
177
+ let deployment;
178
+ let artifactPath;
179
+
180
+ for (let i = 0; i < args.length; i++) {
181
+ const arg = args[i];
182
+
183
+ if (arg === '--deployment' && args[i + 1]) {
184
+ deployment = args[++i];
185
+ } else if (arg.startsWith('--deployment=')) {
186
+ deployment = arg.slice('--deployment='.length);
187
+ } else if (arg === '--artifact' && args[i + 1]) {
188
+ artifactPath = args[++i];
189
+ } else if (arg.startsWith('--artifact=')) {
190
+ artifactPath = arg.slice('--artifact='.length);
191
+ } else {
192
+ positionals.push(arg);
193
+ }
194
+ }
195
+
196
+ return { positionals, deployment, artifactPath };
197
+ }
@@ -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.7.1",
4
4
  "description": "Platform abstraction and Wrapfully deploy client for Makefully games",
5
5
  "type": "module",
6
6
  "main": "./lib/node/index.js",