@juspay/shooter 1.4.0 → 1.5.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 (88) hide show
  1. package/.claude/hooks/notifier.cjs +15 -1
  2. package/README.md +109 -17
  3. package/bin/shooter.cjs +30 -12
  4. package/build/client/_app/immutable/chunks/{BN1NjBrw.js → BIaXC2t9.js} +1 -1
  5. package/build/client/_app/immutable/chunks/BIaXC2t9.js.br +0 -0
  6. package/build/client/_app/immutable/chunks/BIaXC2t9.js.gz +0 -0
  7. package/build/client/_app/immutable/chunks/{CF4lQ45j.js → CRbaG9cv.js} +1 -1
  8. package/build/client/_app/immutable/chunks/CRbaG9cv.js.br +0 -0
  9. package/build/client/_app/immutable/chunks/CRbaG9cv.js.gz +0 -0
  10. package/build/client/_app/immutable/chunks/{CDVSripB.js → CiF38mQq.js} +1 -1
  11. package/build/client/_app/immutable/chunks/CiF38mQq.js.br +0 -0
  12. package/build/client/_app/immutable/chunks/CiF38mQq.js.gz +0 -0
  13. package/build/client/_app/immutable/entry/{app.DwWiuoEC.js → app.CU7KVZja.js} +2 -2
  14. package/build/client/_app/immutable/entry/app.CU7KVZja.js.br +0 -0
  15. package/build/client/_app/immutable/entry/app.CU7KVZja.js.gz +0 -0
  16. package/build/client/_app/immutable/entry/start.RAMZY19t.js +1 -0
  17. package/build/client/_app/immutable/entry/start.RAMZY19t.js.br +2 -0
  18. package/build/client/_app/immutable/entry/start.RAMZY19t.js.gz +0 -0
  19. package/build/client/_app/immutable/nodes/{0.ejabgzDQ.js → 0.Bi3XYMSu.js} +1 -1
  20. package/build/client/_app/immutable/nodes/0.Bi3XYMSu.js.br +0 -0
  21. package/build/client/_app/immutable/nodes/0.Bi3XYMSu.js.gz +0 -0
  22. package/build/client/_app/immutable/nodes/{1.BFK7Ubrr.js → 1.DTmfBFmm.js} +1 -1
  23. package/build/client/_app/immutable/nodes/1.DTmfBFmm.js.br +0 -0
  24. package/build/client/_app/immutable/nodes/1.DTmfBFmm.js.gz +0 -0
  25. package/build/client/_app/immutable/nodes/{2.DV3saFiY.js → 2.Cm269yzt.js} +1 -1
  26. package/build/client/_app/immutable/nodes/2.Cm269yzt.js.br +0 -0
  27. package/build/client/_app/immutable/nodes/2.Cm269yzt.js.gz +0 -0
  28. package/build/client/_app/immutable/nodes/{4.D6NIf10D.js → 4.C25c5hMg.js} +1 -1
  29. package/build/client/_app/immutable/nodes/4.C25c5hMg.js.br +0 -0
  30. package/build/client/_app/immutable/nodes/4.C25c5hMg.js.gz +0 -0
  31. package/build/client/_app/immutable/nodes/{5.g3R-QfIW.js → 5.DIkXVP4q.js} +1 -1
  32. package/build/client/_app/immutable/nodes/5.DIkXVP4q.js.br +0 -0
  33. package/build/client/_app/immutable/nodes/5.DIkXVP4q.js.gz +0 -0
  34. package/build/client/_app/immutable/nodes/{6.DSpd_nYK.js → 6.BPL-HzUX.js} +1 -1
  35. package/build/client/_app/immutable/nodes/6.BPL-HzUX.js.br +0 -0
  36. package/build/client/_app/immutable/nodes/6.BPL-HzUX.js.gz +0 -0
  37. package/build/client/_app/immutable/nodes/{7.F9WBFTz2.js → 7.IgEqce53.js} +1 -1
  38. package/build/client/_app/immutable/nodes/7.IgEqce53.js.br +0 -0
  39. package/build/client/_app/immutable/nodes/7.IgEqce53.js.gz +0 -0
  40. package/build/client/_app/version.json +1 -1
  41. package/build/client/_app/version.json.br +0 -0
  42. package/build/client/_app/version.json.gz +0 -0
  43. package/build/server/chunks/{0-ePgrkfG9.js → 0-DiORznXb.js} +2 -2
  44. package/build/server/chunks/{0-ePgrkfG9.js.map → 0-DiORznXb.js.map} +1 -1
  45. package/build/server/chunks/{1-BV7u1xGo.js → 1-D0N7vVhH.js} +2 -2
  46. package/build/server/chunks/{1-BV7u1xGo.js.map → 1-D0N7vVhH.js.map} +1 -1
  47. package/build/server/chunks/{2-3p1kyvjQ.js → 2-DfSav7a7.js} +2 -2
  48. package/build/server/chunks/{2-3p1kyvjQ.js.map → 2-DfSav7a7.js.map} +1 -1
  49. package/build/server/chunks/{4-ChFYfo_S.js → 4-DV5MZUz_.js} +2 -2
  50. package/build/server/chunks/{4-ChFYfo_S.js.map → 4-DV5MZUz_.js.map} +1 -1
  51. package/build/server/chunks/{5-q-tQLBBu.js → 5-DJhoAjb0.js} +2 -2
  52. package/build/server/chunks/{5-q-tQLBBu.js.map → 5-DJhoAjb0.js.map} +1 -1
  53. package/build/server/chunks/{6-BIaAbm8b.js → 6-Cp8CzYbr.js} +2 -2
  54. package/build/server/chunks/{6-BIaAbm8b.js.map → 6-Cp8CzYbr.js.map} +1 -1
  55. package/build/server/chunks/{7--TmbCgrH.js → 7-BA4xzUj3.js} +2 -2
  56. package/build/server/chunks/{7--TmbCgrH.js.map → 7-BA4xzUj3.js.map} +1 -1
  57. package/build/server/index.js +1 -1
  58. package/build/server/index.js.map +1 -1
  59. package/build/server/manifest.js +8 -8
  60. package/build/server/manifest.js.map +1 -1
  61. package/package.json +5 -2
  62. package/scripts/install.sh +122 -70
  63. package/scripts/setup.cjs +242 -241
  64. package/build/client/_app/immutable/chunks/BN1NjBrw.js.br +0 -0
  65. package/build/client/_app/immutable/chunks/BN1NjBrw.js.gz +0 -0
  66. package/build/client/_app/immutable/chunks/CDVSripB.js.br +0 -0
  67. package/build/client/_app/immutable/chunks/CDVSripB.js.gz +0 -0
  68. package/build/client/_app/immutable/chunks/CF4lQ45j.js.br +0 -0
  69. package/build/client/_app/immutable/chunks/CF4lQ45j.js.gz +0 -0
  70. package/build/client/_app/immutable/entry/app.DwWiuoEC.js.br +0 -0
  71. package/build/client/_app/immutable/entry/app.DwWiuoEC.js.gz +0 -0
  72. package/build/client/_app/immutable/entry/start.DG8BMhrh.js +0 -1
  73. package/build/client/_app/immutable/entry/start.DG8BMhrh.js.br +0 -0
  74. package/build/client/_app/immutable/entry/start.DG8BMhrh.js.gz +0 -0
  75. package/build/client/_app/immutable/nodes/0.ejabgzDQ.js.br +0 -0
  76. package/build/client/_app/immutable/nodes/0.ejabgzDQ.js.gz +0 -0
  77. package/build/client/_app/immutable/nodes/1.BFK7Ubrr.js.br +0 -0
  78. package/build/client/_app/immutable/nodes/1.BFK7Ubrr.js.gz +0 -0
  79. package/build/client/_app/immutable/nodes/2.DV3saFiY.js.br +0 -0
  80. package/build/client/_app/immutable/nodes/2.DV3saFiY.js.gz +0 -0
  81. package/build/client/_app/immutable/nodes/4.D6NIf10D.js.br +0 -0
  82. package/build/client/_app/immutable/nodes/4.D6NIf10D.js.gz +0 -0
  83. package/build/client/_app/immutable/nodes/5.g3R-QfIW.js.br +0 -0
  84. package/build/client/_app/immutable/nodes/5.g3R-QfIW.js.gz +0 -0
  85. package/build/client/_app/immutable/nodes/6.DSpd_nYK.js.br +0 -0
  86. package/build/client/_app/immutable/nodes/6.DSpd_nYK.js.gz +0 -0
  87. package/build/client/_app/immutable/nodes/7.F9WBFTz2.js.br +0 -0
  88. package/build/client/_app/immutable/nodes/7.F9WBFTz2.js.gz +0 -0
package/scripts/setup.cjs CHANGED
@@ -9,7 +9,7 @@ const { execSync, spawn } = require('child_process');
9
9
  const crypto = require('crypto');
10
10
  const fs = require('fs');
11
11
  const path = require('path');
12
- const http = require('http');
12
+
13
13
 
14
14
  // ── ANSI helpers ─────────────────────────────────────────────────────
15
15
  const C = {
@@ -63,9 +63,18 @@ const ROOT = process.env.SHOOTER_PKG_ROOT || path.resolve(__dirname, '..');
63
63
  const SHOOTER_HOME = process.env.SHOOTER_HOME || path.join(require('os').homedir(), '.shooter');
64
64
  const DOT_ENV_PATH = path.join(SHOOTER_HOME, '.env');
65
65
  const AUTO_MODE = process.argv.includes('--auto');
66
+ const PUSH_MODE = process.argv.includes('--push');
66
67
 
67
68
  let rl; // readline interface — created in main()
68
69
 
70
+ // ── Auto-incrementing step counter ──────────────────────────────────
71
+ let _stepNum = 0;
72
+ let _totalSteps = 5;
73
+ function step(label) {
74
+ _stepNum++;
75
+ console.log(`\n${C.blue}[${_stepNum}/${_totalSteps}] ${label}${C.reset}\n`);
76
+ }
77
+
69
78
  // ── Readline helpers ─────────────────────────────────────────────────
70
79
 
71
80
  function ask(question) {
@@ -97,18 +106,6 @@ async function askRequired(question, validator) {
97
106
  }
98
107
  }
99
108
 
100
- async function askMultiline(prompt) {
101
- console.log(cyan(prompt));
102
- console.log(dim(' Paste the content, then press Enter on an empty line to finish:'));
103
- const lines = [];
104
- while (true) {
105
- const line = await ask('');
106
- if (line === '') break;
107
- lines.push(line);
108
- }
109
- return lines.join('\n');
110
- }
111
-
112
109
  // ── Banner ───────────────────────────────────────────────────────────
113
110
 
114
111
  function printBanner() {
@@ -120,7 +117,7 @@ function printBanner() {
120
117
  console.log(`${C.cyan}${C.bold} |____/|_| |_|\\___/ \\___/ \\__\\___|_| ${C.reset}`);
121
118
  console.log('');
122
119
  console.log(bold(' Interactive Setup Wizard'));
123
- console.log(dim(' Push notifications for your coding sessions'));
120
+ console.log(dim(' Remote terminals, session viewer & push notifications'));
124
121
  console.log('');
125
122
  console.log(dim(' ─────────────────────────────────────────'));
126
123
  console.log('');
@@ -129,7 +126,7 @@ function printBanner() {
129
126
  // ── Prerequisite checks ─────────────────────────────────────────────
130
127
 
131
128
  function checkPrerequisites() {
132
- console.log(bold('1. Checking prerequisites...\n'));
129
+ step('Checking prerequisites...');
133
130
 
134
131
  // Node.js version
135
132
  const nodeVersion = process.versions.node;
@@ -205,6 +202,180 @@ function validateEmail(email) {
205
202
 
206
203
  // ── Collect configuration ────────────────────────────────────────────
207
204
 
205
+ // ── Collect push notification config (separate flow) ────────────────
206
+
207
+ async function collectPushConfig(config) {
208
+ // ── iOS Push Notifications ───────────────────────────────────────
209
+ console.log(bold(' iOS Push Notifications\n'));
210
+ config.wantIos = await confirm(' Configure iOS push notifications?');
211
+
212
+ if (config.wantIos) {
213
+ console.log('');
214
+
215
+ // APNs key (.p8) — accept file path or pasted content, with retry on invalid input
216
+ async function askForP8Key() {
217
+ const apnsKeyInput = await askRequired(
218
+ ` APNS_KEY (.p8 file path or paste key): `,
219
+ (val) => {
220
+ if (!val) return 'APNs key is required.';
221
+ return null;
222
+ }
223
+ );
224
+
225
+ let key;
226
+
227
+ // Check if input is a file path
228
+ if (fs.existsSync(apnsKeyInput)) {
229
+ try {
230
+ key = fs.readFileSync(apnsKeyInput, 'utf-8').trim();
231
+ console.log(green(` Read key from ${apnsKeyInput}`));
232
+ } catch (err) {
233
+ console.log(red(` Could not read file: ${err.message}`));
234
+ process.exit(1);
235
+ }
236
+ } else if (apnsKeyInput.includes('BEGIN PRIVATE KEY')) {
237
+ // User pasted inline content
238
+ key = apnsKeyInput;
239
+ } else {
240
+ // Might be a partial path or multiline paste — try multiline
241
+ console.log(yellow(' Input does not look like a file path or key.'));
242
+ console.log(dim(' Paste the full .p8 key contents (press Enter on empty line to finish):'));
243
+ const lines = [apnsKeyInput];
244
+ while (true) {
245
+ const line = await ask('');
246
+ if (line === '') break;
247
+ lines.push(line);
248
+ }
249
+ key = lines.join('\n');
250
+ }
251
+
252
+ if (!key.includes('BEGIN PRIVATE KEY')) {
253
+ console.log(yellow(' Warning: File does not appear to be a valid .p8 private key.'));
254
+ const retry = await ask(cyan(' Re-enter path? [Y/n]: '));
255
+ if (retry.toLowerCase() !== 'n') {
256
+ return askForP8Key(); // Recursive retry
257
+ }
258
+ console.log(yellow(' Skipping APNs configuration.'));
259
+ return null;
260
+ }
261
+
262
+ return key;
263
+ }
264
+
265
+ config.apnsKey = await askForP8Key();
266
+ if (!config.apnsKey) {
267
+ config.wantIos = false;
268
+ }
269
+ console.log('');
270
+
271
+ config.apnsKeyId = await askRequired(
272
+ ' APNS_KEY_ID (10-char key identifier): ',
273
+ validateAPNsKeyId
274
+ );
275
+ config.apnsTeamId = await askRequired(
276
+ ' APNS_TEAM_ID (10-char team identifier): ',
277
+ validateTeamId
278
+ );
279
+ config.apnsBundleId = await askRequired(
280
+ ' APNS_BUNDLE_ID (e.g. com.example.shooter): ',
281
+ validateBundleId
282
+ );
283
+ config.deviceToken = await askRequired(
284
+ ' DEVICE_TOKEN (64-char hex from iOS device): ',
285
+ validateDeviceToken
286
+ );
287
+
288
+ console.log('');
289
+ config.apnsProduction = await confirm(
290
+ ' Use production APNs gateway? (use "yes" for TestFlight/App Store)'
291
+ );
292
+ }
293
+ console.log('');
294
+
295
+ // ── Android Push Notifications ───────────────────────────────────
296
+ console.log(bold(' Android Push Notifications\n'));
297
+ config.wantAndroid = await confirm(' Configure Android push notifications?');
298
+
299
+ if (config.wantAndroid) {
300
+ console.log('');
301
+ config.fcmProjectId = await askRequired(' FCM_PROJECT_ID: ');
302
+ config.fcmClientEmail = await askRequired(' FCM_CLIENT_EMAIL: ', validateEmail);
303
+
304
+ const fcmKeyInput = await askRequired(' FCM_PRIVATE_KEY (file path or paste key): ');
305
+
306
+ if (fs.existsSync(fcmKeyInput)) {
307
+ try {
308
+ config.fcmPrivateKey = fs.readFileSync(fcmKeyInput, 'utf-8').trim();
309
+ console.log(green(` Read key from ${fcmKeyInput}`));
310
+ } catch (err) {
311
+ console.log(red(` Could not read file: ${err.message}`));
312
+ process.exit(1);
313
+ }
314
+ } else if (fcmKeyInput.includes('BEGIN')) {
315
+ config.fcmPrivateKey = fcmKeyInput;
316
+ } else {
317
+ console.log(dim(' Paste the full private key (press Enter on empty line to finish):'));
318
+ const lines = [fcmKeyInput];
319
+ while (true) {
320
+ const line = await ask('');
321
+ if (line === '') break;
322
+ lines.push(line);
323
+ }
324
+ config.fcmPrivateKey = lines.join('\n');
325
+ }
326
+
327
+ if (!config.fcmPrivateKey.includes('BEGIN')) {
328
+ console.log(yellow(' Warning: Key does not look like a PEM private key. Continuing anyway.'));
329
+ }
330
+ console.log('');
331
+
332
+ const androidTokenAnswer = await ask(
333
+ ` ANDROID_DEVICE_TOKEN ${dim('(optional, press Enter to skip)')}: `
334
+ );
335
+ if (androidTokenAnswer) {
336
+ config.androidDeviceToken = androidTokenAnswer;
337
+ }
338
+ }
339
+ console.log('');
340
+ }
341
+
342
+ // ── Load existing push config from .env (preserve during non-push setup) ──
343
+
344
+ function loadExistingPushConfig(config) {
345
+ if (!fs.existsSync(DOT_ENV_PATH)) return;
346
+ const existing = fs.readFileSync(DOT_ENV_PATH, 'utf-8');
347
+
348
+ const get = (key) => {
349
+ const m = existing.match(new RegExp(`^${key}=["']?(.+?)["']?$`, 'm'));
350
+ return m ? m[1] : '';
351
+ };
352
+
353
+ // Preserve iOS config if it was previously set
354
+ const apnsKeyId = get('APNS_KEY_ID');
355
+ if (apnsKeyId) {
356
+ config.wantIos = true;
357
+ // Read the raw APNS_KEY (may have escaped newlines)
358
+ const rawApnsKey = get('APNS_KEY');
359
+ config.apnsKey = rawApnsKey ? rawApnsKey.replace(/\\n/g, '\n') : '';
360
+ config.apnsKeyId = apnsKeyId;
361
+ config.apnsTeamId = get('APNS_TEAM_ID');
362
+ config.apnsBundleId = get('APNS_BUNDLE_ID');
363
+ config.apnsProduction = get('APNS_PRODUCTION') === 'true';
364
+ config.deviceToken = get('DEVICE_TOKEN');
365
+ }
366
+
367
+ // Preserve Android config if it was previously set
368
+ const fcmProjectId = get('FCM_PROJECT_ID');
369
+ if (fcmProjectId) {
370
+ config.wantAndroid = true;
371
+ config.fcmProjectId = fcmProjectId;
372
+ config.fcmClientEmail = get('FCM_CLIENT_EMAIL');
373
+ const rawFcmKey = get('FCM_PRIVATE_KEY');
374
+ config.fcmPrivateKey = rawFcmKey ? rawFcmKey.replace(/\\n/g, '\n') : '';
375
+ config.androidDeviceToken = get('ANDROID_DEVICE_TOKEN');
376
+ }
377
+ }
378
+
208
379
  async function collectConfig() {
209
380
  const config = {
210
381
  apiKey: '',
@@ -226,17 +397,22 @@ async function collectConfig() {
226
397
 
227
398
  // ── Auto mode: reuse existing key or generate new, skip push config ──
228
399
  if (AUTO_MODE) {
229
- console.log(bold('2. Auto-configuring...\n'));
400
+ step('Auto-configuring...');
230
401
 
231
402
  // Preserve existing API_KEY if .env already exists
232
403
  if (fs.existsSync(DOT_ENV_PATH)) {
233
404
  const existing = fs.readFileSync(DOT_ENV_PATH, 'utf-8');
234
- const match = existing.match(/^API_KEY=(.+)$/m);
405
+ const match = existing.match(/^API_KEY=["']?(.+?)["']?$/m);
235
406
  if (match && match[1]) {
236
407
  config.apiKey = match[1];
237
408
  const maskedKey = mask(config.apiKey);
238
409
  console.log(green(` Existing API key preserved: ${maskedKey}`));
239
- console.log(dim(' Push notifications skipped (run "shooter setup" to configure later)'));
410
+ loadExistingPushConfig(config);
411
+ if (config.wantIos || config.wantAndroid) {
412
+ console.log(green(' Existing push config preserved.'));
413
+ } else {
414
+ console.log(dim(' Push notifications not configured (add later with "shooter setup --push")'));
415
+ }
240
416
  console.log('');
241
417
  return config;
242
418
  }
@@ -245,13 +421,13 @@ async function collectConfig() {
245
421
  config.apiKey = generateSecureKey();
246
422
  const maskedKey = mask(config.apiKey);
247
423
  console.log(green(` API key generated: ${maskedKey}`));
248
- console.log(dim(' Push notifications skipped (run "shooter setup" to configure later)'));
424
+ console.log(dim(' Push notifications not configured (add later with "shooter setup --push")'));
249
425
  console.log('');
250
426
  return config;
251
427
  }
252
428
 
253
429
  // ── API Key ──────────────────────────────────────────────────────
254
- console.log(bold('2. Server authentication\n'));
430
+ step('Server authentication');
255
431
  const apiKeyAnswer = await ask(` API_KEY ${dim('(press Enter to auto-generate)')}: `);
256
432
  if (apiKeyAnswer) {
257
433
  config.apiKey = apiKeyAnswer;
@@ -263,69 +439,26 @@ async function collectConfig() {
263
439
  }
264
440
  console.log('');
265
441
 
266
- // ── iOS Push Notifications ───────────────────────────────────────
267
- console.log(bold('3. iOS Push Notifications\n'));
268
- config.wantIos = await confirm(' Do you want iOS push notifications?');
269
-
270
- if (config.wantIos) {
271
- console.log('');
272
-
273
- // APNs key (.p8)
274
- config.apnsKey = await askMultiline(' APNS_KEY (.p8 private key contents):');
275
- if (!config.apnsKey.includes('BEGIN PRIVATE KEY')) {
276
- console.log(yellow(' Warning: Key does not look like a .p8 file. Continuing anyway.'));
277
- }
278
- console.log('');
279
-
280
- config.apnsKeyId = await askRequired(
281
- ' APNS_KEY_ID (10-char key identifier): ',
282
- validateAPNsKeyId
283
- );
284
- config.apnsTeamId = await askRequired(
285
- ' APNS_TEAM_ID (10-char team identifier): ',
286
- validateTeamId
287
- );
288
- config.apnsBundleId = await askRequired(
289
- ' APNS_BUNDLE_ID (e.g. com.example.shooter): ',
290
- validateBundleId
291
- );
292
- config.deviceToken = await askRequired(
293
- ' DEVICE_TOKEN (64-char hex from iOS device): ',
294
- validateDeviceToken
295
- );
296
-
297
- console.log('');
298
- config.apnsProduction = await confirm(
299
- ' Use production APNs gateway? (use "yes" for TestFlight/App Store)'
300
- );
301
- }
302
- console.log('');
303
-
304
- // ── Android Push Notifications ───────────────────────────────────
305
- console.log(bold('4. Android Push Notifications\n'));
306
- config.wantAndroid = await confirm(' Do you want Android push notifications?');
307
-
308
- if (config.wantAndroid) {
309
- console.log('');
310
- config.fcmProjectId = await askRequired(' FCM_PROJECT_ID: ');
311
- config.fcmClientEmail = await askRequired(' FCM_CLIENT_EMAIL: ', validateEmail);
442
+ // ── Push notifications: only if --push flag or user opts in ──────
443
+ if (PUSH_MODE) {
444
+ // Direct push config mode user explicitly asked for it
445
+ step('Push notification setup');
446
+ await collectPushConfig(config);
447
+ } else {
448
+ // Default: skip push, show how to add later
449
+ // Preserve any existing push config from a previous setup
450
+ loadExistingPushConfig(config);
312
451
 
313
- config.fcmPrivateKey = await askMultiline(' FCM_PRIVATE_KEY (service account private key):');
314
- if (!config.fcmPrivateKey.includes('BEGIN')) {
315
- console.log(
316
- yellow(' Warning: Key does not look like a PEM private key. Continuing anyway.')
317
- );
452
+ if (config.wantIos || config.wantAndroid) {
453
+ console.log(dim(' Existing push notification config preserved.'));
454
+ console.log(dim(' To reconfigure: shooter setup --push'));
455
+ } else {
456
+ console.log(dim(' Push notifications are optional and not required for the server.'));
457
+ console.log(dim(' Terminals, sessions, and the web UI work without push config.'));
458
+ console.log(dim(` Add push later: ${cyan('shooter setup --push')}`));
318
459
  }
319
460
  console.log('');
320
-
321
- const androidTokenAnswer = await ask(
322
- ` ANDROID_DEVICE_TOKEN ${dim('(optional, press Enter to skip)')}: `
323
- );
324
- if (androidTokenAnswer) {
325
- config.androidDeviceToken = androidTokenAnswer;
326
- }
327
461
  }
328
- console.log('');
329
462
 
330
463
  return config;
331
464
  }
@@ -407,7 +540,7 @@ function buildEnvContent(config) {
407
540
  }
408
541
 
409
542
  async function writeEnv(config) {
410
- console.log(bold('5. Writing .env file\n'));
543
+ step('Writing .env file');
411
544
 
412
545
  if (fs.existsSync(DOT_ENV_PATH) && !AUTO_MODE) {
413
546
  const overwrite = await confirm(' .env already exists. Overwrite it?');
@@ -427,69 +560,17 @@ async function writeEnv(config) {
427
560
  }
428
561
 
429
562
  fs.writeFileSync(DOT_ENV_PATH, content, { mode: 0o600 });
563
+ // Enforce secure permissions even if file already existed
564
+ fs.chmodSync(DOT_ENV_PATH, 0o600);
430
565
  console.log(green(' .env written successfully.'));
431
566
  console.log('');
432
567
  return true;
433
568
  }
434
569
 
435
- // ── Shell profile export ─────────────────────────────────────────────
436
-
437
- function detectShellProfile() {
438
- const home = require('os').homedir();
439
- const shell = process.env.SHELL || '';
440
-
441
- if (shell.endsWith('/zsh')) {
442
- return path.join(home, '.zshrc');
443
- }
444
- if (shell.endsWith('/bash')) {
445
- // macOS uses .bash_profile for login shells; Linux uses .bashrc
446
- const bashProfile = path.join(home, '.bash_profile');
447
- if (fs.existsSync(bashProfile)) return bashProfile;
448
- return path.join(home, '.bashrc');
449
- }
450
- // Fallback for other shells
451
- return path.join(home, '.profile');
452
- }
453
-
454
- async function offerShellExport(apiKey) {
455
- console.log(bold('6. Shell environment\n'));
456
-
457
- const profilePath = detectShellProfile();
458
- const profileName = path.basename(profilePath);
459
-
460
- // Check if export already exists
461
- if (fs.existsSync(profilePath)) {
462
- const contents = fs.readFileSync(profilePath, 'utf-8');
463
- if (contents.includes('export API_KEY=')) {
464
- console.log(green(` export API_KEY is already in ~/${profileName}`));
465
- console.log('');
466
- return;
467
- }
468
- }
469
-
470
- console.log(dim(' The Claude Code hooks need API_KEY in your shell environment.'));
471
- const shouldAdd = AUTO_MODE || await confirm(` Add 'export API_KEY=...' to ~/${profileName}?`);
472
-
473
- if (shouldAdd) {
474
- const exportLine = `\nexport API_KEY="${escapeForDoubleQuotedShell(apiKey)}"\n`;
475
- fs.appendFileSync(profilePath, exportLine, 'utf-8');
476
- console.log(green(` Added to ~/${profileName}`));
477
- console.log(
478
- yellow(` Run ${cyan(`source ~/${profileName}`)} or open a new terminal for hooks to work.`)
479
- );
480
- } else {
481
- console.log(
482
- yellow(' Skipped. You will need to set API_KEY manually for hooks to authenticate.')
483
- );
484
- console.log(dim(` Add this to your shell profile: export API_KEY="<your key from .env>"`));
485
- }
486
- console.log('');
487
- }
488
-
489
570
  // ── Build ────────────────────────────────────────────────────────────
490
571
 
491
572
  function runBuild() {
492
- console.log(bold('7. Building the project...\n'));
573
+ step('Building the project...');
493
574
  try {
494
575
  execSync('pnpm build', { cwd: ROOT, stdio: 'inherit' });
495
576
  console.log('');
@@ -505,107 +586,6 @@ function runBuild() {
505
586
  }
506
587
  }
507
588
 
508
- // ── Health check ─────────────────────────────────────────────────────
509
-
510
- function testHealth() {
511
- return new Promise((resolve) => {
512
- console.log(bold('9. Testing server health...\n'));
513
-
514
- const port = process.env.PORT || 54007;
515
- let serverProcess;
516
- let resolved = false;
517
-
518
- function finish(ok, msg) {
519
- if (resolved) return;
520
- resolved = true;
521
- if (serverProcess && !serverProcess.killed) {
522
- serverProcess.kill('SIGTERM');
523
- }
524
- if (ok) {
525
- console.log(green(` ${msg}`));
526
- } else {
527
- console.log(yellow(` ${msg}`));
528
- }
529
- console.log('');
530
- resolve(ok);
531
- }
532
-
533
- // Start the server
534
- serverProcess = spawn('node', ['--import', 'tsx', 'server.ts'], {
535
- cwd: ROOT,
536
- stdio: 'pipe',
537
- env: { ...process.env, PORT: port.toString(), SHOOTER_HOME, SHOOTER_PKG_ROOT: ROOT },
538
- });
539
-
540
- serverProcess.on('error', (err) => {
541
- finish(false, `Could not start server: ${err.message}`);
542
- });
543
-
544
- serverProcess.on('exit', (code) => {
545
- if (!resolved) {
546
- finish(false, `Server exited unexpectedly (code ${code}).`);
547
- }
548
- });
549
-
550
- // Capture stderr/stdout for "listening" signal; try health after delay
551
- let output = '';
552
- const collectOutput = (data) => {
553
- output += data.toString();
554
- };
555
- serverProcess.stdout.on('data', collectOutput);
556
- serverProcess.stderr.on('data', collectOutput);
557
-
558
- // Give the server up to 15 seconds to start, polling every second
559
- let attempts = 0;
560
- const maxAttempts = 15;
561
-
562
- const poll = setInterval(() => {
563
- attempts++;
564
- if (resolved) {
565
- clearInterval(poll);
566
- return;
567
- }
568
- if (attempts > maxAttempts) {
569
- clearInterval(poll);
570
- finish(
571
- false,
572
- 'Server did not respond within 15 seconds. You can test manually with: curl http://localhost:' +
573
- port +
574
- '/api/health'
575
- );
576
- return;
577
- }
578
-
579
- const req = http.get(`http://localhost:${port}/api/health`, (res) => {
580
- let body = '';
581
- res.on('data', (chunk) => {
582
- body += chunk;
583
- });
584
- res.on('end', () => {
585
- clearInterval(poll);
586
- try {
587
- const data = JSON.parse(body);
588
- if (data.status === 'healthy') {
589
- finish(true, `Health check passed: status=${data.status}`);
590
- } else {
591
- finish(
592
- true,
593
- `Server running (status=${data.status}). Some optional features may need configuration.`
594
- );
595
- }
596
- } catch {
597
- finish(true, 'Server responded but health response was not JSON.');
598
- }
599
- });
600
- });
601
- req.on('error', () => {
602
- // Server not ready yet — will retry
603
- });
604
- req.setTimeout(2000, () => req.destroy());
605
- }, 1000);
606
- });
607
- }
608
-
609
589
  // ── Main ─────────────────────────────────────────────────────────────
610
590
 
611
591
  async function main() {
@@ -625,17 +605,30 @@ async function main() {
625
605
  rl.close();
626
606
  });
627
607
 
608
+ // Reset step counter; push mode adds one extra step
609
+ _stepNum = 0;
610
+ _totalSteps = (PUSH_MODE && !AUTO_MODE) ? 6 : 5;
611
+
628
612
  printBanner();
613
+
614
+ if (PUSH_MODE) {
615
+ console.log(bold(' Adding push notification support...\n'));
616
+ }
617
+
629
618
  checkPrerequisites();
630
619
 
631
620
  const config = await collectConfig();
632
621
  await writeEnv(config);
633
- await offerShellExport(config.apiKey);
622
+
623
+ // API_KEY is stored in ~/.shooter/.env — hooks read it automatically
624
+ console.log(dim(' API_KEY is stored in ~/.shooter/.env'));
625
+ console.log(dim(' Claude Code hooks read it automatically from there.'));
626
+ console.log('');
634
627
 
635
628
  const buildOk = runBuild();
636
629
 
637
630
  // ── Remote access info ───────────────────────────────────────────────
638
- console.log(bold('8. Remote access (optional)\n'));
631
+ step('Remote access (optional)');
639
632
 
640
633
  let cloudflaredAvailable = false;
641
634
  try {
@@ -710,7 +703,8 @@ async function main() {
710
703
  rl.close();
711
704
 
712
705
  if (buildOk) {
713
- await testHealth();
706
+ console.log(green(' Ready to start! Run: shooter start'));
707
+ console.log('');
714
708
  }
715
709
 
716
710
  // ── Done ───────────────────────────────────────────────────────────
@@ -726,8 +720,15 @@ async function main() {
726
720
  console.log(bold(' Your API key (enter this in the app on your phone):'));
727
721
  console.log(`\n ${C.bold}${C.cyan}${mask(config.apiKey)}${C.reset}\n`);
728
722
  if (!config.wantIos && !config.wantAndroid) {
729
- console.log(yellow(' Note: No push notification platform was configured.'));
730
- console.log(dim(' Run shooter setup again to add iOS or Android push notifications.'));
723
+ console.log(bold(' Optional add-ons:'));
724
+ console.log(` ${dim('Push notifications:')} ${cyan('shooter setup --push')}`);
725
+ console.log(` ${dim('Cloudflare Tunnel:')} ${cyan('shooter start')} ${dim('(auto-starts tunnel)')}`);
726
+ console.log('');
727
+ } else {
728
+ const platforms = [];
729
+ if (config.wantIos) platforms.push('iOS');
730
+ if (config.wantAndroid) platforms.push('Android');
731
+ console.log(green(` Push notifications: ${platforms.join(' + ')} configured`));
731
732
  console.log('');
732
733
  }
733
734
 
@@ -1 +0,0 @@
1
- import{l as o,a as r}from"../chunks/BN1NjBrw.js";export{o as load_css,r as start};