@aalexis.dev/cfx 1.0.2
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 +162 -0
- package/bin/crypto.js +68 -0
- package/bin/index.js +252 -0
- package/bin/prompt.js +73 -0
- package/bin/runner.js +75 -0
- package/bin/storage.js +159 -0
- package/bin/types.js +1 -0
- package/bin/utils.js +69 -0
- package/package.json +44 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
# ⚡ cfx (`@aalexis.dev/cfx`)
|
|
2
|
+
|
|
3
|
+
> **Ultra-lightweight, secure CLI wrapper for managing multiple Cloudflare accounts with Wrangler.**
|
|
4
|
+
>
|
|
5
|
+
> 🔒 Encrypted local storage with `AES-256-GCM` | ⚡ 0 runtime dependencies | 🌐 Cross-platform (Windows, macOS, Linux).
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## 🚀 Features
|
|
10
|
+
|
|
11
|
+
- **Multi-Account Switching:** Switch between Cloudflare accounts instantly using `--acc=<name>` or `-a <name>`.
|
|
12
|
+
- **Batch Execution:** Run commands sequentially across all registered accounts with `--all`.
|
|
13
|
+
- **Transparent Wrangler Proxy:** Forwards all arguments, flags, and real-time outputs (`stdio: inherit`) directly to the `wrangler` binary.
|
|
14
|
+
- **Zero Runtime Dependencies:** Built exclusively on Node.js native modules (`node:crypto`, `node:child_process`, `node:readline`, `node:fs`, `node:os`).
|
|
15
|
+
- **Cryptographic Security:** Credentials (`accountId` and `apiToken`) are encrypted locally with `AES-256-GCM` using a 256-bit master key with strict permissions (`0600` / `0700`).
|
|
16
|
+
- **Cross-Platform:** Works seamlessly across Windows (CMD, PowerShell, Windows Terminal), macOS, and Linux.
|
|
17
|
+
|
|
18
|
+
---
|
|
19
|
+
|
|
20
|
+
## 🛠️ Global Installation (Recommended)
|
|
21
|
+
|
|
22
|
+
Install globally once to use the clean and short `cfx` command anywhere in your terminal:
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
# With pnpm
|
|
26
|
+
pnpm add -g @aalexis.dev/cfx
|
|
27
|
+
|
|
28
|
+
# With npm
|
|
29
|
+
npm install -g @aalexis.dev/cfx
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
Once installed globally, you can simply run:
|
|
33
|
+
```bash
|
|
34
|
+
cfx --add
|
|
35
|
+
cfx --list
|
|
36
|
+
cfx --acc=production deploy
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
---
|
|
40
|
+
|
|
41
|
+
## 📦 Direct Execution (Without Installation)
|
|
42
|
+
|
|
43
|
+
You can also run `@aalexis.dev/cfx` directly using your preferred package manager:
|
|
44
|
+
|
|
45
|
+
```bash
|
|
46
|
+
# With pnpm dlx
|
|
47
|
+
pnpm dlx @aalexis.dev/cfx --add
|
|
48
|
+
pnpm dlx @aalexis.dev/cfx --list
|
|
49
|
+
pnpm dlx @aalexis.dev/cfx --acc=production deploy
|
|
50
|
+
|
|
51
|
+
# With npx
|
|
52
|
+
npx @aalexis.dev/cfx --acc=staging tail
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
---
|
|
56
|
+
|
|
57
|
+
## 📖 Command Reference
|
|
58
|
+
|
|
59
|
+
### 1. Register an Account (`--add`)
|
|
60
|
+
Starts an interactive prompt to enter the account identifier name, **Cloudflare Account ID**, and **Cloudflare API Token** (the token input is securely masked in the terminal):
|
|
61
|
+
|
|
62
|
+
```bash
|
|
63
|
+
cfx --add
|
|
64
|
+
# or
|
|
65
|
+
cfx add
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
### 2. List Registered Accounts (`--list`)
|
|
69
|
+
Displays a formatted list of all configured accounts:
|
|
70
|
+
|
|
71
|
+
```bash
|
|
72
|
+
cfx --list
|
|
73
|
+
# or
|
|
74
|
+
cfx list
|
|
75
|
+
# or
|
|
76
|
+
cfx ls
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
### 3. Remove an Account (`--remove`)
|
|
80
|
+
Deletes an account from your local encrypted storage:
|
|
81
|
+
|
|
82
|
+
```bash
|
|
83
|
+
cfx --remove <name>
|
|
84
|
+
# or
|
|
85
|
+
cfx rm <name>
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
### 4. Run Wrangler on a Specific Account (`--acc` or `-a`)
|
|
89
|
+
Decrypts credentials in memory, injects `CLOUDFLARE_ACCOUNT_ID` and `CLOUDFLARE_API_TOKEN`, and invokes `wrangler`:
|
|
90
|
+
|
|
91
|
+
```bash
|
|
92
|
+
# Deploy a Worker
|
|
93
|
+
cfx --acc=production deploy
|
|
94
|
+
|
|
95
|
+
# Stream real-time logs
|
|
96
|
+
cfx -a staging tail
|
|
97
|
+
|
|
98
|
+
# Inspect current authenticated user and permissions
|
|
99
|
+
cfx --acc=client1 whoami
|
|
100
|
+
|
|
101
|
+
# Generate Cloudflare Worker / D1 / KV types
|
|
102
|
+
cfx --acc=prod types
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
### 5. Run Across All Accounts (`--all`)
|
|
106
|
+
Sequentially executes the specified command on every registered account and presents a summary table:
|
|
107
|
+
|
|
108
|
+
```bash
|
|
109
|
+
cfx --all deploy
|
|
110
|
+
cfx --all whoami
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
---
|
|
114
|
+
|
|
115
|
+
## 🔐 Security & Storage
|
|
116
|
+
|
|
117
|
+
- **Storage Location:** `~/.cfx/accounts.json`
|
|
118
|
+
- **Master Encryption Key:** `~/.cfx/.key` (generated locally with `crypto.randomBytes(32)` and protected with `0600` permissions on Unix systems).
|
|
119
|
+
- **Algorithm:** `AES-256-GCM` with a unique 16-byte initialization vector (`iv`) and 16-byte authentication tag (`authTag`) per encrypted value.
|
|
120
|
+
- Credentials are never transmitted over the network or stored in plain text.
|
|
121
|
+
|
|
122
|
+
---
|
|
123
|
+
|
|
124
|
+
## 💻 Local Development & Publishing
|
|
125
|
+
|
|
126
|
+
### 1. Clone and Install Dev Dependencies
|
|
127
|
+
```bash
|
|
128
|
+
git clone https://github.com/your-username/cfx.git
|
|
129
|
+
cd cfx
|
|
130
|
+
pnpm install
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
### 2. Build the Project (TypeScript)
|
|
134
|
+
```bash
|
|
135
|
+
pnpm build
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
### 3. Run Automated Tests
|
|
139
|
+
```bash
|
|
140
|
+
pnpm test
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
### 4. Link Locally for Testing
|
|
144
|
+
```bash
|
|
145
|
+
pnpm link --global
|
|
146
|
+
```
|
|
147
|
+
You can now test the `cfx` command directly from any terminal window.
|
|
148
|
+
|
|
149
|
+
### 5. Publish to the NPM Registry
|
|
150
|
+
```bash
|
|
151
|
+
# 1. Log in to your npm account (if not already authenticated)
|
|
152
|
+
pnpm login
|
|
153
|
+
|
|
154
|
+
# 2. Publish package with public access
|
|
155
|
+
pnpm publish --access public
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
---
|
|
159
|
+
|
|
160
|
+
## 📄 License
|
|
161
|
+
|
|
162
|
+
MIT © 2026
|
package/bin/crypto.js
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { randomBytes, createCipheriv, createDecipheriv } from 'node:crypto';
|
|
2
|
+
import { existsSync, readFileSync, writeFileSync, chmodSync } from 'node:fs';
|
|
3
|
+
const ALGORITHM = 'aes-256-gcm';
|
|
4
|
+
const IV_LENGTH = 16; // 128 bits
|
|
5
|
+
const KEY_LENGTH = 32; // 256 bits
|
|
6
|
+
let cachedMasterKey = null;
|
|
7
|
+
/**
|
|
8
|
+
* Retrieves or creates the master encryption key at the specified path.
|
|
9
|
+
* Enforces 0600 file permissions on POSIX systems.
|
|
10
|
+
*/
|
|
11
|
+
export function getOrCreateMasterKey(keyFilePath) {
|
|
12
|
+
if (cachedMasterKey) {
|
|
13
|
+
return cachedMasterKey;
|
|
14
|
+
}
|
|
15
|
+
if (existsSync(keyFilePath)) {
|
|
16
|
+
const rawKey = readFileSync(keyFilePath);
|
|
17
|
+
if (rawKey.length === KEY_LENGTH) {
|
|
18
|
+
cachedMasterKey = rawKey;
|
|
19
|
+
return cachedMasterKey;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
// Generate new 256-bit key
|
|
23
|
+
const newKey = randomBytes(KEY_LENGTH);
|
|
24
|
+
writeFileSync(keyFilePath, newKey, { mode: 0o600 });
|
|
25
|
+
// Enforce POSIX permissions when not on Windows
|
|
26
|
+
if (process.platform !== 'win32') {
|
|
27
|
+
try {
|
|
28
|
+
chmodSync(keyFilePath, 0o600);
|
|
29
|
+
}
|
|
30
|
+
catch {
|
|
31
|
+
// Ignore permission failures on unsupported filesystems
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
cachedMasterKey = newKey;
|
|
35
|
+
return cachedMasterKey;
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Encrypts a string using AES-256-GCM and the provided master key.
|
|
39
|
+
*/
|
|
40
|
+
export function encrypt(text, masterKey) {
|
|
41
|
+
const iv = randomBytes(IV_LENGTH);
|
|
42
|
+
const cipher = createCipheriv(ALGORITHM, masterKey, iv);
|
|
43
|
+
const encryptedBuffer = Buffer.concat([
|
|
44
|
+
cipher.update(text, 'utf8'),
|
|
45
|
+
cipher.final(),
|
|
46
|
+
]);
|
|
47
|
+
const authTag = cipher.getAuthTag();
|
|
48
|
+
return {
|
|
49
|
+
iv: iv.toString('hex'),
|
|
50
|
+
authTag: authTag.toString('hex'),
|
|
51
|
+
data: encryptedBuffer.toString('hex'),
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Decrypts an encrypted payload using AES-256-GCM and the master key.
|
|
56
|
+
*/
|
|
57
|
+
export function decrypt(payload, masterKey) {
|
|
58
|
+
const iv = Buffer.from(payload.iv, 'hex');
|
|
59
|
+
const authTag = Buffer.from(payload.authTag, 'hex');
|
|
60
|
+
const encryptedData = Buffer.from(payload.data, 'hex');
|
|
61
|
+
const decipher = createDecipheriv(ALGORITHM, masterKey, iv);
|
|
62
|
+
decipher.setAuthTag(authTag);
|
|
63
|
+
const decryptedBuffer = Buffer.concat([
|
|
64
|
+
decipher.update(encryptedData),
|
|
65
|
+
decipher.final(),
|
|
66
|
+
]);
|
|
67
|
+
return decryptedBuffer.toString('utf8');
|
|
68
|
+
}
|
package/bin/index.js
ADDED
|
@@ -0,0 +1,252 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { argv, exit } from 'node:process';
|
|
3
|
+
import { saveAccount, getStoredAccounts, removeAccount, getDecryptedAccount, getAllDecryptedAccounts, } from './storage.js';
|
|
4
|
+
import { runWrangler } from './runner.js';
|
|
5
|
+
import { askQuestion, askSecret } from './prompt.js';
|
|
6
|
+
import { colors, log, printBanner, printHelp } from './utils.js';
|
|
7
|
+
const VERSION = '1.0.2';
|
|
8
|
+
/**
|
|
9
|
+
* Parses command-line arguments natively
|
|
10
|
+
*/
|
|
11
|
+
function parseCliArgs(rawArgs) {
|
|
12
|
+
const args = rawArgs.slice(2);
|
|
13
|
+
if (args.length === 0) {
|
|
14
|
+
return { action: 'help', wranglerArgs: [] };
|
|
15
|
+
}
|
|
16
|
+
const firstArg = args[0];
|
|
17
|
+
// Help & version commands
|
|
18
|
+
if (args.includes('--help') || args.includes('-h') || firstArg === 'help') {
|
|
19
|
+
return { action: 'help', wranglerArgs: [] };
|
|
20
|
+
}
|
|
21
|
+
if (args.includes('--version') || args.includes('-v') || firstArg === 'version') {
|
|
22
|
+
return { action: 'version', wranglerArgs: [] };
|
|
23
|
+
}
|
|
24
|
+
// Action: Add account
|
|
25
|
+
if (args.includes('--add') || firstArg === 'add') {
|
|
26
|
+
return { action: 'add', wranglerArgs: [] };
|
|
27
|
+
}
|
|
28
|
+
// Action: List accounts
|
|
29
|
+
if (args.includes('--list') || firstArg === 'list' || firstArg === 'ls') {
|
|
30
|
+
return { action: 'list', wranglerArgs: [] };
|
|
31
|
+
}
|
|
32
|
+
// Action: Remove account
|
|
33
|
+
if (firstArg === 'remove' || firstArg === 'rm') {
|
|
34
|
+
return { action: 'remove', removeTarget: args[1], wranglerArgs: [] };
|
|
35
|
+
}
|
|
36
|
+
const removeFlagIndex = args.findIndex((arg) => arg.startsWith('--remove'));
|
|
37
|
+
if (removeFlagIndex !== -1) {
|
|
38
|
+
const flag = args[removeFlagIndex];
|
|
39
|
+
if (flag.includes('=')) {
|
|
40
|
+
return { action: 'remove', removeTarget: flag.split('=')[1], wranglerArgs: [] };
|
|
41
|
+
}
|
|
42
|
+
return { action: 'remove', removeTarget: args[removeFlagIndex + 1], wranglerArgs: [] };
|
|
43
|
+
}
|
|
44
|
+
// Action: Execute across all accounts (--all)
|
|
45
|
+
if (args.includes('--all')) {
|
|
46
|
+
const remaining = args.filter((a) => a !== '--all');
|
|
47
|
+
return { action: 'exec-all', wranglerArgs: remaining };
|
|
48
|
+
}
|
|
49
|
+
// Action: Execute on specific account (--acc=<name> or -a <name>)
|
|
50
|
+
let accountName;
|
|
51
|
+
const filteredWranglerArgs = [];
|
|
52
|
+
for (let i = 0; i < args.length; i++) {
|
|
53
|
+
const current = args[i];
|
|
54
|
+
if (current.startsWith('--acc=')) {
|
|
55
|
+
accountName = current.slice('--acc='.length);
|
|
56
|
+
}
|
|
57
|
+
else if (current === '--acc') {
|
|
58
|
+
accountName = args[++i];
|
|
59
|
+
}
|
|
60
|
+
else if (current.startsWith('-a=')) {
|
|
61
|
+
accountName = current.slice('-a='.length);
|
|
62
|
+
}
|
|
63
|
+
else if (current === '-a') {
|
|
64
|
+
accountName = args[++i];
|
|
65
|
+
}
|
|
66
|
+
else {
|
|
67
|
+
filteredWranglerArgs.push(current);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
if (accountName) {
|
|
71
|
+
return {
|
|
72
|
+
action: 'exec-single',
|
|
73
|
+
accountName,
|
|
74
|
+
wranglerArgs: filteredWranglerArgs,
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
// If no known flags were specified, show unknown action / help
|
|
78
|
+
return { action: 'unknown', wranglerArgs: args };
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* Interactive prompt to register a new Cloudflare account
|
|
82
|
+
*/
|
|
83
|
+
async function handleAddAccount() {
|
|
84
|
+
printBanner();
|
|
85
|
+
console.log(`${colors.bold}Register new Cloudflare account:${colors.reset}\n`);
|
|
86
|
+
const name = await askQuestion(`${colors.cyan}?${colors.reset} Account identifier name (e.g. personal, prod, client1): `);
|
|
87
|
+
if (!name) {
|
|
88
|
+
log.error('Account name cannot be empty.');
|
|
89
|
+
exit(1);
|
|
90
|
+
}
|
|
91
|
+
const accountId = await askQuestion(`${colors.cyan}?${colors.reset} Cloudflare Account ID: `);
|
|
92
|
+
if (!accountId) {
|
|
93
|
+
log.error('Account ID cannot be empty.');
|
|
94
|
+
exit(1);
|
|
95
|
+
}
|
|
96
|
+
const apiToken = await askSecret(`${colors.cyan}?${colors.reset} Cloudflare API Token (hidden): `);
|
|
97
|
+
if (!apiToken) {
|
|
98
|
+
log.error('API Token cannot be empty.');
|
|
99
|
+
exit(1);
|
|
100
|
+
}
|
|
101
|
+
try {
|
|
102
|
+
saveAccount(name, accountId, apiToken);
|
|
103
|
+
console.log();
|
|
104
|
+
log.success(`Account ${colors.bold}"${name}"${colors.reset} saved and encrypted successfully in ~/.cfx/accounts.json`);
|
|
105
|
+
console.log(`\n${colors.gray}You can now use it with:${colors.reset} ${colors.cyan}cfx --acc=${name} <command>${colors.reset}\n`);
|
|
106
|
+
}
|
|
107
|
+
catch (err) {
|
|
108
|
+
const errorMsg = err instanceof Error ? err.message : String(err);
|
|
109
|
+
log.error(`Failed to save account: ${errorMsg}`);
|
|
110
|
+
exit(1);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
114
|
+
* Lists all stored accounts
|
|
115
|
+
*/
|
|
116
|
+
function handleListAccounts() {
|
|
117
|
+
printBanner();
|
|
118
|
+
const accounts = getStoredAccounts();
|
|
119
|
+
if (accounts.length === 0) {
|
|
120
|
+
log.info('No Cloudflare accounts registered.');
|
|
121
|
+
console.log(`\nRegister your first account by running: ${colors.cyan}pnpm dlx @aalexis.dev/cfx --add${colors.reset}\n`);
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
console.log(`${colors.bold}Registered accounts (${accounts.length}):${colors.reset}\n`);
|
|
125
|
+
console.log(` ${colors.bold}${'NAME'.padEnd(20)} ${'CREATED AT'.padEnd(25)} STATUS${colors.reset}`);
|
|
126
|
+
console.log(` ${colors.gray}${'─'.repeat(55)}${colors.reset}`);
|
|
127
|
+
for (const acc of accounts) {
|
|
128
|
+
const date = acc.createdAt ? new Date(acc.createdAt).toLocaleString() : 'N/A';
|
|
129
|
+
console.log(` ${colors.cyan}${acc.name.padEnd(20)}${colors.reset} ${colors.gray}${date.padEnd(25)}${colors.reset} ${colors.green}🔒 Encrypted${colors.reset}`);
|
|
130
|
+
}
|
|
131
|
+
console.log(`\n${colors.gray}Usage:${colors.reset} ${colors.cyan}cfx --acc=<name> [wrangler command]${colors.reset}\n`);
|
|
132
|
+
}
|
|
133
|
+
/**
|
|
134
|
+
* Removes a stored account
|
|
135
|
+
*/
|
|
136
|
+
function handleRemoveAccount(targetName) {
|
|
137
|
+
if (!targetName) {
|
|
138
|
+
log.error('You must specify the name of the account to remove.');
|
|
139
|
+
console.log(`Example: ${colors.cyan}cfx --remove my-account${colors.reset}`);
|
|
140
|
+
exit(1);
|
|
141
|
+
}
|
|
142
|
+
const removed = removeAccount(targetName);
|
|
143
|
+
if (removed) {
|
|
144
|
+
log.success(`Account ${colors.bold}"${targetName}"${colors.reset} removed successfully.`);
|
|
145
|
+
}
|
|
146
|
+
else {
|
|
147
|
+
log.error(`No account found with the name "${targetName}".`);
|
|
148
|
+
const accounts = getStoredAccounts();
|
|
149
|
+
if (accounts.length > 0) {
|
|
150
|
+
console.log(`Available accounts: ${accounts.map((a) => colors.cyan + a.name + colors.reset).join(', ')}`);
|
|
151
|
+
}
|
|
152
|
+
exit(1);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
/**
|
|
156
|
+
* Executes wrangler command for a single account
|
|
157
|
+
*/
|
|
158
|
+
async function handleSingleAccountExec(accountName, wranglerArgs) {
|
|
159
|
+
const account = getDecryptedAccount(accountName);
|
|
160
|
+
if (!account) {
|
|
161
|
+
log.error(`Account ${colors.bold}"${accountName}"${colors.reset} was not found.`);
|
|
162
|
+
const accounts = getStoredAccounts();
|
|
163
|
+
if (accounts.length > 0) {
|
|
164
|
+
console.log(`\nRegistered accounts: ${accounts.map((a) => colors.cyan + a.name + colors.reset).join(', ')}`);
|
|
165
|
+
}
|
|
166
|
+
else {
|
|
167
|
+
console.log(`\nNo accounts registered yet. Add one with: ${colors.cyan}cfx --add${colors.reset}`);
|
|
168
|
+
}
|
|
169
|
+
exit(1);
|
|
170
|
+
}
|
|
171
|
+
const exitCode = await runWrangler({ account, args: wranglerArgs });
|
|
172
|
+
exit(exitCode);
|
|
173
|
+
}
|
|
174
|
+
/**
|
|
175
|
+
* Executes wrangler command sequentially across all registered accounts
|
|
176
|
+
*/
|
|
177
|
+
async function handleAllAccountsExec(wranglerArgs) {
|
|
178
|
+
const accounts = getAllDecryptedAccounts();
|
|
179
|
+
if (accounts.length === 0) {
|
|
180
|
+
log.error('No accounts registered to execute command.');
|
|
181
|
+
console.log(`Add an account with: ${colors.cyan}cfx --add${colors.reset}`);
|
|
182
|
+
exit(1);
|
|
183
|
+
}
|
|
184
|
+
printBanner();
|
|
185
|
+
console.log(`${colors.bold}Executing command across ${accounts.length} account(s):${colors.reset} ${colors.gray}wrangler ${wranglerArgs.join(' ')}${colors.reset}\n`);
|
|
186
|
+
const results = [];
|
|
187
|
+
for (let i = 0; i < accounts.length; i++) {
|
|
188
|
+
const acc = accounts[i];
|
|
189
|
+
const stepLabel = `${i + 1}/${accounts.length}`;
|
|
190
|
+
console.log(`\n${colors.bold}${colors.orange}──────────────────────────────────────────────────${colors.reset}`);
|
|
191
|
+
log.step(stepLabel, `Account: ${colors.bold}${colors.cyan}${acc.name}${colors.reset}`);
|
|
192
|
+
console.log(`${colors.bold}${colors.orange}──────────────────────────────────────────────────${colors.reset}\n`);
|
|
193
|
+
const code = await runWrangler({ account: acc, args: wranglerArgs });
|
|
194
|
+
results.push({ name: acc.name, success: code === 0, code });
|
|
195
|
+
}
|
|
196
|
+
// Summary
|
|
197
|
+
console.log(`\n${colors.bold}═══════════════════ SUMMARY ═══════════════════${colors.reset}`);
|
|
198
|
+
let hasFailures = false;
|
|
199
|
+
for (const res of results) {
|
|
200
|
+
if (res.success) {
|
|
201
|
+
console.log(` ${colors.green}✔${colors.reset} ${res.name.padEnd(20)} ${colors.green}Completed successfully${colors.reset}`);
|
|
202
|
+
}
|
|
203
|
+
else {
|
|
204
|
+
hasFailures = true;
|
|
205
|
+
console.log(` ${colors.red}✖${colors.reset} ${res.name.padEnd(20)} ${colors.red}Failed (code ${res.code})${colors.reset}`);
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
console.log(`${colors.bold}═══════════════════════════════════════════════${colors.reset}\n`);
|
|
209
|
+
if (hasFailures) {
|
|
210
|
+
exit(1);
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
/**
|
|
214
|
+
* Main CLI entry point
|
|
215
|
+
*/
|
|
216
|
+
async function main() {
|
|
217
|
+
const parsed = parseCliArgs(argv);
|
|
218
|
+
switch (parsed.action) {
|
|
219
|
+
case 'help':
|
|
220
|
+
printHelp();
|
|
221
|
+
break;
|
|
222
|
+
case 'version':
|
|
223
|
+
console.log(`cfx v${VERSION}`);
|
|
224
|
+
break;
|
|
225
|
+
case 'add':
|
|
226
|
+
await handleAddAccount();
|
|
227
|
+
break;
|
|
228
|
+
case 'list':
|
|
229
|
+
handleListAccounts();
|
|
230
|
+
break;
|
|
231
|
+
case 'remove':
|
|
232
|
+
handleRemoveAccount(parsed.removeTarget);
|
|
233
|
+
break;
|
|
234
|
+
case 'exec-single':
|
|
235
|
+
if (parsed.accountName) {
|
|
236
|
+
await handleSingleAccountExec(parsed.accountName, parsed.wranglerArgs);
|
|
237
|
+
}
|
|
238
|
+
break;
|
|
239
|
+
case 'exec-all':
|
|
240
|
+
await handleAllAccountsExec(parsed.wranglerArgs);
|
|
241
|
+
break;
|
|
242
|
+
case 'unknown':
|
|
243
|
+
default:
|
|
244
|
+
log.warn(`No recognized account flag or command specified.`);
|
|
245
|
+
printHelp();
|
|
246
|
+
exit(1);
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
main().catch((err) => {
|
|
250
|
+
log.error(`Unexpected error: ${err.message}`);
|
|
251
|
+
exit(1);
|
|
252
|
+
});
|
package/bin/prompt.js
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { createInterface } from 'node:readline';
|
|
2
|
+
import { stdin as input, stdout as output } from 'node:process';
|
|
3
|
+
import { colors } from './utils.js';
|
|
4
|
+
/**
|
|
5
|
+
* Prompts for standard text input
|
|
6
|
+
*/
|
|
7
|
+
export function askQuestion(query) {
|
|
8
|
+
const rl = createInterface({ input, output });
|
|
9
|
+
return new Promise((resolve) => {
|
|
10
|
+
rl.question(query, (answer) => {
|
|
11
|
+
rl.close();
|
|
12
|
+
resolve(answer.trim());
|
|
13
|
+
});
|
|
14
|
+
});
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Prompts for sensitive input (masks input if TTY is available)
|
|
18
|
+
*/
|
|
19
|
+
export function askSecret(query) {
|
|
20
|
+
return new Promise((resolve) => {
|
|
21
|
+
if (!input.isTTY) {
|
|
22
|
+
// Fallback for non-TTY / CI environments or pipes
|
|
23
|
+
const rl = createInterface({ input, output });
|
|
24
|
+
rl.question(query, (answer) => {
|
|
25
|
+
rl.close();
|
|
26
|
+
resolve(answer.trim());
|
|
27
|
+
});
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
output.write(query);
|
|
31
|
+
let buffer = '';
|
|
32
|
+
const onData = (chunk) => {
|
|
33
|
+
const str = chunk.toString();
|
|
34
|
+
for (let i = 0; i < str.length; i++) {
|
|
35
|
+
const char = str[i];
|
|
36
|
+
const code = char.charCodeAt(0);
|
|
37
|
+
// Enter (\r or \n)
|
|
38
|
+
if (code === 13 || code === 10) {
|
|
39
|
+
cleanup();
|
|
40
|
+
output.write('\n');
|
|
41
|
+
resolve(buffer.trim());
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
// Ctrl+C (interruption)
|
|
45
|
+
if (code === 3) {
|
|
46
|
+
cleanup();
|
|
47
|
+
output.write('\n');
|
|
48
|
+
process.exit(130);
|
|
49
|
+
}
|
|
50
|
+
// Backspace (127 or 8)
|
|
51
|
+
if (code === 127 || code === 8) {
|
|
52
|
+
if (buffer.length > 0) {
|
|
53
|
+
buffer = buffer.slice(0, -1);
|
|
54
|
+
output.write('\b \b');
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
else if (code >= 32) {
|
|
58
|
+
// Visible character -> mask with dot
|
|
59
|
+
buffer += char;
|
|
60
|
+
output.write(`${colors.gray}•${colors.reset}`);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
};
|
|
64
|
+
const cleanup = () => {
|
|
65
|
+
input.removeListener('data', onData);
|
|
66
|
+
input.setRawMode(false);
|
|
67
|
+
input.pause();
|
|
68
|
+
};
|
|
69
|
+
input.setRawMode(true);
|
|
70
|
+
input.resume();
|
|
71
|
+
input.on('data', onData);
|
|
72
|
+
});
|
|
73
|
+
}
|
package/bin/runner.js
ADDED
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { spawn } from 'node:child_process';
|
|
2
|
+
import { join, dirname, delimiter } from 'node:path';
|
|
3
|
+
import { colors, log } from './utils.js';
|
|
4
|
+
/**
|
|
5
|
+
* Builds an augmented PATH that includes local and parent monorepo node_modules/.bin directories.
|
|
6
|
+
* This allows cfx to detect locally installed wrangler instances seamlessly.
|
|
7
|
+
*/
|
|
8
|
+
function getAugmentedPath() {
|
|
9
|
+
const binDirs = [];
|
|
10
|
+
let current = process.cwd();
|
|
11
|
+
while (true) {
|
|
12
|
+
binDirs.push(join(current, 'node_modules', '.bin'));
|
|
13
|
+
const parent = dirname(current);
|
|
14
|
+
if (parent === current)
|
|
15
|
+
break;
|
|
16
|
+
current = parent;
|
|
17
|
+
}
|
|
18
|
+
const existingPath = process.env.PATH || '';
|
|
19
|
+
return [...binDirs, existingPath].filter(Boolean).join(delimiter);
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Runs wrangler by injecting account credentials via environment variables.
|
|
23
|
+
* Transparently supports Windows, macOS, and Linux, detecting both local and global wrangler installations.
|
|
24
|
+
*/
|
|
25
|
+
export function runWrangler({ account, args }) {
|
|
26
|
+
return new Promise((resolve) => {
|
|
27
|
+
const isWindows = process.platform === 'win32';
|
|
28
|
+
const env = {
|
|
29
|
+
...process.env,
|
|
30
|
+
PATH: getAugmentedPath(),
|
|
31
|
+
CLOUDFLARE_ACCOUNT_ID: account.accountId,
|
|
32
|
+
CLOUDFLARE_API_TOKEN: account.apiToken,
|
|
33
|
+
};
|
|
34
|
+
// Main command
|
|
35
|
+
const command = 'wrangler';
|
|
36
|
+
// On Windows, use shell: true to resolve .cmd / .ps1 shims automatically
|
|
37
|
+
const child = spawn(command, args, {
|
|
38
|
+
stdio: 'inherit',
|
|
39
|
+
shell: isWindows,
|
|
40
|
+
env,
|
|
41
|
+
});
|
|
42
|
+
// Forward termination signals to child process
|
|
43
|
+
const sigintHandler = () => {
|
|
44
|
+
if (child.pid && !isWindows) {
|
|
45
|
+
child.kill('SIGINT');
|
|
46
|
+
}
|
|
47
|
+
};
|
|
48
|
+
const sigtermHandler = () => {
|
|
49
|
+
if (child.pid && !isWindows) {
|
|
50
|
+
child.kill('SIGTERM');
|
|
51
|
+
}
|
|
52
|
+
};
|
|
53
|
+
process.on('SIGINT', sigintHandler);
|
|
54
|
+
process.on('SIGTERM', sigtermHandler);
|
|
55
|
+
child.on('error', (err) => {
|
|
56
|
+
process.off('SIGINT', sigintHandler);
|
|
57
|
+
process.off('SIGTERM', sigtermHandler);
|
|
58
|
+
if (err.code === 'ENOENT') {
|
|
59
|
+
log.error(`Command ${colors.bold}'wrangler'${colors.reset} was not found in your project or global environment.\n` +
|
|
60
|
+
` You can install it in this project with: ${colors.cyan}pnpm add -D wrangler${colors.reset}\n` +
|
|
61
|
+
` Or install it globally with: ${colors.cyan}pnpm add -g wrangler${colors.reset}`);
|
|
62
|
+
resolve(1);
|
|
63
|
+
}
|
|
64
|
+
else {
|
|
65
|
+
log.error(`Failed to execute wrangler: ${err.message}`);
|
|
66
|
+
resolve(1);
|
|
67
|
+
}
|
|
68
|
+
});
|
|
69
|
+
child.on('close', (code) => {
|
|
70
|
+
process.off('SIGINT', sigintHandler);
|
|
71
|
+
process.off('SIGTERM', sigtermHandler);
|
|
72
|
+
resolve(code ?? 0);
|
|
73
|
+
});
|
|
74
|
+
});
|
|
75
|
+
}
|
package/bin/storage.js
ADDED
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
import { homedir } from 'node:os';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync, chmodSync } from 'node:fs';
|
|
4
|
+
import { getOrCreateMasterKey, encrypt, decrypt } from './crypto.js';
|
|
5
|
+
export const CFX_DIR = join(homedir(), '.cfx');
|
|
6
|
+
export const ACCOUNTS_FILE = join(CFX_DIR, 'accounts.json');
|
|
7
|
+
export const KEY_FILE = join(CFX_DIR, '.key');
|
|
8
|
+
/**
|
|
9
|
+
* Initializes the ~/.cfx configuration directory securely
|
|
10
|
+
*/
|
|
11
|
+
export function ensureConfigDirectory() {
|
|
12
|
+
if (!existsSync(CFX_DIR)) {
|
|
13
|
+
mkdirSync(CFX_DIR, { recursive: true, mode: 0o700 });
|
|
14
|
+
if (process.platform !== 'win32') {
|
|
15
|
+
try {
|
|
16
|
+
chmodSync(CFX_DIR, 0o700);
|
|
17
|
+
}
|
|
18
|
+
catch {
|
|
19
|
+
// Ignore errors if the platform does not support chmod permissions
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Retrieves the local master encryption key
|
|
26
|
+
*/
|
|
27
|
+
function getMasterKey() {
|
|
28
|
+
ensureConfigDirectory();
|
|
29
|
+
return getOrCreateMasterKey(KEY_FILE);
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Reads the accounts.json file from ~/.cfx/
|
|
33
|
+
*/
|
|
34
|
+
export function getStoredAccounts() {
|
|
35
|
+
ensureConfigDirectory();
|
|
36
|
+
if (!existsSync(ACCOUNTS_FILE)) {
|
|
37
|
+
return [];
|
|
38
|
+
}
|
|
39
|
+
try {
|
|
40
|
+
const raw = readFileSync(ACCOUNTS_FILE, 'utf8');
|
|
41
|
+
if (!raw.trim()) {
|
|
42
|
+
return [];
|
|
43
|
+
}
|
|
44
|
+
const parsed = JSON.parse(raw);
|
|
45
|
+
if (Array.isArray(parsed)) {
|
|
46
|
+
return parsed;
|
|
47
|
+
}
|
|
48
|
+
return [];
|
|
49
|
+
}
|
|
50
|
+
catch {
|
|
51
|
+
return [];
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Saves accounts array into accounts.json
|
|
56
|
+
*/
|
|
57
|
+
function saveStoredAccounts(accounts) {
|
|
58
|
+
ensureConfigDirectory();
|
|
59
|
+
writeFileSync(ACCOUNTS_FILE, JSON.stringify(accounts, null, 2), {
|
|
60
|
+
encoding: 'utf8',
|
|
61
|
+
mode: 0o600,
|
|
62
|
+
});
|
|
63
|
+
if (process.platform !== 'win32') {
|
|
64
|
+
try {
|
|
65
|
+
chmodSync(ACCOUNTS_FILE, 0o600);
|
|
66
|
+
}
|
|
67
|
+
catch {
|
|
68
|
+
// Ignore permission errors on unsupported filesystems
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Adds or updates an encrypted account
|
|
74
|
+
*/
|
|
75
|
+
export function saveAccount(name, accountId, apiToken) {
|
|
76
|
+
const masterKey = getMasterKey();
|
|
77
|
+
const accounts = getStoredAccounts();
|
|
78
|
+
const encryptedAccountId = encrypt(accountId.trim(), masterKey);
|
|
79
|
+
const encryptedApiToken = encrypt(apiToken.trim(), masterKey);
|
|
80
|
+
const existingIndex = accounts.findIndex((acc) => acc.name.toLowerCase() === name.trim().toLowerCase());
|
|
81
|
+
const now = new Date().toISOString();
|
|
82
|
+
if (existingIndex >= 0) {
|
|
83
|
+
accounts[existingIndex] = {
|
|
84
|
+
...accounts[existingIndex],
|
|
85
|
+
name: name.trim(),
|
|
86
|
+
accountId: encryptedAccountId,
|
|
87
|
+
apiToken: encryptedApiToken,
|
|
88
|
+
updatedAt: now,
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
else {
|
|
92
|
+
accounts.push({
|
|
93
|
+
name: name.trim(),
|
|
94
|
+
accountId: encryptedAccountId,
|
|
95
|
+
apiToken: encryptedApiToken,
|
|
96
|
+
createdAt: now,
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
saveStoredAccounts(accounts);
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* Removes an account by name
|
|
103
|
+
*/
|
|
104
|
+
export function removeAccount(name) {
|
|
105
|
+
const accounts = getStoredAccounts();
|
|
106
|
+
const targetName = name.trim().toLowerCase();
|
|
107
|
+
const filtered = accounts.filter((acc) => acc.name.toLowerCase() !== targetName);
|
|
108
|
+
if (filtered.length === accounts.length) {
|
|
109
|
+
return false;
|
|
110
|
+
}
|
|
111
|
+
saveStoredAccounts(filtered);
|
|
112
|
+
return true;
|
|
113
|
+
}
|
|
114
|
+
/**
|
|
115
|
+
* Retrieves and decrypts a specific account by name
|
|
116
|
+
*/
|
|
117
|
+
export function getDecryptedAccount(name) {
|
|
118
|
+
const accounts = getStoredAccounts();
|
|
119
|
+
const targetName = name.trim().toLowerCase();
|
|
120
|
+
const found = accounts.find((acc) => acc.name.toLowerCase() === targetName);
|
|
121
|
+
if (!found) {
|
|
122
|
+
return null;
|
|
123
|
+
}
|
|
124
|
+
try {
|
|
125
|
+
const masterKey = getMasterKey();
|
|
126
|
+
const accountId = decrypt(found.accountId, masterKey);
|
|
127
|
+
const apiToken = decrypt(found.apiToken, masterKey);
|
|
128
|
+
return {
|
|
129
|
+
name: found.name,
|
|
130
|
+
accountId,
|
|
131
|
+
apiToken,
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
catch {
|
|
135
|
+
throw new Error(`Could not decrypt credentials for account "${found.name}". Check integrity of ~/.cfx/.key`);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
/**
|
|
139
|
+
* Retrieves and decrypts all registered accounts
|
|
140
|
+
*/
|
|
141
|
+
export function getAllDecryptedAccounts() {
|
|
142
|
+
const accounts = getStoredAccounts();
|
|
143
|
+
if (accounts.length === 0) {
|
|
144
|
+
return [];
|
|
145
|
+
}
|
|
146
|
+
const masterKey = getMasterKey();
|
|
147
|
+
return accounts.map((acc) => {
|
|
148
|
+
try {
|
|
149
|
+
return {
|
|
150
|
+
name: acc.name,
|
|
151
|
+
accountId: decrypt(acc.accountId, masterKey),
|
|
152
|
+
apiToken: decrypt(acc.apiToken, masterKey),
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
catch {
|
|
156
|
+
throw new Error(`Failed to decrypt account "${acc.name}". Check ~/.cfx/.key`);
|
|
157
|
+
}
|
|
158
|
+
});
|
|
159
|
+
}
|
package/bin/types.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/bin/utils.js
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { env, stdout } from 'node:process';
|
|
2
|
+
const isColorSupported = !env.NO_COLOR && (stdout.isTTY || env.FORCE_COLOR !== undefined);
|
|
3
|
+
export const colors = {
|
|
4
|
+
reset: isColorSupported ? '\x1b[0m' : '',
|
|
5
|
+
bold: isColorSupported ? '\x1b[1m' : '',
|
|
6
|
+
dim: isColorSupported ? '\x1b[2m' : '',
|
|
7
|
+
italic: isColorSupported ? '\x1b[3m' : '',
|
|
8
|
+
underline: isColorSupported ? '\x1b[4m' : '',
|
|
9
|
+
// Colors
|
|
10
|
+
cyan: isColorSupported ? '\x1b[36m' : '',
|
|
11
|
+
green: isColorSupported ? '\x1b[32m' : '',
|
|
12
|
+
yellow: isColorSupported ? '\x1b[33m' : '',
|
|
13
|
+
red: isColorSupported ? '\x1b[31m' : '',
|
|
14
|
+
blue: isColorSupported ? '\x1b[34m' : '',
|
|
15
|
+
magenta: isColorSupported ? '\x1b[35m' : '',
|
|
16
|
+
gray: isColorSupported ? '\x1b[90m' : '',
|
|
17
|
+
orange: isColorSupported ? '\x1b[38;5;208m' : '',
|
|
18
|
+
};
|
|
19
|
+
export const log = {
|
|
20
|
+
info: (msg) => console.log(`${colors.cyan}ℹ${colors.reset} ${msg}`),
|
|
21
|
+
success: (msg) => console.log(`${colors.green}✔${colors.reset} ${msg}`),
|
|
22
|
+
warn: (msg) => console.warn(`${colors.yellow}⚠${colors.reset} ${msg}`),
|
|
23
|
+
error: (msg) => console.error(`${colors.red}✖${colors.reset} ${msg}`),
|
|
24
|
+
step: (step, msg) => console.log(`${colors.bold}${colors.orange}[${step}]${colors.reset} ${msg}`),
|
|
25
|
+
};
|
|
26
|
+
export function printBanner() {
|
|
27
|
+
console.log(`${colors.bold}${colors.orange}⚡ cfx${colors.reset} ${colors.gray}- Multi-account Cloudflare Wrangler Proxy${colors.reset}\n`);
|
|
28
|
+
}
|
|
29
|
+
export function printHelp() {
|
|
30
|
+
console.log(`
|
|
31
|
+
${colors.bold}${colors.orange}⚡ cfx${colors.reset} ${colors.gray}- Ultra-lightweight Cloudflare Multi-Account Wrangler CLI${colors.reset}
|
|
32
|
+
|
|
33
|
+
${colors.bold}USAGE:${colors.reset}
|
|
34
|
+
${colors.cyan}cfx${colors.reset} [cfx options] [wrangler commands/args...]
|
|
35
|
+
|
|
36
|
+
${colors.bold}ACCOUNT MANAGEMENT:${colors.reset}
|
|
37
|
+
${colors.cyan}--add${colors.reset}, ${colors.cyan}add${colors.reset} Register a new account interactively
|
|
38
|
+
${colors.cyan}--list${colors.reset}, ${colors.cyan}list${colors.reset}, ${colors.cyan}ls${colors.reset} List all registered accounts
|
|
39
|
+
${colors.cyan}--remove${colors.reset} <name>, ${colors.cyan}rm${colors.reset} <name> Remove a registered account
|
|
40
|
+
|
|
41
|
+
${colors.bold}WRANGLER EXECUTION:${colors.reset}
|
|
42
|
+
${colors.cyan}--acc=<name>${colors.reset} <args...> Run wrangler using account credentials
|
|
43
|
+
${colors.cyan}-a <name>${colors.reset} <args...> Short alias for --acc=<name>
|
|
44
|
+
${colors.cyan}--all${colors.reset} <args...> Run wrangler sequentially across ALL registered accounts
|
|
45
|
+
|
|
46
|
+
${colors.bold}GENERAL:${colors.reset}
|
|
47
|
+
${colors.cyan}--help${colors.reset}, ${colors.cyan}-h${colors.reset} Show this help message
|
|
48
|
+
${colors.cyan}--version${colors.reset}, ${colors.cyan}-v${colors.reset} Show cfx version
|
|
49
|
+
|
|
50
|
+
${colors.bold}EXAMPLES:${colors.reset}
|
|
51
|
+
${colors.gray}# Register an account${colors.reset}
|
|
52
|
+
$ ${colors.cyan}pnpm dlx @aalexis.dev/cfx --add${colors.reset}
|
|
53
|
+
|
|
54
|
+
${colors.gray}# List configured accounts${colors.reset}
|
|
55
|
+
$ ${colors.cyan}pnpm dlx @aalexis.dev/cfx --list${colors.reset}
|
|
56
|
+
|
|
57
|
+
${colors.gray}# Deploy a Worker to a specific account${colors.reset}
|
|
58
|
+
$ ${colors.cyan}pnpm dlx @aalexis.dev/cfx --acc=production deploy${colors.reset}
|
|
59
|
+
|
|
60
|
+
${colors.gray}# Stream real-time logs${colors.reset}
|
|
61
|
+
$ ${colors.cyan}pnpm dlx @aalexis.dev/cfx -a staging tail${colors.reset}
|
|
62
|
+
|
|
63
|
+
${colors.gray}# Deploy across all configured accounts${colors.reset}
|
|
64
|
+
$ ${colors.cyan}pnpm dlx @aalexis.dev/cfx --all deploy${colors.reset}
|
|
65
|
+
|
|
66
|
+
${colors.gray}# Check current account information (whoami)${colors.reset}
|
|
67
|
+
$ ${colors.cyan}pnpm dlx @aalexis.dev/cfx --acc=client1 whoami${colors.reset}
|
|
68
|
+
`);
|
|
69
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@aalexis.dev/cfx",
|
|
3
|
+
"version": "1.0.2",
|
|
4
|
+
"description": "Ultra-lightweight multi-account Cloudflare Wrangler wrapper CLI with encrypted local storage",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./bin/index.js",
|
|
7
|
+
"bin": {
|
|
8
|
+
"cfx": "./bin/index.js",
|
|
9
|
+
"cfpipe": "./bin/index.js"
|
|
10
|
+
},
|
|
11
|
+
"files": [
|
|
12
|
+
"bin",
|
|
13
|
+
"README.md",
|
|
14
|
+
"LICENSE"
|
|
15
|
+
],
|
|
16
|
+
"engines": {
|
|
17
|
+
"node": ">=18.0.0"
|
|
18
|
+
},
|
|
19
|
+
"publishConfig": {
|
|
20
|
+
"access": "public"
|
|
21
|
+
},
|
|
22
|
+
"keywords": [
|
|
23
|
+
"cloudflare",
|
|
24
|
+
"wrangler",
|
|
25
|
+
"cli",
|
|
26
|
+
"multi-account",
|
|
27
|
+
"cfx",
|
|
28
|
+
"cfpipe",
|
|
29
|
+
"security",
|
|
30
|
+
"encryption",
|
|
31
|
+
"pnpm"
|
|
32
|
+
],
|
|
33
|
+
"author": "Alexis",
|
|
34
|
+
"license": "MIT",
|
|
35
|
+
"devDependencies": {
|
|
36
|
+
"@types/node": "^22.0.0",
|
|
37
|
+
"typescript": "^5.5.0"
|
|
38
|
+
},
|
|
39
|
+
"scripts": {
|
|
40
|
+
"build": "tsc && node scripts/make-executable.js",
|
|
41
|
+
"dev": "tsc --watch",
|
|
42
|
+
"test": "tsc -p tsconfig.test.json && node dist-test/tests/test-flow.js && node scripts/cleanup-test.js"
|
|
43
|
+
}
|
|
44
|
+
}
|