@fiodos/cli 0.1.2 → 0.1.4
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/package.json +1 -1
- package/src/index.js +1 -0
- package/src/wireWeb.js +43 -8
- package/src/writeEnv.js +65 -19
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@fiodos/cli",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.4",
|
|
4
4
|
"description": "Fiodos CLI — analyzes your app's source code and generates the in-app voice-agent manifest, then wires the orb. Powers `npx @fiodos/cli analyze`.",
|
|
5
5
|
"type": "commonjs",
|
|
6
6
|
"bin": {
|
package/src/index.js
CHANGED
package/src/wireWeb.js
CHANGED
|
@@ -206,7 +206,7 @@ function printMountPreview(plan, colors) {
|
|
|
206
206
|
}
|
|
207
207
|
|
|
208
208
|
async function wireWebOrb(appRoot, opts = {}) {
|
|
209
|
-
const { colors = {}, assumeYes = false, noWire = false } = opts;
|
|
209
|
+
const { colors = {}, assumeYes = false, noWire = false, runTest = true } = opts;
|
|
210
210
|
if (noWire) return { status: 'skipped' };
|
|
211
211
|
|
|
212
212
|
const framework = detectFramework(appRoot);
|
|
@@ -232,20 +232,53 @@ async function wireWebOrb(appRoot, opts = {}) {
|
|
|
232
232
|
return { status: 'declined', file: plan.rel };
|
|
233
233
|
}
|
|
234
234
|
|
|
235
|
+
const build = async () => {
|
|
236
|
+
try {
|
|
237
|
+
return await runPostWireTest(appRoot, { timeoutMs: 180000 });
|
|
238
|
+
} catch (e) {
|
|
239
|
+
return { ok: false, stage: 'runner', command: '(runner)', output: String((e && e.message) || e) };
|
|
240
|
+
}
|
|
241
|
+
};
|
|
242
|
+
|
|
235
243
|
const backups = backupFiles(plan.files);
|
|
236
244
|
try {
|
|
237
245
|
applyMount(plan);
|
|
238
246
|
if (!verifyMounted(plan)) {
|
|
239
|
-
|
|
247
|
+
revertFiles(backups);
|
|
248
|
+
printSnippet(target, framework, colors, appRoot, 'post-edit verification failed — FiodosAgent not found in edited files');
|
|
249
|
+
return { status: 'failed', file: plan.rel };
|
|
240
250
|
}
|
|
241
|
-
|
|
242
|
-
|
|
251
|
+
|
|
252
|
+
// The orb is a single, trivial component mount — it must APPEAR. We therefore
|
|
253
|
+
// only revert when WE are demonstrably the cause: the app built fine on its
|
|
254
|
+
// own but breaks once the orb is mounted (e.g. the SDK package isn't installed
|
|
255
|
+
// yet). A pre-existing build failure, a timeout, or a missing toolchain must
|
|
256
|
+
// NOT make the orb vanish — that was the silent "orb never shows up" bug.
|
|
257
|
+
// Fast path: one build. Only when it fails do we spend a second build (on the
|
|
258
|
+
// reverted original) to tell "we broke it" from "it was already broken".
|
|
259
|
+
if (runTest) {
|
|
260
|
+
const test = await build();
|
|
261
|
+
if (test.ok || test.stage === 'skipped-no-deps') {
|
|
262
|
+
writeConsentDoc(appRoot, plan);
|
|
263
|
+
return { status: 'added', file: plan.rel, test };
|
|
264
|
+
}
|
|
265
|
+
// Build failed with the orb mounted — was the app building WITHOUT it?
|
|
243
266
|
revertFiles(backups);
|
|
244
|
-
|
|
245
|
-
|
|
267
|
+
const baseline = await build();
|
|
268
|
+
if (baseline.ok) {
|
|
269
|
+
// Built before, fails now → the mount is the regression. Keep it reverted.
|
|
270
|
+
printSnippet(target, framework, colors, appRoot, `mount caused a build regression — reverted (${test.command})`);
|
|
271
|
+
return { status: 'reverted', file: plan.rel, test, reason: test.output };
|
|
272
|
+
}
|
|
273
|
+
// It was already not building (or not verifiable) before us → keep the orb.
|
|
274
|
+
applyMount(plan);
|
|
275
|
+
verifyMounted(plan);
|
|
276
|
+
writeConsentDoc(appRoot, plan);
|
|
277
|
+
return { status: 'added', file: plan.rel, test, preexistingFailure: true };
|
|
246
278
|
}
|
|
279
|
+
|
|
247
280
|
writeConsentDoc(appRoot, plan);
|
|
248
|
-
return { status: 'added', file: plan.rel
|
|
281
|
+
return { status: 'added', file: plan.rel };
|
|
249
282
|
} catch (err) {
|
|
250
283
|
revertFiles(backups);
|
|
251
284
|
printSnippet(target, framework, colors, appRoot, err.message || String(err));
|
|
@@ -259,7 +292,9 @@ function reportWireResult(result, colors = {}) {
|
|
|
259
292
|
switch (result.status) {
|
|
260
293
|
case 'added':
|
|
261
294
|
console.error(`${tag} · ✓ orb added to ${result.file}. Start your web app (e.g. \`npm run dev\`) to see it.`);
|
|
262
|
-
if (result.
|
|
295
|
+
if (result.preexistingFailure) {
|
|
296
|
+
console.error(`${tag} · ${dim || ''}Note: your app was ALREADY not building before the orb was added (${result.test && result.test.stage}); not caused by Fiodos — the orb is kept so it appears once your build is fixed.${reset || ''}`);
|
|
297
|
+
} else if (result.test && result.test.stage === 'skipped-no-deps') {
|
|
263
298
|
console.error(`${tag} · ${dim || ''}build not verified (no node_modules) — run npm install && npm run build locally.${reset || ''}`);
|
|
264
299
|
} else if (result.test && result.test.ok) {
|
|
265
300
|
console.error(`${tag} · ✓ ${result.test.command} passed after mount.`);
|
package/src/writeEnv.js
CHANGED
|
@@ -30,24 +30,36 @@ function readJsonSafe(file) {
|
|
|
30
30
|
}
|
|
31
31
|
|
|
32
32
|
/**
|
|
33
|
-
* Decide which env
|
|
34
|
-
* null when there is no safe .env convention to write (Angular,
|
|
33
|
+
* Decide which env files + variable names the detected framework uses. Returns
|
|
34
|
+
* `file: null` when there is no safe .env convention to write (Angular,
|
|
35
|
+
* unknown). `candidates` is the list of env files the framework loads, ordered
|
|
36
|
+
* HIGHEST precedence first — so we can fix the value WHERE THE APP ACTUALLY
|
|
37
|
+
* READS IT instead of writing a second file that may or may not win. `file` is
|
|
38
|
+
* the default file to create when no candidate already defines the vars.
|
|
35
39
|
*/
|
|
36
40
|
function resolveEnvPlan(appRoot) {
|
|
37
41
|
const pkg = readJsonSafe(path.join(appRoot, 'package.json'));
|
|
38
42
|
const has = (rel) => fs.existsSync(path.join(appRoot, rel));
|
|
39
43
|
if (!pkg && !has('angular.json')) {
|
|
40
|
-
return { framework: null, file: null, keyVar: null, urlVar: null };
|
|
44
|
+
return { framework: null, file: null, candidates: [], keyVar: null, urlVar: null };
|
|
41
45
|
}
|
|
42
46
|
const deps = pkg ? { ...(pkg.dependencies || {}), ...(pkg.devDependencies || {}) } : {};
|
|
43
47
|
|
|
44
48
|
if (deps.next) {
|
|
45
|
-
|
|
49
|
+
// Next dev precedence (first wins): .env.development.local > .env.local >
|
|
50
|
+
// .env.development > .env.
|
|
51
|
+
return {
|
|
52
|
+
framework: 'next',
|
|
53
|
+
file: '.env.local',
|
|
54
|
+
candidates: ['.env.development.local', '.env.local', '.env.development', '.env'],
|
|
55
|
+
keyVar: 'NEXT_PUBLIC_FYODOS_API_KEY',
|
|
56
|
+
urlVar: 'NEXT_PUBLIC_FYODOS_API_URL',
|
|
57
|
+
};
|
|
46
58
|
}
|
|
47
59
|
if (deps['@angular/core'] || has('angular.json')) {
|
|
48
60
|
// Angular has no .env convention (values live in environment.ts), so we
|
|
49
61
|
// never write a file blindly — the caller prints manual guidance instead.
|
|
50
|
-
return { framework: 'angular', file: null, keyVar: null, urlVar: null };
|
|
62
|
+
return { framework: 'angular', file: null, candidates: [], keyVar: null, urlVar: null };
|
|
51
63
|
}
|
|
52
64
|
if (
|
|
53
65
|
deps['@sveltejs/kit'] ||
|
|
@@ -58,9 +70,33 @@ function resolveEnvPlan(appRoot) {
|
|
|
58
70
|
deps['@vitejs/plugin-react'] ||
|
|
59
71
|
deps['react-scripts']
|
|
60
72
|
) {
|
|
61
|
-
|
|
73
|
+
// Vite dev precedence (first wins): .env.development.local >
|
|
74
|
+
// .env.development > .env.local > .env.
|
|
75
|
+
return {
|
|
76
|
+
framework: 'vite',
|
|
77
|
+
file: '.env',
|
|
78
|
+
candidates: ['.env.development.local', '.env.development', '.env.local', '.env'],
|
|
79
|
+
keyVar: 'VITE_FYODOS_API_KEY',
|
|
80
|
+
urlVar: 'VITE_FYODOS_API_URL',
|
|
81
|
+
};
|
|
62
82
|
}
|
|
63
|
-
return { framework: null, file: null, keyVar: null, urlVar: null };
|
|
83
|
+
return { framework: null, file: null, candidates: [], keyVar: null, urlVar: null };
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Scan the framework's candidate env files (highest precedence first) for VAR.
|
|
88
|
+
* Returns { file, value } for the first (highest-precedence) file that defines
|
|
89
|
+
* it — i.e. the value the running app actually reads — or null if none do.
|
|
90
|
+
*/
|
|
91
|
+
function findEffectiveVar(appRoot, candidates, name) {
|
|
92
|
+
for (const rel of candidates) {
|
|
93
|
+
const filePath = path.join(appRoot, rel);
|
|
94
|
+
if (!fs.existsSync(filePath)) continue;
|
|
95
|
+
const lines = fs.readFileSync(filePath, 'utf8').replace(/\n$/, '').split('\n');
|
|
96
|
+
const value = readEnvVar(lines, name);
|
|
97
|
+
if (value !== undefined) return { file: rel, value };
|
|
98
|
+
}
|
|
99
|
+
return null;
|
|
64
100
|
}
|
|
65
101
|
|
|
66
102
|
/** Find the value of VAR in the parsed lines, or undefined if not present. */
|
|
@@ -145,14 +181,24 @@ async function offerWriteEnv({ appRoot, apiKey, apiUrl, defaultApiUrl, assumeYes
|
|
|
145
181
|
return 'manual (unknown framework)';
|
|
146
182
|
}
|
|
147
183
|
|
|
148
|
-
|
|
184
|
+
// Look across ALL the files the framework loads (not just the default one):
|
|
185
|
+
// the app may already keep its Fiodos vars in .env while we would otherwise
|
|
186
|
+
// create a separate .env.local — leaving the stale value the orb actually
|
|
187
|
+
// reads untouched. Fix the value WHERE THE APP READS IT.
|
|
188
|
+
const candidates = plan.candidates && plan.candidates.length ? plan.candidates : [plan.file];
|
|
189
|
+
const effectiveKey = findEffectiveVar(appRoot, candidates, plan.keyVar);
|
|
190
|
+
const effectiveUrl = findEffectiveVar(appRoot, candidates, plan.urlVar);
|
|
191
|
+
const currentKey = effectiveKey ? effectiveKey.value : undefined;
|
|
192
|
+
const currentUrl = effectiveUrl ? effectiveUrl.value : undefined;
|
|
193
|
+
|
|
194
|
+
// Write in the highest-precedence file that already defines either var (so the
|
|
195
|
+
// fix is the value the app loads). If none do, create the framework default.
|
|
196
|
+
const targetRel = (effectiveKey && effectiveKey.file) || (effectiveUrl && effectiveUrl.file) || plan.file;
|
|
197
|
+
const filePath = path.join(appRoot, targetRel);
|
|
149
198
|
const exists = fs.existsSync(filePath);
|
|
150
199
|
const raw = exists ? fs.readFileSync(filePath, 'utf8') : '';
|
|
151
200
|
const lines = raw.length ? raw.replace(/\n$/, '').split('\n') : [];
|
|
152
201
|
|
|
153
|
-
const currentKey = readEnvVar(lines, plan.keyVar);
|
|
154
|
-
const currentUrl = readEnvVar(lines, plan.urlVar);
|
|
155
|
-
|
|
156
202
|
// Only write a URL var when it differs from the public default (the SDK
|
|
157
203
|
// already defaults to production). But if a STALE url var exists and differs
|
|
158
204
|
// from what we published against, we must offer to fix it — that is exactly
|
|
@@ -163,7 +209,7 @@ async function offerWriteEnv({ appRoot, apiKey, apiUrl, defaultApiUrl, assumeYes
|
|
|
163
209
|
const keyMatches = currentKey === apiKey;
|
|
164
210
|
const urlOk = !urlNeedsFix && (wantUrl ? currentUrl === wantUrl : true);
|
|
165
211
|
if (keyMatches && urlOk) {
|
|
166
|
-
log(`✓ Your ${
|
|
212
|
+
log(`✓ Your ${targetRel} already uses the same key/URL as the publication. Nothing to change.`);
|
|
167
213
|
return 'already aligned';
|
|
168
214
|
}
|
|
169
215
|
|
|
@@ -189,7 +235,7 @@ async function offerWriteEnv({ appRoot, apiKey, apiUrl, defaultApiUrl, assumeYes
|
|
|
189
235
|
}
|
|
190
236
|
|
|
191
237
|
if (!changes.length) {
|
|
192
|
-
log(`✓ Your ${
|
|
238
|
+
log(`✓ Your ${targetRel} is already aligned.`);
|
|
193
239
|
return 'already aligned';
|
|
194
240
|
}
|
|
195
241
|
|
|
@@ -197,17 +243,17 @@ async function offerWriteEnv({ appRoot, apiKey, apiUrl, defaultApiUrl, assumeYes
|
|
|
197
243
|
const isOverwrite = (currentKey !== undefined && !keyMatches) || urlNeedsFix;
|
|
198
244
|
log(
|
|
199
245
|
`${isOverwrite ? '⚠︎ ' : ''}For the orb to find its manifest, your app should use the SAME ` +
|
|
200
|
-
`key/URL you just published with. Proposed in ${
|
|
246
|
+
`key/URL you just published with. Proposed in ${targetRel}: ${changes.join('; ')}.`,
|
|
201
247
|
);
|
|
202
248
|
|
|
203
249
|
let proceed = assumeYes;
|
|
204
250
|
if (!proceed) {
|
|
205
251
|
proceed = await askYesNo(
|
|
206
|
-
`◉ Fiodos · Write/update ${
|
|
252
|
+
`◉ Fiodos · Write/update ${targetRel} with the published key/URL? [yes/no] `,
|
|
207
253
|
);
|
|
208
254
|
}
|
|
209
255
|
if (!proceed) {
|
|
210
|
-
log(`Left your ${
|
|
256
|
+
log(`Left your ${targetRel} untouched. Do it by hand: ${changes.join('; ')}.`);
|
|
211
257
|
return 'declined by user';
|
|
212
258
|
}
|
|
213
259
|
|
|
@@ -219,10 +265,10 @@ async function offerWriteEnv({ appRoot, apiKey, apiUrl, defaultApiUrl, assumeYes
|
|
|
219
265
|
out = upsertEnvVar(out, plan.urlVar, apiUrl);
|
|
220
266
|
}
|
|
221
267
|
fs.writeFileSync(filePath, `${out.join('\n')}\n`);
|
|
222
|
-
log(`✓ ${
|
|
268
|
+
log(`✓ ${targetRel} updated (${changes.join('; ')}).`);
|
|
223
269
|
|
|
224
|
-
if (!isGitIgnored(appRoot,
|
|
225
|
-
log(`Reminder: add ${
|
|
270
|
+
if (!isGitIgnored(appRoot, targetRel)) {
|
|
271
|
+
log(`Reminder: add ${targetRel} to .gitignore so you don't commit your API key.`);
|
|
226
272
|
}
|
|
227
273
|
return 'written';
|
|
228
274
|
}
|