@appforge-ci/admin-cli 0.1.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/LICENSE +21 -0
- package/README.md +70 -0
- package/dist/commands/agents.d.ts +55 -0
- package/dist/commands/agents.d.ts.map +1 -0
- package/dist/commands/agents.js +436 -0
- package/dist/commands/agents.js.map +1 -0
- package/dist/commands/agents.test.d.ts +2 -0
- package/dist/commands/agents.test.d.ts.map +1 -0
- package/dist/commands/agents.test.js +127 -0
- package/dist/commands/agents.test.js.map +1 -0
- package/dist/commands/login.d.ts +38 -0
- package/dist/commands/login.d.ts.map +1 -0
- package/dist/commands/login.js +128 -0
- package/dist/commands/login.js.map +1 -0
- package/dist/commands/login.test.d.ts +2 -0
- package/dist/commands/login.test.d.ts.map +1 -0
- package/dist/commands/login.test.js +69 -0
- package/dist/commands/login.test.js.map +1 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +34 -0
- package/dist/index.js.map +1 -0
- package/dist/lib/bootstrap.d.ts +21 -0
- package/dist/lib/bootstrap.d.ts.map +1 -0
- package/dist/lib/bootstrap.js +242 -0
- package/dist/lib/bootstrap.js.map +1 -0
- package/dist/lib/bootstrap.test.d.ts +2 -0
- package/dist/lib/bootstrap.test.d.ts.map +1 -0
- package/dist/lib/bootstrap.test.js +59 -0
- package/dist/lib/bootstrap.test.js.map +1 -0
- package/dist/lib/config.d.ts +27 -0
- package/dist/lib/config.d.ts.map +1 -0
- package/dist/lib/config.js +61 -0
- package/dist/lib/config.js.map +1 -0
- package/dist/lib/config.test.d.ts +2 -0
- package/dist/lib/config.test.d.ts.map +1 -0
- package/dist/lib/config.test.js +120 -0
- package/dist/lib/config.test.js.map +1 -0
- package/dist/lib/prompt.d.ts +11 -0
- package/dist/lib/prompt.d.ts.map +1 -0
- package/dist/lib/prompt.js +54 -0
- package/dist/lib/prompt.js.map +1 -0
- package/dist/lib/prompt.test.d.ts +2 -0
- package/dist/lib/prompt.test.d.ts.map +1 -0
- package/dist/lib/prompt.test.js +22 -0
- package/dist/lib/prompt.test.js.map +1 -0
- package/dist/lib/two-factor.d.ts +21 -0
- package/dist/lib/two-factor.d.ts.map +1 -0
- package/dist/lib/two-factor.js +31 -0
- package/dist/lib/two-factor.js.map +1 -0
- package/dist/lib/two-factor.test.d.ts +2 -0
- package/dist/lib/two-factor.test.d.ts.map +1 -0
- package/dist/lib/two-factor.test.js +51 -0
- package/dist/lib/two-factor.test.js.map +1 -0
- package/dist/test-support/home-sandbox.d.ts +3 -0
- package/dist/test-support/home-sandbox.d.ts.map +1 -0
- package/dist/test-support/home-sandbox.js +47 -0
- package/dist/test-support/home-sandbox.js.map +1 -0
- package/package.json +33 -0
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
import { execFileSync } from "node:child_process";
|
|
2
|
+
import { appendFileSync, existsSync, readFileSync } from "node:fs";
|
|
3
|
+
import { createRequire } from "node:module";
|
|
4
|
+
import { homedir } from "node:os";
|
|
5
|
+
import { join } from "node:path";
|
|
6
|
+
import { installAgentLaunchAgent, stableNodePath } from "@appforge-ci/agent/dist/lib/launchd.js";
|
|
7
|
+
import { loadConfig } from "@appforge-ci/agent/dist/lib/config.js";
|
|
8
|
+
import { AgentApi } from "@appforge-ci/agent/dist/lib/api.js";
|
|
9
|
+
import { installDeviceStreamIfPresent, syncDeviceStreamFleetSecrets } from "@appforge-ci/agent/dist/lib/device-stream-install.js";
|
|
10
|
+
const require = createRequire(import.meta.url);
|
|
11
|
+
/**
|
|
12
|
+
* Automates the toolchain-install half of infra/mac-mini-setup/bootstrap.sh
|
|
13
|
+
* from inside the CLI, split into the same two tiers we drew by hand while
|
|
14
|
+
* enrolling a personal laptop as a runner (see #17):
|
|
15
|
+
*
|
|
16
|
+
* - `bootstrapSafeToolchains` (default, always run): Homebrew installs and
|
|
17
|
+
* PATH wiring only. No `sudo`, no system-setting changes — safe on a
|
|
18
|
+
* machine you actually use day to day, not just a dedicated Mac mini.
|
|
19
|
+
* - `bootstrapPrivileged` (opt-in via `agents bootstrap --full`): the
|
|
20
|
+
* parts of bootstrap.sh that touch system/security settings (a
|
|
21
|
+
* passwordless-sudo sudoers rule for remote reboot, disabling sleep,
|
|
22
|
+
* reinstalling Xcode). Appropriate for a dedicated always-on mini, not
|
|
23
|
+
* a laptop — never runs unless explicitly asked for.
|
|
24
|
+
*
|
|
25
|
+
* Reuses bootstrap.sh's actual commands (brew formula/cask names, the
|
|
26
|
+
* sdkmanager package list, the sudoers line, the pmset flags) rather than
|
|
27
|
+
* re-deriving them, so the two don't drift apart — see that script for the
|
|
28
|
+
* canonical version if you're changing either.
|
|
29
|
+
*/
|
|
30
|
+
function run(cmd, args) {
|
|
31
|
+
console.log(` $ ${cmd} ${args.join(" ")}`);
|
|
32
|
+
execFileSync(cmd, args, { stdio: "inherit" });
|
|
33
|
+
}
|
|
34
|
+
function tryRun(cmd, args) {
|
|
35
|
+
try {
|
|
36
|
+
execFileSync(cmd, args, { stdio: ["ignore", "pipe", "pipe"] });
|
|
37
|
+
return true;
|
|
38
|
+
}
|
|
39
|
+
catch {
|
|
40
|
+
return false;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
function brewFormulaInstalled(name) {
|
|
44
|
+
return tryRun("brew", ["list", "--formula", name]);
|
|
45
|
+
}
|
|
46
|
+
function brewCaskInstalled(name) {
|
|
47
|
+
return tryRun("brew", ["list", "--cask", name]);
|
|
48
|
+
}
|
|
49
|
+
const PATH_BLOCK_START = "# >>> appforge-agent toolchains (added by `appforge-admin agents bootstrap`) >>>";
|
|
50
|
+
const PATH_BLOCK_END = "# <<< appforge-agent toolchains <<<";
|
|
51
|
+
/**
|
|
52
|
+
* Appends a PATH block once, idempotently — re-running `agents bootstrap`
|
|
53
|
+
* (e.g. after installing a toolchain that was missing the first time)
|
|
54
|
+
* shouldn't pile up duplicate exports on every run.
|
|
55
|
+
*
|
|
56
|
+
* Exported for bootstrap.test.ts — every other caller in this file only
|
|
57
|
+
* ever uses it as an internal helper, but its idempotency and
|
|
58
|
+
* existing-content-preservation are worth testing directly rather than
|
|
59
|
+
* only indirectly through bootstrapSafeToolchains(), which can't be
|
|
60
|
+
* exercised at all in a headless environment (it shells out to a real
|
|
61
|
+
* `brew`/`sdkmanager`/Homebrew toolchain install).
|
|
62
|
+
*/
|
|
63
|
+
export function ensurePathBlock(profilePath, lines) {
|
|
64
|
+
const existing = existsSync(profilePath) ? readFileSync(profilePath, "utf8") : "";
|
|
65
|
+
if (existing.includes(PATH_BLOCK_START)) {
|
|
66
|
+
console.log(` ${profilePath} already has the toolchain PATH block, leaving it alone`);
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
const block = `\n${PATH_BLOCK_START}\n${lines.join("\n")}\n${PATH_BLOCK_END}\n`;
|
|
70
|
+
appendFileSync(profilePath, block);
|
|
71
|
+
console.log(` wrote toolchain PATH block to ${profilePath}`);
|
|
72
|
+
}
|
|
73
|
+
async function reloadAgentIfEnrolled() {
|
|
74
|
+
const plistPath = join(homedir(), "Library", "LaunchAgents", "com.appforge.agent.plist");
|
|
75
|
+
if (!existsSync(plistPath))
|
|
76
|
+
return; // not enrolled yet — add-mac will pick up fresh toolchains on first run
|
|
77
|
+
console.log("\n==> Reloading the already-enrolled agent so it re-detects toolchains immediately");
|
|
78
|
+
// Re-runs installAgentLaunchAgent (unload+rewrite+load), not a bare
|
|
79
|
+
// unload/load of the existing plist verbatim — a mini enrolled before
|
|
80
|
+
// #14 published the CLIs to npm has a plist written by the old,
|
|
81
|
+
// now-deleted install.sh pointing at a copied
|
|
82
|
+
// /usr/local/lib/appforge-agent/dist/index.js. A bare reload would just
|
|
83
|
+
// keep launching that stale copy forever; rewriting the plist points it
|
|
84
|
+
// at wherever @appforge-ci/agent actually resolves from *now* (this
|
|
85
|
+
// package's own dependency, whether that's an in-repo checkout or a
|
|
86
|
+
// global npm install), the same as `add-mac` already does.
|
|
87
|
+
const agentDistIndexPath = require.resolve("@appforge-ci/agent/dist/index.js");
|
|
88
|
+
// Same fix as add-mac's own installAgentLaunchAgent call (see agents.ts)
|
|
89
|
+
// — resolve the stable unversioned node symlink before baking it into
|
|
90
|
+
// the plist, so a later `brew upgrade node` doesn't strand launchd
|
|
91
|
+
// pointed at a deleted version-pinned Cellar path.
|
|
92
|
+
const nodePath = stableNodePath(process.execPath);
|
|
93
|
+
installAgentLaunchAgent(nodePath, agentDistIndexPath);
|
|
94
|
+
// Same device-stream install-service fold `add-mac` gets (see
|
|
95
|
+
// agents.ts) — a mini bootstrapped, then reloaded, through this path
|
|
96
|
+
// should end up with Screen Sharing/"Test in browser" wired up too,
|
|
97
|
+
// not just through add-mac. Loads this machine's own already-enrolled
|
|
98
|
+
// agent config (the plist-exists check above already established this
|
|
99
|
+
// machine is enrolled) purely to get the agentId/agentToken this
|
|
100
|
+
// needs; any failure here (a corrupt/missing config despite the plist
|
|
101
|
+
// existing, a network hiccup) is swallowed — reloading the agent's own
|
|
102
|
+
// service above is what this function must always succeed at, never
|
|
103
|
+
// device-stream's setup on top of it.
|
|
104
|
+
try {
|
|
105
|
+
const config = loadConfig();
|
|
106
|
+
const api = new AgentApi(config);
|
|
107
|
+
let outcome;
|
|
108
|
+
try {
|
|
109
|
+
({ outcome } = await syncDeviceStreamFleetSecrets({
|
|
110
|
+
api,
|
|
111
|
+
agentDistIndexPath,
|
|
112
|
+
nodePath,
|
|
113
|
+
apiUrl: config.apiUrl,
|
|
114
|
+
env: process.env,
|
|
115
|
+
agentId: config.agentId,
|
|
116
|
+
}));
|
|
117
|
+
}
|
|
118
|
+
catch (err) {
|
|
119
|
+
console.warn(`Couldn't fetch this org's fleet secrets (${err.message}) — ` +
|
|
120
|
+
`falling back to whatever is set in this shell's environment.`);
|
|
121
|
+
outcome = await installDeviceStreamIfPresent(agentDistIndexPath, nodePath, config.apiUrl, process.env, {}, undefined, undefined, config.agentId);
|
|
122
|
+
}
|
|
123
|
+
if (outcome.message)
|
|
124
|
+
console.log(outcome.message);
|
|
125
|
+
}
|
|
126
|
+
catch (err) {
|
|
127
|
+
console.warn(`Skipping apps/device-stream's install-service reload: ${err.message}`);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
export async function bootstrapSafeToolchains() {
|
|
131
|
+
if (!tryRun("brew", ["--version"])) {
|
|
132
|
+
console.error("Homebrew isn't installed. Install it first (https://brew.sh), then re-run this command.");
|
|
133
|
+
process.exitCode = 1;
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
136
|
+
console.log("==> iOS toolchain: CocoaPods");
|
|
137
|
+
if (brewFormulaInstalled("cocoapods")) {
|
|
138
|
+
console.log(" already installed");
|
|
139
|
+
}
|
|
140
|
+
else {
|
|
141
|
+
run("brew", ["install", "cocoapods"]);
|
|
142
|
+
}
|
|
143
|
+
console.log("\n==> Android toolchain: Java (openjdk)");
|
|
144
|
+
if (!brewFormulaInstalled("openjdk")) {
|
|
145
|
+
run("brew", ["install", "openjdk"]);
|
|
146
|
+
}
|
|
147
|
+
else {
|
|
148
|
+
console.log(" already installed");
|
|
149
|
+
}
|
|
150
|
+
console.log("\n==> Android toolchain: SDK command-line tools");
|
|
151
|
+
// Prefer an SDK that's already on disk (e.g. from Android Studio) over
|
|
152
|
+
// installing a second one via the Homebrew cask — either way, what
|
|
153
|
+
// actually matters for the agent is that ANDROID_HOME/PATH point at
|
|
154
|
+
// platform-tools (adb) somewhere real.
|
|
155
|
+
const existingSdk = join(homedir(), "Library", "Android", "sdk");
|
|
156
|
+
const brewPrefix = execFileSync("brew", ["--prefix"], { encoding: "utf8" }).trim(); // /opt/homebrew on Apple Silicon, /usr/local on Intel
|
|
157
|
+
let androidHome;
|
|
158
|
+
if (existsSync(join(existingSdk, "platform-tools", "adb"))) {
|
|
159
|
+
console.log(` found an existing SDK at ${existingSdk}, using it`);
|
|
160
|
+
androidHome = existingSdk;
|
|
161
|
+
}
|
|
162
|
+
else if (brewCaskInstalled("android-commandlinetools")) {
|
|
163
|
+
androidHome = `${brewPrefix}/share/android-commandlinetools`;
|
|
164
|
+
console.log(` already installed via Homebrew cask (${androidHome})`);
|
|
165
|
+
}
|
|
166
|
+
else {
|
|
167
|
+
run("brew", ["install", "--cask", "android-commandlinetools"]);
|
|
168
|
+
androidHome = `${brewPrefix}/share/android-commandlinetools`;
|
|
169
|
+
// sdkmanager is itself a Java program — needs `java` on PATH to run at
|
|
170
|
+
// all, which openjdk being keg-only means isn't there by default (the
|
|
171
|
+
// exact bug this function's whole PATH-wiring step exists to fix).
|
|
172
|
+
const openjdkBin = execFileSync("brew", ["--prefix", "openjdk"], { encoding: "utf8" }).trim() + "/bin";
|
|
173
|
+
const sdkEnv = { ...process.env, ANDROID_HOME: androidHome, PATH: `${openjdkBin}:${process.env.PATH ?? ""}` };
|
|
174
|
+
const sdkmanager = `${androidHome}/cmdline-tools/latest/bin/sdkmanager`;
|
|
175
|
+
try {
|
|
176
|
+
execFileSync("bash", ["-c", `yes | ${sdkmanager} --licenses`], { stdio: "ignore", env: sdkEnv });
|
|
177
|
+
console.log(` $ ${sdkmanager} platform-tools platforms;android-34 build-tools;34.0.0`);
|
|
178
|
+
execFileSync(sdkmanager, ["platform-tools", "platforms;android-34", "build-tools;34.0.0"], { stdio: "inherit", env: sdkEnv });
|
|
179
|
+
}
|
|
180
|
+
catch {
|
|
181
|
+
console.warn(" sdkmanager package install failed — you may need to accept licenses manually (sdkmanager --licenses)");
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
console.log("\n==> Wiring Java + Android SDK onto the login-shell PATH the agent's toolchain checks use");
|
|
185
|
+
// openjdk is keg-only (Homebrew deliberately doesn't symlink it, to avoid
|
|
186
|
+
// fighting the system `java` wrapper) so it always needs an explicit
|
|
187
|
+
// PATH entry — see packages/agent/src/lib/system-info.ts's tryRunLogin
|
|
188
|
+
// for why this has to land in a *login* shell's profile specifically.
|
|
189
|
+
const pathLines = [
|
|
190
|
+
`export ANDROID_HOME="${androidHome}"`,
|
|
191
|
+
`export PATH="$(brew --prefix openjdk)/bin:$ANDROID_HOME/platform-tools:$ANDROID_HOME/cmdline-tools/latest/bin:$PATH"`,
|
|
192
|
+
// pod --version's first line is a "requires UTF-8 encoding" warning
|
|
193
|
+
// under the "C"/POSIX locale, which packages/agent/src/lib/system-info.ts's
|
|
194
|
+
// firstLine() would capture as the "version" instead of the real one.
|
|
195
|
+
`export LANG=en_US.UTF-8`,
|
|
196
|
+
];
|
|
197
|
+
ensurePathBlock(join(homedir(), ".bash_profile"), pathLines); // read by the agent's `bash -lc` checks
|
|
198
|
+
ensurePathBlock(join(homedir(), ".zshrc"), pathLines); // read by your interactive shell
|
|
199
|
+
await reloadAgentIfEnrolled();
|
|
200
|
+
console.log("\nDone. Run 'appforge-admin agents add-mac' (or check the Fleet dashboard, or 'agents list') to confirm toolchains are now detected.");
|
|
201
|
+
}
|
|
202
|
+
/**
|
|
203
|
+
* The system/security-sensitive half of bootstrap.sh — never run unless
|
|
204
|
+
* explicitly requested via `--full`. Appropriate for a dedicated always-on
|
|
205
|
+
* Mac mini; not something to do to a machine you use day to day.
|
|
206
|
+
*/
|
|
207
|
+
export function bootstrapPrivileged() {
|
|
208
|
+
console.log("\n==> Passwordless sudo for the REBOOT_HOST remote command (scoped, not blanket sudo)");
|
|
209
|
+
const agentUser = execFileSync("whoami", [], { encoding: "utf8" }).trim();
|
|
210
|
+
const sudoersLine = `${agentUser} ALL=(root) NOPASSWD: /sbin/shutdown -r now`;
|
|
211
|
+
const sudoersFile = "/etc/sudoers.d/appforge-agent";
|
|
212
|
+
if (existsSync(sudoersFile)) {
|
|
213
|
+
console.log(` ${sudoersFile} already exists, leaving it alone`);
|
|
214
|
+
}
|
|
215
|
+
else {
|
|
216
|
+
run("bash", ["-c", `echo '${sudoersLine}' | sudo tee ${sudoersFile} >/dev/null && sudo chmod 440 ${sudoersFile}`]);
|
|
217
|
+
console.log(` wrote ${sudoersFile}`);
|
|
218
|
+
}
|
|
219
|
+
console.log("\n==> Disabling sleep so this machine keeps building overnight");
|
|
220
|
+
run("sudo", ["pmset", "-a", "sleep", "0", "disksleep", "0", "displaysleep", "10"]);
|
|
221
|
+
console.log("\n==> Full Xcode (Command Line Tools alone can't run xcodebuild)");
|
|
222
|
+
let hasFullXcode = false;
|
|
223
|
+
try {
|
|
224
|
+
hasFullXcode = execFileSync("xcode-select", ["-p"], { encoding: "utf8" }).includes("/Xcode.app");
|
|
225
|
+
}
|
|
226
|
+
catch {
|
|
227
|
+
/* xcode-select -p failing just means CLT/Xcode isn't selected yet */
|
|
228
|
+
}
|
|
229
|
+
if (hasFullXcode) {
|
|
230
|
+
console.log(" already installed");
|
|
231
|
+
}
|
|
232
|
+
else if (process.env.XCODES_USERNAME && process.env.XCODES_PASSWORD) {
|
|
233
|
+
if (!brewFormulaInstalled("xcodes"))
|
|
234
|
+
run("brew", ["install", "xcodes", "aria2"]);
|
|
235
|
+
run("xcodes", ["install", "--latest", "--experimental-unxip", "--select"]);
|
|
236
|
+
run("sudo", ["xcodebuild", "-license", "accept"]);
|
|
237
|
+
}
|
|
238
|
+
else {
|
|
239
|
+
console.log(" XCODES_USERNAME/XCODES_PASSWORD not set — skipping. Export those (an Apple ID with App Store access) and re-run with --full, or install by hand: 'xcodes install --latest --select'.");
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
//# sourceMappingURL=bootstrap.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"bootstrap.js","sourceRoot":"","sources":["../../src/lib/bootstrap.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"bootstrap.test.d.ts","sourceRoot":"","sources":["../../src/lib/bootstrap.test.ts"],"names":[],"mappings":"AACA,OAAO,iCAAiC,CAAC"}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
// Must stay the FIRST import — see the comment below the imports.
|
|
2
|
+
import "../test-support/home-sandbox.js";
|
|
3
|
+
import { test } from "node:test";
|
|
4
|
+
import assert from "node:assert/strict";
|
|
5
|
+
import { mkdtemp, rm, readFile, writeFile } from "node:fs/promises";
|
|
6
|
+
import { tmpdir } from "node:os";
|
|
7
|
+
import { join } from "node:path";
|
|
8
|
+
import { ensurePathBlock } from "./bootstrap.js";
|
|
9
|
+
// The rest of bootstrap.ts (bootstrapSafeToolchains/bootstrapPrivileged)
|
|
10
|
+
// shells out to a real brew/sdkmanager/Homebrew toolchain install and
|
|
11
|
+
// can't be exercised at all in this headless environment — see
|
|
12
|
+
// android-smoke-test.test.ts's own docblock for the same "no real
|
|
13
|
+
// toolchain here" boundary. ensurePathBlock is the one piece of pure,
|
|
14
|
+
// parameterized filesystem logic in this file, so it's the one thing
|
|
15
|
+
// tested directly.
|
|
16
|
+
test("ensurePathBlock creates the file with the marked block when none exists", async () => {
|
|
17
|
+
const dir = await mkdtemp(join(tmpdir(), "appforge-bootstrap-test-"));
|
|
18
|
+
try {
|
|
19
|
+
const profilePath = join(dir, ".bash_profile");
|
|
20
|
+
ensurePathBlock(profilePath, ['export ANDROID_HOME="/fake/sdk"']);
|
|
21
|
+
const content = await readFile(profilePath, "utf8");
|
|
22
|
+
assert.match(content, /appforge-agent toolchains \(added by `appforge-admin agents bootstrap`\)/);
|
|
23
|
+
assert.match(content, /export ANDROID_HOME="\/fake\/sdk"/);
|
|
24
|
+
assert.match(content, /<<< appforge-agent toolchains <<</);
|
|
25
|
+
}
|
|
26
|
+
finally {
|
|
27
|
+
await rm(dir, { recursive: true, force: true });
|
|
28
|
+
}
|
|
29
|
+
});
|
|
30
|
+
test("ensurePathBlock preserves whatever was already in the file, appending after it", async () => {
|
|
31
|
+
const dir = await mkdtemp(join(tmpdir(), "appforge-bootstrap-test-"));
|
|
32
|
+
try {
|
|
33
|
+
const profilePath = join(dir, ".zshrc");
|
|
34
|
+
await writeFile(profilePath, "# a real user's own existing config\nexport EDITOR=vim\n");
|
|
35
|
+
ensurePathBlock(profilePath, ['export PATH="/fake/bin:$PATH"']);
|
|
36
|
+
const content = await readFile(profilePath, "utf8");
|
|
37
|
+
assert.match(content, /^# a real user's own existing config\nexport EDITOR=vim\n/);
|
|
38
|
+
assert.match(content, /export PATH="\/fake\/bin:\$PATH"/);
|
|
39
|
+
}
|
|
40
|
+
finally {
|
|
41
|
+
await rm(dir, { recursive: true, force: true });
|
|
42
|
+
}
|
|
43
|
+
});
|
|
44
|
+
test("ensurePathBlock is idempotent — re-running it never duplicates the block", async () => {
|
|
45
|
+
const dir = await mkdtemp(join(tmpdir(), "appforge-bootstrap-test-"));
|
|
46
|
+
try {
|
|
47
|
+
const profilePath = join(dir, ".bash_profile");
|
|
48
|
+
ensurePathBlock(profilePath, ['export ANDROID_HOME="/fake/sdk"']);
|
|
49
|
+
ensurePathBlock(profilePath, ['export ANDROID_HOME="/fake/sdk"']);
|
|
50
|
+
ensurePathBlock(profilePath, ['export ANDROID_HOME="/fake/sdk"']);
|
|
51
|
+
const content = await readFile(profilePath, "utf8");
|
|
52
|
+
const occurrences = content.split("appforge-agent toolchains (added by").length - 1;
|
|
53
|
+
assert.equal(occurrences, 1, `expected exactly one PATH block, found ${occurrences}`);
|
|
54
|
+
}
|
|
55
|
+
finally {
|
|
56
|
+
await rm(dir, { recursive: true, force: true });
|
|
57
|
+
}
|
|
58
|
+
});
|
|
59
|
+
//# sourceMappingURL=bootstrap.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"bootstrap.test.js","sourceRoot":"","sources":["../../src/lib/bootstrap.test.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
export interface AdminCliConfig {
|
|
2
|
+
apiUrl: string;
|
|
3
|
+
adminApiUrl: string;
|
|
4
|
+
token: string;
|
|
5
|
+
orgId: string;
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* The "file missing" case already threw a clear message, but a
|
|
9
|
+
* present-but-malformed config.json (a hand-edit typo, disk-full/
|
|
10
|
+
* killed-mid-write, a cloud-synced homedir's conflicted copy) used to
|
|
11
|
+
* throw a raw JSON.parse SyntaxError instead — every CLI command loads
|
|
12
|
+
* this file, so a corrupted one made the CLI permanently unusable with
|
|
13
|
+
* only that unhelpful message to go on (index.ts's top-level .catch()
|
|
14
|
+
* just prints err.message and exits). Same "catch and re-throw with a
|
|
15
|
+
* clear message" shape packages/agent's own loadConfig() already
|
|
16
|
+
* established for the identical bug there.
|
|
17
|
+
*/
|
|
18
|
+
export declare function loadAdminCliConfig(): AdminCliConfig;
|
|
19
|
+
/**
|
|
20
|
+
* Atomic write (temp file + rename) — same pattern packages/agent's own
|
|
21
|
+
* saveConfig() already establishes there for the identical config-file
|
|
22
|
+
* contract, since a process killed mid-write is the most plausible real
|
|
23
|
+
* cause of the corrupted state loadAdminCliConfig() now has to survive.
|
|
24
|
+
*/
|
|
25
|
+
export declare function saveAdminCliConfig(config: AdminCliConfig): void;
|
|
26
|
+
export declare function adminCliConfigPath(): string;
|
|
27
|
+
//# sourceMappingURL=config.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../../src/lib/config.ts"],"names":[],"mappings":"AAWA,MAAM,WAAW,cAAc;IAC7B,MAAM,EAAE,MAAM,CAAC;IACf,WAAW,EAAE,MAAM,CAAC;IACpB,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,MAAM,CAAC;CACf;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,kBAAkB,IAAI,cAAc,CAYnD;AAED;;;;;GAKG;AACH,wBAAgB,kBAAkB,CAAC,MAAM,EAAE,cAAc,QAkBxD;AAED,wBAAgB,kBAAkB,WAEjC"}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { readFileSync, writeFileSync, existsSync, mkdirSync, chmodSync, renameSync } from "node:fs";
|
|
2
|
+
import { homedir } from "node:os";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
// Deliberately a separate config directory from @appforge-ci/cli's
|
|
5
|
+
// ~/.appforge/ — these are two different tools for two different
|
|
6
|
+
// audiences (see this package's README), so being logged into one
|
|
7
|
+
// never implies anything about the other.
|
|
8
|
+
const CONFIG_DIR = join(homedir(), ".appforge-admin");
|
|
9
|
+
const CONFIG_PATH = join(CONFIG_DIR, "config.json");
|
|
10
|
+
/**
|
|
11
|
+
* The "file missing" case already threw a clear message, but a
|
|
12
|
+
* present-but-malformed config.json (a hand-edit typo, disk-full/
|
|
13
|
+
* killed-mid-write, a cloud-synced homedir's conflicted copy) used to
|
|
14
|
+
* throw a raw JSON.parse SyntaxError instead — every CLI command loads
|
|
15
|
+
* this file, so a corrupted one made the CLI permanently unusable with
|
|
16
|
+
* only that unhelpful message to go on (index.ts's top-level .catch()
|
|
17
|
+
* just prints err.message and exits). Same "catch and re-throw with a
|
|
18
|
+
* clear message" shape packages/agent's own loadConfig() already
|
|
19
|
+
* established for the identical bug there.
|
|
20
|
+
*/
|
|
21
|
+
export function loadAdminCliConfig() {
|
|
22
|
+
if (!existsSync(CONFIG_PATH)) {
|
|
23
|
+
throw new Error(`Not logged in. Run "appforge-admin login" first (looked for ${CONFIG_PATH}).`);
|
|
24
|
+
}
|
|
25
|
+
try {
|
|
26
|
+
return JSON.parse(readFileSync(CONFIG_PATH, "utf8"));
|
|
27
|
+
}
|
|
28
|
+
catch (err) {
|
|
29
|
+
throw new Error(`Config at ${CONFIG_PATH} is corrupted (${err.message}). ` +
|
|
30
|
+
`Re-run "appforge-admin login" to regenerate it.`);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Atomic write (temp file + rename) — same pattern packages/agent's own
|
|
35
|
+
* saveConfig() already establishes there for the identical config-file
|
|
36
|
+
* contract, since a process killed mid-write is the most plausible real
|
|
37
|
+
* cause of the corrupted state loadAdminCliConfig() now has to survive.
|
|
38
|
+
*/
|
|
39
|
+
export function saveAdminCliConfig(config) {
|
|
40
|
+
mkdirSync(CONFIG_DIR, { recursive: true });
|
|
41
|
+
const tmpPath = `${CONFIG_PATH}.${process.pid}.tmp`;
|
|
42
|
+
writeFileSync(tmpPath, JSON.stringify(config, null, 2), { mode: 0o600 });
|
|
43
|
+
renameSync(tmpPath, CONFIG_PATH);
|
|
44
|
+
// Belt-and-suspenders, not a fix for a live gap — same correction as
|
|
45
|
+
// @appforge-ci/cli's own config.ts: renameSync() replaces CONFIG_PATH's
|
|
46
|
+
// directory entry with tmpPath's inode wholesale, so the destination
|
|
47
|
+
// already ends up at tmpPath's own mode — 0o600, set by the
|
|
48
|
+
// writeFileSync above, which is *always* creating a brand-new file
|
|
49
|
+
// (tmpPath is unique per process) — regardless of whatever permissions
|
|
50
|
+
// CONFIG_PATH itself had before (verified directly: this repo's own
|
|
51
|
+
// config.test.ts still passes its "re-tightens" assertion with this
|
|
52
|
+
// chmodSync call deleted entirely). Kept anyway as cheap insurance
|
|
53
|
+
// against a *future* edit to saveAdminCliConfig that moves away from
|
|
54
|
+
// the atomic temp-file+rename pattern (e.g. writing in place to an
|
|
55
|
+
// existing CONFIG_PATH) without revisiting this line.
|
|
56
|
+
chmodSync(CONFIG_PATH, 0o600);
|
|
57
|
+
}
|
|
58
|
+
export function adminCliConfigPath() {
|
|
59
|
+
return CONFIG_PATH;
|
|
60
|
+
}
|
|
61
|
+
//# sourceMappingURL=config.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"config.js","sourceRoot":"","sources":["../../src/lib/config.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,aAAa,EAAE,UAAU,EAAE,SAAS,EAAE,SAAS,EAAE,UAAU,EAAE,MAAM,SAAS,CAAC;AACpG,OAAO,EAAE,OAAO,EAAE,MAAM,SAAS,CAAC;AAClC,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AAEjC,mEAAmE;AACnE,iEAAiE;AACjE,kEAAkE;AAClE,0CAA0C;AAC1C,MAAM,UAAU,GAAG,IAAI,CAAC,OAAO,EAAE,EAAE,iBAAiB,CAAC,CAAC;AACtD,MAAM,WAAW,GAAG,IAAI,CAAC,UAAU,EAAE,aAAa,CAAC,CAAC;AASpD;;;;;;;;;;GAUG;AACH,MAAM,UAAU,kBAAkB;IAChC,IAAI,CAAC,UAAU,CAAC,WAAW,CAAC,EAAE,CAAC;QAC7B,MAAM,IAAI,KAAK,CAAC,+DAA+D,WAAW,IAAI,CAAC,CAAC;IAClG,CAAC;IACD,IAAI,CAAC;QACH,OAAO,IAAI,CAAC,KAAK,CAAC,YAAY,CAAC,WAAW,EAAE,MAAM,CAAC,CAAC,CAAC;IACvD,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACb,MAAM,IAAI,KAAK,CACb,aAAa,WAAW,kBAAmB,GAAa,CAAC,OAAO,KAAK;YACnE,iDAAiD,CACpD,CAAC;IACJ,CAAC;AACH,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,kBAAkB,CAAC,MAAsB;IACvD,SAAS,CAAC,UAAU,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;IAC3C,MAAM,OAAO,GAAG,GAAG,WAAW,IAAI,OAAO,CAAC,GAAG,MAAM,CAAC;IACpD,aAAa,CAAC,OAAO,EAAE,IAAI,CAAC,SAAS,CAAC,MAAM,EAAE,IAAI,EAAE,CAAC,CAAC,EAAE,EAAE,IAAI,EAAE,KAAK,EAAE,CAAC,CAAC;IACzE,UAAU,CAAC,OAAO,EAAE,WAAW,CAAC,CAAC;IACjC,qEAAqE;IACrE,wEAAwE;IACxE,qEAAqE;IACrE,4DAA4D;IAC5D,mEAAmE;IACnE,uEAAuE;IACvE,oEAAoE;IACpE,oEAAoE;IACpE,mEAAmE;IACnE,qEAAqE;IACrE,mEAAmE;IACnE,sDAAsD;IACtD,SAAS,CAAC,WAAW,EAAE,KAAK,CAAC,CAAC;AAChC,CAAC;AAED,MAAM,UAAU,kBAAkB;IAChC,OAAO,WAAW,CAAC;AACrB,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"config.test.d.ts","sourceRoot":"","sources":["../../src/lib/config.test.ts"],"names":[],"mappings":"AACA,OAAO,iCAAiC,CAAC"}
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
// Must stay the FIRST import — see the comment below the imports.
|
|
2
|
+
import "../test-support/home-sandbox.js";
|
|
3
|
+
import { test } from "node:test";
|
|
4
|
+
import assert from "node:assert/strict";
|
|
5
|
+
import { rmSync, writeFileSync, statSync, mkdirSync } from "node:fs";
|
|
6
|
+
import { dirname } from "node:path";
|
|
7
|
+
import { loadAdminCliConfig, saveAdminCliConfig, adminCliConfigPath } from "./config.js";
|
|
8
|
+
// adminCliConfigPath() is derived from os.homedir() when ./config.js is
|
|
9
|
+
// first evaluated, and these tests write to and rmSync() it as their
|
|
10
|
+
// fixture — so the sandbox-home guard above MUST stay the first import:
|
|
11
|
+
// it points $HOME at a throwaway temp dir before that constant is
|
|
12
|
+
// computed. (Until 2026-09-13 this file deliberately ran against the
|
|
13
|
+
// real ~/.appforge-admin/config.json — every `pnpm test` here wiped the
|
|
14
|
+
// platform admin's login, first noticed on 2026-09-09; see
|
|
15
|
+
// ../test-support/home-sandbox.ts.) Every test still tears its own
|
|
16
|
+
// fixture down in a finally.
|
|
17
|
+
function permBits(path) {
|
|
18
|
+
return statSync(path).mode & 0o777;
|
|
19
|
+
}
|
|
20
|
+
function cleanup() {
|
|
21
|
+
rmSync(adminCliConfigPath(), { force: true });
|
|
22
|
+
}
|
|
23
|
+
test("loadAdminCliConfig throws a clear error when never logged in", () => {
|
|
24
|
+
cleanup();
|
|
25
|
+
try {
|
|
26
|
+
assert.throws(() => loadAdminCliConfig(), /Not logged in/);
|
|
27
|
+
}
|
|
28
|
+
finally {
|
|
29
|
+
cleanup();
|
|
30
|
+
}
|
|
31
|
+
});
|
|
32
|
+
// Regression test for the bug this cycle's fix closes: a present-but-
|
|
33
|
+
// malformed config.json (a hand-edit typo, disk-full/killed-mid-write, a
|
|
34
|
+
// cloud-synced homedir's conflicted copy) used to throw a raw JSON.parse
|
|
35
|
+
// SyntaxError instead of a clear, actionable message — every CLI command
|
|
36
|
+
// loads this file, so a corrupted one made the CLI permanently unusable
|
|
37
|
+
// with nothing but that unhelpful message to go on. Same regression shape
|
|
38
|
+
// packages/agent's own config.test.ts already established for the
|
|
39
|
+
// identical bug there.
|
|
40
|
+
test("loadAdminCliConfig throws a clear, actionable error (not a raw SyntaxError) for a malformed config file", () => {
|
|
41
|
+
cleanup();
|
|
42
|
+
mkdirSync(dirname(adminCliConfigPath()), { recursive: true });
|
|
43
|
+
writeFileSync(adminCliConfigPath(), "{ not valid json");
|
|
44
|
+
try {
|
|
45
|
+
assert.throws(() => loadAdminCliConfig(), (err) => {
|
|
46
|
+
assert.ok(err instanceof Error);
|
|
47
|
+
assert.ok(!(err instanceof SyntaxError), "expected a wrapped Error, not a raw JSON.parse SyntaxError");
|
|
48
|
+
assert.match(err.message, /corrupted/);
|
|
49
|
+
assert.match(err.message, /appforge-admin login/);
|
|
50
|
+
return true;
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
finally {
|
|
54
|
+
cleanup();
|
|
55
|
+
}
|
|
56
|
+
});
|
|
57
|
+
test("saveAdminCliConfig writes a file loadAdminCliConfig round-trips exactly", () => {
|
|
58
|
+
cleanup();
|
|
59
|
+
try {
|
|
60
|
+
const config = {
|
|
61
|
+
apiUrl: "https://api.example.test",
|
|
62
|
+
adminApiUrl: "https://admin-api.example.test",
|
|
63
|
+
token: "afk_test_token",
|
|
64
|
+
orgId: "org_1",
|
|
65
|
+
};
|
|
66
|
+
saveAdminCliConfig(config);
|
|
67
|
+
assert.deepEqual(loadAdminCliConfig(), config);
|
|
68
|
+
}
|
|
69
|
+
finally {
|
|
70
|
+
cleanup();
|
|
71
|
+
}
|
|
72
|
+
});
|
|
73
|
+
test("saveAdminCliConfig creates a fresh config file with 0600 permissions", () => {
|
|
74
|
+
cleanup();
|
|
75
|
+
try {
|
|
76
|
+
saveAdminCliConfig({
|
|
77
|
+
apiUrl: "https://api.example.test",
|
|
78
|
+
adminApiUrl: "https://admin-api.example.test",
|
|
79
|
+
token: "afk_test_token",
|
|
80
|
+
orgId: "org_1",
|
|
81
|
+
});
|
|
82
|
+
assert.equal(permBits(adminCliConfigPath()), 0o600);
|
|
83
|
+
}
|
|
84
|
+
finally {
|
|
85
|
+
cleanup();
|
|
86
|
+
}
|
|
87
|
+
});
|
|
88
|
+
test("saveAdminCliConfig produces a 0600 file even when overwriting a config that already existed with looser permissions", () => {
|
|
89
|
+
cleanup();
|
|
90
|
+
try {
|
|
91
|
+
// Simulates a file predating this protection (or loosened by hand, a
|
|
92
|
+
// backup restore, a cloud-synced homedir, ...). This assertion holds
|
|
93
|
+
// even with saveAdminCliConfig's own explicit chmodSync call deleted
|
|
94
|
+
// entirely — verified directly, see the comment on that call in
|
|
95
|
+
// config.ts — because renameSync() replaces the destination's
|
|
96
|
+
// directory entry with the freshly-created (and already-0600) temp
|
|
97
|
+
// file's inode wholesale, regardless of what permissions the
|
|
98
|
+
// destination had before. This test guards the actual end-state
|
|
99
|
+
// invariant (the file is 0600 after every save), not any one
|
|
100
|
+
// mechanism that happens to produce it, so it stays meaningful
|
|
101
|
+
// however that invariant ends up being achieved.
|
|
102
|
+
mkdirSync(dirname(adminCliConfigPath()), { recursive: true });
|
|
103
|
+
writeFileSync(adminCliConfigPath(), "{}", { mode: 0o644 });
|
|
104
|
+
assert.equal(permBits(adminCliConfigPath()), 0o644, "test fixture setup should start loose");
|
|
105
|
+
saveAdminCliConfig({
|
|
106
|
+
apiUrl: "https://api.example.test",
|
|
107
|
+
adminApiUrl: "https://admin-api.example.test",
|
|
108
|
+
token: "afk_test_token",
|
|
109
|
+
orgId: "org_1",
|
|
110
|
+
});
|
|
111
|
+
assert.equal(permBits(adminCliConfigPath()), 0o600);
|
|
112
|
+
}
|
|
113
|
+
finally {
|
|
114
|
+
cleanup();
|
|
115
|
+
}
|
|
116
|
+
});
|
|
117
|
+
test("adminCliConfigPath returns a real path under ~/.appforge-admin", () => {
|
|
118
|
+
assert.match(adminCliConfigPath(), /\.appforge-admin[/\\]config\.json$/);
|
|
119
|
+
});
|
|
120
|
+
//# sourceMappingURL=config.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"config.test.js","sourceRoot":"","sources":["../../src/lib/config.test.ts"],"names":[],"mappings":"AAAA,kEAAkE;AAClE,OAAO,iCAAiC,CAAC;AACzC,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AACjC,OAAO,MAAM,MAAM,oBAAoB,CAAC;AACxC,OAAO,EAAE,MAAM,EAAE,aAAa,EAAE,QAAQ,EAAE,SAAS,EAAE,MAAM,SAAS,CAAC;AACrE,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AACpC,OAAO,EAAE,kBAAkB,EAAE,kBAAkB,EAAE,kBAAkB,EAAE,MAAM,aAAa,CAAC;AAEzF,wEAAwE;AACxE,qEAAqE;AACrE,wEAAwE;AACxE,kEAAkE;AAClE,qEAAqE;AACrE,wEAAwE;AACxE,2DAA2D;AAC3D,mEAAmE;AACnE,6BAA6B;AAC7B,SAAS,QAAQ,CAAC,IAAY;IAC5B,OAAO,QAAQ,CAAC,IAAI,CAAC,CAAC,IAAI,GAAG,KAAK,CAAC;AACrC,CAAC;AAED,SAAS,OAAO;IACd,MAAM,CAAC,kBAAkB,EAAE,EAAE,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC,CAAC;AAChD,CAAC;AAED,IAAI,CAAC,8DAA8D,EAAE,GAAG,EAAE;IACxE,OAAO,EAAE,CAAC;IACV,IAAI,CAAC;QACH,MAAM,CAAC,MAAM,CAAC,GAAG,EAAE,CAAC,kBAAkB,EAAE,EAAE,eAAe,CAAC,CAAC;IAC7D,CAAC;YAAS,CAAC;QACT,OAAO,EAAE,CAAC;IACZ,CAAC;AACH,CAAC,CAAC,CAAC;AAEH,sEAAsE;AACtE,yEAAyE;AACzE,yEAAyE;AACzE,yEAAyE;AACzE,wEAAwE;AACxE,0EAA0E;AAC1E,kEAAkE;AAClE,uBAAuB;AACvB,IAAI,CAAC,yGAAyG,EAAE,GAAG,EAAE;IACnH,OAAO,EAAE,CAAC;IACV,SAAS,CAAC,OAAO,CAAC,kBAAkB,EAAE,CAAC,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;IAC9D,aAAa,CAAC,kBAAkB,EAAE,EAAE,kBAAkB,CAAC,CAAC;IACxD,IAAI,CAAC;QACH,MAAM,CAAC,MAAM,CACX,GAAG,EAAE,CAAC,kBAAkB,EAAE,EAC1B,CAAC,GAAY,EAAE,EAAE;YACf,MAAM,CAAC,EAAE,CAAC,GAAG,YAAY,KAAK,CAAC,CAAC;YAChC,MAAM,CAAC,EAAE,CAAC,CAAC,CAAC,GAAG,YAAY,WAAW,CAAC,EAAE,4DAA4D,CAAC,CAAC;YACvG,MAAM,CAAC,KAAK,CAAC,GAAG,CAAC,OAAO,EAAE,WAAW,CAAC,CAAC;YACvC,MAAM,CAAC,KAAK,CAAC,GAAG,CAAC,OAAO,EAAE,sBAAsB,CAAC,CAAC;YAClD,OAAO,IAAI,CAAC;QACd,CAAC,CACF,CAAC;IACJ,CAAC;YAAS,CAAC;QACT,OAAO,EAAE,CAAC;IACZ,CAAC;AACH,CAAC,CAAC,CAAC;AAEH,IAAI,CAAC,yEAAyE,EAAE,GAAG,EAAE;IACnF,OAAO,EAAE,CAAC;IACV,IAAI,CAAC;QACH,MAAM,MAAM,GAAG;YACb,MAAM,EAAE,0BAA0B;YAClC,WAAW,EAAE,gCAAgC;YAC7C,KAAK,EAAE,gBAAgB;YACvB,KAAK,EAAE,OAAO;SACf,CAAC;QACF,kBAAkB,CAAC,MAAM,CAAC,CAAC;QAC3B,MAAM,CAAC,SAAS,CAAC,kBAAkB,EAAE,EAAE,MAAM,CAAC,CAAC;IACjD,CAAC;YAAS,CAAC;QACT,OAAO,EAAE,CAAC;IACZ,CAAC;AACH,CAAC,CAAC,CAAC;AAEH,IAAI,CAAC,sEAAsE,EAAE,GAAG,EAAE;IAChF,OAAO,EAAE,CAAC;IACV,IAAI,CAAC;QACH,kBAAkB,CAAC;YACjB,MAAM,EAAE,0BAA0B;YAClC,WAAW,EAAE,gCAAgC;YAC7C,KAAK,EAAE,gBAAgB;YACvB,KAAK,EAAE,OAAO;SACf,CAAC,CAAC;QACH,MAAM,CAAC,KAAK,CAAC,QAAQ,CAAC,kBAAkB,EAAE,CAAC,EAAE,KAAK,CAAC,CAAC;IACtD,CAAC;YAAS,CAAC;QACT,OAAO,EAAE,CAAC;IACZ,CAAC;AACH,CAAC,CAAC,CAAC;AAEH,IAAI,CACF,qHAAqH,EACrH,GAAG,EAAE;IACH,OAAO,EAAE,CAAC;IACV,IAAI,CAAC;QACH,qEAAqE;QACrE,qEAAqE;QACrE,qEAAqE;QACrE,gEAAgE;QAChE,8DAA8D;QAC9D,mEAAmE;QACnE,6DAA6D;QAC7D,gEAAgE;QAChE,6DAA6D;QAC7D,+DAA+D;QAC/D,iDAAiD;QACjD,SAAS,CAAC,OAAO,CAAC,kBAAkB,EAAE,CAAC,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;QAC9D,aAAa,CAAC,kBAAkB,EAAE,EAAE,IAAI,EAAE,EAAE,IAAI,EAAE,KAAK,EAAE,CAAC,CAAC;QAC3D,MAAM,CAAC,KAAK,CAAC,QAAQ,CAAC,kBAAkB,EAAE,CAAC,EAAE,KAAK,EAAE,uCAAuC,CAAC,CAAC;QAE7F,kBAAkB,CAAC;YACjB,MAAM,EAAE,0BAA0B;YAClC,WAAW,EAAE,gCAAgC;YAC7C,KAAK,EAAE,gBAAgB;YACvB,KAAK,EAAE,OAAO;SACf,CAAC,CAAC;QACH,MAAM,CAAC,KAAK,CAAC,QAAQ,CAAC,kBAAkB,EAAE,CAAC,EAAE,KAAK,CAAC,CAAC;IACtD,CAAC;YAAS,CAAC;QACT,OAAO,EAAE,CAAC;IACZ,CAAC;AACH,CAAC,CACF,CAAC;AAEF,IAAI,CAAC,gEAAgE,EAAE,GAAG,EAAE;IAC1E,MAAM,CAAC,KAAK,CAAC,kBAAkB,EAAE,EAAE,oCAAoC,CAAC,CAAC;AAC3E,CAAC,CAAC,CAAC"}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export declare function prompt(question: string): Promise<string>;
|
|
2
|
+
/**
|
|
3
|
+
* Reads a line from stdin without echoing it back to the terminal.
|
|
4
|
+
* Handles both raw-mode-TTY (one keystroke per `data` event) and piped
|
|
5
|
+
* input (the whole line arrives as one chunk) — see @appforge-ci/cli's copy
|
|
6
|
+
* of this file for the regression story; kept in sync manually since
|
|
7
|
+
* these two CLIs are deliberately independent packages, not a shared
|
|
8
|
+
* dependency (see this package's own module docblock in index.ts).
|
|
9
|
+
*/
|
|
10
|
+
export declare function promptHidden(question: string): Promise<string>;
|
|
11
|
+
//# sourceMappingURL=prompt.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"prompt.d.ts","sourceRoot":"","sources":["../../src/lib/prompt.ts"],"names":[],"mappings":"AAEA,wBAAsB,MAAM,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAO9D;AAKD;;;;;;;GAOG;AACH,wBAAsB,YAAY,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CA+BpE"}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { createInterface } from "node:readline/promises";
|
|
2
|
+
export async function prompt(question) {
|
|
3
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
4
|
+
try {
|
|
5
|
+
return await rl.question(question);
|
|
6
|
+
}
|
|
7
|
+
finally {
|
|
8
|
+
rl.close();
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
const CTRL_C = "\x03";
|
|
12
|
+
const BACKSPACE = ["\x08", "\x7f"];
|
|
13
|
+
/**
|
|
14
|
+
* Reads a line from stdin without echoing it back to the terminal.
|
|
15
|
+
* Handles both raw-mode-TTY (one keystroke per `data` event) and piped
|
|
16
|
+
* input (the whole line arrives as one chunk) — see @appforge-ci/cli's copy
|
|
17
|
+
* of this file for the regression story; kept in sync manually since
|
|
18
|
+
* these two CLIs are deliberately independent packages, not a shared
|
|
19
|
+
* dependency (see this package's own module docblock in index.ts).
|
|
20
|
+
*/
|
|
21
|
+
export async function promptHidden(question) {
|
|
22
|
+
return new Promise((resolve) => {
|
|
23
|
+
process.stdout.write(question);
|
|
24
|
+
const stdin = process.stdin;
|
|
25
|
+
stdin.resume();
|
|
26
|
+
stdin.setRawMode?.(true);
|
|
27
|
+
let value = "";
|
|
28
|
+
const finish = () => {
|
|
29
|
+
stdin.setRawMode?.(false);
|
|
30
|
+
stdin.pause();
|
|
31
|
+
stdin.removeListener("data", onData);
|
|
32
|
+
process.stdout.write("\n");
|
|
33
|
+
resolve(value);
|
|
34
|
+
};
|
|
35
|
+
const onData = (buf) => {
|
|
36
|
+
const chunk = buf.toString("utf8");
|
|
37
|
+
if (chunk.includes(CTRL_C))
|
|
38
|
+
process.exit(1);
|
|
39
|
+
const newlineIndex = chunk.search(/[\n\r]/);
|
|
40
|
+
if (newlineIndex !== -1) {
|
|
41
|
+
value += chunk.slice(0, newlineIndex);
|
|
42
|
+
finish();
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
if (BACKSPACE.includes(chunk)) {
|
|
46
|
+
value = value.slice(0, -1);
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
value += chunk;
|
|
50
|
+
};
|
|
51
|
+
stdin.on("data", onData);
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
//# sourceMappingURL=prompt.js.map
|