@fiodos/cli 0.1.20 → 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 +249 -1
- package/src/wireFlutter.js +170 -0
- package/src/wireReactNative.js +282 -0
- package/src/writeEnv.js +12 -0
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
|
|
|
@@ -94,6 +95,8 @@ const { resolveAnalysisRoot } = require('./resolveAnalysisRoot');
|
|
|
94
95
|
const { analyzeWithAI, correctActionWiring, DEFAULT_MODEL, resolveModel } = require('./aiAnalyze');
|
|
95
96
|
const { verifyManifest } = require('./verify');
|
|
96
97
|
const { wireWebOrb, reportWireResult, connectOrbRegistries, reportRegistriesResult } = require('./wireWeb');
|
|
98
|
+
const { wireReactNativeOrb, reportReactNativeWire } = require('./wireReactNative');
|
|
99
|
+
const { wireFlutterOrb, writeFlutterEnv, reportFlutterWire } = require('./wireFlutter');
|
|
97
100
|
const { wireHandlers, reportHandlerResult } = require('./wireHandlers');
|
|
98
101
|
const { offerWriteEnv, printEnvManualReminder } = require('./writeEnv');
|
|
99
102
|
const { scoreSurface, computeSurface } = require('./scoreSurface');
|
|
@@ -159,6 +162,30 @@ function logUser(line) {
|
|
|
159
162
|
console.log(`${cyan}◉${reset} ${blue}Fiodos${reset} · ${line}`);
|
|
160
163
|
}
|
|
161
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
|
+
|
|
162
189
|
/** Spinner copy: "website" for web targets, "app" for mobile/native. */
|
|
163
190
|
function surfaceNoun(platform) {
|
|
164
191
|
return platform === 'web' ? 'website' : 'app';
|
|
@@ -224,6 +251,14 @@ async function withSpinner(label, work) {
|
|
|
224
251
|
}
|
|
225
252
|
|
|
226
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
|
+
|
|
227
262
|
// Accept an optional leading `analyze` subcommand: `fiodos analyze <dir>`.
|
|
228
263
|
let positionals = positionalArgs();
|
|
229
264
|
if (positionals[0] === 'analyze') positionals = positionals.slice(1);
|
|
@@ -678,7 +713,173 @@ async function main() {
|
|
|
678
713
|
logDev(`[change-registry] could not write: ${e.message}`);
|
|
679
714
|
}
|
|
680
715
|
}
|
|
716
|
+
} else if (platform === 'mobile' && publishing) {
|
|
717
|
+
// ── Mobile (Expo / React Native): mirror the web pipeline, adapted ─────────
|
|
718
|
+
// Same single command that publishes also mounts the orb (via the SDK's
|
|
719
|
+
// one-line <FiodosExpoAgent/>), records the install proof so the dashboard
|
|
720
|
+
// connection screen completes, writes the EXPO_PUBLIC_* env, and prints the
|
|
721
|
+
// EAS production step — exactly like web, just for an app.
|
|
722
|
+
loadAppEnv(analysisRoot);
|
|
723
|
+
const { apiKey, apiUrl } = resolveApiTarget();
|
|
724
|
+
const noWire = process.argv.includes('--no-wire');
|
|
725
|
+
let orbWireResult = null;
|
|
726
|
+
let envWriteResult = null;
|
|
727
|
+
|
|
728
|
+
await withSpinner('Publishing to your project', async ({ setLabel, pause, resume }) => {
|
|
729
|
+
await publishManifest(manifest, publishMeta, analysisToken, { skipSuccessLog: true });
|
|
730
|
+
if (!noWire && !process.argv.includes('--no-orb-wire')) {
|
|
731
|
+
setLabel(orbMountSpinnerLabel(platform));
|
|
732
|
+
orbWireResult = wireReactNativeOrb(analysisRoot, {
|
|
733
|
+
manifest,
|
|
734
|
+
colors: fyodosTermColors(),
|
|
735
|
+
noWire,
|
|
736
|
+
});
|
|
737
|
+
pause();
|
|
738
|
+
reportReactNativeWire(orbWireResult, fyodosTermColors(), { quiet: !isVerbose() });
|
|
739
|
+
resume();
|
|
740
|
+
// Install proof: only when the orb is genuinely wired into the code. If
|
|
741
|
+
// we could only print a snippet, we DON'T post — the dashboard then
|
|
742
|
+
// waits for the runtime heartbeat (FiodosProvider's reportOrbSeen) so a
|
|
743
|
+
// green tick always reflects a real mount, never a guess.
|
|
744
|
+
if (
|
|
745
|
+
apiKey &&
|
|
746
|
+
(orbWireResult.status === 'added' || orbWireResult.status === 'already')
|
|
747
|
+
) {
|
|
748
|
+
const proof = await postOrbWired({
|
|
749
|
+
apiKey,
|
|
750
|
+
apiUrl,
|
|
751
|
+
file: orbWireResult.file || '',
|
|
752
|
+
platform: 'mobile',
|
|
753
|
+
actionsStatus: 'done',
|
|
754
|
+
});
|
|
755
|
+
if (proof && proof.ok === false && proof.reason !== 'no-api-key') {
|
|
756
|
+
logDev(`[orb-wired] mobile install proof not recorded (${proof.status || proof.reason}); dashboard will rely on the runtime ping`);
|
|
757
|
+
}
|
|
758
|
+
}
|
|
759
|
+
}
|
|
760
|
+
});
|
|
761
|
+
logUser('Published to your project');
|
|
762
|
+
|
|
763
|
+
// API key in the app environment (EXPO_PUBLIC_*) — same consent flow as web.
|
|
764
|
+
if (!process.argv.includes('--no-write-env')) {
|
|
765
|
+
try {
|
|
766
|
+
envWriteResult = await offerWriteEnv({
|
|
767
|
+
appRoot: analysisRoot,
|
|
768
|
+
apiKey,
|
|
769
|
+
apiUrl,
|
|
770
|
+
defaultApiUrl: DEFAULT_API_URL,
|
|
771
|
+
assumeYes: process.argv.includes('--write-env-yes'),
|
|
772
|
+
log: logUser,
|
|
773
|
+
logDev,
|
|
774
|
+
});
|
|
775
|
+
if (envWriteResult) printEnvManualReminder(envWriteResult, { logUser, logDev });
|
|
776
|
+
} catch (e) {
|
|
777
|
+
logDev(`[write-env] skipped: ${e.message}`);
|
|
778
|
+
}
|
|
779
|
+
}
|
|
780
|
+
|
|
781
|
+
// Production (EAS) reminder for the build-time public key. A CLI cannot (and
|
|
782
|
+
// must not) authenticate into the user's Expo account, so we print the exact
|
|
783
|
+
// step instead — mirroring the web deploy-provider reminder.
|
|
784
|
+
if (apiKey) printExpoProductionReminder({ apiKey, appRoot: analysisRoot });
|
|
785
|
+
|
|
786
|
+
if (!noWire) {
|
|
787
|
+
try {
|
|
788
|
+
const record = buildRunRecord(analysisRoot, {
|
|
789
|
+
appName: manifest.appName,
|
|
790
|
+
manifest,
|
|
791
|
+
publishMeta,
|
|
792
|
+
orbResult: orbWireResult,
|
|
793
|
+
envResult: envWriteResult,
|
|
794
|
+
});
|
|
795
|
+
const paths = writeChangeRegistry(analysisRoot, record);
|
|
796
|
+
logUser(`Change log: ${paths.mdRel} (history in .fyodos/changes/)`);
|
|
797
|
+
} catch (e) {
|
|
798
|
+
logDev(`[change-registry] could not write: ${e.message}`);
|
|
799
|
+
}
|
|
800
|
+
}
|
|
801
|
+
} else if (platform === 'flutter' && publishing) {
|
|
802
|
+
// ── Flutter: mirror the web/RN pipeline, adapted to Dart ──────────────────
|
|
803
|
+
// The Flutter SDK's one-line FiodosAgent self-fetches the published manifest
|
|
804
|
+
// by API key, so we don't write a manifest file. We publish, wrap lib/main's
|
|
805
|
+
// home: with FiodosAgent, record the install proof, write the dart-define env
|
|
806
|
+
// file, and print the run + production commands.
|
|
807
|
+
loadAppEnv(analysisRoot);
|
|
808
|
+
const { apiKey, apiUrl } = resolveApiTarget();
|
|
809
|
+
const noWire = process.argv.includes('--no-wire');
|
|
810
|
+
let orbWireResult = null;
|
|
811
|
+
let envWriteResult = null;
|
|
812
|
+
|
|
813
|
+
await withSpinner('Publishing to your project', async ({ setLabel, pause, resume }) => {
|
|
814
|
+
await publishManifest(manifest, publishMeta, analysisToken, { skipSuccessLog: true });
|
|
815
|
+
if (!noWire && !process.argv.includes('--no-orb-wire')) {
|
|
816
|
+
setLabel(orbMountSpinnerLabel(platform));
|
|
817
|
+
orbWireResult = wireFlutterOrb(analysisRoot, { colors: fyodosTermColors(), noWire });
|
|
818
|
+
pause();
|
|
819
|
+
reportFlutterWire(orbWireResult, fyodosTermColors(), { quiet: !isVerbose() });
|
|
820
|
+
resume();
|
|
821
|
+
// Install proof only when the orb is genuinely wired into main.dart. If
|
|
822
|
+
// we could only print a snippet, we DON'T post — the dashboard then waits
|
|
823
|
+
// for the runtime heartbeat (FiodosAgent posts orb-seen on warmUp).
|
|
824
|
+
if (
|
|
825
|
+
apiKey &&
|
|
826
|
+
(orbWireResult.status === 'added' || orbWireResult.status === 'already')
|
|
827
|
+
) {
|
|
828
|
+
const proof = await postOrbWired({
|
|
829
|
+
apiKey,
|
|
830
|
+
apiUrl,
|
|
831
|
+
file: orbWireResult.file || '',
|
|
832
|
+
platform: 'flutter',
|
|
833
|
+
actionsStatus: 'done',
|
|
834
|
+
});
|
|
835
|
+
if (proof && proof.ok === false && proof.reason !== 'no-api-key') {
|
|
836
|
+
logDev(`[orb-wired] flutter install proof not recorded (${proof.status || proof.reason}); dashboard will rely on the runtime ping`);
|
|
837
|
+
}
|
|
838
|
+
}
|
|
839
|
+
}
|
|
840
|
+
});
|
|
841
|
+
logUser('Published to your project');
|
|
842
|
+
|
|
843
|
+
// Dart-define env file (git-ignored) so `flutter run` has the key without
|
|
844
|
+
// hardcoding the secret in committed source.
|
|
845
|
+
if (apiKey && !process.argv.includes('--no-write-env')) {
|
|
846
|
+
try {
|
|
847
|
+
envWriteResult = writeFlutterEnv(analysisRoot, {
|
|
848
|
+
apiKey,
|
|
849
|
+
apiUrl,
|
|
850
|
+
defaultApiUrl: DEFAULT_API_URL,
|
|
851
|
+
});
|
|
852
|
+
if (envWriteResult && envWriteResult.written) {
|
|
853
|
+
logUser(`API key saved to ${envWriteResult.file} (git-ignored).`);
|
|
854
|
+
if (envWriteResult.gitignore && envWriteResult.gitignore.changed) {
|
|
855
|
+
logUser(`Added ${envWriteResult.gitignore.pattern} to .gitignore so your key is never pushed to GitHub.`);
|
|
856
|
+
}
|
|
857
|
+
}
|
|
858
|
+
} catch (e) {
|
|
859
|
+
logDev(`[write-env] flutter skipped: ${e.message}`);
|
|
860
|
+
}
|
|
861
|
+
}
|
|
862
|
+
|
|
863
|
+
if (apiKey) printFlutterRunReminder({ apiKey, envFile: envWriteResult && envWriteResult.file });
|
|
864
|
+
|
|
865
|
+
if (!noWire) {
|
|
866
|
+
try {
|
|
867
|
+
const record = buildRunRecord(analysisRoot, {
|
|
868
|
+
appName: manifest.appName,
|
|
869
|
+
manifest,
|
|
870
|
+
publishMeta,
|
|
871
|
+
orbResult: orbWireResult,
|
|
872
|
+
envResult: envWriteResult,
|
|
873
|
+
});
|
|
874
|
+
const paths = writeChangeRegistry(analysisRoot, record);
|
|
875
|
+
logUser(`Change log: ${paths.mdRel} (history in .fyodos/changes/)`);
|
|
876
|
+
} catch (e) {
|
|
877
|
+
logDev(`[change-registry] could not write: ${e.message}`);
|
|
878
|
+
}
|
|
879
|
+
}
|
|
681
880
|
} else if (publishing) {
|
|
881
|
+
// Native (iOS / Android): the orb ships through the native SDK — there is no
|
|
882
|
+
// JS/Dart entrypoint to wire here, so we only publish the manifest.
|
|
682
883
|
loadAppEnv(analysisRoot);
|
|
683
884
|
await withSpinner('Publishing to your project', () =>
|
|
684
885
|
publishManifest(manifest, publishMeta, analysisToken, { skipSuccessLog: true }),
|
|
@@ -687,6 +888,52 @@ async function main() {
|
|
|
687
888
|
}
|
|
688
889
|
}
|
|
689
890
|
|
|
891
|
+
/**
|
|
892
|
+
* How to run a Flutter app with the orb key (dev) and ship it (production).
|
|
893
|
+
* Flutter has no auto-loaded .env: the compile-time key is passed via
|
|
894
|
+
* --dart-define. We never touch the user's build pipeline — we print the exact
|
|
895
|
+
* commands, mirroring the web deploy reminder and the RN EAS reminder.
|
|
896
|
+
*/
|
|
897
|
+
function printFlutterRunReminder({ apiKey, envFile }) {
|
|
898
|
+
const file = envFile || 'fyodos.env.json';
|
|
899
|
+
logUser('Run your app with the orb — pass the key as a compile-time define (2 steps)');
|
|
900
|
+
console.log(' Step 1 — Dev run (reads the git-ignored env file we just wrote):');
|
|
901
|
+
console.log(` flutter run --dart-define-from-file=${file}`);
|
|
902
|
+
console.log('');
|
|
903
|
+
console.log(' Step 2 — Production build — pass the same define to your release build / CI:');
|
|
904
|
+
console.log(` flutter build apk --dart-define-from-file=${file}`);
|
|
905
|
+
console.log(` (or --dart-define=FYODOS_API_KEY=${apiKey})`);
|
|
906
|
+
}
|
|
907
|
+
|
|
908
|
+
/**
|
|
909
|
+
* Production (EAS) step for Expo: EXPO_PUBLIC_* vars are inlined by Metro from
|
|
910
|
+
* the build environment. Local `.env` covers `expo start`, but cloud EAS builds
|
|
911
|
+
* (which never receive a git-ignored .env) need the key set in EAS env so the
|
|
912
|
+
* orb is not invisible in TestFlight / Play builds. We never touch the user's
|
|
913
|
+
* Expo account — we print the exact command + dashboard path.
|
|
914
|
+
*/
|
|
915
|
+
function printExpoProductionReminder({ apiKey, appRoot }) {
|
|
916
|
+
const usesEas = (() => {
|
|
917
|
+
try {
|
|
918
|
+
return fs.existsSync(path.join(appRoot, 'eas.json'));
|
|
919
|
+
} catch {
|
|
920
|
+
return false;
|
|
921
|
+
}
|
|
922
|
+
})();
|
|
923
|
+
logUser('Make the orb appear in PRODUCTION builds — add the key to EAS (2 steps)');
|
|
924
|
+
console.log(' Step 1 — Copy your orb key (this exact value):');
|
|
925
|
+
console.log(` ${apiKey}`);
|
|
926
|
+
console.log('');
|
|
927
|
+
console.log(' Step 2 — Set it in your EAS build environment:');
|
|
928
|
+
console.log(` eas env:create --name EXPO_PUBLIC_FYODOS_API_KEY --value ${apiKey} --environment production`);
|
|
929
|
+
console.log(' (or expo.dev → your project → Environment variables, then rebuild)');
|
|
930
|
+
if (!usesEas) {
|
|
931
|
+
console.log('');
|
|
932
|
+
console.log(' No eas.json yet? `expo start` already reads your local .env;');
|
|
933
|
+
console.log(' add the EAS variable when you set up cloud builds.');
|
|
934
|
+
}
|
|
935
|
+
}
|
|
936
|
+
|
|
690
937
|
/**
|
|
691
938
|
* Resolve the target platform from package.json dependencies — the only signal
|
|
692
939
|
* that reliably tells a Next.js App Router web app (which has an app/ dir) apart
|
|
@@ -979,6 +1226,7 @@ async function postOrbWired({
|
|
|
979
1226
|
apiKey,
|
|
980
1227
|
apiUrl,
|
|
981
1228
|
file,
|
|
1229
|
+
platform = 'web',
|
|
982
1230
|
actionsStatus = 'pending',
|
|
983
1231
|
actionsTotal = 0,
|
|
984
1232
|
actionsDone = 0,
|
|
@@ -990,7 +1238,7 @@ async function postOrbWired({
|
|
|
990
1238
|
headers: { 'Content-Type': 'application/json', 'x-api-key': apiKey },
|
|
991
1239
|
body: JSON.stringify({
|
|
992
1240
|
file: file || '',
|
|
993
|
-
platform
|
|
1241
|
+
platform,
|
|
994
1242
|
actionsStatus,
|
|
995
1243
|
actionsTotal,
|
|
996
1244
|
actionsDone,
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* wireFlutter — auto-mount the Fiodos orb in a Flutter app, mirroring the web
|
|
3
|
+
* and React Native orb auto-mounts (wireWeb.js / wireReactNative.js) adapted to
|
|
4
|
+
* Dart / Flutter.
|
|
5
|
+
*
|
|
6
|
+
* The Flutter SDK already exposes a true one-line drop-in, FiodosAgent, that
|
|
7
|
+
* self-fetches the published manifest by API key, fetches the dashboard orb
|
|
8
|
+
* theme, posts the orb-seen heartbeat, and renders the themed orb. So there is
|
|
9
|
+
* no manifest file to write (unlike RN): we only
|
|
10
|
+
* 1. add `import 'package:fiodos_flutter/fiodos_flutter.dart';`, and
|
|
11
|
+
* 2. wrap the app's `home:` widget with FiodosAgent (reading the key from a
|
|
12
|
+
* compile-time --dart-define, never hardcoding the secret in source).
|
|
13
|
+
*
|
|
14
|
+
* Edits are idempotent and reversible (FYODOS:ORB markers). The orb must live
|
|
15
|
+
* INSIDE MaterialApp/CupertinoApp (it needs a Directionality/Material ancestor),
|
|
16
|
+
* which is exactly where `home:` is — so we wrap there, not at runApp(). When the
|
|
17
|
+
* `home:` value is not a simple widget expression we DON'T touch the file: we
|
|
18
|
+
* print a short snippet instead (like the web/RN paths), and the caller then
|
|
19
|
+
* skips the install proof so the dashboard waits for the runtime heartbeat.
|
|
20
|
+
*/
|
|
21
|
+
'use strict';
|
|
22
|
+
|
|
23
|
+
const fs = require('fs');
|
|
24
|
+
const path = require('path');
|
|
25
|
+
|
|
26
|
+
const { ensureEnvGitIgnored } = require('./writeEnv');
|
|
27
|
+
|
|
28
|
+
const IMPORT_START = '// FYODOS:ORB:START (auto-generated — safe to remove)';
|
|
29
|
+
const IMPORT_END = '// FYODOS:ORB:END';
|
|
30
|
+
const SDK_IMPORT = "import 'package:fiodos_flutter/fiodos_flutter.dart';";
|
|
31
|
+
|
|
32
|
+
const MAIN_CANDIDATES = ['lib/main.dart'];
|
|
33
|
+
|
|
34
|
+
function findMainDart(appRoot) {
|
|
35
|
+
for (const rel of MAIN_CANDIDATES) {
|
|
36
|
+
const abs = path.join(appRoot, rel);
|
|
37
|
+
if (fs.existsSync(abs)) return { abs, rel };
|
|
38
|
+
}
|
|
39
|
+
return null;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function insertImportAfterLastImport(source, block) {
|
|
43
|
+
const lines = source.split('\n');
|
|
44
|
+
let lastImport = -1;
|
|
45
|
+
for (let i = 0; i < lines.length; i += 1) {
|
|
46
|
+
if (/^\s*import\s+['"]/.test(lines[i])) lastImport = i;
|
|
47
|
+
}
|
|
48
|
+
if (lastImport === -1) return `${block}\n${source}`;
|
|
49
|
+
lines.splice(lastImport + 1, 0, block);
|
|
50
|
+
return lines.join('\n');
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Match the `home:` value when it is a single, simple widget constructor — the
|
|
55
|
+
* common case (e.g. `home: const TodoListScreen()`, `home: MyHomePage(title: 'x')`).
|
|
56
|
+
* We deliberately do NOT try to parse arbitrarily nested expressions: a complex
|
|
57
|
+
* home (Builder, ternaries, nested constructors with parens) falls back to the
|
|
58
|
+
* snippet, never to a risky edit. Group 1 = the whole widget expr (with const).
|
|
59
|
+
*/
|
|
60
|
+
const HOME_RE = /home:\s*((?:const\s+)?[A-Za-z_][\w.]*\([^()]*\))/;
|
|
61
|
+
|
|
62
|
+
/** Build the wired main.dart, or null when home: is not a simple widget. */
|
|
63
|
+
function buildWiredMain(source) {
|
|
64
|
+
if (!HOME_RE.test(source)) return null;
|
|
65
|
+
const wrapped = source.replace(HOME_RE, (full, widget) =>
|
|
66
|
+
'home: FiodosAgent(\n' +
|
|
67
|
+
" apiKey: const String.fromEnvironment('FYODOS_API_KEY'),\n" +
|
|
68
|
+
" baseUrl: const String.fromEnvironment('FYODOS_API_URL', defaultValue: kFiodosDefaultApiUrl),\n" +
|
|
69
|
+
` child: ${widget},\n` +
|
|
70
|
+
' )',
|
|
71
|
+
);
|
|
72
|
+
const importBlock = `${IMPORT_START}\n${SDK_IMPORT}\n${IMPORT_END}`;
|
|
73
|
+
return insertImportAfterLastImport(wrapped, importBlock);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/** Copy-paste snippet shown when we cannot safely auto-wrap. */
|
|
77
|
+
function mountSnippet() {
|
|
78
|
+
return (
|
|
79
|
+
`${SDK_IMPORT}\n\n` +
|
|
80
|
+
`// Wrap your app's home widget (inside MaterialApp) with FiodosAgent:\n` +
|
|
81
|
+
`home: FiodosAgent(\n` +
|
|
82
|
+
` apiKey: const String.fromEnvironment('FYODOS_API_KEY'),\n` +
|
|
83
|
+
` child: const MyHomePage(),\n` +
|
|
84
|
+
`),`
|
|
85
|
+
);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function printSnippet(colors, rel, reason) {
|
|
89
|
+
const { blue, cyan, dim, reset } = colors;
|
|
90
|
+
console.error(`\n${cyan}◉${reset} ${blue}Fiodos${reset} · ${dim}add the orb to ${rel || 'lib/main.dart'}${reason ? ` (${reason})` : ''}:${reset}`);
|
|
91
|
+
console.error(mountSnippet());
|
|
92
|
+
console.error(`${dim}Then re-run analyze — the mount becomes automatic once home: is a simple widget.${reset}\n`);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Auto-mount the orb into a Flutter app.
|
|
97
|
+
* @returns {{status:'added'|'already'|'printed'|'skipped', file?:string, reason?:string}}
|
|
98
|
+
*/
|
|
99
|
+
function wireFlutterOrb(appRoot, { colors = {}, noWire = false } = {}) {
|
|
100
|
+
const main = findMainDart(appRoot);
|
|
101
|
+
if (!main) {
|
|
102
|
+
printSnippet(colors, null, 'no lib/main.dart found');
|
|
103
|
+
return { status: 'printed', reason: 'no-main' };
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
let source;
|
|
107
|
+
try {
|
|
108
|
+
source = fs.readFileSync(main.abs, 'utf8');
|
|
109
|
+
} catch (e) {
|
|
110
|
+
return { status: 'skipped', reason: `read-failed: ${e.message}` };
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
if (source.includes('FiodosAgent(') || source.includes('FYODOS:ORB')) {
|
|
114
|
+
return { status: 'already', file: main.rel };
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
if (noWire) return { status: 'skipped', reason: 'no-wire' };
|
|
118
|
+
|
|
119
|
+
const wired = buildWiredMain(source);
|
|
120
|
+
if (!wired) {
|
|
121
|
+
printSnippet(colors, main.rel, 'home: is not a simple widget');
|
|
122
|
+
return { status: 'printed', file: main.rel, reason: 'unrecognized-home' };
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
try {
|
|
126
|
+
fs.writeFileSync(main.abs, wired);
|
|
127
|
+
} catch (e) {
|
|
128
|
+
return { status: 'skipped', reason: `write-failed: ${e.message}` };
|
|
129
|
+
}
|
|
130
|
+
return { status: 'added', file: main.rel };
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Flutter env: there is no auto-loaded `.env`. The compile-time keys are passed
|
|
135
|
+
* with --dart-define; the convention for a checked-in run command is a
|
|
136
|
+
* dart-define-from-file JSON. We write a git-ignored `fyodos.env.json` holding
|
|
137
|
+
* the publishable key + URL so `flutter run --dart-define-from-file=fyodos.env.json`
|
|
138
|
+
* works locally, and the secret never lands in committed source.
|
|
139
|
+
* @returns {{file:string, written:boolean, gitignore:object}}
|
|
140
|
+
*/
|
|
141
|
+
function writeFlutterEnv(appRoot, { apiKey, apiUrl, defaultApiUrl }) {
|
|
142
|
+
if (!apiKey) return { file: null, written: false, gitignore: null };
|
|
143
|
+
const rel = 'fyodos.env.json';
|
|
144
|
+
const abs = path.join(appRoot, rel);
|
|
145
|
+
const payload = { FYODOS_API_KEY: apiKey };
|
|
146
|
+
if (apiUrl && apiUrl !== defaultApiUrl) payload.FYODOS_API_URL = apiUrl;
|
|
147
|
+
fs.writeFileSync(abs, `${JSON.stringify(payload, null, 2)}\n`);
|
|
148
|
+
const gitignore = ensureEnvGitIgnored(appRoot, rel);
|
|
149
|
+
return { file: rel, written: true, gitignore };
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function reportFlutterWire(result, colors = {}, { quiet = false } = {}) {
|
|
153
|
+
const { blue, cyan, dim, reset } = colors;
|
|
154
|
+
if (!result) return;
|
|
155
|
+
if (result.status === 'added') {
|
|
156
|
+
console.error(`\n${cyan}◉${reset} ${blue}Fiodos${reset} · orb mounted in ${result.file}`);
|
|
157
|
+
} else if (result.status === 'already' && !quiet) {
|
|
158
|
+
console.error(`\n${cyan}◉${reset} ${blue}Fiodos${reset} · ${dim}orb already mounted in ${result.file}${reset}`);
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
module.exports = {
|
|
163
|
+
wireFlutterOrb,
|
|
164
|
+
writeFlutterEnv,
|
|
165
|
+
reportFlutterWire,
|
|
166
|
+
// exported for tests
|
|
167
|
+
buildWiredMain,
|
|
168
|
+
findMainDart,
|
|
169
|
+
HOME_RE,
|
|
170
|
+
};
|
|
@@ -0,0 +1,282 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* wireReactNative — auto-mount the Fiodos orb in an Expo Router app, mirroring
|
|
3
|
+
* the web orb auto-mount (wireWeb.js) but adapted to React Native.
|
|
4
|
+
*
|
|
5
|
+
* Web drops a one-line <FiodosAgent/> into the app entry. The RN counterpart is
|
|
6
|
+
* the SDK's <FiodosExpoAgent/> (from '@fiodos/react-native/expo'), which wires
|
|
7
|
+
* the provider + floating layer + Expo adapters + published orb design behind a
|
|
8
|
+
* single component. So here we:
|
|
9
|
+
* 1. write the published manifest to `fyodos/manifest.json`, and
|
|
10
|
+
* 2. wrap the Expo Router root layout's returned tree with <FiodosExpoAgent>.
|
|
11
|
+
*
|
|
12
|
+
* Edits are idempotent and reversible (FYODOS:ORB markers). When the layout is
|
|
13
|
+
* not in the safe, recognized shape we DON'T touch it — we print a short snippet
|
|
14
|
+
* instead (exactly like the web path), and the caller then skips the install
|
|
15
|
+
* proof so the dashboard waits for the runtime heartbeat rather than lying.
|
|
16
|
+
*/
|
|
17
|
+
'use strict';
|
|
18
|
+
|
|
19
|
+
const fs = require('fs');
|
|
20
|
+
const path = require('path');
|
|
21
|
+
|
|
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';
|
|
29
|
+
const IMPORT_START = '// FYODOS:ORB:START (auto-generated — safe to remove)';
|
|
30
|
+
const IMPORT_END = '// FYODOS:ORB:END';
|
|
31
|
+
|
|
32
|
+
const LAYOUT_CANDIDATES = [
|
|
33
|
+
'app/_layout.tsx',
|
|
34
|
+
'app/_layout.jsx',
|
|
35
|
+
'src/app/_layout.tsx',
|
|
36
|
+
'src/app/_layout.jsx',
|
|
37
|
+
];
|
|
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
|
+
|
|
56
|
+
function findRootLayout(appRoot) {
|
|
57
|
+
for (const rel of LAYOUT_CANDIDATES) {
|
|
58
|
+
const abs = path.join(appRoot, rel);
|
|
59
|
+
if (fs.existsSync(abs)) return { abs, rel };
|
|
60
|
+
}
|
|
61
|
+
return null;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** Write the published manifest the SDK mounts from. Returns the rel path. */
|
|
65
|
+
function writeManifestFile(appRoot, manifest) {
|
|
66
|
+
const dir = path.join(appRoot, 'fyodos');
|
|
67
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
68
|
+
const file = path.join(dir, 'manifest.json');
|
|
69
|
+
fs.writeFileSync(file, `${JSON.stringify(manifest, null, 2)}\n`);
|
|
70
|
+
return path.relative(appRoot, file);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/** Import path (POSIX) from the layout file to fyodos/manifest.json. */
|
|
74
|
+
function manifestImportSpecifier(layoutAbs, appRoot) {
|
|
75
|
+
const manifestAbs = path.join(appRoot, 'fyodos', 'manifest.json');
|
|
76
|
+
let rel = path.relative(path.dirname(layoutAbs), manifestAbs);
|
|
77
|
+
rel = rel.split(path.sep).join('/');
|
|
78
|
+
if (!rel.startsWith('.')) rel = `./${rel}`;
|
|
79
|
+
return rel;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function insertImportsAfterLastImport(source, block) {
|
|
83
|
+
const lines = source.split('\n');
|
|
84
|
+
let lastImport = -1;
|
|
85
|
+
for (let i = 0; i < lines.length; i += 1) {
|
|
86
|
+
if (/^\s*import\b/.test(lines[i])) lastImport = i;
|
|
87
|
+
}
|
|
88
|
+
if (lastImport === -1) return `${block}\n${source}`;
|
|
89
|
+
lines.splice(lastImport + 1, 0, block);
|
|
90
|
+
return lines.join('\n');
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Find the parenthesized expression of the FIRST `return (` and return its inner
|
|
95
|
+
* span [start,end) (exclusive of the wrapping parens). null when the return is
|
|
96
|
+
* not the safe `return ( … )` shape we auto-wrap.
|
|
97
|
+
*/
|
|
98
|
+
function findReturnParenSpan(source) {
|
|
99
|
+
const m = /return\s*\(/.exec(source);
|
|
100
|
+
if (!m) return null;
|
|
101
|
+
const open = m.index + m[0].length - 1; // index of '('
|
|
102
|
+
let depth = 0;
|
|
103
|
+
for (let i = open; i < source.length; i += 1) {
|
|
104
|
+
const ch = source[i];
|
|
105
|
+
if (ch === '(') depth += 1;
|
|
106
|
+
else if (ch === ')') {
|
|
107
|
+
depth -= 1;
|
|
108
|
+
if (depth === 0) return { innerStart: open + 1, innerEnd: i, openIdx: open, closeIdx: i };
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
return null;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/** Detect indentation (spaces) of the line containing index. */
|
|
115
|
+
function indentAt(source, index) {
|
|
116
|
+
const lineStart = source.lastIndexOf('\n', index - 1) + 1;
|
|
117
|
+
const m = /^[ \t]*/.exec(source.slice(lineStart));
|
|
118
|
+
return m ? m[0] : '';
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Produce the wired layout source, or null when the layout is not auto-wrappable.
|
|
123
|
+
*/
|
|
124
|
+
function buildWiredLayout(source, manifestSpecifier) {
|
|
125
|
+
const span = findReturnParenSpan(source);
|
|
126
|
+
if (!span) return null;
|
|
127
|
+
const inner = source.slice(span.innerStart, span.innerEnd);
|
|
128
|
+
const baseIndent = indentAt(source, span.openIdx);
|
|
129
|
+
const innerIndent = `${baseIndent} `;
|
|
130
|
+
|
|
131
|
+
// Re-indent the existing tree one level deeper so it nests cleanly.
|
|
132
|
+
const nested = inner
|
|
133
|
+
.replace(/^\n/, '')
|
|
134
|
+
.split('\n')
|
|
135
|
+
.map((l) => (l.trim() ? ` ${l}` : l))
|
|
136
|
+
.join('\n');
|
|
137
|
+
|
|
138
|
+
const wrapped =
|
|
139
|
+
`\n${innerIndent}${ORB_START}\n` +
|
|
140
|
+
`${innerIndent}<FiodosExpoAgent manifest={fyodosManifest as never}>\n` +
|
|
141
|
+
`${nested}\n` +
|
|
142
|
+
`${innerIndent}</FiodosExpoAgent>\n` +
|
|
143
|
+
`${innerIndent}${ORB_END}\n${baseIndent}`;
|
|
144
|
+
|
|
145
|
+
const withWrap = source.slice(0, span.innerStart) + wrapped + source.slice(span.innerEnd);
|
|
146
|
+
|
|
147
|
+
const importBlock =
|
|
148
|
+
`${IMPORT_START}\n` +
|
|
149
|
+
`import { FiodosExpoAgent } from '@fiodos/react-native/expo';\n` +
|
|
150
|
+
`import fyodosManifest from '${manifestSpecifier}';\n` +
|
|
151
|
+
`${IMPORT_END}`;
|
|
152
|
+
|
|
153
|
+
return insertImportsAfterLastImport(withWrap, importBlock);
|
|
154
|
+
}
|
|
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
|
+
|
|
179
|
+
/** The copy-paste snippet shown when we can't safely auto-wrap. */
|
|
180
|
+
function mountSnippet(manifestSpecifier) {
|
|
181
|
+
return (
|
|
182
|
+
`import { FiodosExpoAgent } from '@fiodos/react-native/expo';\n` +
|
|
183
|
+
`import fyodosManifest from '${manifestSpecifier}';\n\n` +
|
|
184
|
+
`// Wrap your root layout's returned tree:\n` +
|
|
185
|
+
`<FiodosExpoAgent manifest={fyodosManifest as never}>\n` +
|
|
186
|
+
` {/* your existing <Stack/> / <Slot/> tree */}\n` +
|
|
187
|
+
`</FiodosExpoAgent>`
|
|
188
|
+
);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
function printSnippet(colors, layoutRel, manifestSpecifier, reason) {
|
|
192
|
+
const { blue, cyan, dim, reset } = colors;
|
|
193
|
+
console.error(`\n${cyan}◉${reset} ${blue}Fiodos${reset} · ${dim}add the orb to ${layoutRel || 'app/_layout.tsx'}${reason ? ` (${reason})` : ''}:${reset}`);
|
|
194
|
+
console.error(mountSnippet(manifestSpecifier));
|
|
195
|
+
console.error(`${dim}Then re-run analyze — the mount becomes automatic once the layout matches.${reset}\n`);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* Auto-mount the orb into an Expo Router app.
|
|
200
|
+
* @returns {{status:'added'|'already'|'printed'|'skipped', file?:string, reason?:string}}
|
|
201
|
+
*/
|
|
202
|
+
function wireReactNativeOrb(appRoot, { manifest, colors = {}, noWire = false } = {}) {
|
|
203
|
+
const layout = findRootLayout(appRoot);
|
|
204
|
+
const manifestSpecifier = layout
|
|
205
|
+
? manifestImportSpecifier(layout.abs, appRoot)
|
|
206
|
+
: './fyodos/manifest.json';
|
|
207
|
+
|
|
208
|
+
if (!layout) {
|
|
209
|
+
if (!noWire && manifest) writeManifestFile(appRoot, manifest);
|
|
210
|
+
printSnippet(colors, null, manifestSpecifier, 'no Expo Router app/_layout found');
|
|
211
|
+
return { status: 'printed', reason: 'no-layout' };
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
let source;
|
|
215
|
+
try {
|
|
216
|
+
source = fs.readFileSync(layout.abs, 'utf8');
|
|
217
|
+
} catch (e) {
|
|
218
|
+
return { status: 'skipped', reason: `read-failed: ${e.message}` };
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
if (source.includes('FiodosExpoAgent') || source.includes('FYODOS:ORB')) {
|
|
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
|
+
}
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
if (noWire) return { status: 'skipped', reason: 'no-wire' };
|
|
238
|
+
|
|
239
|
+
if (manifest) writeManifestFile(appRoot, manifest);
|
|
240
|
+
const wired = buildWiredLayout(source, manifestSpecifier);
|
|
241
|
+
if (!wired) {
|
|
242
|
+
printSnippet(colors, layout.rel, manifestSpecifier, "layout has no plain `return ( … )`");
|
|
243
|
+
return { status: 'printed', file: layout.rel, reason: 'unrecognized-layout' };
|
|
244
|
+
}
|
|
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
|
+
|
|
253
|
+
try {
|
|
254
|
+
fs.writeFileSync(layout.abs, wired);
|
|
255
|
+
} catch (e) {
|
|
256
|
+
return { status: 'skipped', reason: `write-failed: ${e.message}` };
|
|
257
|
+
}
|
|
258
|
+
return { status: 'added', file: layout.rel };
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
function reportReactNativeWire(result, colors = {}, { quiet = false } = {}) {
|
|
262
|
+
const { blue, cyan, dim, reset } = colors;
|
|
263
|
+
if (!result) return;
|
|
264
|
+
if (result.status === 'added') {
|
|
265
|
+
console.error(`\n${cyan}◉${reset} ${blue}Fiodos${reset} · orb mounted in ${result.file}`);
|
|
266
|
+
} else if (result.status === 'already' && !quiet) {
|
|
267
|
+
console.error(`\n${cyan}◉${reset} ${blue}Fiodos${reset} · ${dim}orb already mounted in ${result.file}${reset}`);
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
module.exports = {
|
|
272
|
+
wireReactNativeOrb,
|
|
273
|
+
reportReactNativeWire,
|
|
274
|
+
// exported for tests
|
|
275
|
+
buildWiredLayout,
|
|
276
|
+
unwireOrb,
|
|
277
|
+
parsesAsTsx,
|
|
278
|
+
findReturnParenSpan,
|
|
279
|
+
writeManifestFile,
|
|
280
|
+
manifestImportSpecifier,
|
|
281
|
+
findRootLayout,
|
|
282
|
+
};
|
package/src/writeEnv.js
CHANGED
|
@@ -60,6 +60,18 @@ function resolveEnvPlan(appRoot) {
|
|
|
60
60
|
if (deps['@angular/core'] || has('angular.json')) {
|
|
61
61
|
return { framework: 'angular', file: null, candidates: [], keyVar: null, urlVar: null };
|
|
62
62
|
}
|
|
63
|
+
// Expo / React Native: the orb reads EXPO_PUBLIC_* (Expo inlines these at
|
|
64
|
+
// build time and exposes them via process.env). A plain `.env` at the app root
|
|
65
|
+
// is the convention; we still ensure it is git-ignored like any secret file.
|
|
66
|
+
if (deps.expo || deps['expo-router'] || deps['react-native']) {
|
|
67
|
+
return {
|
|
68
|
+
framework: 'expo',
|
|
69
|
+
file: '.env',
|
|
70
|
+
candidates: ['.env.local', '.env.development.local', '.env.development', '.env'],
|
|
71
|
+
keyVar: 'EXPO_PUBLIC_FYODOS_API_KEY',
|
|
72
|
+
urlVar: 'EXPO_PUBLIC_FYODOS_API_URL',
|
|
73
|
+
};
|
|
74
|
+
}
|
|
63
75
|
if (
|
|
64
76
|
deps['@sveltejs/kit'] ||
|
|
65
77
|
deps.svelte ||
|