@fiodos/cli 0.1.21 → 0.1.22
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 +33 -0
- package/src/wireReactNative.js +69 -4
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@fiodos/cli",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.22",
|
|
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
|
@@ -85,6 +85,7 @@
|
|
|
85
85
|
const fs = require('fs');
|
|
86
86
|
const os = require('os');
|
|
87
87
|
const path = require('path');
|
|
88
|
+
const readline = require('readline');
|
|
88
89
|
const { loadEnv, loadAppEnv } = require('./loadEnv');
|
|
89
90
|
loadEnv();
|
|
90
91
|
|
|
@@ -161,6 +162,30 @@ function logUser(line) {
|
|
|
161
162
|
console.log(`${cyan}◉${reset} ${blue}Fiodos${reset} · ${line}`);
|
|
162
163
|
}
|
|
163
164
|
|
|
165
|
+
/**
|
|
166
|
+
* Ask the developer to confirm before the analysis runs. `yes` continues; any
|
|
167
|
+
* other answer (or `no`) cancels instantly — the command ends without analyzing.
|
|
168
|
+
* Non-interactive runs (no TTY: CI, pipes) proceed automatically so automation
|
|
169
|
+
* is never blocked waiting on stdin.
|
|
170
|
+
*/
|
|
171
|
+
function askProceed() {
|
|
172
|
+
return new Promise((resolve) => {
|
|
173
|
+
if (!process.stdin.isTTY) {
|
|
174
|
+
resolve(true);
|
|
175
|
+
return;
|
|
176
|
+
}
|
|
177
|
+
const { blue, cyan, reset } = fyodosTermColors();
|
|
178
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stderr });
|
|
179
|
+
rl.question(
|
|
180
|
+
`${cyan}◉${reset} ${blue}Fiodos${reset} · Analyze and publish this project? (yes/no) `,
|
|
181
|
+
(answer) => {
|
|
182
|
+
rl.close();
|
|
183
|
+
resolve(/^(y|yes|s|si|sí)$/i.test((answer || '').trim()));
|
|
184
|
+
},
|
|
185
|
+
);
|
|
186
|
+
});
|
|
187
|
+
}
|
|
188
|
+
|
|
164
189
|
/** Spinner copy: "website" for web targets, "app" for mobile/native. */
|
|
165
190
|
function surfaceNoun(platform) {
|
|
166
191
|
return platform === 'web' ? 'website' : 'app';
|
|
@@ -226,6 +251,14 @@ async function withSpinner(label, work) {
|
|
|
226
251
|
}
|
|
227
252
|
|
|
228
253
|
async function main() {
|
|
254
|
+
// Consent gate: confirm before doing anything. `yes` continues; `no` (or any
|
|
255
|
+
// other answer) cancels instantly — no analysis is run, the command just ends.
|
|
256
|
+
const proceed = await askProceed();
|
|
257
|
+
if (!proceed) {
|
|
258
|
+
logUser('Cancelled — no analysis was run.');
|
|
259
|
+
return;
|
|
260
|
+
}
|
|
261
|
+
|
|
229
262
|
// Accept an optional leading `analyze` subcommand: `fiodos analyze <dir>`.
|
|
230
263
|
let positionals = positionalArgs();
|
|
231
264
|
if (positionals[0] === 'analyze') positionals = positionals.slice(1);
|
package/src/wireReactNative.js
CHANGED
|
@@ -19,8 +19,13 @@
|
|
|
19
19
|
const fs = require('fs');
|
|
20
20
|
const path = require('path');
|
|
21
21
|
|
|
22
|
-
|
|
23
|
-
|
|
22
|
+
// Markers are JS LINE comments, never JSX `{/* */}` comment-expressions. A
|
|
23
|
+
// `return ( … )` accepts exactly ONE root JSX node, and a `{/* */}` is itself an
|
|
24
|
+
// expression node: placing it as a sibling of the wrapper ("{/* */} <Orb>…")
|
|
25
|
+
// makes the return have multiple roots → "Unexpected token, expected ','".
|
|
26
|
+
// Line comments are pure trivia, valid before/after the single root element.
|
|
27
|
+
const ORB_START = '// FYODOS:ORB:START (auto-generated — safe to remove)';
|
|
28
|
+
const ORB_END = '// FYODOS:ORB:END';
|
|
24
29
|
const IMPORT_START = '// FYODOS:ORB:START (auto-generated — safe to remove)';
|
|
25
30
|
const IMPORT_END = '// FYODOS:ORB:END';
|
|
26
31
|
|
|
@@ -31,6 +36,23 @@ const LAYOUT_CANDIDATES = [
|
|
|
31
36
|
'src/app/_layout.jsx',
|
|
32
37
|
];
|
|
33
38
|
|
|
39
|
+
/**
|
|
40
|
+
* Last-line defense: NEVER write a layout that won't parse. esbuild is already a
|
|
41
|
+
* CLI dependency; we use it to validate the generated TSX/JSX. On any doubt we
|
|
42
|
+
* return false and the caller falls back to the (non-destructive) snippet path.
|
|
43
|
+
*/
|
|
44
|
+
function parsesAsTsx(source, layoutRel) {
|
|
45
|
+
try {
|
|
46
|
+
const esbuild = require('esbuild');
|
|
47
|
+
const loader = layoutRel && layoutRel.endsWith('.jsx') ? 'jsx' : 'tsx';
|
|
48
|
+
esbuild.transformSync(source, { loader });
|
|
49
|
+
return true;
|
|
50
|
+
} catch (e) {
|
|
51
|
+
if (e && e.code === 'MODULE_NOT_FOUND') return true; // esbuild unavailable → don't block
|
|
52
|
+
return false;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
34
56
|
function findRootLayout(appRoot) {
|
|
35
57
|
for (const rel of LAYOUT_CANDIDATES) {
|
|
36
58
|
const abs = path.join(appRoot, rel);
|
|
@@ -131,6 +153,29 @@ function buildWiredLayout(source, manifestSpecifier) {
|
|
|
131
153
|
return insertImportsAfterLastImport(withWrap, importBlock);
|
|
132
154
|
}
|
|
133
155
|
|
|
156
|
+
/**
|
|
157
|
+
* Strip a previous orb wrap so we can re-wire cleanly. Removes our markers (both
|
|
158
|
+
* the current line-comment form and the legacy broken JSX-comment form), our
|
|
159
|
+
* imports, and the single-line FiodosExpoAgent open/close tags — leaving the
|
|
160
|
+
* user's original tree (possibly over-indented, which is harmless / re-wrapped).
|
|
161
|
+
*/
|
|
162
|
+
function unwireOrb(source) {
|
|
163
|
+
const drop = (line) => {
|
|
164
|
+
const t = line.trim();
|
|
165
|
+
if (/FYODOS:ORB/.test(line)) return true; // `// …` or `{/* … */}` markers
|
|
166
|
+
if (/^import\s+\{?\s*FiodosExpoAgent\b/.test(t)) return true;
|
|
167
|
+
if (/^import\s+fyodosManifest\b/.test(t)) return true;
|
|
168
|
+
if (/^<FiodosExpoAgent\b[^]*>$/.test(t)) return true; // our single-line open tag
|
|
169
|
+
if (/^<\/FiodosExpoAgent>$/.test(t)) return true;
|
|
170
|
+
return false;
|
|
171
|
+
};
|
|
172
|
+
return source
|
|
173
|
+
.split('\n')
|
|
174
|
+
.filter((line) => !drop(line))
|
|
175
|
+
.join('\n')
|
|
176
|
+
.replace(/\n{3,}/g, '\n\n');
|
|
177
|
+
}
|
|
178
|
+
|
|
134
179
|
/** The copy-paste snippet shown when we can't safely auto-wrap. */
|
|
135
180
|
function mountSnippet(manifestSpecifier) {
|
|
136
181
|
return (
|
|
@@ -174,8 +219,19 @@ function wireReactNativeOrb(appRoot, { manifest, colors = {}, noWire = false } =
|
|
|
174
219
|
}
|
|
175
220
|
|
|
176
221
|
if (source.includes('FiodosExpoAgent') || source.includes('FYODOS:ORB')) {
|
|
177
|
-
|
|
178
|
-
|
|
222
|
+
// Already wired AND still valid → nothing to do.
|
|
223
|
+
if (parsesAsTsx(source, layout.rel)) {
|
|
224
|
+
if (!noWire && manifest) writeManifestFile(appRoot, manifest);
|
|
225
|
+
return { status: 'already', file: layout.rel };
|
|
226
|
+
}
|
|
227
|
+
// A previous CLI version may have left an UNPARSEABLE wrap (e.g. JSX
|
|
228
|
+
// `{/* */}` markers as siblings of the return root). Self-heal: strip our
|
|
229
|
+
// own markers/imports and re-wire cleanly below.
|
|
230
|
+
if (!noWire) {
|
|
231
|
+
source = unwireOrb(source);
|
|
232
|
+
} else {
|
|
233
|
+
return { status: 'skipped', reason: 'no-wire' };
|
|
234
|
+
}
|
|
179
235
|
}
|
|
180
236
|
|
|
181
237
|
if (noWire) return { status: 'skipped', reason: 'no-wire' };
|
|
@@ -187,6 +243,13 @@ function wireReactNativeOrb(appRoot, { manifest, colors = {}, noWire = false } =
|
|
|
187
243
|
return { status: 'printed', file: layout.rel, reason: 'unrecognized-layout' };
|
|
188
244
|
}
|
|
189
245
|
|
|
246
|
+
// Safety net: if the auto-wrap would produce code that doesn't parse, do NOT
|
|
247
|
+
// write it. Leave the file untouched and show the manual snippet instead.
|
|
248
|
+
if (!parsesAsTsx(wired, layout.rel)) {
|
|
249
|
+
printSnippet(colors, layout.rel, manifestSpecifier, 'auto-wrap would not parse — left untouched');
|
|
250
|
+
return { status: 'printed', file: layout.rel, reason: 'wrap-unparseable' };
|
|
251
|
+
}
|
|
252
|
+
|
|
190
253
|
try {
|
|
191
254
|
fs.writeFileSync(layout.abs, wired);
|
|
192
255
|
} catch (e) {
|
|
@@ -210,6 +273,8 @@ module.exports = {
|
|
|
210
273
|
reportReactNativeWire,
|
|
211
274
|
// exported for tests
|
|
212
275
|
buildWiredLayout,
|
|
276
|
+
unwireOrb,
|
|
277
|
+
parsesAsTsx,
|
|
213
278
|
findReturnParenSpan,
|
|
214
279
|
writeManifestFile,
|
|
215
280
|
manifestImportSpecifier,
|