@makefully/adaptfully 3.11.0 → 3.12.0

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