canopy-deploy 1.0.0 → 1.1.1

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