canopy-deploy 1.0.0 → 1.1.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/package.json +3 -3
- package/src/index.ts +72 -5
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "canopy-deploy",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.1.0",
|
|
4
4
|
"description": "Canopy CLI — security scanner & deploy tool for vibecoded apps",
|
|
5
5
|
"bin": {
|
|
6
6
|
"canopy": "dist/index.js"
|
|
@@ -10,8 +10,8 @@
|
|
|
10
10
|
},
|
|
11
11
|
"dependencies": {
|
|
12
12
|
"commander": "^12.0.0",
|
|
13
|
-
"@canopy/scanner": "1.
|
|
14
|
-
"@canopy/deploy": "1.
|
|
13
|
+
"@canopy/scanner": "1.1.0",
|
|
14
|
+
"@canopy/deploy": "1.1.0"
|
|
15
15
|
},
|
|
16
16
|
"keywords": [
|
|
17
17
|
"security",
|
package/src/index.ts
CHANGED
|
@@ -6,6 +6,7 @@ import {
|
|
|
6
6
|
deploy, getStatus, getLogs, loadConfig, saveConfig,
|
|
7
7
|
listDeployments, removeDeployment, deleteServer, getDeployment,
|
|
8
8
|
getServerForApp, removeServer, sshExec, validateAppName,
|
|
9
|
+
deployTemplate, listTemplates, loadTemplate,
|
|
9
10
|
} from '@canopy/deploy';
|
|
10
11
|
import type { CanopyState } from '@canopy/deploy';
|
|
11
12
|
import * as path from 'path';
|
|
@@ -83,7 +84,8 @@ function printMeta(meta: ScanMeta, summary: ScanSummary): void {
|
|
|
83
84
|
const PHASE_ICONS: Record<string, string> = {
|
|
84
85
|
scan: '🔍', detect: '🔎', state: '💾', provision: '☁️ ',
|
|
85
86
|
dockerfile: '🐳', upload: '📦', build: '🔨', container: '▶️ ',
|
|
86
|
-
nginx: '🌐', ssl: '🔒', env: '🔑', vpn: '🛡️ ',
|
|
87
|
+
nginx: '🌐', ssl: '🔒', env: '🔑', vpn: '🛡️ ', clone: '📥',
|
|
88
|
+
deploy: '🚀', default: ' ',
|
|
87
89
|
};
|
|
88
90
|
|
|
89
91
|
program.name('canopy').description('Security scanner & deploy tool for vibecoded apps').version('1.0.0');
|
|
@@ -111,8 +113,9 @@ program.command('init').description('Initialize Canopy config').action(() => {
|
|
|
111
113
|
console.log(` ${c.green}✓${c.reset} Config saved to ~/.canopy/config.json`);
|
|
112
114
|
});
|
|
113
115
|
|
|
114
|
-
program.command('deploy [path]').description('Deploy a project to a Hetzner VPS')
|
|
116
|
+
program.command('deploy [path]').description('Deploy a project (or a template with --template) to a Hetzner VPS')
|
|
115
117
|
.requiredOption('--name <name>', 'App name (used for subdomain)')
|
|
118
|
+
.option('--template <template>', 'Deploy from a predefined template (run `canopy templates` to list)')
|
|
116
119
|
.option('--json', 'Output raw JSON')
|
|
117
120
|
.option('--verbose', 'Show detailed deploy progress')
|
|
118
121
|
.option('--force', 'Skip scanner gate (deploy with critical findings)')
|
|
@@ -122,11 +125,9 @@ program.command('deploy [path]').description('Deploy a project to a Hetzner VPS'
|
|
|
122
125
|
.option('--no-ssl', 'Skip SSL/certbot setup')
|
|
123
126
|
.option('--private', 'Make app VPN-only (WireGuard)')
|
|
124
127
|
.action(async (targetPath: string | undefined, opts: {
|
|
125
|
-
name: string; json?: boolean; verbose?: boolean; force?: boolean;
|
|
128
|
+
name: string; template?: string; json?: boolean; verbose?: boolean; force?: boolean;
|
|
126
129
|
new?: boolean; region?: string; envFile?: string; ssl?: boolean; private?: boolean;
|
|
127
130
|
}) => {
|
|
128
|
-
const projectPath = path.resolve(targetPath || process.cwd());
|
|
129
|
-
|
|
130
131
|
// Load env vars from --env-file if provided
|
|
131
132
|
let envVars: Record<string, string> | undefined;
|
|
132
133
|
if (opts.envFile) {
|
|
@@ -145,6 +146,53 @@ program.command('deploy [path]').description('Deploy a project to a Hetzner VPS'
|
|
|
145
146
|
const verboseLog = opts.verbose
|
|
146
147
|
? (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
148
|
: undefined;
|
|
149
|
+
|
|
150
|
+
// Template deploy path
|
|
151
|
+
if (opts.template) {
|
|
152
|
+
try {
|
|
153
|
+
const tmpl = loadTemplate(opts.template);
|
|
154
|
+
console.log(); console.log(` ${c.bold}canopy deploy${c.reset} ${c.dim}template: ${tmpl.name}${c.reset}`);
|
|
155
|
+
console.log(` ${c.dim}name: ${opts.name}${c.reset}`); console.log();
|
|
156
|
+
|
|
157
|
+
// Collect required env vars from env-file or process.env
|
|
158
|
+
const templateEnv: Record<string, string> = { ...(envVars || {}) };
|
|
159
|
+
for (const req of tmpl.env_required) {
|
|
160
|
+
if (!templateEnv[req.name] && process.env[req.name]) {
|
|
161
|
+
templateEnv[req.name] = process.env[req.name]!;
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
for (const opt of tmpl.env_optional) {
|
|
165
|
+
if (!templateEnv[opt.name] && process.env[opt.name]) {
|
|
166
|
+
templateEnv[opt.name] = process.env[opt.name]!;
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
const result = await deployTemplate({
|
|
171
|
+
templateName: opts.template, appName: opts.name, env: templateEnv,
|
|
172
|
+
region: opts.region, private: opts.private, log: verboseLog,
|
|
173
|
+
});
|
|
174
|
+
if (opts.json) { console.log(JSON.stringify(result, null, 2)); process.exit(result.status === 'deployed' ? 0 : 1); }
|
|
175
|
+
if (result.status === 'missing-env') { console.error(` ${c.red}✗${c.reset} ${result.error}`); process.exit(1); }
|
|
176
|
+
if (result.status !== 'deployed') { console.error(` ${c.red}✗${c.reset} ${result.status}: ${result.error}`); process.exit(1); }
|
|
177
|
+
console.log(` ${c.green}✓${c.reset} Deployed (template: ${opts.template})`);
|
|
178
|
+
console.log(` ${c.dim}URL:${c.reset} ${result.url}`);
|
|
179
|
+
console.log(` ${c.dim}IP:${c.reset} ${result.ip}:${result.port}`);
|
|
180
|
+
console.log(` ${c.dim}Template:${c.reset} ${opts.template}`);
|
|
181
|
+
if (result.vpnConfig) {
|
|
182
|
+
console.log();
|
|
183
|
+
console.log(` ${c.bold}🔒 VPN Config${c.reset} (import into WireGuard app):`);
|
|
184
|
+
console.log(` ${c.dim}${'─'.repeat(50)}${c.reset}`);
|
|
185
|
+
for (const line of result.vpnConfig.split('\n')) console.log(` ${c.dim}${line}${c.reset}`);
|
|
186
|
+
console.log(` ${c.dim}${'─'.repeat(50)}${c.reset}`);
|
|
187
|
+
console.log(` ${c.yellow}This app is VPN-only.${c.reset}`);
|
|
188
|
+
}
|
|
189
|
+
console.log();
|
|
190
|
+
} catch (err: any) { console.error(` ${c.red}Error:${c.reset} ${err.message}`); process.exit(2); }
|
|
191
|
+
return;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// Regular deploy path
|
|
195
|
+
const projectPath = path.resolve(targetPath || process.cwd());
|
|
148
196
|
try {
|
|
149
197
|
console.log(); console.log(` ${c.bold}canopy deploy${c.reset} ${c.dim}${projectPath}${c.reset}`);
|
|
150
198
|
console.log(` ${c.dim}name: ${opts.name}${c.reset}`); console.log();
|
|
@@ -279,4 +327,23 @@ program.command('destroy <name>').description('Remove an app (deletes server if
|
|
|
279
327
|
} catch (err: any) { console.error(` ${c.red}Error:${c.reset} ${err.message}`); process.exit(2); }
|
|
280
328
|
});
|
|
281
329
|
|
|
330
|
+
program.command('templates').description('List available deployment templates')
|
|
331
|
+
.option('--json', 'Output raw JSON')
|
|
332
|
+
.action((opts: { json?: boolean }) => {
|
|
333
|
+
const templates = listTemplates();
|
|
334
|
+
if (opts.json) { console.log(JSON.stringify(templates, null, 2)); return; }
|
|
335
|
+
console.log();
|
|
336
|
+
console.log(` ${c.bold}Available templates${c.reset}`);
|
|
337
|
+
console.log();
|
|
338
|
+
for (const t of templates) {
|
|
339
|
+
console.log(` ${c.bold}${t.name}${c.reset} ${c.dim}${t.description}${c.reset}`);
|
|
340
|
+
console.log(` ${c.dim}type: ${t.type} ports: ${t.ports.join(', ')}${t.min_ram ? ` min-ram: ${t.min_ram}` : ''}${c.reset}`);
|
|
341
|
+
if (t.env_required.length > 0) {
|
|
342
|
+
console.log(` ${c.dim}required env: ${t.env_required.map((e) => e.name).join(', ')}${c.reset}`);
|
|
343
|
+
}
|
|
344
|
+
console.log(` ${c.dim}docs: ${t.docs}${c.reset}`);
|
|
345
|
+
console.log();
|
|
346
|
+
}
|
|
347
|
+
});
|
|
348
|
+
|
|
282
349
|
program.parse();
|