@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.
- package/.claude/hooks/notifier.cjs +15 -1
- package/README.md +109 -17
- package/bin/shooter.cjs +30 -12
- package/build/client/_app/immutable/chunks/{BN1NjBrw.js → BIaXC2t9.js} +1 -1
- package/build/client/_app/immutable/chunks/BIaXC2t9.js.br +0 -0
- package/build/client/_app/immutable/chunks/BIaXC2t9.js.gz +0 -0
- package/build/client/_app/immutable/chunks/{CF4lQ45j.js → CRbaG9cv.js} +1 -1
- package/build/client/_app/immutable/chunks/CRbaG9cv.js.br +0 -0
- package/build/client/_app/immutable/chunks/CRbaG9cv.js.gz +0 -0
- package/build/client/_app/immutable/chunks/{CDVSripB.js → CiF38mQq.js} +1 -1
- package/build/client/_app/immutable/chunks/CiF38mQq.js.br +0 -0
- package/build/client/_app/immutable/chunks/CiF38mQq.js.gz +0 -0
- package/build/client/_app/immutable/entry/{app.DwWiuoEC.js → app.CU7KVZja.js} +2 -2
- package/build/client/_app/immutable/entry/app.CU7KVZja.js.br +0 -0
- package/build/client/_app/immutable/entry/app.CU7KVZja.js.gz +0 -0
- package/build/client/_app/immutable/entry/start.RAMZY19t.js +1 -0
- package/build/client/_app/immutable/entry/start.RAMZY19t.js.br +2 -0
- package/build/client/_app/immutable/entry/start.RAMZY19t.js.gz +0 -0
- package/build/client/_app/immutable/nodes/{0.ejabgzDQ.js → 0.Bi3XYMSu.js} +1 -1
- package/build/client/_app/immutable/nodes/0.Bi3XYMSu.js.br +0 -0
- package/build/client/_app/immutable/nodes/0.Bi3XYMSu.js.gz +0 -0
- package/build/client/_app/immutable/nodes/{1.BFK7Ubrr.js → 1.DTmfBFmm.js} +1 -1
- package/build/client/_app/immutable/nodes/1.DTmfBFmm.js.br +0 -0
- package/build/client/_app/immutable/nodes/1.DTmfBFmm.js.gz +0 -0
- package/build/client/_app/immutable/nodes/{2.DV3saFiY.js → 2.Cm269yzt.js} +1 -1
- package/build/client/_app/immutable/nodes/2.Cm269yzt.js.br +0 -0
- package/build/client/_app/immutable/nodes/2.Cm269yzt.js.gz +0 -0
- package/build/client/_app/immutable/nodes/{4.D6NIf10D.js → 4.C25c5hMg.js} +1 -1
- package/build/client/_app/immutable/nodes/4.C25c5hMg.js.br +0 -0
- package/build/client/_app/immutable/nodes/4.C25c5hMg.js.gz +0 -0
- package/build/client/_app/immutable/nodes/{5.g3R-QfIW.js → 5.DIkXVP4q.js} +1 -1
- package/build/client/_app/immutable/nodes/5.DIkXVP4q.js.br +0 -0
- package/build/client/_app/immutable/nodes/5.DIkXVP4q.js.gz +0 -0
- package/build/client/_app/immutable/nodes/{6.DSpd_nYK.js → 6.BPL-HzUX.js} +1 -1
- package/build/client/_app/immutable/nodes/6.BPL-HzUX.js.br +0 -0
- package/build/client/_app/immutable/nodes/6.BPL-HzUX.js.gz +0 -0
- package/build/client/_app/immutable/nodes/{7.F9WBFTz2.js → 7.IgEqce53.js} +1 -1
- package/build/client/_app/immutable/nodes/7.IgEqce53.js.br +0 -0
- package/build/client/_app/immutable/nodes/7.IgEqce53.js.gz +0 -0
- package/build/client/_app/version.json +1 -1
- package/build/client/_app/version.json.br +0 -0
- package/build/client/_app/version.json.gz +0 -0
- package/build/server/chunks/{0-ePgrkfG9.js → 0-DiORznXb.js} +2 -2
- package/build/server/chunks/{0-ePgrkfG9.js.map → 0-DiORznXb.js.map} +1 -1
- package/build/server/chunks/{1-BV7u1xGo.js → 1-D0N7vVhH.js} +2 -2
- package/build/server/chunks/{1-BV7u1xGo.js.map → 1-D0N7vVhH.js.map} +1 -1
- package/build/server/chunks/{2-3p1kyvjQ.js → 2-DfSav7a7.js} +2 -2
- package/build/server/chunks/{2-3p1kyvjQ.js.map → 2-DfSav7a7.js.map} +1 -1
- package/build/server/chunks/{4-ChFYfo_S.js → 4-DV5MZUz_.js} +2 -2
- package/build/server/chunks/{4-ChFYfo_S.js.map → 4-DV5MZUz_.js.map} +1 -1
- package/build/server/chunks/{5-q-tQLBBu.js → 5-DJhoAjb0.js} +2 -2
- package/build/server/chunks/{5-q-tQLBBu.js.map → 5-DJhoAjb0.js.map} +1 -1
- package/build/server/chunks/{6-BIaAbm8b.js → 6-Cp8CzYbr.js} +2 -2
- package/build/server/chunks/{6-BIaAbm8b.js.map → 6-Cp8CzYbr.js.map} +1 -1
- package/build/server/chunks/{7--TmbCgrH.js → 7-BA4xzUj3.js} +2 -2
- package/build/server/chunks/{7--TmbCgrH.js.map → 7-BA4xzUj3.js.map} +1 -1
- package/build/server/index.js +1 -1
- package/build/server/index.js.map +1 -1
- package/build/server/manifest.js +8 -8
- package/build/server/manifest.js.map +1 -1
- package/package.json +5 -2
- package/scripts/install.sh +122 -70
- package/scripts/setup.cjs +242 -241
- package/build/client/_app/immutable/chunks/BN1NjBrw.js.br +0 -0
- package/build/client/_app/immutable/chunks/BN1NjBrw.js.gz +0 -0
- package/build/client/_app/immutable/chunks/CDVSripB.js.br +0 -0
- package/build/client/_app/immutable/chunks/CDVSripB.js.gz +0 -0
- package/build/client/_app/immutable/chunks/CF4lQ45j.js.br +0 -0
- package/build/client/_app/immutable/chunks/CF4lQ45j.js.gz +0 -0
- package/build/client/_app/immutable/entry/app.DwWiuoEC.js.br +0 -0
- package/build/client/_app/immutable/entry/app.DwWiuoEC.js.gz +0 -0
- package/build/client/_app/immutable/entry/start.DG8BMhrh.js +0 -1
- package/build/client/_app/immutable/entry/start.DG8BMhrh.js.br +0 -0
- package/build/client/_app/immutable/entry/start.DG8BMhrh.js.gz +0 -0
- package/build/client/_app/immutable/nodes/0.ejabgzDQ.js.br +0 -0
- package/build/client/_app/immutable/nodes/0.ejabgzDQ.js.gz +0 -0
- package/build/client/_app/immutable/nodes/1.BFK7Ubrr.js.br +0 -0
- package/build/client/_app/immutable/nodes/1.BFK7Ubrr.js.gz +0 -0
- package/build/client/_app/immutable/nodes/2.DV3saFiY.js.br +0 -0
- package/build/client/_app/immutable/nodes/2.DV3saFiY.js.gz +0 -0
- package/build/client/_app/immutable/nodes/4.D6NIf10D.js.br +0 -0
- package/build/client/_app/immutable/nodes/4.D6NIf10D.js.gz +0 -0
- package/build/client/_app/immutable/nodes/5.g3R-QfIW.js.br +0 -0
- package/build/client/_app/immutable/nodes/5.g3R-QfIW.js.gz +0 -0
- package/build/client/_app/immutable/nodes/6.DSpd_nYK.js.br +0 -0
- package/build/client/_app/immutable/nodes/6.DSpd_nYK.js.gz +0 -0
- package/build/client/_app/immutable/nodes/7.F9WBFTz2.js.br +0 -0
- 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
|
-
|
|
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('
|
|
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
|
-
|
|
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
|
-
|
|
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=(
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
// ──
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
//
|
|
274
|
-
config
|
|
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.
|
|
314
|
-
|
|
315
|
-
console.log(
|
|
316
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
730
|
-
console.log(dim('
|
|
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
|
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
import{l as o,a as r}from"../chunks/BN1NjBrw.js";export{o as load_css,r as start};
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|