@fiodos/cli 0.1.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/src/wireWeb.js ADDED
@@ -0,0 +1,295 @@
1
+ /**
2
+ * wireWeb — automatic, consent-based orb wiring for web apps.
3
+ *
4
+ * Runs at the end of a WEB analysis (never for mobile). Every supported web
5
+ * ecosystem gets the same contract:
6
+ * 1. Detect mount point (framework-aware).
7
+ * 2. Verify we can edit safely (assessMount) — else honest snippet fallback.
8
+ * 3. Show what will change + ask [y/N] in terminal.
9
+ * 4. On yes → edit with FYODOS:ORB markers, post-build verify, revert on failure.
10
+ */
11
+ 'use strict';
12
+
13
+ const fs = require('fs');
14
+ const path = require('path');
15
+ const readline = require('readline');
16
+ const { runPostWireTest } = require('./postWireTest');
17
+ const {
18
+ assessMount,
19
+ describePlan,
20
+ applyMount,
21
+ verifyMounted,
22
+ backupFiles,
23
+ revertFiles,
24
+ writeConsentDoc,
25
+ agentJsx,
26
+ envExpr,
27
+ IMPORT_NAME,
28
+ WRAPPER_BASENAME,
29
+ } = require('./wireWebMount');
30
+
31
+ function readJsonSafe(file) {
32
+ try {
33
+ return JSON.parse(fs.readFileSync(file, 'utf8'));
34
+ } catch {
35
+ return null;
36
+ }
37
+ }
38
+
39
+ function detectFramework(appRoot) {
40
+ const pkg = readJsonSafe(path.join(appRoot, 'package.json')) || {};
41
+ const deps = { ...(pkg.dependencies || {}), ...(pkg.devDependencies || {}) };
42
+ const has = (rel) => fs.existsSync(path.join(appRoot, rel));
43
+
44
+ if (deps.next) return 'next';
45
+ if (deps['@angular/core'] || has('angular.json')) return 'angular';
46
+ if (deps['@sveltejs/kit']) return 'sveltekit';
47
+ if (deps.svelte || has('svelte.config.js') || has('svelte.config.ts')) return 'svelte';
48
+ if (deps.vue || deps['@vitejs/plugin-vue']) return 'vue';
49
+ if (deps.vite || deps['@vitejs/plugin-react'] || deps['react-scripts']) return 'vite';
50
+ return null;
51
+ }
52
+
53
+ function firstExisting(appRoot, rels) {
54
+ for (const rel of rels) {
55
+ const abs = path.join(appRoot, rel);
56
+ if (fs.existsSync(abs)) return abs;
57
+ }
58
+ return null;
59
+ }
60
+
61
+ function detectTarget(appRoot, framework = detectFramework(appRoot)) {
62
+ if (framework === 'vue') {
63
+ const f = firstExisting(appRoot, ['src/App.vue', 'src/main.ts', 'src/main.js']);
64
+ return f ? { kind: 'vue', file: f, ext: path.extname(f), framework } : { kind: 'vue', file: null, framework };
65
+ }
66
+ if (framework === 'sveltekit') {
67
+ const f = firstExisting(appRoot, [
68
+ 'src/routes/+layout.svelte', 'src/routes/+page.svelte', 'src/App.svelte',
69
+ ]);
70
+ return f ? { kind: 'sveltekit', file: f, ext: '.svelte', framework } : { kind: 'sveltekit', file: null, framework };
71
+ }
72
+ if (framework === 'svelte') {
73
+ const f = firstExisting(appRoot, ['src/App.svelte', 'src/routes/+layout.svelte', 'src/main.ts']);
74
+ return f ? { kind: 'svelte', file: f, ext: '.svelte', framework } : { kind: 'svelte', file: null, framework };
75
+ }
76
+ if (framework === 'angular') {
77
+ const f = firstExisting(appRoot, [
78
+ 'src/app/app.component.ts', 'src/app/app.ts', 'src/main.ts',
79
+ ]);
80
+ return f ? { kind: 'angular', file: f, ext: '.ts', framework } : { kind: 'angular', file: null, framework };
81
+ }
82
+
83
+ const appLayout = firstExisting(appRoot, [
84
+ 'app/layout.tsx', 'app/layout.jsx',
85
+ 'src/app/layout.tsx', 'src/app/layout.jsx',
86
+ ]);
87
+ if (appLayout) return { kind: 'next-app', file: appLayout, ext: path.extname(appLayout), framework: framework || 'next' };
88
+
89
+ const pagesApp = firstExisting(appRoot, [
90
+ 'pages/_app.tsx', 'pages/_app.jsx',
91
+ 'src/pages/_app.tsx', 'src/pages/_app.jsx',
92
+ ]);
93
+ if (pagesApp) return { kind: 'next-pages', file: pagesApp, ext: path.extname(pagesApp), framework: framework || 'next' };
94
+
95
+ const viteEntry = firstExisting(appRoot, [
96
+ 'src/main.tsx', 'src/main.jsx', 'src/index.tsx', 'src/App.tsx', 'src/App.jsx',
97
+ ]);
98
+ if (viteEntry) return { kind: 'vite', file: viteEntry, ext: path.extname(viteEntry), framework: framework || 'vite' };
99
+
100
+ return null;
101
+ }
102
+
103
+ function agentSnippetVue() {
104
+ return (
105
+ `<script setup lang="ts">\n` +
106
+ `import { FiodosAgent } from '@fiodos/vue';\n` +
107
+ `const fyodosApiKey = import.meta.env.VITE_FYODOS_API_KEY;\n` +
108
+ `const fyodosApiUrl = import.meta.env.VITE_FYODOS_API_URL;\n` +
109
+ `</script>\n\n` +
110
+ `<template>\n` +
111
+ ` <!-- ...your app... -->\n` +
112
+ ` <FiodosAgent :api-key="fyodosApiKey" :base-url="fyodosApiUrl" />\n` +
113
+ `</template>`
114
+ );
115
+ }
116
+
117
+ function agentSnippetSvelte() {
118
+ return (
119
+ `<script lang="ts">\n` +
120
+ ` import FiodosAgent from '@fiodos/svelte/FiodosAgent.svelte';\n` +
121
+ `</script>\n\n` +
122
+ `<FiodosAgent apiKey={import.meta.env.VITE_FYODOS_API_KEY} baseUrl={import.meta.env.VITE_FYODOS_API_URL} />`
123
+ );
124
+ }
125
+
126
+ function agentSnippetAngular() {
127
+ return (
128
+ `import { FiodosAgentComponent } from '@fiodos/angular/angular';\n\n` +
129
+ `@Component({\n` +
130
+ ` imports: [FiodosAgentComponent],\n` +
131
+ ` templateUrl: './app.component.html',\n` +
132
+ `})\n` +
133
+ `export class AppComponent {\n` +
134
+ ` fyodosApiKey = environment.fyodosApiKey;\n` +
135
+ `}\n\n` +
136
+ `<!-- app.component.html -->\n` +
137
+ `<fyodos-agent [apiKey]="fyodosApiKey" />`
138
+ );
139
+ }
140
+
141
+ function askYesNo(question) {
142
+ return new Promise((resolve) => {
143
+ if (!process.stdin.isTTY) {
144
+ resolve(false);
145
+ return;
146
+ }
147
+ const rl = readline.createInterface({ input: process.stdin, output: process.stderr });
148
+ rl.question(question, (answer) => {
149
+ rl.close();
150
+ resolve(/^(y|yes)/i.test((answer || '').trim()));
151
+ });
152
+ });
153
+ }
154
+
155
+ function printSnippet(target, framework, colors, appRoot, reason) {
156
+ const { blue, cyan, dim, reset } = colors;
157
+ const ts = !target || target.ext === '.tsx';
158
+ console.error(`\n${cyan}◉${reset} ${blue}Fiodos${reset} · ${dim}add the orb to your web app (copy this):${reset}`);
159
+ if (reason) {
160
+ console.error(`${dim}Reason: ${reason}${reset}\n`);
161
+ }
162
+
163
+ if (framework === 'vue') {
164
+ const where = target && target.file ? path.relative(appRoot, target.file) : 'src/App.vue';
165
+ console.error(`${dim}In ${where}:${reset}\n`);
166
+ console.error(agentSnippetVue());
167
+ return;
168
+ }
169
+ if (framework === 'svelte' || framework === 'sveltekit') {
170
+ const def = framework === 'sveltekit' ? 'src/routes/+layout.svelte' : 'src/App.svelte';
171
+ const where = target && target.file ? path.relative(appRoot, target.file) : def;
172
+ console.error(`${dim}In ${where}:${reset}\n`);
173
+ console.error(agentSnippetSvelte());
174
+ return;
175
+ }
176
+ if (framework === 'angular') {
177
+ const where = target && target.file ? path.relative(appRoot, target.file) : 'src/app/app.component.ts';
178
+ console.error(`${dim}In ${where}:${reset}\n`);
179
+ console.error(agentSnippetAngular());
180
+ return;
181
+ }
182
+
183
+ if (!target || target.kind === 'next-app') {
184
+ const where = target ? path.relative(appRoot, target.file) : 'app/layout.tsx';
185
+ console.error(`${dim}Next App Router — client wrapper + layout edit:${reset}\n`);
186
+ console.error(
187
+ `'use client';\nimport { FiodosAgent } from '@fiodos/react';\n` +
188
+ `export default function ${IMPORT_NAME}() { return (${agentJsx(framework || 'next', ts, '\n ')}\n ); }`,
189
+ );
190
+ console.error(`\n${dim}In ${where}, before </body>: <${IMPORT_NAME} />${reset}`);
191
+ } else {
192
+ const where = path.relative(appRoot, target.file);
193
+ console.error(`${dim}In ${where}:${reset}\n`);
194
+ console.error(`import { FiodosAgent } from '@fiodos/react';\n\n${agentJsx(framework || 'vite', ts, '')}`);
195
+ }
196
+ console.error('');
197
+ }
198
+
199
+ function printMountPreview(plan, colors) {
200
+ const { dim, reset } = colors;
201
+ console.error(`${dim}Planned orb mount (${plan.rel}):${reset}`);
202
+ for (const line of describePlan(plan)) {
203
+ console.error(`${dim} · ${line}${reset}`);
204
+ }
205
+ console.error('');
206
+ }
207
+
208
+ async function wireWebOrb(appRoot, opts = {}) {
209
+ const { colors = {}, assumeYes = false, noWire = false } = opts;
210
+ if (noWire) return { status: 'skipped' };
211
+
212
+ const framework = detectFramework(appRoot);
213
+ const target = detectTarget(appRoot, framework);
214
+ const plan = assessMount(target, framework, appRoot);
215
+
216
+ if (plan.already) {
217
+ return { status: 'already', file: plan.rel || (target && path.relative(appRoot, target.file)) };
218
+ }
219
+
220
+ if (!plan.ok) {
221
+ printSnippet(target, framework, colors, appRoot, plan.reason);
222
+ return { status: 'printed', reason: plan.reason };
223
+ }
224
+
225
+ printMountPreview(plan, colors);
226
+
227
+ const yes = assumeYes || (await askYesNo(
228
+ `${colors.cyan || ''}◉${colors.reset || ''} About to add the orb to ${plan.rel}. Proceed? [y/N] `,
229
+ ));
230
+ if (!yes) {
231
+ printSnippet(target, framework, colors, appRoot, 'you declined the automatic edit');
232
+ return { status: 'declined', file: plan.rel };
233
+ }
234
+
235
+ const backups = backupFiles(plan.files);
236
+ try {
237
+ applyMount(plan);
238
+ if (!verifyMounted(plan)) {
239
+ throw new Error('post-edit verification failed — FiodosAgent not found in edited files');
240
+ }
241
+ const test = await runPostWireTest(appRoot, { timeoutMs: 180000 });
242
+ if (!test.ok && test.stage !== 'skipped-no-deps') {
243
+ revertFiles(backups);
244
+ printSnippet(target, framework, colors, appRoot, `build/typecheck failed after mount — reverted (${test.command})`);
245
+ return { status: 'reverted', file: plan.rel, test, reason: test.output };
246
+ }
247
+ writeConsentDoc(appRoot, plan);
248
+ return { status: 'added', file: plan.rel, test };
249
+ } catch (err) {
250
+ revertFiles(backups);
251
+ printSnippet(target, framework, colors, appRoot, err.message || String(err));
252
+ return { status: 'failed', file: plan.rel, error: err };
253
+ }
254
+ }
255
+
256
+ function reportWireResult(result, colors = {}) {
257
+ const { blue, cyan, dim, reset } = colors;
258
+ const tag = `${cyan || ''}◉${reset || ''} ${blue || ''}Fiodos${reset || ''}`;
259
+ switch (result.status) {
260
+ case 'added':
261
+ console.error(`${tag} · ✓ orb added to ${result.file}. Start your web app (e.g. \`npm run dev\`) to see it.`);
262
+ if (result.test && result.test.stage === 'skipped-no-deps') {
263
+ console.error(`${tag} · ${dim || ''}build not verified (no node_modules) — run npm install && npm run build locally.${reset || ''}`);
264
+ } else if (result.test && result.test.ok) {
265
+ console.error(`${tag} · ✓ ${result.test.command} passed after mount.`);
266
+ }
267
+ break;
268
+ case 'already':
269
+ console.error(`${tag} · ✓ orb already mounted in ${result.file}. Nothing to do.`);
270
+ break;
271
+ case 'declined':
272
+ console.error(`${tag} · ${dim || ''}did not modify your code. Paste the snippet above to mount the orb.${reset || ''}`);
273
+ break;
274
+ case 'printed':
275
+ console.error(`${tag} · ${dim || ''}automatic mount not possible${result.reason ? `: ${result.reason}` : ''}. Paste the snippet above.${reset || ''}`);
276
+ break;
277
+ case 'reverted':
278
+ console.error(`${tag} · ✗ mount reverted — app would not build. Paste the snippet above or fix manually.`);
279
+ if (result.reason) console.error(`${dim}${result.reason}${reset}`);
280
+ break;
281
+ case 'failed':
282
+ console.error(`${tag} · ✗ could not safely edit ${result.file}. Paste the snippet above.`);
283
+ break;
284
+ case 'skipped':
285
+ default:
286
+ break;
287
+ }
288
+ }
289
+
290
+ module.exports = {
291
+ wireWebOrb,
292
+ reportWireResult,
293
+ detectFramework,
294
+ detectTarget,
295
+ };