@maestro-js/cli 1.0.0-alpha.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +252 -0
- package/dist/bin.d.ts +2 -0
- package/dist/bin.js +848 -0
- package/package.json +43 -0
package/README.md
ADDED
|
@@ -0,0 +1,252 @@
|
|
|
1
|
+
# @maestro-js/cli
|
|
2
|
+
|
|
3
|
+
CLI tool for the Maestro framework, built with [sade](https://github.com/lukeed/sade). Provides commands for database migrations, environment file encryption, Docker container creation, and AWS WireGuard client provisioning.
|
|
4
|
+
|
|
5
|
+
Binary name: `maestro`. Installed via the `bin` field in package.json as `./dist/bin.js`.
|
|
6
|
+
|
|
7
|
+
## Package Location
|
|
8
|
+
|
|
9
|
+
- Source: `packages/maestro-cli/src/`
|
|
10
|
+
- Entry point: `packages/maestro-cli/src/bin.ts`
|
|
11
|
+
- Commands: `packages/maestro-cli/src/commands/`
|
|
12
|
+
- Utilities: `packages/maestro-cli/src/utils/`
|
|
13
|
+
- No tests directory exists for this package.
|
|
14
|
+
|
|
15
|
+
## Available Commands
|
|
16
|
+
|
|
17
|
+
| Command | Description |
|
|
18
|
+
|---|---|
|
|
19
|
+
| `maestro db:make-migration <name>` | Create a new database migration |
|
|
20
|
+
| `maestro db:migrate` | Run pending database migrations |
|
|
21
|
+
| `maestro db:rollback` | Rollback database migrations |
|
|
22
|
+
| `maestro db:status` | Show database migration status |
|
|
23
|
+
| `maestro db:make-container` | Create a Docker database container |
|
|
24
|
+
| `maestro env:generate-key` | Generate an encryption key |
|
|
25
|
+
| `maestro env:encrypt` | Encrypt a .env file |
|
|
26
|
+
| `maestro env:decrypt` | Decrypt a .env.encrypted file |
|
|
27
|
+
| `maestro aws:make-wireguard-client [id]` | Add a WireGuard client peer to a VPN server |
|
|
28
|
+
|
|
29
|
+
## Command Details
|
|
30
|
+
|
|
31
|
+
### db:make-migration
|
|
32
|
+
|
|
33
|
+
Create a new migration in `infrastructure/db-migrations/` relative to cwd. Delegates to `DbMigrations.scaffold()` from `@maestro-js/db-migrate`.
|
|
34
|
+
|
|
35
|
+
```
|
|
36
|
+
maestro db:make-migration <name>
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
Generates three files: a `.ts` migration file, an `.up.sql` file, and a `.down.sql` file.
|
|
40
|
+
|
|
41
|
+
### db:migrate
|
|
42
|
+
|
|
43
|
+
Run pending database migrations. Connects to the database using resolved config (CLI flags > env vars > config file).
|
|
44
|
+
|
|
45
|
+
```
|
|
46
|
+
maestro db:migrate [options]
|
|
47
|
+
--host Database host (env: DB_HOST)
|
|
48
|
+
--port Database port (env: DB_PORT)
|
|
49
|
+
--user Database user (env: DB_USER)
|
|
50
|
+
--password Database password (env: DB_PASSWORD)
|
|
51
|
+
--database Database name (env: DB_DATABASE)
|
|
52
|
+
--dry-run Preview migrations without executing
|
|
53
|
+
--count Number of migrations to run
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
### db:rollback
|
|
57
|
+
|
|
58
|
+
Rollback database migrations. Default count is 1.
|
|
59
|
+
|
|
60
|
+
```
|
|
61
|
+
maestro db:rollback [options]
|
|
62
|
+
--host Database host (env: DB_HOST)
|
|
63
|
+
--port Database port (env: DB_PORT)
|
|
64
|
+
--user Database user (env: DB_USER)
|
|
65
|
+
--password Database password (env: DB_PASSWORD)
|
|
66
|
+
--database Database name (env: DB_DATABASE)
|
|
67
|
+
--dry-run Preview rollback without executing
|
|
68
|
+
--count Number of migrations to rollback (default: 1)
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
### db:status
|
|
72
|
+
|
|
73
|
+
Show which migrations have been applied and which are pending.
|
|
74
|
+
|
|
75
|
+
```
|
|
76
|
+
maestro db:status [options]
|
|
77
|
+
--host Database host (env: DB_HOST)
|
|
78
|
+
--port Database port (env: DB_PORT)
|
|
79
|
+
--user Database user (env: DB_USER)
|
|
80
|
+
--password Database password (env: DB_PASSWORD)
|
|
81
|
+
--database Database name (env: DB_DATABASE)
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
### db:make-container
|
|
85
|
+
|
|
86
|
+
Interactive command that prompts for database type (MySQL, PostgreSQL, Redis), container name, port, database name, and password. Pulls the Docker image and runs the container.
|
|
87
|
+
|
|
88
|
+
```
|
|
89
|
+
maestro db:make-container
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
Supported database images: `mysql:8`, `postgres:17`, `redis:7`.
|
|
93
|
+
|
|
94
|
+
### env:generate-key
|
|
95
|
+
|
|
96
|
+
Generate an encryption key via `Config.generateEncryptionKey()` from `@maestro-js/config`. Prints the key to stdout.
|
|
97
|
+
|
|
98
|
+
```
|
|
99
|
+
maestro env:generate-key
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
### env:encrypt
|
|
103
|
+
|
|
104
|
+
Encrypt a `.env` file to `.env.encrypted`. Requires an encryption key via `--key` flag or `MAESTRO_ENV_ENCRYPTION_KEY` env var.
|
|
105
|
+
|
|
106
|
+
```
|
|
107
|
+
maestro env:encrypt [options]
|
|
108
|
+
--env Environment name (e.g. production, creates .env.production.encrypted)
|
|
109
|
+
--key Encryption key (env: MAESTRO_ENV_ENCRYPTION_KEY)
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
### env:decrypt
|
|
113
|
+
|
|
114
|
+
Decrypt a `.env.encrypted` file back to `.env`. Requires an encryption key via `--key` flag or `MAESTRO_ENV_ENCRYPTION_KEY` env var.
|
|
115
|
+
|
|
116
|
+
```
|
|
117
|
+
maestro env:decrypt [options]
|
|
118
|
+
--env Environment name (e.g. production)
|
|
119
|
+
--key Encryption key (env: MAESTRO_ENV_ENCRYPTION_KEY)
|
|
120
|
+
--force Overwrite existing .env file
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
### aws:make-wireguard-client
|
|
124
|
+
|
|
125
|
+
Add a WireGuard client peer to an existing VPN server managed via AWS SSM Parameter Store. If no `[id]` argument is provided, lists available WireGuard configurations from SSM and prompts for selection.
|
|
126
|
+
|
|
127
|
+
```
|
|
128
|
+
maestro aws:make-wireguard-client [id] [options]
|
|
129
|
+
--region AWS region (env: AWS_REGION)
|
|
130
|
+
--name Label for the peer
|
|
131
|
+
--endpoint Override auto-detected server IP
|
|
132
|
+
--dns DNS for client config
|
|
133
|
+
--allowed-ips Client AllowedIPs (default: "10.0.0.0/16, 10.100.0.0/24")
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
Generates a WireGuard client configuration, stores the peer in SSM, and applies it on the server via SSM Run Command.
|
|
137
|
+
|
|
138
|
+
## Adding New Commands
|
|
139
|
+
|
|
140
|
+
### 1. Create the command file
|
|
141
|
+
|
|
142
|
+
Add a new file in `packages/maestro-cli/src/commands/`. Follow the existing pattern: export a single async function that receives parsed options.
|
|
143
|
+
|
|
144
|
+
```ts
|
|
145
|
+
// packages/maestro-cli/src/commands/my-command.ts
|
|
146
|
+
interface Options {
|
|
147
|
+
flag?: string
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
export async function myCommand(opts: Options) {
|
|
151
|
+
// Implementation
|
|
152
|
+
console.log('Done.')
|
|
153
|
+
}
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
### 2. Register in bin.ts
|
|
157
|
+
|
|
158
|
+
Import the command function and register it with sade in `packages/maestro-cli/src/bin.ts`:
|
|
159
|
+
|
|
160
|
+
```ts
|
|
161
|
+
import { myCommand } from './commands/my-command.ts'
|
|
162
|
+
|
|
163
|
+
prog
|
|
164
|
+
.command('namespace:my-command')
|
|
165
|
+
.describe('Short description of what it does')
|
|
166
|
+
.option('--flag', 'Description of the flag')
|
|
167
|
+
.action(myCommand)
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
### 3. Command naming convention
|
|
171
|
+
|
|
172
|
+
Use colon-separated namespaces matching existing groups:
|
|
173
|
+
- `db:*` for database-related commands
|
|
174
|
+
- `env:*` for environment/config commands
|
|
175
|
+
- `aws:*` for AWS infrastructure commands
|
|
176
|
+
|
|
177
|
+
### 4. For database commands
|
|
178
|
+
|
|
179
|
+
Use the shared utilities in `packages/maestro-cli/src/utils/db.ts`:
|
|
180
|
+
|
|
181
|
+
```ts
|
|
182
|
+
import { createDb, resolveDbConfig, type DbConnectionConfig } from '../utils/db.ts'
|
|
183
|
+
|
|
184
|
+
export async function myDbCommand(opts: Partial<DbConnectionConfig>) {
|
|
185
|
+
const config = await resolveDbConfig(opts)
|
|
186
|
+
const db = await createDb(config)
|
|
187
|
+
try {
|
|
188
|
+
// Use db...
|
|
189
|
+
} finally {
|
|
190
|
+
await db.destroy()
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
The `resolveDbConfig` function resolves database connection settings with this precedence: CLI flags > environment variables > config file (loaded via `@maestro-js/config` with config name `db-migrate`).
|
|
196
|
+
|
|
197
|
+
## Common Patterns
|
|
198
|
+
|
|
199
|
+
### Database config resolution
|
|
200
|
+
|
|
201
|
+
All `db:*` commands (except `db:make-migration` and `db:make-container`) share the same connection options and resolution logic from `utils/db.ts`. The `resolveDbConfig` function reads from:
|
|
202
|
+
1. CLI flags (`--host`, `--port`, `--user`, `--password`, `--database`)
|
|
203
|
+
2. Environment variables (`DB_HOST`, `DB_PORT`, `DB_USER`, `DB_PASSWORD`, `DB_DATABASE`)
|
|
204
|
+
3. Config file via `@maestro-js/config` with config name `db-migrate`
|
|
205
|
+
|
|
206
|
+
### Migration directory
|
|
207
|
+
|
|
208
|
+
All migration commands expect migrations in `infrastructure/db-migrations/` relative to the current working directory.
|
|
209
|
+
|
|
210
|
+
### Interactive prompts
|
|
211
|
+
|
|
212
|
+
The `prompts` library is used for interactive input in `db:make-container` and `aws:make-wireguard-client`. All prompt flows call `process.exit(0)` on cancellation.
|
|
213
|
+
|
|
214
|
+
### Error handling
|
|
215
|
+
|
|
216
|
+
Commands print errors to stderr via `console.error()` and call `process.exit(1)` on failure. Database commands use `try/finally` to ensure `db.destroy()` is always called.
|
|
217
|
+
|
|
218
|
+
## Cross-Package Integration
|
|
219
|
+
|
|
220
|
+
| Dependency | Usage |
|
|
221
|
+
|---|---|
|
|
222
|
+
| `@maestro-js/config` | Env encryption/decryption (`Config.generateEncryptedEnvFile`, `Config.generateDecryptedEnvFile`, `Config.generateEncryptionKey`), config file loading for DB credentials |
|
|
223
|
+
| `@maestro-js/db` | Database connection creation via `Db.Provider.create` with `Db.drivers.mysql` driver |
|
|
224
|
+
| `@maestro-js/db-migrate` | Migration operations: `DbMigrations.scaffold`, `DbMigrations.migrate`, `DbMigrations.rollback`, `DbMigrations.status` |
|
|
225
|
+
| `sade` | CLI argument parsing and command registration |
|
|
226
|
+
| `prompts` | Interactive terminal prompts for `db:make-container` and `aws:make-wireguard-client` |
|
|
227
|
+
| `@aws-sdk/client-ssm` | SSM parameter read/write and Run Command for WireGuard provisioning |
|
|
228
|
+
| `@aws-sdk/client-sts` | Caller identity resolution for default peer naming |
|
|
229
|
+
|
|
230
|
+
## Utility Modules
|
|
231
|
+
|
|
232
|
+
### utils/db.ts
|
|
233
|
+
|
|
234
|
+
Exports `resolveDbConfig`, `createDb`, and the `DbConnectionConfig` / `CliDb` interfaces. Central place for database connection setup across all db commands.
|
|
235
|
+
|
|
236
|
+
### utils/migrations.ts
|
|
237
|
+
|
|
238
|
+
Re-exports `DbMigrations` from `@maestro-js/db-migrate`.
|
|
239
|
+
|
|
240
|
+
### utils/scaffolding.ts
|
|
241
|
+
|
|
242
|
+
General-purpose file scaffolding utility (`Scaffolding.scaffold`, `Scaffolding.previewScaffold`, `Scaffolding.fromDirectory`). Supports token replacement with `{{token}}` syntax, skip-existing behavior, and reading directory trees into scaffolding objects. Not currently used by any CLI command but available for future scaffolding needs.
|
|
243
|
+
|
|
244
|
+
## Build and Development
|
|
245
|
+
|
|
246
|
+
```bash
|
|
247
|
+
pnpm --filter @maestro-js/cli build # Build with tsup
|
|
248
|
+
pnpm --filter @maestro-js/cli typecheck # Typecheck
|
|
249
|
+
pnpm --filter @maestro-js/cli watch # Watch mode
|
|
250
|
+
```
|
|
251
|
+
|
|
252
|
+
No tests exist for this package. When adding tests, use `beartest-js` with `expect` assertions consistent with the rest of the monorepo.
|
package/dist/bin.d.ts
ADDED
package/dist/bin.js
ADDED
|
@@ -0,0 +1,848 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/bin.ts
|
|
4
|
+
import { createRequire } from "module";
|
|
5
|
+
import sade from "sade";
|
|
6
|
+
|
|
7
|
+
// src/commands/aws-make-wireguard-client.ts
|
|
8
|
+
import { generateKeyPairSync } from "crypto";
|
|
9
|
+
import {
|
|
10
|
+
SSMClient,
|
|
11
|
+
DescribeParametersCommand,
|
|
12
|
+
GetParameterCommand,
|
|
13
|
+
PutParameterCommand,
|
|
14
|
+
SendCommandCommand
|
|
15
|
+
} from "@aws-sdk/client-ssm";
|
|
16
|
+
import { STSClient, GetCallerIdentityCommand } from "@aws-sdk/client-sts";
|
|
17
|
+
import prompts from "prompts";
|
|
18
|
+
async function awsMakeWireguardClient(id, opts) {
|
|
19
|
+
const region = resolveRegion(opts.region);
|
|
20
|
+
const client = createSsmClient(region);
|
|
21
|
+
if (!id) {
|
|
22
|
+
const ids = await listWireguardIds(client);
|
|
23
|
+
if (ids.length === 0) {
|
|
24
|
+
console.error("Error: No WireGuard configurations found in SSM.");
|
|
25
|
+
process.exit(1);
|
|
26
|
+
}
|
|
27
|
+
id = await promptForId(ids);
|
|
28
|
+
}
|
|
29
|
+
const { privateKey, publicKey } = generateClientKeypair();
|
|
30
|
+
const serverPublicKey = await ssmGet(client, `/wireguard/${id}/public-key`);
|
|
31
|
+
const endpoint = opts.endpoint ?? await ssmGet(client, `/wireguard/${id}/endpoint`);
|
|
32
|
+
const instanceId = await ssmGet(client, `/wireguard/${id}/instance-id`);
|
|
33
|
+
const existingPeers = await ssmGetOrEmpty(client, `/wireguard/${id}/peers`);
|
|
34
|
+
const clientIp = allocateNextIp(existingPeers);
|
|
35
|
+
const name = opts.name ?? await getCallerName(region) ?? `client-${clientIp.split(".").pop()}`;
|
|
36
|
+
const allowedIps = opts["allowed-ips"] ?? "10.0.0.0/16, 10.100.0.0/24";
|
|
37
|
+
await appendPeerToSsm(client, `/wireguard/${id}/peers`, publicKey, clientIp, name, existingPeers);
|
|
38
|
+
await applyPeerOnServer(client, instanceId, id);
|
|
39
|
+
const dns = opts.dns ? `
|
|
40
|
+
DNS = ${opts.dns}` : "";
|
|
41
|
+
const config = [
|
|
42
|
+
"[Interface]",
|
|
43
|
+
`PrivateKey = ${privateKey}`,
|
|
44
|
+
`Address = ${clientIp}/32${dns}`,
|
|
45
|
+
"",
|
|
46
|
+
"[Peer]",
|
|
47
|
+
`PublicKey = ${serverPublicKey}`,
|
|
48
|
+
`AllowedIPs = ${allowedIps}`,
|
|
49
|
+
`Endpoint = ${endpoint}:51820`,
|
|
50
|
+
"PersistentKeepalive = 25"
|
|
51
|
+
].join("\n");
|
|
52
|
+
console.log(`
|
|
53
|
+
${config}
|
|
54
|
+
`);
|
|
55
|
+
console.log(`Peer "${name}" registered with IP ${clientIp}/32`);
|
|
56
|
+
}
|
|
57
|
+
function createSsmClient(region) {
|
|
58
|
+
return new SSMClient({ region });
|
|
59
|
+
}
|
|
60
|
+
async function listWireguardIds(client) {
|
|
61
|
+
try {
|
|
62
|
+
const response = await client.send(
|
|
63
|
+
new DescribeParametersCommand({
|
|
64
|
+
ParameterFilters: [{ Key: "Name", Option: "BeginsWith", Values: ["/wireguard/"] }]
|
|
65
|
+
})
|
|
66
|
+
);
|
|
67
|
+
const names = (response.Parameters ?? []).map((p) => p.Name).filter(Boolean);
|
|
68
|
+
const ids = /* @__PURE__ */ new Set();
|
|
69
|
+
for (const name of names) {
|
|
70
|
+
const match = name.match(/^\/wireguard\/([^/]+)\//);
|
|
71
|
+
if (match) ids.add(match[1]);
|
|
72
|
+
}
|
|
73
|
+
return [...ids].sort();
|
|
74
|
+
} catch {
|
|
75
|
+
console.error("Error: Failed to list WireGuard SSM parameters.");
|
|
76
|
+
process.exit(1);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
async function promptForId(ids) {
|
|
80
|
+
const { id } = await prompts({
|
|
81
|
+
type: "select",
|
|
82
|
+
name: "id",
|
|
83
|
+
message: "Select a WireGuard configuration",
|
|
84
|
+
choices: ids.map((id2) => ({ title: id2, value: id2 }))
|
|
85
|
+
});
|
|
86
|
+
if (!id) {
|
|
87
|
+
process.exit(0);
|
|
88
|
+
}
|
|
89
|
+
return id;
|
|
90
|
+
}
|
|
91
|
+
function resolveRegion(flag) {
|
|
92
|
+
const region = flag ?? process.env.AWS_REGION ?? process.env.AWS_DEFAULT_REGION;
|
|
93
|
+
if (!region) {
|
|
94
|
+
console.error("Error: No AWS region specified. Use --region or set AWS_REGION.");
|
|
95
|
+
process.exit(1);
|
|
96
|
+
}
|
|
97
|
+
return region;
|
|
98
|
+
}
|
|
99
|
+
function generateClientKeypair() {
|
|
100
|
+
const { privateKey, publicKey } = generateKeyPairSync("x25519", {
|
|
101
|
+
privateKeyEncoding: { type: "pkcs8", format: "der" },
|
|
102
|
+
publicKeyEncoding: { type: "spki", format: "der" }
|
|
103
|
+
});
|
|
104
|
+
return {
|
|
105
|
+
privateKey: privateKey.subarray(-32).toString("base64"),
|
|
106
|
+
publicKey: publicKey.subarray(-32).toString("base64")
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
async function ssmGet(client, name) {
|
|
110
|
+
try {
|
|
111
|
+
const response = await client.send(new GetParameterCommand({ Name: name, WithDecryption: true }));
|
|
112
|
+
return response.Parameter.Value;
|
|
113
|
+
} catch {
|
|
114
|
+
console.error(`Error: Failed to read SSM parameter "${name}". Is the WireGuard stack deployed?`);
|
|
115
|
+
process.exit(1);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
async function ssmGetOrEmpty(client, name) {
|
|
119
|
+
try {
|
|
120
|
+
const response = await client.send(new GetParameterCommand({ Name: name, WithDecryption: true }));
|
|
121
|
+
return response.Parameter.Value;
|
|
122
|
+
} catch {
|
|
123
|
+
return "";
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
async function getCallerName(region) {
|
|
127
|
+
try {
|
|
128
|
+
const sts = new STSClient({ region });
|
|
129
|
+
const { Arn } = await sts.send(new GetCallerIdentityCommand({}));
|
|
130
|
+
if (!Arn) return void 0;
|
|
131
|
+
const resource = Arn.split(":").pop();
|
|
132
|
+
const name = resource.includes("/") ? resource.split("/")[1] : resource;
|
|
133
|
+
return name;
|
|
134
|
+
} catch {
|
|
135
|
+
return void 0;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
function allocateNextIp(existingPeers) {
|
|
139
|
+
const usedOctets = /* @__PURE__ */ new Set([1]);
|
|
140
|
+
const re = /AllowedIPs\s*=\s*10\.100\.0\.(\d+)/g;
|
|
141
|
+
let match;
|
|
142
|
+
while ((match = re.exec(existingPeers)) !== null) {
|
|
143
|
+
usedOctets.add(Number(match[1]));
|
|
144
|
+
}
|
|
145
|
+
for (let i = 2; i <= 254; i++) {
|
|
146
|
+
if (!usedOctets.has(i)) return `10.100.0.${i}`;
|
|
147
|
+
}
|
|
148
|
+
console.error("Error: No available IPs in 10.100.0.0/24 subnet.");
|
|
149
|
+
process.exit(1);
|
|
150
|
+
}
|
|
151
|
+
async function appendPeerToSsm(client, name, publicKey, clientIp, peerName, existingPeers) {
|
|
152
|
+
const newPeer = [`[Peer]`, `# ${peerName}`, `PublicKey = ${publicKey}`, `AllowedIPs = ${clientIp}/32`].join("\n");
|
|
153
|
+
const updated = existingPeers ? `${existingPeers}
|
|
154
|
+
|
|
155
|
+
${newPeer}` : newPeer;
|
|
156
|
+
await client.send(
|
|
157
|
+
new PutParameterCommand({
|
|
158
|
+
Name: name,
|
|
159
|
+
Type: "SecureString",
|
|
160
|
+
Value: updated,
|
|
161
|
+
Overwrite: true
|
|
162
|
+
})
|
|
163
|
+
);
|
|
164
|
+
}
|
|
165
|
+
async function applyPeerOnServer(client, instanceId, wireguardId) {
|
|
166
|
+
const script = [
|
|
167
|
+
'TOKEN=$(curl -s -X PUT "http://169.254.169.254/latest/api/token" -H "X-aws-ec2-metadata-token-ttl-seconds: 21600")',
|
|
168
|
+
'REGION=$(curl -s -H "X-aws-ec2-metadata-token: $TOKEN" http://169.254.169.254/latest/meta-data/placement/region)',
|
|
169
|
+
"sed -i '/^\\[Peer\\]/,$d' /etc/wireguard/wg0.conf",
|
|
170
|
+
`aws ssm get-parameter --name "/wireguard/${wireguardId}/peers" --with-decryption --region $REGION --query "Parameter.Value" --output text >> /etc/wireguard/wg0.conf`,
|
|
171
|
+
"systemctl restart wg-quick@wg0"
|
|
172
|
+
];
|
|
173
|
+
try {
|
|
174
|
+
await client.send(
|
|
175
|
+
new SendCommandCommand({
|
|
176
|
+
InstanceIds: [instanceId],
|
|
177
|
+
DocumentName: "AWS-RunShellScript",
|
|
178
|
+
Parameters: { commands: script }
|
|
179
|
+
})
|
|
180
|
+
);
|
|
181
|
+
} catch {
|
|
182
|
+
console.error("Warning: Failed to apply peers via SSM Run Command. The peers are saved but may need a server restart.");
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// src/commands/db-make-container.ts
|
|
187
|
+
import { execSync } from "child_process";
|
|
188
|
+
import prompts2 from "prompts";
|
|
189
|
+
async function createDockerDb() {
|
|
190
|
+
assertDockerAvailable();
|
|
191
|
+
const config = await promptForConfig();
|
|
192
|
+
const db = DATABASE_CONFIGS[config.dbType];
|
|
193
|
+
pull(db.image);
|
|
194
|
+
run(config, db);
|
|
195
|
+
printSummary(config, db);
|
|
196
|
+
}
|
|
197
|
+
var DATABASE_CONFIGS = {
|
|
198
|
+
mysql: {
|
|
199
|
+
image: "mysql:8",
|
|
200
|
+
defaultPort: 3306,
|
|
201
|
+
envFlags: (dbName, password) => ["-e", `MYSQL_ROOT_PASSWORD=${password}`, "-e", `MYSQL_DATABASE=${dbName}`]
|
|
202
|
+
},
|
|
203
|
+
postgres: {
|
|
204
|
+
image: "postgres:17",
|
|
205
|
+
defaultPort: 5432,
|
|
206
|
+
envFlags: (dbName, password) => ["-e", `POSTGRES_PASSWORD=${password}`, "-e", `POSTGRES_DB=${dbName}`]
|
|
207
|
+
},
|
|
208
|
+
redis: {
|
|
209
|
+
image: "redis:7",
|
|
210
|
+
defaultPort: 6379,
|
|
211
|
+
envFlags: () => []
|
|
212
|
+
}
|
|
213
|
+
};
|
|
214
|
+
var onCancel = () => process.exit(0);
|
|
215
|
+
async function promptForConfig() {
|
|
216
|
+
const { dbType } = await prompts2(
|
|
217
|
+
{
|
|
218
|
+
type: "select",
|
|
219
|
+
name: "dbType",
|
|
220
|
+
message: "Which database?",
|
|
221
|
+
choices: [
|
|
222
|
+
{ title: "MySQL", value: "mysql" },
|
|
223
|
+
{ title: "PostgreSQL", value: "postgres" },
|
|
224
|
+
{ title: "Redis", value: "redis" }
|
|
225
|
+
]
|
|
226
|
+
},
|
|
227
|
+
{ onCancel }
|
|
228
|
+
);
|
|
229
|
+
const isRedis = dbType === "redis";
|
|
230
|
+
const defaultPort = DATABASE_CONFIGS[dbType].defaultPort;
|
|
231
|
+
const answers = await prompts2(
|
|
232
|
+
[
|
|
233
|
+
{
|
|
234
|
+
type: "text",
|
|
235
|
+
name: "containerName",
|
|
236
|
+
message: "Container name",
|
|
237
|
+
initial: `maestro-${dbType}`,
|
|
238
|
+
validate: (v) => /^[a-zA-Z0-9][a-zA-Z0-9_.-]*$/.test(v) ? true : "Alphanumeric, hyphens, dots, underscores only"
|
|
239
|
+
},
|
|
240
|
+
{
|
|
241
|
+
type: "number",
|
|
242
|
+
name: "port",
|
|
243
|
+
message: "Host port",
|
|
244
|
+
initial: defaultPort
|
|
245
|
+
},
|
|
246
|
+
{
|
|
247
|
+
type: isRedis ? null : "text",
|
|
248
|
+
name: "dbName",
|
|
249
|
+
message: "Database name",
|
|
250
|
+
initial: "maestro"
|
|
251
|
+
},
|
|
252
|
+
{
|
|
253
|
+
type: isRedis ? null : "password",
|
|
254
|
+
name: "password",
|
|
255
|
+
message: "Root password",
|
|
256
|
+
initial: "maestro"
|
|
257
|
+
}
|
|
258
|
+
],
|
|
259
|
+
{ onCancel }
|
|
260
|
+
);
|
|
261
|
+
return {
|
|
262
|
+
dbType,
|
|
263
|
+
containerName: answers.containerName,
|
|
264
|
+
port: answers.port,
|
|
265
|
+
dbName: answers.dbName ?? "",
|
|
266
|
+
password: answers.password ?? ""
|
|
267
|
+
};
|
|
268
|
+
}
|
|
269
|
+
function assertDockerAvailable() {
|
|
270
|
+
try {
|
|
271
|
+
execSync("docker info", { stdio: "ignore" });
|
|
272
|
+
} catch {
|
|
273
|
+
console.error("Error: Docker is not running. Please start Docker and try again.");
|
|
274
|
+
process.exit(1);
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
function pull(image) {
|
|
278
|
+
console.log(`
|
|
279
|
+
Pulling ${image}...`);
|
|
280
|
+
try {
|
|
281
|
+
execSync(`docker pull ${image}`, { stdio: "inherit" });
|
|
282
|
+
} catch {
|
|
283
|
+
console.error(`
|
|
284
|
+
Failed to pull image ${image}.`);
|
|
285
|
+
process.exit(1);
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
function run(config, db) {
|
|
289
|
+
const args = [
|
|
290
|
+
"docker",
|
|
291
|
+
"run",
|
|
292
|
+
"-d",
|
|
293
|
+
"--name",
|
|
294
|
+
config.containerName,
|
|
295
|
+
"-p",
|
|
296
|
+
`${config.port}:${db.defaultPort}`,
|
|
297
|
+
...db.envFlags(config.dbName, config.password),
|
|
298
|
+
db.image
|
|
299
|
+
];
|
|
300
|
+
try {
|
|
301
|
+
execSync(args.join(" "), { stdio: "pipe" });
|
|
302
|
+
} catch (e) {
|
|
303
|
+
const stderr = e instanceof Error && "stderr" in e ? e.stderr?.toString() : "";
|
|
304
|
+
console.error(`
|
|
305
|
+
Failed to start container:
|
|
306
|
+
${stderr}`);
|
|
307
|
+
process.exit(1);
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
function printSummary(config, db) {
|
|
311
|
+
console.log(`
|
|
312
|
+
Container "${config.containerName}" is running.`);
|
|
313
|
+
console.log(` Image: ${db.image}`);
|
|
314
|
+
console.log(` Port: ${config.port}`);
|
|
315
|
+
if (config.dbName) console.log(` DB: ${config.dbName}`);
|
|
316
|
+
if (config.password) console.log(` Pass: ${config.password}`);
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
// src/commands/agent.ts
|
|
320
|
+
import { execSync as execSync2 } from "child_process";
|
|
321
|
+
import { AgentContainer } from "@maestro-js/agent-container";
|
|
322
|
+
import prompts3 from "prompts";
|
|
323
|
+
|
|
324
|
+
// src/utils/agent.ts
|
|
325
|
+
import { Config } from "@maestro-js/config";
|
|
326
|
+
async function resolveAgentConfig(opts) {
|
|
327
|
+
const config = await Config.getConfig({
|
|
328
|
+
rootDir: process.cwd(),
|
|
329
|
+
configName: "agent-container",
|
|
330
|
+
envName: null,
|
|
331
|
+
encryptionKey: null
|
|
332
|
+
});
|
|
333
|
+
return {
|
|
334
|
+
memory: opts.memory ?? process.env.AGENT_MEMORY ?? config.AGENT_MEMORY,
|
|
335
|
+
cpus: opts.cpus ?? (process.env.AGENT_CPUS ?? config.AGENT_CPUS ? Number(process.env.AGENT_CPUS ?? config.AGENT_CPUS) : void 0),
|
|
336
|
+
workDir: process.env.AGENT_WORK_DIR ?? config.AGENT_WORK_DIR,
|
|
337
|
+
dockerfile: config.AGENT_DOCKERFILE
|
|
338
|
+
};
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
// src/commands/agent.ts
|
|
342
|
+
async function agentMake(name, opts) {
|
|
343
|
+
const repoPath = opts.repo ?? findGitRoot();
|
|
344
|
+
const env = parseEnv(opts.env);
|
|
345
|
+
const agentConfig = await resolveAgentConfig(opts);
|
|
346
|
+
const container = await AgentContainer.create({ name, repoPath, env, ...agentConfig });
|
|
347
|
+
console.log(`Make container: ${container.id}`);
|
|
348
|
+
}
|
|
349
|
+
async function agentKill(name) {
|
|
350
|
+
const target = name ?? await selectContainer("Select a container to kill");
|
|
351
|
+
if (!target) return;
|
|
352
|
+
await AgentContainer.kill(target);
|
|
353
|
+
console.log(`Killed container: ${target}`);
|
|
354
|
+
}
|
|
355
|
+
async function agentShell(name) {
|
|
356
|
+
const target = name ?? await selectContainer("Select a container to shell into");
|
|
357
|
+
if (!target) return;
|
|
358
|
+
await AgentContainer.shell(target);
|
|
359
|
+
}
|
|
360
|
+
async function agentOpen(name) {
|
|
361
|
+
const target = name ?? await selectContainer("Select a container to open in VS Code");
|
|
362
|
+
if (!target) return;
|
|
363
|
+
await AgentContainer.open(target);
|
|
364
|
+
}
|
|
365
|
+
async function agentPull(name, opts) {
|
|
366
|
+
const target = name ?? await selectContainer("Select a container to pull from");
|
|
367
|
+
if (!target) return;
|
|
368
|
+
const repoPath = opts.repo ?? findGitRoot();
|
|
369
|
+
const result = await AgentContainer.pull(target, repoPath);
|
|
370
|
+
if (result.applied) {
|
|
371
|
+
console.log(`Pulled changes from container ${target} \u2192 host`);
|
|
372
|
+
} else {
|
|
373
|
+
console.log(`No changes to pull from container ${target}`);
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
async function selectContainer(message) {
|
|
377
|
+
const containers = await AgentContainer.list();
|
|
378
|
+
if (containers.length === 0) {
|
|
379
|
+
console.log("No running agent containers.");
|
|
380
|
+
return;
|
|
381
|
+
}
|
|
382
|
+
const onCancel3 = () => process.exit(0);
|
|
383
|
+
const { name } = await prompts3(
|
|
384
|
+
{
|
|
385
|
+
type: "select",
|
|
386
|
+
name: "name",
|
|
387
|
+
message,
|
|
388
|
+
choices: containers.map((c) => ({ title: `${c.name.replace("maestro-agent-", "")} (${c.status})`, value: c.name }))
|
|
389
|
+
},
|
|
390
|
+
{ onCancel: onCancel3 }
|
|
391
|
+
);
|
|
392
|
+
return name;
|
|
393
|
+
}
|
|
394
|
+
function findGitRoot() {
|
|
395
|
+
try {
|
|
396
|
+
return execSync2("git rev-parse --show-toplevel", { encoding: "utf8" }).trim();
|
|
397
|
+
} catch {
|
|
398
|
+
return process.cwd();
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
function parseEnv(env) {
|
|
402
|
+
if (!env) return {};
|
|
403
|
+
const entries = Array.isArray(env) ? env : [env];
|
|
404
|
+
return Object.fromEntries(
|
|
405
|
+
entries.map((e) => {
|
|
406
|
+
const i = e.indexOf("=");
|
|
407
|
+
return [e.slice(0, i), e.slice(i + 1)];
|
|
408
|
+
})
|
|
409
|
+
);
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
// src/commands/db-migrations.ts
|
|
413
|
+
import { join } from "path";
|
|
414
|
+
import { DbMigrations } from "@maestro-js/db-migrate";
|
|
415
|
+
|
|
416
|
+
// src/utils/db.ts
|
|
417
|
+
import { Config as Config2 } from "@maestro-js/config";
|
|
418
|
+
import { Db } from "@maestro-js/db";
|
|
419
|
+
async function resolveDbConfig(opts) {
|
|
420
|
+
const config = await Config2.getConfig({
|
|
421
|
+
rootDir: process.cwd(),
|
|
422
|
+
configName: "db-migrate",
|
|
423
|
+
envName: null,
|
|
424
|
+
encryptionKey: null
|
|
425
|
+
});
|
|
426
|
+
const database = opts.database ?? process.env.DB_DATABASE ?? config.DB_DATABASE;
|
|
427
|
+
const host = opts.host ?? process.env.DB_HOST ?? config.DB_HOST ?? "localhost";
|
|
428
|
+
const port = opts.port ?? (process.env.DB_PORT ?? config.DB_PORT ? Number(process.env.DB_PORT ?? config.DB_PORT) : 3306);
|
|
429
|
+
const user = opts.user ?? process.env.DB_USER ?? config.DB_USER ?? "root";
|
|
430
|
+
const password = opts.password ?? process.env.DB_PASSWORD ?? config.DB_PASSWORD ?? "err";
|
|
431
|
+
if (!database) {
|
|
432
|
+
console.error("Error: --database flag or DB_DATABASE env var is required.");
|
|
433
|
+
process.exit(1);
|
|
434
|
+
}
|
|
435
|
+
const resolved = {
|
|
436
|
+
host,
|
|
437
|
+
port,
|
|
438
|
+
user,
|
|
439
|
+
password,
|
|
440
|
+
database
|
|
441
|
+
};
|
|
442
|
+
console.log(resolved);
|
|
443
|
+
return resolved;
|
|
444
|
+
}
|
|
445
|
+
async function createDb(config) {
|
|
446
|
+
return Db.Provider.create({
|
|
447
|
+
driver: Db.drivers.mysql({
|
|
448
|
+
host: config.host,
|
|
449
|
+
port: config.port,
|
|
450
|
+
user: config.user,
|
|
451
|
+
password: config.password,
|
|
452
|
+
database: config.database,
|
|
453
|
+
multipleStatements: true
|
|
454
|
+
})
|
|
455
|
+
});
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
// src/commands/db-migrations.ts
|
|
459
|
+
async function dbMakeMigration(name) {
|
|
460
|
+
const dir = join(process.cwd(), "infrastructure", "db-migrations");
|
|
461
|
+
const files = await DbMigrations.scaffold({ name, dir });
|
|
462
|
+
const cwd = process.cwd();
|
|
463
|
+
console.log(`Created migration:`);
|
|
464
|
+
console.log(` ${files.ts.replace(cwd + "/", "")}`);
|
|
465
|
+
console.log(` ${files.upSql.replace(cwd + "/", "")}`);
|
|
466
|
+
console.log(` ${files.downSql.replace(cwd + "/", "")}`);
|
|
467
|
+
}
|
|
468
|
+
async function dbMigrate(opts) {
|
|
469
|
+
const config = await resolveDbConfig(opts);
|
|
470
|
+
const db = await createDb(config);
|
|
471
|
+
try {
|
|
472
|
+
const dir = join(process.cwd(), "infrastructure", "db-migrations");
|
|
473
|
+
const results = await DbMigrations.migrate(db, dir, {
|
|
474
|
+
dryRun: opts["dry-run"],
|
|
475
|
+
count: opts.count
|
|
476
|
+
});
|
|
477
|
+
if (!results.length) {
|
|
478
|
+
console.log("No pending migrations.");
|
|
479
|
+
return;
|
|
480
|
+
}
|
|
481
|
+
const prefix = opts["dry-run"] ? "[dry-run] " : "";
|
|
482
|
+
for (const r of results) {
|
|
483
|
+
console.log(`${prefix}Migrated: ${r.filename} (${r.durationMs}ms)`);
|
|
484
|
+
}
|
|
485
|
+
} finally {
|
|
486
|
+
await db.destroy();
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
async function dbRollback(opts) {
|
|
490
|
+
const config = await resolveDbConfig(opts);
|
|
491
|
+
const db = await createDb(config);
|
|
492
|
+
try {
|
|
493
|
+
const dir = join(process.cwd(), "infrastructure", "db-migrations");
|
|
494
|
+
const results = await DbMigrations.rollback(db, dir, {
|
|
495
|
+
dryRun: opts["dry-run"],
|
|
496
|
+
count: opts.count ?? 1
|
|
497
|
+
});
|
|
498
|
+
if (!results.length) {
|
|
499
|
+
console.log("Nothing to rollback.");
|
|
500
|
+
return;
|
|
501
|
+
}
|
|
502
|
+
const prefix = opts["dry-run"] ? "[dry-run] " : "";
|
|
503
|
+
for (const r of results) {
|
|
504
|
+
console.log(`${prefix}Rolled back: ${r.filename} (${r.durationMs}ms)`);
|
|
505
|
+
}
|
|
506
|
+
} finally {
|
|
507
|
+
await db.destroy();
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
async function dbStatus(opts) {
|
|
511
|
+
const config = await resolveDbConfig(opts);
|
|
512
|
+
const db = await createDb(config);
|
|
513
|
+
try {
|
|
514
|
+
const dir = join(process.cwd(), "infrastructure", "db-migrations");
|
|
515
|
+
const statuses = await DbMigrations.status(db, dir);
|
|
516
|
+
if (!statuses.length) {
|
|
517
|
+
console.log("No migrations found.");
|
|
518
|
+
return;
|
|
519
|
+
}
|
|
520
|
+
for (const s of statuses) {
|
|
521
|
+
if (s.applied) {
|
|
522
|
+
console.log(`[x] ${s.filename} (applied ${s.run_on})`);
|
|
523
|
+
} else {
|
|
524
|
+
console.log(`[ ] ${s.filename} (pending)`);
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
} finally {
|
|
528
|
+
await db.destroy();
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
// src/commands/env.ts
|
|
533
|
+
import { Config as Config3 } from "@maestro-js/config";
|
|
534
|
+
function envGenKey() {
|
|
535
|
+
const key = Config3.generateEncryptionKey();
|
|
536
|
+
console.log(key);
|
|
537
|
+
}
|
|
538
|
+
function envEncrypt(opts) {
|
|
539
|
+
const encryptionKey = opts.key || process.env.MAESTRO_ENV_ENCRYPTION_KEY;
|
|
540
|
+
if (!encryptionKey) {
|
|
541
|
+
console.error("Encryption key is required. Use --key or set MAESTRO_ENV_ENCRYPTION_KEY.");
|
|
542
|
+
process.exit(1);
|
|
543
|
+
}
|
|
544
|
+
const envName = opts.env || null;
|
|
545
|
+
Config3.generateEncryptedEnvFile({
|
|
546
|
+
rootDir: process.cwd(),
|
|
547
|
+
envName,
|
|
548
|
+
encryptionKey
|
|
549
|
+
});
|
|
550
|
+
const base = envName ? `.env.${envName}` : ".env";
|
|
551
|
+
console.log(`Encrypted ${base} \u2192 ${base}.encrypted`);
|
|
552
|
+
}
|
|
553
|
+
function envDecrypt(opts) {
|
|
554
|
+
const encryptionKey = opts.key || process.env.MAESTRO_ENV_ENCRYPTION_KEY;
|
|
555
|
+
if (!encryptionKey) {
|
|
556
|
+
console.error("Encryption key is required. Use --key or set MAESTRO_ENV_ENCRYPTION_KEY.");
|
|
557
|
+
process.exit(1);
|
|
558
|
+
}
|
|
559
|
+
const envName = opts.env || null;
|
|
560
|
+
Config3.generateDecryptedEnvFile({
|
|
561
|
+
rootDir: process.cwd(),
|
|
562
|
+
envName,
|
|
563
|
+
encryptionKey,
|
|
564
|
+
force: opts.force
|
|
565
|
+
});
|
|
566
|
+
const base = envName ? `.env.${envName}` : ".env";
|
|
567
|
+
console.log(`Decrypted ${base}.encrypted \u2192 ${base}`);
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
// src/commands/crypt.ts
|
|
571
|
+
import crypto from "crypto";
|
|
572
|
+
function cryptMakeKey() {
|
|
573
|
+
const key = crypto.randomBytes(32).toString("base64");
|
|
574
|
+
console.log(key);
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
// src/commands/hash.ts
|
|
578
|
+
import crypto2 from "crypto";
|
|
579
|
+
function hashMakeKey() {
|
|
580
|
+
const key = crypto2.randomBytes(32).toString("base64");
|
|
581
|
+
console.log(key);
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
// src/commands/config.ts
|
|
585
|
+
import { execSync as execSync3 } from "child_process";
|
|
586
|
+
import { Config as Config4 } from "@maestro-js/config";
|
|
587
|
+
async function configRun(opts) {
|
|
588
|
+
const configName = opts.config;
|
|
589
|
+
if (!configName) {
|
|
590
|
+
console.error("Missing required --config <name> option");
|
|
591
|
+
process.exit(1);
|
|
592
|
+
}
|
|
593
|
+
const rootDir = opts.root ?? process.cwd();
|
|
594
|
+
const envName = opts.env ?? null;
|
|
595
|
+
const encryptionKey = opts.key ?? process.env["MAESTRO_ENV_ENCRYPTION_KEY"] ?? null;
|
|
596
|
+
const config = await Config4.getConfig({ rootDir, configName, envName, encryptionKey });
|
|
597
|
+
const command = opts._.join(" ");
|
|
598
|
+
if (!command) {
|
|
599
|
+
console.error("No command provided. Usage: maestro config:run --config <name> [options] -- <command>");
|
|
600
|
+
process.exit(1);
|
|
601
|
+
}
|
|
602
|
+
execSync3(command, {
|
|
603
|
+
stdio: "inherit",
|
|
604
|
+
env: { ...process.env, ...config }
|
|
605
|
+
});
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
// src/commands/components.ts
|
|
609
|
+
import { Components } from "@maestro-js/components";
|
|
610
|
+
async function componentsList() {
|
|
611
|
+
const names = await Components.list();
|
|
612
|
+
for (const name of names) {
|
|
613
|
+
console.log(name);
|
|
614
|
+
}
|
|
615
|
+
}
|
|
616
|
+
async function componentsAdd(name, opts) {
|
|
617
|
+
const components = opts.all ? await Components.list() : [name, ...opts._ || []];
|
|
618
|
+
const directory = opts.directory || process.cwd();
|
|
619
|
+
const written = await Components.add({ components, directory, overwrite: opts.overwrite });
|
|
620
|
+
for (const file of written) {
|
|
621
|
+
console.log(file);
|
|
622
|
+
}
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
// src/commands/upgrade.ts
|
|
626
|
+
import { execSync as execSync4 } from "child_process";
|
|
627
|
+
import fs from "fs";
|
|
628
|
+
import path from "path";
|
|
629
|
+
import { fileURLToPath } from "url";
|
|
630
|
+
import prompts4 from "prompts";
|
|
631
|
+
var onCancel2 = () => process.exit(0);
|
|
632
|
+
async function upgrade() {
|
|
633
|
+
const current = getCurrentVersion();
|
|
634
|
+
console.log(`Current @maestro-js version: ${current}`);
|
|
635
|
+
const versions = fetchAvailableVersions();
|
|
636
|
+
const newer = filterNewerVersions(versions, current);
|
|
637
|
+
if (newer.length === 0) {
|
|
638
|
+
console.log("Already on the latest version.");
|
|
639
|
+
return;
|
|
640
|
+
}
|
|
641
|
+
const choices = buildVersionChoices(newer, current);
|
|
642
|
+
const target = await promptForVersion(choices);
|
|
643
|
+
const updated = findAndUpdatePackageJsonFiles(target);
|
|
644
|
+
if (updated === 0) {
|
|
645
|
+
console.log("No @maestro-js/* dependencies found in any package.json.");
|
|
646
|
+
return;
|
|
647
|
+
}
|
|
648
|
+
console.log(`
|
|
649
|
+
Updated ${updated} package.json file${updated === 1 ? "" : "s"}.`);
|
|
650
|
+
await runInstall();
|
|
651
|
+
}
|
|
652
|
+
function getCurrentVersion() {
|
|
653
|
+
const dir = path.dirname(fileURLToPath(import.meta.url));
|
|
654
|
+
const raw = fs.readFileSync(path.join(dir, "../package.json"), "utf-8");
|
|
655
|
+
const pkg = JSON.parse(raw);
|
|
656
|
+
return pkg.version;
|
|
657
|
+
}
|
|
658
|
+
function fetchAvailableVersions() {
|
|
659
|
+
try {
|
|
660
|
+
const raw = execSync4("npm view @maestro-js/cli versions --json", { encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] });
|
|
661
|
+
const parsed = JSON.parse(raw);
|
|
662
|
+
if (typeof parsed === "string") return [parsed];
|
|
663
|
+
if (Array.isArray(parsed)) return parsed;
|
|
664
|
+
return [];
|
|
665
|
+
} catch {
|
|
666
|
+
console.error("Failed to fetch available versions from npm registry.");
|
|
667
|
+
process.exit(1);
|
|
668
|
+
}
|
|
669
|
+
}
|
|
670
|
+
function filterNewerVersions(versions, current) {
|
|
671
|
+
return versions.filter((v) => compareSemver(v, current) > 0);
|
|
672
|
+
}
|
|
673
|
+
function buildVersionChoices(versions, current) {
|
|
674
|
+
const currentMajor = parseSemver(current).major;
|
|
675
|
+
const byMajor = /* @__PURE__ */ new Map();
|
|
676
|
+
for (const v of versions) {
|
|
677
|
+
const { major } = parseSemver(v);
|
|
678
|
+
const existing = byMajor.get(major);
|
|
679
|
+
if (existing) {
|
|
680
|
+
existing.push(v);
|
|
681
|
+
} else {
|
|
682
|
+
byMajor.set(major, [v]);
|
|
683
|
+
}
|
|
684
|
+
}
|
|
685
|
+
const choices = [];
|
|
686
|
+
for (const [major, group] of [...byMajor.entries()].sort((a, b) => a[0] - b[0])) {
|
|
687
|
+
const latest = group.sort(compareSemver).at(-1);
|
|
688
|
+
const label = major > currentMajor ? `${latest} [major upgrade]` : latest;
|
|
689
|
+
choices.push({ title: label, value: latest });
|
|
690
|
+
}
|
|
691
|
+
return choices.reverse();
|
|
692
|
+
}
|
|
693
|
+
async function promptForVersion(choices) {
|
|
694
|
+
const { version: version2 } = await prompts4(
|
|
695
|
+
{
|
|
696
|
+
type: "select",
|
|
697
|
+
name: "version",
|
|
698
|
+
message: "Which version?",
|
|
699
|
+
choices
|
|
700
|
+
},
|
|
701
|
+
{ onCancel: onCancel2 }
|
|
702
|
+
);
|
|
703
|
+
return version2;
|
|
704
|
+
}
|
|
705
|
+
function findAndUpdatePackageJsonFiles(targetVersion) {
|
|
706
|
+
const root = process.cwd();
|
|
707
|
+
const files = findPackageJsonFiles(root);
|
|
708
|
+
let updated = 0;
|
|
709
|
+
for (const filePath of files) {
|
|
710
|
+
if (updateMaestroDependencies(filePath, targetVersion)) {
|
|
711
|
+
updated++;
|
|
712
|
+
}
|
|
713
|
+
}
|
|
714
|
+
return updated;
|
|
715
|
+
}
|
|
716
|
+
function findPackageJsonFiles(dir) {
|
|
717
|
+
const results = [];
|
|
718
|
+
const skipDirs = /* @__PURE__ */ new Set(["node_modules", ".git", "dist", ".next", ".nuxt"]);
|
|
719
|
+
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
720
|
+
if (entry.isDirectory()) {
|
|
721
|
+
if (!skipDirs.has(entry.name)) {
|
|
722
|
+
results.push(...findPackageJsonFiles(path.join(dir, entry.name)));
|
|
723
|
+
}
|
|
724
|
+
} else if (entry.name === "package.json") {
|
|
725
|
+
results.push(path.join(dir, entry.name));
|
|
726
|
+
}
|
|
727
|
+
}
|
|
728
|
+
return results;
|
|
729
|
+
}
|
|
730
|
+
function updateMaestroDependencies(filePath, targetVersion) {
|
|
731
|
+
const raw = fs.readFileSync(filePath, "utf-8");
|
|
732
|
+
const pkg = JSON.parse(raw);
|
|
733
|
+
let changed = false;
|
|
734
|
+
for (const depKey of ["dependencies", "devDependencies", "peerDependencies", "optionalDependencies"]) {
|
|
735
|
+
const deps = pkg[depKey];
|
|
736
|
+
if (!deps || typeof deps !== "object") continue;
|
|
737
|
+
for (const [name, value] of Object.entries(deps)) {
|
|
738
|
+
if (!name.startsWith("@maestro-js/")) continue;
|
|
739
|
+
if (value.startsWith("workspace:")) continue;
|
|
740
|
+
const prefix = value.match(/^([^\d]*)/)?.[1] ?? "";
|
|
741
|
+
const newValue = `${prefix}${targetVersion}`;
|
|
742
|
+
if (value !== newValue) {
|
|
743
|
+
;
|
|
744
|
+
deps[name] = newValue;
|
|
745
|
+
changed = true;
|
|
746
|
+
}
|
|
747
|
+
}
|
|
748
|
+
}
|
|
749
|
+
if (changed) {
|
|
750
|
+
const indent = detectIndent(raw);
|
|
751
|
+
const trailingNewline = raw.endsWith("\n");
|
|
752
|
+
let output = JSON.stringify(pkg, null, indent);
|
|
753
|
+
if (trailingNewline) output += "\n";
|
|
754
|
+
fs.writeFileSync(filePath, output, "utf-8");
|
|
755
|
+
console.log(` Updated ${path.relative(process.cwd(), filePath)}`);
|
|
756
|
+
}
|
|
757
|
+
return changed;
|
|
758
|
+
}
|
|
759
|
+
async function runInstall() {
|
|
760
|
+
const pm = detectPackageManager();
|
|
761
|
+
console.log(`
|
|
762
|
+
Running ${pm} install...`);
|
|
763
|
+
try {
|
|
764
|
+
execSync4(`${pm} install`, { stdio: "inherit", cwd: process.cwd() });
|
|
765
|
+
console.log("Done.");
|
|
766
|
+
} catch {
|
|
767
|
+
console.warn(`
|
|
768
|
+
${pm} install failed. package.json files have been updated \u2014 run install manually.`);
|
|
769
|
+
}
|
|
770
|
+
}
|
|
771
|
+
function detectPackageManager() {
|
|
772
|
+
const root = process.cwd();
|
|
773
|
+
try {
|
|
774
|
+
const raw = fs.readFileSync(path.join(root, "package.json"), "utf-8");
|
|
775
|
+
const pkg = JSON.parse(raw);
|
|
776
|
+
if (pkg.packageManager) {
|
|
777
|
+
const name = pkg.packageManager.split("@")[0];
|
|
778
|
+
if (["pnpm", "yarn", "npm"].includes(name)) return name;
|
|
779
|
+
}
|
|
780
|
+
} catch {
|
|
781
|
+
}
|
|
782
|
+
if (fs.existsSync(path.join(root, "pnpm-lock.yaml"))) return "pnpm";
|
|
783
|
+
if (fs.existsSync(path.join(root, "yarn.lock"))) return "yarn";
|
|
784
|
+
return "npm";
|
|
785
|
+
}
|
|
786
|
+
function detectIndent(json) {
|
|
787
|
+
const match = json.match(/^[\t ]*(?=")/m);
|
|
788
|
+
return match?.[0]?.length ?? 2;
|
|
789
|
+
}
|
|
790
|
+
function parseSemver(version2) {
|
|
791
|
+
const idx = version2.indexOf("-");
|
|
792
|
+
const core = idx === -1 ? version2 : version2.slice(0, idx);
|
|
793
|
+
const pre = idx === -1 ? "" : version2.slice(idx + 1);
|
|
794
|
+
const [major = 0, minor = 0, patch = 0] = core.split(".").map(Number);
|
|
795
|
+
return { major, minor, patch, prerelease: pre ? pre.split(/[.-]/) : [] };
|
|
796
|
+
}
|
|
797
|
+
var PRE_ORDER = { alpha: 0, beta: 1, rc: 2 };
|
|
798
|
+
function compareSemver(a, b) {
|
|
799
|
+
const sa = parseSemver(a);
|
|
800
|
+
const sb = parseSemver(b);
|
|
801
|
+
const d = sa.major - sb.major || sa.minor - sb.minor || sa.patch - sb.patch;
|
|
802
|
+
if (d !== 0) return d;
|
|
803
|
+
if (sa.prerelease.length === 0 && sb.prerelease.length === 0) return 0;
|
|
804
|
+
if (sa.prerelease.length === 0) return 1;
|
|
805
|
+
if (sb.prerelease.length === 0) return -1;
|
|
806
|
+
const maxLen = Math.max(sa.prerelease.length, sb.prerelease.length);
|
|
807
|
+
for (let i = 0; i < maxLen; i++) {
|
|
808
|
+
const pa = sa.prerelease[i];
|
|
809
|
+
const pb = sb.prerelease[i];
|
|
810
|
+
if (pa === void 0) return -1;
|
|
811
|
+
if (pb === void 0) return 1;
|
|
812
|
+
if (pa === pb) continue;
|
|
813
|
+
const na = PRE_ORDER[pa] ?? -1;
|
|
814
|
+
const nb = PRE_ORDER[pb] ?? -1;
|
|
815
|
+
if (na !== -1 || nb !== -1) return na - nb;
|
|
816
|
+
const numA = Number(pa);
|
|
817
|
+
const numB = Number(pb);
|
|
818
|
+
if (!Number.isNaN(numA) && !Number.isNaN(numB)) return numA - numB;
|
|
819
|
+
return pa < pb ? -1 : 1;
|
|
820
|
+
}
|
|
821
|
+
return 0;
|
|
822
|
+
}
|
|
823
|
+
|
|
824
|
+
// src/bin.ts
|
|
825
|
+
var require2 = createRequire(import.meta.url);
|
|
826
|
+
var { version } = require2("../package.json");
|
|
827
|
+
var prog = sade("maestro").version(version);
|
|
828
|
+
prog.command("aws:make-wireguard-client [id]").describe("Add a WireGuard client peer to an existing VPN server").option("--region", "AWS region (env: AWS_REGION)").option("--name", "Label for the peer").option("--endpoint", "Override auto-detected server IP").option("--dns", "DNS for client config").option("--allowed-ips", "Client AllowedIPs", "10.0.0.0/16, 10.100.0.0/24").action(awsMakeWireguardClient);
|
|
829
|
+
prog.command("db:make-migration <name>").describe("Create a new database migration").action(dbMakeMigration);
|
|
830
|
+
prog.command("db:make-container").describe("Create a Docker database container").action(createDockerDb);
|
|
831
|
+
prog.command("db:migrate").describe("Run pending database migrations").option("--host", "Database host (env: DB_HOST)").option("--port", "Database port (env: DB_PORT)").option("--user", "Database user (env: DB_USER)").option("--password", "Database password (env: DB_PASSWORD)").option("--database", "Database name (env: DB_DATABASE)").option("--dry-run", "Preview migrations without executing").option("--count", "Number of migrations to run").action(dbMigrate);
|
|
832
|
+
prog.command("db:rollback").describe("Rollback database migrations").option("--host", "Database host (env: DB_HOST)").option("--port", "Database port (env: DB_PORT)").option("--user", "Database user (env: DB_USER)").option("--password", "Database password (env: DB_PASSWORD)").option("--database", "Database name (env: DB_DATABASE)").option("--dry-run", "Preview rollback without executing").option("--count", "Number of migrations to rollback", 1).action(dbRollback);
|
|
833
|
+
prog.command("db:status").describe("Show database migration status").option("--host", "Database host (env: DB_HOST)").option("--port", "Database port (env: DB_PORT)").option("--user", "Database user (env: DB_USER)").option("--password", "Database password (env: DB_PASSWORD)").option("--database", "Database name (env: DB_DATABASE)").action(dbStatus);
|
|
834
|
+
prog.command("env:generate-key").describe("Generate an encryption key").action(envGenKey);
|
|
835
|
+
prog.command("crypt:make-key").describe("Generate an encryption key for use with @maestro-js/crypt").action(cryptMakeKey);
|
|
836
|
+
prog.command("hash:make-key").describe("Generate a signing key for use with @maestro-js/hash").action(hashMakeKey);
|
|
837
|
+
prog.command("env:encrypt").describe("Encrypt a .env file").option("--env", "Environment name (e.g. production)").option("--key", "Encryption key (env: MAESTRO_ENV_ENCRYPTION_KEY)").action(envEncrypt);
|
|
838
|
+
prog.command("env:decrypt").describe("Decrypt a .env.encrypted file").option("--env", "Environment name (e.g. production)").option("--key", "Encryption key (env: MAESTRO_ENV_ENCRYPTION_KEY)").option("--force", "Overwrite existing .env file").action(envDecrypt);
|
|
839
|
+
prog.command("components:list").describe("List available components").action(componentsList);
|
|
840
|
+
prog.command("components:add [name]").describe("Add components to the current project").option("--directory", "Target directory (default: cwd)").option("--overwrite", "Overwrite existing files").option("--all", "Add all available components").action(componentsAdd);
|
|
841
|
+
prog.command("config:run").describe("Resolve config and run a command with the config values as environment variables").option("--config", "Config name (required)").option("--root", "Project root directory (default: cwd)").option("--env", "Environment name (e.g. production)").option("--key", "Encryption key (env: MAESTRO_ENV_ENCRYPTION_KEY)").action(configRun);
|
|
842
|
+
prog.command("upgrade").describe("Upgrade @maestro-js/* packages to a newer version").action(upgrade);
|
|
843
|
+
prog.command("agent:make [name]").describe("Launch an interactive shell in an isolated Docker agent container").option("--repo", "Path to the repository (default: cwd)").option("--env", "Environment variable as KEY=VALUE (repeatable)").option("--memory", "Memory limit (env: AGENT_MEMORY)").option("--cpus", "Number of CPUs (env: AGENT_CPUS)").action(agentMake);
|
|
844
|
+
prog.command("agent:kill [name]").describe("Kill an agent container (prompts if no name given)").action(agentKill);
|
|
845
|
+
prog.command("agent:shell [name]").describe("Shell into an agent container (prompts if no name given)").action(agentShell);
|
|
846
|
+
prog.command("agent:pull [name]").describe("Pull container changes onto the host working tree (prompts if no name given)").option("--repo", "Path to the repository (default: cwd git root)").action(agentPull);
|
|
847
|
+
prog.command("agent:open [name]").describe("Open an agent container in VS Code (prompts if no name given)").action(agentOpen);
|
|
848
|
+
prog.parse(process.argv);
|
package/package.json
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@maestro-js/cli",
|
|
3
|
+
"description": "Skill for the @maestro-js/cli package, the command-line interface for the Maestro framework. Use when working with @maestro-js/cli or the maestro-cli package directory. Key capabilities: database migrations (create, run, rollback, status), environment encryption/decryption, Docker database container creation, and AWS WireGuard VPN client provisioning. Trigger when adding CLI commands, modifying migration workflows, working with env encryption, or integrating new Maestro packages into the CLI.",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"bin": {
|
|
6
|
+
"maestro": "./dist/bin.js"
|
|
7
|
+
},
|
|
8
|
+
"exports": {},
|
|
9
|
+
"dependencies": {
|
|
10
|
+
"@aws-sdk/client-ssm": "^3.750.0",
|
|
11
|
+
"@aws-sdk/client-sts": "^3.993.0",
|
|
12
|
+
"prompts": "^2.4.2",
|
|
13
|
+
"sade": "^1.8.1",
|
|
14
|
+
"@maestro-js/components": "1.0.0-alpha.0",
|
|
15
|
+
"@maestro-js/config": "1.0.0-alpha.0",
|
|
16
|
+
"@maestro-js/db-migrate": "1.0.0-alpha.0",
|
|
17
|
+
"@maestro-js/agent-container": "1.0.0-alpha.0",
|
|
18
|
+
"@maestro-js/db": "1.0.0-alpha.0"
|
|
19
|
+
},
|
|
20
|
+
"devDependencies": {
|
|
21
|
+
"@types/node": "^22.19.11",
|
|
22
|
+
"@types/prompts": "^2.4.9"
|
|
23
|
+
},
|
|
24
|
+
"version": "1.0.0-alpha.0",
|
|
25
|
+
"publishConfig": {
|
|
26
|
+
"access": "restricted"
|
|
27
|
+
},
|
|
28
|
+
"files": [
|
|
29
|
+
"dist"
|
|
30
|
+
],
|
|
31
|
+
"license": "UNLICENSED",
|
|
32
|
+
"engines": {
|
|
33
|
+
"node": ">=22.18.0"
|
|
34
|
+
},
|
|
35
|
+
"repository": "https://github.com/Marcato-Partners/maestro-js",
|
|
36
|
+
"scripts": {
|
|
37
|
+
"build": "tsup",
|
|
38
|
+
"watch": "tsup --watch",
|
|
39
|
+
"typecheck": "tsc --noEmit",
|
|
40
|
+
"format": "prettier --write src/",
|
|
41
|
+
"lint": "prettier --check src/"
|
|
42
|
+
}
|
|
43
|
+
}
|