securenow 5.2.2 → 5.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/cli/apps.js ADDED
@@ -0,0 +1,167 @@
1
+ 'use strict';
2
+
3
+ const { api, requireAuth } = require('./client');
4
+ const config = require('./config');
5
+ const ui = require('./ui');
6
+
7
+ async function list(args, flags) {
8
+ requireAuth();
9
+ const s = ui.spinner('Fetching applications');
10
+
11
+ try {
12
+ const apps = await api.get('/applications');
13
+ s.stop(`Found ${apps.length} application${apps.length !== 1 ? 's' : ''}`);
14
+ console.log('');
15
+
16
+ if (flags.json) {
17
+ ui.json(apps);
18
+ return;
19
+ }
20
+
21
+ const defaultApp = config.getDefaultApp();
22
+ const rows = apps.map(app => [
23
+ app.name + (app.key === defaultApp ? ui.c.cyan(' (default)') : ''),
24
+ ui.c.dim(app.key),
25
+ app.hosts?.length ? app.hosts.join(', ') : ui.c.dim('—'),
26
+ ui.timeAgo(app.createdAt),
27
+ ]);
28
+
29
+ ui.table(['Name', 'Key', 'Hosts', 'Created'], rows);
30
+ console.log('');
31
+
32
+ if (!defaultApp && apps.length > 0) {
33
+ ui.info(`Tip: Set a default app with ${ui.c.bold('securenow config set defaultApp <key>')}`);
34
+ console.log('');
35
+ }
36
+ } catch (err) {
37
+ s.fail('Failed to fetch applications');
38
+ throw err;
39
+ }
40
+ }
41
+
42
+ async function create(args, flags) {
43
+ requireAuth();
44
+
45
+ let name = args[0];
46
+ if (!name) {
47
+ name = await ui.prompt('Application name');
48
+ if (!name) {
49
+ ui.error('Application name is required');
50
+ process.exit(1);
51
+ }
52
+ }
53
+
54
+ const body = { name };
55
+ if (flags.hosts) {
56
+ body.hosts = flags.hosts.split(',').map(h => h.trim());
57
+ }
58
+ if (flags.instance) {
59
+ body.instanceId = flags.instance;
60
+ }
61
+
62
+ const s = ui.spinner(`Creating application "${name}"`);
63
+ try {
64
+ const result = await api.post('/applications', body);
65
+ const app = result.application || result;
66
+ s.stop(`Application created`);
67
+
68
+ console.log('');
69
+ ui.keyValue([
70
+ ['Name', app.name],
71
+ ['Key', ui.c.green(ui.c.bold(app.key))],
72
+ ['ID', ui.c.dim(app._id)],
73
+ ['Hosts', app.hosts?.length ? app.hosts.join(', ') : ui.c.dim('none')],
74
+ ]);
75
+
76
+ console.log('');
77
+ console.log(` ${ui.c.bold('Add to your .env.local:')}`);
78
+ console.log('');
79
+ console.log(` SECURENOW_APPID=${app.key}`);
80
+ console.log('');
81
+ ui.info(`Set as default: ${ui.c.bold(`securenow config set defaultApp ${app.key}`)}`);
82
+ console.log('');
83
+
84
+ if (flags.json) {
85
+ ui.json(app);
86
+ }
87
+ } catch (err) {
88
+ s.fail('Failed to create application');
89
+ throw err;
90
+ }
91
+ }
92
+
93
+ async function info(args, flags) {
94
+ requireAuth();
95
+
96
+ const id = args[0];
97
+ if (!id) {
98
+ ui.error('Application ID is required. Usage: securenow apps info <id>');
99
+ process.exit(1);
100
+ }
101
+
102
+ const s = ui.spinner('Fetching application details');
103
+ try {
104
+ const app = await api.get(`/applications/${id}`);
105
+ s.stop('Application details loaded');
106
+
107
+ if (flags.json) {
108
+ ui.json(app);
109
+ return;
110
+ }
111
+
112
+ console.log('');
113
+ ui.heading(app.name);
114
+ console.log('');
115
+ ui.keyValue([
116
+ ['Key', ui.c.bold(app.key)],
117
+ ['ID', ui.c.dim(app._id)],
118
+ ['Hosts', app.hosts?.length ? app.hosts.join(', ') : ui.c.dim('none')],
119
+ ['Instance', app.instanceId || ui.c.dim('default')],
120
+ ['Created', app.createdAt ? new Date(app.createdAt).toLocaleString() : '—'],
121
+ ['Updated', app.updatedAt ? new Date(app.updatedAt).toLocaleString() : '—'],
122
+ ]);
123
+ console.log('');
124
+ } catch (err) {
125
+ s.fail('Failed to fetch application');
126
+ throw err;
127
+ }
128
+ }
129
+
130
+ async function remove(args, flags) {
131
+ requireAuth();
132
+
133
+ const id = args[0];
134
+ if (!id) {
135
+ ui.error('Application ID is required. Usage: securenow apps delete <id>');
136
+ process.exit(1);
137
+ }
138
+
139
+ if (!flags.force && !flags.yes) {
140
+ const ok = await ui.confirm('Are you sure you want to delete this application?');
141
+ if (!ok) {
142
+ ui.info('Cancelled');
143
+ return;
144
+ }
145
+ }
146
+
147
+ const s = ui.spinner('Deleting application');
148
+ try {
149
+ await api.delete(`/applications/${id}`);
150
+ s.stop('Application deleted');
151
+ } catch (err) {
152
+ s.fail('Failed to delete application');
153
+ throw err;
154
+ }
155
+ }
156
+
157
+ async function setDefault(args) {
158
+ const key = args[0];
159
+ if (!key) {
160
+ ui.error('App key is required. Usage: securenow apps default <key>');
161
+ process.exit(1);
162
+ }
163
+ config.setConfigValue('defaultApp', key);
164
+ ui.success(`Default application set to ${ui.c.bold(key)}`);
165
+ }
166
+
167
+ module.exports = { list, create, info, remove, setDefault };
package/cli/auth.js ADDED
@@ -0,0 +1,208 @@
1
+ 'use strict';
2
+
3
+ const http = require('http');
4
+ const { execSync } = require('child_process');
5
+ const config = require('./config');
6
+ const { api, CLIError } = require('./client');
7
+ const ui = require('./ui');
8
+
9
+ function openBrowser(url) {
10
+ try {
11
+ const platform = process.platform;
12
+ if (platform === 'darwin') execSync(`open "${url}"`);
13
+ else if (platform === 'win32') execSync(`start "" "${url}"`);
14
+ else execSync(`xdg-open "${url}"`);
15
+ return true;
16
+ } catch {
17
+ return false;
18
+ }
19
+ }
20
+
21
+ function decodeJwtPayload(token) {
22
+ try {
23
+ const parts = token.split('.');
24
+ if (parts.length !== 3) return null;
25
+ const payload = Buffer.from(parts[1], 'base64url').toString('utf8');
26
+ return JSON.parse(payload);
27
+ } catch {
28
+ try {
29
+ const parts = token.split('.');
30
+ const base64 = parts[1].replace(/-/g, '+').replace(/_/g, '/');
31
+ const padded = base64 + '='.repeat((4 - base64.length % 4) % 4);
32
+ const payload = Buffer.from(padded, 'base64').toString('utf8');
33
+ return JSON.parse(payload);
34
+ } catch {
35
+ return null;
36
+ }
37
+ }
38
+ }
39
+
40
+ async function loginWithBrowser() {
41
+ const appUrl = config.getAppUrl();
42
+
43
+ return new Promise((resolve, reject) => {
44
+ const server = http.createServer((req, res) => {
45
+ const url = new URL(req.url, `http://localhost`);
46
+
47
+ if (url.pathname === '/callback') {
48
+ const token = url.searchParams.get('token');
49
+ const error = url.searchParams.get('error');
50
+
51
+ res.writeHead(200, { 'Content-Type': 'text/html' });
52
+
53
+ if (error) {
54
+ res.end('<html><body style="font-family:system-ui;text-align:center;padding:60px"><h2>Authentication Failed</h2><p>You can close this window.</p></body></html>');
55
+ server.close();
56
+ reject(new CLIError(`Authentication failed: ${error}`));
57
+ return;
58
+ }
59
+
60
+ if (token) {
61
+ res.end('<html><body style="font-family:system-ui;text-align:center;padding:60px"><h2 style="color:#22c55e">✓ Logged in to SecureNow</h2><p>You can close this window and return to the terminal.</p></body></html>');
62
+ server.close();
63
+ resolve(token);
64
+ return;
65
+ }
66
+
67
+ res.end('<html><body style="font-family:system-ui;text-align:center;padding:60px"><h2>Something went wrong</h2><p>No token received. Please try again.</p></body></html>');
68
+ server.close();
69
+ reject(new CLIError('No token received in callback'));
70
+ return;
71
+ }
72
+
73
+ res.writeHead(404);
74
+ res.end();
75
+ });
76
+
77
+ server.listen(0, '127.0.0.1', () => {
78
+ const port = server.address().port;
79
+ const authUrl = `${appUrl}/cli/auth?callback=http://localhost:${port}/callback`;
80
+
81
+ console.log('');
82
+ ui.info('Opening browser for authentication...');
83
+ console.log('');
84
+
85
+ const opened = openBrowser(authUrl);
86
+ if (!opened) {
87
+ console.log(' Open this URL in your browser to log in:\n');
88
+ console.log(` ${ui.c.underline(ui.c.cyan(authUrl))}\n`);
89
+ } else {
90
+ console.log(` If the browser didn't open, visit:`);
91
+ console.log(` ${ui.c.underline(ui.c.cyan(authUrl))}\n`);
92
+ }
93
+
94
+ console.log(ui.c.dim(' Waiting for authentication...'));
95
+
96
+ const timeout = setTimeout(() => {
97
+ server.close();
98
+ reject(new CLIError('Login timed out after 5 minutes. Try `securenow login --token <TOKEN>` instead.'));
99
+ }, 5 * 60 * 1000);
100
+
101
+ server.on('close', () => clearTimeout(timeout));
102
+ });
103
+
104
+ server.on('error', (err) => {
105
+ reject(new CLIError(`Failed to start local server: ${err.message}`));
106
+ });
107
+ });
108
+ }
109
+
110
+ async function loginWithToken(token) {
111
+ const s = ui.spinner('Validating token');
112
+ try {
113
+ await api.get('/applications', { token });
114
+ s.stop('Token is valid');
115
+ return token;
116
+ } catch (err) {
117
+ s.fail('Token validation failed');
118
+ throw new CLIError(`Invalid token: ${err.message}`);
119
+ }
120
+ }
121
+
122
+ async function login(args, flags) {
123
+ if (flags.token) {
124
+ const token = flags.token;
125
+ await loginWithToken(token);
126
+ const payload = decodeJwtPayload(token);
127
+ const email = payload?.email || 'unknown';
128
+ const exp = payload?.exp ? payload.exp * 1000 : null;
129
+
130
+ config.setAuth(token, email, exp);
131
+ console.log('');
132
+ ui.success(`Logged in as ${ui.c.bold(email)}`);
133
+ if (exp) {
134
+ const days = Math.ceil((exp - Date.now()) / (1000 * 60 * 60 * 24));
135
+ ui.info(`Session expires in ${days} days`);
136
+ }
137
+ return;
138
+ }
139
+
140
+ try {
141
+ const token = await loginWithBrowser();
142
+ const payload = decodeJwtPayload(token);
143
+ const email = payload?.email || 'unknown';
144
+ const exp = payload?.exp ? payload.exp * 1000 : null;
145
+
146
+ config.setAuth(token, email, exp);
147
+ console.log('');
148
+ ui.success(`Logged in as ${ui.c.bold(email)}`);
149
+ if (exp) {
150
+ const days = Math.ceil((exp - Date.now()) / (1000 * 60 * 60 * 24));
151
+ ui.info(`Session expires in ${days} days`);
152
+ }
153
+ } catch (err) {
154
+ if (err.message.includes('timed out')) {
155
+ console.log('');
156
+ ui.warn('Browser login timed out. You can also login with a token:');
157
+ console.log('');
158
+ console.log(` 1. Go to ${ui.c.cyan(config.getAppUrl() + '/dashboard/settings')}`);
159
+ console.log(` 2. Copy your CLI token`);
160
+ console.log(` 3. Run: ${ui.c.bold('securenow login --token <YOUR_TOKEN>')}`);
161
+ console.log('');
162
+ } else {
163
+ throw err;
164
+ }
165
+ }
166
+ }
167
+
168
+ async function logout() {
169
+ const creds = config.loadCredentials();
170
+ config.clearCredentials();
171
+ if (creds.email) {
172
+ ui.success(`Logged out from ${ui.c.bold(creds.email)}`);
173
+ } else {
174
+ ui.success('Logged out');
175
+ }
176
+ }
177
+
178
+ async function whoami() {
179
+ const creds = config.loadCredentials();
180
+ const token = config.getToken();
181
+
182
+ if (!token) {
183
+ ui.error('Not logged in. Run `securenow login` to authenticate.');
184
+ process.exit(1);
185
+ }
186
+
187
+ const payload = decodeJwtPayload(token);
188
+
189
+ ui.heading('Current Session');
190
+ console.log('');
191
+ const pairs = [
192
+ ['Email', creds.email || payload?.email || 'unknown'],
193
+ ['User ID', payload?.sub || 'unknown'],
194
+ ['API', config.getApiUrl()],
195
+ ];
196
+ if (creds.expiresAt) {
197
+ const days = Math.ceil((creds.expiresAt - Date.now()) / (1000 * 60 * 60 * 24));
198
+ pairs.push(['Expires', days > 0 ? `in ${days} days` : ui.c.red('expired')]);
199
+ }
200
+ const defaultApp = config.getDefaultApp();
201
+ if (defaultApp) {
202
+ pairs.push(['Default App', defaultApp]);
203
+ }
204
+ ui.keyValue(pairs);
205
+ console.log('');
206
+ }
207
+
208
+ module.exports = { login, logout, whoami };
package/cli/client.js ADDED
@@ -0,0 +1,113 @@
1
+ 'use strict';
2
+
3
+ const https = require('https');
4
+ const http = require('http');
5
+ const { URL } = require('url');
6
+ const config = require('./config');
7
+ const ui = require('./ui');
8
+
9
+ function buildQueryString(params) {
10
+ if (!params) return '';
11
+ const entries = Object.entries(params).filter(([, v]) => v != null && v !== '');
12
+ if (!entries.length) return '';
13
+ return '?' + entries.map(([k, v]) => `${encodeURIComponent(k)}=${encodeURIComponent(v)}`).join('&');
14
+ }
15
+
16
+ function request(method, endpoint, { body, query, token, raw } = {}) {
17
+ const baseUrl = config.getApiUrl();
18
+ const qs = buildQueryString(query);
19
+ const urlStr = `${baseUrl}/api${endpoint}${qs}`;
20
+ const url = new URL(urlStr);
21
+ const mod = url.protocol === 'https:' ? https : http;
22
+
23
+ const authToken = token || config.getToken();
24
+
25
+ return new Promise((resolve, reject) => {
26
+ const options = {
27
+ hostname: url.hostname,
28
+ port: url.port || (url.protocol === 'https:' ? 443 : 80),
29
+ path: url.pathname + url.search,
30
+ method,
31
+ headers: { 'Content-Type': 'application/json', 'User-Agent': 'securenow-cli' },
32
+ };
33
+
34
+ if (authToken) {
35
+ options.headers['Authorization'] = `Bearer ${authToken}`;
36
+ }
37
+
38
+ const req = mod.request(options, (res) => {
39
+ let data = '';
40
+ res.on('data', (chunk) => (data += chunk));
41
+ res.on('end', () => {
42
+ if (raw) {
43
+ return resolve({ status: res.statusCode, headers: res.headers, body: data });
44
+ }
45
+
46
+ let parsed;
47
+ try {
48
+ parsed = JSON.parse(data);
49
+ } catch {
50
+ parsed = data;
51
+ }
52
+
53
+ if (res.statusCode === 401) {
54
+ reject(new CLIError('Session expired. Run `securenow login` to re-authenticate.', 401));
55
+ return;
56
+ }
57
+ if (res.statusCode === 403) {
58
+ reject(new CLIError('Access denied. You may need to upgrade your plan.', 403));
59
+ return;
60
+ }
61
+ if (res.statusCode >= 400) {
62
+ const msg = parsed?.error || parsed?.message || `Request failed (HTTP ${res.statusCode})`;
63
+ reject(new CLIError(msg, res.statusCode));
64
+ return;
65
+ }
66
+
67
+ resolve(parsed);
68
+ });
69
+ });
70
+
71
+ req.on('error', (err) => {
72
+ if (err.code === 'ECONNREFUSED') {
73
+ reject(new CLIError(`Cannot connect to ${baseUrl}. Is the API server running?`));
74
+ } else if (err.code === 'ENOTFOUND') {
75
+ reject(new CLIError(`Cannot resolve ${url.hostname}. Check your internet connection.`));
76
+ } else {
77
+ reject(new CLIError(`Network error: ${err.message}`));
78
+ }
79
+ });
80
+
81
+ if (body) {
82
+ req.write(JSON.stringify(body));
83
+ }
84
+ req.end();
85
+ });
86
+ }
87
+
88
+ class CLIError extends Error {
89
+ constructor(message, statusCode) {
90
+ super(message);
91
+ this.name = 'CLIError';
92
+ this.statusCode = statusCode;
93
+ }
94
+ }
95
+
96
+ function requireAuth() {
97
+ const token = config.getToken();
98
+ if (!token) {
99
+ ui.error('Not logged in. Run `securenow login` first.');
100
+ process.exit(1);
101
+ }
102
+ return token;
103
+ }
104
+
105
+ const api = {
106
+ get: (endpoint, opts) => request('GET', endpoint, opts),
107
+ post: (endpoint, body, opts) => request('POST', endpoint, { body, ...opts }),
108
+ put: (endpoint, body, opts) => request('PUT', endpoint, { body, ...opts }),
109
+ patch: (endpoint, body, opts) => request('PATCH', endpoint, { body, ...opts }),
110
+ delete: (endpoint, opts) => request('DELETE', endpoint, opts),
111
+ };
112
+
113
+ module.exports = { api, CLIError, requireAuth, buildQueryString };
package/cli/config.js ADDED
@@ -0,0 +1,111 @@
1
+ 'use strict';
2
+
3
+ const fs = require('fs');
4
+ const path = require('path');
5
+ const os = require('os');
6
+
7
+ const CONFIG_DIR = path.join(os.homedir(), '.securenow');
8
+ const CONFIG_FILE = path.join(CONFIG_DIR, 'config.json');
9
+ const CREDENTIALS_FILE = path.join(CONFIG_DIR, 'credentials.json');
10
+
11
+ const DEFAULTS = {
12
+ apiUrl: 'https://api.securenow.ai',
13
+ appUrl: 'https://app.securenow.ai',
14
+ defaultApp: null,
15
+ output: 'table',
16
+ };
17
+
18
+ function ensureDir() {
19
+ if (!fs.existsSync(CONFIG_DIR)) {
20
+ fs.mkdirSync(CONFIG_DIR, { recursive: true, mode: 0o700 });
21
+ }
22
+ }
23
+
24
+ function loadJSON(filepath) {
25
+ try {
26
+ return JSON.parse(fs.readFileSync(filepath, 'utf8'));
27
+ } catch {
28
+ return {};
29
+ }
30
+ }
31
+
32
+ function saveJSON(filepath, data) {
33
+ ensureDir();
34
+ fs.writeFileSync(filepath, JSON.stringify(data, null, 2), { encoding: 'utf8', mode: 0o600 });
35
+ }
36
+
37
+ function loadConfig() {
38
+ return { ...DEFAULTS, ...loadJSON(CONFIG_FILE) };
39
+ }
40
+
41
+ function saveConfig(config) {
42
+ const existing = loadJSON(CONFIG_FILE);
43
+ saveJSON(CONFIG_FILE, { ...existing, ...config });
44
+ }
45
+
46
+ function getConfigValue(key) {
47
+ const config = loadConfig();
48
+ return config[key];
49
+ }
50
+
51
+ function setConfigValue(key, value) {
52
+ saveConfig({ [key]: value });
53
+ }
54
+
55
+ function loadCredentials() {
56
+ return loadJSON(CREDENTIALS_FILE);
57
+ }
58
+
59
+ function saveCredentials(creds) {
60
+ saveJSON(CREDENTIALS_FILE, creds);
61
+ }
62
+
63
+ function clearCredentials() {
64
+ try {
65
+ fs.unlinkSync(CREDENTIALS_FILE);
66
+ } catch {}
67
+ }
68
+
69
+ function getToken() {
70
+ const creds = loadCredentials();
71
+ if (!creds.token) return null;
72
+
73
+ if (creds.expiresAt && Date.now() > creds.expiresAt) {
74
+ return null;
75
+ }
76
+ return creds.token;
77
+ }
78
+
79
+ function setAuth(token, email, expiresAt) {
80
+ saveCredentials({ token, email, expiresAt });
81
+ }
82
+
83
+ function getApiUrl() {
84
+ return process.env.SECURENOW_API_URL || loadConfig().apiUrl;
85
+ }
86
+
87
+ function getAppUrl() {
88
+ return process.env.SECURENOW_APP_URL || loadConfig().appUrl;
89
+ }
90
+
91
+ function getDefaultApp() {
92
+ return process.env.SECURENOW_APP || loadConfig().defaultApp;
93
+ }
94
+
95
+ module.exports = {
96
+ CONFIG_DIR,
97
+ CONFIG_FILE,
98
+ CREDENTIALS_FILE,
99
+ loadConfig,
100
+ saveConfig,
101
+ getConfigValue,
102
+ setConfigValue,
103
+ loadCredentials,
104
+ saveCredentials,
105
+ clearCredentials,
106
+ getToken,
107
+ setAuth,
108
+ getApiUrl,
109
+ getAppUrl,
110
+ getDefaultApp,
111
+ };
package/cli/init.js ADDED
@@ -0,0 +1,100 @@
1
+ 'use strict';
2
+
3
+ const fs = require('fs');
4
+ const path = require('path');
5
+ const ui = require('./ui');
6
+
7
+ const templates = {
8
+ typescript: `import { registerSecureNow } from 'securenow/nextjs';
9
+
10
+ export function register() {
11
+ registerSecureNow();
12
+ }
13
+ `,
14
+ javascript: `const { registerSecureNow } = require('securenow/nextjs');
15
+
16
+ export function register() {
17
+ registerSecureNow();
18
+ }
19
+ `,
20
+ env: `# SecureNow Configuration
21
+ SECURENOW_APPID=my-nextjs-app
22
+ SECURENOW_INSTANCE=http://your-otlp-backend:4318
23
+ # OTEL_EXPORTER_OTLP_HEADERS="x-api-key=your-api-key-here"
24
+ # OTEL_LOG_LEVEL=info
25
+ `,
26
+ };
27
+
28
+ function isNextJsProject(dir) {
29
+ try {
30
+ const pkgPath = path.join(dir, 'package.json');
31
+ if (!fs.existsSync(pkgPath)) return false;
32
+ const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8'));
33
+ const deps = { ...pkg.dependencies, ...pkg.devDependencies };
34
+ return !!deps.next;
35
+ } catch {
36
+ return false;
37
+ }
38
+ }
39
+
40
+ async function init(args, flags) {
41
+ const cwd = process.cwd();
42
+
43
+ console.log('');
44
+ ui.heading('SecureNow Setup');
45
+ console.log('');
46
+
47
+ const isNext = isNextJsProject(cwd);
48
+ if (!isNext && !flags.force) {
49
+ ui.warn("This doesn't appear to be a Next.js project.");
50
+ ui.info('Use --force to proceed anyway.');
51
+ process.exit(1);
52
+ }
53
+
54
+ const useTS = flags.typescript || flags.ts ||
55
+ (!flags.javascript && !flags.js && fs.existsSync(path.join(cwd, 'tsconfig.json')));
56
+ const useSrc = flags.src ||
57
+ (!flags.root && fs.existsSync(path.join(cwd, 'src')));
58
+
59
+ const fileName = useTS ? 'instrumentation.ts' : 'instrumentation.js';
60
+ const filePath = useSrc ? path.join(cwd, 'src', fileName) : path.join(cwd, fileName);
61
+
62
+ if (fs.existsSync(filePath) && !flags.force) {
63
+ ui.error(`${useSrc ? 'src/' : ''}${fileName} already exists. Use --force to overwrite.`);
64
+ process.exit(1);
65
+ }
66
+
67
+ try {
68
+ const template = useTS ? templates.typescript : templates.javascript;
69
+ if (useSrc) fs.mkdirSync(path.join(cwd, 'src'), { recursive: true });
70
+ fs.writeFileSync(filePath, template, 'utf8');
71
+ ui.success(`Created ${useSrc ? 'src/' : ''}${fileName}`);
72
+ } catch (err) {
73
+ ui.error(`Failed to create instrumentation file: ${err.message}`);
74
+ process.exit(1);
75
+ }
76
+
77
+ const envPath = path.join(cwd, '.env.local');
78
+ if (!fs.existsSync(envPath) || flags.force) {
79
+ try {
80
+ fs.writeFileSync(envPath, templates.env, 'utf8');
81
+ ui.success('Created .env.local template');
82
+ } catch (err) {
83
+ ui.warn(`Could not create .env.local: ${err.message}`);
84
+ }
85
+ } else {
86
+ ui.info('.env.local already exists (skipped)');
87
+ }
88
+
89
+ console.log('');
90
+ console.log(` ${ui.c.bold('Next steps:')}`);
91
+ console.log('');
92
+ console.log(` 1. Edit ${ui.c.cyan('.env.local')} and set your SECURENOW_APPID`);
93
+ console.log(` 2. Run ${ui.c.cyan('npm run dev')} to start your app`);
94
+ console.log(` 3. Check traces in the SecureNow dashboard`);
95
+ console.log('');
96
+ ui.info(`Don't have an app key yet? Run ${ui.c.bold('securenow apps create <name>')}`);
97
+ console.log('');
98
+ }
99
+
100
+ module.exports = { init };