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.
Files changed (2) hide show
  1. package/package.json +3 -3
  2. package/src/index.ts +72 -5
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "canopy-deploy",
3
- "version": "1.0.0",
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.0.0",
14
- "@canopy/deploy": "1.0.0"
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: '🛡️ ', default: ' ',
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();