mirror of
https://github.com/zapstore/zapstore.git
synced 2026-10-05 12:38:24 +00:00
Removes tools folder, purplestack mcp and related files.
This commit is contained in:
+14
-30
@@ -51,7 +51,7 @@ packages:
|
||||
source: hosted
|
||||
version: "1.0.4"
|
||||
archive:
|
||||
dependency: "direct dev"
|
||||
dependency: transitive
|
||||
description:
|
||||
name: archive
|
||||
sha256: "2fde1607386ab523f7a36bb3e7edb43bd58e6edaf2ffb29d8a6d578b297fdbbd"
|
||||
@@ -59,7 +59,7 @@ packages:
|
||||
source: hosted
|
||||
version: "4.0.7"
|
||||
args:
|
||||
dependency: "direct dev"
|
||||
dependency: transitive
|
||||
description:
|
||||
name: args
|
||||
sha256: d0481093c50b1da8910eb0bb301626d4d8eb7284aa739614d2b394ee09e3ea04
|
||||
@@ -114,14 +114,6 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.3.0"
|
||||
bm25:
|
||||
dependency: "direct dev"
|
||||
description:
|
||||
name: bm25
|
||||
sha256: "75f9f627fcdb884db420510d34906fb5e7ec7caef9d9cbdc35be17317b637f7b"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.2.3"
|
||||
boolean_selector:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -589,26 +581,26 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: leak_tracker
|
||||
sha256: "6bb818ecbdffe216e81182c2f0714a2e62b593f4a4f13098713ff1685dfb6ab0"
|
||||
sha256: "33e2e26bdd85a0112ec15400c8cbffea70d0f9c3407491f672a2fad47915e2de"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "10.0.9"
|
||||
version: "11.0.2"
|
||||
leak_tracker_flutter_testing:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: leak_tracker_flutter_testing
|
||||
sha256: f8b613e7e6a13ec79cfdc0e97638fddb3ab848452eff057653abd3edba760573
|
||||
sha256: "1dbc140bb5a23c75ea9c4811222756104fbcd1a27173f0c34ca01e16bea473c1"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.0.9"
|
||||
version: "3.0.10"
|
||||
leak_tracker_testing:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: leak_tracker_testing
|
||||
sha256: "6ba465d5d76e67ddf503e1161d1f4a6bc42306f9d66ca1e8f079a47290fb06d3"
|
||||
sha256: "8d5a2d49f4a66b49744b23b018848400d23e54caf9463f4eb20df3eb8acb2eb1"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.0.1"
|
||||
version: "3.0.2"
|
||||
lints:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -657,22 +649,14 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.11.1"
|
||||
mcp_dart:
|
||||
dependency: "direct dev"
|
||||
description:
|
||||
name: mcp_dart
|
||||
sha256: "4bc05574bc8bda0ae779e177ab5639f580b37059b5cb1248288b7a79979dd5e6"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.5.3"
|
||||
meta:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: meta
|
||||
sha256: e3641ec5d63ebf0d9b41bd43201a66e3fc79a65db5f61fc181f04cd27aab950c
|
||||
sha256: "23f08335362185a5ea2ad3a4e597f1375e78bce8a040df5c600c8d3552ef2394"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.16.0"
|
||||
version: "1.17.0"
|
||||
mime:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -1157,10 +1141,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: test_api
|
||||
sha256: fb31f383e2ee25fbbfe06b40fe21e1e458d14080e3c67e7ba0acfde4df4e0bbd
|
||||
sha256: ab2726c1a94d3176a45960b6234466ec367179b87dd74f1611adb1f3b5fb9d55
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.7.4"
|
||||
version: "0.7.7"
|
||||
timezone:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -1277,10 +1261,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: vector_math
|
||||
sha256: "80b3257d1492ce4d091729e3a67a60407d227c27241d6927be0130c98e741803"
|
||||
sha256: d530bd74fea330e6e364cda7a85019c434070188383e1cd8d9777ee586914c5b
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.1.4"
|
||||
version: "2.2.0"
|
||||
vm_service:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
||||
@@ -83,10 +83,6 @@ dev_dependencies:
|
||||
# package. See that file for information about deactivating specific lint
|
||||
# rules and activating additional ones.
|
||||
flutter_lints: ^5.0.0
|
||||
mcp_dart: ^0.5.3
|
||||
bm25: ^2.2.3
|
||||
archive: ^4.0.7
|
||||
args: ^2.7.0
|
||||
flutter_launcher_icons: ^0.14.4
|
||||
|
||||
# For information on the generic Dart part of this file, see the
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,29 +0,0 @@
|
||||
{
|
||||
"model": "claude-sonnet-4",
|
||||
"temperature": 0.2,
|
||||
"mcpServers": {
|
||||
"purplestack": {
|
||||
"type": "stdio",
|
||||
"command": "bash",
|
||||
"args": [
|
||||
"tools/scripts/run-mcp.sh"
|
||||
]
|
||||
},
|
||||
"nostr": {
|
||||
"type": "stdio",
|
||||
"command": "npx",
|
||||
"args": [
|
||||
"-y",
|
||||
"@nostrbook/mcp@latest"
|
||||
]
|
||||
},
|
||||
"developer": {
|
||||
"type": "stdio",
|
||||
"command": "npx",
|
||||
"args": [
|
||||
"-y",
|
||||
"@soapbox.pub/developer-mcp@latest"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
Binary file not shown.
@@ -1,15 +0,0 @@
|
||||
#!/bin/bash
|
||||
set -e
|
||||
|
||||
# Get the directory where this script is located
|
||||
SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
|
||||
|
||||
echo "Generating MCP content..."
|
||||
cd "$SCRIPT_DIR/../../../../purplebase/purplestack-context"
|
||||
./generate-content.sh
|
||||
|
||||
echo "Copying MCP content to project..."
|
||||
cp "$SCRIPT_DIR/../../../../purplebase/purplestack/tools/content/mcp-content.zip" "$SCRIPT_DIR/mcp-content.zip"
|
||||
|
||||
echo "MCP content updated successfully!"
|
||||
|
||||
Binary file not shown.
@@ -1,372 +0,0 @@
|
||||
import 'dart:io';
|
||||
import 'dart:convert';
|
||||
import 'package:archive/archive.dart';
|
||||
import 'package:path/path.dart' as path;
|
||||
import 'package:bm25/bm25.dart';
|
||||
import 'package:mcp_dart/mcp_dart.dart';
|
||||
|
||||
/// Main entry point for the MCP server
|
||||
void main() async {
|
||||
final server = PurpleStackMcpServer();
|
||||
|
||||
try {
|
||||
await server.initialize();
|
||||
server.start();
|
||||
} catch (e) {
|
||||
stderr.writeln('Failed to initialize server: $e');
|
||||
exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
/// MCP server that serves recipes and API documentation
|
||||
class PurpleStackMcpServer {
|
||||
static String get contentZipPath {
|
||||
final scriptUri = Platform.script;
|
||||
final scriptDir = path.dirname(scriptUri.toFilePath());
|
||||
return path.normalize(
|
||||
path.join(scriptDir, '..', 'content', 'mcp-content.zip'),
|
||||
);
|
||||
}
|
||||
|
||||
late McpServer _server;
|
||||
|
||||
// Content storage - now using full paths as keys
|
||||
final Map<String, String> _recipes = {};
|
||||
final Map<String, String> _docs = {};
|
||||
|
||||
// Search indexes
|
||||
BM25? _recipeSearchIndex;
|
||||
BM25? _docSearchIndex;
|
||||
List<String> _recipePaths = [];
|
||||
List<String> _docPaths = [];
|
||||
|
||||
PurpleStackMcpServer();
|
||||
|
||||
/// Initialize the server and load content
|
||||
Future<void> initialize() async {
|
||||
// Create MCP server
|
||||
_server = McpServer(
|
||||
Implementation(name: 'Purplestack Context Server', version: '1.0.0'),
|
||||
options: ServerOptions(
|
||||
capabilities: ServerCapabilities(tools: ServerCapabilitiesTools()),
|
||||
),
|
||||
);
|
||||
|
||||
// Load content from zip file
|
||||
await _loadContent();
|
||||
|
||||
// Build search indexes
|
||||
await _buildSearchIndexes();
|
||||
|
||||
// Register tools
|
||||
_registerTools();
|
||||
}
|
||||
|
||||
/// Start the MCP server
|
||||
void start() {
|
||||
_server.connect(StdioServerTransport());
|
||||
}
|
||||
|
||||
/// Load content from the zip file
|
||||
Future<void> _loadContent() async {
|
||||
final file = File(contentZipPath);
|
||||
if (!file.existsSync()) {
|
||||
throw Exception('Content zip file not found: $contentZipPath');
|
||||
}
|
||||
|
||||
final bytes = await file.readAsBytes();
|
||||
final archive = ZipDecoder().decodeBytes(bytes);
|
||||
|
||||
for (final file in archive) {
|
||||
if (file.isFile &&
|
||||
(file.name.endsWith('.md') || file.name.endsWith('.html'))) {
|
||||
final content = utf8.decode(file.content as List<int>);
|
||||
|
||||
if (file.name.startsWith('recipes/')) {
|
||||
// Use full path as key, removing only the 'recipes/' prefix
|
||||
final recipePath = file.name.substring('recipes/'.length);
|
||||
_recipes[recipePath] = content;
|
||||
} else if (file.name.startsWith('api-docs/')) {
|
||||
// Use full path as key, removing only the 'api-docs/' prefix
|
||||
final docPath = file.name.substring('api-docs/'.length);
|
||||
_docs[docPath] = content;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Build search indexes for recipes and docs
|
||||
Future<void> _buildSearchIndexes() async {
|
||||
if (_recipes.isNotEmpty) {
|
||||
// Build recipe search index
|
||||
_recipePaths = _recipes.keys.toList();
|
||||
final recipeDocuments = _recipes.entries.map((entry) {
|
||||
return '${entry.key} ${entry.value}';
|
||||
}).toList();
|
||||
_recipeSearchIndex = await BM25.build(recipeDocuments);
|
||||
}
|
||||
|
||||
if (_docs.isNotEmpty) {
|
||||
// Build docs search index
|
||||
_docPaths = _docs.keys.toList();
|
||||
final docDocuments = _docs.entries.map((entry) {
|
||||
return '${entry.key} ${entry.value}';
|
||||
}).toList();
|
||||
_docSearchIndex = await BM25.build(docDocuments);
|
||||
}
|
||||
}
|
||||
|
||||
/// Register all tools with the MCP server
|
||||
void _registerTools() {
|
||||
// List recipes tool
|
||||
_server.tool(
|
||||
'list_recipes',
|
||||
description: 'List all available recipes',
|
||||
callback: ({args, extra}) async {
|
||||
final result = await _listRecipes({});
|
||||
return CallToolResult.fromContent(content: [TextContent(text: result)]);
|
||||
},
|
||||
);
|
||||
|
||||
// Read recipe tool
|
||||
_server.tool(
|
||||
'read_recipe',
|
||||
description: 'Read a specific recipe by name',
|
||||
inputSchemaProperties: {
|
||||
'name': {'type': 'string', 'description': 'Name of the recipe to read'},
|
||||
},
|
||||
callback: ({args, extra}) async {
|
||||
final result = await _readRecipe(args ?? {});
|
||||
return CallToolResult.fromContent(content: [TextContent(text: result)]);
|
||||
},
|
||||
);
|
||||
|
||||
// Search recipes tool
|
||||
_server.tool(
|
||||
'search_recipes',
|
||||
description: 'Search recipes by query',
|
||||
inputSchemaProperties: {
|
||||
'query': {'type': 'string', 'description': 'Search query for recipes'},
|
||||
},
|
||||
callback: ({args, extra}) async {
|
||||
final result = await _searchRecipes(args ?? {});
|
||||
return CallToolResult.fromContent(content: [TextContent(text: result)]);
|
||||
},
|
||||
);
|
||||
|
||||
// List docs tool
|
||||
_server.tool(
|
||||
'list_docs',
|
||||
description: 'List all available documentation',
|
||||
callback: ({args, extra}) async {
|
||||
final result = await _listDocs({});
|
||||
return CallToolResult.fromContent(content: [TextContent(text: result)]);
|
||||
},
|
||||
);
|
||||
|
||||
// Read doc tool
|
||||
_server.tool(
|
||||
'read_doc',
|
||||
description: 'Read a specific document by name',
|
||||
inputSchemaProperties: {
|
||||
'name': {
|
||||
'type': 'string',
|
||||
'description': 'Name of the document to read',
|
||||
},
|
||||
},
|
||||
callback: ({args, extra}) async {
|
||||
final result = await _readDoc(args ?? {});
|
||||
return CallToolResult.fromContent(content: [TextContent(text: result)]);
|
||||
},
|
||||
);
|
||||
|
||||
// Search docs tool
|
||||
_server.tool(
|
||||
'search_docs',
|
||||
description: 'Search documentation by query',
|
||||
inputSchemaProperties: {
|
||||
'query': {
|
||||
'type': 'string',
|
||||
'description': 'Search query for documentation',
|
||||
},
|
||||
},
|
||||
callback: ({args, extra}) async {
|
||||
final result = await _searchDocs(args ?? {});
|
||||
return CallToolResult.fromContent(content: [TextContent(text: result)]);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
// Tool handlers
|
||||
|
||||
Future<String> _listRecipes(Map<String, dynamic> arguments) async {
|
||||
if (_recipes.isEmpty) {
|
||||
return 'No recipes available.';
|
||||
}
|
||||
|
||||
final recipeList = _recipes.keys.toList()..sort();
|
||||
return 'Available recipes:\n${recipeList.map((path) => '- $path').join('\n')}';
|
||||
}
|
||||
|
||||
Future<String> _readRecipe(Map<String, dynamic> arguments) async {
|
||||
final name = arguments['name'] as String?;
|
||||
if (name == null) {
|
||||
return 'Error: Recipe name is required';
|
||||
}
|
||||
|
||||
// Try exact match first
|
||||
var recipe = _recipes[name];
|
||||
if (recipe != null) {
|
||||
return recipe;
|
||||
}
|
||||
|
||||
// Try partial match for backwards compatibility
|
||||
final matchingKeys = _recipes.keys
|
||||
.where(
|
||||
(key) =>
|
||||
key.toLowerCase().contains(name.toLowerCase()) ||
|
||||
path.basenameWithoutExtension(key).toLowerCase() ==
|
||||
name.toLowerCase(),
|
||||
)
|
||||
.toList();
|
||||
|
||||
if (matchingKeys.length == 1) {
|
||||
return _recipes[matchingKeys.first]!;
|
||||
} else if (matchingKeys.length > 1) {
|
||||
return 'Multiple recipes found. Please be more specific:\n${matchingKeys.map((key) => '- $key').join('\n')}';
|
||||
}
|
||||
|
||||
final suggestions = _findSimilarKeys(name, _recipes.keys.toList());
|
||||
final suggestionText = suggestions.isNotEmpty
|
||||
? '\n\nDid you mean: ${suggestions.join(', ')}?'
|
||||
: '';
|
||||
return 'Recipe "$name" not found.$suggestionText';
|
||||
}
|
||||
|
||||
Future<String> _searchRecipes(Map<String, dynamic> arguments) async {
|
||||
final query = arguments['query'] as String?;
|
||||
if (query == null || query.isEmpty) {
|
||||
return 'Error: Search query is required';
|
||||
}
|
||||
|
||||
if (_recipeSearchIndex == null) {
|
||||
return 'Search index not available';
|
||||
}
|
||||
|
||||
final results = await _recipeSearchIndex!.search(query);
|
||||
if (results.isEmpty) {
|
||||
return 'No recipes found for query: "$query"';
|
||||
}
|
||||
|
||||
// Build documents list for index lookup
|
||||
final recipeDocuments = _recipes.entries.map((entry) {
|
||||
return '${entry.key} ${entry.value}';
|
||||
}).toList();
|
||||
|
||||
final resultText = StringBuffer('Search results for "$query":\n\n');
|
||||
for (final result in results.take(5)) {
|
||||
final index = recipeDocuments.indexOf(result.doc.text);
|
||||
if (index != -1) {
|
||||
final recipePath = _recipePaths[index];
|
||||
final score = result.score.toStringAsFixed(2);
|
||||
resultText.writeln('$recipePath ($score)');
|
||||
}
|
||||
}
|
||||
|
||||
return resultText.toString().trim();
|
||||
}
|
||||
|
||||
Future<String> _listDocs(Map<String, dynamic> arguments) async {
|
||||
if (_docs.isEmpty) {
|
||||
return 'No documentation available.';
|
||||
}
|
||||
|
||||
final docList = _docs.keys.toList()..sort();
|
||||
return 'Available documentation:\n${docList.map((path) => '- $path').join('\n')}';
|
||||
}
|
||||
|
||||
Future<String> _readDoc(Map<String, dynamic> arguments) async {
|
||||
final name = arguments['name'] as String?;
|
||||
if (name == null) {
|
||||
return 'Error: Document name is required';
|
||||
}
|
||||
|
||||
// Try exact match first
|
||||
var doc = _docs[name];
|
||||
if (doc != null) {
|
||||
return doc;
|
||||
}
|
||||
|
||||
// Try partial match for backwards compatibility
|
||||
final matchingKeys = _docs.keys
|
||||
.where(
|
||||
(key) =>
|
||||
key.toLowerCase().contains(name.toLowerCase()) ||
|
||||
path.basenameWithoutExtension(key).toLowerCase() ==
|
||||
name.toLowerCase(),
|
||||
)
|
||||
.toList();
|
||||
|
||||
if (matchingKeys.length == 1) {
|
||||
return _docs[matchingKeys.first]!;
|
||||
} else if (matchingKeys.length > 1) {
|
||||
return 'Multiple documents found. Please be more specific:\n${matchingKeys.map((key) => '- $key').join('\n')}';
|
||||
}
|
||||
|
||||
final suggestions = _findSimilarKeys(name, _docs.keys.toList());
|
||||
final suggestionText = suggestions.isNotEmpty
|
||||
? '\n\nDid you mean: ${suggestions.join(', ')}?'
|
||||
: '';
|
||||
return 'Document "$name" not found.$suggestionText';
|
||||
}
|
||||
|
||||
Future<String> _searchDocs(Map<String, dynamic> arguments) async {
|
||||
final query = arguments['query'] as String?;
|
||||
if (query == null || query.isEmpty) {
|
||||
return 'Error: Search query is required';
|
||||
}
|
||||
|
||||
if (_docSearchIndex == null) {
|
||||
return 'Search index not available';
|
||||
}
|
||||
|
||||
final results = await _docSearchIndex!.search(query);
|
||||
if (results.isEmpty) {
|
||||
return 'No documentation found for query: "$query"';
|
||||
}
|
||||
|
||||
// Build documents list for index lookup
|
||||
final docDocuments = _docs.entries.map((entry) {
|
||||
return '${entry.key} ${entry.value}';
|
||||
}).toList();
|
||||
|
||||
final resultText = StringBuffer('Search results for "$query":\n\n');
|
||||
for (final result in results.take(5)) {
|
||||
final index = docDocuments.indexOf(result.doc.text);
|
||||
if (index != -1) {
|
||||
final docPath = _docPaths[index];
|
||||
final score = result.score.toStringAsFixed(2);
|
||||
resultText.writeln('$docPath ($score)');
|
||||
}
|
||||
}
|
||||
|
||||
return resultText.toString().trim();
|
||||
}
|
||||
|
||||
/// Find similar keys for suggestions
|
||||
List<String> _findSimilarKeys(String input, List<String> keys) {
|
||||
final inputLower = input.toLowerCase();
|
||||
return keys
|
||||
.where(
|
||||
(key) =>
|
||||
key.toLowerCase().contains(inputLower) ||
|
||||
inputLower.contains(key.toLowerCase()) ||
|
||||
path
|
||||
.basenameWithoutExtension(key)
|
||||
.toLowerCase()
|
||||
.contains(inputLower),
|
||||
)
|
||||
.take(3)
|
||||
.toList();
|
||||
}
|
||||
}
|
||||
@@ -1,545 +0,0 @@
|
||||
// ignore_for_file: avoid_print
|
||||
|
||||
import 'dart:io';
|
||||
import 'package:args/args.dart';
|
||||
|
||||
const String kOriginalAppId = 'com.example.purplestack';
|
||||
const String kOriginalAppName = 'Purplestack';
|
||||
const String kOriginalAppNameSnakeCase = 'purplestack';
|
||||
|
||||
/// Converts a string to PascalCase (removes spaces and capitalizes each word)
|
||||
String _toPascalCase(String input) {
|
||||
return input
|
||||
.split(RegExp(r'[\s_-]+')) // Split on spaces, underscores, and hyphens
|
||||
.where((word) => word.isNotEmpty)
|
||||
.map((word) => word[0].toUpperCase() + word.substring(1).toLowerCase())
|
||||
.join('');
|
||||
}
|
||||
|
||||
void main(List<String> args) async {
|
||||
final parser = ArgParser()
|
||||
..addOption('name', abbr: 'n', help: 'App display name')
|
||||
..addOption(
|
||||
'app-id',
|
||||
abbr: 'i',
|
||||
help: 'App ID in reverse domain notation (e.g., com.company.app)',
|
||||
)
|
||||
..addOption(
|
||||
'description',
|
||||
abbr: 'd',
|
||||
help: 'App description for pubspec.yaml',
|
||||
)
|
||||
..addOption(
|
||||
'version',
|
||||
abbr: 'v',
|
||||
help: 'App version for pubspec.yaml (default: 0.1.0)',
|
||||
)
|
||||
..addOption('icon', help: 'Path to main app icon image')
|
||||
..addOption(
|
||||
'adaptive-background',
|
||||
help: 'Path to adaptive background image (Android)',
|
||||
)
|
||||
..addOption(
|
||||
'adaptive-foreground',
|
||||
help: 'Path to adaptive foreground image (Android)',
|
||||
)
|
||||
..addOption(
|
||||
'adaptive-monochrome',
|
||||
help: 'Path to adaptive monochrome image (Android)',
|
||||
)
|
||||
..addOption(
|
||||
'notification-icon',
|
||||
help: 'Path to notification icon image (Android)',
|
||||
)
|
||||
..addFlag(
|
||||
'help',
|
||||
abbr: 'h',
|
||||
help: 'Show usage information',
|
||||
negatable: false,
|
||||
);
|
||||
|
||||
late ArgResults results;
|
||||
try {
|
||||
results = parser.parse(args);
|
||||
} catch (e) {
|
||||
print('Error: $e\n');
|
||||
_printUsage(parser);
|
||||
exit(1);
|
||||
}
|
||||
|
||||
if (results['help'] as bool) {
|
||||
_printUsage(parser);
|
||||
exit(0);
|
||||
}
|
||||
|
||||
// Check for mandatory parameters
|
||||
if (results['name'] == null) {
|
||||
print('Error: --name is required\n');
|
||||
_printUsage(parser);
|
||||
exit(1);
|
||||
}
|
||||
|
||||
if (results['app-id'] == null) {
|
||||
print('Error: --app-id is required\n');
|
||||
_printUsage(parser);
|
||||
exit(1);
|
||||
}
|
||||
|
||||
final appName = results['name'] as String;
|
||||
final appId = results['app-id'] as String;
|
||||
final appDescription = results['description'] as String?;
|
||||
final appVersion = results['version'] as String? ?? '0.1.0';
|
||||
|
||||
// Create PascalCase version for Dart code (removes spaces, capitalizes each word)
|
||||
final appNamePascalCase = _toPascalCase(appName);
|
||||
final iconPath = results['icon'] as String?;
|
||||
final adaptiveBackground = results['adaptive-background'] as String?;
|
||||
final adaptiveForeground = results['adaptive-foreground'] as String?;
|
||||
final adaptiveMonochrome = results['adaptive-monochrome'] as String?;
|
||||
final notificationIcon = results['notification-icon'] as String?;
|
||||
|
||||
// Validate app ID format
|
||||
if (!RegExp(r'^[a-z][a-z0-9]*(\.[a-z][a-z0-9]*)+$').hasMatch(appId)) {
|
||||
print(
|
||||
'Error: App ID must be in reverse domain notation (e.g., com.company.app)',
|
||||
);
|
||||
print(
|
||||
'Use only lowercase letters, numbers, and dots. Each segment must start with a letter.',
|
||||
);
|
||||
exit(1);
|
||||
}
|
||||
|
||||
// Validate version format
|
||||
if (!RegExp(r'^\d+\.\d+\.\d+(\+\d+)?$').hasMatch(appVersion)) {
|
||||
print(
|
||||
'Error: Version must be in format x.y.z or x.y.z+build (e.g., 1.0.0 or 1.0.0+1)',
|
||||
);
|
||||
exit(1);
|
||||
}
|
||||
|
||||
// Validate icon paths exist if provided
|
||||
final iconPaths = [
|
||||
iconPath,
|
||||
adaptiveBackground,
|
||||
adaptiveForeground,
|
||||
adaptiveMonochrome,
|
||||
notificationIcon,
|
||||
].where((path) => path != null).cast<String>();
|
||||
|
||||
for (final iconPathToCheck in iconPaths) {
|
||||
if (!File(iconPathToCheck).existsSync()) {
|
||||
print('Error: Icon file not found: $iconPathToCheck');
|
||||
exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
final appNameSnakeCase = appName.toLowerCase().replaceAll(
|
||||
RegExp(r'[^a-z0-9]'),
|
||||
'_',
|
||||
);
|
||||
|
||||
print('Renaming Electric app...');
|
||||
print('Original App ID: $kOriginalAppId → $appId');
|
||||
print('Original App Name: $kOriginalAppName → $appName');
|
||||
print('App Name (PascalCase): $appNamePascalCase');
|
||||
print('Original Snake Case: $kOriginalAppNameSnakeCase → $appNameSnakeCase');
|
||||
if (appDescription != null) print('Description: $appDescription');
|
||||
print('Version: $appVersion');
|
||||
if (iconPath != null) print('Main Icon: $iconPath');
|
||||
if (adaptiveBackground != null) {
|
||||
print('Adaptive Background: $adaptiveBackground');
|
||||
}
|
||||
if (adaptiveForeground != null) {
|
||||
print('Adaptive Foreground: $adaptiveForeground');
|
||||
}
|
||||
if (adaptiveMonochrome != null) {
|
||||
print('Adaptive Monochrome: $adaptiveMonochrome');
|
||||
}
|
||||
if (notificationIcon != null) print('Notification Icon: $notificationIcon');
|
||||
print('');
|
||||
|
||||
final renamer = AppRenamer(
|
||||
appName,
|
||||
appNamePascalCase,
|
||||
appId,
|
||||
appNameSnakeCase,
|
||||
appDescription,
|
||||
appVersion,
|
||||
iconPath: iconPath,
|
||||
adaptiveBackground: adaptiveBackground,
|
||||
adaptiveForeground: adaptiveForeground,
|
||||
adaptiveMonochrome: adaptiveMonochrome,
|
||||
notificationIcon: notificationIcon,
|
||||
);
|
||||
|
||||
try {
|
||||
await renamer.renameApp();
|
||||
|
||||
// Clean and get dependencies
|
||||
await _cleanAndGetDependencies();
|
||||
|
||||
// Generate icons if any icon paths were provided
|
||||
if (renamer._hasIconPaths()) {
|
||||
await renamer._generateIcons();
|
||||
}
|
||||
|
||||
print('\n✅ Electric app renamed successfully!');
|
||||
print('\nNext steps:');
|
||||
print('1. Test the app on your target platforms');
|
||||
print('2. Commit your changes to version control');
|
||||
} catch (e) {
|
||||
print('\n❌ Error renaming app: $e');
|
||||
exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
void _printUsage(ArgParser parser) {
|
||||
print(
|
||||
'Electric App Renamer - Rename your Electric app across all platforms\n',
|
||||
);
|
||||
print(
|
||||
'Usage: dart rename_app.dart --name "App Name" --app-id "com.company.app" [options]\n',
|
||||
);
|
||||
print(
|
||||
'This tool will search all files in android/, ios/, lib/, linux/, macos/, test/, windows/ and update pubspec.yaml',
|
||||
);
|
||||
print('(excludes tools/ directory)');
|
||||
print('and replace:');
|
||||
print('- "$kOriginalAppId" with your new app ID (FIRST)');
|
||||
print(
|
||||
'- "$kOriginalAppName" with your new app name in PascalCase (for code)',
|
||||
);
|
||||
print('- "$kOriginalAppNameSnakeCase" with your new app name in snake_case');
|
||||
print('- "com.example" with your new app name (LAST)');
|
||||
print('- pubspec.yaml name, description, and version fields specifically');
|
||||
print('- version defaults to 0.1.0 if not provided\n');
|
||||
print('Options:');
|
||||
print(parser.usage);
|
||||
print('\nExamples:');
|
||||
print(' # Basic rename (spaces in name are allowed)');
|
||||
print(
|
||||
' dart rename_app.dart --name "My Super App" --app-id "com.mycompany.myapp"',
|
||||
);
|
||||
print('');
|
||||
print(' # With description and version');
|
||||
print(
|
||||
' dart rename_app.dart --name "My App" --app-id "com.mycompany.myapp" --description "A Flutter app for managing tasks" --version "1.0.0"',
|
||||
);
|
||||
print('');
|
||||
print(' # With main icon');
|
||||
print(
|
||||
' dart rename_app.dart --name "My App" --app-id "com.mycompany.myapp" --icon "assets/icon.png"',
|
||||
);
|
||||
print('');
|
||||
print(' # With Android adaptive icons');
|
||||
print(
|
||||
' dart rename_app.dart --name "My App" --app-id "com.mycompany.myapp" \\',
|
||||
);
|
||||
print(' --icon "assets/icon.png" \\');
|
||||
print(' --adaptive-background "assets/adaptive-bg.png" \\');
|
||||
print(' --adaptive-foreground "assets/adaptive-fg.png" \\');
|
||||
print(' --adaptive-monochrome "assets/adaptive-mono.png"');
|
||||
}
|
||||
|
||||
class AppRenamer {
|
||||
final String appName;
|
||||
final String appNamePascalCase;
|
||||
final String appId;
|
||||
final String appNameSnakeCase;
|
||||
final String? appDescription;
|
||||
final String appVersion;
|
||||
final String? iconPath;
|
||||
final String? adaptiveBackground;
|
||||
final String? adaptiveForeground;
|
||||
final String? adaptiveMonochrome;
|
||||
final String? notificationIcon;
|
||||
|
||||
AppRenamer(
|
||||
this.appName,
|
||||
this.appNamePascalCase,
|
||||
this.appId,
|
||||
this.appNameSnakeCase,
|
||||
this.appDescription,
|
||||
this.appVersion, {
|
||||
this.iconPath,
|
||||
this.adaptiveBackground,
|
||||
this.adaptiveForeground,
|
||||
this.adaptiveMonochrome,
|
||||
this.notificationIcon,
|
||||
});
|
||||
|
||||
Future<void> renameApp() async {
|
||||
print('🔍 Searching and replacing in all files...');
|
||||
|
||||
// Directories to search (excluding tools)
|
||||
final searchDirs = [
|
||||
'android',
|
||||
'ios',
|
||||
'lib',
|
||||
'linux',
|
||||
'macos',
|
||||
'test',
|
||||
'windows',
|
||||
];
|
||||
|
||||
int filesProcessed = 0;
|
||||
int filesChanged = 0;
|
||||
|
||||
// Process pubspec.yaml specifically
|
||||
final pubspecFile = File('pubspec.yaml');
|
||||
if (pubspecFile.existsSync()) {
|
||||
final result = await _processPubspecFile(pubspecFile);
|
||||
filesProcessed++;
|
||||
if (result) filesChanged++;
|
||||
}
|
||||
|
||||
// Process all files in search directories
|
||||
for (final dirName in searchDirs) {
|
||||
final dir = Directory(dirName);
|
||||
if (!dir.existsSync()) {
|
||||
print('⚠️ Directory $dirName not found, skipping');
|
||||
continue;
|
||||
}
|
||||
|
||||
await for (final entity in dir.list(recursive: true)) {
|
||||
if (entity is File) {
|
||||
final result = await _processFile(entity);
|
||||
filesProcessed++;
|
||||
if (result) filesChanged++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
print('✅ Processed $filesProcessed files, changed $filesChanged files');
|
||||
}
|
||||
|
||||
Future<bool> _processPubspecFile(File file) async {
|
||||
try {
|
||||
String content = await file.readAsString();
|
||||
String originalContent = content;
|
||||
|
||||
// Replace the exact snake_case project name
|
||||
content = content.replaceAll(
|
||||
RegExp('^name:\\s+$kOriginalAppNameSnakeCase\$', multiLine: true),
|
||||
'name: $appNameSnakeCase',
|
||||
);
|
||||
|
||||
// Replace description if provided
|
||||
if (appDescription != null) {
|
||||
content = content.replaceAll(
|
||||
RegExp(r'^description:.*$', multiLine: true),
|
||||
'description: $appDescription',
|
||||
);
|
||||
}
|
||||
|
||||
// Replace version (always overwrite with default 0.1.0 or provided value)
|
||||
content = content.replaceAll(
|
||||
RegExp(r'^version:\s+[\d\.]+(\+\d+)?$', multiLine: true),
|
||||
'version: $appVersion',
|
||||
);
|
||||
|
||||
// Write back if content changed
|
||||
if (content != originalContent) {
|
||||
await file.writeAsString(content);
|
||||
print(' ✓ Updated ${file.path}');
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
} catch (e) {
|
||||
print(' ⚠️ Error updating ${file.path}: $e');
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
Future<bool> _processFile(File file) async {
|
||||
try {
|
||||
String content = await file.readAsString();
|
||||
String originalContent = content;
|
||||
|
||||
// Perform exact string replacements - APP ID FIRST!
|
||||
content = content.replaceAll(kOriginalAppId, appId);
|
||||
|
||||
// Use PascalCase for Dart code identifiers (no spaces)
|
||||
content = content.replaceAll(kOriginalAppName, appNamePascalCase);
|
||||
content = content.replaceAll(kOriginalAppNameSnakeCase, appNameSnakeCase);
|
||||
|
||||
// For display names in platform files, we might want the full name with spaces
|
||||
// Check if this is a platform-specific file that needs display names
|
||||
if (_shouldUseDisplayName(file.path)) {
|
||||
// In platform files, replace PascalCase back with display name for labels
|
||||
content = content.replaceAll(appNamePascalCase, appName);
|
||||
}
|
||||
|
||||
content = content.replaceAll('com.example', appName);
|
||||
|
||||
// Write back if content changed
|
||||
if (content != originalContent) {
|
||||
await file.writeAsString(content);
|
||||
print(' ✓ Updated ${file.path}');
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
} catch (e) {
|
||||
// Skip binary files or files we can't read/write
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// Determines if a file should use the display name (with spaces) instead of PascalCase
|
||||
/// Platform files like AndroidManifest.xml and Info.plist need display names for labels
|
||||
bool _shouldUseDisplayName(String filePath) {
|
||||
return filePath.contains('AndroidManifest.xml') ||
|
||||
filePath.contains('Info.plist') ||
|
||||
filePath.contains('strings.xml') ||
|
||||
filePath.contains('.xml') && filePath.contains('android') ||
|
||||
filePath.endsWith('.plist');
|
||||
}
|
||||
|
||||
bool _hasIconPaths() {
|
||||
return iconPath != null ||
|
||||
adaptiveBackground != null ||
|
||||
adaptiveForeground != null ||
|
||||
adaptiveMonochrome != null ||
|
||||
notificationIcon != null;
|
||||
}
|
||||
|
||||
Future<void> _generateIcons() async {
|
||||
print('🎨 Generating app icons...');
|
||||
|
||||
final pubspecFile = File('pubspec.yaml');
|
||||
if (!pubspecFile.existsSync()) {
|
||||
throw Exception('pubspec.yaml not found');
|
||||
}
|
||||
|
||||
String originalContent = await pubspecFile.readAsString();
|
||||
String modifiedContent = originalContent;
|
||||
|
||||
try {
|
||||
// Add icons_launcher configuration to pubspec.yaml
|
||||
final iconsConfig = _buildIconsLauncherConfig();
|
||||
modifiedContent = '$originalContent\n$iconsConfig';
|
||||
await pubspecFile.writeAsString(modifiedContent);
|
||||
|
||||
// Run icons_launcher
|
||||
final result = await Process.run('dart', [
|
||||
'run',
|
||||
'icons_launcher:create',
|
||||
]);
|
||||
|
||||
if (result.exitCode == 0) {
|
||||
print('✅ Icons generated successfully');
|
||||
if (result.stdout.toString().isNotEmpty) {
|
||||
print('Icons output: ${result.stdout}');
|
||||
}
|
||||
} else {
|
||||
print('❌ Failed to generate icons');
|
||||
print('Error: ${result.stderr}');
|
||||
throw Exception('Icon generation failed: ${result.stderr}');
|
||||
}
|
||||
} finally {
|
||||
// Restore original pubspec.yaml
|
||||
await pubspecFile.writeAsString(originalContent);
|
||||
}
|
||||
}
|
||||
|
||||
String _buildIconsLauncherConfig() {
|
||||
final buffer = StringBuffer();
|
||||
buffer.writeln('icons_launcher:');
|
||||
|
||||
// Main icon path (required by icons_launcher)
|
||||
final mainIcon = iconPath ?? adaptiveForeground ?? adaptiveBackground;
|
||||
if (mainIcon != null) {
|
||||
buffer.writeln(' image_path: "$mainIcon"');
|
||||
}
|
||||
|
||||
buffer.writeln(' platforms:');
|
||||
|
||||
// Android configuration
|
||||
buffer.writeln(' android:');
|
||||
buffer.writeln(' enable: true');
|
||||
if (iconPath != null) {
|
||||
buffer.writeln(' image_path: "$iconPath"');
|
||||
}
|
||||
if (notificationIcon != null) {
|
||||
buffer.writeln(' notification_image: "$notificationIcon"');
|
||||
} else if (iconPath != null) {
|
||||
buffer.writeln(' notification_image: "$iconPath"');
|
||||
}
|
||||
if (adaptiveBackground != null) {
|
||||
buffer.writeln(' adaptive_background_image: "$adaptiveBackground"');
|
||||
}
|
||||
if (adaptiveForeground != null) {
|
||||
buffer.writeln(' adaptive_foreground_image: "$adaptiveForeground"');
|
||||
}
|
||||
if (adaptiveMonochrome != null) {
|
||||
buffer.writeln(' adaptive_monochrome_image: "$adaptiveMonochrome"');
|
||||
}
|
||||
|
||||
// iOS configuration
|
||||
buffer.writeln(' ios:');
|
||||
buffer.writeln(' enable: true');
|
||||
if (iconPath != null) {
|
||||
buffer.writeln(' image_path: "$iconPath"');
|
||||
}
|
||||
|
||||
// macOS configuration
|
||||
buffer.writeln(' macos:');
|
||||
buffer.writeln(' enable: true');
|
||||
if (iconPath != null) {
|
||||
buffer.writeln(' image_path: "$iconPath"');
|
||||
}
|
||||
|
||||
// Windows configuration
|
||||
buffer.writeln(' windows:');
|
||||
buffer.writeln(' enable: true');
|
||||
if (iconPath != null) {
|
||||
buffer.writeln(' image_path: "$iconPath"');
|
||||
}
|
||||
|
||||
// Linux configuration
|
||||
buffer.writeln(' linux:');
|
||||
buffer.writeln(' enable: true');
|
||||
if (iconPath != null) {
|
||||
buffer.writeln(' image_path: "$iconPath"');
|
||||
}
|
||||
|
||||
return buffer.toString();
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _cleanAndGetDependencies() async {
|
||||
print('\n🧹 Cleaning pub cache and getting dependencies...');
|
||||
|
||||
try {
|
||||
// Check if we have flutter command available
|
||||
print('Running flutter clean...');
|
||||
final cleanResult = await Process.run('flutter', ['clean']);
|
||||
if (cleanResult.exitCode != 0) {
|
||||
print('⚠️ Flutter clean failed, but continuing...');
|
||||
}
|
||||
|
||||
print('Running dart pub cache clean...');
|
||||
final cacheResult = await Process.run('dart', ['pub', 'cache', 'clean']);
|
||||
if (cacheResult.exitCode != 0) {
|
||||
print('⚠️ Pub cache clean failed, but continuing...');
|
||||
}
|
||||
|
||||
print('Running flutter pub get...');
|
||||
final pubGetResult = await Process.run('flutter', ['pub', 'get']);
|
||||
if (pubGetResult.exitCode != 0) {
|
||||
print('❌ Flutter pub get failed:');
|
||||
print(pubGetResult.stderr);
|
||||
throw Exception('Failed to run flutter pub get');
|
||||
}
|
||||
|
||||
print('✓ Dependencies updated successfully');
|
||||
} catch (e) {
|
||||
print('⚠️ Error cleaning dependencies: $e');
|
||||
print('Please run the following commands manually:');
|
||||
print(' flutter clean');
|
||||
print(' dart pub cache clean');
|
||||
print(' flutter pub get');
|
||||
}
|
||||
}
|
||||
@@ -1,4 +0,0 @@
|
||||
#!/bin/bash
|
||||
# Run the purplestack MCP server
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
(fvm flutter pub get || flutter pub get) && dart run "$SCRIPT_DIR/purplestack_mcp.dart"
|
||||
@@ -1,22 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
set -e
|
||||
|
||||
UPSTREAM_URL="https://github.com/purplebase/purplestack.git"
|
||||
UPSTREAM_BRANCH="main"
|
||||
UPSTREAM_LOCAL_DIR=/tmp/purplestack-tmp
|
||||
FOLDER="tools"
|
||||
|
||||
# 1. shallow clone upstream into a hidden mirror
|
||||
if [ ! -d "$UPSTREAM_LOCAL_DIR" ]; then
|
||||
git clone --depth 1 --branch "$UPSTREAM_BRANCH" "$UPSTREAM_URL" "$UPSTREAM_LOCAL_DIR"
|
||||
fi
|
||||
|
||||
# 2. fetch latest
|
||||
git -C "$UPSTREAM_LOCAL_DIR" fetch --depth 1 origin "$UPSTREAM_BRANCH"
|
||||
git -C "$UPSTREAM_LOCAL_DIR" reset --hard "origin/$UPSTREAM_BRANCH"
|
||||
|
||||
# 3. copy only the folder we care about
|
||||
rm -rf "$FOLDER"
|
||||
cp -a "$UPSTREAM_LOCAL_DIR/$FOLDER" "$(dirname "$FOLDER")"
|
||||
|
||||
fvm flutter pub upgrade || flutter pub upgrade
|
||||
Reference in New Issue
Block a user