@fiodos/cli 0.1.0 → 0.1.2
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 +17 -0
- package/package.json +1 -1
- package/src/index.js +81 -4
- package/src/postWireTest.js +3 -3
- package/src/renderProbe.js +68 -22
- package/src/renderProbeVue.js +12 -8
- package/src/verifyWire.js +7 -7
- package/src/wireHandlers.js +179 -180
- package/src/writeEnv.js +230 -0
package/src/writeEnv.js
ADDED
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* writeEnv — optional, consent-based convenience that ties the app's RUNTIME
|
|
3
|
+
* key/URL to the SAME key/URL the CLI just published with.
|
|
4
|
+
*
|
|
5
|
+
* The #1 silent-failure cause is a mismatch: you publish with key A against
|
|
6
|
+
* production, but your app runs with key B against localhost, so the orb fetches
|
|
7
|
+
* its manifest from the wrong place, finds nothing, and renders invisibly. This
|
|
8
|
+
* removes the second manual copy of the key by offering to write it into the
|
|
9
|
+
* correct env file for the detected framework.
|
|
10
|
+
*
|
|
11
|
+
* SAFEGUARDS (mirrors the handler-wiring philosophy — never act blindly):
|
|
12
|
+
* - Never overwrites silently: if the var exists and DIFFERS, it warns and asks.
|
|
13
|
+
* - Always asks for consent (unless --yes), showing the exact file/var/value.
|
|
14
|
+
* - Warns to keep the key out of git (suggests .gitignore) for committed files.
|
|
15
|
+
* - On unknown framework / no package.json / Angular (no .env convention), it
|
|
16
|
+
* does NOT write — it prints manual instructions instead.
|
|
17
|
+
*/
|
|
18
|
+
'use strict';
|
|
19
|
+
|
|
20
|
+
const fs = require('fs');
|
|
21
|
+
const path = require('path');
|
|
22
|
+
const readline = require('readline');
|
|
23
|
+
|
|
24
|
+
function readJsonSafe(file) {
|
|
25
|
+
try {
|
|
26
|
+
return JSON.parse(fs.readFileSync(file, 'utf8'));
|
|
27
|
+
} catch {
|
|
28
|
+
return null;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Decide which env file + variable names the detected framework uses. Returns
|
|
34
|
+
* null when there is no safe .env convention to write (Angular, or unknown).
|
|
35
|
+
*/
|
|
36
|
+
function resolveEnvPlan(appRoot) {
|
|
37
|
+
const pkg = readJsonSafe(path.join(appRoot, 'package.json'));
|
|
38
|
+
const has = (rel) => fs.existsSync(path.join(appRoot, rel));
|
|
39
|
+
if (!pkg && !has('angular.json')) {
|
|
40
|
+
return { framework: null, file: null, keyVar: null, urlVar: null };
|
|
41
|
+
}
|
|
42
|
+
const deps = pkg ? { ...(pkg.dependencies || {}), ...(pkg.devDependencies || {}) } : {};
|
|
43
|
+
|
|
44
|
+
if (deps.next) {
|
|
45
|
+
return { framework: 'next', file: '.env.local', keyVar: 'NEXT_PUBLIC_FYODOS_API_KEY', urlVar: 'NEXT_PUBLIC_FYODOS_API_URL' };
|
|
46
|
+
}
|
|
47
|
+
if (deps['@angular/core'] || has('angular.json')) {
|
|
48
|
+
// Angular has no .env convention (values live in environment.ts), so we
|
|
49
|
+
// never write a file blindly — the caller prints manual guidance instead.
|
|
50
|
+
return { framework: 'angular', file: null, keyVar: null, urlVar: null };
|
|
51
|
+
}
|
|
52
|
+
if (
|
|
53
|
+
deps['@sveltejs/kit'] ||
|
|
54
|
+
deps.svelte ||
|
|
55
|
+
deps.vue ||
|
|
56
|
+
deps['@vitejs/plugin-vue'] ||
|
|
57
|
+
deps.vite ||
|
|
58
|
+
deps['@vitejs/plugin-react'] ||
|
|
59
|
+
deps['react-scripts']
|
|
60
|
+
) {
|
|
61
|
+
return { framework: 'vite', file: '.env', keyVar: 'VITE_FYODOS_API_KEY', urlVar: 'VITE_FYODOS_API_URL' };
|
|
62
|
+
}
|
|
63
|
+
return { framework: null, file: null, keyVar: null, urlVar: null };
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** Find the value of VAR in the parsed lines, or undefined if not present. */
|
|
67
|
+
function readEnvVar(lines, name) {
|
|
68
|
+
const re = new RegExp(`^\\s*${name}\\s*=(.*)$`);
|
|
69
|
+
for (const line of lines) {
|
|
70
|
+
const m = line.match(re);
|
|
71
|
+
if (m) return stripQuotes(m[1].trim());
|
|
72
|
+
}
|
|
73
|
+
return undefined;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function stripQuotes(v) {
|
|
77
|
+
return v.replace(/^['"]/, '').replace(/['"]$/, '');
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/** Upsert VAR=value: replace the existing line, or append a new one. */
|
|
81
|
+
function upsertEnvVar(lines, name, value) {
|
|
82
|
+
const re = new RegExp(`^\\s*${name}\\s*=`);
|
|
83
|
+
const idx = lines.findIndex((l) => re.test(l));
|
|
84
|
+
if (idx >= 0) {
|
|
85
|
+
lines[idx] = `${name}=${value}`;
|
|
86
|
+
return lines;
|
|
87
|
+
}
|
|
88
|
+
lines.push(`${name}=${value}`);
|
|
89
|
+
return lines;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function maskKey(key) {
|
|
93
|
+
if (!key) return '';
|
|
94
|
+
return key.length <= 14 ? key : `${key.slice(0, 12)}…`;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function askYesNo(question) {
|
|
98
|
+
return new Promise((resolve) => {
|
|
99
|
+
if (!process.stdin.isTTY) {
|
|
100
|
+
resolve(false);
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stderr });
|
|
104
|
+
rl.question(question, (answer) => {
|
|
105
|
+
rl.close();
|
|
106
|
+
resolve(/^(y|yes|s|si|sí)/i.test((answer || '').trim()));
|
|
107
|
+
});
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function isGitIgnored(appRoot, fileName) {
|
|
112
|
+
const gi = path.join(appRoot, '.gitignore');
|
|
113
|
+
if (!fs.existsSync(gi)) return false;
|
|
114
|
+
try {
|
|
115
|
+
const content = fs.readFileSync(gi, 'utf8');
|
|
116
|
+
const patterns = content.split('\n').map((l) => l.trim());
|
|
117
|
+
return patterns.some(
|
|
118
|
+
(p) => p === fileName || p === `/${fileName}` || p === '.env*' || p === '*.local' || p === '.env.local',
|
|
119
|
+
);
|
|
120
|
+
} catch {
|
|
121
|
+
return false;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Offer to write key/URL to the app's env file. `log` is the branded logUser.
|
|
127
|
+
* Returns a short status string for the final report.
|
|
128
|
+
*/
|
|
129
|
+
async function offerWriteEnv({ appRoot, apiKey, apiUrl, defaultApiUrl, assumeYes, log }) {
|
|
130
|
+
if (!apiKey) return 'skipped (no API key)';
|
|
131
|
+
|
|
132
|
+
const plan = resolveEnvPlan(appRoot);
|
|
133
|
+
if (!plan.file) {
|
|
134
|
+
if (plan.framework === 'angular') {
|
|
135
|
+
log(
|
|
136
|
+
'Angular does not use .env: put these values in src/environments/environment.ts → ' +
|
|
137
|
+
`fyodosApiKey: '${maskKey(apiKey)}', fyodosApiUrl: '${apiUrl}'`,
|
|
138
|
+
);
|
|
139
|
+
return 'manual (angular)';
|
|
140
|
+
}
|
|
141
|
+
log(
|
|
142
|
+
'Could not identify the framework with confidence; add by hand to your .env: ' +
|
|
143
|
+
`(NEXT_PUBLIC_/VITE_)FYODOS_API_KEY=${maskKey(apiKey)} and the API URL ${apiUrl}`,
|
|
144
|
+
);
|
|
145
|
+
return 'manual (unknown framework)';
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
const filePath = path.join(appRoot, plan.file);
|
|
149
|
+
const exists = fs.existsSync(filePath);
|
|
150
|
+
const raw = exists ? fs.readFileSync(filePath, 'utf8') : '';
|
|
151
|
+
const lines = raw.length ? raw.replace(/\n$/, '').split('\n') : [];
|
|
152
|
+
|
|
153
|
+
const currentKey = readEnvVar(lines, plan.keyVar);
|
|
154
|
+
const currentUrl = readEnvVar(lines, plan.urlVar);
|
|
155
|
+
|
|
156
|
+
// Only write a URL var when it differs from the public default (the SDK
|
|
157
|
+
// already defaults to production). But if a STALE url var exists and differs
|
|
158
|
+
// from what we published against, we must offer to fix it — that is exactly
|
|
159
|
+
// the localhost-vs-production mismatch that hides the orb.
|
|
160
|
+
const wantUrl = apiUrl && apiUrl !== defaultApiUrl ? apiUrl : null;
|
|
161
|
+
const urlNeedsFix = currentUrl !== undefined && currentUrl !== apiUrl;
|
|
162
|
+
|
|
163
|
+
const keyMatches = currentKey === apiKey;
|
|
164
|
+
const urlOk = !urlNeedsFix && (wantUrl ? currentUrl === wantUrl : true);
|
|
165
|
+
if (keyMatches && urlOk) {
|
|
166
|
+
log(`✓ Your ${plan.file} already uses the same key/URL as the publication. Nothing to change.`);
|
|
167
|
+
return 'already aligned';
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// Build the change description.
|
|
171
|
+
const changes = [];
|
|
172
|
+
if (!keyMatches) {
|
|
173
|
+
changes.push(
|
|
174
|
+
currentKey === undefined
|
|
175
|
+
? `add ${plan.keyVar}=${maskKey(apiKey)}`
|
|
176
|
+
: `update ${plan.keyVar} (was ${maskKey(currentKey)}) → ${maskKey(apiKey)}`,
|
|
177
|
+
);
|
|
178
|
+
}
|
|
179
|
+
if (wantUrl && currentUrl !== wantUrl) {
|
|
180
|
+
changes.push(
|
|
181
|
+
currentUrl === undefined
|
|
182
|
+
? `add ${plan.urlVar}=${wantUrl}`
|
|
183
|
+
: `update ${plan.urlVar} (was ${currentUrl}) → ${wantUrl}`,
|
|
184
|
+
);
|
|
185
|
+
} else if (urlNeedsFix && !wantUrl) {
|
|
186
|
+
// The app points somewhere (e.g. localhost) but we published against the
|
|
187
|
+
// public default; the var is misleading. Offer to align it explicitly.
|
|
188
|
+
changes.push(`update ${plan.urlVar} (was ${currentUrl}) → ${apiUrl}`);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
if (!changes.length) {
|
|
192
|
+
log(`✓ Your ${plan.file} is already aligned.`);
|
|
193
|
+
return 'already aligned';
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// Loud warning when we are about to CHANGE an existing, different value.
|
|
197
|
+
const isOverwrite = (currentKey !== undefined && !keyMatches) || urlNeedsFix;
|
|
198
|
+
log(
|
|
199
|
+
`${isOverwrite ? '⚠︎ ' : ''}For the orb to find its manifest, your app should use the SAME ` +
|
|
200
|
+
`key/URL you just published with. Proposed in ${plan.file}: ${changes.join('; ')}.`,
|
|
201
|
+
);
|
|
202
|
+
|
|
203
|
+
let proceed = assumeYes;
|
|
204
|
+
if (!proceed) {
|
|
205
|
+
proceed = await askYesNo(
|
|
206
|
+
`◉ Fiodos · Write/update ${plan.file} with the published key/URL? [yes/no] `,
|
|
207
|
+
);
|
|
208
|
+
}
|
|
209
|
+
if (!proceed) {
|
|
210
|
+
log(`Left your ${plan.file} untouched. Do it by hand: ${changes.join('; ')}.`);
|
|
211
|
+
return 'declined by user';
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
let out = lines.slice();
|
|
215
|
+
if (!keyMatches) out = upsertEnvVar(out, plan.keyVar, apiKey);
|
|
216
|
+
if (wantUrl) {
|
|
217
|
+
out = upsertEnvVar(out, plan.urlVar, wantUrl);
|
|
218
|
+
} else if (urlNeedsFix) {
|
|
219
|
+
out = upsertEnvVar(out, plan.urlVar, apiUrl);
|
|
220
|
+
}
|
|
221
|
+
fs.writeFileSync(filePath, `${out.join('\n')}\n`);
|
|
222
|
+
log(`✓ ${plan.file} updated (${changes.join('; ')}).`);
|
|
223
|
+
|
|
224
|
+
if (!isGitIgnored(appRoot, plan.file)) {
|
|
225
|
+
log(`Reminder: add ${plan.file} to .gitignore so you don't commit your API key.`);
|
|
226
|
+
}
|
|
227
|
+
return 'written';
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
module.exports = { offerWriteEnv, resolveEnvPlan };
|