@stackframe/stack-cli 2.8.83 → 2.8.85
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/dist/.env.development +89 -0
- package/dist/emulator/cloud-init/emulator/meta-data +2 -0
- package/dist/emulator/cloud-init/emulator/user-data +615 -0
- package/dist/emulator/common.sh +70 -0
- package/dist/emulator/run-emulator.sh +402 -0
- package/dist/index.js +242 -28
- package/dist/index.js.map +1 -1
- package/package.json +4 -4
package/dist/index.js
CHANGED
|
@@ -7,6 +7,7 @@ import * as path from "path";
|
|
|
7
7
|
import { dirname, join, resolve } from "path";
|
|
8
8
|
import { StackClientApp } from "@stackframe/js";
|
|
9
9
|
import * as os from "os";
|
|
10
|
+
import { homedir } from "os";
|
|
10
11
|
import { detectImportPackageFromDir, renderConfigFileContent } from "@stackframe/stack-shared/dist/config-rendering";
|
|
11
12
|
import { checkbox, confirm, input, select } from "@inquirer/prompts";
|
|
12
13
|
import { ALL_APPS } from "@stackframe/stack-shared/dist/apps/apps-config";
|
|
@@ -72,6 +73,9 @@ function resolveRefreshToken() {
|
|
|
72
73
|
if (!token) throw new AuthError("Not logged in. Run `stack login` first.");
|
|
73
74
|
return token;
|
|
74
75
|
}
|
|
76
|
+
function resolveSecretServerKey() {
|
|
77
|
+
return process.env.STACK_SECRET_SERVER_KEY ?? null;
|
|
78
|
+
}
|
|
75
79
|
function resolveProjectId(flags) {
|
|
76
80
|
const projectId = flags.projectId ?? process.env.STACK_PROJECT_ID;
|
|
77
81
|
if (!projectId) throw new AuthError("No project ID specified. Use --project-id or set STACK_PROJECT_ID.");
|
|
@@ -90,11 +94,23 @@ function resolveSessionAuth(flags) {
|
|
|
90
94
|
};
|
|
91
95
|
}
|
|
92
96
|
function resolveAuth(flags) {
|
|
97
|
+
const secretServerKey = resolveSecretServerKey();
|
|
98
|
+
if (secretServerKey) return {
|
|
99
|
+
...resolveLoginConfig(flags),
|
|
100
|
+
projectId: resolveProjectId(flags),
|
|
101
|
+
secretServerKey
|
|
102
|
+
};
|
|
93
103
|
return {
|
|
94
104
|
...resolveSessionAuth(flags),
|
|
95
105
|
projectId: resolveProjectId(flags)
|
|
96
106
|
};
|
|
97
107
|
}
|
|
108
|
+
function isProjectAuthWithSecretServerKey(auth) {
|
|
109
|
+
return "secretServerKey" in auth;
|
|
110
|
+
}
|
|
111
|
+
function isProjectAuthWithRefreshToken(auth) {
|
|
112
|
+
return "refreshToken" in auth;
|
|
113
|
+
}
|
|
98
114
|
|
|
99
115
|
//#endregion
|
|
100
116
|
//#region src/commands/login.ts
|
|
@@ -170,7 +186,9 @@ function getErrorMessage(err) {
|
|
|
170
186
|
function registerExecCommand(program) {
|
|
171
187
|
program.command("exec [javascript]").description("Execute JavaScript with a pre-configured StackServerApp as `stackServerApp`").addHelpText("after", "\nFor available API methods, see: https://docs.stack-auth.com/docs/sdk").action(async (javascript) => {
|
|
172
188
|
if (javascript === void 0) throw new CliError("Missing JavaScript argument. Use `stack exec \"<javascript>\"` or `stack exec --help`.");
|
|
173
|
-
const
|
|
189
|
+
const auth = resolveAuth(program.opts());
|
|
190
|
+
if (!isProjectAuthWithRefreshToken(auth)) throw new CliError("`stack exec` requires `stack login`. Remove STACK_SECRET_SERVER_KEY and try again.");
|
|
191
|
+
const project = await getAdminProject(auth);
|
|
174
192
|
const AsyncFunction = Object.getPrototypeOf(async function() {}).constructor;
|
|
175
193
|
let fn;
|
|
176
194
|
try {
|
|
@@ -190,15 +208,72 @@ function registerExecCommand(program) {
|
|
|
190
208
|
|
|
191
209
|
//#endregion
|
|
192
210
|
//#region src/commands/config-file.ts
|
|
193
|
-
function
|
|
211
|
+
function isConfigOverride(value) {
|
|
194
212
|
if (value === null || typeof value !== "object" || Array.isArray(value)) return false;
|
|
195
213
|
const prototype = Object.getPrototypeOf(value);
|
|
196
214
|
return prototype === Object.prototype || prototype === null;
|
|
197
215
|
}
|
|
216
|
+
function parseGitHubRepository() {
|
|
217
|
+
const repository = process.env.GITHUB_REPOSITORY;
|
|
218
|
+
if (!repository) return null;
|
|
219
|
+
const slashIndex = repository.indexOf("/");
|
|
220
|
+
if (slashIndex <= 0 || slashIndex >= repository.length - 1) return null;
|
|
221
|
+
return {
|
|
222
|
+
owner: repository.slice(0, slashIndex),
|
|
223
|
+
repo: repository.slice(slashIndex + 1)
|
|
224
|
+
};
|
|
225
|
+
}
|
|
226
|
+
function buildConfigPushSource(configFilePath) {
|
|
227
|
+
const repository = parseGitHubRepository();
|
|
228
|
+
const sha = process.env.GITHUB_SHA;
|
|
229
|
+
const branch = process.env.GITHUB_REF_NAME;
|
|
230
|
+
if (repository && sha && branch) return {
|
|
231
|
+
type: "pushed-from-github",
|
|
232
|
+
owner: repository.owner,
|
|
233
|
+
repo: repository.repo,
|
|
234
|
+
branch,
|
|
235
|
+
commit_hash: sha,
|
|
236
|
+
config_file_path: configFilePath
|
|
237
|
+
};
|
|
238
|
+
return { type: "pushed-from-unknown" };
|
|
239
|
+
}
|
|
240
|
+
async function pushConfigWithSecretServerKey(auth, config, source) {
|
|
241
|
+
const endpoint = `${auth.apiUrl.replace(/\/$/, "")}/api/v1/internal/config/override/branch`;
|
|
242
|
+
const response = await fetch(endpoint, {
|
|
243
|
+
method: "PUT",
|
|
244
|
+
headers: {
|
|
245
|
+
"content-type": "application/json",
|
|
246
|
+
"x-stack-project-id": auth.projectId,
|
|
247
|
+
"x-stack-access-type": "server",
|
|
248
|
+
"x-stack-secret-server-key": auth.secretServerKey
|
|
249
|
+
},
|
|
250
|
+
body: JSON.stringify({
|
|
251
|
+
config_string: JSON.stringify(config),
|
|
252
|
+
source
|
|
253
|
+
})
|
|
254
|
+
});
|
|
255
|
+
if (response.ok) return;
|
|
256
|
+
const responseText = await response.text();
|
|
257
|
+
throw new CliError(`Failed to push config with STACK_SECRET_SERVER_KEY: ${responseText.length > 0 ? responseText : `Request failed with status ${response.status}.`}`);
|
|
258
|
+
}
|
|
259
|
+
function sourceToSdkSource(source) {
|
|
260
|
+
if (source.type === "pushed-from-github") return {
|
|
261
|
+
type: "pushed-from-github",
|
|
262
|
+
owner: source.owner,
|
|
263
|
+
repo: source.repo,
|
|
264
|
+
branch: source.branch,
|
|
265
|
+
commitHash: source.commit_hash,
|
|
266
|
+
configFilePath: source.config_file_path
|
|
267
|
+
};
|
|
268
|
+
if (source.type === "pushed-from-unknown") return { type: "pushed-from-unknown" };
|
|
269
|
+
return { type: "unlinked" };
|
|
270
|
+
}
|
|
198
271
|
function registerConfigCommand(program) {
|
|
199
272
|
const config = program.command("config").description("Manage project configuration files");
|
|
200
273
|
config.command("pull").description("Pull branch config to a local file").requiredOption("--config-file <path>", "Path to write config file (.ts)").option("--overwrite", "Overwrite an existing config file").action(async (opts) => {
|
|
201
|
-
const
|
|
274
|
+
const auth = resolveAuth(program.opts());
|
|
275
|
+
if (!isProjectAuthWithRefreshToken(auth)) throw new CliError("`stack config pull` requires `stack login`. Remove STACK_SECRET_SERVER_KEY and try again.");
|
|
276
|
+
const configOverride = await (await getAdminProject(auth)).getConfigOverride("branch");
|
|
202
277
|
const filePath = path.resolve(opts.configFile);
|
|
203
278
|
if (path.extname(filePath) !== ".ts") throw new CliError("Config file must have a .ts extension. Typed config files require TypeScript.");
|
|
204
279
|
if (fs.existsSync(filePath) && !opts.overwrite) throw new CliError(`Config file already exists at ${filePath}. Stage or back up your changes, then re-run with --overwrite.`);
|
|
@@ -207,15 +282,20 @@ function registerConfigCommand(program) {
|
|
|
207
282
|
console.log(`Config written to ${filePath}`);
|
|
208
283
|
});
|
|
209
284
|
config.command("push").description("Push a local config file to branch config").requiredOption("--config-file <path>", "Path to config file (.js or .ts)").action(async (opts) => {
|
|
210
|
-
const
|
|
285
|
+
const auth = resolveAuth(program.opts());
|
|
211
286
|
const filePath = path.resolve(opts.configFile);
|
|
212
287
|
const ext = path.extname(filePath);
|
|
213
288
|
if (ext !== ".js" && ext !== ".ts") throw new CliError("Config file must have a .js or .ts extension.");
|
|
214
289
|
if (!fs.existsSync(filePath)) throw new CliError(`Config file not found: ${filePath}`);
|
|
215
290
|
const { createJiti } = await import("jiti");
|
|
216
291
|
const config = (await createJiti(import.meta.url).import(filePath)).config;
|
|
217
|
-
if (!
|
|
218
|
-
|
|
292
|
+
if (!isConfigOverride(config)) throw new CliError(`Config file must export a plain \`config\` object. Example: import type { StackConfig } from "${detectImportPackageFromDir(path.dirname(filePath)) ?? "@stackframe/js"}"; export const config: StackConfig = { ... };`);
|
|
293
|
+
const source = buildConfigPushSource(opts.configFile);
|
|
294
|
+
if (isProjectAuthWithSecretServerKey(auth)) await pushConfigWithSecretServerKey(auth, config, source);
|
|
295
|
+
else {
|
|
296
|
+
if (!isProjectAuthWithRefreshToken(auth)) throw new CliError("`stack config push` requires either STACK_SECRET_SERVER_KEY or `stack login`.");
|
|
297
|
+
await (await getAdminProject(auth)).pushConfig(config, { source: sourceToSdkSource(source) });
|
|
298
|
+
}
|
|
219
299
|
console.log("Config pushed successfully.");
|
|
220
300
|
});
|
|
221
301
|
}
|
|
@@ -723,6 +803,60 @@ function registerProjectCommand(program) {
|
|
|
723
803
|
|
|
724
804
|
//#endregion
|
|
725
805
|
//#region src/commands/emulator.ts
|
|
806
|
+
const DEFAULT_EMULATOR_BACKEND_PORT = 26701;
|
|
807
|
+
function emulatorBackendPort() {
|
|
808
|
+
const raw = process.env.EMULATOR_BACKEND_PORT;
|
|
809
|
+
if (!raw) return DEFAULT_EMULATOR_BACKEND_PORT;
|
|
810
|
+
const parsed = Number(raw);
|
|
811
|
+
if (!Number.isInteger(parsed) || parsed <= 0) throw new CliError(`Invalid EMULATOR_BACKEND_PORT: ${raw}`);
|
|
812
|
+
return parsed;
|
|
813
|
+
}
|
|
814
|
+
function emulatorHome() {
|
|
815
|
+
return process.env.STACK_EMULATOR_HOME ?? join(homedir(), ".stack", "emulator");
|
|
816
|
+
}
|
|
817
|
+
function emulatorRunDir() {
|
|
818
|
+
return join(emulatorHome(), "run");
|
|
819
|
+
}
|
|
820
|
+
function emulatorImageDir() {
|
|
821
|
+
return join(emulatorHome(), "images");
|
|
822
|
+
}
|
|
823
|
+
function internalPckPath() {
|
|
824
|
+
return join(emulatorRunDir(), "vm", "internal-pck");
|
|
825
|
+
}
|
|
826
|
+
async function readInternalPck(timeoutMs = 6e4) {
|
|
827
|
+
const path = internalPckPath();
|
|
828
|
+
const deadline = Date.now() + timeoutMs;
|
|
829
|
+
let delay = 250;
|
|
830
|
+
while (Date.now() < deadline) {
|
|
831
|
+
if (existsSync(path)) {
|
|
832
|
+
const contents = readFileSync(path, "utf-8").trim();
|
|
833
|
+
if (contents) return contents;
|
|
834
|
+
}
|
|
835
|
+
await new Promise((r) => setTimeout(r, delay));
|
|
836
|
+
delay = Math.min(delay * 2, 2e3);
|
|
837
|
+
}
|
|
838
|
+
throw new CliError(`Timed out waiting for emulator internal publishable client key at ${path}`);
|
|
839
|
+
}
|
|
840
|
+
async function fetchEmulatorCredentials(pck, backendPort, configFile) {
|
|
841
|
+
const url = `http://127.0.0.1:${backendPort}/api/v1/internal/local-emulator/project`;
|
|
842
|
+
const res = await fetch(url, {
|
|
843
|
+
method: "POST",
|
|
844
|
+
headers: {
|
|
845
|
+
"Content-Type": "application/json",
|
|
846
|
+
"X-Stack-Project-Id": "internal",
|
|
847
|
+
"X-Stack-Access-Type": "client",
|
|
848
|
+
"X-Stack-Publishable-Client-Key": pck
|
|
849
|
+
},
|
|
850
|
+
body: JSON.stringify({ absolute_file_path: configFile })
|
|
851
|
+
});
|
|
852
|
+
if (!res.ok) throw new CliError(`Failed to initialize local emulator project (${res.status}): ${await res.text()}`);
|
|
853
|
+
const data = await res.json();
|
|
854
|
+
return {
|
|
855
|
+
project_id: data.project_id,
|
|
856
|
+
publishable_client_key: data.publishable_client_key,
|
|
857
|
+
secret_server_key: data.secret_server_key
|
|
858
|
+
};
|
|
859
|
+
}
|
|
726
860
|
function gh(args) {
|
|
727
861
|
try {
|
|
728
862
|
return execFileSync("gh", args, {
|
|
@@ -738,28 +872,57 @@ function gh(args) {
|
|
|
738
872
|
throw new CliError("GitHub CLI (gh) is required. Install: https://cli.github.com/");
|
|
739
873
|
}
|
|
740
874
|
}
|
|
741
|
-
function
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
875
|
+
function emulatorScriptsDir() {
|
|
876
|
+
const here = dirname(fileURLToPath(import.meta.url));
|
|
877
|
+
const bundled = join(here, "emulator");
|
|
878
|
+
if (existsSync(join(bundled, "run-emulator.sh"))) return bundled;
|
|
879
|
+
const repo = resolve(here, "../../../docker/local-emulator/qemu");
|
|
880
|
+
if (existsSync(join(repo, "run-emulator.sh"))) return repo;
|
|
881
|
+
throw new CliError("Emulator scripts not found in CLI bundle.");
|
|
882
|
+
}
|
|
883
|
+
function emulatorSpawnEnv(extra) {
|
|
884
|
+
return {
|
|
885
|
+
...process.env,
|
|
886
|
+
EMULATOR_RUN_DIR: emulatorRunDir(),
|
|
887
|
+
EMULATOR_IMAGE_DIR: emulatorImageDir(),
|
|
888
|
+
...extra
|
|
889
|
+
};
|
|
747
890
|
}
|
|
748
891
|
function runEmulator(action, env) {
|
|
749
|
-
const
|
|
750
|
-
|
|
751
|
-
|
|
892
|
+
const scriptsDir = emulatorScriptsDir();
|
|
893
|
+
mkdirSync(emulatorRunDir(), { recursive: true });
|
|
894
|
+
mkdirSync(emulatorImageDir(), { recursive: true });
|
|
895
|
+
return new Promise((resolvePromise, reject) => {
|
|
896
|
+
const child = spawn(join(scriptsDir, "run-emulator.sh"), [action], {
|
|
752
897
|
stdio: "inherit",
|
|
753
|
-
env:
|
|
754
|
-
|
|
755
|
-
...env
|
|
756
|
-
},
|
|
757
|
-
cwd: qemuDir
|
|
898
|
+
env: emulatorSpawnEnv(env),
|
|
899
|
+
cwd: scriptsDir
|
|
758
900
|
});
|
|
759
|
-
child.on("close", (code) => code === 0 ?
|
|
901
|
+
child.on("close", (code) => code === 0 ? resolvePromise() : reject(new CliError(`run-emulator.sh ${action} exited with code ${code}`)));
|
|
760
902
|
child.on("error", (err) => reject(new CliError(`Failed to run run-emulator.sh: ${err.message}`)));
|
|
761
903
|
});
|
|
762
904
|
}
|
|
905
|
+
function isEmulatorRunning() {
|
|
906
|
+
const scriptsDir = emulatorScriptsDir();
|
|
907
|
+
try {
|
|
908
|
+
execFileSync(join(scriptsDir, "run-emulator.sh"), ["status"], {
|
|
909
|
+
stdio: "pipe",
|
|
910
|
+
cwd: scriptsDir,
|
|
911
|
+
env: emulatorSpawnEnv()
|
|
912
|
+
});
|
|
913
|
+
return true;
|
|
914
|
+
} catch {
|
|
915
|
+
return false;
|
|
916
|
+
}
|
|
917
|
+
}
|
|
918
|
+
async function startEmulator(arch) {
|
|
919
|
+
mkdirSync(emulatorImageDir(), { recursive: true });
|
|
920
|
+
if (!existsSync(join(emulatorImageDir(), `stack-emulator-${arch}.qcow2`))) {
|
|
921
|
+
console.log("No emulator image found. Pulling latest...");
|
|
922
|
+
pullRelease(arch);
|
|
923
|
+
}
|
|
924
|
+
await runEmulator("start", { EMULATOR_ARCH: arch });
|
|
925
|
+
}
|
|
763
926
|
function resolveArch(raw) {
|
|
764
927
|
const arch = raw ?? (process.arch === "arm64" ? "arm64" : process.arch === "x64" ? "amd64" : null);
|
|
765
928
|
if (arch === "arm64" || arch === "amd64") return arch;
|
|
@@ -770,7 +933,7 @@ function pullRelease(arch, opts = {}) {
|
|
|
770
933
|
const branch = opts.branch ?? "dev";
|
|
771
934
|
const tag = opts.tag ?? `emulator-${branch}-latest`;
|
|
772
935
|
const asset = `stack-emulator-${arch}.qcow2`;
|
|
773
|
-
const imageDir =
|
|
936
|
+
const imageDir = emulatorImageDir();
|
|
774
937
|
mkdirSync(imageDir, { recursive: true });
|
|
775
938
|
const dest = join(imageDir, asset);
|
|
776
939
|
const tmpDest = `${dest}.download`;
|
|
@@ -832,7 +995,7 @@ function registerEmulatorCommand(program) {
|
|
|
832
995
|
if (runs.length === 0) throw new CliError(`No successful build found for PR #${opts.pr} (branch: ${headRefName}).`);
|
|
833
996
|
runId = String(runs[0].databaseId);
|
|
834
997
|
}
|
|
835
|
-
const imageDir =
|
|
998
|
+
const imageDir = emulatorImageDir();
|
|
836
999
|
mkdirSync(imageDir, { recursive: true });
|
|
837
1000
|
const dest = join(imageDir, `stack-emulator-${arch}.qcow2`);
|
|
838
1001
|
if (existsSync(dest)) unlinkSync(dest);
|
|
@@ -860,13 +1023,64 @@ function registerEmulatorCommand(program) {
|
|
|
860
1023
|
tag: opts.tag
|
|
861
1024
|
});
|
|
862
1025
|
});
|
|
863
|
-
emulator.command("start").description("Start the emulator in the background (auto-pulls the latest image if none exists)").option("--arch <arch>", "Target architecture (default: current system arch). Non-native uses software emulation and is significantly slower.").action(async (opts) => {
|
|
1026
|
+
emulator.command("start").description("Start the emulator in the background (auto-pulls the latest image if none exists)").option("--arch <arch>", "Target architecture (default: current system arch). Non-native uses software emulation and is significantly slower.").option("--config-file <path>", "Path to a config file; when set, credentials for this project are printed to stdout as JSON").action(async (opts) => {
|
|
864
1027
|
const arch = resolveArch(opts.arch);
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
1028
|
+
let resolvedConfigFile;
|
|
1029
|
+
if (opts.configFile) {
|
|
1030
|
+
resolvedConfigFile = resolve(opts.configFile);
|
|
1031
|
+
if (!existsSync(resolvedConfigFile)) throw new CliError(`Config file not found: ${resolvedConfigFile}`);
|
|
868
1032
|
}
|
|
869
|
-
|
|
1033
|
+
if (isEmulatorRunning()) console.warn("Emulator already running, reusing existing instance.");
|
|
1034
|
+
else await startEmulator(arch);
|
|
1035
|
+
if (resolvedConfigFile) {
|
|
1036
|
+
const creds = await fetchEmulatorCredentials(await readInternalPck(), emulatorBackendPort(), resolvedConfigFile);
|
|
1037
|
+
console.log(JSON.stringify(creds, null, 2));
|
|
1038
|
+
}
|
|
1039
|
+
});
|
|
1040
|
+
emulator.command("run").description("Start the emulator, run a command, and stop the emulator when the command exits").argument("<cmd>", "Command to run (e.g. \"npm run dev\")").option("--arch <arch>", "Target architecture").option("--config-file <path>", "Path to a config file; fetches credentials and injects STACK_PROJECT_ID / STACK_PUBLISHABLE_CLIENT_KEY / STACK_SECRET_SERVER_KEY into the child").action(async (cmd, opts) => {
|
|
1041
|
+
const arch = resolveArch(opts.arch);
|
|
1042
|
+
let resolvedConfigFile;
|
|
1043
|
+
if (opts.configFile) {
|
|
1044
|
+
resolvedConfigFile = resolve(opts.configFile);
|
|
1045
|
+
if (!existsSync(resolvedConfigFile)) throw new CliError(`Config file not found: ${resolvedConfigFile}`);
|
|
1046
|
+
}
|
|
1047
|
+
const alreadyRunning = isEmulatorRunning();
|
|
1048
|
+
if (alreadyRunning) console.log("Emulator already running, reusing existing instance.");
|
|
1049
|
+
else await startEmulator(arch);
|
|
1050
|
+
const childEnv = { ...process.env };
|
|
1051
|
+
if (resolvedConfigFile) {
|
|
1052
|
+
const pck = await readInternalPck();
|
|
1053
|
+
const backendPort = emulatorBackendPort();
|
|
1054
|
+
const creds = await fetchEmulatorCredentials(pck, backendPort, resolvedConfigFile);
|
|
1055
|
+
const apiUrl = `http://127.0.0.1:${backendPort}`;
|
|
1056
|
+
childEnv.STACK_PROJECT_ID = creds.project_id;
|
|
1057
|
+
childEnv.NEXT_PUBLIC_STACK_PROJECT_ID = creds.project_id;
|
|
1058
|
+
childEnv.STACK_PUBLISHABLE_CLIENT_KEY = creds.publishable_client_key;
|
|
1059
|
+
childEnv.NEXT_PUBLIC_STACK_PUBLISHABLE_CLIENT_KEY = creds.publishable_client_key;
|
|
1060
|
+
childEnv.STACK_SECRET_SERVER_KEY = creds.secret_server_key;
|
|
1061
|
+
childEnv.STACK_API_URL = apiUrl;
|
|
1062
|
+
childEnv.NEXT_PUBLIC_STACK_API_URL = apiUrl;
|
|
1063
|
+
}
|
|
1064
|
+
const child = spawn(cmd, {
|
|
1065
|
+
shell: true,
|
|
1066
|
+
stdio: "inherit",
|
|
1067
|
+
env: childEnv
|
|
1068
|
+
});
|
|
1069
|
+
const forward = (signal) => () => child.kill(signal);
|
|
1070
|
+
const onSigint = forward("SIGINT");
|
|
1071
|
+
const onSigterm = forward("SIGTERM");
|
|
1072
|
+
process.on("SIGINT", onSigint);
|
|
1073
|
+
process.on("SIGTERM", onSigterm);
|
|
1074
|
+
child.on("close", (code) => {
|
|
1075
|
+
process.off("SIGINT", onSigint);
|
|
1076
|
+
process.off("SIGTERM", onSigterm);
|
|
1077
|
+
const exitCode = code ?? 1;
|
|
1078
|
+
if (alreadyRunning) process.exit(exitCode);
|
|
1079
|
+
else {
|
|
1080
|
+
console.log("\nStopping emulator...");
|
|
1081
|
+
runEmulator("stop").catch(() => {}).finally(() => process.exit(exitCode));
|
|
1082
|
+
}
|
|
1083
|
+
});
|
|
870
1084
|
});
|
|
871
1085
|
emulator.command("stop").description("Stop the emulator (data preserved; use 'reset' to clear)").action(() => runEmulator("stop"));
|
|
872
1086
|
emulator.command("reset").description("Reset emulator state for a fresh boot").action(() => runEmulator("reset"));
|