-
Notifications
You must be signed in to change notification settings - Fork 19
Add: github workflow for maintaining structure and build checks #11
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,35 @@ | ||||||||||||||
| name: Flutter CI | ||||||||||||||
|
|
||||||||||||||
| on: | ||||||||||||||
| push: | ||||||||||||||
| branches: [ main ] | ||||||||||||||
| pull_request: | ||||||||||||||
| branches: [ main ] | ||||||||||||||
|
|
||||||||||||||
| jobs: | ||||||||||||||
| build: | ||||||||||||||
| runs-on: ubuntu-latest | ||||||||||||||
|
|
||||||||||||||
| steps: | ||||||||||||||
| - name: Checkout repository | ||||||||||||||
| uses: actions/checkout@v3 | ||||||||||||||
|
|
||||||||||||||
|
Comment on lines
+15
to
+16
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Upgrade checkout to v4 (v3 is deprecated and flagged by actionlint). actionlint warns that actions/checkout@v3 is too old for current runners. Move to v4 to avoid workflow failures and to get Node 20 compatibility. Apply this diff: - - name: Checkout repository
- uses: actions/checkout@v3
+ - name: Checkout repository
+ uses: actions/checkout@v4
+ with:
+ fetch-depth: 0📝 Committable suggestion
Suggested change
🧰 Tools🪛 actionlint (1.7.7)15-15: the runner of "actions/checkout@v3" action is too old to run on GitHub Actions. update the action's version to fix this issue (action) 🤖 Prompt for AI Agents |
||||||||||||||
| - name: Setup environment file from template | ||||||||||||||
| run: cp .env.stencil .env | ||||||||||||||
|
|
||||||||||||||
| - name: Setup Flutter | ||||||||||||||
| uses: subosito/flutter-action@v2 | ||||||||||||||
| with: | ||||||||||||||
| channel: 'stable' | ||||||||||||||
|
|
||||||||||||||
| - name: Install dependencies | ||||||||||||||
| run: flutter pub get | ||||||||||||||
|
|
||||||||||||||
| - name: Dart analyze | ||||||||||||||
| run: flutter analyze | ||||||||||||||
|
|
||||||||||||||
|
Comment on lines
+28
to
+30
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 💡 Verification agent 🧩 Analysis chainAdd unit tests to CI (if present). Running tests is a core CI signal and catches regressions beyond static analysis. Proposed step: - name: Dart analyze
run: flutter analyze
+
+ - name: Flutter tests
+ run: flutter test -r expandedTo verify whether this repo has tests before adding the step, run: 🏁 Script executed: #!/bin/bash
# List *_test.dart files with context
rg -nP --type=dart -C2 '_test\.dart$' || trueLength of output: 46 🏁 Script executed: #!/bin/bash
# Find directories named test or tests
find . -type d \( -name test -o -name tests \) || true
# List Dart test files by suffix
rg -n --type=dart '_test\.dart$' || true
# Search for test import statements
rg -n --type=dart 'import .+test\.dart' || true
# Search for any test function calls
rg -n --type=dart 'test\(' || true
# Search specifically for Flutter widget tests
rg -n --type=dart 'testWidgets' || trueLength of output: 384 Add Flutter test step to CI workflow Tests already exist in test/widget_test.dart (using testWidgets), so let’s surface them in CI. Update .github/workflows/flutter.yaml as follows: • File: .github/workflows/flutter.yaml - name: Dart analyze
run: flutter analyze
+ - name: Run Flutter tests
+ run: flutter test -r expanded📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||
| - name: Check formatting | ||||||||||||||
| run: dart format --output=none --set-exit-if-changed . | ||||||||||||||
|
|
||||||||||||||
| - name: Flutter build (apk) | ||||||||||||||
| run: flutter build apk --release --no-tree-shake-icons | grep -v "deprecated" | ||||||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -33,7 +33,7 @@ class UniversalNavbar extends StatelessWidget implements PreferredSizeWidget { | |
| bottom: 0, | ||
| left: 0, | ||
| right: 0, | ||
| child: Container( | ||
| child: SizedBox( | ||
| height: 40, | ||
| child: _buildPlantIllustrations(), | ||
| ), | ||
|
|
@@ -54,7 +54,7 @@ class UniversalNavbar extends StatelessWidget implements PreferredSizeWidget { | |
| color: Colors.white, | ||
| borderRadius: BorderRadius.circular(8), | ||
| border: Border.all( | ||
| color: Colors.white.withOpacity(0.3), | ||
| color: Colors.white, | ||
| width: 1, | ||
| ), | ||
| ), | ||
|
|
@@ -151,14 +151,13 @@ class UniversalNavbar extends StatelessWidget implements PreferredSizeWidget { | |
| Widget _buildPlantIllustrations() { | ||
| return Container( | ||
| decoration: BoxDecoration( | ||
| color: const Color.fromARGB(255, 251, 251, 99) | ||
| .withOpacity(0.9), // Beige background | ||
| color: const Color.fromARGB(255, 251, 251, 99), | ||
| borderRadius: const BorderRadius.only( | ||
| topLeft: Radius.circular(40), | ||
| topRight: Radius.circular(40), | ||
| ), | ||
| border: Border.all( | ||
| color: Colors.black.withOpacity(0.2), | ||
| color: Colors.black, | ||
| width: 1, | ||
| ), | ||
| ), | ||
|
|
@@ -183,7 +182,7 @@ class UniversalNavbar extends StatelessWidget implements PreferredSizeWidget { | |
| child: Row( | ||
| mainAxisAlignment: MainAxisAlignment.spaceEvenly, | ||
| children: treeImages.map((imagePath) { | ||
| return Container( | ||
| return SizedBox( | ||
| width: plantWidth, | ||
| height: plantWidth, | ||
| child: Image.asset( | ||
|
|
@@ -241,15 +240,15 @@ class UniversalNavbar extends StatelessWidget implements PreferredSizeWidget { | |
| constraints: const BoxConstraints(maxWidth: 100, minHeight: 20), | ||
| // Limit max width | ||
| decoration: BoxDecoration( | ||
| color: Colors.white.withOpacity(0.95), | ||
| color: Colors.white, | ||
| borderRadius: BorderRadius.circular(16), | ||
| border: Border.all( | ||
| color: Colors.green.withOpacity(0.3), | ||
| color: Colors.green, | ||
| width: 1, | ||
| ), | ||
| boxShadow: [ | ||
| BoxShadow( | ||
| color: Colors.black.withOpacity(0.1), | ||
| color: Colors.black, | ||
| blurRadius: 4, | ||
| offset: const Offset(0, 2), | ||
| ), | ||
|
|
@@ -267,7 +266,7 @@ class UniversalNavbar extends StatelessWidget implements PreferredSizeWidget { | |
| color: Colors.green[700], | ||
| ), | ||
| const SizedBox(width: 4), | ||
| Container( | ||
| SizedBox( | ||
| width: 10, | ||
| child: Flexible( | ||
| child: Text( | ||
|
Comment on lines
+269
to
272
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Critical: Flexible cannot be a child of SizedBox (will throw “Incorrect use of ParentDataWidget”)
Fix by removing - SizedBox(
- width: 10,
- child: Flexible(
- child: Text(
+ Flexible(
+ child: Text(
formatAddress(walletProvider.currentAddress!),
style: TextStyle(
color: Colors.green[700],
fontWeight: FontWeight.w600,
fontSize: 10,
),
overflow: TextOverflow.ellipsis,
- ),
- ),
- ),
+ ),
+ ),If you want a fixed cap, you can alternatively use: // Outside selected range (illustrative)
SizedBox(
width: 100,
child: Text(formatAddress(...), overflow: TextOverflow.ellipsis, style: ...),
)🤖 Prompt for AI Agents
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @IronJam11 Please look into this, check if there are any errors in running of this widget There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
|
|
@@ -366,15 +365,15 @@ class UniversalNavbar extends StatelessWidget implements PreferredSizeWidget { | |
| return Container( | ||
| constraints: const BoxConstraints(maxWidth: 80), // Limit max width | ||
| decoration: BoxDecoration( | ||
| color: Colors.white.withOpacity(0.95), | ||
| color: Colors.white, | ||
| borderRadius: BorderRadius.circular(16), | ||
| border: Border.all( | ||
| color: Colors.green.withOpacity(0.3), | ||
| color: Colors.green, | ||
| width: 1, | ||
| ), | ||
| boxShadow: [ | ||
| BoxShadow( | ||
| color: Colors.black.withOpacity(0.1), | ||
| color: Colors.black, | ||
| blurRadius: 4, | ||
| offset: const Offset(0, 2), | ||
| ), | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -40,9 +40,12 @@ class WalletConnectDialog extends StatelessWidget { | |
| onPressed: () async { | ||
| try { | ||
| await walletProvider.openWallet(wallet, uri); | ||
| // ignore: use_build_context_synchronously | ||
| Navigator.of(context).pop(); | ||
| } catch (e) { | ||
| // ignore: use_build_context_synchronously | ||
| Navigator.of(context).pop(); | ||
| // ignore: use_build_context_synchronously | ||
| ScaffoldMessenger.of(context).showSnackBar( | ||
|
Comment on lines
+43
to
49
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 💡 Verification agent ❓ Verification inconclusiveDon’t suppress Ignoring the lint risks using a disposed context after Apply this diff: onPressed: () async {
try {
await walletProvider.openWallet(wallet, uri);
- // ignore: use_build_context_synchronously
- Navigator.of(context).pop();
+ if (!context.mounted) return;
+ Navigator.of(context).pop();
} catch (e) {
- // ignore: use_build_context_synchronously
- Navigator.of(context).pop();
- // ignore: use_build_context_synchronously
- ScaffoldMessenger.of(context).showSnackBar(
+ if (!context.mounted) return;
+ Navigator.of(context).pop();
+ ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(e.toString()),
backgroundColor: Colors.red,
),
);
}
},
@@
onPressed: () async {
await Clipboard.setData(ClipboardData(text: uri));
- // ignore: use_build_context_synchronously
- Navigator.of(context).pop();
- // ignore: use_build_context_synchronously
- ScaffoldMessenger.of(context).showSnackBar(
+ if (!context.mounted) return;
+ Navigator.of(context).pop();
+ ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('URI copied to clipboard!'),
backgroundColor: Colors.green,
),
);
},If you support older Flutter without Also applies to: 82-89 Guard against disposed BuildContext instead of suppressing the lint Ignoring • Affected locations: For Flutter ≥ 3.7 (with onPressed: () async {
try {
await walletProvider.openWallet(wallet, uri);
- // ignore: use_build_context_synchronously
- Navigator.of(context).pop();
+ // avoid using a disposed context
+ if (!context.mounted) return;
+ Navigator.of(context).pop();
} catch (e) {
- // ignore: use_build_context_synchronously
- Navigator.of(context).pop();
- // ignore: use_build_context_synchronously
- ScaffoldMessenger.of(context).showSnackBar(
+ if (!context.mounted) return;
+ Navigator.of(context).pop();
+ ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(e.toString()),
backgroundColor: Colors.red,
),
);
}
},
…
onPressed: () async {
await Clipboard.setData(ClipboardData(text: uri));
- // ignore: use_build_context_synchronously
- Navigator.of(context).pop();
- // ignore: use_build_context_synchronously
- ScaffoldMessenger.of(context).showSnackBar(
+ if (!context.mounted) return;
+ Navigator.of(context).pop();
+ ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('URI copied to clipboard!'),
backgroundColor: Colors.green,
),
);
},Fallback for pre-3.7 Flutter (no onPressed: () async {
final nav = Navigator.of(context);
final messenger = ScaffoldMessenger.of(context);
try {
await walletProvider.openWallet(wallet, uri);
nav.pop();
} catch (e) {
nav.pop();
messenger.showSnackBar(
SnackBar(
content: Text(e.toString()),
backgroundColor: Colors.red,
),
);
}
},
…
onPressed: () async {
final nav = Navigator.of(context);
final messenger = ScaffoldMessenger.of(context);
await Clipboard.setData(ClipboardData(text: uri));
nav.pop();
messenger.showSnackBar(
const SnackBar(
content: Text('URI copied to clipboard!'),
backgroundColor: Colors.green,
),
);
},🤖 Prompt for AI Agents |
||
| SnackBar( | ||
| content: Text(e.toString()), | ||
|
|
@@ -75,7 +78,9 @@ class WalletConnectDialog extends StatelessWidget { | |
| child: OutlinedButton.icon( | ||
| onPressed: () async { | ||
| await Clipboard.setData(ClipboardData(text: uri)); | ||
| // ignore: use_build_context_synchronously | ||
| Navigator.of(context).pop(); | ||
| // ignore: use_build_context_synchronously | ||
| ScaffoldMessenger.of(context).showSnackBar( | ||
| const SnackBar( | ||
| content: Text('URI copied to clipboard!'), | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,7 +1,7 @@ | ||
| import 'package:flutter/material.dart'; | ||
| import 'package:flutter_dotenv/flutter_dotenv.dart'; | ||
|
|
||
| final String ALCHEMY_API_KEY = dotenv.env['ALCHEMY_API_KEY'] ?? ''; | ||
| final String alchemyApiKey = dotenv.env['ALCHEMY_API_KEY'] ?? ''; | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Top-level dotenv read at import time can initialize to empty before dotenv.load() runs. Dart evaluates top-level initializers before Apply this diff to make the API key lookup lazy (safe even if -import 'package:flutter/material.dart';
-import 'package:flutter_dotenv/flutter_dotenv.dart';
-
-final String alchemyApiKey = dotenv.env['ALCHEMY_API_KEY'] ?? '';
+import 'package:flutter/material.dart';
+import 'package:flutter_dotenv/flutter_dotenv.dart';
+
+String _alchemyApiKey() => dotenv.maybeGet('ALCHEMY_API_KEY') ?? '';And convert -final Map<String, String> rpcUrls = {
- '11155111': 'https://eth-sepolia.g.alchemy.com/v2/$alchemyApiKey',
- '1': 'https://eth-mainnet.g.alchemy.com/v2/$alchemyApiKey',
-};
+Map<String, String> get rpcUrls => {
+ '11155111': 'https://eth-sepolia.g.alchemy.com/v2/${_alchemyApiKey()}',
+ '1': 'https://eth-mainnet.g.alchemy.com/v2/${_alchemyApiKey()}',
+};
@@
-final Map<String, Map<String, dynamic>> chainInfoList = {
+Map<String, Map<String, dynamic>> get chainInfoList => {
'1': {
'name': 'Ethereum Mainnet',
- 'rpcUrl': 'https://eth-mainnet.g.alchemy.com/v2/$alchemyApiKey',
+ 'rpcUrl': 'https://eth-mainnet.g.alchemy.com/v2/${_alchemyApiKey()}',
@@
'11155111': {
'name': 'Sepolia Testnet',
- 'rpcUrl': 'https://eth-sepolia.g.alchemy.com/v2/$alchemyApiKey',
+ 'rpcUrl': 'https://eth-sepolia.g.alchemy.com/v2/${_alchemyApiKey()}',Optionally, fail fast in debug if the key is missing where you first consume these maps. 🤖 Prompt for AI Agents |
||
|
|
||
| class WalletOption { | ||
| final String name; | ||
|
|
@@ -30,14 +30,14 @@ final List<WalletOption> walletOptionsList = [ | |
| ]; | ||
|
|
||
| final Map<String, String> rpcUrls = { | ||
| '11155111': 'https://eth-sepolia.g.alchemy.com/v2/$ALCHEMY_API_KEY', | ||
| '1': 'https://eth-mainnet.g.alchemy.com/v2/$ALCHEMY_API_KEY', | ||
| '11155111': 'https://eth-sepolia.g.alchemy.com/v2/$alchemyApiKey', | ||
| '1': 'https://eth-mainnet.g.alchemy.com/v2/$alchemyApiKey', | ||
| }; | ||
|
|
||
| final Map<String, Map<String, dynamic>> chainInfoList = { | ||
| '1': { | ||
| 'name': 'Ethereum Mainnet', | ||
| 'rpcUrl': 'https://eth-mainnet.g.alchemy.com/v2/$ALCHEMY_API_KEY', | ||
| 'rpcUrl': 'https://eth-mainnet.g.alchemy.com/v2/$alchemyApiKey', | ||
| 'nativeCurrency': { | ||
| 'name': 'Ether', | ||
| 'symbol': 'ETH', | ||
|
|
@@ -47,7 +47,7 @@ final Map<String, Map<String, dynamic>> chainInfoList = { | |
| }, | ||
| '11155111': { | ||
| 'name': 'Sepolia Testnet', | ||
| 'rpcUrl': 'https://eth-sepolia.g.alchemy.com/v2/$ALCHEMY_API_KEY', | ||
| 'rpcUrl': 'https://eth-sepolia.g.alchemy.com/v2/$alchemyApiKey', | ||
| 'nativeCurrency': { | ||
| 'name': 'Sepolia Ether', | ||
| 'symbol': 'SEP', | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -42,12 +42,15 @@ class _MultipleImageUploadPageState extends State<MultipleImageUploadPage> { | |
| if (images.isEmpty) return; | ||
|
|
||
| logger.d('Selected ${images.length} images for upload'); | ||
|
|
||
| // ignore: use_build_context_synchronously | ||
| final provider = Provider.of<MintNftProvider>(context, listen: false); | ||
|
|
||
|
Comment on lines
+46
to
+48
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🛠️ Refactor suggestion Stop ignoring use_build_context_synchronously; capture provider before awaiting and add mounted guards Using context after an await can crash if the widget unmounts meanwhile. Don’t suppress the lint; fix the flow by capturing the provider before the first await and guarding UI updates with mounted. Apply this small in-range cleanup first: - // ignore: use_build_context_synchronously
- final provider = Provider.of<MintNftProvider>(context, listen: false);
+ // Provider captured earlier (see revised function below).Then revise the function to safely capture the provider and avoid post-await context usage: Future<void> _pickAndUploadImages() async {
// capture before any await
final provider = context.read<MintNftProvider>();
try {
final List<XFile> images = await _picker.pickMultiImage();
if (images.isEmpty) return;
if (!mounted) return;
setState(() {
_processingImages = images.map((image) => File(image.path)).toList();
_isUploading = true;
});
final List<String> newHashes = [];
for (int i = 0; i < images.length; i++) {
if (!mounted) return;
setState(() => _uploadingIndex = i);
try {
final File imageFile = File(images[i].path);
final String? hash = await uploadToIPFS(imageFile, (isUploading) {});
if (!mounted) return;
if (hash != null) {
newHashes.add(hash);
setState(() => _uploadedHashes.add(hash));
logger.d('Successfully uploaded image ${i + 1}: $hash');
} else {
_showSnackBar('Failed to upload image ${i + 1}');
}
} catch (e) {
logger.e('Error uploading image ${i + 1}: $e');
if (mounted) _showSnackBar('Error uploading image ${i + 1}: $e');
}
}
if (!mounted) return;
setState(() {
_isUploading = false;
_uploadingIndex = -1;
_processingImages.clear();
});
provider.setInitialPhotos(_uploadedHashes);
if (newHashes.isNotEmpty && mounted) {
_showSnackBar('Successfully uploaded ${newHashes.length} images');
}
} catch (e) {
if (!mounted) return;
setState(() {
_isUploading = false;
_uploadingIndex = -1;
_processingImages.clear();
});
if (mounted) _showSnackBar('Error selecting images: $e');
}
}Note: context.read() is equivalent to Provider.of(context, listen: false) but clearer. 🤖 Prompt for AI Agents |
||
| setState(() { | ||
| _processingImages = images.map((image) => File(image.path)).toList(); | ||
| _isUploading = true; | ||
| }); | ||
|
Comment on lines
49
to
52
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🛠️ Refactor suggestion Guard setState/snackbars with mounted after awaits There are multiple UI updates that can run after awaited calls; add mounted checks to prevent setState on a disposed State and avoid using a stale context for SnackBars. Minimal inline guards you can apply: @@
- setState(() {
+ if (!mounted) return;
+ setState(() {
_processingImages = images.map((image) => File(image.path)).toList();
_isUploading = true;
});
@@
- setState(() {
+ if (!mounted) return;
+ setState(() {
_uploadingIndex = i;
});
@@
- setState(() {
+ if (!mounted) return;
+ setState(() {
_uploadedHashes.add(hash);
});
@@
- setState(() {
+ if (!mounted) return;
+ setState(() {
_isUploading = false;
_uploadingIndex = -1;
_processingImages.clear();
});
@@
- setState(() {
+ if (!mounted) return;
+ setState(() {
_isUploading = false;
_uploadingIndex = -1;
_processingImages.clear();
});
- _showSnackBar('Error selecting images: $e');
+ if (mounted) _showSnackBar('Error selecting images: $e');Also applies to: 57-59, 67-69, 80-85, 91-97 🤖 Prompt for AI Agents |
||
|
|
||
| final provider = Provider.of<MintNftProvider>(context, listen: false); | ||
| List<String> newHashes = []; | ||
|
|
||
| for (int i = 0; i < images.length; i++) { | ||
|
|
@@ -252,7 +255,7 @@ class _MultipleImageUploadPageState extends State<MultipleImageUploadPage> { | |
| width: 120, | ||
| height: 120, | ||
| decoration: BoxDecoration( | ||
| color: Colors.green.withOpacity(0.8), | ||
| color: Colors.green, | ||
| borderRadius: | ||
| BorderRadius.circular(8), | ||
| ), | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
💡 Verification agent
🧩 Analysis chain
Verify APPLICATION_ID usage and docs
Confirm that APPLICATION_ID is actually read by the app and mentioned in README/Getting Started so newcomers don’t miss it.
Run:
🏁 Script executed:
Length of output: 160
🏁 Script executed:
Length of output: 6796
🏁 Script executed:
Length of output: 1177
Add and document APPLICATION_ID environment support
It looks like
APPLICATION_IDis declared in.env.stencilbut never actually read by the Flutter code, and the README has no mention of it. To avoid confusion for newcomers, please:Load the variable in Dart
In
lib/main.dart, afterawait dotenv.load(...), add something like:so that your app can access the value at runtime.
Pass it through to your Linux runner
Modify
linux/CMakeLists.txtto use the environment value (e.g. via$ENV{APPLICATION_ID}) or otherwise propagateapplicationIdinto the desktop build.Remove or update
.env.stencilIf you’re not actually using
APPLICATION_IDat runtime, remove it from.env.stencil. Otherwise, keep it and document it fully.Document in README.md
Under “Getting Started,” add steps to:
.env.stencilto.envAPPLICATION_ID=<your.app.id>This ensures the environment variable is both used and clearly documented for all platforms.
🧰 Tools
🪛 dotenv-linter (3.3.0)
[warning] 6-6: [EndingBlankLine] No blank line at the end of the file
(EndingBlankLine)
[warning] 6-6: [UnorderedKey] The APPLICATION_ID key should go before the CONTRACT_ADDRESS key
(UnorderedKey)
🤖 Prompt for AI Agents