shopify-tunnel-dev 0.2.0 → 0.3.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 CHANGED
@@ -47,18 +47,20 @@ pnpm add -D shopify-tunnel-dev
47
47
  "scripts": { "dev": "shopify-tunnel-dev" }
48
48
  ```
49
49
 
50
- `.gitignore`:
51
-
52
- ```
53
- .cloudflared/
50
+ ```bash
51
+ npx shopify-tunnel-dev init
54
52
  ```
55
53
 
54
+ `init` creates `.cloudflared/account.json` (empty template, mode 600), adds `.cloudflared/` to
55
+ `.gitignore`, and adds `DEV_HOST=` / `DEV_PORT=` to `.env`. It never overwrites anything, and
56
+ `pnpm dev` runs it automatically in a fresh clone.
57
+
56
58
  Give each dev their own app in the Partner Dashboard; the app URL lives on Shopify, not in the tunnel.
57
59
 
58
60
  ## Per dev
59
61
 
60
- Copy `cert.pem` and `account.json` from the password manager into `.cloudflared/`
61
- (`chmod 600 .cloudflared/*`), then add to `.env`:
62
+ Run `pnpm dev` once to scaffold, then fill `.cloudflared/account.json` and add `.cloudflared/cert.pem`
63
+ from the password manager (`chmod 600 .cloudflared/*`). Set in `.env`:
62
64
 
63
65
  ```
64
66
  DEV_HOST=dev-myapp-alice.example.com
@@ -3,9 +3,15 @@ import { spawn } from 'node:child_process';
3
3
  import { resolveCloudflared } from '../src/cloudflared-binary.js';
4
4
  import { ConfigError, loadConfig } from '../src/load-config.js';
5
5
  import { stopCloudflared } from '../src/run-cloudflared.js';
6
+ import { scaffoldProject } from '../src/scaffold-project.js';
6
7
  import { startNamedTunnel, startQuickTunnel } from '../src/start-tunnel.js';
7
8
 
8
9
  const USAGE = `Usage: shopify-tunnel-dev [dev] [options] [-- shopify app dev args]
10
+ shopify-tunnel-dev init
11
+
12
+ Commands:
13
+ dev Start the tunnel and shopify app dev (default)
14
+ init Create .cloudflared/account.json, gitignore it, add DEV_HOST/DEV_PORT to .env
9
15
 
10
16
  Options:
11
17
  -q, --quick Random *.trycloudflare.com URL, no Cloudflare credentials needed
@@ -14,9 +20,13 @@ Options:
14
20
 
15
21
  Any other argument (e.g. --reset, --store=foo) is passed to \`shopify app dev\`.`;
16
22
 
23
+ const NEXT_STEPS =
24
+ 'set DEV_HOST and DEV_PORT in .env, fill .cloudflared/account.json and add .cloudflared/cert.pem';
25
+
17
26
  function parseArgs(argv) {
18
- const args = argv[0] === 'dev' ? argv.slice(1) : argv;
19
- const opts = { quick: false, force: false, help: false, passthrough: [] };
27
+ const init = argv[0] === 'init';
28
+ const args = argv[0] === 'dev' || init ? argv.slice(1) : argv;
29
+ const opts = { init, quick: false, force: false, help: false, passthrough: [] };
20
30
  for (const arg of args) {
21
31
  if (arg === '-q' || arg === '--quick') opts.quick = true;
22
32
  else if (arg === '-f' || arg === '--force') opts.force = true;
@@ -29,6 +39,15 @@ function parseArgs(argv) {
29
39
  async function main() {
30
40
  const opts = parseArgs(process.argv.slice(2));
31
41
  if (opts.help) return console.log(USAGE);
42
+ // `init` scaffolds explicitly; a named `dev` run in a fresh clone does the same before complaining,
43
+ // so the config error points at files that already exist.
44
+ if (opts.init || !opts.quick) {
45
+ const changes = scaffoldProject();
46
+ changes.forEach((change) => console.log(`[init] ${change}`));
47
+ if (changes.length) console.log(`[init] next: ${NEXT_STEPS}`);
48
+ else if (opts.init) console.log('[init] nothing to do');
49
+ if (opts.init) return;
50
+ }
32
51
 
33
52
  const cfg = loadConfig({ quick: opts.quick });
34
53
  const bin = await resolveCloudflared();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "shopify-tunnel-dev",
3
- "version": "0.2.0",
3
+ "version": "0.3.0",
4
4
  "description": "Run `shopify app dev` behind a fixed-hostname Cloudflare named tunnel that the whole team shares one credential set for",
5
5
  "type": "module",
6
6
  "bin": {
@@ -0,0 +1,44 @@
1
+ import { appendFileSync, existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
2
+ import path from 'node:path';
3
+ import { parseEnv } from 'node:util';
4
+
5
+ const ACCOUNT_TEMPLATE = { CF_ACCOUNT_TAG: '', CF_SECRET_KEY: '', CF_HOST_PATTERN: '' };
6
+
7
+ // Idempotent: only creates what is missing, never overwrites. Returns the list of changes made.
8
+ export function scaffoldProject(cwd = process.cwd()) {
9
+ const changes = [];
10
+ const dir = path.join(cwd, '.cloudflared');
11
+ const accountPath = path.join(dir, 'account.json');
12
+
13
+ if (!existsSync(dir)) {
14
+ mkdirSync(dir, { mode: 0o700 });
15
+ changes.push('created .cloudflared/');
16
+ }
17
+ if (!existsSync(accountPath)) {
18
+ writeFileSync(accountPath, `${JSON.stringify(ACCOUNT_TEMPLATE, null, 2)}\n`, { mode: 0o600 });
19
+ changes.push('created .cloudflared/account.json (fill in from your password manager)');
20
+ }
21
+
22
+ // Keep the shared secrets out of git before anyone pastes them in.
23
+ const gitignorePath = path.join(cwd, '.gitignore');
24
+ const gitignore = existsSync(gitignorePath) ? readFileSync(gitignorePath, 'utf8') : '';
25
+ if (!/^\/?\.cloudflared\/?(\*)?\s*$/m.test(gitignore)) {
26
+ appendLine(gitignorePath, gitignore, '.cloudflared/');
27
+ changes.push('added .cloudflared/ to .gitignore');
28
+ }
29
+
30
+ const envPath = [path.join(cwd, 'web/.env'), path.join(cwd, '.env')].find((p) => existsSync(p)) ?? path.join(cwd, '.env');
31
+ const envText = existsSync(envPath) ? readFileSync(envPath, 'utf8') : '';
32
+ const env = parseEnv(envText);
33
+ const missingKeys = ['DEV_HOST', 'DEV_PORT'].filter((key) => !(key in env));
34
+ if (missingKeys.length) {
35
+ appendLine(envPath, envText, missingKeys.map((key) => `${key}=`).join('\n'));
36
+ changes.push(`added ${missingKeys.join(', ')} to ${path.relative(cwd, envPath)}`);
37
+ }
38
+
39
+ return changes;
40
+ }
41
+
42
+ function appendLine(file, current, line) {
43
+ appendFileSync(file, `${current && !current.endsWith('\n') ? '\n' : ''}${line}\n`);
44
+ }