@appspacer/cli 1.0.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.
Files changed (49) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +271 -0
  3. package/dist/__tests__/api.test.d.ts +1 -0
  4. package/dist/__tests__/api.test.js +142 -0
  5. package/dist/__tests__/config.test.d.ts +1 -0
  6. package/dist/__tests__/config.test.js +109 -0
  7. package/dist/__tests__/hash.test.d.ts +1 -0
  8. package/dist/__tests__/hash.test.js +47 -0
  9. package/dist/__tests__/setup-injections.test.d.ts +1 -0
  10. package/dist/__tests__/setup-injections.test.js +238 -0
  11. package/dist/__tests__/zip.test.d.ts +1 -0
  12. package/dist/__tests__/zip.test.js +62 -0
  13. package/dist/api.d.ts +6 -0
  14. package/dist/api.js +52 -0
  15. package/dist/commands/deployments.d.ts +2 -0
  16. package/dist/commands/deployments.js +39 -0
  17. package/dist/commands/envsync.d.ts +2 -0
  18. package/dist/commands/envsync.js +230 -0
  19. package/dist/commands/login.d.ts +2 -0
  20. package/dist/commands/login.js +41 -0
  21. package/dist/commands/release-flutter.d.ts +2 -0
  22. package/dist/commands/release-flutter.js +176 -0
  23. package/dist/commands/release-react-native.d.ts +2 -0
  24. package/dist/commands/release-react-native.js +143 -0
  25. package/dist/commands/release.d.ts +2 -0
  26. package/dist/commands/release.js +106 -0
  27. package/dist/commands/rollback.d.ts +2 -0
  28. package/dist/commands/rollback.js +43 -0
  29. package/dist/commands/setup.d.ts +22 -0
  30. package/dist/commands/setup.js +575 -0
  31. package/dist/commands/vault.d.ts +2 -0
  32. package/dist/commands/vault.js +292 -0
  33. package/dist/commands/whoami.d.ts +2 -0
  34. package/dist/commands/whoami.js +16 -0
  35. package/dist/config.d.ts +8 -0
  36. package/dist/config.js +45 -0
  37. package/dist/index.d.ts +2 -0
  38. package/dist/index.js +25 -0
  39. package/dist/utils/bundle.d.ts +8 -0
  40. package/dist/utils/bundle.js +59 -0
  41. package/dist/utils/hash.d.ts +4 -0
  42. package/dist/utils/hash.js +9 -0
  43. package/dist/utils/ui.d.ts +19 -0
  44. package/dist/utils/ui.js +43 -0
  45. package/dist/utils/validators.d.ts +25 -0
  46. package/dist/utils/validators.js +65 -0
  47. package/dist/utils/zip.d.ts +5 -0
  48. package/dist/utils/zip.js +17 -0
  49. package/package.json +66 -0
@@ -0,0 +1,292 @@
1
+ import fs from "fs";
2
+ import path from "path";
3
+ import { Command } from "commander";
4
+ import chalk from "chalk";
5
+ import ora from "ora";
6
+ import dotenv from "dotenv";
7
+ import { select, confirm } from "@inquirer/prompts";
8
+ import { apiRequest } from "../api.js";
9
+ const VAULT_CONF = path.join(process.cwd(), ".vault.json");
10
+ const LOCAL_ENV = path.join(process.cwd(), ".env");
11
+ function loadVaultConfig() {
12
+ if (!fs.existsSync(VAULT_CONF)) {
13
+ throw new Error(`Vault not initialized. Run ${chalk.cyan("appspacer vault init")} first.`);
14
+ }
15
+ return JSON.parse(fs.readFileSync(VAULT_CONF, "utf-8"));
16
+ }
17
+ function saveVaultConfig(config) {
18
+ fs.writeFileSync(VAULT_CONF, JSON.stringify(config, null, 2));
19
+ }
20
+ // ── INIT ────────────────────────────────────────────────────────
21
+ const init = async () => {
22
+ const spinner = ora("Fetching vault projects...").start();
23
+ try {
24
+ const projects = await apiRequest("/cli/vault/projects");
25
+ spinner.stop();
26
+ if (!projects || projects.length === 0) {
27
+ console.log(chalk.red("✖ No Vault projects found. Create one in the AppSpacer dashboard first."));
28
+ return;
29
+ }
30
+ const projectId = await select({
31
+ message: "Select a vault project:",
32
+ choices: projects.map(p => ({ name: p.name, value: p.id }))
33
+ });
34
+ spinner.start("Fetching environments...");
35
+ const envs = await apiRequest(`/cli/vault/environments/${projectId}`);
36
+ spinner.stop();
37
+ if (!envs || envs.length === 0) {
38
+ console.log(chalk.red("✖ No environments found for this project."));
39
+ return;
40
+ }
41
+ const envId = await select({
42
+ message: "Select an environment:",
43
+ choices: envs.map(e => ({ name: e.name, value: e.id }))
44
+ });
45
+ const selectedEnv = envs.find(e => e.id === envId);
46
+ saveVaultConfig({ projectId, envId, envName: selectedEnv.name });
47
+ // Ensure .env is ignored
48
+ const gitignorePath = path.join(process.cwd(), ".gitignore");
49
+ if (fs.existsSync(gitignorePath)) {
50
+ const content = fs.readFileSync(gitignorePath, "utf-8");
51
+ if (!content.includes(".env")) {
52
+ const prefix = content.endsWith("\n") ? "" : "\n";
53
+ fs.appendFileSync(gitignorePath, `${prefix}\n# AppSpacer Vault\n.env\n`);
54
+ console.log(chalk.gray(` Added .env to .gitignore`));
55
+ }
56
+ }
57
+ else {
58
+ fs.writeFileSync(gitignorePath, "# AppSpacer Vault\n.env\n");
59
+ console.log(chalk.gray(` Created .gitignore with .env`));
60
+ }
61
+ console.log(chalk.green(`✔ Vault initialised!`));
62
+ console.log(` Project: ${projects.find(p => p.id === projectId)?.name}`);
63
+ console.log(` Environment: ${selectedEnv.name}`);
64
+ console.log(` Target: .env\n`);
65
+ console.log(`Run ${chalk.cyan("appspacer vault pull")} to download variables.`);
66
+ }
67
+ catch (err) {
68
+ spinner.fail(err instanceof Error ? err.message : "Init failed");
69
+ }
70
+ };
71
+ // ── PUSH ────────────────────────────────────────────────────────
72
+ const push = async () => {
73
+ try {
74
+ const conf = loadVaultConfig();
75
+ if (!fs.existsSync(LOCAL_ENV)) {
76
+ console.log(chalk.yellow(`⚠ No .env file found in ${process.cwd()}`));
77
+ return;
78
+ }
79
+ const envString = fs.readFileSync(LOCAL_ENV, "utf-8");
80
+ const parsedCount = Object.keys(dotenv.parse(Buffer.from(envString))).length;
81
+ if (parsedCount === 0) {
82
+ console.log(chalk.yellow("⚠ Local .env is empty. Nothing to push."));
83
+ return;
84
+ }
85
+ const spinner = ora(`Pushing ${parsedCount} variables to vault ${chalk.bold(conf.envName)}...`).start();
86
+ const res = await apiRequest("/cli/vault/variable/import", {
87
+ method: "POST",
88
+ body: JSON.stringify({ environment_id: conf.envId, envString })
89
+ });
90
+ spinner.succeed(chalk.green(`${res.count} secrets pushed successfully!`));
91
+ }
92
+ catch (err) {
93
+ console.log(chalk.red("✖ " + (err instanceof Error ? err.message : "Failed to push secrets")));
94
+ }
95
+ };
96
+ // ── PULL ────────────────────────────────────────────────────────
97
+ const pull = async () => {
98
+ try {
99
+ if (fs.existsSync(LOCAL_ENV)) {
100
+ const ok = await confirm({
101
+ message: "A .env file already exists. Overwrite it with vault secrets?",
102
+ default: false
103
+ });
104
+ if (!ok) {
105
+ console.log(chalk.gray("Aborted."));
106
+ return;
107
+ }
108
+ }
109
+ const conf = loadVaultConfig();
110
+ const spinner = ora(`Pulling secrets from ${chalk.bold(conf.envName)}...`).start();
111
+ const vars = await apiRequest(`/cli/vault/variables/${conf.envId}?decrypt=true`);
112
+ if (vars.length === 0) {
113
+ spinner.info(chalk.yellow("No variables found in this environment."));
114
+ return;
115
+ }
116
+ const lines = vars.map(v => `${v.key}=${v.value}`);
117
+ const outString = lines.join("\n") + "\n";
118
+ // Atomic write — prevent a partial file if the process is interrupted
119
+ const tmpPath = LOCAL_ENV + ".tmp";
120
+ fs.writeFileSync(tmpPath, outString);
121
+ fs.renameSync(tmpPath, LOCAL_ENV);
122
+ spinner.succeed(chalk.green(`${vars.length} secrets pulled to .env`));
123
+ }
124
+ catch (err) {
125
+ console.log(chalk.red("✖ " + (err instanceof Error ? err.message : "Failed to pull secrets")));
126
+ }
127
+ };
128
+ // ── ENV COMMANDS ────────────────────────────────────────────────
129
+ const envCommand = new Command("env").description("Manage synced environments");
130
+ envCommand.command("list")
131
+ .description("List all environments in the current project")
132
+ .action(async () => {
133
+ try {
134
+ const conf = loadVaultConfig();
135
+ const spinner = ora("Fetching environments...").start();
136
+ const envs = await apiRequest(`/cli/vault/environments/${conf.projectId}`);
137
+ spinner.stop();
138
+ console.log(chalk.bold("\nEnvironments:"));
139
+ envs.forEach(e => {
140
+ const mark = e.id === conf.envId ? chalk.green("●") : "○";
141
+ console.log(` ${mark} ${e.id === conf.envId ? chalk.green(e.name) : e.name}`);
142
+ });
143
+ console.log();
144
+ }
145
+ catch (err) {
146
+ console.log(chalk.red("✖ " + (err instanceof Error ? err.message : "Failed")));
147
+ }
148
+ });
149
+ envCommand.command("use")
150
+ .description("Switch the active environment")
151
+ .argument("<env_name>", "Name of the environment to switch to")
152
+ .action(async (envName) => {
153
+ try {
154
+ const conf = loadVaultConfig();
155
+ const spinner = ora(`Finding environment '${envName}'...`).start();
156
+ const envs = await apiRequest(`/cli/vault/environments/${conf.projectId}`);
157
+ const target = envs.find(e => e.name.toLowerCase() === envName.toLowerCase());
158
+ if (!target) {
159
+ spinner.fail(`Environment '${envName}' not found in this project.`);
160
+ return;
161
+ }
162
+ saveVaultConfig({ ...conf, envId: target.id, envName: target.name });
163
+ spinner.succeed(`Switched to ${chalk.bold(target.name)}`);
164
+ await pull();
165
+ }
166
+ catch (err) {
167
+ console.log(chalk.red("✖ " + (err instanceof Error ? err.message : "Failed")));
168
+ }
169
+ });
170
+ // ── SECRETS COMMANDS ────────────────────────────────────────────
171
+ const secretsCommand = new Command("secrets").description("Inspect and edit individual secrets");
172
+ secretsCommand.command("ls")
173
+ .description("List all remote secrets (values masked)")
174
+ .action(async () => {
175
+ try {
176
+ const conf = loadVaultConfig();
177
+ const spinner = ora(`Fetching secrets from ${conf.envName}...`).start();
178
+ const vars = await apiRequest(`/cli/vault/variables/${conf.envId}`);
179
+ spinner.stop();
180
+ console.log(chalk.bold(`\nSecrets for ${conf.envName}:`));
181
+ if (vars.length === 0) {
182
+ console.log(chalk.gray(" (No secrets found)"));
183
+ }
184
+ else {
185
+ vars.forEach(v => {
186
+ console.log(` ${chalk.cyan(v.key)} = ${chalk.gray("••••••••")}`);
187
+ });
188
+ }
189
+ console.log();
190
+ }
191
+ catch (err) {
192
+ console.log(chalk.red("✖ " + (err instanceof Error ? err.message : "Failed")));
193
+ }
194
+ });
195
+ secretsCommand.command("set")
196
+ .description("Set a single remote variable (e.g. KEY=VALUE)")
197
+ .argument("<keyval>", "Format: KEY=VALUE")
198
+ .action(async (keyval) => {
199
+ const parts = keyval.split("=");
200
+ if (parts.length < 2) {
201
+ console.log(chalk.red("✖ Invalid format. Usage: appspacer vault secrets set KEY=VALUE"));
202
+ return;
203
+ }
204
+ const key = parts[0].trim();
205
+ const value = parts.slice(1).join("=").trim(); // in case value has =
206
+ try {
207
+ const conf = loadVaultConfig();
208
+ const spinner = ora(`Setting ${key}...`).start();
209
+ await apiRequest("/cli/vault/variable/create", {
210
+ method: "POST",
211
+ body: JSON.stringify({ environment_id: conf.envId, key, value })
212
+ });
213
+ spinner.succeed(`Set ${chalk.cyan(key)} in ${chalk.bold(conf.envName)}`);
214
+ }
215
+ catch (err) {
216
+ console.log(chalk.red("✖ " + (err instanceof Error ? err.message : "Failed")));
217
+ }
218
+ });
219
+ // ── AUDIT ───────────────────────────────────────────────────────
220
+ const audit = async () => {
221
+ try {
222
+ const conf = loadVaultConfig();
223
+ const spinner = ora(`Fetching audit logs...`).start();
224
+ const logs = await apiRequest(`/cli/vault/audit/${conf.envId}`);
225
+ spinner.stop();
226
+ console.log(chalk.bold(`\nAudit Logs - ${conf.envName}`));
227
+ if (logs.length === 0) {
228
+ console.log(chalk.gray(" No audit history found."));
229
+ return;
230
+ }
231
+ logs.forEach(log => {
232
+ const userStr = log.users
233
+ ? `${log.users.first_name} ${log.users.last_name}`.trim() || log.users.email
234
+ : "System";
235
+ const dateStr = new Date(log.created_at).toLocaleString();
236
+ let actionStr = chalk.bold(log.action);
237
+ if (log.action === "PULL")
238
+ actionStr = chalk.cyan(actionStr);
239
+ else if (log.action === "PUSH")
240
+ actionStr = chalk.yellow(actionStr);
241
+ else if (log.action === "SET_VAR")
242
+ actionStr = chalk.green(actionStr);
243
+ // Format metadata as readable key=value pairs instead of raw JSON
244
+ const meta = log.metadata;
245
+ const metaStr = meta
246
+ ? Object.entries(meta)
247
+ .map(([k, v]) => `${chalk.dim(k + "=")}${v}`)
248
+ .join(" ")
249
+ : "";
250
+ const line = metaStr
251
+ ? `${chalk.gray(dateStr)} ${actionStr.padEnd(10)} ${userStr} ${chalk.dim(metaStr)}`
252
+ : `${chalk.gray(dateStr)} ${actionStr.padEnd(10)} ${userStr}`;
253
+ console.log(line);
254
+ });
255
+ console.log();
256
+ }
257
+ catch (err) {
258
+ console.log(chalk.red("✖ " + (err instanceof Error ? err.message : "Failed")));
259
+ }
260
+ };
261
+ // ── ROLLBACK ────────────────────────────────────────────────────
262
+ const rollback = async () => {
263
+ try {
264
+ const conf = loadVaultConfig();
265
+ const ok = await confirm({
266
+ message: `Are you sure you want to revert the last change in ${chalk.bold(conf.envName)}?`,
267
+ default: false
268
+ });
269
+ if (!ok) {
270
+ console.log(chalk.gray("Aborted."));
271
+ return;
272
+ }
273
+ const spinner = ora(`Rolling back last change in ${conf.envName}...`).start();
274
+ const res = await apiRequest(`/cli/vault/rollback/${conf.envId}`, {
275
+ method: "POST"
276
+ });
277
+ spinner.succeed(chalk.green(res.message));
278
+ console.log(`Run ${chalk.cyan("appspacer vault pull")} to update your local .env file.`);
279
+ }
280
+ catch (err) {
281
+ console.log(chalk.red("✖ " + (err instanceof Error ? err.message : "Failed to rollback")));
282
+ }
283
+ };
284
+ export const vaultCommand = new Command("vault")
285
+ .description("Manage and sync environment variables via AppSpacer Vault");
286
+ vaultCommand.command("init").description("Initialize vault in the current directory").action(init);
287
+ vaultCommand.command("push").description("Push local .env to remote vault").action(push);
288
+ vaultCommand.command("pull").description("Pull variables from remote vault into local .env").action(pull);
289
+ vaultCommand.command("rollback").description("Revert the most recent change in the synced environment").action(rollback);
290
+ vaultCommand.command("audit").description("View recent audit history for the synced environment").action(audit);
291
+ vaultCommand.addCommand(envCommand);
292
+ vaultCommand.addCommand(secretsCommand);
@@ -0,0 +1,2 @@
1
+ import { Command } from "commander";
2
+ export declare const whoamiCommand: Command;
@@ -0,0 +1,16 @@
1
+ import { Command } from "commander";
2
+ import chalk from "chalk";
3
+ import { apiRequest } from "../api.js";
4
+ export const whoamiCommand = new Command("whoami")
5
+ .description("Show the currently authenticated user")
6
+ .action(async () => {
7
+ try {
8
+ const user = await apiRequest("/profile");
9
+ const displayName = `${user.first_name || ""} ${user.last_name || ""}`.trim() || user.email;
10
+ console.log(chalk.green(`✓ Logged in as ${chalk.bold(displayName)} (${user.email})`));
11
+ }
12
+ catch (err) {
13
+ console.error(chalk.red(`✗ ${err.message}`));
14
+ process.exit(1);
15
+ }
16
+ });
@@ -0,0 +1,8 @@
1
+ export interface AppSpacerConfig {
2
+ apiUrl: string;
3
+ accessToken: string | null;
4
+ }
5
+ export declare function loadConfig(): AppSpacerConfig;
6
+ export declare function saveConfig(config: Partial<AppSpacerConfig>): void;
7
+ export declare function getToken(): string;
8
+ export declare function getApiUrl(): string;
package/dist/config.js ADDED
@@ -0,0 +1,45 @@
1
+ import fs from "fs";
2
+ import path from "path";
3
+ import os from "os";
4
+ const CONFIG_DIR = path.join(os.homedir(), ".appspacer");
5
+ const CONFIG_FILE = path.join(CONFIG_DIR, "config.json");
6
+ const DEFAULT_CONFIG = {
7
+ apiUrl: "https://api.appspacer.com/api",
8
+ accessToken: null,
9
+ };
10
+ export function loadConfig() {
11
+ try {
12
+ if (fs.existsSync(CONFIG_FILE)) {
13
+ const raw = fs.readFileSync(CONFIG_FILE, "utf-8");
14
+ return { ...DEFAULT_CONFIG, ...JSON.parse(raw) };
15
+ }
16
+ }
17
+ catch {
18
+ // Corrupt or unreadable config — reset to defaults
19
+ }
20
+ return { ...DEFAULT_CONFIG };
21
+ }
22
+ export function saveConfig(config) {
23
+ const current = loadConfig();
24
+ const merged = { ...current, ...config };
25
+ if (!fs.existsSync(CONFIG_DIR)) {
26
+ fs.mkdirSync(CONFIG_DIR, { recursive: true });
27
+ }
28
+ // Atomic write: write to a temp file then rename so a concurrent read
29
+ // never sees a partially-written file.
30
+ const tmpFile = CONFIG_FILE + ".tmp";
31
+ fs.writeFileSync(tmpFile, JSON.stringify(merged, null, 2), {
32
+ mode: 0o600, // owner read/write only — protects the access token on Unix
33
+ });
34
+ fs.renameSync(tmpFile, CONFIG_FILE);
35
+ }
36
+ export function getToken() {
37
+ const config = loadConfig();
38
+ if (!config.accessToken) {
39
+ throw new Error("Not logged in. Run `appspacer login` first.");
40
+ }
41
+ return config.accessToken;
42
+ }
43
+ export function getApiUrl() {
44
+ return loadConfig().apiUrl;
45
+ }
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
package/dist/index.js ADDED
@@ -0,0 +1,25 @@
1
+ #!/usr/bin/env node
2
+ import { Command } from "commander";
3
+ import { loginCommand } from "./commands/login.js";
4
+ import { releaseCommand } from "./commands/release.js";
5
+ import { deploymentsCommand } from "./commands/deployments.js";
6
+ import { whoamiCommand } from "./commands/whoami.js";
7
+ import { releaseReactNativeCommand } from "./commands/release-react-native.js";
8
+ import { rollbackCommand } from "./commands/rollback.js";
9
+ import { setupCommand, undoCommand } from "./commands/setup.js";
10
+ import { vaultCommand } from "./commands/vault.js";
11
+ const program = new Command();
12
+ program
13
+ .name("appspacer")
14
+ .description("AppSpacer CLI — push OTA updates to React Native apps")
15
+ .version("1.0.0");
16
+ program.addCommand(loginCommand);
17
+ program.addCommand(releaseCommand);
18
+ program.addCommand(deploymentsCommand);
19
+ program.addCommand(whoamiCommand);
20
+ program.addCommand(releaseReactNativeCommand);
21
+ program.addCommand(rollbackCommand);
22
+ program.addCommand(setupCommand);
23
+ program.addCommand(undoCommand);
24
+ program.addCommand(vaultCommand);
25
+ program.parse();
@@ -0,0 +1,8 @@
1
+ /**
2
+ * Run the React Native bundler for the given platform.
3
+ * Output goes into `outputDir`.
4
+ *
5
+ * When `includeAssets` is false (default), only the JS bundle is produced.
6
+ * This keeps OTA updates small since assets are already in the native binary.
7
+ */
8
+ export declare function runReactNativeBundle(platform: "android" | "ios", outputDir: string, includeAssets?: boolean): void;
@@ -0,0 +1,59 @@
1
+ import { execSync } from "child_process";
2
+ import fs from "fs";
3
+ import path from "path";
4
+ import chalk from "chalk";
5
+ /**
6
+ * Run the React Native bundler for the given platform.
7
+ * Output goes into `outputDir`.
8
+ *
9
+ * When `includeAssets` is false (default), only the JS bundle is produced.
10
+ * This keeps OTA updates small since assets are already in the native binary.
11
+ */
12
+ export function runReactNativeBundle(platform, outputDir, includeAssets = false) {
13
+ // ── Validation ──────────────────────────────────────────
14
+ const packageJsonPath = path.resolve("package.json");
15
+ if (!fs.existsSync(packageJsonPath)) {
16
+ console.error(chalk.red("✗ Error: No package.json found. Run this command in your React Native project root."));
17
+ process.exit(1);
18
+ }
19
+ const pkg = JSON.parse(fs.readFileSync(packageJsonPath, "utf-8"));
20
+ const isReactNative = pkg.dependencies?.["react-native"] || pkg.devDependencies?.["react-native"];
21
+ if (!isReactNative) {
22
+ console.error(chalk.red("✗ Error: This is not a React Native project (missing 'react-native' dependency)."));
23
+ process.exit(1);
24
+ }
25
+ const entryFile = "index.js";
26
+ if (!fs.existsSync(path.resolve(entryFile))) {
27
+ console.error(chalk.red(`✗ Error: Entry file "${entryFile}" not found.`));
28
+ process.exit(1);
29
+ }
30
+ // Ensure output directory exists
31
+ if (!fs.existsSync(outputDir)) {
32
+ fs.mkdirSync(outputDir, { recursive: true });
33
+ }
34
+ const bundleOutput = platform === "android"
35
+ ? path.join(outputDir, "index.android.bundle")
36
+ : path.join(outputDir, "main.jsbundle");
37
+ const parts = [
38
+ "npx --no-install react-native bundle",
39
+ `--platform ${platform}`,
40
+ "--dev false",
41
+ "--reset-cache",
42
+ `--entry-file ${entryFile}`,
43
+ `--bundle-output ${bundleOutput}`,
44
+ ];
45
+ if (includeAssets) {
46
+ // Put assets in a subdirectory to keep things organized
47
+ const assetsDir = path.join(outputDir, "assets");
48
+ if (!fs.existsSync(assetsDir)) {
49
+ fs.mkdirSync(assetsDir, { recursive: true });
50
+ }
51
+ parts.push(`--assets-dest ${assetsDir}`);
52
+ }
53
+ try {
54
+ execSync(parts.join(" "), { stdio: "inherit" });
55
+ }
56
+ catch (err) {
57
+ throw new Error("React Native bundling failed. See above for details.");
58
+ }
59
+ }
@@ -0,0 +1,4 @@
1
+ /**
2
+ * Compute a SHA-256 hex digest for the file at `filePath`.
3
+ */
4
+ export declare function computeFileHash(filePath: string): string;
@@ -0,0 +1,9 @@
1
+ import crypto from "crypto";
2
+ import fs from "fs";
3
+ /**
4
+ * Compute a SHA-256 hex digest for the file at `filePath`.
5
+ */
6
+ export function computeFileHash(filePath) {
7
+ const fileBuffer = fs.readFileSync(filePath);
8
+ return crypto.createHash("sha256").update(fileBuffer).digest("hex");
9
+ }
@@ -0,0 +1,19 @@
1
+ /**
2
+ * Returns a formatted step label for use with ora spinners.
3
+ * e.g. [1/4] Bundling React Native app...
4
+ */
5
+ export declare function stepLabel(n: number, total: number, text: string): string;
6
+ /**
7
+ * Prints a branded header box for major commands.
8
+ */
9
+ export declare function printHeader(title: string): void;
10
+ /**
11
+ * Prints a formatted success summary section after a command completes.
12
+ * @param title - headline text shown next to the green ✓
13
+ * @param rows - array of [label, value] pairs displayed below
14
+ */
15
+ export declare function printSummary(title: string, rows: Array<[string, string]>): void;
16
+ /**
17
+ * Prints a key/value info line — used for lightweight status messages.
18
+ */
19
+ export declare function infoLine(label: string, value: string): void;
@@ -0,0 +1,43 @@
1
+ import chalk from "chalk";
2
+ const BRAND = "#7C3AED";
3
+ /**
4
+ * Returns a formatted step label for use with ora spinners.
5
+ * e.g. [1/4] Bundling React Native app...
6
+ */
7
+ export function stepLabel(n, total, text) {
8
+ return `${chalk.bold.dim(`[${n}/${total}]`)} ${text}`;
9
+ }
10
+ /**
11
+ * Prints a branded header box for major commands.
12
+ */
13
+ export function printHeader(title) {
14
+ const WIDTH = 38;
15
+ const inner = ` ${title} `;
16
+ const padded = inner.padStart(Math.floor((WIDTH + inner.length) / 2)).padEnd(WIDTH);
17
+ console.log("");
18
+ console.log(chalk.bold.hex(BRAND)(" ╔" + "═".repeat(WIDTH) + "╗"));
19
+ console.log(chalk.bold.hex(BRAND)(" ║") + chalk.bold.white(padded) + chalk.bold.hex(BRAND)("║"));
20
+ console.log(chalk.bold.hex(BRAND)(" ╚" + "═".repeat(WIDTH) + "╝"));
21
+ console.log("");
22
+ }
23
+ /**
24
+ * Prints a formatted success summary section after a command completes.
25
+ * @param title - headline text shown next to the green ✓
26
+ * @param rows - array of [label, value] pairs displayed below
27
+ */
28
+ export function printSummary(title, rows) {
29
+ const labelWidth = Math.max(...rows.map(([l]) => l.length));
30
+ console.log("");
31
+ console.log(chalk.green(" ✓ ") + chalk.bold.green(title));
32
+ console.log(chalk.dim(" " + "─".repeat(46)));
33
+ for (const [label, value] of rows) {
34
+ console.log(` ${chalk.dim((label + ":").padEnd(labelWidth + 1))} ${chalk.white(value)}`);
35
+ }
36
+ console.log("");
37
+ }
38
+ /**
39
+ * Prints a key/value info line — used for lightweight status messages.
40
+ */
41
+ export function infoLine(label, value) {
42
+ console.log(` ${chalk.dim(label + ":")} ${chalk.white(value)}`);
43
+ }
@@ -0,0 +1,25 @@
1
+ declare const VALID_PLATFORMS: readonly ["android", "ios"];
2
+ export type Platform = typeof VALID_PLATFORMS[number];
3
+ /**
4
+ * Validates the platform argument and exits with a clear message if invalid.
5
+ */
6
+ export declare function requireValidPlatform(platform: string): asserts platform is Platform;
7
+ /**
8
+ * Validates that a deployment name contains only safe characters,
9
+ * preventing malformed API requests from confusing error responses.
10
+ */
11
+ export declare function requireValidDeployment(name: string): void;
12
+ /**
13
+ * Validates that a semver or semver-range string is plausibly well-formed
14
+ * before sending it to the server.
15
+ */
16
+ export declare function requireValidVersion(version: string, flag?: string): void;
17
+ /**
18
+ * Exits with a clear error if a required string option is empty.
19
+ */
20
+ export declare function requireNonEmpty(value: string, label: string): void;
21
+ /**
22
+ * Validates that rollout is an integer between 0 and 100.
23
+ */
24
+ export declare function requireValidRollout(value: string): number;
25
+ export {};
@@ -0,0 +1,65 @@
1
+ import chalk from "chalk";
2
+ const VALID_PLATFORMS = ["android", "ios"];
3
+ /**
4
+ * Validates the platform argument and exits with a clear message if invalid.
5
+ */
6
+ export function requireValidPlatform(platform) {
7
+ if (!VALID_PLATFORMS.includes(platform)) {
8
+ console.error(chalk.red(` ✗ Invalid platform "${platform}".`) +
9
+ chalk.dim(` Expected: ${VALID_PLATFORMS.join(" | ")}`));
10
+ process.exit(1);
11
+ }
12
+ }
13
+ /**
14
+ * Validates that a deployment name contains only safe characters,
15
+ * preventing malformed API requests from confusing error responses.
16
+ */
17
+ export function requireValidDeployment(name) {
18
+ if (!name || name.trim().length === 0) {
19
+ console.error(chalk.red(" ✗ Deployment name cannot be empty."));
20
+ process.exit(1);
21
+ }
22
+ if (!/^[a-zA-Z0-9_\- ]+$/.test(name)) {
23
+ console.error(chalk.red(` ✗ Invalid deployment name "${name}".`) +
24
+ chalk.dim(" Only letters, numbers, spaces, hyphens, and underscores are allowed."));
25
+ process.exit(1);
26
+ }
27
+ }
28
+ /**
29
+ * Validates that a semver or semver-range string is plausibly well-formed
30
+ * before sending it to the server.
31
+ */
32
+ export function requireValidVersion(version, flag = "-t") {
33
+ if (!version || version.trim().length === 0) {
34
+ console.error(chalk.red(` ✗ Target version (${flag}) cannot be empty.`));
35
+ process.exit(1);
36
+ }
37
+ // Accept: 1.0.0 / ^1.0.0 / ~1.0 / >=1.0.0 <2.0.0 / *
38
+ const semverLike = /^[\^~><=*\s\d.]+$/;
39
+ if (!semverLike.test(version)) {
40
+ console.error(chalk.red(` ✗ Invalid version "${version}".`) +
41
+ chalk.dim(` Expected a semver string like 1.0.0 or ^1.2.0 (${flag})`));
42
+ process.exit(1);
43
+ }
44
+ }
45
+ /**
46
+ * Exits with a clear error if a required string option is empty.
47
+ */
48
+ export function requireNonEmpty(value, label) {
49
+ if (!value || value.trim().length === 0) {
50
+ console.error(chalk.red(` ✗ ${label} cannot be empty.`));
51
+ process.exit(1);
52
+ }
53
+ }
54
+ /**
55
+ * Validates that rollout is an integer between 0 and 100.
56
+ */
57
+ export function requireValidRollout(value) {
58
+ const n = parseInt(value, 10);
59
+ if (isNaN(n) || n < 0 || n > 100) {
60
+ console.error(chalk.red(` ✗ Invalid rollout "${value}".`) +
61
+ chalk.dim(" Expected an integer between 0 and 100."));
62
+ process.exit(1);
63
+ }
64
+ return n;
65
+ }
@@ -0,0 +1,5 @@
1
+ /**
2
+ * Zip the contents of `sourceDir` into `outputPath`.
3
+ * Returns a promise that resolves when the archive is finalized.
4
+ */
5
+ export declare function createZip(sourceDir: string, outputPath: string): Promise<void>;
@@ -0,0 +1,17 @@
1
+ import archiver from "archiver";
2
+ import fs from "fs";
3
+ /**
4
+ * Zip the contents of `sourceDir` into `outputPath`.
5
+ * Returns a promise that resolves when the archive is finalized.
6
+ */
7
+ export function createZip(sourceDir, outputPath) {
8
+ return new Promise((resolve, reject) => {
9
+ const output = fs.createWriteStream(outputPath);
10
+ const archive = archiver("zip", { zlib: { level: 9 } });
11
+ output.on("close", () => resolve());
12
+ archive.on("error", (err) => reject(err));
13
+ archive.pipe(output);
14
+ archive.directory(sourceDir, false);
15
+ archive.finalize();
16
+ });
17
+ }