canopy-deploy 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.
@@ -0,0 +1,3 @@
1
+ #!/usr/bin/env node
2
+ export {};
3
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":""}
package/dist/index.js ADDED
@@ -0,0 +1,378 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
4
+ if (k2 === undefined) k2 = k;
5
+ var desc = Object.getOwnPropertyDescriptor(m, k);
6
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
7
+ desc = { enumerable: true, get: function() { return m[k]; } };
8
+ }
9
+ Object.defineProperty(o, k2, desc);
10
+ }) : (function(o, m, k, k2) {
11
+ if (k2 === undefined) k2 = k;
12
+ o[k2] = m[k];
13
+ }));
14
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
15
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
16
+ }) : function(o, v) {
17
+ o["default"] = v;
18
+ });
19
+ var __importStar = (this && this.__importStar) || (function () {
20
+ var ownKeys = function(o) {
21
+ ownKeys = Object.getOwnPropertyNames || function (o) {
22
+ var ar = [];
23
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
24
+ return ar;
25
+ };
26
+ return ownKeys(o);
27
+ };
28
+ return function (mod) {
29
+ if (mod && mod.__esModule) return mod;
30
+ var result = {};
31
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
32
+ __setModuleDefault(result, mod);
33
+ return result;
34
+ };
35
+ })();
36
+ Object.defineProperty(exports, "__esModule", { value: true });
37
+ const commander_1 = require("commander");
38
+ const scanner_1 = require("@canopy/scanner");
39
+ const deploy_1 = require("@canopy/deploy");
40
+ const path = __importStar(require("path"));
41
+ const fs = __importStar(require("fs"));
42
+ const c = {
43
+ reset: '\x1b[0m', bold: '\x1b[1m', dim: '\x1b[2m',
44
+ red: '\x1b[31m', green: '\x1b[32m', yellow: '\x1b[33m',
45
+ cyan: '\x1b[36m', white: '\x1b[37m',
46
+ bgRed: '\x1b[41m', bgGreen: '\x1b[42m', bgYellow: '\x1b[43m',
47
+ };
48
+ const SEVERITY_COLORS = {
49
+ critical: c.red, high: c.yellow, medium: c.cyan, low: c.dim,
50
+ };
51
+ const SEVERITY_LABELS = {
52
+ critical: `${c.bgRed}${c.white}${c.bold} CRITICAL ${c.reset}`,
53
+ high: `${c.bgYellow}${c.bold} HIGH ${c.reset}`,
54
+ medium: `${c.cyan}${c.bold}MEDIUM${c.reset}`,
55
+ low: `${c.dim}LOW${c.reset}`,
56
+ };
57
+ function scoreColor(score) {
58
+ if (score < 40)
59
+ return c.red;
60
+ if (score < 70)
61
+ return c.yellow;
62
+ return c.green;
63
+ }
64
+ function printScore(score) {
65
+ console.log();
66
+ console.log(` ${scoreColor(score)}${c.bold}${score}${c.reset}${c.dim}/100${c.reset} Security Score`);
67
+ console.log();
68
+ }
69
+ function printFindings(findings) {
70
+ if (findings.length === 0) {
71
+ console.log(` ${c.green}✓${c.reset} No security issues found.`);
72
+ console.log();
73
+ return;
74
+ }
75
+ const groups = { critical: [], high: [], medium: [], low: [] };
76
+ for (const f of findings)
77
+ groups[f.severity].push(f);
78
+ for (const severity of ['critical', 'high', 'medium', 'low']) {
79
+ const items = groups[severity];
80
+ if (items.length === 0)
81
+ continue;
82
+ console.log(` ${SEVERITY_LABELS[severity]} ${c.dim}(${items.length})${c.reset}`);
83
+ console.log();
84
+ for (const f of items) {
85
+ const loc = f.line ? `${f.file}:${f.line}` : f.file;
86
+ console.log(` ${SEVERITY_COLORS[severity]}●${c.reset} ${c.bold}${f.title}${c.reset}`);
87
+ console.log(` ${c.dim}${loc}${c.reset}`);
88
+ if (f.snippet) {
89
+ console.log();
90
+ for (const line of f.snippet.split('\n'))
91
+ console.log(` ${c.dim}${line}${c.reset}`);
92
+ }
93
+ console.log();
94
+ console.log(` ${f.description.split('\n\n')[0]}`);
95
+ console.log();
96
+ console.log(` ${c.green}Fix:${c.reset} ${f.fix}`);
97
+ console.log();
98
+ }
99
+ }
100
+ }
101
+ function printMeta(meta, summary) {
102
+ const parts = [`${meta.filesScanned} files scanned`, `${meta.timeMs}ms`];
103
+ const counts = [];
104
+ if (summary.critical > 0)
105
+ counts.push(`${c.red}${summary.critical} critical${c.reset}`);
106
+ if (summary.high > 0)
107
+ counts.push(`${c.yellow}${summary.high} high${c.reset}`);
108
+ if (summary.medium > 0)
109
+ counts.push(`${c.cyan}${summary.medium} medium${c.reset}`);
110
+ if (summary.low > 0)
111
+ counts.push(`${c.dim}${summary.low} low${c.reset}`);
112
+ console.log(counts.length > 0
113
+ ? ` ${c.dim}${parts.join(' · ')} · ${c.reset}${counts.join(', ')}`
114
+ : ` ${c.dim}${parts.join(' · ')}${c.reset}`);
115
+ console.log();
116
+ }
117
+ const PHASE_ICONS = {
118
+ scan: '🔍', detect: '🔎', state: '💾', provision: '☁️ ',
119
+ dockerfile: '🐳', upload: '📦', build: '🔨', container: '▶️ ',
120
+ nginx: '🌐', ssl: '🔒', env: '🔑', vpn: '🛡️ ', default: ' ',
121
+ };
122
+ commander_1.program.name('canopy').description('Security scanner & deploy tool for vibecoded apps').version('1.0.0');
123
+ commander_1.program.command('scan [path]').description('Scan a project for security issues')
124
+ .option('--json', 'Output raw JSON')
125
+ .action((targetPath, opts) => {
126
+ const resolved = path.resolve(targetPath || process.cwd());
127
+ try {
128
+ const result = (0, scanner_1.scan)(resolved);
129
+ if (opts.json) {
130
+ console.log(JSON.stringify(result, null, 2));
131
+ process.exit(result.summary.critical > 0 ? 1 : 0);
132
+ }
133
+ console.log();
134
+ console.log(` ${c.bold}canopy scan${c.reset} ${c.dim}${resolved}${c.reset}`);
135
+ printScore(result.score);
136
+ printFindings(result.findings);
137
+ printMeta(result.meta, result.summary);
138
+ process.exit(result.summary.critical > 0 ? 1 : 0);
139
+ }
140
+ catch (err) {
141
+ console.error(`${c.red}Error:${c.reset} ${err.message}`);
142
+ process.exit(2);
143
+ }
144
+ });
145
+ commander_1.program.command('init').description('Initialize Canopy config').action(() => {
146
+ const config = (0, deploy_1.loadConfig)();
147
+ const token = process.env.CANOPY_HETZNER_TOKEN;
148
+ if (token)
149
+ console.log(` ${c.green}✓${c.reset} Hetzner token found in CANOPY_HETZNER_TOKEN env var`);
150
+ else if (config.hetznerToken)
151
+ console.log(` ${c.green}✓${c.reset} Hetzner token found in config`);
152
+ else
153
+ console.log(` ${c.yellow}!${c.reset} No Hetzner token. Set CANOPY_HETZNER_TOKEN env var to deploy.`);
154
+ (0, deploy_1.saveConfig)(config);
155
+ console.log(` ${c.green}✓${c.reset} Config saved to ~/.canopy/config.json`);
156
+ });
157
+ commander_1.program.command('deploy [path]').description('Deploy a project to a Hetzner VPS')
158
+ .requiredOption('--name <name>', 'App name (used for subdomain)')
159
+ .option('--json', 'Output raw JSON')
160
+ .option('--verbose', 'Show detailed deploy progress')
161
+ .option('--force', 'Skip scanner gate (deploy with critical findings)')
162
+ .option('--new', 'Force new server (don\'t reuse existing)')
163
+ .option('--region <region>', 'Server region (fsn1, nbg1, hel1, ash, hil, sin)', 'hel1')
164
+ .option('--env-file <path>', 'Path to .env file to load')
165
+ .option('--no-ssl', 'Skip SSL/certbot setup')
166
+ .option('--private', 'Make app VPN-only (WireGuard)')
167
+ .action(async (targetPath, opts) => {
168
+ const projectPath = path.resolve(targetPath || process.cwd());
169
+ // Load env vars from --env-file if provided
170
+ let envVars;
171
+ if (opts.envFile) {
172
+ const envPath = path.resolve(opts.envFile);
173
+ if (!fs.existsSync(envPath)) {
174
+ console.error(` ${c.red}Error:${c.reset} Env file not found: ${envPath}`);
175
+ process.exit(2);
176
+ }
177
+ envVars = {};
178
+ const lines = fs.readFileSync(envPath, 'utf-8').split('\n');
179
+ for (const line of lines) {
180
+ const trimmed = line.trim();
181
+ if (!trimmed || trimmed.startsWith('#'))
182
+ continue;
183
+ const eqIdx = trimmed.indexOf('=');
184
+ if (eqIdx > 0)
185
+ envVars[trimmed.slice(0, eqIdx)] = trimmed.slice(eqIdx + 1);
186
+ }
187
+ }
188
+ const verboseLog = opts.verbose
189
+ ? (phase, msg) => { const icon = PHASE_ICONS[phase] || PHASE_ICONS.default; console.log(` ${c.dim}${icon}${c.reset} ${c.dim}[${phase}]${c.reset} ${msg}`); }
190
+ : undefined;
191
+ try {
192
+ console.log();
193
+ console.log(` ${c.bold}canopy deploy${c.reset} ${c.dim}${projectPath}${c.reset}`);
194
+ console.log(` ${c.dim}name: ${opts.name}${c.reset}`);
195
+ console.log();
196
+ const result = await (0, deploy_1.deploy)({
197
+ projectPath, name: opts.name, env: envVars,
198
+ force: opts.force, newServer: opts.new, region: opts.region,
199
+ noSsl: opts.ssl === false, private: opts.private, log: verboseLog,
200
+ });
201
+ if (opts.json) {
202
+ console.log(JSON.stringify(result, null, 2));
203
+ process.exit(result.status === 'deployed' ? 0 : 1);
204
+ }
205
+ if (result.status === 'blocked') {
206
+ console.log(` ${c.red}✗${c.reset} Deploy blocked: ${result.reason}`);
207
+ if (result.scan) {
208
+ printScore(result.scan.score);
209
+ printFindings(result.scan.findings);
210
+ }
211
+ process.exit(1);
212
+ }
213
+ if (result.status === 'build-failed') {
214
+ console.log(` ${c.red}✗${c.reset} Build failed:`);
215
+ console.log(result.error);
216
+ process.exit(1);
217
+ }
218
+ if (result.status === 'run-failed') {
219
+ console.log(` ${c.red}✗${c.reset} Container failed to start:`);
220
+ console.log(result.error);
221
+ process.exit(1);
222
+ }
223
+ console.log(` ${c.green}✓${c.reset} Deployed`);
224
+ console.log(` ${c.dim}URL:${c.reset} ${result.url}`);
225
+ console.log(` ${c.dim}IP:${c.reset} ${result.ip}:${result.port}`);
226
+ console.log(` ${c.dim}Framework:${c.reset} ${result.framework}`);
227
+ console.log(` ${c.dim}Score:${c.reset} ${result.scan?.score}/100`);
228
+ if (result.vpnConfig) {
229
+ console.log();
230
+ console.log(` ${c.bold}🔒 VPN Config${c.reset} (import into WireGuard app):`);
231
+ console.log(` ${c.dim}${'─'.repeat(50)}${c.reset}`);
232
+ for (const line of result.vpnConfig.split('\n')) {
233
+ console.log(` ${c.dim}${line}${c.reset}`);
234
+ }
235
+ console.log(` ${c.dim}${'─'.repeat(50)}${c.reset}`);
236
+ console.log(` ${c.yellow}This app is VPN-only.${c.reset}`);
237
+ console.log(` ${c.dim}1. Import config into WireGuard app${c.reset}`);
238
+ console.log(` ${c.dim}2. Activate the tunnel${c.reset}`);
239
+ console.log(` ${c.dim}3. Access via: ${result.url}${c.reset}`);
240
+ console.log(` ${c.dim}Note: If using Chrome, disable "Use secure DNS" in chrome://settings/security${c.reset}`);
241
+ }
242
+ console.log();
243
+ }
244
+ catch (err) {
245
+ console.error(` ${c.red}Error:${c.reset} ${err.message}`);
246
+ process.exit(2);
247
+ }
248
+ });
249
+ commander_1.program.command('status <name>').description('Check app status').option('--json', 'Output raw JSON')
250
+ .action(async (name, opts) => {
251
+ try {
252
+ const result = await (0, deploy_1.getStatus)(name);
253
+ if (opts.json) {
254
+ console.log(JSON.stringify(result, null, 2));
255
+ return;
256
+ }
257
+ if (result.status === 'not-found') {
258
+ console.log(` ${c.yellow}!${c.reset} No deployment found for "${name}"`);
259
+ process.exit(1);
260
+ }
261
+ console.log();
262
+ console.log(` ${c.bold}${name}${c.reset}`);
263
+ console.log(` ${c.dim}Container:${c.reset} ${result.container}`);
264
+ console.log(` ${c.dim}URL:${c.reset} ${result.url}`);
265
+ console.log(` ${c.dim}IP:${c.reset} ${result.ip}`);
266
+ console.log(` ${c.dim}Framework:${c.reset} ${result.framework}`);
267
+ console.log(` ${c.dim}Deployed:${c.reset} ${result.lastDeploy}`);
268
+ console.log();
269
+ }
270
+ catch (err) {
271
+ console.error(` ${c.red}Error:${c.reset} ${err.message}`);
272
+ process.exit(2);
273
+ }
274
+ });
275
+ commander_1.program.command('logs <name>').description('Get app logs').option('--lines <n>', 'Number of lines', '100')
276
+ .action(async (name, opts) => {
277
+ try {
278
+ const result = await (0, deploy_1.getLogs)(name, parseInt(opts.lines, 10));
279
+ if (result.status === 'not-found') {
280
+ console.log(` ${c.yellow}!${c.reset} No deployment found for "${name}"`);
281
+ process.exit(1);
282
+ }
283
+ console.log(result.logs);
284
+ }
285
+ catch (err) {
286
+ console.error(` ${c.red}Error:${c.reset} ${err.message}`);
287
+ process.exit(2);
288
+ }
289
+ });
290
+ commander_1.program.command('list').description('List all deployments').option('--json', 'Output raw JSON')
291
+ .action((opts) => {
292
+ const state = (0, deploy_1.listDeployments)();
293
+ if (opts.json) {
294
+ console.log(JSON.stringify(state, null, 2));
295
+ return;
296
+ }
297
+ const appNames = Object.keys(state.apps || {});
298
+ if (appNames.length === 0) {
299
+ console.log(` ${c.dim}No deployments yet.${c.reset}`);
300
+ return;
301
+ }
302
+ console.log();
303
+ // Group apps by server
304
+ const serverApps = {};
305
+ for (const [name, app] of Object.entries(state.apps)) {
306
+ if (!serverApps[app.serverId])
307
+ serverApps[app.serverId] = [];
308
+ serverApps[app.serverId].push(name);
309
+ }
310
+ for (const [srvId, apps] of Object.entries(serverApps)) {
311
+ const srv = state.servers?.[srvId];
312
+ const ip = srv?.ip || 'unknown';
313
+ const loc = srv?.location || '?';
314
+ console.log(` ${c.dim}${srvId}${c.reset} ${c.dim}${ip} (${loc})${c.reset} ${c.dim}${apps.length} app(s)${c.reset}`);
315
+ for (const name of apps) {
316
+ const app = state.apps[name];
317
+ console.log(` ${c.bold}${name}${c.reset} ${c.dim}:${app.port} ${app.framework} ${app.private ? '🔒 private' : 'public'} ${app.lastDeploy || 'pending'}${c.reset}`);
318
+ }
319
+ console.log();
320
+ }
321
+ });
322
+ commander_1.program.command('destroy <name>').description('Remove an app (deletes server if last app)')
323
+ .action(async (name) => {
324
+ try {
325
+ (0, deploy_1.validateAppName)(name);
326
+ const app = (0, deploy_1.getDeployment)(name);
327
+ if (!app) {
328
+ console.log(` ${c.yellow}!${c.reset} No deployment found for "${name}"`);
329
+ process.exit(1);
330
+ }
331
+ const srvInfo = (0, deploy_1.getServerForApp)(name);
332
+ const isLastApp = srvInfo ? srvInfo.server.apps.length <= 1 : false;
333
+ // Step 1: If last app, delete Hetzner server FIRST (before touching local state)
334
+ if (isLastApp && srvInfo) {
335
+ console.log(` Last app on server — deleting server ${srvInfo.server.id} (${srvInfo.server.ip})...`);
336
+ try {
337
+ await (0, deploy_1.deleteServer)(srvInfo.server.id);
338
+ }
339
+ catch (err) {
340
+ // 404 = server already gone on Hetzner, safe to clean up local state
341
+ if (err.message?.includes('404')) {
342
+ console.log(` ${c.dim}Server already deleted on Hetzner, cleaning up local state...${c.reset}`);
343
+ }
344
+ else {
345
+ console.error(` ${c.red}✗${c.reset} Failed to delete server on Hetzner: ${err.message}`);
346
+ console.error(` ${c.dim}State NOT modified. Server may still be running. Check Hetzner console.${c.reset}`);
347
+ process.exit(1);
348
+ }
349
+ }
350
+ }
351
+ // Step 2: Clean up on server (best-effort, server might be gone already)
352
+ if (!isLastApp) {
353
+ console.log(` Stopping container ${name}...`);
354
+ try {
355
+ await (0, deploy_1.sshExec)(app.serverIp, `docker stop ${name} 2>/dev/null; docker rm ${name} 2>/dev/null`);
356
+ await (0, deploy_1.sshExec)(app.serverIp, `rm -f /etc/nginx/sites-enabled/${name} /etc/nginx/sites-available/${name}`);
357
+ await (0, deploy_1.sshExec)(app.serverIp, `nginx -t && systemctl reload nginx 2>/dev/null`);
358
+ await (0, deploy_1.sshExec)(app.serverIp, `rm -rf /home/canopy/${name}`);
359
+ }
360
+ catch { /* server unreachable — acceptable if we're removing the app from state */ }
361
+ }
362
+ // Step 3: Update local state AFTER remote operations succeed
363
+ (0, deploy_1.removeDeployment)(name);
364
+ if (isLastApp && srvInfo) {
365
+ (0, deploy_1.removeServer)(srvInfo.serverId);
366
+ console.log(` ${c.green}✓${c.reset} Destroyed ${name} and server ${srvInfo.server.ip}`);
367
+ }
368
+ else {
369
+ console.log(` ${c.green}✓${c.reset} Removed ${name} (server still has other apps)`);
370
+ }
371
+ }
372
+ catch (err) {
373
+ console.error(` ${c.red}Error:${c.reset} ${err.message}`);
374
+ process.exit(2);
375
+ }
376
+ });
377
+ commander_1.program.parse();
378
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAEA,yCAAoC;AACpC,6CAAuC;AACvC,2CAIwB;AAExB,2CAA6B;AAC7B,uCAAyB;AAGzB,MAAM,CAAC,GAAG;IACR,KAAK,EAAE,SAAS,EAAE,IAAI,EAAE,SAAS,EAAE,GAAG,EAAE,SAAS;IACjD,GAAG,EAAE,UAAU,EAAE,KAAK,EAAE,UAAU,EAAE,MAAM,EAAE,UAAU;IACtD,IAAI,EAAE,UAAU,EAAE,KAAK,EAAE,UAAU;IACnC,KAAK,EAAE,UAAU,EAAE,OAAO,EAAE,UAAU,EAAE,QAAQ,EAAE,UAAU;CAC7D,CAAC;AAEF,MAAM,eAAe,GAA6B;IAChD,QAAQ,EAAE,CAAC,CAAC,GAAG,EAAE,IAAI,EAAE,CAAC,CAAC,MAAM,EAAE,MAAM,EAAE,CAAC,CAAC,IAAI,EAAE,GAAG,EAAE,CAAC,CAAC,GAAG;CAC5D,CAAC;AAEF,MAAM,eAAe,GAA6B;IAChD,QAAQ,EAAE,GAAG,CAAC,CAAC,KAAK,GAAG,CAAC,CAAC,KAAK,GAAG,CAAC,CAAC,IAAI,aAAa,CAAC,CAAC,KAAK,EAAE;IAC7D,IAAI,EAAE,GAAG,CAAC,CAAC,QAAQ,GAAG,CAAC,CAAC,IAAI,SAAS,CAAC,CAAC,KAAK,EAAE;IAC9C,MAAM,EAAE,GAAG,CAAC,CAAC,IAAI,GAAG,CAAC,CAAC,IAAI,SAAS,CAAC,CAAC,KAAK,EAAE;IAC5C,GAAG,EAAE,GAAG,CAAC,CAAC,GAAG,MAAM,CAAC,CAAC,KAAK,EAAE;CAC7B,CAAC;AAEF,SAAS,UAAU,CAAC,KAAa;IAC/B,IAAI,KAAK,GAAG,EAAE;QAAE,OAAO,CAAC,CAAC,GAAG,CAAC;IAC7B,IAAI,KAAK,GAAG,EAAE;QAAE,OAAO,CAAC,CAAC,MAAM,CAAC;IAChC,OAAO,CAAC,CAAC,KAAK,CAAC;AACjB,CAAC;AAED,SAAS,UAAU,CAAC,KAAa;IAC/B,OAAO,CAAC,GAAG,EAAE,CAAC;IACd,OAAO,CAAC,GAAG,CAAC,KAAK,UAAU,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,IAAI,GAAG,KAAK,GAAG,CAAC,CAAC,KAAK,GAAG,CAAC,CAAC,GAAG,OAAO,CAAC,CAAC,KAAK,kBAAkB,CAAC,CAAC;IACvG,OAAO,CAAC,GAAG,EAAE,CAAC;AAChB,CAAC;AAED,SAAS,aAAa,CAAC,QAAmB;IACxC,IAAI,QAAQ,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QAC1B,OAAO,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC,KAAK,IAAI,CAAC,CAAC,KAAK,4BAA4B,CAAC,CAAC;QACjE,OAAO,CAAC,GAAG,EAAE,CAAC;QACd,OAAO;IACT,CAAC;IACD,MAAM,MAAM,GAAgC,EAAE,QAAQ,EAAE,EAAE,EAAE,IAAI,EAAE,EAAE,EAAE,MAAM,EAAE,EAAE,EAAE,GAAG,EAAE,EAAE,EAAE,CAAC;IAC5F,KAAK,MAAM,CAAC,IAAI,QAAQ;QAAE,MAAM,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;IAErD,KAAK,MAAM,QAAQ,IAAI,CAAC,UAAU,EAAE,MAAM,EAAE,QAAQ,EAAE,KAAK,CAAe,EAAE,CAAC;QAC3E,MAAM,KAAK,GAAG,MAAM,CAAC,QAAQ,CAAC,CAAC;QAC/B,IAAI,KAAK,CAAC,MAAM,KAAK,CAAC;YAAE,SAAS;QACjC,OAAO,CAAC,GAAG,CAAC,KAAK,eAAe,CAAC,QAAQ,CAAC,KAAK,CAAC,CAAC,GAAG,IAAI,KAAK,CAAC,MAAM,IAAI,CAAC,CAAC,KAAK,EAAE,CAAC,CAAC;QACnF,OAAO,CAAC,GAAG,EAAE,CAAC;QACd,KAAK,MAAM,CAAC,IAAI,KAAK,EAAE,CAAC;YACtB,MAAM,GAAG,GAAG,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,IAAI,IAAI,CAAC,CAAC,IAAI,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC;YACpD,OAAO,CAAC,GAAG,CAAC,KAAK,eAAe,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAC,KAAK,IAAI,CAAC,CAAC,IAAI,GAAG,CAAC,CAAC,KAAK,GAAG,CAAC,CAAC,KAAK,EAAE,CAAC,CAAC;YACvF,OAAO,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,GAAG,GAAG,GAAG,GAAG,CAAC,CAAC,KAAK,EAAE,CAAC,CAAC;YAC5C,IAAI,CAAC,CAAC,OAAO,EAAE,CAAC;gBAAC,OAAO,CAAC,GAAG,EAAE,CAAC;gBAAC,KAAK,MAAM,IAAI,IAAI,CAAC,CAAC,OAAO,CAAC,KAAK,CAAC,IAAI,CAAC;oBAAE,OAAO,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,GAAG,GAAG,IAAI,GAAG,CAAC,CAAC,KAAK,EAAE,CAAC,CAAC;YAAC,CAAC;YACzH,OAAO,CAAC,GAAG,EAAE,CAAC;YAAC,OAAO,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,WAAW,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC;YACpE,OAAO,CAAC,GAAG,EAAE,CAAC;YAAC,OAAO,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,KAAK,OAAO,CAAC,CAAC,KAAK,IAAI,CAAC,CAAC,GAAG,EAAE,CAAC,CAAC;YAAC,OAAO,CAAC,GAAG,EAAE,CAAC;QACrF,CAAC;IACH,CAAC;AACH,CAAC;AAED,SAAS,SAAS,CAAC,IAAc,EAAE,OAAoB;IACrD,MAAM,KAAK,GAAG,CAAC,GAAG,IAAI,CAAC,YAAY,gBAAgB,EAAE,GAAG,IAAI,CAAC,MAAM,IAAI,CAAC,CAAC;IACzE,MAAM,MAAM,GAAa,EAAE,CAAC;IAC5B,IAAI,OAAO,CAAC,QAAQ,GAAG,CAAC;QAAE,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,GAAG,GAAG,OAAO,CAAC,QAAQ,YAAY,CAAC,CAAC,KAAK,EAAE,CAAC,CAAC;IACxF,IAAI,OAAO,CAAC,IAAI,GAAG,CAAC;QAAE,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,MAAM,GAAG,OAAO,CAAC,IAAI,QAAQ,CAAC,CAAC,KAAK,EAAE,CAAC,CAAC;IAC/E,IAAI,OAAO,CAAC,MAAM,GAAG,CAAC;QAAE,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,IAAI,GAAG,OAAO,CAAC,MAAM,UAAU,CAAC,CAAC,KAAK,EAAE,CAAC,CAAC;IACnF,IAAI,OAAO,CAAC,GAAG,GAAG,CAAC;QAAE,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,GAAG,GAAG,OAAO,CAAC,GAAG,OAAO,CAAC,CAAC,KAAK,EAAE,CAAC,CAAC;IACzE,OAAO,CAAC,GAAG,CAAC,MAAM,CAAC,MAAM,GAAG,CAAC;QAC3B,CAAC,CAAC,KAAK,CAAC,CAAC,GAAG,GAAG,KAAK,CAAC,IAAI,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC,KAAK,GAAG,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE;QACnE,CAAC,CAAC,KAAK,CAAC,CAAC,GAAG,GAAG,KAAK,CAAC,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,KAAK,EAAE,CAAC,CAAC;IAChD,OAAO,CAAC,GAAG,EAAE,CAAC;AAChB,CAAC;AAED,MAAM,WAAW,GAA2B;IAC1C,IAAI,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,KAAK,EAAE,IAAI,EAAE,SAAS,EAAE,KAAK;IACvD,UAAU,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,KAAK,EAAE,IAAI,EAAE,SAAS,EAAE,KAAK;IAC7D,KAAK,EAAE,IAAI,EAAE,GAAG,EAAE,IAAI,EAAE,GAAG,EAAE,IAAI,EAAE,GAAG,EAAE,MAAM,EAAE,OAAO,EAAE,IAAI;CAC9D,CAAC;AAEF,mBAAO,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC,WAAW,CAAC,mDAAmD,CAAC,CAAC,OAAO,CAAC,OAAO,CAAC,CAAC;AAEzG,mBAAO,CAAC,OAAO,CAAC,aAAa,CAAC,CAAC,WAAW,CAAC,oCAAoC,CAAC;KAC7E,MAAM,CAAC,QAAQ,EAAE,iBAAiB,CAAC;KACnC,MAAM,CAAC,CAAC,UAA8B,EAAE,IAAwB,EAAE,EAAE;IACnE,MAAM,QAAQ,GAAG,IAAI,CAAC,OAAO,CAAC,UAAU,IAAI,OAAO,CAAC,GAAG,EAAE,CAAC,CAAC;IAC3D,IAAI,CAAC;QACH,MAAM,MAAM,GAAG,IAAA,cAAI,EAAC,QAAQ,CAAC,CAAC;QAC9B,IAAI,IAAI,CAAC,IAAI,EAAE,CAAC;YAAC,OAAO,CAAC,GAAG,CAAC,IAAI,CAAC,SAAS,CAAC,MAAM,EAAE,IAAI,EAAE,CAAC,CAAC,CAAC,CAAC;YAAC,OAAO,CAAC,IAAI,CAAC,MAAM,CAAC,OAAO,CAAC,QAAQ,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;QAAC,CAAC;QACnH,OAAO,CAAC,GAAG,EAAE,CAAC;QAAC,OAAO,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC,IAAI,cAAc,CAAC,CAAC,KAAK,KAAK,CAAC,CAAC,GAAG,GAAG,QAAQ,GAAG,CAAC,CAAC,KAAK,EAAE,CAAC,CAAC;QAC9F,UAAU,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;QAAC,aAAa,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAC;QAAC,SAAS,CAAC,MAAM,CAAC,IAAI,EAAE,MAAM,CAAC,OAAO,CAAC,CAAC;QACjG,OAAO,CAAC,IAAI,CAAC,MAAM,CAAC,OAAO,CAAC,QAAQ,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;IACpD,CAAC;IAAC,OAAO,GAAQ,EAAE,CAAC;QAAC,OAAO,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,GAAG,SAAS,CAAC,CAAC,KAAK,IAAI,GAAG,CAAC,OAAO,EAAE,CAAC,CAAC;QAAC,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;IAAC,CAAC;AACnG,CAAC,CAAC,CAAC;AAEL,mBAAO,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC,WAAW,CAAC,0BAA0B,CAAC,CAAC,MAAM,CAAC,GAAG,EAAE;IAC1E,MAAM,MAAM,GAAG,IAAA,mBAAU,GAAE,CAAC;IAC5B,MAAM,KAAK,GAAG,OAAO,CAAC,GAAG,CAAC,oBAAoB,CAAC;IAC/C,IAAI,KAAK;QAAE,OAAO,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC,KAAK,IAAI,CAAC,CAAC,KAAK,sDAAsD,CAAC,CAAC;SACjG,IAAI,MAAM,CAAC,YAAY;QAAE,OAAO,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC,KAAK,IAAI,CAAC,CAAC,KAAK,gCAAgC,CAAC,CAAC;;QAC9F,OAAO,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC,MAAM,IAAI,CAAC,CAAC,KAAK,gEAAgE,CAAC,CAAC;IAC3G,IAAA,mBAAU,EAAC,MAAM,CAAC,CAAC;IACnB,OAAO,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC,KAAK,IAAI,CAAC,CAAC,KAAK,wCAAwC,CAAC,CAAC;AAC/E,CAAC,CAAC,CAAC;AAEH,mBAAO,CAAC,OAAO,CAAC,eAAe,CAAC,CAAC,WAAW,CAAC,mCAAmC,CAAC;KAC9E,cAAc,CAAC,eAAe,EAAE,+BAA+B,CAAC;KAChE,MAAM,CAAC,QAAQ,EAAE,iBAAiB,CAAC;KACnC,MAAM,CAAC,WAAW,EAAE,+BAA+B,CAAC;KACpD,MAAM,CAAC,SAAS,EAAE,mDAAmD,CAAC;KACtE,MAAM,CAAC,OAAO,EAAE,0CAA0C,CAAC;KAC3D,MAAM,CAAC,mBAAmB,EAAE,iDAAiD,EAAE,MAAM,CAAC;KACtF,MAAM,CAAC,mBAAmB,EAAE,2BAA2B,CAAC;KACxD,MAAM,CAAC,UAAU,EAAE,wBAAwB,CAAC;KAC5C,MAAM,CAAC,WAAW,EAAE,+BAA+B,CAAC;KACpD,MAAM,CAAC,KAAK,EAAE,UAA8B,EAAE,IAG9C,EAAE,EAAE;IACH,MAAM,WAAW,GAAG,IAAI,CAAC,OAAO,CAAC,UAAU,IAAI,OAAO,CAAC,GAAG,EAAE,CAAC,CAAC;IAE9D,4CAA4C;IAC5C,IAAI,OAA2C,CAAC;IAChD,IAAI,IAAI,CAAC,OAAO,EAAE,CAAC;QACjB,MAAM,OAAO,GAAG,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;QAC3C,IAAI,CAAC,EAAE,CAAC,UAAU,CAAC,OAAO,CAAC,EAAE,CAAC;YAAC,OAAO,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC,GAAG,SAAS,CAAC,CAAC,KAAK,wBAAwB,OAAO,EAAE,CAAC,CAAC;YAAC,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;QAAC,CAAC;QAC7H,OAAO,GAAG,EAAE,CAAC;QACb,MAAM,KAAK,GAAG,EAAE,CAAC,YAAY,CAAC,OAAO,EAAE,OAAO,CAAC,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;QAC5D,KAAK,MAAM,IAAI,IAAI,KAAK,EAAE,CAAC;YACzB,MAAM,OAAO,GAAG,IAAI,CAAC,IAAI,EAAE,CAAC;YAC5B,IAAI,CAAC,OAAO,IAAI,OAAO,CAAC,UAAU,CAAC,GAAG,CAAC;gBAAE,SAAS;YAClD,MAAM,KAAK,GAAG,OAAO,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC;YACnC,IAAI,KAAK,GAAG,CAAC;gBAAE,OAAO,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC,EAAE,KAAK,CAAC,CAAC,GAAG,OAAO,CAAC,KAAK,CAAC,KAAK,GAAG,CAAC,CAAC,CAAC;QAC7E,CAAC;IACH,CAAC;IAED,MAAM,UAAU,GAAG,IAAI,CAAC,OAAO;QAC7B,CAAC,CAAC,CAAC,KAAa,EAAE,GAAW,EAAE,EAAE,GAAG,MAAM,IAAI,GAAG,WAAW,CAAC,KAAK,CAAC,IAAI,WAAW,CAAC,OAAO,CAAC,CAAC,OAAO,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC,GAAG,GAAG,IAAI,GAAG,CAAC,CAAC,KAAK,IAAI,CAAC,CAAC,GAAG,IAAI,KAAK,IAAI,CAAC,CAAC,KAAK,IAAI,GAAG,EAAE,CAAC,CAAC,CAAC,CAAC;QAC7K,CAAC,CAAC,SAAS,CAAC;IACd,IAAI,CAAC;QACH,OAAO,CAAC,GAAG,EAAE,CAAC;QAAC,OAAO,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC,IAAI,gBAAgB,CAAC,CAAC,KAAK,KAAK,CAAC,CAAC,GAAG,GAAG,WAAW,GAAG,CAAC,CAAC,KAAK,EAAE,CAAC,CAAC;QACnG,OAAO,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC,GAAG,SAAS,IAAI,CAAC,IAAI,GAAG,CAAC,CAAC,KAAK,EAAE,CAAC,CAAC;QAAC,OAAO,CAAC,GAAG,EAAE,CAAC;QACrE,MAAM,MAAM,GAAG,MAAM,IAAA,eAAM,EAAC;YAC1B,WAAW,EAAE,IAAI,EAAE,IAAI,CAAC,IAAI,EAAE,GAAG,EAAE,OAAO;YAC1C,KAAK,EAAE,IAAI,CAAC,KAAK,EAAE,SAAS,EAAE,IAAI,CAAC,GAAG,EAAE,MAAM,EAAE,IAAI,CAAC,MAAM;YAC3D,KAAK,EAAE,IAAI,CAAC,GAAG,KAAK,KAAK,EAAE,OAAO,EAAE,IAAI,CAAC,OAAO,EAAE,GAAG,EAAE,UAAU;SAClE,CAAC,CAAC;QACH,IAAI,IAAI,CAAC,IAAI,EAAE,CAAC;YAAC,OAAO,CAAC,GAAG,CAAC,IAAI,CAAC,SAAS,CAAC,MAAM,EAAE,IAAI,EAAE,CAAC,CAAC,CAAC,CAAC;YAAC,OAAO,CAAC,IAAI,CAAC,MAAM,CAAC,MAAM,KAAK,UAAU,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;QAAC,CAAC;QACpH,IAAI,MAAM,CAAC,MAAM,KAAK,SAAS,EAAE,CAAC;YAAC,OAAO,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC,GAAG,IAAI,CAAC,CAAC,KAAK,oBAAoB,MAAM,CAAC,MAAM,EAAE,CAAC,CAAC;YAAC,IAAI,MAAM,CAAC,IAAI,EAAE,CAAC;gBAAC,UAAU,CAAC,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;gBAAC,aAAa,CAAC,MAAM,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC;YAAC,CAAC;YAAC,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;QAAC,CAAC;QACrN,IAAI,MAAM,CAAC,MAAM,KAAK,cAAc,EAAE,CAAC;YAAC,OAAO,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC,GAAG,IAAI,CAAC,CAAC,KAAK,gBAAgB,CAAC,CAAC;YAAC,OAAO,CAAC,GAAG,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;YAAC,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;QAAC,CAAC;QACzI,IAAI,MAAM,CAAC,MAAM,KAAK,YAAY,EAAE,CAAC;YAAC,OAAO,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC,GAAG,IAAI,CAAC,CAAC,KAAK,6BAA6B,CAAC,CAAC;YAAC,OAAO,CAAC,GAAG,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;YAAC,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;QAAC,CAAC;QACpJ,OAAO,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC,KAAK,IAAI,CAAC,CAAC,KAAK,WAAW,CAAC,CAAC;QAChD,OAAO,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC,GAAG,OAAO,CAAC,CAAC,KAAK,UAAU,MAAM,CAAC,GAAG,EAAE,CAAC,CAAC;QAC5D,OAAO,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC,GAAG,MAAM,CAAC,CAAC,KAAK,WAAW,MAAM,CAAC,EAAE,IAAI,MAAM,CAAC,IAAI,EAAE,CAAC,CAAC;QAC1E,OAAO,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC,GAAG,aAAa,CAAC,CAAC,KAAK,IAAI,MAAM,CAAC,SAAS,EAAE,CAAC,CAAC;QAClE,OAAO,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC,GAAG,SAAS,CAAC,CAAC,KAAK,QAAQ,MAAM,CAAC,IAAI,EAAE,KAAK,MAAM,CAAC,CAAC;QACxE,IAAI,MAAM,CAAC,SAAS,EAAE,CAAC;YACrB,OAAO,CAAC,GAAG,EAAE,CAAC;YACd,OAAO,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC,IAAI,gBAAgB,CAAC,CAAC,KAAK,+BAA+B,CAAC,CAAC;YAC/E,OAAO,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC,GAAG,GAAG,GAAG,CAAC,MAAM,CAAC,EAAE,CAAC,GAAG,CAAC,CAAC,KAAK,EAAE,CAAC,CAAC;YACrD,KAAK,MAAM,IAAI,IAAI,MAAM,CAAC,SAAS,CAAC,KAAK,CAAC,IAAI,CAAC,EAAE,CAAC;gBAChD,OAAO,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC,GAAG,GAAG,IAAI,GAAG,CAAC,CAAC,KAAK,EAAE,CAAC,CAAC;YAC7C,CAAC;YACD,OAAO,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC,GAAG,GAAG,GAAG,CAAC,MAAM,CAAC,EAAE,CAAC,GAAG,CAAC,CAAC,KAAK,EAAE,CAAC,CAAC;YACrD,OAAO,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC,MAAM,wBAAwB,CAAC,CAAC,KAAK,EAAE,CAAC,CAAC;YAC5D,OAAO,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC,GAAG,sCAAsC,CAAC,CAAC,KAAK,EAAE,CAAC,CAAC;YACvE,OAAO,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC,GAAG,yBAAyB,CAAC,CAAC,KAAK,EAAE,CAAC,CAAC;YAC1D,OAAO,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC,GAAG,kBAAkB,MAAM,CAAC,GAAG,GAAG,CAAC,CAAC,KAAK,EAAE,CAAC,CAAC;YAChE,OAAO,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC,GAAG,gFAAgF,CAAC,CAAC,KAAK,EAAE,CAAC,CAAC;QACnH,CAAC;QACD,OAAO,CAAC,GAAG,EAAE,CAAC;IAChB,CAAC;IAAC,OAAO,GAAQ,EAAE,CAAC;QAAC,OAAO,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC,GAAG,SAAS,CAAC,CAAC,KAAK,IAAI,GAAG,CAAC,OAAO,EAAE,CAAC,CAAC;QAAC,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;IAAC,CAAC;AACrG,CAAC,CAAC,CAAC;AAEL,mBAAO,CAAC,OAAO,CAAC,eAAe,CAAC,CAAC,WAAW,CAAC,kBAAkB,CAAC,CAAC,MAAM,CAAC,QAAQ,EAAE,iBAAiB,CAAC;KACjG,MAAM,CAAC,KAAK,EAAE,IAAY,EAAE,IAAwB,EAAE,EAAE;IACvD,IAAI,CAAC;QACH,MAAM,MAAM,GAAG,MAAM,IAAA,kBAAS,EAAC,IAAI,CAAC,CAAC;QACrC,IAAI,IAAI,CAAC,IAAI,EAAE,CAAC;YAAC,OAAO,CAAC,GAAG,CAAC,IAAI,CAAC,SAAS,CAAC,MAAM,EAAE,IAAI,EAAE,CAAC,CAAC,CAAC,CAAC;YAAC,OAAO;QAAC,CAAC;QACxE,IAAI,MAAM,CAAC,MAAM,KAAK,WAAW,EAAE,CAAC;YAAC,OAAO,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC,MAAM,IAAI,CAAC,CAAC,KAAK,6BAA6B,IAAI,GAAG,CAAC,CAAC;YAAC,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;QAAC,CAAC;QAClI,OAAO,CAAC,GAAG,EAAE,CAAC;QAAC,OAAO,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC,IAAI,GAAG,IAAI,GAAG,CAAC,CAAC,KAAK,EAAE,CAAC,CAAC;QAC3D,OAAO,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC,GAAG,aAAa,CAAC,CAAC,KAAK,IAAI,MAAM,CAAC,SAAS,EAAE,CAAC,CAAC;QAAC,OAAO,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC,GAAG,OAAO,CAAC,CAAC,KAAK,UAAU,MAAM,CAAC,GAAG,EAAE,CAAC,CAAC;QAC/H,OAAO,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC,GAAG,MAAM,CAAC,CAAC,KAAK,WAAW,MAAM,CAAC,EAAE,EAAE,CAAC,CAAC;QAAC,OAAO,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC,GAAG,aAAa,CAAC,CAAC,KAAK,IAAI,MAAM,CAAC,SAAS,EAAE,CAAC,CAAC;QAC9H,OAAO,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC,GAAG,YAAY,CAAC,CAAC,KAAK,KAAK,MAAM,CAAC,UAAU,EAAE,CAAC,CAAC;QAAC,OAAO,CAAC,GAAG,EAAE,CAAC;IACpF,CAAC;IAAC,OAAO,GAAQ,EAAE,CAAC;QAAC,OAAO,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC,GAAG,SAAS,CAAC,CAAC,KAAK,IAAI,GAAG,CAAC,OAAO,EAAE,CAAC,CAAC;QAAC,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;IAAC,CAAC;AACrG,CAAC,CAAC,CAAC;AAEL,mBAAO,CAAC,OAAO,CAAC,aAAa,CAAC,CAAC,WAAW,CAAC,cAAc,CAAC,CAAC,MAAM,CAAC,aAAa,EAAE,iBAAiB,EAAE,KAAK,CAAC;KACvG,MAAM,CAAC,KAAK,EAAE,IAAY,EAAE,IAAuB,EAAE,EAAE;IACtD,IAAI,CAAC;QACH,MAAM,MAAM,GAAG,MAAM,IAAA,gBAAO,EAAC,IAAI,EAAE,QAAQ,CAAC,IAAI,CAAC,KAAK,EAAE,EAAE,CAAC,CAAC,CAAC;QAC7D,IAAI,MAAM,CAAC,MAAM,KAAK,WAAW,EAAE,CAAC;YAAC,OAAO,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC,MAAM,IAAI,CAAC,CAAC,KAAK,6BAA6B,IAAI,GAAG,CAAC,CAAC;YAAC,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;QAAC,CAAC;QAClI,OAAO,CAAC,GAAG,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC;IAC3B,CAAC;IAAC,OAAO,GAAQ,EAAE,CAAC;QAAC,OAAO,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC,GAAG,SAAS,CAAC,CAAC,KAAK,IAAI,GAAG,CAAC,OAAO,EAAE,CAAC,CAAC;QAAC,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;IAAC,CAAC;AACrG,CAAC,CAAC,CAAC;AAEL,mBAAO,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC,WAAW,CAAC,sBAAsB,CAAC,CAAC,MAAM,CAAC,QAAQ,EAAE,iBAAiB,CAAC;KAC5F,MAAM,CAAC,CAAC,IAAwB,EAAE,EAAE;IACnC,MAAM,KAAK,GAAG,IAAA,wBAAe,GAAE,CAAC;IAChC,IAAI,IAAI,CAAC,IAAI,EAAE,CAAC;QAAC,OAAO,CAAC,GAAG,CAAC,IAAI,CAAC,SAAS,CAAC,KAAK,EAAE,IAAI,EAAE,CAAC,CAAC,CAAC,CAAC;QAAC,OAAO;IAAC,CAAC;IACvE,MAAM,QAAQ,GAAG,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,IAAI,IAAI,EAAE,CAAC,CAAC;IAC/C,IAAI,QAAQ,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QAAC,OAAO,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC,GAAG,sBAAsB,CAAC,CAAC,KAAK,EAAE,CAAC,CAAC;QAAC,OAAO;IAAC,CAAC;IAE9F,OAAO,CAAC,GAAG,EAAE,CAAC;IACd,uBAAuB;IACvB,MAAM,UAAU,GAA6B,EAAE,CAAC;IAChD,KAAK,MAAM,CAAC,IAAI,EAAE,GAAG,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,KAAK,CAAC,IAAI,CAAC,EAAE,CAAC;QACrD,IAAI,CAAC,UAAU,CAAC,GAAG,CAAC,QAAQ,CAAC;YAAE,UAAU,CAAC,GAAG,CAAC,QAAQ,CAAC,GAAG,EAAE,CAAC;QAC7D,UAAU,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IACtC,CAAC;IAED,KAAK,MAAM,CAAC,KAAK,EAAE,IAAI,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,UAAU,CAAC,EAAE,CAAC;QACvD,MAAM,GAAG,GAAG,KAAK,CAAC,OAAO,EAAE,CAAC,KAAK,CAAC,CAAC;QACnC,MAAM,EAAE,GAAG,GAAG,EAAE,EAAE,IAAI,SAAS,CAAC;QAChC,MAAM,GAAG,GAAG,GAAG,EAAE,QAAQ,IAAI,GAAG,CAAC;QACjC,OAAO,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC,GAAG,GAAG,KAAK,GAAG,CAAC,CAAC,KAAK,KAAK,CAAC,CAAC,GAAG,GAAG,EAAE,KAAK,GAAG,IAAI,CAAC,CAAC,KAAK,KAAK,CAAC,CAAC,GAAG,GAAG,IAAI,CAAC,MAAM,UAAU,CAAC,CAAC,KAAK,EAAE,CAAC,CAAC;QACvH,KAAK,MAAM,IAAI,IAAI,IAAI,EAAE,CAAC;YACxB,MAAM,GAAG,GAAG,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;YAC7B,OAAO,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,IAAI,GAAG,IAAI,GAAG,CAAC,CAAC,KAAK,KAAK,CAAC,CAAC,GAAG,IAAI,GAAG,CAAC,IAAI,KAAK,GAAG,CAAC,SAAS,KAAK,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,YAAY,CAAC,CAAC,CAAC,QAAQ,KAAK,GAAG,CAAC,UAAU,IAAI,SAAS,GAAG,CAAC,CAAC,KAAK,EAAE,CAAC,CAAC;QAC5K,CAAC;QACD,OAAO,CAAC,GAAG,EAAE,CAAC;IAChB,CAAC;AACH,CAAC,CAAC,CAAC;AAEL,mBAAO,CAAC,OAAO,CAAC,gBAAgB,CAAC,CAAC,WAAW,CAAC,4CAA4C,CAAC;KACxF,MAAM,CAAC,KAAK,EAAE,IAAY,EAAE,EAAE;IAC7B,IAAI,CAAC;QACH,IAAA,wBAAe,EAAC,IAAI,CAAC,CAAC;QACtB,MAAM,GAAG,GAAG,IAAA,sBAAa,EAAC,IAAI,CAAC,CAAC;QAChC,IAAI,CAAC,GAAG,EAAE,CAAC;YAAC,OAAO,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC,MAAM,IAAI,CAAC,CAAC,KAAK,6BAA6B,IAAI,GAAG,CAAC,CAAC;YAAC,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;QAAC,CAAC;QAEzG,MAAM,OAAO,GAAG,IAAA,wBAAe,EAAC,IAAI,CAAC,CAAC;QACtC,MAAM,SAAS,GAAG,OAAO,CAAC,CAAC,CAAC,OAAO,CAAC,MAAM,CAAC,IAAI,CAAC,MAAM,IAAI,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC;QAEpE,iFAAiF;QACjF,IAAI,SAAS,IAAI,OAAO,EAAE,CAAC;YACzB,OAAO,CAAC,GAAG,CAAC,0CAA0C,OAAO,CAAC,MAAM,CAAC,EAAE,KAAK,OAAO,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC,CAAC;YACrG,IAAI,CAAC;gBACH,MAAM,IAAA,qBAAY,EAAC,OAAO,CAAC,MAAM,CAAC,EAAE,CAAC,CAAC;YACxC,CAAC;YAAC,OAAO,GAAQ,EAAE,CAAC;gBAClB,qEAAqE;gBACrE,IAAI,GAAG,CAAC,OAAO,EAAE,QAAQ,CAAC,KAAK,CAAC,EAAE,CAAC;oBACjC,OAAO,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC,GAAG,gEAAgE,CAAC,CAAC,KAAK,EAAE,CAAC,CAAC;gBACnG,CAAC;qBAAM,CAAC;oBACN,OAAO,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC,GAAG,IAAI,CAAC,CAAC,KAAK,wCAAwC,GAAG,CAAC,OAAO,EAAE,CAAC,CAAC;oBAC1F,OAAO,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC,GAAG,0EAA0E,CAAC,CAAC,KAAK,EAAE,CAAC,CAAC;oBAC7G,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;gBAClB,CAAC;YACH,CAAC;QACH,CAAC;QAED,yEAAyE;QACzE,IAAI,CAAC,SAAS,EAAE,CAAC;YACf,OAAO,CAAC,GAAG,CAAC,wBAAwB,IAAI,KAAK,CAAC,CAAC;YAC/C,IAAI,CAAC;gBACH,MAAM,IAAA,gBAAO,EAAC,GAAG,CAAC,QAAQ,EAAE,eAAe,IAAI,2BAA2B,IAAI,cAAc,CAAC,CAAC;gBAC9F,MAAM,IAAA,gBAAO,EAAC,GAAG,CAAC,QAAQ,EAAE,kCAAkC,IAAI,+BAA+B,IAAI,EAAE,CAAC,CAAC;gBACzG,MAAM,IAAA,gBAAO,EAAC,GAAG,CAAC,QAAQ,EAAE,gDAAgD,CAAC,CAAC;gBAC9E,MAAM,IAAA,gBAAO,EAAC,GAAG,CAAC,QAAQ,EAAE,uBAAuB,IAAI,EAAE,CAAC,CAAC;YAC7D,CAAC;YAAC,MAAM,CAAC,CAAC,0EAA0E,CAAC,CAAC;QACxF,CAAC;QAED,6DAA6D;QAC7D,IAAA,yBAAgB,EAAC,IAAI,CAAC,CAAC;QACvB,IAAI,SAAS,IAAI,OAAO,EAAE,CAAC;YACzB,IAAA,qBAAY,EAAC,OAAO,CAAC,QAAQ,CAAC,CAAC;YAC/B,OAAO,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC,KAAK,IAAI,CAAC,CAAC,KAAK,cAAc,IAAI,eAAe,OAAO,CAAC,MAAM,CAAC,EAAE,EAAE,CAAC,CAAC;QAC3F,CAAC;aAAM,CAAC;YACN,OAAO,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC,KAAK,IAAI,CAAC,CAAC,KAAK,YAAY,IAAI,gCAAgC,CAAC,CAAC;QACvF,CAAC;IACH,CAAC;IAAC,OAAO,GAAQ,EAAE,CAAC;QAAC,OAAO,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC,GAAG,SAAS,CAAC,CAAC,KAAK,IAAI,GAAG,CAAC,OAAO,EAAE,CAAC,CAAC;QAAC,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;IAAC,CAAC;AACrG,CAAC,CAAC,CAAC;AAEL,mBAAO,CAAC,KAAK,EAAE,CAAC"}
package/package.json ADDED
@@ -0,0 +1,36 @@
1
+ {
2
+ "name": "canopy-deploy",
3
+ "version": "1.0.0",
4
+ "description": "Canopy CLI — security scanner & deploy tool for vibecoded apps",
5
+ "bin": {
6
+ "canopy": "dist/index.js"
7
+ },
8
+ "scripts": {
9
+ "build": "tsc"
10
+ },
11
+ "dependencies": {
12
+ "commander": "^12.0.0",
13
+ "@canopy/scanner": "1.0.0",
14
+ "@canopy/deploy": "1.0.0"
15
+ },
16
+ "keywords": [
17
+ "security",
18
+ "scanner",
19
+ "deploy",
20
+ "vps",
21
+ "hetzner",
22
+ "docker",
23
+ "cli",
24
+ "vibecoding",
25
+ "ai-native"
26
+ ],
27
+ "repository": {
28
+ "type": "git",
29
+ "url": "https://github.com/KenEzekiel/canopy.git"
30
+ },
31
+ "author": "Canopy Contributors",
32
+ "license": "MIT",
33
+ "publishConfig": {
34
+ "access": "public"
35
+ }
36
+ }
package/src/index.ts ADDED
@@ -0,0 +1,282 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { program } from 'commander';
4
+ import { scan } from '@canopy/scanner';
5
+ import {
6
+ deploy, getStatus, getLogs, loadConfig, saveConfig,
7
+ listDeployments, removeDeployment, deleteServer, getDeployment,
8
+ getServerForApp, removeServer, sshExec, validateAppName,
9
+ } from '@canopy/deploy';
10
+ import type { CanopyState } from '@canopy/deploy';
11
+ import * as path from 'path';
12
+ import * as fs from 'fs';
13
+ import type { Finding, ScanSummary, ScanMeta, Severity } from '@canopy/scanner';
14
+
15
+ const c = {
16
+ reset: '\x1b[0m', bold: '\x1b[1m', dim: '\x1b[2m',
17
+ red: '\x1b[31m', green: '\x1b[32m', yellow: '\x1b[33m',
18
+ cyan: '\x1b[36m', white: '\x1b[37m',
19
+ bgRed: '\x1b[41m', bgGreen: '\x1b[42m', bgYellow: '\x1b[43m',
20
+ };
21
+
22
+ const SEVERITY_COLORS: Record<Severity, string> = {
23
+ critical: c.red, high: c.yellow, medium: c.cyan, low: c.dim,
24
+ };
25
+
26
+ const SEVERITY_LABELS: Record<Severity, string> = {
27
+ critical: `${c.bgRed}${c.white}${c.bold} CRITICAL ${c.reset}`,
28
+ high: `${c.bgYellow}${c.bold} HIGH ${c.reset}`,
29
+ medium: `${c.cyan}${c.bold}MEDIUM${c.reset}`,
30
+ low: `${c.dim}LOW${c.reset}`,
31
+ };
32
+
33
+ function scoreColor(score: number): string {
34
+ if (score < 40) return c.red;
35
+ if (score < 70) return c.yellow;
36
+ return c.green;
37
+ }
38
+
39
+ function printScore(score: number): void {
40
+ console.log();
41
+ console.log(` ${scoreColor(score)}${c.bold}${score}${c.reset}${c.dim}/100${c.reset} Security Score`);
42
+ console.log();
43
+ }
44
+
45
+ function printFindings(findings: Finding[]): void {
46
+ if (findings.length === 0) {
47
+ console.log(` ${c.green}✓${c.reset} No security issues found.`);
48
+ console.log();
49
+ return;
50
+ }
51
+ const groups: Record<Severity, Finding[]> = { critical: [], high: [], medium: [], low: [] };
52
+ for (const f of findings) groups[f.severity].push(f);
53
+
54
+ for (const severity of ['critical', 'high', 'medium', 'low'] as Severity[]) {
55
+ const items = groups[severity];
56
+ if (items.length === 0) continue;
57
+ console.log(` ${SEVERITY_LABELS[severity]} ${c.dim}(${items.length})${c.reset}`);
58
+ console.log();
59
+ for (const f of items) {
60
+ const loc = f.line ? `${f.file}:${f.line}` : f.file;
61
+ console.log(` ${SEVERITY_COLORS[severity]}●${c.reset} ${c.bold}${f.title}${c.reset}`);
62
+ console.log(` ${c.dim}${loc}${c.reset}`);
63
+ if (f.snippet) { console.log(); for (const line of f.snippet.split('\n')) console.log(` ${c.dim}${line}${c.reset}`); }
64
+ console.log(); console.log(` ${f.description.split('\n\n')[0]}`);
65
+ console.log(); console.log(` ${c.green}Fix:${c.reset} ${f.fix}`); console.log();
66
+ }
67
+ }
68
+ }
69
+
70
+ function printMeta(meta: ScanMeta, summary: ScanSummary): void {
71
+ const parts = [`${meta.filesScanned} files scanned`, `${meta.timeMs}ms`];
72
+ const counts: string[] = [];
73
+ if (summary.critical > 0) counts.push(`${c.red}${summary.critical} critical${c.reset}`);
74
+ if (summary.high > 0) counts.push(`${c.yellow}${summary.high} high${c.reset}`);
75
+ if (summary.medium > 0) counts.push(`${c.cyan}${summary.medium} medium${c.reset}`);
76
+ if (summary.low > 0) counts.push(`${c.dim}${summary.low} low${c.reset}`);
77
+ console.log(counts.length > 0
78
+ ? ` ${c.dim}${parts.join(' · ')} · ${c.reset}${counts.join(', ')}`
79
+ : ` ${c.dim}${parts.join(' · ')}${c.reset}`);
80
+ console.log();
81
+ }
82
+
83
+ const PHASE_ICONS: Record<string, string> = {
84
+ scan: '🔍', detect: '🔎', state: '💾', provision: '☁️ ',
85
+ dockerfile: '🐳', upload: '📦', build: '🔨', container: '▶️ ',
86
+ nginx: '🌐', ssl: '🔒', env: '🔑', vpn: '🛡️ ', default: ' ',
87
+ };
88
+
89
+ program.name('canopy').description('Security scanner & deploy tool for vibecoded apps').version('1.0.0');
90
+
91
+ program.command('scan [path]').description('Scan a project for security issues')
92
+ .option('--json', 'Output raw JSON')
93
+ .action((targetPath: string | undefined, opts: { json?: boolean }) => {
94
+ const resolved = path.resolve(targetPath || process.cwd());
95
+ try {
96
+ const result = scan(resolved);
97
+ if (opts.json) { console.log(JSON.stringify(result, null, 2)); process.exit(result.summary.critical > 0 ? 1 : 0); }
98
+ console.log(); console.log(` ${c.bold}canopy scan${c.reset} ${c.dim}${resolved}${c.reset}`);
99
+ printScore(result.score); printFindings(result.findings); printMeta(result.meta, result.summary);
100
+ process.exit(result.summary.critical > 0 ? 1 : 0);
101
+ } catch (err: any) { console.error(`${c.red}Error:${c.reset} ${err.message}`); process.exit(2); }
102
+ });
103
+
104
+ program.command('init').description('Initialize Canopy config').action(() => {
105
+ const config = loadConfig();
106
+ const token = process.env.CANOPY_HETZNER_TOKEN;
107
+ if (token) console.log(` ${c.green}✓${c.reset} Hetzner token found in CANOPY_HETZNER_TOKEN env var`);
108
+ else if (config.hetznerToken) console.log(` ${c.green}✓${c.reset} Hetzner token found in config`);
109
+ else console.log(` ${c.yellow}!${c.reset} No Hetzner token. Set CANOPY_HETZNER_TOKEN env var to deploy.`);
110
+ saveConfig(config);
111
+ console.log(` ${c.green}✓${c.reset} Config saved to ~/.canopy/config.json`);
112
+ });
113
+
114
+ program.command('deploy [path]').description('Deploy a project to a Hetzner VPS')
115
+ .requiredOption('--name <name>', 'App name (used for subdomain)')
116
+ .option('--json', 'Output raw JSON')
117
+ .option('--verbose', 'Show detailed deploy progress')
118
+ .option('--force', 'Skip scanner gate (deploy with critical findings)')
119
+ .option('--new', 'Force new server (don\'t reuse existing)')
120
+ .option('--region <region>', 'Server region (fsn1, nbg1, hel1, ash, hil, sin)', 'hel1')
121
+ .option('--env-file <path>', 'Path to .env file to load')
122
+ .option('--no-ssl', 'Skip SSL/certbot setup')
123
+ .option('--private', 'Make app VPN-only (WireGuard)')
124
+ .action(async (targetPath: string | undefined, opts: {
125
+ name: string; json?: boolean; verbose?: boolean; force?: boolean;
126
+ new?: boolean; region?: string; envFile?: string; ssl?: boolean; private?: boolean;
127
+ }) => {
128
+ const projectPath = path.resolve(targetPath || process.cwd());
129
+
130
+ // Load env vars from --env-file if provided
131
+ let envVars: Record<string, string> | undefined;
132
+ if (opts.envFile) {
133
+ const envPath = path.resolve(opts.envFile);
134
+ if (!fs.existsSync(envPath)) { console.error(` ${c.red}Error:${c.reset} Env file not found: ${envPath}`); process.exit(2); }
135
+ envVars = {};
136
+ const lines = fs.readFileSync(envPath, 'utf-8').split('\n');
137
+ for (const line of lines) {
138
+ const trimmed = line.trim();
139
+ if (!trimmed || trimmed.startsWith('#')) continue;
140
+ const eqIdx = trimmed.indexOf('=');
141
+ if (eqIdx > 0) envVars[trimmed.slice(0, eqIdx)] = trimmed.slice(eqIdx + 1);
142
+ }
143
+ }
144
+
145
+ const verboseLog = opts.verbose
146
+ ? (phase: string, msg: string) => { const icon = PHASE_ICONS[phase] || PHASE_ICONS.default; console.log(` ${c.dim}${icon}${c.reset} ${c.dim}[${phase}]${c.reset} ${msg}`); }
147
+ : undefined;
148
+ try {
149
+ console.log(); console.log(` ${c.bold}canopy deploy${c.reset} ${c.dim}${projectPath}${c.reset}`);
150
+ console.log(` ${c.dim}name: ${opts.name}${c.reset}`); console.log();
151
+ const result = await deploy({
152
+ projectPath, name: opts.name, env: envVars,
153
+ force: opts.force, newServer: opts.new, region: opts.region,
154
+ noSsl: opts.ssl === false, private: opts.private, log: verboseLog,
155
+ });
156
+ if (opts.json) { console.log(JSON.stringify(result, null, 2)); process.exit(result.status === 'deployed' ? 0 : 1); }
157
+ if (result.status === 'blocked') { console.log(` ${c.red}✗${c.reset} Deploy blocked: ${result.reason}`); if (result.scan) { printScore(result.scan.score); printFindings(result.scan.findings); } process.exit(1); }
158
+ if (result.status === 'build-failed') { console.log(` ${c.red}✗${c.reset} Build failed:`); console.log(result.error); process.exit(1); }
159
+ if (result.status === 'run-failed') { console.log(` ${c.red}✗${c.reset} Container failed to start:`); console.log(result.error); process.exit(1); }
160
+ console.log(` ${c.green}✓${c.reset} Deployed`);
161
+ console.log(` ${c.dim}URL:${c.reset} ${result.url}`);
162
+ console.log(` ${c.dim}IP:${c.reset} ${result.ip}:${result.port}`);
163
+ console.log(` ${c.dim}Framework:${c.reset} ${result.framework}`);
164
+ console.log(` ${c.dim}Score:${c.reset} ${result.scan?.score}/100`);
165
+ if (result.vpnConfig) {
166
+ console.log();
167
+ console.log(` ${c.bold}🔒 VPN Config${c.reset} (import into WireGuard app):`);
168
+ console.log(` ${c.dim}${'─'.repeat(50)}${c.reset}`);
169
+ for (const line of result.vpnConfig.split('\n')) {
170
+ console.log(` ${c.dim}${line}${c.reset}`);
171
+ }
172
+ console.log(` ${c.dim}${'─'.repeat(50)}${c.reset}`);
173
+ console.log(` ${c.yellow}This app is VPN-only.${c.reset}`);
174
+ console.log(` ${c.dim}1. Import config into WireGuard app${c.reset}`);
175
+ console.log(` ${c.dim}2. Activate the tunnel${c.reset}`);
176
+ console.log(` ${c.dim}3. Access via: ${result.url}${c.reset}`);
177
+ console.log(` ${c.dim}Note: If using Chrome, disable "Use secure DNS" in chrome://settings/security${c.reset}`);
178
+ }
179
+ console.log();
180
+ } catch (err: any) { console.error(` ${c.red}Error:${c.reset} ${err.message}`); process.exit(2); }
181
+ });
182
+
183
+ program.command('status <name>').description('Check app status').option('--json', 'Output raw JSON')
184
+ .action(async (name: string, opts: { json?: boolean }) => {
185
+ try {
186
+ const result = await getStatus(name);
187
+ if (opts.json) { console.log(JSON.stringify(result, null, 2)); return; }
188
+ if (result.status === 'not-found') { console.log(` ${c.yellow}!${c.reset} No deployment found for "${name}"`); process.exit(1); }
189
+ console.log(); console.log(` ${c.bold}${name}${c.reset}`);
190
+ console.log(` ${c.dim}Container:${c.reset} ${result.container}`); console.log(` ${c.dim}URL:${c.reset} ${result.url}`);
191
+ console.log(` ${c.dim}IP:${c.reset} ${result.ip}`); console.log(` ${c.dim}Framework:${c.reset} ${result.framework}`);
192
+ console.log(` ${c.dim}Deployed:${c.reset} ${result.lastDeploy}`); console.log();
193
+ } catch (err: any) { console.error(` ${c.red}Error:${c.reset} ${err.message}`); process.exit(2); }
194
+ });
195
+
196
+ program.command('logs <name>').description('Get app logs').option('--lines <n>', 'Number of lines', '100')
197
+ .action(async (name: string, opts: { lines: string }) => {
198
+ try {
199
+ const result = await getLogs(name, parseInt(opts.lines, 10));
200
+ if (result.status === 'not-found') { console.log(` ${c.yellow}!${c.reset} No deployment found for "${name}"`); process.exit(1); }
201
+ console.log(result.logs);
202
+ } catch (err: any) { console.error(` ${c.red}Error:${c.reset} ${err.message}`); process.exit(2); }
203
+ });
204
+
205
+ program.command('list').description('List all deployments').option('--json', 'Output raw JSON')
206
+ .action((opts: { json?: boolean }) => {
207
+ const state = listDeployments();
208
+ if (opts.json) { console.log(JSON.stringify(state, null, 2)); return; }
209
+ const appNames = Object.keys(state.apps || {});
210
+ if (appNames.length === 0) { console.log(` ${c.dim}No deployments yet.${c.reset}`); return; }
211
+
212
+ console.log();
213
+ // Group apps by server
214
+ const serverApps: Record<string, string[]> = {};
215
+ for (const [name, app] of Object.entries(state.apps)) {
216
+ if (!serverApps[app.serverId]) serverApps[app.serverId] = [];
217
+ serverApps[app.serverId].push(name);
218
+ }
219
+
220
+ for (const [srvId, apps] of Object.entries(serverApps)) {
221
+ const srv = state.servers?.[srvId];
222
+ const ip = srv?.ip || 'unknown';
223
+ const loc = srv?.location || '?';
224
+ console.log(` ${c.dim}${srvId}${c.reset} ${c.dim}${ip} (${loc})${c.reset} ${c.dim}${apps.length} app(s)${c.reset}`);
225
+ for (const name of apps) {
226
+ const app = state.apps[name];
227
+ console.log(` ${c.bold}${name}${c.reset} ${c.dim}:${app.port} ${app.framework} ${app.private ? '🔒 private' : 'public'} ${app.lastDeploy || 'pending'}${c.reset}`);
228
+ }
229
+ console.log();
230
+ }
231
+ });
232
+
233
+ program.command('destroy <name>').description('Remove an app (deletes server if last app)')
234
+ .action(async (name: string) => {
235
+ try {
236
+ validateAppName(name);
237
+ const app = getDeployment(name);
238
+ if (!app) { console.log(` ${c.yellow}!${c.reset} No deployment found for "${name}"`); process.exit(1); }
239
+
240
+ const srvInfo = getServerForApp(name);
241
+ const isLastApp = srvInfo ? srvInfo.server.apps.length <= 1 : false;
242
+
243
+ // Step 1: If last app, delete Hetzner server FIRST (before touching local state)
244
+ if (isLastApp && srvInfo) {
245
+ console.log(` Last app on server — deleting server ${srvInfo.server.id} (${srvInfo.server.ip})...`);
246
+ try {
247
+ await deleteServer(srvInfo.server.id);
248
+ } catch (err: any) {
249
+ // 404 = server already gone on Hetzner, safe to clean up local state
250
+ if (err.message?.includes('404')) {
251
+ console.log(` ${c.dim}Server already deleted on Hetzner, cleaning up local state...${c.reset}`);
252
+ } else {
253
+ console.error(` ${c.red}✗${c.reset} Failed to delete server on Hetzner: ${err.message}`);
254
+ console.error(` ${c.dim}State NOT modified. Server may still be running. Check Hetzner console.${c.reset}`);
255
+ process.exit(1);
256
+ }
257
+ }
258
+ }
259
+
260
+ // Step 2: Clean up on server (best-effort, server might be gone already)
261
+ if (!isLastApp) {
262
+ console.log(` Stopping container ${name}...`);
263
+ try {
264
+ await sshExec(app.serverIp, `docker stop ${name} 2>/dev/null; docker rm ${name} 2>/dev/null`);
265
+ await sshExec(app.serverIp, `rm -f /etc/nginx/sites-enabled/${name} /etc/nginx/sites-available/${name}`);
266
+ await sshExec(app.serverIp, `nginx -t && systemctl reload nginx 2>/dev/null`);
267
+ await sshExec(app.serverIp, `rm -rf /home/canopy/${name}`);
268
+ } catch { /* server unreachable — acceptable if we're removing the app from state */ }
269
+ }
270
+
271
+ // Step 3: Update local state AFTER remote operations succeed
272
+ removeDeployment(name);
273
+ if (isLastApp && srvInfo) {
274
+ removeServer(srvInfo.serverId);
275
+ console.log(` ${c.green}✓${c.reset} Destroyed ${name} and server ${srvInfo.server.ip}`);
276
+ } else {
277
+ console.log(` ${c.green}✓${c.reset} Removed ${name} (server still has other apps)`);
278
+ }
279
+ } catch (err: any) { console.error(` ${c.red}Error:${c.reset} ${err.message}`); process.exit(2); }
280
+ });
281
+
282
+ program.parse();
package/tsconfig.json ADDED
@@ -0,0 +1,8 @@
1
+ {
2
+ "extends": "../../tsconfig.base.json",
3
+ "compilerOptions": {
4
+ "outDir": "dist",
5
+ "rootDir": "src"
6
+ },
7
+ "include": ["src"]
8
+ }