From a6f500ae8650f4469518c0b1fca6bdaa5dee3683 Mon Sep 17 00:00:00 2001 From: toki Date: Sat, 21 Mar 2026 11:34:09 +0900 Subject: [PATCH 01/18] Update .gitignore with Flutter-related patterns --- .gitignore | 66 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 66 insertions(+) create mode 100644 .gitignore diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..931d8c5 --- /dev/null +++ b/.gitignore @@ -0,0 +1,66 @@ +# Created by https://www.toptal.com/developers/gitignore/api/flutter +# Edit at https://www.toptal.com/developers/gitignore?templates=flutter + +### Flutter ### +# Flutter/Dart/Pub related +**/doc/api/ +.dart_tool/ +.flutter-plugins +.flutter-plugins-dependencies +.fvm/flutter_sdk +.packages +.pub-cache/ +.pub/ +build/ +coverage/ +lib/generated_plugin_registrant.dart +# For library packages, don't commit the pubspec.lock file. +# Regenerating the pubspec.lock file lets you test your package against the latest compatible versions of its dependencies. +# See https://dart.dev/guides/libraries/private-files#pubspeclock +#pubspec.lock + +# Android related +**/android/**/gradle-wrapper.jar +**/android/.gradle +**/android/captures/ +**/android/gradlew +**/android/gradlew.bat +**/android/key.properties +**/android/local.properties +**/android/**/GeneratedPluginRegistrant.java + +# iOS/XCode related +**/ios/**/*.mode1v3 +**/ios/**/*.mode2v3 +**/ios/**/*.moved-aside +**/ios/**/*.pbxuser +**/ios/**/*.perspectivev3 +**/ios/**/*sync/ +**/ios/**/.sconsign.dblite +**/ios/**/.tags* +**/ios/**/.vagrant/ +**/ios/**/DerivedData/ +**/ios/**/Icon? +**/ios/**/Pods/ +**/ios/**/.symlinks/ +**/ios/**/profile +**/ios/**/xcuserdata +**/ios/.generated/ +**/ios/Flutter/.last_build_id +**/ios/Flutter/App.framework +**/ios/Flutter/Flutter.framework +**/ios/Flutter/Flutter.podspec +**/ios/Flutter/Generated.xcconfig +**/ios/Flutter/app.flx +**/ios/Flutter/app.zip +**/ios/Flutter/flutter_assets/ +**/ios/Flutter/flutter_export_environment.sh +**/ios/ServiceDefinitions.json +**/ios/Runner/GeneratedPluginRegistrant.* + +# Exceptions to above rules. +!**/ios/**/default.mode1v3 +!**/ios/**/default.mode2v3 +!**/ios/**/default.pbxuser +!**/ios/**/default.perspectivev3 +!/packages/flutter_tools/test/data/dart_dependencies_test/**/.packages \ No newline at end of file From d36154a3c32a8496d8705c59b3595124945da78c Mon Sep 17 00:00:00 2001 From: toki Date: Sat, 21 Mar 2026 11:41:47 +0900 Subject: [PATCH 02/18] Initial commit: Flutter project setup --- .idea/libraries/Dart_SDK.xml | 19 + .idea/libraries/KotlinJavaRuntime.xml | 15 + .idea/modules.xml | 9 + .idea/runConfigurations/main_dart.xml | 6 + .idea/workspace.xml | 36 + .metadata | 45 ++ README.md | 17 + analysis_options.yaml | 28 + android/.gitignore | 14 + android/app/build.gradle.kts | 44 ++ android/app/src/debug/AndroidManifest.xml | 7 + android/app/src/main/AndroidManifest.xml | 45 ++ .../com/nomadcode/nomadcode/MainActivity.kt | 5 + .../res/drawable-v21/launch_background.xml | 12 + .../main/res/drawable/launch_background.xml | 12 + .../src/main/res/mipmap-hdpi/ic_launcher.png | Bin 0 -> 544 bytes .../src/main/res/mipmap-mdpi/ic_launcher.png | Bin 0 -> 442 bytes .../src/main/res/mipmap-xhdpi/ic_launcher.png | Bin 0 -> 721 bytes .../main/res/mipmap-xxhdpi/ic_launcher.png | Bin 0 -> 1031 bytes .../main/res/mipmap-xxxhdpi/ic_launcher.png | Bin 0 -> 1443 bytes .../app/src/main/res/values-night/styles.xml | 18 + android/app/src/main/res/values/styles.xml | 18 + android/app/src/profile/AndroidManifest.xml | 7 + android/build.gradle.kts | 24 + android/gradle.properties | 2 + .../gradle/wrapper/gradle-wrapper.properties | 5 + android/nomadcode_android.iml | 29 + android/settings.gradle.kts | 26 + ios/.gitignore | 34 + ios/Flutter/AppFrameworkInfo.plist | 24 + ios/Flutter/Debug.xcconfig | 1 + ios/Flutter/Release.xcconfig | 1 + ios/Runner.xcodeproj/project.pbxproj | 620 +++++++++++++++ .../contents.xcworkspacedata | 7 + .../xcshareddata/IDEWorkspaceChecks.plist | 8 + .../xcshareddata/WorkspaceSettings.xcsettings | 8 + .../xcshareddata/xcschemes/Runner.xcscheme | 101 +++ .../contents.xcworkspacedata | 7 + .../xcshareddata/IDEWorkspaceChecks.plist | 8 + .../xcshareddata/WorkspaceSettings.xcsettings | 8 + ios/Runner/AppDelegate.swift | 16 + .../AppIcon.appiconset/Contents.json | 122 +++ .../Icon-App-1024x1024@1x.png | Bin 0 -> 10932 bytes .../AppIcon.appiconset/Icon-App-20x20@1x.png | Bin 0 -> 295 bytes .../AppIcon.appiconset/Icon-App-20x20@2x.png | Bin 0 -> 406 bytes .../AppIcon.appiconset/Icon-App-20x20@3x.png | Bin 0 -> 450 bytes .../AppIcon.appiconset/Icon-App-29x29@1x.png | Bin 0 -> 282 bytes .../AppIcon.appiconset/Icon-App-29x29@2x.png | Bin 0 -> 462 bytes .../AppIcon.appiconset/Icon-App-29x29@3x.png | Bin 0 -> 704 bytes .../AppIcon.appiconset/Icon-App-40x40@1x.png | Bin 0 -> 406 bytes .../AppIcon.appiconset/Icon-App-40x40@2x.png | Bin 0 -> 586 bytes .../AppIcon.appiconset/Icon-App-40x40@3x.png | Bin 0 -> 862 bytes .../AppIcon.appiconset/Icon-App-60x60@2x.png | Bin 0 -> 862 bytes .../AppIcon.appiconset/Icon-App-60x60@3x.png | Bin 0 -> 1674 bytes .../AppIcon.appiconset/Icon-App-76x76@1x.png | Bin 0 -> 762 bytes .../AppIcon.appiconset/Icon-App-76x76@2x.png | Bin 0 -> 1226 bytes .../Icon-App-83.5x83.5@2x.png | Bin 0 -> 1418 bytes .../LaunchImage.imageset/Contents.json | 23 + .../LaunchImage.imageset/LaunchImage.png | Bin 0 -> 68 bytes .../LaunchImage.imageset/LaunchImage@2x.png | Bin 0 -> 68 bytes .../LaunchImage.imageset/LaunchImage@3x.png | Bin 0 -> 68 bytes .../LaunchImage.imageset/README.md | 5 + ios/Runner/Base.lproj/LaunchScreen.storyboard | 37 + ios/Runner/Base.lproj/Main.storyboard | 26 + ios/Runner/Info.plist | 70 ++ ios/Runner/Runner-Bridging-Header.h | 1 + ios/Runner/SceneDelegate.swift | 6 + ios/RunnerTests/RunnerTests.swift | 12 + lib/main.dart | 122 +++ linux/.gitignore | 1 + linux/CMakeLists.txt | 128 ++++ linux/flutter/CMakeLists.txt | 88 +++ linux/flutter/generated_plugin_registrant.cc | 11 + linux/flutter/generated_plugin_registrant.h | 15 + linux/flutter/generated_plugins.cmake | 23 + linux/runner/CMakeLists.txt | 26 + linux/runner/main.cc | 6 + linux/runner/my_application.cc | 148 ++++ linux/runner/my_application.h | 21 + macos/.gitignore | 7 + macos/Flutter/Flutter-Debug.xcconfig | 1 + macos/Flutter/Flutter-Release.xcconfig | 1 + macos/Flutter/GeneratedPluginRegistrant.swift | 10 + macos/Runner.xcodeproj/project.pbxproj | 705 ++++++++++++++++++ .../xcshareddata/IDEWorkspaceChecks.plist | 8 + .../xcshareddata/xcschemes/Runner.xcscheme | 99 +++ .../contents.xcworkspacedata | 7 + .../xcshareddata/IDEWorkspaceChecks.plist | 8 + macos/Runner/AppDelegate.swift | 13 + .../AppIcon.appiconset/Contents.json | 68 ++ .../AppIcon.appiconset/app_icon_1024.png | Bin 0 -> 102994 bytes .../AppIcon.appiconset/app_icon_128.png | Bin 0 -> 5680 bytes .../AppIcon.appiconset/app_icon_16.png | Bin 0 -> 520 bytes .../AppIcon.appiconset/app_icon_256.png | Bin 0 -> 14142 bytes .../AppIcon.appiconset/app_icon_32.png | Bin 0 -> 1066 bytes .../AppIcon.appiconset/app_icon_512.png | Bin 0 -> 36406 bytes .../AppIcon.appiconset/app_icon_64.png | Bin 0 -> 2218 bytes macos/Runner/Base.lproj/MainMenu.xib | 343 +++++++++ macos/Runner/Configs/AppInfo.xcconfig | 14 + macos/Runner/Configs/Debug.xcconfig | 2 + macos/Runner/Configs/Release.xcconfig | 2 + macos/Runner/Configs/Warnings.xcconfig | 13 + macos/Runner/DebugProfile.entitlements | 12 + macos/Runner/Info.plist | 32 + macos/Runner/MainFlutterWindow.swift | 15 + macos/Runner/Release.entitlements | 8 + macos/RunnerTests/RunnerTests.swift | 12 + nomadcode.iml | 17 + pubspec.lock | 213 ++++++ pubspec.yaml | 89 +++ test/widget_test.dart | 30 + web/favicon.png | Bin 0 -> 917 bytes web/icons/Icon-192.png | Bin 0 -> 5292 bytes web/icons/Icon-512.png | Bin 0 -> 8252 bytes web/icons/Icon-maskable-192.png | Bin 0 -> 5594 bytes web/icons/Icon-maskable-512.png | Bin 0 -> 20998 bytes web/index.html | 46 ++ web/manifest.json | 35 + windows/.gitignore | 17 + windows/CMakeLists.txt | 108 +++ windows/flutter/CMakeLists.txt | 109 +++ .../flutter/generated_plugin_registrant.cc | 11 + windows/flutter/generated_plugin_registrant.h | 15 + windows/flutter/generated_plugins.cmake | 23 + windows/runner/CMakeLists.txt | 40 + windows/runner/Runner.rc | 121 +++ windows/runner/flutter_window.cpp | 71 ++ windows/runner/flutter_window.h | 33 + windows/runner/main.cpp | 43 ++ windows/runner/resource.h | 16 + windows/runner/resources/app_icon.ico | Bin 0 -> 33772 bytes windows/runner/runner.exe.manifest | 14 + windows/runner/utils.cpp | 65 ++ windows/runner/utils.h | 19 + windows/runner/win32_window.cpp | 288 +++++++ windows/runner/win32_window.h | 102 +++ 136 files changed, 5082 insertions(+) create mode 100644 .idea/libraries/Dart_SDK.xml create mode 100644 .idea/libraries/KotlinJavaRuntime.xml create mode 100644 .idea/modules.xml create mode 100644 .idea/runConfigurations/main_dart.xml create mode 100644 .idea/workspace.xml create mode 100644 .metadata create mode 100644 README.md create mode 100644 analysis_options.yaml create mode 100644 android/.gitignore create mode 100644 android/app/build.gradle.kts create mode 100644 android/app/src/debug/AndroidManifest.xml create mode 100644 android/app/src/main/AndroidManifest.xml create mode 100644 android/app/src/main/kotlin/com/nomadcode/nomadcode/MainActivity.kt create mode 100644 android/app/src/main/res/drawable-v21/launch_background.xml create mode 100644 android/app/src/main/res/drawable/launch_background.xml create mode 100644 android/app/src/main/res/mipmap-hdpi/ic_launcher.png create mode 100644 android/app/src/main/res/mipmap-mdpi/ic_launcher.png create mode 100644 android/app/src/main/res/mipmap-xhdpi/ic_launcher.png create mode 100644 android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png create mode 100644 android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png create mode 100644 android/app/src/main/res/values-night/styles.xml create mode 100644 android/app/src/main/res/values/styles.xml create mode 100644 android/app/src/profile/AndroidManifest.xml create mode 100644 android/build.gradle.kts create mode 100644 android/gradle.properties create mode 100644 android/gradle/wrapper/gradle-wrapper.properties create mode 100644 android/nomadcode_android.iml create mode 100644 android/settings.gradle.kts create mode 100644 ios/.gitignore create mode 100644 ios/Flutter/AppFrameworkInfo.plist create mode 100644 ios/Flutter/Debug.xcconfig create mode 100644 ios/Flutter/Release.xcconfig create mode 100644 ios/Runner.xcodeproj/project.pbxproj create mode 100644 ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata create mode 100644 ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist create mode 100644 ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings create mode 100644 ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme create mode 100644 ios/Runner.xcworkspace/contents.xcworkspacedata create mode 100644 ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist create mode 100644 ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings create mode 100644 ios/Runner/AppDelegate.swift create mode 100644 ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json create mode 100644 ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png create mode 100644 ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png create mode 100644 ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png create mode 100644 ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png create mode 100644 ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png create mode 100644 ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png create mode 100644 ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png create mode 100644 ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png create mode 100644 ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png create mode 100644 ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png create mode 100644 ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png create mode 100644 ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png create mode 100644 ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png create mode 100644 ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png create mode 100644 ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png create mode 100644 ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json create mode 100644 ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png create mode 100644 ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png create mode 100644 ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png create mode 100644 ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md create mode 100644 ios/Runner/Base.lproj/LaunchScreen.storyboard create mode 100644 ios/Runner/Base.lproj/Main.storyboard create mode 100644 ios/Runner/Info.plist create mode 100644 ios/Runner/Runner-Bridging-Header.h create mode 100644 ios/Runner/SceneDelegate.swift create mode 100644 ios/RunnerTests/RunnerTests.swift create mode 100644 lib/main.dart create mode 100644 linux/.gitignore create mode 100644 linux/CMakeLists.txt create mode 100644 linux/flutter/CMakeLists.txt create mode 100644 linux/flutter/generated_plugin_registrant.cc create mode 100644 linux/flutter/generated_plugin_registrant.h create mode 100644 linux/flutter/generated_plugins.cmake create mode 100644 linux/runner/CMakeLists.txt create mode 100644 linux/runner/main.cc create mode 100644 linux/runner/my_application.cc create mode 100644 linux/runner/my_application.h create mode 100644 macos/.gitignore create mode 100644 macos/Flutter/Flutter-Debug.xcconfig create mode 100644 macos/Flutter/Flutter-Release.xcconfig create mode 100644 macos/Flutter/GeneratedPluginRegistrant.swift create mode 100644 macos/Runner.xcodeproj/project.pbxproj create mode 100644 macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist create mode 100644 macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme create mode 100644 macos/Runner.xcworkspace/contents.xcworkspacedata create mode 100644 macos/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist create mode 100644 macos/Runner/AppDelegate.swift create mode 100644 macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json create mode 100644 macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png create mode 100644 macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png create mode 100644 macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png create mode 100644 macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png create mode 100644 macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png create mode 100644 macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png create mode 100644 macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png create mode 100644 macos/Runner/Base.lproj/MainMenu.xib create mode 100644 macos/Runner/Configs/AppInfo.xcconfig create mode 100644 macos/Runner/Configs/Debug.xcconfig create mode 100644 macos/Runner/Configs/Release.xcconfig create mode 100644 macos/Runner/Configs/Warnings.xcconfig create mode 100644 macos/Runner/DebugProfile.entitlements create mode 100644 macos/Runner/Info.plist create mode 100644 macos/Runner/MainFlutterWindow.swift create mode 100644 macos/Runner/Release.entitlements create mode 100644 macos/RunnerTests/RunnerTests.swift create mode 100644 nomadcode.iml create mode 100644 pubspec.lock create mode 100644 pubspec.yaml create mode 100644 test/widget_test.dart create mode 100644 web/favicon.png create mode 100644 web/icons/Icon-192.png create mode 100644 web/icons/Icon-512.png create mode 100644 web/icons/Icon-maskable-192.png create mode 100644 web/icons/Icon-maskable-512.png create mode 100644 web/index.html create mode 100644 web/manifest.json create mode 100644 windows/.gitignore create mode 100644 windows/CMakeLists.txt create mode 100644 windows/flutter/CMakeLists.txt create mode 100644 windows/flutter/generated_plugin_registrant.cc create mode 100644 windows/flutter/generated_plugin_registrant.h create mode 100644 windows/flutter/generated_plugins.cmake create mode 100644 windows/runner/CMakeLists.txt create mode 100644 windows/runner/Runner.rc create mode 100644 windows/runner/flutter_window.cpp create mode 100644 windows/runner/flutter_window.h create mode 100644 windows/runner/main.cpp create mode 100644 windows/runner/resource.h create mode 100644 windows/runner/resources/app_icon.ico create mode 100644 windows/runner/runner.exe.manifest create mode 100644 windows/runner/utils.cpp create mode 100644 windows/runner/utils.h create mode 100644 windows/runner/win32_window.cpp create mode 100644 windows/runner/win32_window.h diff --git a/.idea/libraries/Dart_SDK.xml b/.idea/libraries/Dart_SDK.xml new file mode 100644 index 0000000..9c1307b --- /dev/null +++ b/.idea/libraries/Dart_SDK.xml @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/libraries/KotlinJavaRuntime.xml b/.idea/libraries/KotlinJavaRuntime.xml new file mode 100644 index 0000000..2b96ac4 --- /dev/null +++ b/.idea/libraries/KotlinJavaRuntime.xml @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/.idea/modules.xml b/.idea/modules.xml new file mode 100644 index 0000000..b0a1eb4 --- /dev/null +++ b/.idea/modules.xml @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/.idea/runConfigurations/main_dart.xml b/.idea/runConfigurations/main_dart.xml new file mode 100644 index 0000000..aab7b5c --- /dev/null +++ b/.idea/runConfigurations/main_dart.xml @@ -0,0 +1,6 @@ + + + + \ No newline at end of file diff --git a/.idea/workspace.xml b/.idea/workspace.xml new file mode 100644 index 0000000..5b3388c --- /dev/null +++ b/.idea/workspace.xml @@ -0,0 +1,36 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/.metadata b/.metadata new file mode 100644 index 0000000..52e942a --- /dev/null +++ b/.metadata @@ -0,0 +1,45 @@ +# This file tracks properties of this Flutter project. +# Used by Flutter tool to assess capabilities and perform upgrades etc. +# +# This file should be version controlled and should not be manually edited. + +version: + revision: "582a0e7c5581dc0ca5f7bfd8662bb8db6f59d536" + channel: "stable" + +project_type: app + +# Tracks metadata for the flutter migrate command +migration: + platforms: + - platform: root + create_revision: 582a0e7c5581dc0ca5f7bfd8662bb8db6f59d536 + base_revision: 582a0e7c5581dc0ca5f7bfd8662bb8db6f59d536 + - platform: android + create_revision: 582a0e7c5581dc0ca5f7bfd8662bb8db6f59d536 + base_revision: 582a0e7c5581dc0ca5f7bfd8662bb8db6f59d536 + - platform: ios + create_revision: 582a0e7c5581dc0ca5f7bfd8662bb8db6f59d536 + base_revision: 582a0e7c5581dc0ca5f7bfd8662bb8db6f59d536 + - platform: linux + create_revision: 582a0e7c5581dc0ca5f7bfd8662bb8db6f59d536 + base_revision: 582a0e7c5581dc0ca5f7bfd8662bb8db6f59d536 + - platform: macos + create_revision: 582a0e7c5581dc0ca5f7bfd8662bb8db6f59d536 + base_revision: 582a0e7c5581dc0ca5f7bfd8662bb8db6f59d536 + - platform: web + create_revision: 582a0e7c5581dc0ca5f7bfd8662bb8db6f59d536 + base_revision: 582a0e7c5581dc0ca5f7bfd8662bb8db6f59d536 + - platform: windows + create_revision: 582a0e7c5581dc0ca5f7bfd8662bb8db6f59d536 + base_revision: 582a0e7c5581dc0ca5f7bfd8662bb8db6f59d536 + + # User provided section + + # List of Local paths (relative to this file) that should be + # ignored by the migrate tool. + # + # Files that are not part of the templates will be ignored by default. + unmanaged_files: + - 'lib/main.dart' + - 'ios/Runner.xcodeproj/project.pbxproj' diff --git a/README.md b/README.md new file mode 100644 index 0000000..4a680c8 --- /dev/null +++ b/README.md @@ -0,0 +1,17 @@ +# nomadcode + +A new Flutter project. + +## Getting Started + +This project is a starting point for a Flutter application. + +A few resources to get you started if this is your first Flutter project: + +- [Learn Flutter](https://docs.flutter.dev/get-started/learn-flutter) +- [Write your first Flutter app](https://docs.flutter.dev/get-started/codelab) +- [Flutter learning resources](https://docs.flutter.dev/reference/learning-resources) + +For help getting started with Flutter development, view the +[online documentation](https://docs.flutter.dev/), which offers tutorials, +samples, guidance on mobile development, and a full API reference. diff --git a/analysis_options.yaml b/analysis_options.yaml new file mode 100644 index 0000000..0d29021 --- /dev/null +++ b/analysis_options.yaml @@ -0,0 +1,28 @@ +# This file configures the analyzer, which statically analyzes Dart code to +# check for errors, warnings, and lints. +# +# The issues identified by the analyzer are surfaced in the UI of Dart-enabled +# IDEs (https://dart.dev/tools#ides-and-editors). The analyzer can also be +# invoked from the command line by running `flutter analyze`. + +# The following line activates a set of recommended lints for Flutter apps, +# packages, and plugins designed to encourage good coding practices. +include: package:flutter_lints/flutter.yaml + +linter: + # The lint rules applied to this project can be customized in the + # section below to disable rules from the `package:flutter_lints/flutter.yaml` + # included above or to enable additional rules. A list of all available lints + # and their documentation is published at https://dart.dev/lints. + # + # Instead of disabling a lint rule for the entire project in the + # section below, it can also be suppressed for a single line of code + # or a specific dart file by using the `// ignore: name_of_lint` and + # `// ignore_for_file: name_of_lint` syntax on the line or in the file + # producing the lint. + rules: + # avoid_print: false # Uncomment to disable the `avoid_print` rule + # prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule + +# Additional information about this file can be found at +# https://dart.dev/guides/language/analysis-options diff --git a/android/.gitignore b/android/.gitignore new file mode 100644 index 0000000..be3943c --- /dev/null +++ b/android/.gitignore @@ -0,0 +1,14 @@ +gradle-wrapper.jar +/.gradle +/captures/ +/gradlew +/gradlew.bat +/local.properties +GeneratedPluginRegistrant.java +.cxx/ + +# Remember to never publicly share your keystore. +# See https://flutter.dev/to/reference-keystore +key.properties +**/*.keystore +**/*.jks diff --git a/android/app/build.gradle.kts b/android/app/build.gradle.kts new file mode 100644 index 0000000..1ab7a8a --- /dev/null +++ b/android/app/build.gradle.kts @@ -0,0 +1,44 @@ +plugins { + id("com.android.application") + id("kotlin-android") + // The Flutter Gradle Plugin must be applied after the Android and Kotlin Gradle plugins. + id("dev.flutter.flutter-gradle-plugin") +} + +android { + namespace = "com.nomadcode.nomadcode" + compileSdk = flutter.compileSdkVersion + ndkVersion = flutter.ndkVersion + + compileOptions { + sourceCompatibility = JavaVersion.VERSION_17 + targetCompatibility = JavaVersion.VERSION_17 + } + + kotlinOptions { + jvmTarget = JavaVersion.VERSION_17.toString() + } + + defaultConfig { + // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). + applicationId = "com.nomadcode.nomadcode" + // You can update the following values to match your application needs. + // For more information, see: https://flutter.dev/to/review-gradle-config. + minSdk = flutter.minSdkVersion + targetSdk = flutter.targetSdkVersion + versionCode = flutter.versionCode + versionName = flutter.versionName + } + + buildTypes { + release { + // TODO: Add your own signing config for the release build. + // Signing with the debug keys for now, so `flutter run --release` works. + signingConfig = signingConfigs.getByName("debug") + } + } +} + +flutter { + source = "../.." +} diff --git a/android/app/src/debug/AndroidManifest.xml b/android/app/src/debug/AndroidManifest.xml new file mode 100644 index 0000000..399f698 --- /dev/null +++ b/android/app/src/debug/AndroidManifest.xml @@ -0,0 +1,7 @@ + + + + diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml new file mode 100644 index 0000000..99c2c49 --- /dev/null +++ b/android/app/src/main/AndroidManifest.xml @@ -0,0 +1,45 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/android/app/src/main/kotlin/com/nomadcode/nomadcode/MainActivity.kt b/android/app/src/main/kotlin/com/nomadcode/nomadcode/MainActivity.kt new file mode 100644 index 0000000..dd5e295 --- /dev/null +++ b/android/app/src/main/kotlin/com/nomadcode/nomadcode/MainActivity.kt @@ -0,0 +1,5 @@ +package com.nomadcode.nomadcode + +import io.flutter.embedding.android.FlutterActivity + +class MainActivity : FlutterActivity() diff --git a/android/app/src/main/res/drawable-v21/launch_background.xml b/android/app/src/main/res/drawable-v21/launch_background.xml new file mode 100644 index 0000000..f74085f --- /dev/null +++ b/android/app/src/main/res/drawable-v21/launch_background.xml @@ -0,0 +1,12 @@ + + + + + + + + diff --git a/android/app/src/main/res/drawable/launch_background.xml b/android/app/src/main/res/drawable/launch_background.xml new file mode 100644 index 0000000..304732f --- /dev/null +++ b/android/app/src/main/res/drawable/launch_background.xml @@ -0,0 +1,12 @@ + + + + + + + + diff --git a/android/app/src/main/res/mipmap-hdpi/ic_launcher.png b/android/app/src/main/res/mipmap-hdpi/ic_launcher.png new file mode 100644 index 0000000000000000000000000000000000000000..db77bb4b7b0906d62b1847e87f15cdcacf6a4f29 GIT binary patch literal 544 zcmeAS@N?(olHy`uVBq!ia0vp^9w5xY3?!3`olAj~WQl7;NpOBzNqJ&XDuZK6ep0G} zXKrG8YEWuoN@d~6R2!h8bpbvhu0Wd6uZuB!w&u2PAxD2eNXD>P5D~Wn-+_Wa#27Xc zC?Zj|6r#X(-D3u$NCt}(Ms06KgJ4FxJVv{GM)!I~&n8Bnc94O7-Hd)cjDZswgC;Qs zO=b+9!WcT8F?0rF7!Uys2bs@gozCP?z~o%U|N3vA*22NaGQG zlg@K`O_XuxvZ&Ks^m&R!`&1=spLvfx7oGDKDwpwW`#iqdw@AL`7MR}m`rwr|mZgU`8P7SBkL78fFf!WnuYWm$5Z0 zNXhDbCv&49sM544K|?c)WrFfiZvCi9h0O)B3Pgg&ebxsLQ05GG~ AQ2+n{ literal 0 HcmV?d00001 diff --git a/android/app/src/main/res/mipmap-mdpi/ic_launcher.png b/android/app/src/main/res/mipmap-mdpi/ic_launcher.png new file mode 100644 index 0000000000000000000000000000000000000000..17987b79bb8a35cc66c3c1fd44f5a5526c1b78be GIT binary patch literal 442 zcmeAS@N?(olHy`uVBq!ia0vp^1|ZDA3?vioaBc-sk|nMYCBgY=CFO}lsSJ)O`AMk? zp1FzXsX?iUDV2pMQ*D5Xx&nMcT!A!W`0S9QKQy;}1Cl^CgaH=;G9cpY;r$Q>i*pfB zP2drbID<_#qf;rPZx^FqH)F_D#*k@@q03KywUtLX8Ua?`H+NMzkczFPK3lFz@i_kW%1NOn0|D2I9n9wzH8m|-tHjsw|9>@K=iMBhxvkv6m8Y-l zytQ?X=U+MF$@3 zt`~i=@j|6y)RWMK--}M|=T`o&^Ni>IoWKHEbBXz7?A@mgWoL>!*SXo`SZH-*HSdS+ yn*9;$7;m`l>wYBC5bq;=U}IMqLzqbYCidGC!)_gkIk_C@Uy!y&wkt5C($~2D>~)O*cj@FGjOCM)M>_ixfudOh)?xMu#Fs z#}Y=@YDTwOM)x{K_j*Q;dPdJ?Mz0n|pLRx{4n|)f>SXlmV)XB04CrSJn#dS5nK2lM zrZ9#~WelCp7&e13Y$jvaEXHskn$2V!!DN-nWS__6T*l;H&Fopn?A6HZ-6WRLFP=R` zqG+CE#d4|IbyAI+rJJ`&x9*T`+a=p|0O(+s{UBcyZdkhj=yS1>AirP+0R;mf2uMgM zC}@~JfByORAh4SyRgi&!(cja>F(l*O+nd+@4m$|6K6KDn_&uvCpV23&>G9HJp{xgg zoq1^2_p9@|WEo z*X_Uko@K)qYYv~>43eQGMdbiGbo>E~Q& zrYBH{QP^@Sti!`2)uG{irBBq@y*$B zi#&(U-*=fp74j)RyIw49+0MRPMRU)+a2r*PJ$L5roHt2$UjExCTZSbq%V!HeS7J$N zdG@vOZB4v_lF7Plrx+hxo7(fCV&}fHq)$ literal 0 HcmV?d00001 diff --git a/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png new file mode 100644 index 0000000000000000000000000000000000000000..d5f1c8d34e7a88e3f88bea192c3a370d44689c3c GIT binary patch literal 1031 zcmeAS@N?(olHy`uVBq!ia0vp^6F``Q8Ax83A=Cw=BuiW)N`mv#O3D+9QW+dm@{>{( zJaZG%Q-e|yQz{EjrrIztFa`(sgt!6~Yi|1%a`XoT0ojZ}lNrNjb9xjc(B0U1_% zz5^97Xt*%oq$rQy4?0GKNfJ44uvxI)gC`h-NZ|&0-7(qS@?b!5r36oQ}zyZrNO3 zMO=Or+<~>+A&uN&E!^Sl+>xE!QC-|oJv`ApDhqC^EWD|@=#J`=d#Xzxs4ah}w&Jnc z$|q_opQ^2TrnVZ0o~wh<3t%W&flvYGe#$xqda2bR_R zvPYgMcHgjZ5nSA^lJr%;<&0do;O^tDDh~=pIxA#coaCY>&N%M2^tq^U%3DB@ynvKo}b?yu-bFc-u0JHzced$sg7S3zqI(2 z#Km{dPr7I=pQ5>FuK#)QwK?Y`E`B?nP+}U)I#c1+FM*1kNvWG|a(TpksZQ3B@sD~b zpQ2)*V*TdwjFOtHvV|;OsiDqHi=6%)o4b!)x$)%9pGTsE z-JL={-Ffv+T87W(Xpooq<`r*VzWQcgBN$$`u}f>-ZQI1BB8ykN*=e4rIsJx9>z}*o zo~|9I;xof literal 0 HcmV?d00001 diff --git a/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png new file mode 100644 index 0000000000000000000000000000000000000000..4d6372eebdb28e45604e46eeda8dd24651419bc0 GIT binary patch literal 1443 zcmb`G{WsKk6vsdJTdFg%tJav9_E4vzrOaqkWF|A724Nly!y+?N9`YV6wZ}5(X(D_N(?!*n3`|_r0Hc?=PQw&*vnU?QTFY zB_MsH|!j$PP;I}?dppoE_gA(4uc!jV&0!l7_;&p2^pxNo>PEcNJv za5_RT$o2Mf!<+r?&EbHH6nMoTsDOa;mN(wv8RNsHpG)`^ymG-S5By8=l9iVXzN_eG%Xg2@Xeq76tTZ*dGh~Lo9vl;Zfs+W#BydUw zCkZ$o1LqWQO$FC9aKlLl*7x9^0q%0}$OMlp@Kk_jHXOjofdePND+j!A{q!8~Jn+s3 z?~~w@4?egS02}8NuulUA=L~QQfm;MzCGd)XhiftT;+zFO&JVyp2mBww?;QByS_1w! zrQlx%{^cMj0|Bo1FjwY@Q8?Hx0cIPF*@-ZRFpPc#bBw{5@tD(5%sClzIfl8WU~V#u zm5Q;_F!wa$BSpqhN>W@2De?TKWR*!ujY;Yylk_X5#~V!L*Gw~;$%4Q8~Mad z@`-kG?yb$a9cHIApZDVZ^U6Xkp<*4rU82O7%}0jjHlK{id@?-wpN*fCHXyXh(bLt* zPc}H-x0e4E&nQ>y%B-(EL=9}RyC%MyX=upHuFhAk&MLbsF0LP-q`XnH78@fT+pKPW zu72MW`|?8ht^tz$iC}ZwLp4tB;Q49K!QCF3@!iB1qOI=?w z7In!}F~ij(18UYUjnbmC!qKhPo%24?8U1x{7o(+?^Zu0Hx81|FuS?bJ0jgBhEMzf< zCgUq7r2OCB(`XkKcN-TL>u5y#dD6D!)5W?`O5)V^>jb)P)GBdy%t$uUMpf$SNV31$ zb||OojAbvMP?T@$h_ZiFLFVHDmbyMhJF|-_)HX3%m=CDI+ID$0^C>kzxprBW)hw(v zr!Gmda);ICoQyhV_oP5+C%?jcG8v+D@9f?Dk*!BxY}dazmrT@64UrP3hlslANK)bq z$67n83eh}OeW&SV@HG95P|bjfqJ7gw$e+`Hxo!4cx`jdK1bJ>YDSpGKLPZ^1cv$ek zIB?0S<#tX?SJCLWdMd{-ME?$hc7A$zBOdIJ)4!KcAwb=VMov)nK;9z>x~rfT1>dS+ zZ6#`2v@`jgbqq)P22H)Tx2CpmM^o1$B+xT6`(v%5xJ(?j#>Q$+rx_R|7TzDZe{J6q zG1*EcU%tE?!kO%^M;3aM6JN*LAKUVb^xz8-Pxo#jR5(-KBeLJvA@-gxNHx0M-ZJLl z;#JwQoh~9V?`UVo#}{6ka@II>++D@%KqGpMdlQ}?9E*wFcf5(#XQnP$Dk5~%iX^>f z%$y;?M0BLp{O3a(-4A?ewryHrrD%cx#Q^%KY1H zNre$ve+vceSLZcNY4U(RBX&)oZn*Py()h)XkE?PL$!bNb{N5FVI2Y%LKEm%yvpyTP z(1P?z~7YxD~Rf<(a@_y` literal 0 HcmV?d00001 diff --git a/android/app/src/main/res/values-night/styles.xml b/android/app/src/main/res/values-night/styles.xml new file mode 100644 index 0000000..06952be --- /dev/null +++ b/android/app/src/main/res/values-night/styles.xml @@ -0,0 +1,18 @@ + + + + + + + diff --git a/android/app/src/main/res/values/styles.xml b/android/app/src/main/res/values/styles.xml new file mode 100644 index 0000000..cb1ef88 --- /dev/null +++ b/android/app/src/main/res/values/styles.xml @@ -0,0 +1,18 @@ + + + + + + + diff --git a/android/app/src/profile/AndroidManifest.xml b/android/app/src/profile/AndroidManifest.xml new file mode 100644 index 0000000..399f698 --- /dev/null +++ b/android/app/src/profile/AndroidManifest.xml @@ -0,0 +1,7 @@ + + + + diff --git a/android/build.gradle.kts b/android/build.gradle.kts new file mode 100644 index 0000000..dbee657 --- /dev/null +++ b/android/build.gradle.kts @@ -0,0 +1,24 @@ +allprojects { + repositories { + google() + mavenCentral() + } +} + +val newBuildDir: Directory = + rootProject.layout.buildDirectory + .dir("../../build") + .get() +rootProject.layout.buildDirectory.value(newBuildDir) + +subprojects { + val newSubprojectBuildDir: Directory = newBuildDir.dir(project.name) + project.layout.buildDirectory.value(newSubprojectBuildDir) +} +subprojects { + project.evaluationDependsOn(":app") +} + +tasks.register("clean") { + delete(rootProject.layout.buildDirectory) +} diff --git a/android/gradle.properties b/android/gradle.properties new file mode 100644 index 0000000..fbee1d8 --- /dev/null +++ b/android/gradle.properties @@ -0,0 +1,2 @@ +org.gradle.jvmargs=-Xmx8G -XX:MaxMetaspaceSize=4G -XX:ReservedCodeCacheSize=512m -XX:+HeapDumpOnOutOfMemoryError +android.useAndroidX=true diff --git a/android/gradle/wrapper/gradle-wrapper.properties b/android/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..e4ef43f --- /dev/null +++ b/android/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,5 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-8.14-all.zip diff --git a/android/nomadcode_android.iml b/android/nomadcode_android.iml new file mode 100644 index 0000000..3bc4b3b --- /dev/null +++ b/android/nomadcode_android.iml @@ -0,0 +1,29 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/android/settings.gradle.kts b/android/settings.gradle.kts new file mode 100644 index 0000000..ca7fe06 --- /dev/null +++ b/android/settings.gradle.kts @@ -0,0 +1,26 @@ +pluginManagement { + val flutterSdkPath = + run { + val properties = java.util.Properties() + file("local.properties").inputStream().use { properties.load(it) } + val flutterSdkPath = properties.getProperty("flutter.sdk") + require(flutterSdkPath != null) { "flutter.sdk not set in local.properties" } + flutterSdkPath + } + + includeBuild("$flutterSdkPath/packages/flutter_tools/gradle") + + repositories { + google() + mavenCentral() + gradlePluginPortal() + } +} + +plugins { + id("dev.flutter.flutter-plugin-loader") version "1.0.0" + id("com.android.application") version "8.11.1" apply false + id("org.jetbrains.kotlin.android") version "2.2.20" apply false +} + +include(":app") diff --git a/ios/.gitignore b/ios/.gitignore new file mode 100644 index 0000000..7a7f987 --- /dev/null +++ b/ios/.gitignore @@ -0,0 +1,34 @@ +**/dgph +*.mode1v3 +*.mode2v3 +*.moved-aside +*.pbxuser +*.perspectivev3 +**/*sync/ +.sconsign.dblite +.tags* +**/.vagrant/ +**/DerivedData/ +Icon? +**/Pods/ +**/.symlinks/ +profile +xcuserdata +**/.generated/ +Flutter/App.framework +Flutter/Flutter.framework +Flutter/Flutter.podspec +Flutter/Generated.xcconfig +Flutter/ephemeral/ +Flutter/app.flx +Flutter/app.zip +Flutter/flutter_assets/ +Flutter/flutter_export_environment.sh +ServiceDefinitions.json +Runner/GeneratedPluginRegistrant.* + +# Exceptions to above rules. +!default.mode1v3 +!default.mode2v3 +!default.pbxuser +!default.perspectivev3 diff --git a/ios/Flutter/AppFrameworkInfo.plist b/ios/Flutter/AppFrameworkInfo.plist new file mode 100644 index 0000000..391a902 --- /dev/null +++ b/ios/Flutter/AppFrameworkInfo.plist @@ -0,0 +1,24 @@ + + + + + CFBundleDevelopmentRegion + en + CFBundleExecutable + App + CFBundleIdentifier + io.flutter.flutter.app + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + App + CFBundlePackageType + FMWK + CFBundleShortVersionString + 1.0 + CFBundleSignature + ???? + CFBundleVersion + 1.0 + + diff --git a/ios/Flutter/Debug.xcconfig b/ios/Flutter/Debug.xcconfig new file mode 100644 index 0000000..592ceee --- /dev/null +++ b/ios/Flutter/Debug.xcconfig @@ -0,0 +1 @@ +#include "Generated.xcconfig" diff --git a/ios/Flutter/Release.xcconfig b/ios/Flutter/Release.xcconfig new file mode 100644 index 0000000..592ceee --- /dev/null +++ b/ios/Flutter/Release.xcconfig @@ -0,0 +1 @@ +#include "Generated.xcconfig" diff --git a/ios/Runner.xcodeproj/project.pbxproj b/ios/Runner.xcodeproj/project.pbxproj new file mode 100644 index 0000000..19d9d87 --- /dev/null +++ b/ios/Runner.xcodeproj/project.pbxproj @@ -0,0 +1,620 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 54; + objects = { + +/* Begin PBXBuildFile section */ + 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; }; + 331C808B294A63AB00263BE5 /* RunnerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 331C807B294A618700263BE5 /* RunnerTests.swift */; }; + 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; }; + 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; }; + 7884E8682EC3CC0700C636F2 /* SceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7884E8672EC3CC0400C636F2 /* SceneDelegate.swift */; }; + 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; }; + 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; }; + 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; }; +/* End PBXBuildFile section */ + +/* Begin PBXContainerItemProxy section */ + 331C8085294A63A400263BE5 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 97C146E61CF9000F007C117D /* Project object */; + proxyType = 1; + remoteGlobalIDString = 97C146ED1CF9000F007C117D; + remoteInfo = Runner; + }; +/* End PBXContainerItemProxy section */ + +/* Begin PBXCopyFilesBuildPhase section */ + 9705A1C41CF9048500538489 /* Embed Frameworks */ = { + isa = PBXCopyFilesBuildPhase; + buildActionMask = 2147483647; + dstPath = ""; + dstSubfolderSpec = 10; + files = ( + ); + name = "Embed Frameworks"; + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXCopyFilesBuildPhase section */ + +/* Begin PBXFileReference section */ + 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = ""; }; + 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = ""; }; + 331C807B294A618700263BE5 /* RunnerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RunnerTests.swift; sourceTree = ""; }; + 331C8081294A63A400263BE5 /* RunnerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; + 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = ""; }; + 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = ""; }; + 74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; + 7884E8672EC3CC0400C636F2 /* SceneDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SceneDelegate.swift; sourceTree = ""; }; + 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = ""; }; + 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = ""; }; + 9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = ""; }; + 97C146EE1CF9000F007C117D /* Runner.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Runner.app; sourceTree = BUILT_PRODUCTS_DIR; }; + 97C146FB1CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; + 97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; + 97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; + 97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 97C146EB1CF9000F007C117D /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 331C8082294A63A400263BE5 /* RunnerTests */ = { + isa = PBXGroup; + children = ( + 331C807B294A618700263BE5 /* RunnerTests.swift */, + ); + path = RunnerTests; + sourceTree = ""; + }; + 9740EEB11CF90186004384FC /* Flutter */ = { + isa = PBXGroup; + children = ( + 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */, + 9740EEB21CF90195004384FC /* Debug.xcconfig */, + 7AFA3C8E1D35360C0083082E /* Release.xcconfig */, + 9740EEB31CF90195004384FC /* Generated.xcconfig */, + ); + name = Flutter; + sourceTree = ""; + }; + 97C146E51CF9000F007C117D = { + isa = PBXGroup; + children = ( + 9740EEB11CF90186004384FC /* Flutter */, + 97C146F01CF9000F007C117D /* Runner */, + 97C146EF1CF9000F007C117D /* Products */, + 331C8082294A63A400263BE5 /* RunnerTests */, + ); + sourceTree = ""; + }; + 97C146EF1CF9000F007C117D /* Products */ = { + isa = PBXGroup; + children = ( + 97C146EE1CF9000F007C117D /* Runner.app */, + 331C8081294A63A400263BE5 /* RunnerTests.xctest */, + ); + name = Products; + sourceTree = ""; + }; + 97C146F01CF9000F007C117D /* Runner */ = { + isa = PBXGroup; + children = ( + 97C146FA1CF9000F007C117D /* Main.storyboard */, + 97C146FD1CF9000F007C117D /* Assets.xcassets */, + 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */, + 97C147021CF9000F007C117D /* Info.plist */, + 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */, + 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */, + 74858FAE1ED2DC5600515810 /* AppDelegate.swift */, + 7884E8672EC3CC0400C636F2 /* SceneDelegate.swift */, + 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */, + ); + path = Runner; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + 331C8080294A63A400263BE5 /* RunnerTests */ = { + isa = PBXNativeTarget; + buildConfigurationList = 331C8087294A63A400263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */; + buildPhases = ( + 331C807D294A63A400263BE5 /* Sources */, + 331C807F294A63A400263BE5 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + 331C8086294A63A400263BE5 /* PBXTargetDependency */, + ); + name = RunnerTests; + productName = RunnerTests; + productReference = 331C8081294A63A400263BE5 /* RunnerTests.xctest */; + productType = "com.apple.product-type.bundle.unit-test"; + }; + 97C146ED1CF9000F007C117D /* Runner */ = { + isa = PBXNativeTarget; + buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */; + buildPhases = ( + 9740EEB61CF901F6004384FC /* Run Script */, + 97C146EA1CF9000F007C117D /* Sources */, + 97C146EB1CF9000F007C117D /* Frameworks */, + 97C146EC1CF9000F007C117D /* Resources */, + 9705A1C41CF9048500538489 /* Embed Frameworks */, + 3B06AD1E1E4923F5004D2608 /* Thin Binary */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = Runner; + productName = Runner; + productReference = 97C146EE1CF9000F007C117D /* Runner.app */; + productType = "com.apple.product-type.application"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 97C146E61CF9000F007C117D /* Project object */ = { + isa = PBXProject; + attributes = { + BuildIndependentTargetsInParallel = YES; + LastUpgradeCheck = 1510; + ORGANIZATIONNAME = ""; + TargetAttributes = { + 331C8080294A63A400263BE5 = { + CreatedOnToolsVersion = 14.0; + TestTargetID = 97C146ED1CF9000F007C117D; + }; + 97C146ED1CF9000F007C117D = { + CreatedOnToolsVersion = 7.3.1; + LastSwiftMigration = 1100; + }; + }; + }; + buildConfigurationList = 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */; + compatibilityVersion = "Xcode 9.3"; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = 97C146E51CF9000F007C117D; + productRefGroup = 97C146EF1CF9000F007C117D /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 97C146ED1CF9000F007C117D /* Runner */, + 331C8080294A63A400263BE5 /* RunnerTests */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + 331C807F294A63A400263BE5 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 97C146EC1CF9000F007C117D /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */, + 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */, + 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */, + 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXShellScriptBuildPhase section */ + 3B06AD1E1E4923F5004D2608 /* Thin Binary */ = { + isa = PBXShellScriptBuildPhase; + alwaysOutOfDate = 1; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + "${TARGET_BUILD_DIR}/${INFOPLIST_PATH}", + ); + name = "Thin Binary"; + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin"; + }; + 9740EEB61CF901F6004384FC /* Run Script */ = { + isa = PBXShellScriptBuildPhase; + alwaysOutOfDate = 1; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + ); + name = "Run Script"; + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build"; + }; +/* End PBXShellScriptBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 331C807D294A63A400263BE5 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 331C808B294A63AB00263BE5 /* RunnerTests.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 97C146EA1CF9000F007C117D /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */, + 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */, + 7884E8682EC3CC0700C636F2 /* SceneDelegate.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin PBXTargetDependency section */ + 331C8086294A63A400263BE5 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 97C146ED1CF9000F007C117D /* Runner */; + targetProxy = 331C8085294A63A400263BE5 /* PBXContainerItemProxy */; + }; +/* End PBXTargetDependency section */ + +/* Begin PBXVariantGroup section */ + 97C146FA1CF9000F007C117D /* Main.storyboard */ = { + isa = PBXVariantGroup; + children = ( + 97C146FB1CF9000F007C117D /* Base */, + ); + name = Main.storyboard; + sourceTree = ""; + }; + 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */ = { + isa = PBXVariantGroup; + children = ( + 97C147001CF9000F007C117D /* Base */, + ); + name = LaunchScreen.storyboard; + sourceTree = ""; + }; +/* End PBXVariantGroup section */ + +/* Begin XCBuildConfiguration section */ + 249021D3217E4FDB00AE95B9 /* Profile */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_USER_SCRIPT_SANDBOXING = NO; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 13.0; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = iphoneos; + SUPPORTED_PLATFORMS = iphoneos; + TARGETED_DEVICE_FAMILY = "1,2"; + VALIDATE_PRODUCT = YES; + }; + name = Profile; + }; + 249021D4217E4FDB00AE95B9 /* Profile */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; + ENABLE_BITCODE = NO; + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = com.nomadcode.nomadcode; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; + SWIFT_VERSION = 5.0; + VERSIONING_SYSTEM = "apple-generic"; + }; + name = Profile; + }; + 331C8088294A63A400263BE5 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + GENERATE_INFOPLIST_FILE = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.nomadcode.nomadcode.RunnerTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 5.0; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner"; + }; + name = Debug; + }; + 331C8089294A63A400263BE5 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + GENERATE_INFOPLIST_FILE = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.nomadcode.nomadcode.RunnerTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 5.0; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner"; + }; + name = Release; + }; + 331C808A294A63A400263BE5 /* Profile */ = { + isa = XCBuildConfiguration; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + GENERATE_INFOPLIST_FILE = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.nomadcode.nomadcode.RunnerTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 5.0; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner"; + }; + name = Profile; + }; + 97C147031CF9000F007C117D /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = dwarf; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + ENABLE_USER_SCRIPT_SANDBOXING = NO; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 13.0; + MTL_ENABLE_DEBUG_INFO = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = iphoneos; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + 97C147041CF9000F007C117D /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_USER_SCRIPT_SANDBOXING = NO; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 13.0; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = iphoneos; + SUPPORTED_PLATFORMS = iphoneos; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_OPTIMIZATION_LEVEL = "-O"; + TARGETED_DEVICE_FAMILY = "1,2"; + VALIDATE_PRODUCT = YES; + }; + name = Release; + }; + 97C147061CF9000F007C117D /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; + ENABLE_BITCODE = NO; + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = com.nomadcode.nomadcode; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 5.0; + VERSIONING_SYSTEM = "apple-generic"; + }; + name = Debug; + }; + 97C147071CF9000F007C117D /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; + ENABLE_BITCODE = NO; + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = com.nomadcode.nomadcode; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; + SWIFT_VERSION = 5.0; + VERSIONING_SYSTEM = "apple-generic"; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 331C8087294A63A400263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 331C8088294A63A400263BE5 /* Debug */, + 331C8089294A63A400263BE5 /* Release */, + 331C808A294A63A400263BE5 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 97C147031CF9000F007C117D /* Debug */, + 97C147041CF9000F007C117D /* Release */, + 249021D3217E4FDB00AE95B9 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 97C147061CF9000F007C117D /* Debug */, + 97C147071CF9000F007C117D /* Release */, + 249021D4217E4FDB00AE95B9 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = 97C146E61CF9000F007C117D /* Project object */; +} diff --git a/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata new file mode 100644 index 0000000..919434a --- /dev/null +++ b/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 0000000..18d9810 --- /dev/null +++ b/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings b/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings new file mode 100644 index 0000000..f9b0d7c --- /dev/null +++ b/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings @@ -0,0 +1,8 @@ + + + + + PreviewsEnabled + + + diff --git a/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme new file mode 100644 index 0000000..e3773d4 --- /dev/null +++ b/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -0,0 +1,101 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ios/Runner.xcworkspace/contents.xcworkspacedata b/ios/Runner.xcworkspace/contents.xcworkspacedata new file mode 100644 index 0000000..1d526a1 --- /dev/null +++ b/ios/Runner.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 0000000..18d9810 --- /dev/null +++ b/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings b/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings new file mode 100644 index 0000000..f9b0d7c --- /dev/null +++ b/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings @@ -0,0 +1,8 @@ + + + + + PreviewsEnabled + + + diff --git a/ios/Runner/AppDelegate.swift b/ios/Runner/AppDelegate.swift new file mode 100644 index 0000000..c30b367 --- /dev/null +++ b/ios/Runner/AppDelegate.swift @@ -0,0 +1,16 @@ +import Flutter +import UIKit + +@main +@objc class AppDelegate: FlutterAppDelegate, FlutterImplicitEngineDelegate { + override func application( + _ application: UIApplication, + didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? + ) -> Bool { + return super.application(application, didFinishLaunchingWithOptions: launchOptions) + } + + func didInitializeImplicitFlutterEngine(_ engineBridge: FlutterImplicitEngineBridge) { + GeneratedPluginRegistrant.register(with: engineBridge.pluginRegistry) + } +} diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 0000000..d36b1fa --- /dev/null +++ b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,122 @@ +{ + "images" : [ + { + "size" : "20x20", + "idiom" : "iphone", + "filename" : "Icon-App-20x20@2x.png", + "scale" : "2x" + }, + { + "size" : "20x20", + "idiom" : "iphone", + "filename" : "Icon-App-20x20@3x.png", + "scale" : "3x" + }, + { + "size" : "29x29", + "idiom" : "iphone", + "filename" : "Icon-App-29x29@1x.png", + "scale" : "1x" + }, + { + "size" : "29x29", + "idiom" : "iphone", + "filename" : "Icon-App-29x29@2x.png", + "scale" : "2x" + }, + { + "size" : "29x29", + "idiom" : "iphone", + "filename" : "Icon-App-29x29@3x.png", + "scale" : "3x" + }, + { + "size" : "40x40", + "idiom" : "iphone", + "filename" : "Icon-App-40x40@2x.png", + "scale" : "2x" + }, + { + "size" : "40x40", + "idiom" : "iphone", + "filename" : "Icon-App-40x40@3x.png", + "scale" : "3x" + }, + { + "size" : "60x60", + "idiom" : "iphone", + "filename" : "Icon-App-60x60@2x.png", + "scale" : "2x" + }, + { + "size" : "60x60", + "idiom" : "iphone", + "filename" : "Icon-App-60x60@3x.png", + "scale" : "3x" + }, + { + "size" : "20x20", + "idiom" : "ipad", + "filename" : "Icon-App-20x20@1x.png", + "scale" : "1x" + }, + { + "size" : "20x20", + "idiom" : "ipad", + "filename" : "Icon-App-20x20@2x.png", + "scale" : "2x" + }, + { + "size" : "29x29", + "idiom" : "ipad", + "filename" : "Icon-App-29x29@1x.png", + "scale" : "1x" + }, + { + "size" : "29x29", + "idiom" : "ipad", + "filename" : "Icon-App-29x29@2x.png", + "scale" : "2x" + }, + { + "size" : "40x40", + "idiom" : "ipad", + "filename" : "Icon-App-40x40@1x.png", + "scale" : "1x" + }, + { + "size" : "40x40", + "idiom" : "ipad", + "filename" : "Icon-App-40x40@2x.png", + "scale" : "2x" + }, + { + "size" : "76x76", + "idiom" : "ipad", + "filename" : "Icon-App-76x76@1x.png", + "scale" : "1x" + }, + { + "size" : "76x76", + "idiom" : "ipad", + "filename" : "Icon-App-76x76@2x.png", + "scale" : "2x" + }, + { + "size" : "83.5x83.5", + "idiom" : "ipad", + "filename" : "Icon-App-83.5x83.5@2x.png", + "scale" : "2x" + }, + { + "size" : "1024x1024", + "idiom" : "ios-marketing", + "filename" : "Icon-App-1024x1024@1x.png", + "scale" : "1x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png new file mode 100644 index 0000000000000000000000000000000000000000..dc9ada4725e9b0ddb1deab583e5b5102493aa332 GIT binary patch literal 10932 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 literal 0 HcmV?d00001 diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..797d452e458972bab9d994556c8305db4c827017 GIT binary patch literal 406 zcmV;H0crk;P))>cdjpWt&rLJgVp-t?DREyuq1A%0Z4)6_WsQ7{nzjN zo!X zGXV)2i3kcZIL~_j>uIKPK_zib+3T+Nt3Mb&Br)s)UIaA}@p{wDda>7=Q|mGRp7pqY zkJ!7E{MNz$9nOwoVqpFb)}$IP24Wn2JJ=Cw(!`OXJBr45rP>>AQr$6c7slJWvbpNW z@KTwna6d?PP>hvXCcp=4F;=GR@R4E7{4VU^0p4F>v^#A|>07*qoM6N<$f*5nx ACIA2c literal 0 HcmV?d00001 diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png new file mode 100644 index 0000000000000000000000000000000000000000..6ed2d933e1120817fe9182483a228007b18ab6ae GIT binary patch literal 450 zcmV;z0X_bSP)iGWQ_5NJQ_~rNh*z)}eT%KUb z`7gNk0#AwF^#0T0?hIa^`~Ck;!}#m+_uT050aTR(J!bU#|IzRL%^UsMS#KsYnTF*!YeDOytlP4VhV?b} z%rz_<=#CPc)tU1MZTq~*2=8~iZ!lSa<{9b@2Jl;?IEV8)=fG217*|@)CCYgFze-x? zIFODUIA>nWKpE+bn~n7;-89sa>#DR>TSlqWk*!2hSN6D~Qb#VqbP~4Fk&m`@1$JGr zXPIdeRE&b2Thd#{MtDK$px*d3-Wx``>!oimf%|A-&-q*6KAH)e$3|6JV%HX{Hig)k suLT-RhftRq8b9;(V=235Wa|I=027H2wCDra;{X5v07*qoM6N<$f;9x^2LJ#7 literal 0 HcmV?d00001 diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png new file mode 100644 index 0000000000000000000000000000000000000000..4cd7b0099ca80c806f8fe495613e8d6c69460d76 GIT binary patch literal 282 zcmV+#0p(^bcu7P-R4C8Q z&e;xxFbF_Vrezo%_kH*OKhshZ6BFpG-Y1e10`QXJKbND7AMQ&cMj60B5TNObaZxYybcN07*qoM6N<$g3m;S%K!iX literal 0 HcmV?d00001 diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..fe730945a01f64a61e2235dbe3f45b08f7729182 GIT binary patch literal 462 zcmV;<0WtoGP)-}iV`2<;=$?g5M=KQbZ{F&YRNy7Nn@%_*5{gvDM0aKI4?ESmw z{NnZg)A0R`+4?NF_RZexyVB&^^ZvN!{I28tr{Vje;QNTz`dG&Jz0~Ek&f2;*Z7>B|cg}xYpxEFY+0YrKLF;^Q+-HreN0P{&i zK~zY`?b7ECf-n?@;d<&orQ*Q7KoR%4|C>{W^h6@&01>0SKS`dn{Q}GT%Qj_{PLZ_& zs`MFI#j-(>?bvdZ!8^xTwlY{qA)T4QLbY@j(!YJ7aXJervHy6HaG_2SB`6CC{He}f zHVw(fJWApwPq!6VY7r1w-Fs)@ox~N+q|w~e;JI~C4Vf^@d>Wvj=fl`^u9x9wd9 zR%3*Q+)t%S!MU_`id^@&Y{y7-r98lZX0?YrHlfmwb?#}^1b{8g&KzmkE(L>Z&)179 zp<)v6Y}pRl100G2FL_t(o!|l{-Q-VMg#&MKg7c{O0 z2wJImOS3Gy*Z2Qifdv~JYOp;v+U)a|nLoc7hNH;I$;lzDt$}rkaFw1mYK5_0Q(Sut zvbEloxON7$+HSOgC9Z8ltuC&0OSF!-mXv5caV>#bc3@hBPX@I$58-z}(ZZE!t-aOG zpjNkbau@>yEzH(5Yj4kZiMH32XI!4~gVXNnjAvRx;Sdg^`>2DpUEwoMhTs_st8pKG z(%SHyHdU&v%f36~uERh!bd`!T2dw;z6PrOTQ7Vt*#9F2uHlUVnb#ev_o^fh}Dzmq} zWtlk35}k=?xj28uO|5>>$yXadTUE@@IPpgH`gJ~Ro4>jd1IF|(+IX>8M4Ps{PNvmI zNj4D+XgN83gPt_Gm}`Ybv{;+&yu-C(Grdiahmo~BjG-l&mWM+{e5M1sm&=xduwgM9 z`8OEh`=F3r`^E{n_;%9weN{cf2%7=VzC@cYj+lg>+3|D|_1C@{hcU(DyQG_BvBWe? zvTv``=%b1zrol#=R`JB)>cdjpWt&rLJgVp-t?DREyuq1A%0Z4)6_WsQ7{nzjN zo!X zGXV)2i3kcZIL~_j>uIKPK_zib+3T+Nt3Mb&Br)s)UIaA}@p{wDda>7=Q|mGRp7pqY zkJ!7E{MNz$9nOwoVqpFb)}$IP24Wn2JJ=Cw(!`OXJBr45rP>>AQr$6c7slJWvbpNW z@KTwna6d?PP>hvXCcp=4F;=GR@R4E7{4VU^0p4F>v^#A|>07*qoM6N<$f*5nx ACIA2c literal 0 HcmV?d00001 diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..502f463a9bc882b461c96aadf492d1729e49e725 GIT binary patch literal 586 zcmV-Q0=4~#P)+}#`wDE{8-2Mebf5<{{PqV{TgVcv*r8?UZ3{-|G?_}T*&y;@cqf{ z{Q*~+qr%%p!1pS*_Uicl#q9lc(D`!D`LN62sNwq{oYw(Wmhk)k<@f$!$@ng~_5)Ru z0Z)trIA5^j{DIW^c+vT2%lW+2<(RtE2wR;4O@)Tm`Xr*?A(qYoM}7i5Yxw>D(&6ou zxz!_Xr~yNF+waPe00049Nkl*;a!v6h%{rlvIH#gW3s8p;bFr=l}mRqpW2h zw=OA%hdyL~z+UHOzl0eKhEr$YYOL-c-%Y<)=j?(bzDweB7{b+%_ypvm_cG{SvM=DK zhv{K@m>#Bw>2W$eUI#iU)Wdgs8Y3U+A$Gd&{+j)d)BmGKx+43U_!tik_YlN)>$7G! zhkE!s;%oku3;IwG3U^2kw?z+HM)jB{@zFhK8P#KMSytSthr+4!c(5c%+^UBn`0X*2 zy3(k600_CSZj?O$Qu%&$;|TGUJrptR(HzyIx>5E(2r{eA(<6t3e3I0B)7d6s7?Z5J zZ!rtKvA{MiEBm&KFtoifx>5P^Z=vl)95XJn()aS5%ad(s?4-=Tkis9IGu{`Fy8r+H07*qoM6N<$f20Z)wqMt%V?S?~D#06};F zA3KcL`Wb+>5ObvgQIG&ig8(;V04hz?@cqy3{mSh8o!|U|)cI!1_+!fWH@o*8vh^CU z^ws0;(c$gI+2~q^tO#GDHf@=;DncUw00J^eL_t(&-tE|HQ`%4vfZ;WsBqu-$0nu1R zq^Vj;p$clf^?twn|KHO+IGt^q#a3X?w9dXC@*yxhv&l}F322(8Y1&=P&I}~G@#h6; z1CV9ecD9ZEe87{{NtI*)_aJ<`kJa z?5=RBtFF50s;jQLFil-`)m2wrb=6h(&brpj%nG_U&ut~$?8Rokzxi8zJoWr#2dto5 zOX_URcc<1`Iky+jc;A%Vzx}1QU{2$|cKPom2Vf1{8m`vja4{F>HS?^Nc^rp}xo+Nh zxd}eOm`fm3@MQC1< zIk&aCjb~Yh%5+Yq0`)D;q{#-Uqlv*o+Oor zE!I71Z@ASH3grl8&P^L0WpavHoP|UX4e?!igT`4?AZk$hu*@%6WJ;zDOGlw7kj@ zY5!B-0ft0f?Lgb>C;$Ke07*qoM6N<$f~t1N9smFU literal 0 HcmV?d00001 diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..0ec303439225b78712f49115768196d8d76f6790 GIT binary patch literal 862 zcmV-k1EKthP)20Z)wqMt%V?S?~D#06};F zA3KcL`Wb+>5ObvgQIG&ig8(;V04hz?@cqy3{mSh8o!|U|)cI!1_+!fWH@o*8vh^CU z^ws0;(c$gI+2~q^tO#GDHf@=;DncUw00J^eL_t(&-tE|HQ`%4vfZ;WsBqu-$0nu1R zq^Vj;p$clf^?twn|KHO+IGt^q#a3X?w9dXC@*yxhv&l}F322(8Y1&=P&I}~G@#h6; z1CV9ecD9ZEe87{{NtI*)_aJ<`kJa z?5=RBtFF50s;jQLFil-`)m2wrb=6h(&brpj%nG_U&ut~$?8Rokzxi8zJoWr#2dto5 zOX_URcc<1`Iky+jc;A%Vzx}1QU{2$|cKPom2Vf1{8m`vja4{F>HS?^Nc^rp}xo+Nh zxd}eOm`fm3@MQC1< zIk&aCjb~Yh%5+Yq0`)D;q{#-Uqlv*o+Oor zE!I71Z@ASH3grl8&P^L0WpavHoP|UX4e?!igT`4?AZk$hu*@%6WJ;zDOGlw7kj@ zY5!B-0ft0f?Lgb>C;$Ke07*qoM6N<$f~t1N9smFU literal 0 HcmV?d00001 diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png new file mode 100644 index 0000000000000000000000000000000000000000..e9f5fea27c705180eb716271f41b582e76dcbd90 GIT binary patch literal 1674 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 literal 0 HcmV?d00001 diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..0467bf12aa4d28f374bb26596605a46dcbb3e7c8 GIT binary patch literal 1418 zcmV;51$Fv~P)q zKfU)WzW*n(@|xWGCA9ScMt*e9`2kdxPQ&&>|-UCa7_51w+ zLUsW@ZzZSW0y$)Hp~e9%PvP|a03ks1`~K?q{u;6NC8*{AOqIUq{CL&;p56Lf$oQGq z^={4hPQv)y=I|4n+?>7Fim=dxt1 z2H+Dm+1+fh+IF>G0SjJMkQQre1x4|G*Z==(Ot&kCnUrL4I(rf(ucITwmuHf^hXiJT zkdTm&kdTm&kdTm&kdP`esgWG0BcWCVkVZ&2dUwN`cgM8QJb`Z7Z~e<&Yj2(}>Tmf` zm1{eLgw!b{bXkjWbF%dTkTZEJWyWOb##Lfw4EK2}<0d6%>AGS{po>WCOy&f$Tay_> z?NBlkpo@s-O;0V%Y_Xa-G#_O08q5LR*~F%&)}{}r&L%Sbs8AS4t7Y0NEx*{soY=0MZExqA5XHQkqi#4gW3 zqODM^iyZl;dvf)-bOXtOru(s)Uc7~BFx{w-FK;2{`VA?(g&@3z&bfLFyctOH!cVsF z7IL=fo-qBndRUm;kAdXR4e6>k-z|21AaN%ubeVrHl*<|s&Ax@W-t?LR(P-24A5=>a z*R9#QvjzF8n%@1Nw@?CG@6(%>+-0ASK~jEmCV|&a*7-GKT72W<(TbSjf)&Eme6nGE z>Gkj4Sq&2e+-G%|+NM8OOm5zVl9{Z8Dd8A5z3y8mZ=4Bv4%>as_{9cN#bm~;h>62( zdqY93Zy}v&c4n($Vv!UybR8ocs7#zbfX1IY-*w~)p}XyZ-SFC~4w>BvMVr`dFbelV{lLL0bx7@*ZZdebr3`sP;? zVImji)kG)(6Juv0lz@q`F!k1FE;CQ(D0iG$wchPbKZQELlsZ#~rt8#90Y_Xh&3U-< z{s<&cCV_1`^TD^ia9!*mQDq& zn2{r`j};V|uV%_wsP!zB?m%;FeaRe+X47K0e+KE!8C{gAWF8)lCd1u1%~|M!XNRvw zvtqy3iz0WSpWdhn6$hP8PaRBmp)q`#PCA`Vd#Tc$@f1tAcM>f_I@bC)hkI9|o(Iqv zo}Piadq!j76}004RBio<`)70k^`K1NK)q>w?p^C6J2ZC!+UppiK6&y3Kmbv&O!oYF z34$0Z;QO!JOY#!`qyGH<3Pd}Pt@q*A0V=3SVtWKRR8d8Z&@)3qLPA19LPA19LPEUC YUoZo%k(ykuW&i*H07*qoM6N<$f+CH{y8r+H literal 0 HcmV?d00001 diff --git a/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json b/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json new file mode 100644 index 0000000..0bedcf2 --- /dev/null +++ b/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "LaunchImage.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "LaunchImage@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "filename" : "LaunchImage@3x.png", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} diff --git a/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png b/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png new file mode 100644 index 0000000000000000000000000000000000000000..9da19eacad3b03bb08bbddbbf4ac48dd78b3d838 GIT binary patch literal 68 zcmeAS@N?(olHy`uVBq!ia0vp^j3CUx0wlM}@Gt=>Zci7-kcv6Uzs@r-FtIZ-&5|)J Q1PU{Fy85}Sb4q9e0B4a5jsO4v literal 0 HcmV?d00001 diff --git a/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png b/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..9da19eacad3b03bb08bbddbbf4ac48dd78b3d838 GIT binary patch literal 68 zcmeAS@N?(olHy`uVBq!ia0vp^j3CUx0wlM}@Gt=>Zci7-kcv6Uzs@r-FtIZ-&5|)J Q1PU{Fy85}Sb4q9e0B4a5jsO4v literal 0 HcmV?d00001 diff --git a/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png b/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png new file mode 100644 index 0000000000000000000000000000000000000000..9da19eacad3b03bb08bbddbbf4ac48dd78b3d838 GIT binary patch literal 68 zcmeAS@N?(olHy`uVBq!ia0vp^j3CUx0wlM}@Gt=>Zci7-kcv6Uzs@r-FtIZ-&5|)J Q1PU{Fy85}Sb4q9e0B4a5jsO4v literal 0 HcmV?d00001 diff --git a/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md b/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md new file mode 100644 index 0000000..89c2725 --- /dev/null +++ b/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md @@ -0,0 +1,5 @@ +# Launch Screen Assets + +You can customize the launch screen with your own desired assets by replacing the image files in this directory. + +You can also do it by opening your Flutter project's Xcode project with `open ios/Runner.xcworkspace`, selecting `Runner/Assets.xcassets` in the Project Navigator and dropping in the desired images. \ No newline at end of file diff --git a/ios/Runner/Base.lproj/LaunchScreen.storyboard b/ios/Runner/Base.lproj/LaunchScreen.storyboard new file mode 100644 index 0000000..f2e259c --- /dev/null +++ b/ios/Runner/Base.lproj/LaunchScreen.storyboard @@ -0,0 +1,37 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ios/Runner/Base.lproj/Main.storyboard b/ios/Runner/Base.lproj/Main.storyboard new file mode 100644 index 0000000..f3c2851 --- /dev/null +++ b/ios/Runner/Base.lproj/Main.storyboard @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ios/Runner/Info.plist b/ios/Runner/Info.plist new file mode 100644 index 0000000..93cd453 --- /dev/null +++ b/ios/Runner/Info.plist @@ -0,0 +1,70 @@ + + + + + CADisableMinimumFrameDurationOnPhone + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleDisplayName + Nomadcode + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + nomadcode + CFBundlePackageType + APPL + CFBundleShortVersionString + $(FLUTTER_BUILD_NAME) + CFBundleSignature + ???? + CFBundleVersion + $(FLUTTER_BUILD_NUMBER) + LSRequiresIPhoneOS + + UIApplicationSceneManifest + + UIApplicationSupportsMultipleScenes + + UISceneConfigurations + + UIWindowSceneSessionRoleApplication + + + UISceneClassName + UIWindowScene + UISceneConfigurationName + flutter + UISceneDelegateClassName + $(PRODUCT_MODULE_NAME).SceneDelegate + UISceneStoryboardFile + Main + + + + + UIApplicationSupportsIndirectInputEvents + + UILaunchStoryboardName + LaunchScreen + UIMainStoryboardFile + Main + UISupportedInterfaceOrientations + + UIInterfaceOrientationPortrait + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + UISupportedInterfaceOrientations~ipad + + UIInterfaceOrientationPortrait + UIInterfaceOrientationPortraitUpsideDown + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + + diff --git a/ios/Runner/Runner-Bridging-Header.h b/ios/Runner/Runner-Bridging-Header.h new file mode 100644 index 0000000..308a2a5 --- /dev/null +++ b/ios/Runner/Runner-Bridging-Header.h @@ -0,0 +1 @@ +#import "GeneratedPluginRegistrant.h" diff --git a/ios/Runner/SceneDelegate.swift b/ios/Runner/SceneDelegate.swift new file mode 100644 index 0000000..b9ce8ea --- /dev/null +++ b/ios/Runner/SceneDelegate.swift @@ -0,0 +1,6 @@ +import Flutter +import UIKit + +class SceneDelegate: FlutterSceneDelegate { + +} diff --git a/ios/RunnerTests/RunnerTests.swift b/ios/RunnerTests/RunnerTests.swift new file mode 100644 index 0000000..86a7c3b --- /dev/null +++ b/ios/RunnerTests/RunnerTests.swift @@ -0,0 +1,12 @@ +import Flutter +import UIKit +import XCTest + +class RunnerTests: XCTestCase { + + func testExample() { + // If you add code to the Runner application, consider adding tests here. + // See https://developer.apple.com/documentation/xctest for more information about using XCTest. + } + +} diff --git a/lib/main.dart b/lib/main.dart new file mode 100644 index 0000000..244a702 --- /dev/null +++ b/lib/main.dart @@ -0,0 +1,122 @@ +import 'package:flutter/material.dart'; + +void main() { + runApp(const MyApp()); +} + +class MyApp extends StatelessWidget { + const MyApp({super.key}); + + // This widget is the root of your application. + @override + Widget build(BuildContext context) { + return MaterialApp( + title: 'Flutter Demo', + theme: ThemeData( + // This is the theme of your application. + // + // TRY THIS: Try running your application with "flutter run". You'll see + // the application has a purple toolbar. Then, without quitting the app, + // try changing the seedColor in the colorScheme below to Colors.green + // and then invoke "hot reload" (save your changes or press the "hot + // reload" button in a Flutter-supported IDE, or press "r" if you used + // the command line to start the app). + // + // Notice that the counter didn't reset back to zero; the application + // state is not lost during the reload. To reset the state, use hot + // restart instead. + // + // This works for code too, not just values: Most code changes can be + // tested with just a hot reload. + colorScheme: .fromSeed(seedColor: Colors.deepPurple), + ), + home: const MyHomePage(title: 'Flutter Demo Home Page'), + ); + } +} + +class MyHomePage extends StatefulWidget { + const MyHomePage({super.key, required this.title}); + + // This widget is the home page of your application. It is stateful, meaning + // that it has a State object (defined below) that contains fields that affect + // how it looks. + + // This class is the configuration for the state. It holds the values (in this + // case the title) provided by the parent (in this case the App widget) and + // used by the build method of the State. Fields in a Widget subclass are + // always marked "final". + + final String title; + + @override + State createState() => _MyHomePageState(); +} + +class _MyHomePageState extends State { + int _counter = 0; + + void _incrementCounter() { + setState(() { + // This call to setState tells the Flutter framework that something has + // changed in this State, which causes it to rerun the build method below + // so that the display can reflect the updated values. If we changed + // _counter without calling setState(), then the build method would not be + // called again, and so nothing would appear to happen. + _counter++; + }); + } + + @override + Widget build(BuildContext context) { + // This method is rerun every time setState is called, for instance as done + // by the _incrementCounter method above. + // + // The Flutter framework has been optimized to make rerunning build methods + // fast, so that you can just rebuild anything that needs updating rather + // than having to individually change instances of widgets. + return Scaffold( + appBar: AppBar( + // TRY THIS: Try changing the color here to a specific color (to + // Colors.amber, perhaps?) and trigger a hot reload to see the AppBar + // change color while the other colors stay the same. + backgroundColor: Theme.of(context).colorScheme.inversePrimary, + // Here we take the value from the MyHomePage object that was created by + // the App.build method, and use it to set our appbar title. + title: Text(widget.title), + ), + body: Center( + // Center is a layout widget. It takes a single child and positions it + // in the middle of the parent. + child: Column( + // Column is also a layout widget. It takes a list of children and + // arranges them vertically. By default, it sizes itself to fit its + // children horizontally, and tries to be as tall as its parent. + // + // Column has various properties to control how it sizes itself and + // how it positions its children. Here we use mainAxisAlignment to + // center the children vertically; the main axis here is the vertical + // axis because Columns are vertical (the cross axis would be + // horizontal). + // + // TRY THIS: Invoke "debug painting" (choose the "Toggle Debug Paint" + // action in the IDE, or press "p" in the console), to see the + // wireframe for each widget. + mainAxisAlignment: .center, + children: [ + const Text('You have pushed the button this many times:'), + Text( + '$_counter', + style: Theme.of(context).textTheme.headlineMedium, + ), + ], + ), + ), + floatingActionButton: FloatingActionButton( + onPressed: _incrementCounter, + tooltip: 'Increment', + child: const Icon(Icons.add), + ), + ); + } +} diff --git a/linux/.gitignore b/linux/.gitignore new file mode 100644 index 0000000..d3896c9 --- /dev/null +++ b/linux/.gitignore @@ -0,0 +1 @@ +flutter/ephemeral diff --git a/linux/CMakeLists.txt b/linux/CMakeLists.txt new file mode 100644 index 0000000..ea9f018 --- /dev/null +++ b/linux/CMakeLists.txt @@ -0,0 +1,128 @@ +# Project-level configuration. +cmake_minimum_required(VERSION 3.13) +project(runner LANGUAGES CXX) + +# The name of the executable created for the application. Change this to change +# the on-disk name of your application. +set(BINARY_NAME "nomadcode") +# The unique GTK application identifier for this application. See: +# https://wiki.gnome.org/HowDoI/ChooseApplicationID +set(APPLICATION_ID "com.nomadcode.nomadcode") + +# Explicitly opt in to modern CMake behaviors to avoid warnings with recent +# versions of CMake. +cmake_policy(SET CMP0063 NEW) + +# Load bundled libraries from the lib/ directory relative to the binary. +set(CMAKE_INSTALL_RPATH "$ORIGIN/lib") + +# Root filesystem for cross-building. +if(FLUTTER_TARGET_PLATFORM_SYSROOT) + set(CMAKE_SYSROOT ${FLUTTER_TARGET_PLATFORM_SYSROOT}) + set(CMAKE_FIND_ROOT_PATH ${CMAKE_SYSROOT}) + set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER) + set(CMAKE_FIND_ROOT_PATH_MODE_PACKAGE ONLY) + set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY) + set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY) +endif() + +# Define build configuration options. +if(NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES) + set(CMAKE_BUILD_TYPE "Debug" CACHE + STRING "Flutter build mode" FORCE) + set_property(CACHE CMAKE_BUILD_TYPE PROPERTY STRINGS + "Debug" "Profile" "Release") +endif() + +# Compilation settings that should be applied to most targets. +# +# Be cautious about adding new options here, as plugins use this function by +# default. In most cases, you should add new options to specific targets instead +# of modifying this function. +function(APPLY_STANDARD_SETTINGS TARGET) + target_compile_features(${TARGET} PUBLIC cxx_std_14) + target_compile_options(${TARGET} PRIVATE -Wall -Werror) + target_compile_options(${TARGET} PRIVATE "$<$>:-O3>") + target_compile_definitions(${TARGET} PRIVATE "$<$>:NDEBUG>") +endfunction() + +# Flutter library and tool build rules. +set(FLUTTER_MANAGED_DIR "${CMAKE_CURRENT_SOURCE_DIR}/flutter") +add_subdirectory(${FLUTTER_MANAGED_DIR}) + +# System-level dependencies. +find_package(PkgConfig REQUIRED) +pkg_check_modules(GTK REQUIRED IMPORTED_TARGET gtk+-3.0) + +# Application build; see runner/CMakeLists.txt. +add_subdirectory("runner") + +# Run the Flutter tool portions of the build. This must not be removed. +add_dependencies(${BINARY_NAME} flutter_assemble) + +# Only the install-generated bundle's copy of the executable will launch +# correctly, since the resources must in the right relative locations. To avoid +# people trying to run the unbundled copy, put it in a subdirectory instead of +# the default top-level location. +set_target_properties(${BINARY_NAME} + PROPERTIES + RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/intermediates_do_not_run" +) + + +# Generated plugin build rules, which manage building the plugins and adding +# them to the application. +include(flutter/generated_plugins.cmake) + + +# === Installation === +# By default, "installing" just makes a relocatable bundle in the build +# directory. +set(BUILD_BUNDLE_DIR "${PROJECT_BINARY_DIR}/bundle") +if(CMAKE_INSTALL_PREFIX_INITIALIZED_TO_DEFAULT) + set(CMAKE_INSTALL_PREFIX "${BUILD_BUNDLE_DIR}" CACHE PATH "..." FORCE) +endif() + +# Start with a clean build bundle directory every time. +install(CODE " + file(REMOVE_RECURSE \"${BUILD_BUNDLE_DIR}/\") + " COMPONENT Runtime) + +set(INSTALL_BUNDLE_DATA_DIR "${CMAKE_INSTALL_PREFIX}/data") +set(INSTALL_BUNDLE_LIB_DIR "${CMAKE_INSTALL_PREFIX}/lib") + +install(TARGETS ${BINARY_NAME} RUNTIME DESTINATION "${CMAKE_INSTALL_PREFIX}" + COMPONENT Runtime) + +install(FILES "${FLUTTER_ICU_DATA_FILE}" DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" + COMPONENT Runtime) + +install(FILES "${FLUTTER_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" + COMPONENT Runtime) + +foreach(bundled_library ${PLUGIN_BUNDLED_LIBRARIES}) + install(FILES "${bundled_library}" + DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" + COMPONENT Runtime) +endforeach(bundled_library) + +# Copy the native assets provided by the build.dart from all packages. +set(NATIVE_ASSETS_DIR "${PROJECT_BUILD_DIR}native_assets/linux/") +install(DIRECTORY "${NATIVE_ASSETS_DIR}" + DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" + COMPONENT Runtime) + +# Fully re-copy the assets directory on each build to avoid having stale files +# from a previous install. +set(FLUTTER_ASSET_DIR_NAME "flutter_assets") +install(CODE " + file(REMOVE_RECURSE \"${INSTALL_BUNDLE_DATA_DIR}/${FLUTTER_ASSET_DIR_NAME}\") + " COMPONENT Runtime) +install(DIRECTORY "${PROJECT_BUILD_DIR}/${FLUTTER_ASSET_DIR_NAME}" + DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" COMPONENT Runtime) + +# Install the AOT library on non-Debug builds only. +if(NOT CMAKE_BUILD_TYPE MATCHES "Debug") + install(FILES "${AOT_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" + COMPONENT Runtime) +endif() diff --git a/linux/flutter/CMakeLists.txt b/linux/flutter/CMakeLists.txt new file mode 100644 index 0000000..d5bd016 --- /dev/null +++ b/linux/flutter/CMakeLists.txt @@ -0,0 +1,88 @@ +# This file controls Flutter-level build steps. It should not be edited. +cmake_minimum_required(VERSION 3.10) + +set(EPHEMERAL_DIR "${CMAKE_CURRENT_SOURCE_DIR}/ephemeral") + +# Configuration provided via flutter tool. +include(${EPHEMERAL_DIR}/generated_config.cmake) + +# TODO: Move the rest of this into files in ephemeral. See +# https://github.com/flutter/flutter/issues/57146. + +# Serves the same purpose as list(TRANSFORM ... PREPEND ...), +# which isn't available in 3.10. +function(list_prepend LIST_NAME PREFIX) + set(NEW_LIST "") + foreach(element ${${LIST_NAME}}) + list(APPEND NEW_LIST "${PREFIX}${element}") + endforeach(element) + set(${LIST_NAME} "${NEW_LIST}" PARENT_SCOPE) +endfunction() + +# === Flutter Library === +# System-level dependencies. +find_package(PkgConfig REQUIRED) +pkg_check_modules(GTK REQUIRED IMPORTED_TARGET gtk+-3.0) +pkg_check_modules(GLIB REQUIRED IMPORTED_TARGET glib-2.0) +pkg_check_modules(GIO REQUIRED IMPORTED_TARGET gio-2.0) + +set(FLUTTER_LIBRARY "${EPHEMERAL_DIR}/libflutter_linux_gtk.so") + +# Published to parent scope for install step. +set(FLUTTER_LIBRARY ${FLUTTER_LIBRARY} PARENT_SCOPE) +set(FLUTTER_ICU_DATA_FILE "${EPHEMERAL_DIR}/icudtl.dat" PARENT_SCOPE) +set(PROJECT_BUILD_DIR "${PROJECT_DIR}/build/" PARENT_SCOPE) +set(AOT_LIBRARY "${PROJECT_DIR}/build/lib/libapp.so" PARENT_SCOPE) + +list(APPEND FLUTTER_LIBRARY_HEADERS + "fl_basic_message_channel.h" + "fl_binary_codec.h" + "fl_binary_messenger.h" + "fl_dart_project.h" + "fl_engine.h" + "fl_json_message_codec.h" + "fl_json_method_codec.h" + "fl_message_codec.h" + "fl_method_call.h" + "fl_method_channel.h" + "fl_method_codec.h" + "fl_method_response.h" + "fl_plugin_registrar.h" + "fl_plugin_registry.h" + "fl_standard_message_codec.h" + "fl_standard_method_codec.h" + "fl_string_codec.h" + "fl_value.h" + "fl_view.h" + "flutter_linux.h" +) +list_prepend(FLUTTER_LIBRARY_HEADERS "${EPHEMERAL_DIR}/flutter_linux/") +add_library(flutter INTERFACE) +target_include_directories(flutter INTERFACE + "${EPHEMERAL_DIR}" +) +target_link_libraries(flutter INTERFACE "${FLUTTER_LIBRARY}") +target_link_libraries(flutter INTERFACE + PkgConfig::GTK + PkgConfig::GLIB + PkgConfig::GIO +) +add_dependencies(flutter flutter_assemble) + +# === Flutter tool backend === +# _phony_ is a non-existent file to force this command to run every time, +# since currently there's no way to get a full input/output list from the +# flutter tool. +add_custom_command( + OUTPUT ${FLUTTER_LIBRARY} ${FLUTTER_LIBRARY_HEADERS} + ${CMAKE_CURRENT_BINARY_DIR}/_phony_ + COMMAND ${CMAKE_COMMAND} -E env + ${FLUTTER_TOOL_ENVIRONMENT} + "${FLUTTER_ROOT}/packages/flutter_tools/bin/tool_backend.sh" + ${FLUTTER_TARGET_PLATFORM} ${CMAKE_BUILD_TYPE} + VERBATIM +) +add_custom_target(flutter_assemble DEPENDS + "${FLUTTER_LIBRARY}" + ${FLUTTER_LIBRARY_HEADERS} +) diff --git a/linux/flutter/generated_plugin_registrant.cc b/linux/flutter/generated_plugin_registrant.cc new file mode 100644 index 0000000..e71a16d --- /dev/null +++ b/linux/flutter/generated_plugin_registrant.cc @@ -0,0 +1,11 @@ +// +// Generated file. Do not edit. +// + +// clang-format off + +#include "generated_plugin_registrant.h" + + +void fl_register_plugins(FlPluginRegistry* registry) { +} diff --git a/linux/flutter/generated_plugin_registrant.h b/linux/flutter/generated_plugin_registrant.h new file mode 100644 index 0000000..e0f0a47 --- /dev/null +++ b/linux/flutter/generated_plugin_registrant.h @@ -0,0 +1,15 @@ +// +// Generated file. Do not edit. +// + +// clang-format off + +#ifndef GENERATED_PLUGIN_REGISTRANT_ +#define GENERATED_PLUGIN_REGISTRANT_ + +#include + +// Registers Flutter plugins. +void fl_register_plugins(FlPluginRegistry* registry); + +#endif // GENERATED_PLUGIN_REGISTRANT_ diff --git a/linux/flutter/generated_plugins.cmake b/linux/flutter/generated_plugins.cmake new file mode 100644 index 0000000..2e1de87 --- /dev/null +++ b/linux/flutter/generated_plugins.cmake @@ -0,0 +1,23 @@ +# +# Generated file, do not edit. +# + +list(APPEND FLUTTER_PLUGIN_LIST +) + +list(APPEND FLUTTER_FFI_PLUGIN_LIST +) + +set(PLUGIN_BUNDLED_LIBRARIES) + +foreach(plugin ${FLUTTER_PLUGIN_LIST}) + add_subdirectory(flutter/ephemeral/.plugin_symlinks/${plugin}/linux plugins/${plugin}) + target_link_libraries(${BINARY_NAME} PRIVATE ${plugin}_plugin) + list(APPEND PLUGIN_BUNDLED_LIBRARIES $) + list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${plugin}_bundled_libraries}) +endforeach(plugin) + +foreach(ffi_plugin ${FLUTTER_FFI_PLUGIN_LIST}) + add_subdirectory(flutter/ephemeral/.plugin_symlinks/${ffi_plugin}/linux plugins/${ffi_plugin}) + list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${ffi_plugin}_bundled_libraries}) +endforeach(ffi_plugin) diff --git a/linux/runner/CMakeLists.txt b/linux/runner/CMakeLists.txt new file mode 100644 index 0000000..e97dabc --- /dev/null +++ b/linux/runner/CMakeLists.txt @@ -0,0 +1,26 @@ +cmake_minimum_required(VERSION 3.13) +project(runner LANGUAGES CXX) + +# Define the application target. To change its name, change BINARY_NAME in the +# top-level CMakeLists.txt, not the value here, or `flutter run` will no longer +# work. +# +# Any new source files that you add to the application should be added here. +add_executable(${BINARY_NAME} + "main.cc" + "my_application.cc" + "${FLUTTER_MANAGED_DIR}/generated_plugin_registrant.cc" +) + +# Apply the standard set of build settings. This can be removed for applications +# that need different build settings. +apply_standard_settings(${BINARY_NAME}) + +# Add preprocessor definitions for the application ID. +add_definitions(-DAPPLICATION_ID="${APPLICATION_ID}") + +# Add dependency libraries. Add any application-specific dependencies here. +target_link_libraries(${BINARY_NAME} PRIVATE flutter) +target_link_libraries(${BINARY_NAME} PRIVATE PkgConfig::GTK) + +target_include_directories(${BINARY_NAME} PRIVATE "${CMAKE_SOURCE_DIR}") diff --git a/linux/runner/main.cc b/linux/runner/main.cc new file mode 100644 index 0000000..e7c5c54 --- /dev/null +++ b/linux/runner/main.cc @@ -0,0 +1,6 @@ +#include "my_application.h" + +int main(int argc, char** argv) { + g_autoptr(MyApplication) app = my_application_new(); + return g_application_run(G_APPLICATION(app), argc, argv); +} diff --git a/linux/runner/my_application.cc b/linux/runner/my_application.cc new file mode 100644 index 0000000..c2891c0 --- /dev/null +++ b/linux/runner/my_application.cc @@ -0,0 +1,148 @@ +#include "my_application.h" + +#include +#ifdef GDK_WINDOWING_X11 +#include +#endif + +#include "flutter/generated_plugin_registrant.h" + +struct _MyApplication { + GtkApplication parent_instance; + char** dart_entrypoint_arguments; +}; + +G_DEFINE_TYPE(MyApplication, my_application, GTK_TYPE_APPLICATION) + +// Called when first Flutter frame received. +static void first_frame_cb(MyApplication* self, FlView* view) { + gtk_widget_show(gtk_widget_get_toplevel(GTK_WIDGET(view))); +} + +// Implements GApplication::activate. +static void my_application_activate(GApplication* application) { + MyApplication* self = MY_APPLICATION(application); + GtkWindow* window = + GTK_WINDOW(gtk_application_window_new(GTK_APPLICATION(application))); + + // Use a header bar when running in GNOME as this is the common style used + // by applications and is the setup most users will be using (e.g. Ubuntu + // desktop). + // If running on X and not using GNOME then just use a traditional title bar + // in case the window manager does more exotic layout, e.g. tiling. + // If running on Wayland assume the header bar will work (may need changing + // if future cases occur). + gboolean use_header_bar = TRUE; +#ifdef GDK_WINDOWING_X11 + GdkScreen* screen = gtk_window_get_screen(window); + if (GDK_IS_X11_SCREEN(screen)) { + const gchar* wm_name = gdk_x11_screen_get_window_manager_name(screen); + if (g_strcmp0(wm_name, "GNOME Shell") != 0) { + use_header_bar = FALSE; + } + } +#endif + if (use_header_bar) { + GtkHeaderBar* header_bar = GTK_HEADER_BAR(gtk_header_bar_new()); + gtk_widget_show(GTK_WIDGET(header_bar)); + gtk_header_bar_set_title(header_bar, "nomadcode"); + gtk_header_bar_set_show_close_button(header_bar, TRUE); + gtk_window_set_titlebar(window, GTK_WIDGET(header_bar)); + } else { + gtk_window_set_title(window, "nomadcode"); + } + + gtk_window_set_default_size(window, 1280, 720); + + g_autoptr(FlDartProject) project = fl_dart_project_new(); + fl_dart_project_set_dart_entrypoint_arguments( + project, self->dart_entrypoint_arguments); + + FlView* view = fl_view_new(project); + GdkRGBA background_color; + // Background defaults to black, override it here if necessary, e.g. #00000000 + // for transparent. + gdk_rgba_parse(&background_color, "#000000"); + fl_view_set_background_color(view, &background_color); + gtk_widget_show(GTK_WIDGET(view)); + gtk_container_add(GTK_CONTAINER(window), GTK_WIDGET(view)); + + // Show the window when Flutter renders. + // Requires the view to be realized so we can start rendering. + g_signal_connect_swapped(view, "first-frame", G_CALLBACK(first_frame_cb), + self); + gtk_widget_realize(GTK_WIDGET(view)); + + fl_register_plugins(FL_PLUGIN_REGISTRY(view)); + + gtk_widget_grab_focus(GTK_WIDGET(view)); +} + +// Implements GApplication::local_command_line. +static gboolean my_application_local_command_line(GApplication* application, + gchar*** arguments, + int* exit_status) { + MyApplication* self = MY_APPLICATION(application); + // Strip out the first argument as it is the binary name. + self->dart_entrypoint_arguments = g_strdupv(*arguments + 1); + + g_autoptr(GError) error = nullptr; + if (!g_application_register(application, nullptr, &error)) { + g_warning("Failed to register: %s", error->message); + *exit_status = 1; + return TRUE; + } + + g_application_activate(application); + *exit_status = 0; + + return TRUE; +} + +// Implements GApplication::startup. +static void my_application_startup(GApplication* application) { + // MyApplication* self = MY_APPLICATION(object); + + // Perform any actions required at application startup. + + G_APPLICATION_CLASS(my_application_parent_class)->startup(application); +} + +// Implements GApplication::shutdown. +static void my_application_shutdown(GApplication* application) { + // MyApplication* self = MY_APPLICATION(object); + + // Perform any actions required at application shutdown. + + G_APPLICATION_CLASS(my_application_parent_class)->shutdown(application); +} + +// Implements GObject::dispose. +static void my_application_dispose(GObject* object) { + MyApplication* self = MY_APPLICATION(object); + g_clear_pointer(&self->dart_entrypoint_arguments, g_strfreev); + G_OBJECT_CLASS(my_application_parent_class)->dispose(object); +} + +static void my_application_class_init(MyApplicationClass* klass) { + G_APPLICATION_CLASS(klass)->activate = my_application_activate; + G_APPLICATION_CLASS(klass)->local_command_line = + my_application_local_command_line; + G_APPLICATION_CLASS(klass)->startup = my_application_startup; + G_APPLICATION_CLASS(klass)->shutdown = my_application_shutdown; + G_OBJECT_CLASS(klass)->dispose = my_application_dispose; +} + +static void my_application_init(MyApplication* self) {} + +MyApplication* my_application_new() { + // Set the program name to the application ID, which helps various systems + // like GTK and desktop environments map this running application to its + // corresponding .desktop file. This ensures better integration by allowing + // the application to be recognized beyond its binary name. + g_set_prgname(APPLICATION_ID); + + return MY_APPLICATION(g_object_new(my_application_get_type(), + "application-id", APPLICATION_ID, "flags", + G_APPLICATION_NON_UNIQUE, nullptr)); +} diff --git a/linux/runner/my_application.h b/linux/runner/my_application.h new file mode 100644 index 0000000..db16367 --- /dev/null +++ b/linux/runner/my_application.h @@ -0,0 +1,21 @@ +#ifndef FLUTTER_MY_APPLICATION_H_ +#define FLUTTER_MY_APPLICATION_H_ + +#include + +G_DECLARE_FINAL_TYPE(MyApplication, + my_application, + MY, + APPLICATION, + GtkApplication) + +/** + * my_application_new: + * + * Creates a new Flutter-based application. + * + * Returns: a new #MyApplication. + */ +MyApplication* my_application_new(); + +#endif // FLUTTER_MY_APPLICATION_H_ diff --git a/macos/.gitignore b/macos/.gitignore new file mode 100644 index 0000000..746adbb --- /dev/null +++ b/macos/.gitignore @@ -0,0 +1,7 @@ +# Flutter-related +**/Flutter/ephemeral/ +**/Pods/ + +# Xcode-related +**/dgph +**/xcuserdata/ diff --git a/macos/Flutter/Flutter-Debug.xcconfig b/macos/Flutter/Flutter-Debug.xcconfig new file mode 100644 index 0000000..c2efd0b --- /dev/null +++ b/macos/Flutter/Flutter-Debug.xcconfig @@ -0,0 +1 @@ +#include "ephemeral/Flutter-Generated.xcconfig" diff --git a/macos/Flutter/Flutter-Release.xcconfig b/macos/Flutter/Flutter-Release.xcconfig new file mode 100644 index 0000000..c2efd0b --- /dev/null +++ b/macos/Flutter/Flutter-Release.xcconfig @@ -0,0 +1 @@ +#include "ephemeral/Flutter-Generated.xcconfig" diff --git a/macos/Flutter/GeneratedPluginRegistrant.swift b/macos/Flutter/GeneratedPluginRegistrant.swift new file mode 100644 index 0000000..cccf817 --- /dev/null +++ b/macos/Flutter/GeneratedPluginRegistrant.swift @@ -0,0 +1,10 @@ +// +// Generated file. Do not edit. +// + +import FlutterMacOS +import Foundation + + +func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { +} diff --git a/macos/Runner.xcodeproj/project.pbxproj b/macos/Runner.xcodeproj/project.pbxproj new file mode 100644 index 0000000..b29ac57 --- /dev/null +++ b/macos/Runner.xcodeproj/project.pbxproj @@ -0,0 +1,705 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 54; + objects = { + +/* Begin PBXAggregateTarget section */ + 33CC111A2044C6BA0003C045 /* Flutter Assemble */ = { + isa = PBXAggregateTarget; + buildConfigurationList = 33CC111B2044C6BA0003C045 /* Build configuration list for PBXAggregateTarget "Flutter Assemble" */; + buildPhases = ( + 33CC111E2044C6BF0003C045 /* ShellScript */, + ); + dependencies = ( + ); + name = "Flutter Assemble"; + productName = FLX; + }; +/* End PBXAggregateTarget section */ + +/* Begin PBXBuildFile section */ + 331C80D8294CF71000263BE5 /* RunnerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 331C80D7294CF71000263BE5 /* RunnerTests.swift */; }; + 335BBD1B22A9A15E00E9071D /* GeneratedPluginRegistrant.swift in Sources */ = {isa = PBXBuildFile; fileRef = 335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */; }; + 33CC10F12044A3C60003C045 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 33CC10F02044A3C60003C045 /* AppDelegate.swift */; }; + 33CC10F32044A3C60003C045 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 33CC10F22044A3C60003C045 /* Assets.xcassets */; }; + 33CC10F62044A3C60003C045 /* MainMenu.xib in Resources */ = {isa = PBXBuildFile; fileRef = 33CC10F42044A3C60003C045 /* MainMenu.xib */; }; + 33CC11132044BFA00003C045 /* MainFlutterWindow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 33CC11122044BFA00003C045 /* MainFlutterWindow.swift */; }; +/* End PBXBuildFile section */ + +/* Begin PBXContainerItemProxy section */ + 331C80D9294CF71000263BE5 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 33CC10E52044A3C60003C045 /* Project object */; + proxyType = 1; + remoteGlobalIDString = 33CC10EC2044A3C60003C045; + remoteInfo = Runner; + }; + 33CC111F2044C79F0003C045 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 33CC10E52044A3C60003C045 /* Project object */; + proxyType = 1; + remoteGlobalIDString = 33CC111A2044C6BA0003C045; + remoteInfo = FLX; + }; +/* End PBXContainerItemProxy section */ + +/* Begin PBXCopyFilesBuildPhase section */ + 33CC110E2044A8840003C045 /* Bundle Framework */ = { + isa = PBXCopyFilesBuildPhase; + buildActionMask = 2147483647; + dstPath = ""; + dstSubfolderSpec = 10; + files = ( + ); + name = "Bundle Framework"; + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXCopyFilesBuildPhase section */ + +/* Begin PBXFileReference section */ + 331C80D5294CF71000263BE5 /* RunnerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; + 331C80D7294CF71000263BE5 /* RunnerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RunnerTests.swift; sourceTree = ""; }; + 333000ED22D3DE5D00554162 /* Warnings.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Warnings.xcconfig; sourceTree = ""; }; + 335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GeneratedPluginRegistrant.swift; sourceTree = ""; }; + 33CC10ED2044A3C60003C045 /* nomadcode.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "nomadcode.app"; sourceTree = BUILT_PRODUCTS_DIR; }; + 33CC10F02044A3C60003C045 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; + 33CC10F22044A3C60003C045 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; name = Assets.xcassets; path = Runner/Assets.xcassets; sourceTree = ""; }; + 33CC10F52044A3C60003C045 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = Base; path = Base.lproj/MainMenu.xib; sourceTree = ""; }; + 33CC10F72044A3C60003C045 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; name = Info.plist; path = Runner/Info.plist; sourceTree = ""; }; + 33CC11122044BFA00003C045 /* MainFlutterWindow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainFlutterWindow.swift; sourceTree = ""; }; + 33CEB47222A05771004F2AC0 /* Flutter-Debug.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = "Flutter-Debug.xcconfig"; sourceTree = ""; }; + 33CEB47422A05771004F2AC0 /* Flutter-Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = "Flutter-Release.xcconfig"; sourceTree = ""; }; + 33CEB47722A0578A004F2AC0 /* Flutter-Generated.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = "Flutter-Generated.xcconfig"; path = "ephemeral/Flutter-Generated.xcconfig"; sourceTree = ""; }; + 33E51913231747F40026EE4D /* DebugProfile.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = DebugProfile.entitlements; sourceTree = ""; }; + 33E51914231749380026EE4D /* Release.entitlements */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.entitlements; path = Release.entitlements; sourceTree = ""; }; + 33E5194F232828860026EE4D /* AppInfo.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = AppInfo.xcconfig; sourceTree = ""; }; + 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Release.xcconfig; sourceTree = ""; }; + 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = Debug.xcconfig; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 331C80D2294CF70F00263BE5 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 33CC10EA2044A3C60003C045 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 331C80D6294CF71000263BE5 /* RunnerTests */ = { + isa = PBXGroup; + children = ( + 331C80D7294CF71000263BE5 /* RunnerTests.swift */, + ); + path = RunnerTests; + sourceTree = ""; + }; + 33BA886A226E78AF003329D5 /* Configs */ = { + isa = PBXGroup; + children = ( + 33E5194F232828860026EE4D /* AppInfo.xcconfig */, + 9740EEB21CF90195004384FC /* Debug.xcconfig */, + 7AFA3C8E1D35360C0083082E /* Release.xcconfig */, + 333000ED22D3DE5D00554162 /* Warnings.xcconfig */, + ); + path = Configs; + sourceTree = ""; + }; + 33CC10E42044A3C60003C045 = { + isa = PBXGroup; + children = ( + 33FAB671232836740065AC1E /* Runner */, + 33CEB47122A05771004F2AC0 /* Flutter */, + 331C80D6294CF71000263BE5 /* RunnerTests */, + 33CC10EE2044A3C60003C045 /* Products */, + D73912EC22F37F3D000D13A0 /* Frameworks */, + ); + sourceTree = ""; + }; + 33CC10EE2044A3C60003C045 /* Products */ = { + isa = PBXGroup; + children = ( + 33CC10ED2044A3C60003C045 /* nomadcode.app */, + 331C80D5294CF71000263BE5 /* RunnerTests.xctest */, + ); + name = Products; + sourceTree = ""; + }; + 33CC11242044D66E0003C045 /* Resources */ = { + isa = PBXGroup; + children = ( + 33CC10F22044A3C60003C045 /* Assets.xcassets */, + 33CC10F42044A3C60003C045 /* MainMenu.xib */, + 33CC10F72044A3C60003C045 /* Info.plist */, + ); + name = Resources; + path = ..; + sourceTree = ""; + }; + 33CEB47122A05771004F2AC0 /* Flutter */ = { + isa = PBXGroup; + children = ( + 335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */, + 33CEB47222A05771004F2AC0 /* Flutter-Debug.xcconfig */, + 33CEB47422A05771004F2AC0 /* Flutter-Release.xcconfig */, + 33CEB47722A0578A004F2AC0 /* Flutter-Generated.xcconfig */, + ); + path = Flutter; + sourceTree = ""; + }; + 33FAB671232836740065AC1E /* Runner */ = { + isa = PBXGroup; + children = ( + 33CC10F02044A3C60003C045 /* AppDelegate.swift */, + 33CC11122044BFA00003C045 /* MainFlutterWindow.swift */, + 33E51913231747F40026EE4D /* DebugProfile.entitlements */, + 33E51914231749380026EE4D /* Release.entitlements */, + 33CC11242044D66E0003C045 /* Resources */, + 33BA886A226E78AF003329D5 /* Configs */, + ); + path = Runner; + sourceTree = ""; + }; + D73912EC22F37F3D000D13A0 /* Frameworks */ = { + isa = PBXGroup; + children = ( + ); + name = Frameworks; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + 331C80D4294CF70F00263BE5 /* RunnerTests */ = { + isa = PBXNativeTarget; + buildConfigurationList = 331C80DE294CF71000263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */; + buildPhases = ( + 331C80D1294CF70F00263BE5 /* Sources */, + 331C80D2294CF70F00263BE5 /* Frameworks */, + 331C80D3294CF70F00263BE5 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + 331C80DA294CF71000263BE5 /* PBXTargetDependency */, + ); + name = RunnerTests; + productName = RunnerTests; + productReference = 331C80D5294CF71000263BE5 /* RunnerTests.xctest */; + productType = "com.apple.product-type.bundle.unit-test"; + }; + 33CC10EC2044A3C60003C045 /* Runner */ = { + isa = PBXNativeTarget; + buildConfigurationList = 33CC10FB2044A3C60003C045 /* Build configuration list for PBXNativeTarget "Runner" */; + buildPhases = ( + 33CC10E92044A3C60003C045 /* Sources */, + 33CC10EA2044A3C60003C045 /* Frameworks */, + 33CC10EB2044A3C60003C045 /* Resources */, + 33CC110E2044A8840003C045 /* Bundle Framework */, + 3399D490228B24CF009A79C7 /* ShellScript */, + ); + buildRules = ( + ); + dependencies = ( + 33CC11202044C79F0003C045 /* PBXTargetDependency */, + ); + name = Runner; + productName = Runner; + productReference = 33CC10ED2044A3C60003C045 /* nomadcode.app */; + productType = "com.apple.product-type.application"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 33CC10E52044A3C60003C045 /* Project object */ = { + isa = PBXProject; + attributes = { + BuildIndependentTargetsInParallel = YES; + LastSwiftUpdateCheck = 0920; + LastUpgradeCheck = 1510; + ORGANIZATIONNAME = ""; + TargetAttributes = { + 331C80D4294CF70F00263BE5 = { + CreatedOnToolsVersion = 14.0; + TestTargetID = 33CC10EC2044A3C60003C045; + }; + 33CC10EC2044A3C60003C045 = { + CreatedOnToolsVersion = 9.2; + LastSwiftMigration = 1100; + ProvisioningStyle = Automatic; + SystemCapabilities = { + com.apple.Sandbox = { + enabled = 1; + }; + }; + }; + 33CC111A2044C6BA0003C045 = { + CreatedOnToolsVersion = 9.2; + ProvisioningStyle = Manual; + }; + }; + }; + buildConfigurationList = 33CC10E82044A3C60003C045 /* Build configuration list for PBXProject "Runner" */; + compatibilityVersion = "Xcode 9.3"; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = 33CC10E42044A3C60003C045; + productRefGroup = 33CC10EE2044A3C60003C045 /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 33CC10EC2044A3C60003C045 /* Runner */, + 331C80D4294CF70F00263BE5 /* RunnerTests */, + 33CC111A2044C6BA0003C045 /* Flutter Assemble */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + 331C80D3294CF70F00263BE5 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 33CC10EB2044A3C60003C045 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 33CC10F32044A3C60003C045 /* Assets.xcassets in Resources */, + 33CC10F62044A3C60003C045 /* MainMenu.xib in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXShellScriptBuildPhase section */ + 3399D490228B24CF009A79C7 /* ShellScript */ = { + isa = PBXShellScriptBuildPhase; + alwaysOutOfDate = 1; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + ); + outputFileListPaths = ( + ); + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "echo \"$PRODUCT_NAME.app\" > \"$PROJECT_DIR\"/Flutter/ephemeral/.app_filename && \"$FLUTTER_ROOT\"/packages/flutter_tools/bin/macos_assemble.sh embed\n"; + }; + 33CC111E2044C6BF0003C045 /* ShellScript */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + Flutter/ephemeral/FlutterInputs.xcfilelist, + ); + inputPaths = ( + Flutter/ephemeral/tripwire, + ); + outputFileListPaths = ( + Flutter/ephemeral/FlutterOutputs.xcfilelist, + ); + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"$FLUTTER_ROOT\"/packages/flutter_tools/bin/macos_assemble.sh && touch Flutter/ephemeral/tripwire"; + }; +/* End PBXShellScriptBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 331C80D1294CF70F00263BE5 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 331C80D8294CF71000263BE5 /* RunnerTests.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 33CC10E92044A3C60003C045 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 33CC11132044BFA00003C045 /* MainFlutterWindow.swift in Sources */, + 33CC10F12044A3C60003C045 /* AppDelegate.swift in Sources */, + 335BBD1B22A9A15E00E9071D /* GeneratedPluginRegistrant.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin PBXTargetDependency section */ + 331C80DA294CF71000263BE5 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 33CC10EC2044A3C60003C045 /* Runner */; + targetProxy = 331C80D9294CF71000263BE5 /* PBXContainerItemProxy */; + }; + 33CC11202044C79F0003C045 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 33CC111A2044C6BA0003C045 /* Flutter Assemble */; + targetProxy = 33CC111F2044C79F0003C045 /* PBXContainerItemProxy */; + }; +/* End PBXTargetDependency section */ + +/* Begin PBXVariantGroup section */ + 33CC10F42044A3C60003C045 /* MainMenu.xib */ = { + isa = PBXVariantGroup; + children = ( + 33CC10F52044A3C60003C045 /* Base */, + ); + name = MainMenu.xib; + path = Runner; + sourceTree = ""; + }; +/* End PBXVariantGroup section */ + +/* Begin XCBuildConfiguration section */ + 331C80DB294CF71000263BE5 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CURRENT_PROJECT_VERSION = 1; + GENERATE_INFOPLIST_FILE = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.nomadcode.nomadcode.RunnerTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 5.0; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/nomadcode.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/nomadcode"; + }; + name = Debug; + }; + 331C80DC294CF71000263BE5 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CURRENT_PROJECT_VERSION = 1; + GENERATE_INFOPLIST_FILE = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.nomadcode.nomadcode.RunnerTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 5.0; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/nomadcode.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/nomadcode"; + }; + name = Release; + }; + 331C80DD294CF71000263BE5 /* Profile */ = { + isa = XCBuildConfiguration; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CURRENT_PROJECT_VERSION = 1; + GENERATE_INFOPLIST_FILE = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.nomadcode.nomadcode.RunnerTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 5.0; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/nomadcode.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/nomadcode"; + }; + name = Profile; + }; + 338D0CE9231458BD00FA5F75 /* Profile */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CODE_SIGN_IDENTITY = "-"; + COPY_PHASE_STRIP = NO; + DEAD_CODE_STRIPPING = YES; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_USER_SCRIPT_SANDBOXING = NO; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + MACOSX_DEPLOYMENT_TARGET = 10.15; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = macosx; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_OPTIMIZATION_LEVEL = "-O"; + }; + name = Profile; + }; + 338D0CEA231458BD00FA5F75 /* Profile */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 33E5194F232828860026EE4D /* AppInfo.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CODE_SIGN_ENTITLEMENTS = Runner/DebugProfile.entitlements; + CODE_SIGN_STYLE = Automatic; + COMBINE_HIDPI_IMAGES = YES; + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/../Frameworks", + ); + PROVISIONING_PROFILE_SPECIFIER = ""; + SWIFT_VERSION = 5.0; + }; + name = Profile; + }; + 338D0CEB231458BD00FA5F75 /* Profile */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_STYLE = Manual; + PRODUCT_NAME = "$(TARGET_NAME)"; + }; + name = Profile; + }; + 33CC10F92044A3C60003C045 /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CODE_SIGN_IDENTITY = "-"; + COPY_PHASE_STRIP = NO; + DEAD_CODE_STRIPPING = YES; + DEBUG_INFORMATION_FORMAT = dwarf; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + ENABLE_USER_SCRIPT_SANDBOXING = NO; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + MACOSX_DEPLOYMENT_TARGET = 10.15; + MTL_ENABLE_DEBUG_INFO = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = macosx; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + }; + name = Debug; + }; + 33CC10FA2044A3C60003C045 /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CODE_SIGN_IDENTITY = "-"; + COPY_PHASE_STRIP = NO; + DEAD_CODE_STRIPPING = YES; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_USER_SCRIPT_SANDBOXING = NO; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + MACOSX_DEPLOYMENT_TARGET = 10.15; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = macosx; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_OPTIMIZATION_LEVEL = "-O"; + }; + name = Release; + }; + 33CC10FC2044A3C60003C045 /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 33E5194F232828860026EE4D /* AppInfo.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CODE_SIGN_ENTITLEMENTS = Runner/DebugProfile.entitlements; + CODE_SIGN_STYLE = Automatic; + COMBINE_HIDPI_IMAGES = YES; + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/../Frameworks", + ); + PROVISIONING_PROFILE_SPECIFIER = ""; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 5.0; + }; + name = Debug; + }; + 33CC10FD2044A3C60003C045 /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 33E5194F232828860026EE4D /* AppInfo.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CODE_SIGN_ENTITLEMENTS = Runner/Release.entitlements; + CODE_SIGN_STYLE = Automatic; + COMBINE_HIDPI_IMAGES = YES; + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/../Frameworks", + ); + PROVISIONING_PROFILE_SPECIFIER = ""; + SWIFT_VERSION = 5.0; + }; + name = Release; + }; + 33CC111C2044C6BA0003C045 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_STYLE = Manual; + PRODUCT_NAME = "$(TARGET_NAME)"; + }; + name = Debug; + }; + 33CC111D2044C6BA0003C045 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_STYLE = Automatic; + PRODUCT_NAME = "$(TARGET_NAME)"; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 331C80DE294CF71000263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 331C80DB294CF71000263BE5 /* Debug */, + 331C80DC294CF71000263BE5 /* Release */, + 331C80DD294CF71000263BE5 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 33CC10E82044A3C60003C045 /* Build configuration list for PBXProject "Runner" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 33CC10F92044A3C60003C045 /* Debug */, + 33CC10FA2044A3C60003C045 /* Release */, + 338D0CE9231458BD00FA5F75 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 33CC10FB2044A3C60003C045 /* Build configuration list for PBXNativeTarget "Runner" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 33CC10FC2044A3C60003C045 /* Debug */, + 33CC10FD2044A3C60003C045 /* Release */, + 338D0CEA231458BD00FA5F75 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 33CC111B2044C6BA0003C045 /* Build configuration list for PBXAggregateTarget "Flutter Assemble" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 33CC111C2044C6BA0003C045 /* Debug */, + 33CC111D2044C6BA0003C045 /* Release */, + 338D0CEB231458BD00FA5F75 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = 33CC10E52044A3C60003C045 /* Project object */; +} diff --git a/macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 0000000..18d9810 --- /dev/null +++ b/macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme new file mode 100644 index 0000000..566d7ae --- /dev/null +++ b/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -0,0 +1,99 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/macos/Runner.xcworkspace/contents.xcworkspacedata b/macos/Runner.xcworkspace/contents.xcworkspacedata new file mode 100644 index 0000000..1d526a1 --- /dev/null +++ b/macos/Runner.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/macos/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/macos/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 0000000..18d9810 --- /dev/null +++ b/macos/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/macos/Runner/AppDelegate.swift b/macos/Runner/AppDelegate.swift new file mode 100644 index 0000000..b3c1761 --- /dev/null +++ b/macos/Runner/AppDelegate.swift @@ -0,0 +1,13 @@ +import Cocoa +import FlutterMacOS + +@main +class AppDelegate: FlutterAppDelegate { + override func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool { + return true + } + + override func applicationSupportsSecureRestorableState(_ app: NSApplication) -> Bool { + return true + } +} diff --git a/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json b/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 0000000..a2ec33f --- /dev/null +++ b/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,68 @@ +{ + "images" : [ + { + "size" : "16x16", + "idiom" : "mac", + "filename" : "app_icon_16.png", + "scale" : "1x" + }, + { + "size" : "16x16", + "idiom" : "mac", + "filename" : "app_icon_32.png", + "scale" : "2x" + }, + { + "size" : "32x32", + "idiom" : "mac", + "filename" : "app_icon_32.png", + "scale" : "1x" + }, + { + "size" : "32x32", + "idiom" : "mac", + "filename" : "app_icon_64.png", + "scale" : "2x" + }, + { + "size" : "128x128", + "idiom" : "mac", + "filename" : "app_icon_128.png", + "scale" : "1x" + }, + { + "size" : "128x128", + "idiom" : "mac", + "filename" : "app_icon_256.png", + "scale" : "2x" + }, + { + "size" : "256x256", + "idiom" : "mac", + "filename" : "app_icon_256.png", + "scale" : "1x" + }, + { + "size" : "256x256", + "idiom" : "mac", + "filename" : "app_icon_512.png", + "scale" : "2x" + }, + { + "size" : "512x512", + "idiom" : "mac", + "filename" : "app_icon_512.png", + "scale" : "1x" + }, + { + "size" : "512x512", + "idiom" : "mac", + "filename" : "app_icon_1024.png", + "scale" : "2x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} diff --git a/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png b/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png new file mode 100644 index 0000000000000000000000000000000000000000..82b6f9d9a33e198f5747104729e1fcef999772a5 GIT binary patch literal 102994 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ⅅ8MglG&ER{i5lN315cO?EdHNLrg? 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 z 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 Pe7HEdAEg|=c=LY&YVNkY literal 0 HcmV?d00001 diff --git a/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png b/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png new file mode 100644 index 0000000000000000000000000000000000000000..13b35eba55c6dabc3aac36f33d859266c18fa0d0 GIT binary patch literal 5680 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 literal 0 HcmV?d00001 diff --git a/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png b/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png new file mode 100644 index 0000000000000000000000000000000000000000..0a3f5fa40fb3d1e0710331a48de5d256da3f275d GIT binary patch literal 520 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 literal 0 HcmV?d00001 diff --git a/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png b/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png new file mode 100644 index 0000000000000000000000000000000000000000..2f1632cfddf3d9dade342351e627a0a75609fb46 GIT binary patch literal 2218 zcmV;b2vzrqP)Px#L}ge>W=%~1DgXcg2mk?xX#fNO00031000^Q000001E2u_0{{R30RRC20H6W@ z1ONa40RR91K%fHv1ONa40RR91KmY&$07g+lumAuE6iGxuRCodHTWf3-RTMruyW6Fu zQYeUM04eX6D5c0FCjKKPrco1(K`<0SL=crI{PC3-^hZU0kQie$gh-5!7z6SH6Q0J% zqot*`H1q{R5fHFYS}dje@;kG=v$L0(yY0?wY2%*c?A&{2?!D*x?m71{of2gv!$5|C z3>qG_BW}7K_yUcT3A5C6QD<+{aq?x;MAUyAiJn#Jv8_zZtQ{P zTRzbL3U9!qVuZzS$xKU10KiW~Bgdcv1-!uAhQxf3a7q+dU6lj?yoO4Lq4TUN4}h{N z*fIM=SS8|C2$(T>w$`t@3Tka!(r!7W`x z-isCVgQD^mG-MJ;XtJuK3V{Vy72GQ83KRWsHU?e*wrhKk=ApIYeDqLi;JI1e zuvv}5^Dc=k7F7?nm3nIw$NVmU-+R>> zyqOR$-2SDpJ}Pt;^RkJytDVXNTsu|mI1`~G7yw`EJR?VkGfNdqK9^^8P`JdtTV&tX4CNcV4 z&N06nZa??Fw1AgQOUSE2AmPE@WO(Fvo`%m`cDgiv(fAeRA%3AGXUbsGw{7Q`cY;1BI#ac3iN$$Hw z0LT0;xc%=q)me?Y*$xI@GRAw?+}>=9D+KTk??-HJ4=A>`V&vKFS75@MKdSF1JTq{S zc1!^8?YA|t+uKigaq!sT;Z!&0F2=k7F0PIU;F$leJLaw2UI6FL^w}OG&!;+b%ya1c z1n+6-inU<0VM-Y_s5iTElq)ThyF?StVcebpGI znw#+zLx2@ah{$_2jn+@}(zJZ{+}_N9BM;z)0yr|gF-4=Iyu@hI*Lk=-A8f#bAzc9f z`Kd6K--x@t04swJVC3JK1cHY-Hq+=|PN-VO;?^_C#;coU6TDP7Bt`;{JTG;!+jj(` zw5cLQ-(Cz-Tlb`A^w7|R56Ce;Wmr0)$KWOUZ6ai0PhzPeHwdl0H(etP zUV`va_i0s-4#DkNM8lUlqI7>YQLf)(lz9Q3Uw`)nc(z3{m5ZE77Ul$V%m)E}3&8L0 z-XaU|eB~Is08eORPk;=<>!1w)Kf}FOVS2l&9~A+@R#koFJ$Czd%Y(ENTV&A~U(IPI z;UY+gf+&6ioZ=roly<0Yst8ck>(M=S?B-ys3mLdM&)ex!hbt+ol|T6CTS+Sc0jv(& z7ijdvFwBq;0a{%3GGwkDKTeG`b+lyj0jjS1OMkYnepCdoosNY`*zmBIo*981BU%%U z@~$z0V`OVtIbEx5pa|Tct|Lg#ZQf5OYMUMRD>Wdxm5SAqV2}3!ceE-M2 z@O~lQ0OiKQp}o9I;?uxCgYVV?FH|?Riri*U$Zi_`V2eiA>l zdSm6;SEm6#T+SpcE8Ro_f2AwxzI z44hfe^WE3!h@W3RDyA_H440cpmYkv*)6m1XazTqw%=E5Xv7^@^^T7Q2wxr+Z2kVYr + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/macos/Runner/Configs/AppInfo.xcconfig b/macos/Runner/Configs/AppInfo.xcconfig new file mode 100644 index 0000000..30f214a --- /dev/null +++ b/macos/Runner/Configs/AppInfo.xcconfig @@ -0,0 +1,14 @@ +// Application-level settings for the Runner target. +// +// This may be replaced with something auto-generated from metadata (e.g., pubspec.yaml) in the +// future. If not, the values below would default to using the project name when this becomes a +// 'flutter create' template. + +// The application's name. By default this is also the title of the Flutter window. +PRODUCT_NAME = nomadcode + +// The application's bundle identifier +PRODUCT_BUNDLE_IDENTIFIER = com.nomadcode.nomadcode + +// The copyright displayed in application information +PRODUCT_COPYRIGHT = Copyright © 2026 com.nomadcode. All rights reserved. diff --git a/macos/Runner/Configs/Debug.xcconfig b/macos/Runner/Configs/Debug.xcconfig new file mode 100644 index 0000000..36b0fd9 --- /dev/null +++ b/macos/Runner/Configs/Debug.xcconfig @@ -0,0 +1,2 @@ +#include "../../Flutter/Flutter-Debug.xcconfig" +#include "Warnings.xcconfig" diff --git a/macos/Runner/Configs/Release.xcconfig b/macos/Runner/Configs/Release.xcconfig new file mode 100644 index 0000000..dff4f49 --- /dev/null +++ b/macos/Runner/Configs/Release.xcconfig @@ -0,0 +1,2 @@ +#include "../../Flutter/Flutter-Release.xcconfig" +#include "Warnings.xcconfig" diff --git a/macos/Runner/Configs/Warnings.xcconfig b/macos/Runner/Configs/Warnings.xcconfig new file mode 100644 index 0000000..42bcbf4 --- /dev/null +++ b/macos/Runner/Configs/Warnings.xcconfig @@ -0,0 +1,13 @@ +WARNING_CFLAGS = -Wall -Wconditional-uninitialized -Wnullable-to-nonnull-conversion -Wmissing-method-return-type -Woverlength-strings +GCC_WARN_UNDECLARED_SELECTOR = YES +CLANG_UNDEFINED_BEHAVIOR_SANITIZER_NULLABILITY = YES +CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE +CLANG_WARN__DUPLICATE_METHOD_MATCH = YES +CLANG_WARN_PRAGMA_PACK = YES +CLANG_WARN_STRICT_PROTOTYPES = YES +CLANG_WARN_COMMA = YES +GCC_WARN_STRICT_SELECTOR_MATCH = YES +CLANG_WARN_OBJC_REPEATED_USE_OF_WEAK = YES +CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES +GCC_WARN_SHADOW = YES +CLANG_WARN_UNREACHABLE_CODE = YES diff --git a/macos/Runner/DebugProfile.entitlements b/macos/Runner/DebugProfile.entitlements new file mode 100644 index 0000000..dddb8a3 --- /dev/null +++ b/macos/Runner/DebugProfile.entitlements @@ -0,0 +1,12 @@ + + + + + com.apple.security.app-sandbox + + com.apple.security.cs.allow-jit + + com.apple.security.network.server + + + diff --git a/macos/Runner/Info.plist b/macos/Runner/Info.plist new file mode 100644 index 0000000..4789daa --- /dev/null +++ b/macos/Runner/Info.plist @@ -0,0 +1,32 @@ + + + + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIconFile + + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + APPL + CFBundleShortVersionString + $(FLUTTER_BUILD_NAME) + CFBundleVersion + $(FLUTTER_BUILD_NUMBER) + LSMinimumSystemVersion + $(MACOSX_DEPLOYMENT_TARGET) + NSHumanReadableCopyright + $(PRODUCT_COPYRIGHT) + NSMainNibFile + MainMenu + NSPrincipalClass + NSApplication + + diff --git a/macos/Runner/MainFlutterWindow.swift b/macos/Runner/MainFlutterWindow.swift new file mode 100644 index 0000000..3cc05eb --- /dev/null +++ b/macos/Runner/MainFlutterWindow.swift @@ -0,0 +1,15 @@ +import Cocoa +import FlutterMacOS + +class MainFlutterWindow: NSWindow { + override func awakeFromNib() { + let flutterViewController = FlutterViewController() + let windowFrame = self.frame + self.contentViewController = flutterViewController + self.setFrame(windowFrame, display: true) + + RegisterGeneratedPlugins(registry: flutterViewController) + + super.awakeFromNib() + } +} diff --git a/macos/Runner/Release.entitlements b/macos/Runner/Release.entitlements new file mode 100644 index 0000000..852fa1a --- /dev/null +++ b/macos/Runner/Release.entitlements @@ -0,0 +1,8 @@ + + + + + com.apple.security.app-sandbox + + + diff --git a/macos/RunnerTests/RunnerTests.swift b/macos/RunnerTests/RunnerTests.swift new file mode 100644 index 0000000..61f3bd1 --- /dev/null +++ b/macos/RunnerTests/RunnerTests.swift @@ -0,0 +1,12 @@ +import Cocoa +import FlutterMacOS +import XCTest + +class RunnerTests: XCTestCase { + + func testExample() { + // If you add code to the Runner application, consider adding tests here. + // See https://developer.apple.com/documentation/xctest for more information about using XCTest. + } + +} diff --git a/nomadcode.iml b/nomadcode.iml new file mode 100644 index 0000000..f66303d --- /dev/null +++ b/nomadcode.iml @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + + diff --git a/pubspec.lock b/pubspec.lock new file mode 100644 index 0000000..764bc87 --- /dev/null +++ b/pubspec.lock @@ -0,0 +1,213 @@ +# Generated by pub +# See https://dart.dev/tools/pub/glossary#lockfile +packages: + async: + dependency: transitive + description: + name: async + sha256: "758e6d74e971c3e5aceb4110bfd6698efc7f501675bcfe0c775459a8140750eb" + url: "https://pub.dev" + source: hosted + version: "2.13.0" + boolean_selector: + dependency: transitive + description: + name: boolean_selector + sha256: "8aab1771e1243a5063b8b0ff68042d67334e3feab9e95b9490f9a6ebf73b42ea" + url: "https://pub.dev" + source: hosted + version: "2.1.2" + characters: + dependency: transitive + description: + name: characters + sha256: faf38497bda5ead2a8c7615f4f7939df04333478bf32e4173fcb06d428b5716b + url: "https://pub.dev" + source: hosted + version: "1.4.1" + clock: + dependency: transitive + description: + name: clock + sha256: fddb70d9b5277016c77a80201021d40a2247104d9f4aa7bab7157b7e3f05b84b + url: "https://pub.dev" + source: hosted + version: "1.1.2" + collection: + dependency: transitive + description: + name: collection + sha256: "2f5709ae4d3d59dd8f7cd309b4e023046b57d8a6c82130785d2b0e5868084e76" + url: "https://pub.dev" + source: hosted + version: "1.19.1" + cupertino_icons: + dependency: "direct main" + description: + name: cupertino_icons + sha256: ba631d1c7f7bef6b729a622b7b752645a2d076dba9976925b8f25725a30e1ee6 + url: "https://pub.dev" + source: hosted + version: "1.0.8" + fake_async: + dependency: transitive + description: + name: fake_async + sha256: "5368f224a74523e8d2e7399ea1638b37aecfca824a3cc4dfdf77bf1fa905ac44" + url: "https://pub.dev" + source: hosted + version: "1.3.3" + flutter: + dependency: "direct main" + description: flutter + source: sdk + version: "0.0.0" + flutter_lints: + dependency: "direct dev" + description: + name: flutter_lints + sha256: "3105dc8492f6183fb076ccf1f351ac3d60564bff92e20bfc4af9cc1651f4e7e1" + url: "https://pub.dev" + source: hosted + version: "6.0.0" + flutter_test: + dependency: "direct dev" + description: flutter + source: sdk + version: "0.0.0" + leak_tracker: + dependency: transitive + description: + name: leak_tracker + sha256: "33e2e26bdd85a0112ec15400c8cbffea70d0f9c3407491f672a2fad47915e2de" + url: "https://pub.dev" + source: hosted + version: "11.0.2" + leak_tracker_flutter_testing: + dependency: transitive + description: + name: leak_tracker_flutter_testing + sha256: "1dbc140bb5a23c75ea9c4811222756104fbcd1a27173f0c34ca01e16bea473c1" + url: "https://pub.dev" + source: hosted + version: "3.0.10" + leak_tracker_testing: + dependency: transitive + description: + name: leak_tracker_testing + sha256: "8d5a2d49f4a66b49744b23b018848400d23e54caf9463f4eb20df3eb8acb2eb1" + url: "https://pub.dev" + source: hosted + version: "3.0.2" + lints: + dependency: transitive + description: + name: lints + sha256: "12f842a479589fea194fe5c5a3095abc7be0c1f2ddfa9a0e76aed1dbd26a87df" + url: "https://pub.dev" + source: hosted + version: "6.1.0" + matcher: + dependency: transitive + description: + name: matcher + sha256: "12956d0ad8390bbcc63ca2e1469c0619946ccb52809807067a7020d57e647aa6" + url: "https://pub.dev" + source: hosted + version: "0.12.18" + material_color_utilities: + dependency: transitive + description: + name: material_color_utilities + sha256: "9c337007e82b1889149c82ed242ed1cb24a66044e30979c44912381e9be4c48b" + url: "https://pub.dev" + source: hosted + version: "0.13.0" + meta: + dependency: transitive + description: + name: meta + sha256: "23f08335362185a5ea2ad3a4e597f1375e78bce8a040df5c600c8d3552ef2394" + url: "https://pub.dev" + source: hosted + version: "1.17.0" + path: + dependency: transitive + description: + name: path + sha256: "75cca69d1490965be98c73ceaea117e8a04dd21217b37b292c9ddbec0d955bc5" + url: "https://pub.dev" + source: hosted + version: "1.9.1" + sky_engine: + dependency: transitive + description: flutter + source: sdk + version: "0.0.0" + source_span: + dependency: transitive + description: + name: source_span + sha256: "56a02f1f4cd1a2d96303c0144c93bd6d909eea6bee6bf5a0e0b685edbd4c47ab" + url: "https://pub.dev" + source: hosted + version: "1.10.2" + stack_trace: + dependency: transitive + description: + name: stack_trace + sha256: "8b27215b45d22309b5cddda1aa2b19bdfec9df0e765f2de506401c071d38d1b1" + url: "https://pub.dev" + source: hosted + version: "1.12.1" + stream_channel: + dependency: transitive + description: + name: stream_channel + sha256: "969e04c80b8bcdf826f8f16579c7b14d780458bd97f56d107d3950fdbeef059d" + url: "https://pub.dev" + source: hosted + version: "2.1.4" + string_scanner: + dependency: transitive + description: + name: string_scanner + sha256: "921cd31725b72fe181906c6a94d987c78e3b98c2e205b397ea399d4054872b43" + url: "https://pub.dev" + source: hosted + version: "1.4.1" + term_glyph: + dependency: transitive + description: + name: term_glyph + sha256: "7f554798625ea768a7518313e58f83891c7f5024f88e46e7182a4558850a4b8e" + url: "https://pub.dev" + source: hosted + version: "1.2.2" + test_api: + dependency: transitive + description: + name: test_api + sha256: "93167629bfc610f71560ab9312acdda4959de4df6fac7492c89ff0d3886f6636" + url: "https://pub.dev" + source: hosted + version: "0.7.9" + vector_math: + dependency: transitive + description: + name: vector_math + sha256: d530bd74fea330e6e364cda7a85019c434070188383e1cd8d9777ee586914c5b + url: "https://pub.dev" + source: hosted + version: "2.2.0" + vm_service: + dependency: transitive + description: + name: vm_service + sha256: "45caa6c5917fa127b5dbcfbd1fa60b14e583afdc08bfc96dda38886ca252eb60" + url: "https://pub.dev" + source: hosted + version: "15.0.2" +sdks: + dart: ">=3.11.0 <4.0.0" + flutter: ">=3.18.0-18.0.pre.54" diff --git a/pubspec.yaml b/pubspec.yaml new file mode 100644 index 0000000..8deaf9f --- /dev/null +++ b/pubspec.yaml @@ -0,0 +1,89 @@ +name: nomadcode +description: "A new Flutter project." +# The following line prevents the package from being accidentally published to +# pub.dev using `flutter pub publish`. This is preferred for private packages. +publish_to: 'none' # Remove this line if you wish to publish to pub.dev + +# The following defines the version and build number for your application. +# A version number is three numbers separated by dots, like 1.2.43 +# followed by an optional build number separated by a +. +# Both the version and the builder number may be overridden in flutter +# build by specifying --build-name and --build-number, respectively. +# In Android, build-name is used as versionName while build-number used as versionCode. +# Read more about Android versioning at https://developer.android.com/studio/publish/versioning +# In iOS, build-name is used as CFBundleShortVersionString while build-number is used as CFBundleVersion. +# Read more about iOS versioning at +# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html +# In Windows, build-name is used as the major, minor, and patch parts +# of the product and file versions while build-number is used as the build suffix. +version: 1.0.0+1 + +environment: + sdk: ^3.11.0 + +# Dependencies specify other packages that your package needs in order to work. +# To automatically upgrade your package dependencies to the latest versions +# consider running `flutter pub upgrade --major-versions`. Alternatively, +# dependencies can be manually updated by changing the version numbers below to +# the latest version available on pub.dev. To see which dependencies have newer +# versions available, run `flutter pub outdated`. +dependencies: + flutter: + sdk: flutter + + # The following adds the Cupertino Icons font to your application. + # Use with the CupertinoIcons class for iOS style icons. + cupertino_icons: ^1.0.8 + +dev_dependencies: + flutter_test: + sdk: flutter + + # The "flutter_lints" package below contains a set of recommended lints to + # encourage good coding practices. The lint set provided by the package is + # activated in the `analysis_options.yaml` file located at the root of your + # package. See that file for information about deactivating specific lint + # rules and activating additional ones. + flutter_lints: ^6.0.0 + +# For information on the generic Dart part of this file, see the +# following page: https://dart.dev/tools/pub/pubspec + +# The following section is specific to Flutter packages. +flutter: + + # The following line ensures that the Material Icons font is + # included with your application, so that you can use the icons in + # the material Icons class. + uses-material-design: true + + # To add assets to your application, add an assets section, like this: + # assets: + # - images/a_dot_burr.jpeg + # - images/a_dot_ham.jpeg + + # An image asset can refer to one or more resolution-specific "variants", see + # https://flutter.dev/to/resolution-aware-images + + # For details regarding adding assets from package dependencies, see + # https://flutter.dev/to/asset-from-package + + # To add custom fonts to your application, add a fonts section here, + # in this "flutter" section. Each entry in this list should have a + # "family" key with the font family name, and a "fonts" key with a + # list giving the asset and other descriptors for the font. For + # example: + # fonts: + # - family: Schyler + # fonts: + # - asset: fonts/Schyler-Regular.ttf + # - asset: fonts/Schyler-Italic.ttf + # style: italic + # - family: Trajan Pro + # fonts: + # - asset: fonts/TrajanPro.ttf + # - asset: fonts/TrajanPro_Bold.ttf + # weight: 700 + # + # For details regarding fonts from package dependencies, + # see https://flutter.dev/to/font-from-package diff --git a/test/widget_test.dart b/test/widget_test.dart new file mode 100644 index 0000000..d3998ee --- /dev/null +++ b/test/widget_test.dart @@ -0,0 +1,30 @@ +// This is a basic Flutter widget test. +// +// To perform an interaction with a widget in your test, use the WidgetTester +// utility in the flutter_test package. For example, you can send tap and scroll +// gestures. You can also use WidgetTester to find child widgets in the widget +// tree, read text, and verify that the values of widget properties are correct. + +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; + +import 'package:nomadcode/main.dart'; + +void main() { + testWidgets('Counter increments smoke test', (WidgetTester tester) async { + // Build our app and trigger a frame. + await tester.pumpWidget(const MyApp()); + + // Verify that our counter starts at 0. + expect(find.text('0'), findsOneWidget); + expect(find.text('1'), findsNothing); + + // Tap the '+' icon and trigger a frame. + await tester.tap(find.byIcon(Icons.add)); + await tester.pump(); + + // Verify that our counter has incremented. + expect(find.text('0'), findsNothing); + expect(find.text('1'), findsOneWidget); + }); +} diff --git a/web/favicon.png b/web/favicon.png new file mode 100644 index 0000000000000000000000000000000000000000..8aaa46ac1ae21512746f852a42ba87e4165dfdd1 GIT binary patch literal 917 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!61|;P_|4#%`jKx9jP7LeL$-D$|I14-?iy0X7 zltGxWVyS%@P(fs7NJL45ua8x7ey(0(N`6wRUPW#JP&EUCO@$SZnVVXYs8ErclUHn2 zVXFjIVFhG^g!Ppaz)DK8ZIvQ?0~DO|i&7O#^-S~(l1AfjnEK zjFOT9D}DX)@^Za$W4-*MbbUihOG|wNBYh(yU7!lx;>x^|#0uTKVr7USFmqf|i<65o z3raHc^AtelCMM;Vme?vOfh>Xph&xL%(-1c06+^uR^q@XSM&D4+Kp$>4P^%3{)XKjo zGZknv$b36P8?Z_gF{nK@`XI}Z90TzwSQO}0J1!f2c(B=V`5aP@1P1a|PZ!4!3&Gl8 zTYqUsf!gYFyJnXpu0!n&N*SYAX-%d(5gVjrHJWqXQshj@!Zm{!01WsQrH~9=kTxW#6SvuapgMqt>$=j#%eyGrQzr zP{L-3gsMA^$I1&gsBAEL+vxi1*Igl=8#8`5?A-T5=z-sk46WA1IUT)AIZHx1rdUrf zVJrJn<74DDw`j)Ki#gt}mIT-Q`XRa2-jQXQoI%w`nb|XblvzK${ZzlV)m-XcwC(od z71_OEC5Bt9GEXosOXaPTYOia#R4ID2TiU~`zVMl08TV_C%DnU4^+HE>9(CE4D6?Fz oujB08i7adh9xk7*FX66dWH6F5TM;?E2b5PlUHx3vIVCg!0Dx9vYXATM literal 0 HcmV?d00001 diff --git a/web/icons/Icon-192.png b/web/icons/Icon-192.png new file mode 100644 index 0000000000000000000000000000000000000000..b749bfef07473333cf1dd31e9eed89862a5d52aa GIT binary patch literal 5292 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 literal 0 HcmV?d00001 diff --git a/web/icons/Icon-512.png b/web/icons/Icon-512.png new file mode 100644 index 0000000000000000000000000000000000000000..88cfd48dff1169879ba46840804b412fe02fefd6 GIT binary patch literal 8252 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 literal 0 HcmV?d00001 diff --git a/web/icons/Icon-maskable-192.png b/web/icons/Icon-maskable-192.png new file mode 100644 index 0000000000000000000000000000000000000000..eb9b4d76e525556d5d89141648c724331630325d GIT binary patch literal 5594 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 X>c!)3PgKfL&lX^$Z>Cpu&6)6jvi^Z! literal 0 HcmV?d00001 diff --git a/web/icons/Icon-maskable-512.png b/web/icons/Icon-maskable-512.png new file mode 100644 index 0000000000000000000000000000000000000000..d69c56691fbdb0b7efa65097c7cc1edac12a6d3e GIT binary patch literal 20998 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 MPgg&ebxsLQ025IeI{*Lx literal 0 HcmV?d00001 diff --git a/web/index.html b/web/index.html new file mode 100644 index 0000000..c562ebf --- /dev/null +++ b/web/index.html @@ -0,0 +1,46 @@ + + + + + + + + + + + + + + + + + + + + nomadcode + + + + + + + diff --git a/web/manifest.json b/web/manifest.json new file mode 100644 index 0000000..c76897c --- /dev/null +++ b/web/manifest.json @@ -0,0 +1,35 @@ +{ + "name": "nomadcode", + "short_name": "nomadcode", + "start_url": ".", + "display": "standalone", + "background_color": "#0175C2", + "theme_color": "#0175C2", + "description": "A new Flutter project.", + "orientation": "portrait-primary", + "prefer_related_applications": false, + "icons": [ + { + "src": "icons/Icon-192.png", + "sizes": "192x192", + "type": "image/png" + }, + { + "src": "icons/Icon-512.png", + "sizes": "512x512", + "type": "image/png" + }, + { + "src": "icons/Icon-maskable-192.png", + "sizes": "192x192", + "type": "image/png", + "purpose": "maskable" + }, + { + "src": "icons/Icon-maskable-512.png", + "sizes": "512x512", + "type": "image/png", + "purpose": "maskable" + } + ] +} diff --git a/windows/.gitignore b/windows/.gitignore new file mode 100644 index 0000000..d492d0d --- /dev/null +++ b/windows/.gitignore @@ -0,0 +1,17 @@ +flutter/ephemeral/ + +# Visual Studio user-specific files. +*.suo +*.user +*.userosscache +*.sln.docstates + +# Visual Studio build-related files. +x64/ +x86/ + +# Visual Studio cache files +# files ending in .cache can be ignored +*.[Cc]ache +# but keep track of directories ending in .cache +!*.[Cc]ache/ diff --git a/windows/CMakeLists.txt b/windows/CMakeLists.txt new file mode 100644 index 0000000..7f79357 --- /dev/null +++ b/windows/CMakeLists.txt @@ -0,0 +1,108 @@ +# Project-level configuration. +cmake_minimum_required(VERSION 3.14) +project(nomadcode LANGUAGES CXX) + +# The name of the executable created for the application. Change this to change +# the on-disk name of your application. +set(BINARY_NAME "nomadcode") + +# Explicitly opt in to modern CMake behaviors to avoid warnings with recent +# versions of CMake. +cmake_policy(VERSION 3.14...3.25) + +# Define build configuration option. +get_property(IS_MULTICONFIG GLOBAL PROPERTY GENERATOR_IS_MULTI_CONFIG) +if(IS_MULTICONFIG) + set(CMAKE_CONFIGURATION_TYPES "Debug;Profile;Release" + CACHE STRING "" FORCE) +else() + if(NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES) + set(CMAKE_BUILD_TYPE "Debug" CACHE + STRING "Flutter build mode" FORCE) + set_property(CACHE CMAKE_BUILD_TYPE PROPERTY STRINGS + "Debug" "Profile" "Release") + endif() +endif() +# Define settings for the Profile build mode. +set(CMAKE_EXE_LINKER_FLAGS_PROFILE "${CMAKE_EXE_LINKER_FLAGS_RELEASE}") +set(CMAKE_SHARED_LINKER_FLAGS_PROFILE "${CMAKE_SHARED_LINKER_FLAGS_RELEASE}") +set(CMAKE_C_FLAGS_PROFILE "${CMAKE_C_FLAGS_RELEASE}") +set(CMAKE_CXX_FLAGS_PROFILE "${CMAKE_CXX_FLAGS_RELEASE}") + +# Use Unicode for all projects. +add_definitions(-DUNICODE -D_UNICODE) + +# Compilation settings that should be applied to most targets. +# +# Be cautious about adding new options here, as plugins use this function by +# default. In most cases, you should add new options to specific targets instead +# of modifying this function. +function(APPLY_STANDARD_SETTINGS TARGET) + target_compile_features(${TARGET} PUBLIC cxx_std_17) + target_compile_options(${TARGET} PRIVATE /W4 /WX /wd"4100") + target_compile_options(${TARGET} PRIVATE /EHsc) + target_compile_definitions(${TARGET} PRIVATE "_HAS_EXCEPTIONS=0") + target_compile_definitions(${TARGET} PRIVATE "$<$:_DEBUG>") +endfunction() + +# Flutter library and tool build rules. +set(FLUTTER_MANAGED_DIR "${CMAKE_CURRENT_SOURCE_DIR}/flutter") +add_subdirectory(${FLUTTER_MANAGED_DIR}) + +# Application build; see runner/CMakeLists.txt. +add_subdirectory("runner") + + +# Generated plugin build rules, which manage building the plugins and adding +# them to the application. +include(flutter/generated_plugins.cmake) + + +# === Installation === +# Support files are copied into place next to the executable, so that it can +# run in place. This is done instead of making a separate bundle (as on Linux) +# so that building and running from within Visual Studio will work. +set(BUILD_BUNDLE_DIR "$") +# Make the "install" step default, as it's required to run. +set(CMAKE_VS_INCLUDE_INSTALL_TO_DEFAULT_BUILD 1) +if(CMAKE_INSTALL_PREFIX_INITIALIZED_TO_DEFAULT) + set(CMAKE_INSTALL_PREFIX "${BUILD_BUNDLE_DIR}" CACHE PATH "..." FORCE) +endif() + +set(INSTALL_BUNDLE_DATA_DIR "${CMAKE_INSTALL_PREFIX}/data") +set(INSTALL_BUNDLE_LIB_DIR "${CMAKE_INSTALL_PREFIX}") + +install(TARGETS ${BINARY_NAME} RUNTIME DESTINATION "${CMAKE_INSTALL_PREFIX}" + COMPONENT Runtime) + +install(FILES "${FLUTTER_ICU_DATA_FILE}" DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" + COMPONENT Runtime) + +install(FILES "${FLUTTER_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" + COMPONENT Runtime) + +if(PLUGIN_BUNDLED_LIBRARIES) + install(FILES "${PLUGIN_BUNDLED_LIBRARIES}" + DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" + COMPONENT Runtime) +endif() + +# Copy the native assets provided by the build.dart from all packages. +set(NATIVE_ASSETS_DIR "${PROJECT_BUILD_DIR}native_assets/windows/") +install(DIRECTORY "${NATIVE_ASSETS_DIR}" + DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" + COMPONENT Runtime) + +# Fully re-copy the assets directory on each build to avoid having stale files +# from a previous install. +set(FLUTTER_ASSET_DIR_NAME "flutter_assets") +install(CODE " + file(REMOVE_RECURSE \"${INSTALL_BUNDLE_DATA_DIR}/${FLUTTER_ASSET_DIR_NAME}\") + " COMPONENT Runtime) +install(DIRECTORY "${PROJECT_BUILD_DIR}/${FLUTTER_ASSET_DIR_NAME}" + DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" COMPONENT Runtime) + +# Install the AOT library on non-Debug builds only. +install(FILES "${AOT_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" + CONFIGURATIONS Profile;Release + COMPONENT Runtime) diff --git a/windows/flutter/CMakeLists.txt b/windows/flutter/CMakeLists.txt new file mode 100644 index 0000000..903f489 --- /dev/null +++ b/windows/flutter/CMakeLists.txt @@ -0,0 +1,109 @@ +# This file controls Flutter-level build steps. It should not be edited. +cmake_minimum_required(VERSION 3.14) + +set(EPHEMERAL_DIR "${CMAKE_CURRENT_SOURCE_DIR}/ephemeral") + +# Configuration provided via flutter tool. +include(${EPHEMERAL_DIR}/generated_config.cmake) + +# TODO: Move the rest of this into files in ephemeral. See +# https://github.com/flutter/flutter/issues/57146. +set(WRAPPER_ROOT "${EPHEMERAL_DIR}/cpp_client_wrapper") + +# Set fallback configurations for older versions of the flutter tool. +if (NOT DEFINED FLUTTER_TARGET_PLATFORM) + set(FLUTTER_TARGET_PLATFORM "windows-x64") +endif() + +# === Flutter Library === +set(FLUTTER_LIBRARY "${EPHEMERAL_DIR}/flutter_windows.dll") + +# Published to parent scope for install step. +set(FLUTTER_LIBRARY ${FLUTTER_LIBRARY} PARENT_SCOPE) +set(FLUTTER_ICU_DATA_FILE "${EPHEMERAL_DIR}/icudtl.dat" PARENT_SCOPE) +set(PROJECT_BUILD_DIR "${PROJECT_DIR}/build/" PARENT_SCOPE) +set(AOT_LIBRARY "${PROJECT_DIR}/build/windows/app.so" PARENT_SCOPE) + +list(APPEND FLUTTER_LIBRARY_HEADERS + "flutter_export.h" + "flutter_windows.h" + "flutter_messenger.h" + "flutter_plugin_registrar.h" + "flutter_texture_registrar.h" +) +list(TRANSFORM FLUTTER_LIBRARY_HEADERS PREPEND "${EPHEMERAL_DIR}/") +add_library(flutter INTERFACE) +target_include_directories(flutter INTERFACE + "${EPHEMERAL_DIR}" +) +target_link_libraries(flutter INTERFACE "${FLUTTER_LIBRARY}.lib") +add_dependencies(flutter flutter_assemble) + +# === Wrapper === +list(APPEND CPP_WRAPPER_SOURCES_CORE + "core_implementations.cc" + "standard_codec.cc" +) +list(TRANSFORM CPP_WRAPPER_SOURCES_CORE PREPEND "${WRAPPER_ROOT}/") +list(APPEND CPP_WRAPPER_SOURCES_PLUGIN + "plugin_registrar.cc" +) +list(TRANSFORM CPP_WRAPPER_SOURCES_PLUGIN PREPEND "${WRAPPER_ROOT}/") +list(APPEND CPP_WRAPPER_SOURCES_APP + "flutter_engine.cc" + "flutter_view_controller.cc" +) +list(TRANSFORM CPP_WRAPPER_SOURCES_APP PREPEND "${WRAPPER_ROOT}/") + +# Wrapper sources needed for a plugin. +add_library(flutter_wrapper_plugin STATIC + ${CPP_WRAPPER_SOURCES_CORE} + ${CPP_WRAPPER_SOURCES_PLUGIN} +) +apply_standard_settings(flutter_wrapper_plugin) +set_target_properties(flutter_wrapper_plugin PROPERTIES + POSITION_INDEPENDENT_CODE ON) +set_target_properties(flutter_wrapper_plugin PROPERTIES + CXX_VISIBILITY_PRESET hidden) +target_link_libraries(flutter_wrapper_plugin PUBLIC flutter) +target_include_directories(flutter_wrapper_plugin PUBLIC + "${WRAPPER_ROOT}/include" +) +add_dependencies(flutter_wrapper_plugin flutter_assemble) + +# Wrapper sources needed for the runner. +add_library(flutter_wrapper_app STATIC + ${CPP_WRAPPER_SOURCES_CORE} + ${CPP_WRAPPER_SOURCES_APP} +) +apply_standard_settings(flutter_wrapper_app) +target_link_libraries(flutter_wrapper_app PUBLIC flutter) +target_include_directories(flutter_wrapper_app PUBLIC + "${WRAPPER_ROOT}/include" +) +add_dependencies(flutter_wrapper_app flutter_assemble) + +# === Flutter tool backend === +# _phony_ is a non-existent file to force this command to run every time, +# since currently there's no way to get a full input/output list from the +# flutter tool. +set(PHONY_OUTPUT "${CMAKE_CURRENT_BINARY_DIR}/_phony_") +set_source_files_properties("${PHONY_OUTPUT}" PROPERTIES SYMBOLIC TRUE) +add_custom_command( + OUTPUT ${FLUTTER_LIBRARY} ${FLUTTER_LIBRARY_HEADERS} + ${CPP_WRAPPER_SOURCES_CORE} ${CPP_WRAPPER_SOURCES_PLUGIN} + ${CPP_WRAPPER_SOURCES_APP} + ${PHONY_OUTPUT} + COMMAND ${CMAKE_COMMAND} -E env + ${FLUTTER_TOOL_ENVIRONMENT} + "${FLUTTER_ROOT}/packages/flutter_tools/bin/tool_backend.bat" + ${FLUTTER_TARGET_PLATFORM} $ + VERBATIM +) +add_custom_target(flutter_assemble DEPENDS + "${FLUTTER_LIBRARY}" + ${FLUTTER_LIBRARY_HEADERS} + ${CPP_WRAPPER_SOURCES_CORE} + ${CPP_WRAPPER_SOURCES_PLUGIN} + ${CPP_WRAPPER_SOURCES_APP} +) diff --git a/windows/flutter/generated_plugin_registrant.cc b/windows/flutter/generated_plugin_registrant.cc new file mode 100644 index 0000000..8b6d468 --- /dev/null +++ b/windows/flutter/generated_plugin_registrant.cc @@ -0,0 +1,11 @@ +// +// Generated file. Do not edit. +// + +// clang-format off + +#include "generated_plugin_registrant.h" + + +void RegisterPlugins(flutter::PluginRegistry* registry) { +} diff --git a/windows/flutter/generated_plugin_registrant.h b/windows/flutter/generated_plugin_registrant.h new file mode 100644 index 0000000..dc139d8 --- /dev/null +++ b/windows/flutter/generated_plugin_registrant.h @@ -0,0 +1,15 @@ +// +// Generated file. Do not edit. +// + +// clang-format off + +#ifndef GENERATED_PLUGIN_REGISTRANT_ +#define GENERATED_PLUGIN_REGISTRANT_ + +#include + +// Registers Flutter plugins. +void RegisterPlugins(flutter::PluginRegistry* registry); + +#endif // GENERATED_PLUGIN_REGISTRANT_ diff --git a/windows/flutter/generated_plugins.cmake b/windows/flutter/generated_plugins.cmake new file mode 100644 index 0000000..b93c4c3 --- /dev/null +++ b/windows/flutter/generated_plugins.cmake @@ -0,0 +1,23 @@ +# +# Generated file, do not edit. +# + +list(APPEND FLUTTER_PLUGIN_LIST +) + +list(APPEND FLUTTER_FFI_PLUGIN_LIST +) + +set(PLUGIN_BUNDLED_LIBRARIES) + +foreach(plugin ${FLUTTER_PLUGIN_LIST}) + add_subdirectory(flutter/ephemeral/.plugin_symlinks/${plugin}/windows plugins/${plugin}) + target_link_libraries(${BINARY_NAME} PRIVATE ${plugin}_plugin) + list(APPEND PLUGIN_BUNDLED_LIBRARIES $) + list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${plugin}_bundled_libraries}) +endforeach(plugin) + +foreach(ffi_plugin ${FLUTTER_FFI_PLUGIN_LIST}) + add_subdirectory(flutter/ephemeral/.plugin_symlinks/${ffi_plugin}/windows plugins/${ffi_plugin}) + list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${ffi_plugin}_bundled_libraries}) +endforeach(ffi_plugin) diff --git a/windows/runner/CMakeLists.txt b/windows/runner/CMakeLists.txt new file mode 100644 index 0000000..394917c --- /dev/null +++ b/windows/runner/CMakeLists.txt @@ -0,0 +1,40 @@ +cmake_minimum_required(VERSION 3.14) +project(runner LANGUAGES CXX) + +# Define the application target. To change its name, change BINARY_NAME in the +# top-level CMakeLists.txt, not the value here, or `flutter run` will no longer +# work. +# +# Any new source files that you add to the application should be added here. +add_executable(${BINARY_NAME} WIN32 + "flutter_window.cpp" + "main.cpp" + "utils.cpp" + "win32_window.cpp" + "${FLUTTER_MANAGED_DIR}/generated_plugin_registrant.cc" + "Runner.rc" + "runner.exe.manifest" +) + +# Apply the standard set of build settings. This can be removed for applications +# that need different build settings. +apply_standard_settings(${BINARY_NAME}) + +# Add preprocessor definitions for the build version. +target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION=\"${FLUTTER_VERSION}\"") +target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_MAJOR=${FLUTTER_VERSION_MAJOR}") +target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_MINOR=${FLUTTER_VERSION_MINOR}") +target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_PATCH=${FLUTTER_VERSION_PATCH}") +target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_BUILD=${FLUTTER_VERSION_BUILD}") + +# Disable Windows macros that collide with C++ standard library functions. +target_compile_definitions(${BINARY_NAME} PRIVATE "NOMINMAX") + +# Add dependency libraries and include directories. Add any application-specific +# dependencies here. +target_link_libraries(${BINARY_NAME} PRIVATE flutter flutter_wrapper_app) +target_link_libraries(${BINARY_NAME} PRIVATE "dwmapi.lib") +target_include_directories(${BINARY_NAME} PRIVATE "${CMAKE_SOURCE_DIR}") + +# Run the Flutter tool portions of the build. This must not be removed. +add_dependencies(${BINARY_NAME} flutter_assemble) diff --git a/windows/runner/Runner.rc b/windows/runner/Runner.rc new file mode 100644 index 0000000..7eefe46 --- /dev/null +++ b/windows/runner/Runner.rc @@ -0,0 +1,121 @@ +// Microsoft Visual C++ generated resource script. +// +#pragma code_page(65001) +#include "resource.h" + +#define APSTUDIO_READONLY_SYMBOLS +///////////////////////////////////////////////////////////////////////////// +// +// Generated from the TEXTINCLUDE 2 resource. +// +#include "winres.h" + +///////////////////////////////////////////////////////////////////////////// +#undef APSTUDIO_READONLY_SYMBOLS + +///////////////////////////////////////////////////////////////////////////// +// English (United States) resources + +#if !defined(AFX_RESOURCE_DLL) || defined(AFX_TARG_ENU) +LANGUAGE LANG_ENGLISH, SUBLANG_ENGLISH_US + +#ifdef APSTUDIO_INVOKED +///////////////////////////////////////////////////////////////////////////// +// +// TEXTINCLUDE +// + +1 TEXTINCLUDE +BEGIN + "resource.h\0" +END + +2 TEXTINCLUDE +BEGIN + "#include ""winres.h""\r\n" + "\0" +END + +3 TEXTINCLUDE +BEGIN + "\r\n" + "\0" +END + +#endif // APSTUDIO_INVOKED + + +///////////////////////////////////////////////////////////////////////////// +// +// Icon +// + +// Icon with lowest ID value placed first to ensure application icon +// remains consistent on all systems. +IDI_APP_ICON ICON "resources\\app_icon.ico" + + +///////////////////////////////////////////////////////////////////////////// +// +// Version +// + +#if defined(FLUTTER_VERSION_MAJOR) && defined(FLUTTER_VERSION_MINOR) && defined(FLUTTER_VERSION_PATCH) && defined(FLUTTER_VERSION_BUILD) +#define VERSION_AS_NUMBER FLUTTER_VERSION_MAJOR,FLUTTER_VERSION_MINOR,FLUTTER_VERSION_PATCH,FLUTTER_VERSION_BUILD +#else +#define VERSION_AS_NUMBER 1,0,0,0 +#endif + +#if defined(FLUTTER_VERSION) +#define VERSION_AS_STRING FLUTTER_VERSION +#else +#define VERSION_AS_STRING "1.0.0" +#endif + +VS_VERSION_INFO VERSIONINFO + FILEVERSION VERSION_AS_NUMBER + PRODUCTVERSION VERSION_AS_NUMBER + FILEFLAGSMASK VS_FFI_FILEFLAGSMASK +#ifdef _DEBUG + FILEFLAGS VS_FF_DEBUG +#else + FILEFLAGS 0x0L +#endif + FILEOS VOS__WINDOWS32 + FILETYPE VFT_APP + FILESUBTYPE 0x0L +BEGIN + BLOCK "StringFileInfo" + BEGIN + BLOCK "040904e4" + BEGIN + VALUE "CompanyName", "com.nomadcode" "\0" + VALUE "FileDescription", "nomadcode" "\0" + VALUE "FileVersion", VERSION_AS_STRING "\0" + VALUE "InternalName", "nomadcode" "\0" + VALUE "LegalCopyright", "Copyright (C) 2026 com.nomadcode. All rights reserved." "\0" + VALUE "OriginalFilename", "nomadcode.exe" "\0" + VALUE "ProductName", "nomadcode" "\0" + VALUE "ProductVersion", VERSION_AS_STRING "\0" + END + END + BLOCK "VarFileInfo" + BEGIN + VALUE "Translation", 0x409, 1252 + END +END + +#endif // English (United States) resources +///////////////////////////////////////////////////////////////////////////// + + + +#ifndef APSTUDIO_INVOKED +///////////////////////////////////////////////////////////////////////////// +// +// Generated from the TEXTINCLUDE 3 resource. +// + + +///////////////////////////////////////////////////////////////////////////// +#endif // not APSTUDIO_INVOKED diff --git a/windows/runner/flutter_window.cpp b/windows/runner/flutter_window.cpp new file mode 100644 index 0000000..955ee30 --- /dev/null +++ b/windows/runner/flutter_window.cpp @@ -0,0 +1,71 @@ +#include "flutter_window.h" + +#include + +#include "flutter/generated_plugin_registrant.h" + +FlutterWindow::FlutterWindow(const flutter::DartProject& project) + : project_(project) {} + +FlutterWindow::~FlutterWindow() {} + +bool FlutterWindow::OnCreate() { + if (!Win32Window::OnCreate()) { + return false; + } + + RECT frame = GetClientArea(); + + // The size here must match the window dimensions to avoid unnecessary surface + // creation / destruction in the startup path. + flutter_controller_ = std::make_unique( + frame.right - frame.left, frame.bottom - frame.top, project_); + // Ensure that basic setup of the controller was successful. + if (!flutter_controller_->engine() || !flutter_controller_->view()) { + return false; + } + RegisterPlugins(flutter_controller_->engine()); + SetChildContent(flutter_controller_->view()->GetNativeWindow()); + + flutter_controller_->engine()->SetNextFrameCallback([&]() { + this->Show(); + }); + + // Flutter can complete the first frame before the "show window" callback is + // registered. The following call ensures a frame is pending to ensure the + // window is shown. It is a no-op if the first frame hasn't completed yet. + flutter_controller_->ForceRedraw(); + + return true; +} + +void FlutterWindow::OnDestroy() { + if (flutter_controller_) { + flutter_controller_ = nullptr; + } + + Win32Window::OnDestroy(); +} + +LRESULT +FlutterWindow::MessageHandler(HWND hwnd, UINT const message, + WPARAM const wparam, + LPARAM const lparam) noexcept { + // Give Flutter, including plugins, an opportunity to handle window messages. + if (flutter_controller_) { + std::optional result = + flutter_controller_->HandleTopLevelWindowProc(hwnd, message, wparam, + lparam); + if (result) { + return *result; + } + } + + switch (message) { + case WM_FONTCHANGE: + flutter_controller_->engine()->ReloadSystemFonts(); + break; + } + + return Win32Window::MessageHandler(hwnd, message, wparam, lparam); +} diff --git a/windows/runner/flutter_window.h b/windows/runner/flutter_window.h new file mode 100644 index 0000000..6da0652 --- /dev/null +++ b/windows/runner/flutter_window.h @@ -0,0 +1,33 @@ +#ifndef RUNNER_FLUTTER_WINDOW_H_ +#define RUNNER_FLUTTER_WINDOW_H_ + +#include +#include + +#include + +#include "win32_window.h" + +// A window that does nothing but host a Flutter view. +class FlutterWindow : public Win32Window { + public: + // Creates a new FlutterWindow hosting a Flutter view running |project|. + explicit FlutterWindow(const flutter::DartProject& project); + virtual ~FlutterWindow(); + + protected: + // Win32Window: + bool OnCreate() override; + void OnDestroy() override; + LRESULT MessageHandler(HWND window, UINT const message, WPARAM const wparam, + LPARAM const lparam) noexcept override; + + private: + // The project to run. + flutter::DartProject project_; + + // The Flutter instance hosted by this window. + std::unique_ptr flutter_controller_; +}; + +#endif // RUNNER_FLUTTER_WINDOW_H_ diff --git a/windows/runner/main.cpp b/windows/runner/main.cpp new file mode 100644 index 0000000..e62d737 --- /dev/null +++ b/windows/runner/main.cpp @@ -0,0 +1,43 @@ +#include +#include +#include + +#include "flutter_window.h" +#include "utils.h" + +int APIENTRY wWinMain(_In_ HINSTANCE instance, _In_opt_ HINSTANCE prev, + _In_ wchar_t *command_line, _In_ int show_command) { + // Attach to console when present (e.g., 'flutter run') or create a + // new console when running with a debugger. + if (!::AttachConsole(ATTACH_PARENT_PROCESS) && ::IsDebuggerPresent()) { + CreateAndAttachConsole(); + } + + // Initialize COM, so that it is available for use in the library and/or + // plugins. + ::CoInitializeEx(nullptr, COINIT_APARTMENTTHREADED); + + flutter::DartProject project(L"data"); + + std::vector command_line_arguments = + GetCommandLineArguments(); + + project.set_dart_entrypoint_arguments(std::move(command_line_arguments)); + + FlutterWindow window(project); + Win32Window::Point origin(10, 10); + Win32Window::Size size(1280, 720); + if (!window.Create(L"nomadcode", origin, size)) { + return EXIT_FAILURE; + } + window.SetQuitOnClose(true); + + ::MSG msg; + while (::GetMessage(&msg, nullptr, 0, 0)) { + ::TranslateMessage(&msg); + ::DispatchMessage(&msg); + } + + ::CoUninitialize(); + return EXIT_SUCCESS; +} diff --git a/windows/runner/resource.h b/windows/runner/resource.h new file mode 100644 index 0000000..66a65d1 --- /dev/null +++ b/windows/runner/resource.h @@ -0,0 +1,16 @@ +//{{NO_DEPENDENCIES}} +// Microsoft Visual C++ generated include file. +// Used by Runner.rc +// +#define IDI_APP_ICON 101 + +// Next default values for new objects +// +#ifdef APSTUDIO_INVOKED +#ifndef APSTUDIO_READONLY_SYMBOLS +#define _APS_NEXT_RESOURCE_VALUE 102 +#define _APS_NEXT_COMMAND_VALUE 40001 +#define _APS_NEXT_CONTROL_VALUE 1001 +#define _APS_NEXT_SYMED_VALUE 101 +#endif +#endif diff --git a/windows/runner/resources/app_icon.ico b/windows/runner/resources/app_icon.ico new file mode 100644 index 0000000000000000000000000000000000000000..c04e20caf6370ebb9253ad831cc31de4a9c965f6 GIT binary patch literal 33772 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 literal 0 HcmV?d00001 diff --git a/windows/runner/runner.exe.manifest b/windows/runner/runner.exe.manifest new file mode 100644 index 0000000..153653e --- /dev/null +++ b/windows/runner/runner.exe.manifest @@ -0,0 +1,14 @@ + + + + + PerMonitorV2 + + + + + + + + + diff --git a/windows/runner/utils.cpp b/windows/runner/utils.cpp new file mode 100644 index 0000000..3a0b465 --- /dev/null +++ b/windows/runner/utils.cpp @@ -0,0 +1,65 @@ +#include "utils.h" + +#include +#include +#include +#include + +#include + +void CreateAndAttachConsole() { + if (::AllocConsole()) { + FILE *unused; + if (freopen_s(&unused, "CONOUT$", "w", stdout)) { + _dup2(_fileno(stdout), 1); + } + if (freopen_s(&unused, "CONOUT$", "w", stderr)) { + _dup2(_fileno(stdout), 2); + } + std::ios::sync_with_stdio(); + FlutterDesktopResyncOutputStreams(); + } +} + +std::vector GetCommandLineArguments() { + // Convert the UTF-16 command line arguments to UTF-8 for the Engine to use. + int argc; + wchar_t** argv = ::CommandLineToArgvW(::GetCommandLineW(), &argc); + if (argv == nullptr) { + return std::vector(); + } + + std::vector command_line_arguments; + + // Skip the first argument as it's the binary name. + for (int i = 1; i < argc; i++) { + command_line_arguments.push_back(Utf8FromUtf16(argv[i])); + } + + ::LocalFree(argv); + + return command_line_arguments; +} + +std::string Utf8FromUtf16(const wchar_t* utf16_string) { + if (utf16_string == nullptr) { + return std::string(); + } + unsigned int target_length = ::WideCharToMultiByte( + CP_UTF8, WC_ERR_INVALID_CHARS, utf16_string, + -1, nullptr, 0, nullptr, nullptr) + -1; // remove the trailing null character + int input_length = (int)wcslen(utf16_string); + std::string utf8_string; + if (target_length == 0 || target_length > utf8_string.max_size()) { + return utf8_string; + } + utf8_string.resize(target_length); + int converted_length = ::WideCharToMultiByte( + CP_UTF8, WC_ERR_INVALID_CHARS, utf16_string, + input_length, utf8_string.data(), target_length, nullptr, nullptr); + if (converted_length == 0) { + return std::string(); + } + return utf8_string; +} diff --git a/windows/runner/utils.h b/windows/runner/utils.h new file mode 100644 index 0000000..3879d54 --- /dev/null +++ b/windows/runner/utils.h @@ -0,0 +1,19 @@ +#ifndef RUNNER_UTILS_H_ +#define RUNNER_UTILS_H_ + +#include +#include + +// Creates a console for the process, and redirects stdout and stderr to +// it for both the runner and the Flutter library. +void CreateAndAttachConsole(); + +// Takes a null-terminated wchar_t* encoded in UTF-16 and returns a std::string +// encoded in UTF-8. Returns an empty std::string on failure. +std::string Utf8FromUtf16(const wchar_t* utf16_string); + +// Gets the command line arguments passed in as a std::vector, +// encoded in UTF-8. Returns an empty std::vector on failure. +std::vector GetCommandLineArguments(); + +#endif // RUNNER_UTILS_H_ diff --git a/windows/runner/win32_window.cpp b/windows/runner/win32_window.cpp new file mode 100644 index 0000000..60608d0 --- /dev/null +++ b/windows/runner/win32_window.cpp @@ -0,0 +1,288 @@ +#include "win32_window.h" + +#include +#include + +#include "resource.h" + +namespace { + +/// Window attribute that enables dark mode window decorations. +/// +/// Redefined in case the developer's machine has a Windows SDK older than +/// version 10.0.22000.0. +/// See: https://docs.microsoft.com/windows/win32/api/dwmapi/ne-dwmapi-dwmwindowattribute +#ifndef DWMWA_USE_IMMERSIVE_DARK_MODE +#define DWMWA_USE_IMMERSIVE_DARK_MODE 20 +#endif + +constexpr const wchar_t kWindowClassName[] = L"FLUTTER_RUNNER_WIN32_WINDOW"; + +/// Registry key for app theme preference. +/// +/// A value of 0 indicates apps should use dark mode. A non-zero or missing +/// value indicates apps should use light mode. +constexpr const wchar_t kGetPreferredBrightnessRegKey[] = + L"Software\\Microsoft\\Windows\\CurrentVersion\\Themes\\Personalize"; +constexpr const wchar_t kGetPreferredBrightnessRegValue[] = L"AppsUseLightTheme"; + +// The number of Win32Window objects that currently exist. +static int g_active_window_count = 0; + +using EnableNonClientDpiScaling = BOOL __stdcall(HWND hwnd); + +// Scale helper to convert logical scaler values to physical using passed in +// scale factor +int Scale(int source, double scale_factor) { + return static_cast(source * scale_factor); +} + +// Dynamically loads the |EnableNonClientDpiScaling| from the User32 module. +// This API is only needed for PerMonitor V1 awareness mode. +void EnableFullDpiSupportIfAvailable(HWND hwnd) { + HMODULE user32_module = LoadLibraryA("User32.dll"); + if (!user32_module) { + return; + } + auto enable_non_client_dpi_scaling = + reinterpret_cast( + GetProcAddress(user32_module, "EnableNonClientDpiScaling")); + if (enable_non_client_dpi_scaling != nullptr) { + enable_non_client_dpi_scaling(hwnd); + } + FreeLibrary(user32_module); +} + +} // namespace + +// Manages the Win32Window's window class registration. +class WindowClassRegistrar { + public: + ~WindowClassRegistrar() = default; + + // Returns the singleton registrar instance. + static WindowClassRegistrar* GetInstance() { + if (!instance_) { + instance_ = new WindowClassRegistrar(); + } + return instance_; + } + + // Returns the name of the window class, registering the class if it hasn't + // previously been registered. + const wchar_t* GetWindowClass(); + + // Unregisters the window class. Should only be called if there are no + // instances of the window. + void UnregisterWindowClass(); + + private: + WindowClassRegistrar() = default; + + static WindowClassRegistrar* instance_; + + bool class_registered_ = false; +}; + +WindowClassRegistrar* WindowClassRegistrar::instance_ = nullptr; + +const wchar_t* WindowClassRegistrar::GetWindowClass() { + if (!class_registered_) { + WNDCLASS window_class{}; + window_class.hCursor = LoadCursor(nullptr, IDC_ARROW); + window_class.lpszClassName = kWindowClassName; + window_class.style = CS_HREDRAW | CS_VREDRAW; + window_class.cbClsExtra = 0; + window_class.cbWndExtra = 0; + window_class.hInstance = GetModuleHandle(nullptr); + window_class.hIcon = + LoadIcon(window_class.hInstance, MAKEINTRESOURCE(IDI_APP_ICON)); + window_class.hbrBackground = 0; + window_class.lpszMenuName = nullptr; + window_class.lpfnWndProc = Win32Window::WndProc; + RegisterClass(&window_class); + class_registered_ = true; + } + return kWindowClassName; +} + +void WindowClassRegistrar::UnregisterWindowClass() { + UnregisterClass(kWindowClassName, nullptr); + class_registered_ = false; +} + +Win32Window::Win32Window() { + ++g_active_window_count; +} + +Win32Window::~Win32Window() { + --g_active_window_count; + Destroy(); +} + +bool Win32Window::Create(const std::wstring& title, + const Point& origin, + const Size& size) { + Destroy(); + + const wchar_t* window_class = + WindowClassRegistrar::GetInstance()->GetWindowClass(); + + const POINT target_point = {static_cast(origin.x), + static_cast(origin.y)}; + HMONITOR monitor = MonitorFromPoint(target_point, MONITOR_DEFAULTTONEAREST); + UINT dpi = FlutterDesktopGetDpiForMonitor(monitor); + double scale_factor = dpi / 96.0; + + HWND window = CreateWindow( + window_class, title.c_str(), WS_OVERLAPPEDWINDOW, + Scale(origin.x, scale_factor), Scale(origin.y, scale_factor), + Scale(size.width, scale_factor), Scale(size.height, scale_factor), + nullptr, nullptr, GetModuleHandle(nullptr), this); + + if (!window) { + return false; + } + + UpdateTheme(window); + + return OnCreate(); +} + +bool Win32Window::Show() { + return ShowWindow(window_handle_, SW_SHOWNORMAL); +} + +// static +LRESULT CALLBACK Win32Window::WndProc(HWND const window, + UINT const message, + WPARAM const wparam, + LPARAM const lparam) noexcept { + if (message == WM_NCCREATE) { + auto window_struct = reinterpret_cast(lparam); + SetWindowLongPtr(window, GWLP_USERDATA, + reinterpret_cast(window_struct->lpCreateParams)); + + auto that = static_cast(window_struct->lpCreateParams); + EnableFullDpiSupportIfAvailable(window); + that->window_handle_ = window; + } else if (Win32Window* that = GetThisFromHandle(window)) { + return that->MessageHandler(window, message, wparam, lparam); + } + + return DefWindowProc(window, message, wparam, lparam); +} + +LRESULT +Win32Window::MessageHandler(HWND hwnd, + UINT const message, + WPARAM const wparam, + LPARAM const lparam) noexcept { + switch (message) { + case WM_DESTROY: + window_handle_ = nullptr; + Destroy(); + if (quit_on_close_) { + PostQuitMessage(0); + } + return 0; + + case WM_DPICHANGED: { + auto newRectSize = reinterpret_cast(lparam); + LONG newWidth = newRectSize->right - newRectSize->left; + LONG newHeight = newRectSize->bottom - newRectSize->top; + + SetWindowPos(hwnd, nullptr, newRectSize->left, newRectSize->top, newWidth, + newHeight, SWP_NOZORDER | SWP_NOACTIVATE); + + return 0; + } + case WM_SIZE: { + RECT rect = GetClientArea(); + if (child_content_ != nullptr) { + // Size and position the child window. + MoveWindow(child_content_, rect.left, rect.top, rect.right - rect.left, + rect.bottom - rect.top, TRUE); + } + return 0; + } + + case WM_ACTIVATE: + if (child_content_ != nullptr) { + SetFocus(child_content_); + } + return 0; + + case WM_DWMCOLORIZATIONCOLORCHANGED: + UpdateTheme(hwnd); + return 0; + } + + return DefWindowProc(window_handle_, message, wparam, lparam); +} + +void Win32Window::Destroy() { + OnDestroy(); + + if (window_handle_) { + DestroyWindow(window_handle_); + window_handle_ = nullptr; + } + if (g_active_window_count == 0) { + WindowClassRegistrar::GetInstance()->UnregisterWindowClass(); + } +} + +Win32Window* Win32Window::GetThisFromHandle(HWND const window) noexcept { + return reinterpret_cast( + GetWindowLongPtr(window, GWLP_USERDATA)); +} + +void Win32Window::SetChildContent(HWND content) { + child_content_ = content; + SetParent(content, window_handle_); + RECT frame = GetClientArea(); + + MoveWindow(content, frame.left, frame.top, frame.right - frame.left, + frame.bottom - frame.top, true); + + SetFocus(child_content_); +} + +RECT Win32Window::GetClientArea() { + RECT frame; + GetClientRect(window_handle_, &frame); + return frame; +} + +HWND Win32Window::GetHandle() { + return window_handle_; +} + +void Win32Window::SetQuitOnClose(bool quit_on_close) { + quit_on_close_ = quit_on_close; +} + +bool Win32Window::OnCreate() { + // No-op; provided for subclasses. + return true; +} + +void Win32Window::OnDestroy() { + // No-op; provided for subclasses. +} + +void Win32Window::UpdateTheme(HWND const window) { + DWORD light_mode; + DWORD light_mode_size = sizeof(light_mode); + LSTATUS result = RegGetValue(HKEY_CURRENT_USER, kGetPreferredBrightnessRegKey, + kGetPreferredBrightnessRegValue, + RRF_RT_REG_DWORD, nullptr, &light_mode, + &light_mode_size); + + if (result == ERROR_SUCCESS) { + BOOL enable_dark_mode = light_mode == 0; + DwmSetWindowAttribute(window, DWMWA_USE_IMMERSIVE_DARK_MODE, + &enable_dark_mode, sizeof(enable_dark_mode)); + } +} diff --git a/windows/runner/win32_window.h b/windows/runner/win32_window.h new file mode 100644 index 0000000..e901dde --- /dev/null +++ b/windows/runner/win32_window.h @@ -0,0 +1,102 @@ +#ifndef RUNNER_WIN32_WINDOW_H_ +#define RUNNER_WIN32_WINDOW_H_ + +#include + +#include +#include +#include + +// A class abstraction for a high DPI-aware Win32 Window. Intended to be +// inherited from by classes that wish to specialize with custom +// rendering and input handling +class Win32Window { + public: + struct Point { + unsigned int x; + unsigned int y; + Point(unsigned int x, unsigned int y) : x(x), y(y) {} + }; + + struct Size { + unsigned int width; + unsigned int height; + Size(unsigned int width, unsigned int height) + : width(width), height(height) {} + }; + + Win32Window(); + virtual ~Win32Window(); + + // Creates a win32 window with |title| that is positioned and sized using + // |origin| and |size|. New windows are created on the default monitor. Window + // sizes are specified to the OS in physical pixels, hence to ensure a + // consistent size this function will scale the inputted width and height as + // as appropriate for the default monitor. The window is invisible until + // |Show| is called. Returns true if the window was created successfully. + bool Create(const std::wstring& title, const Point& origin, const Size& size); + + // Show the current window. Returns true if the window was successfully shown. + bool Show(); + + // Release OS resources associated with window. + void Destroy(); + + // Inserts |content| into the window tree. + void SetChildContent(HWND content); + + // Returns the backing Window handle to enable clients to set icon and other + // window properties. Returns nullptr if the window has been destroyed. + HWND GetHandle(); + + // If true, closing this window will quit the application. + void SetQuitOnClose(bool quit_on_close); + + // Return a RECT representing the bounds of the current client area. + RECT GetClientArea(); + + protected: + // Processes and route salient window messages for mouse handling, + // size change and DPI. Delegates handling of these to member overloads that + // inheriting classes can handle. + virtual LRESULT MessageHandler(HWND window, + UINT const message, + WPARAM const wparam, + LPARAM const lparam) noexcept; + + // Called when CreateAndShow is called, allowing subclass window-related + // setup. Subclasses should return false if setup fails. + virtual bool OnCreate(); + + // Called when Destroy is called. + virtual void OnDestroy(); + + private: + friend class WindowClassRegistrar; + + // OS callback called by message pump. Handles the WM_NCCREATE message which + // is passed when the non-client area is being created and enables automatic + // non-client DPI scaling so that the non-client area automatically + // responds to changes in DPI. All other messages are handled by + // MessageHandler. + static LRESULT CALLBACK WndProc(HWND const window, + UINT const message, + WPARAM const wparam, + LPARAM const lparam) noexcept; + + // Retrieves a class instance pointer for |window| + static Win32Window* GetThisFromHandle(HWND const window) noexcept; + + // Update the window frame's theme to match the system theme. + static void UpdateTheme(HWND const window); + + bool quit_on_close_ = false; + + // window handle for top level window. + HWND window_handle_ = nullptr; + + // window handle for hosted content. + HWND child_content_ = nullptr; +}; + +#endif // RUNNER_WIN32_WINDOW_H_ From 73375477bad9b4e7b6bca40ecf59960dfadba197 Mon Sep 17 00:00:00 2001 From: toki Date: Sat, 21 Mar 2026 15:04:37 +0900 Subject: [PATCH 03/18] Add agent-ops rules and documentation files --- CLAUDE.md | 9 + GEMINI.md | 9 + .../rules/_templates/domain-rule-template.md | 28 ++ .../rules/domain/android-native/rules.md | 43 ++ agent-ops/rules/domain/database/rules.md | 48 ++ agent-ops/rules/domain/flutter-app/rules.md | 43 ++ .../rules/domain/push-notification/rules.md | 51 +++ agent-ops/rules/global.md | 23 + agent-ops/skills/_templates/skill-template.md | 30 ++ agent-ops/skills/code-change/SKILL.md | 44 ++ agent-ops/skills/find-domain/SKILL.md | 42 ++ agent-ops/skills/router.md | 43 ++ push-notification-todo.md | 432 ++++++++++++++++++ 13 files changed, 845 insertions(+) create mode 100644 CLAUDE.md create mode 100644 GEMINI.md create mode 100644 agent-ops/rules/_templates/domain-rule-template.md create mode 100644 agent-ops/rules/domain/android-native/rules.md create mode 100644 agent-ops/rules/domain/database/rules.md create mode 100644 agent-ops/rules/domain/flutter-app/rules.md create mode 100644 agent-ops/rules/domain/push-notification/rules.md create mode 100644 agent-ops/rules/global.md create mode 100644 agent-ops/skills/_templates/skill-template.md create mode 100644 agent-ops/skills/code-change/SKILL.md create mode 100644 agent-ops/skills/find-domain/SKILL.md create mode 100644 agent-ops/skills/router.md create mode 100644 push-notification-todo.md diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..94ab725 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,9 @@ +## 응답 언어 +한국어로 응답한다. + +## 기본 원칙 +- 기존 구조를 우선한다. +- 새 파일 생성보다 기존 파일 수정을 우선한다. +- 작업 전 `agent-ops/skills/router.md`를 참조하여 필요한 skill 또는 domain rule만 로드한다. +- 관련 rule / skill만 최소로 로드한다. +- 세부 규칙은 `agent-ops/` 아래 파일을 따른다. diff --git a/GEMINI.md b/GEMINI.md new file mode 100644 index 0000000..94ab725 --- /dev/null +++ b/GEMINI.md @@ -0,0 +1,9 @@ +## 응답 언어 +한국어로 응답한다. + +## 기본 원칙 +- 기존 구조를 우선한다. +- 새 파일 생성보다 기존 파일 수정을 우선한다. +- 작업 전 `agent-ops/skills/router.md`를 참조하여 필요한 skill 또는 domain rule만 로드한다. +- 관련 rule / skill만 최소로 로드한다. +- 세부 규칙은 `agent-ops/` 아래 파일을 따른다. diff --git a/agent-ops/rules/_templates/domain-rule-template.md b/agent-ops/rules/_templates/domain-rule-template.md new file mode 100644 index 0000000..5026f83 --- /dev/null +++ b/agent-ops/rules/_templates/domain-rule-template.md @@ -0,0 +1,28 @@ +--- +name: +description: <도메인 한 줄 설명> +type: +--- + +# + +## 목적 / 책임 +- 이 도메인이 담당하는 핵심 책임 + +## 포함 경로 +- `path/to/included/` + +## 제외 경로 +- `path/to/excluded/` + +## 주요 구성 요소 +- `ComponentName`: 역할 설명 + +## 유지할 패턴 +- 기존 코드에서 반드시 유지해야 할 패턴 + +## 다른 도메인과의 경계 +- ****: 이 도메인은 ~하고, 은 ~한다. + +## 금지 사항 +- 이 도메인에서 하면 안 되는 것 diff --git a/agent-ops/rules/domain/android-native/rules.md b/agent-ops/rules/domain/android-native/rules.md new file mode 100644 index 0000000..2b5918e --- /dev/null +++ b/agent-ops/rules/domain/android-native/rules.md @@ -0,0 +1,43 @@ +--- +name: android-native +description: Android 앱 진입점, 채널 등록, 앱 생명주기 관리 (Supporting Domain) +type: supporting +--- + +# Android Native + +## 목적 / 책임 +- Android 앱 진입점 및 초기화 +- Flutter ↔ Android 채널(EventChannel, MethodChannel) 등록 +- 알림 채널(NotificationChannel) 생성 및 관리 +- 앱 생명주기(포그라운드/백그라운드) 상태 추적 + +## 포함 경로 +- `android/app/src/main/java/com/tokilabs/mattermost/MainActivity.kt` +- `android/app/src/main/java/com/tokilabs/mattermost/MainApplication.kt` +- `android/app/src/main/java/com/tokilabs/mattermost/AppLifecycleTracker.kt` +- `android/app/src/main/AndroidManifest.xml` +- `android/app/build.gradle.kts` + +## 제외 경로 +- `android/.../db/` (database 도메인) +- `android/.../helpers/` (각 담당 도메인) +- `android/.../MattermostFirebaseMessagingService.kt` (push-notification 도메인) + +## 주요 구성 요소 +- `MainActivity`: EventChannel & MethodChannel 등록 +- `MainApplication`: 앱 시작 시 알림 채널 생성, AppLifecycleTracker 등록 +- `AppLifecycleTracker`: 앱 포그라운드/백그라운드 상태 추적 (ActivityLifecycleCallbacks) + +## 유지할 패턴 +- 알림 채널은 `MainApplication.onCreate()`에서 생성 +- EventChannel ID: `com.tokilabs.mattermost/events` +- MethodChannel ID: `com.tokilabs.mattermost/methods` (또는 실제 ID 확인 후 갱신) + +## 다른 도메인과의 경계 +- **push-notification**: 알림 채널 생성은 android-native, 실제 알림 표시는 push-notification +- **flutter-app**: Flutter 레이어와의 통신은 채널을 통해서만, 직접 Flutter 코드 수정 지양 + +## 금지 사항 +- 비즈니스 로직을 MainActivity/MainApplication에 넣지 않는다. +- 채널 ID를 하드코딩할 경우 반드시 상수로 추출한다. diff --git a/agent-ops/rules/domain/database/rules.md b/agent-ops/rules/domain/database/rules.md new file mode 100644 index 0000000..73af9d7 --- /dev/null +++ b/agent-ops/rules/domain/database/rules.md @@ -0,0 +1,48 @@ +--- +name: database +description: Room DB 기반 로컬 데이터 저장 - 서버/사용자/디바이스 토큰 관리 (Supporting Domain) +type: supporting +--- + +# Database + +## 목적 / 책임 +- Room Database를 통한 로컬 데이터 저장 +- Global DB: 디바이스 토큰, 서버 목록 관리 +- Server DB: 서버별 Config, User, System 데이터 관리 +- DAO 쿼리 구현 및 엔티티 정의 + +## 포함 경로 +- `android/app/src/main/java/com/tokilabs/mattermost/db/GlobalDatabase.kt` +- `android/app/src/main/java/com/tokilabs/mattermost/db/MattermostDatabase.kt` +- `android/app/src/main/java/com/tokilabs/mattermost/db/GlobalEntities.kt` +- `android/app/src/main/java/com/tokilabs/mattermost/db/ServerEntities.kt` +- `android/app/src/main/java/com/tokilabs/mattermost/helpers/DatabaseHelper.kt` +- `android/app/src/main/java/com/tokilabs/mattermost/helpers/PushNotificationDataHelper.kt` + +## 제외 경로 +- DB를 사용하는 다른 도메인 파일 (MattermostFirebaseMessagingService 등) + +## 주요 구성 요소 +- `GlobalDatabase`: 디바이스 토큰, 서버 목록 (앱 전역) +- `MattermostDatabase`: 서버별 DB (Config, User, System 테이블) +- `GlobalEntities`: Global DB 엔티티 + DAO 인터페이스 +- `ServerEntities`: Server DB 엔티티 + DAO 인터페이스 +- `DatabaseHelper`: DB 인스턴스 접근 추상화 +- `PushNotificationDataHelper`: **🔴 현재 STUB** - DAO 쿼리 구현 필요 (TODO-1) + +## 유지할 패턴 +- GlobalDatabase / MattermostDatabase 분리 구조 유지 +- DatabaseHelper를 통한 간접 접근 (직접 Room 인스턴스 접근 지양) +- 서버별 DB는 서버 URL을 키로 구분 + +## TODO (push-notification-todo.md 기준) +- **[TODO-1] 🔴**: `PushNotificationDataHelper.kt` Room DAO 쿼리 구현 + +## 다른 도메인과의 경계 +- **push-notification**: 알림 데이터 조회 요청은 push-notification이 하고, 실제 쿼리는 이 도메인 +- **network**: DB와 네트워크는 분리, 캐시 전략은 각 도메인이 독립 관리 + +## 금지 사항 +- DB 스키마 변경 시 반드시 마이그레이션 전략을 함께 고려한다. +- 비즈니스 로직을 DAO에 넣지 않는다. diff --git a/agent-ops/rules/domain/flutter-app/rules.md b/agent-ops/rules/domain/flutter-app/rules.md new file mode 100644 index 0000000..4a81ce7 --- /dev/null +++ b/agent-ops/rules/domain/flutter-app/rules.md @@ -0,0 +1,43 @@ +--- +name: flutter-app +description: Flutter UI 레이어 - 앱 진입점, 화면 라우팅, 로그인, 상태관리 담당 (Core Domain) +type: core +--- + +# Flutter App + +## 목적 / 책임 +- Flutter 앱 진입점 및 위젯 트리 관리 +- 화면 라우팅 (알림 탭 시 채널/DM 이동) +- 로그인 화면 및 인증 토큰 설정 +- Firebase 초기화 + +## 포함 경로 +- `lib/main.dart` +- `lib/` (서비스 파일 제외한 화면/위젯 코드) + +## 제외 경로 +- `lib/services/` (push-notification 도메인) +- `android/` (android-native 도메인) + +## 주요 구성 요소 +- `main.dart`: Firebase 초기화, PushNotificationService 세팅, 앱 위젯 +- `MyApp`: MaterialApp 루트 위젯 +- `MyHomePage`: 현재 기본 홈 화면 (TODO: 실제 라우터로 교체 필요) + +## 유지할 패턴 +- Firebase.initializeApp() 은 앱 시작 시 가장 먼저 호출 +- PushNotificationService 초기화는 Firebase 초기화 후 + +## TODO (push-notification-todo.md 기준) +- **[TODO-2] 🔴**: 앱 라우터 및 화면 네비게이션 구현 +- **[TODO-3] 🔴**: 로그인 화면 및 setAuthToken 연동 +- **[TODO-4] 🟡**: 포그라운드 알림 UI 표시 + +## 다른 도메인과의 경계 +- **push-notification**: 알림 탭 이벤트 수신은 push-notification, 이후 화면 이동은 flutter-app +- **android-native**: Flutter ↔ Android 통신은 EventChannel/MethodChannel 사용, 직접 수정 최소화 + +## 금지 사항 +- Android 네이티브 파일을 이 도메인에서 직접 수정하지 않는다. +- 서비스 로직을 위젯 코드에 섞지 않는다. diff --git a/agent-ops/rules/domain/push-notification/rules.md b/agent-ops/rules/domain/push-notification/rules.md new file mode 100644 index 0000000..480d203 --- /dev/null +++ b/agent-ops/rules/domain/push-notification/rules.md @@ -0,0 +1,51 @@ +--- +name: push-notification +description: FCM 기반 푸시 알림 수신, 표시, ACK 처리 담당 (Core Domain) +type: core +--- + +# Push Notification + +## 목적 / 책임 +- Firebase Cloud Messaging(FCM) 메시지 수신 및 처리 +- 포그라운드/백그라운드/종료 상태별 알림 표시 +- 알림 수신 ACK를 Mattermost 서버로 전달 +- Flutter ↔ Android 간 이벤트 채널 통신 + +## 포함 경로 +- `android/app/src/main/java/com/tokilabs/mattermost/MattermostFirebaseMessagingService.kt` +- `android/app/src/main/java/com/tokilabs/mattermost/helpers/CustomPushNotificationHelper.java` +- `android/app/src/main/java/com/tokilabs/mattermost/helpers/NotificationHelper.kt` +- `android/app/src/main/java/com/tokilabs/mattermost/NotificationReplyBroadcastReceiver.java` +- `android/app/src/main/java/com/tokilabs/mattermost/NotificationDismissService.java` +- `android/app/src/main/java/com/tokilabs/mattermost/ReceiptDelivery.java` +- `lib/services/push_notification_service.dart` +- `lib/services/push_notification_background.dart` + +## 제외 경로 +- `lib/` 내 UI 화면 관련 코드 (flutter-app 도메인) +- `android/.../db/` (database 도메인) +- `android/.../helpers/Network.kt` (network 도메인) + +## 주요 구성 요소 +- `MattermostFirebaseMessagingService`: FCM 메시지 수신 진입점, 포그라운드/백그라운드 분기 +- `CustomPushNotificationHelper`: 알림 UI 빌드, 아바타 이미지 처리 +- `NotificationHelper`: 알림 ID, 그룹 요약 관리 +- `ReceiptDelivery`: `/api/v4/notifications/ack` ACK 전송 +- `PushNotificationService`: Flutter 싱글톤 서비스, EventChannel 수신 +- `push_notification_background.dart`: 백그라운드 FCM 핸들러 + +## 유지할 패턴 +- FCM 수신 → `MattermostFirebaseMessagingService` → `CustomPushNotificationHelper` 흐름 +- EventChannel을 통한 Android → Flutter 이벤트 전달 +- 알림 표시 전 반드시 ACK 전송 (`ReceiptDelivery`) +- Singleton 패턴: `PushNotificationService.instance` + +## 다른 도메인과의 경계 +- **database**: push-notification은 알림 데이터를 읽기만 하고, DB 스키마/쿼리 구현은 database 도메인 +- **network**: HTTP 통신 구현은 network 도메인의 `Network.kt` 사용 +- **flutter-app**: 알림 탭 시 화면 라우팅은 flutter-app 도메인 + +## 금지 사항 +- DB 스키마 변경을 이 도메인에서 직접 하지 않는다. +- 알림 채널 설정은 `MainApplication.kt`(android-native 도메인)에서 한다. diff --git a/agent-ops/rules/global.md b/agent-ops/rules/global.md new file mode 100644 index 0000000..ada5255 --- /dev/null +++ b/agent-ops/rules/global.md @@ -0,0 +1,23 @@ +--- +name: global +description: 전 프로젝트 공통 규칙 +version: 1.0.0 +--- + +# Global Rules + +## 응답 언어 +- 한국어로 응답한다. + +## 기존 구조 우선 +- 새 파일 생성보다 기존 파일 수정을 우선한다. +- 기존 패턴과 컨벤션을 따른다. + +## 코드 변경 원칙 +- 코드 변경 전 관련 domain rule을 확인한다. +- 요청 범위를 넘는 변경을 금지한다. +- 불확실하면 단정하지 말고 후보로 제시한다. + +## 작업 전 확인 +- `agent-ops/skills/router.md`를 먼저 참조하여 필요한 skill 또는 domain rule만 로드한다. +- 관련 rule / skill만 최소로 로드한다. diff --git a/agent-ops/skills/_templates/skill-template.md b/agent-ops/skills/_templates/skill-template.md new file mode 100644 index 0000000..b71bbed --- /dev/null +++ b/agent-ops/skills/_templates/skill-template.md @@ -0,0 +1,30 @@ +--- +name: +description: <스킬 한 줄 설명> +version: 1.0.0 +--- + +# + +## 목적 +이 스킬이 해결하는 문제 + +## 언제 호출할지 +- 이 스킬을 사용해야 하는 상황 + +## 입력 +- 필요한 정보나 컨텍스트 + +## 먼저 확인할 것 +- 실행 전 반드시 확인해야 할 사항 + +## 실행 절차 +1. 첫 번째 단계 +2. 두 번째 단계 +3. ... + +## 출력 형식 +- 결과물의 형태 설명 + +## 금지 사항 +- 이 스킬 실행 중 하면 안 되는 것 diff --git a/agent-ops/skills/code-change/SKILL.md b/agent-ops/skills/code-change/SKILL.md new file mode 100644 index 0000000..15b6ebf --- /dev/null +++ b/agent-ops/skills/code-change/SKILL.md @@ -0,0 +1,44 @@ +--- +name: code-change +description: 코드 변경 작업 전 domain rule 확인 및 변경 범위 검증 +version: 1.0.0 +--- + +# Code Change + +## 목적 +코드 변경 시 도메인 규칙을 위반하지 않도록 변경 전 검증하고, +변경 범위를 요청 범위 내로 제한한다. + +## 언제 호출할지 +- 코드를 수정하기 전 +- 새 파일을 생성하기 전 +- 기존 패턴과 다른 방식으로 구현하려 할 때 + +## 입력 +- 변경하려는 파일 경로 +- 변경 내용 요약 + +## 먼저 확인할 것 +1. `find-domain` 스킬로 도메인 판별 +2. 해당 도메인의 `rules.md` 로드 +3. `push-notification-todo.md` - 현재 TODO 상태 확인 (기존 TODO와 충돌 여부) + +## 실행 절차 +1. 도메인 판별 (find-domain 스킬 참조) +2. 도메인 rules.md의 **금지 사항** 확인 +3. 도메인 rules.md의 **유지할 패턴** 확인 +4. 변경이 요청 범위를 벗어나지 않는지 검토 +5. 변경 후 영향받는 다른 도메인이 있으면 명시 +6. 코드 변경 실행 + +## 출력 형식 +- 변경 대상 파일 +- 적용된 domain rule 요약 +- 변경 내용 +- 영향받는 도메인 (있는 경우) + +## 금지 사항 +- 요청 범위를 넘는 코드를 수정하지 않는다. +- domain rule의 금지 사항을 위반하지 않는다. +- TODO 항목을 무시하고 임의로 구현하지 않는다. diff --git a/agent-ops/skills/find-domain/SKILL.md b/agent-ops/skills/find-domain/SKILL.md new file mode 100644 index 0000000..40102cb --- /dev/null +++ b/agent-ops/skills/find-domain/SKILL.md @@ -0,0 +1,42 @@ +--- +name: find-domain +description: 변경 대상 파일/기능이 어느 도메인에 속하는지 판별 +version: 1.0.0 +--- + +# Find Domain + +## 목적 +변경하려는 파일이나 기능이 어느 도메인에 속하는지 빠르게 판별하여 +올바른 domain rule을 로드하도록 안내한다. + +## 언제 호출할지 +- 어떤 파일을 수정해야 할지는 알지만 도메인이 불명확할 때 +- 여러 도메인에 걸쳐 있는 작업을 시작하기 전 + +## 입력 +- 수정하려는 파일 경로 또는 기능 설명 + +## 먼저 확인할 것 +- `agent-ops/skills/router.md`의 도메인 맵 + +## 실행 절차 +1. 파일 경로를 `router.md` 도메인 맵과 대조한다. +2. 경로가 불명확하면 아래 기준으로 판별한다. + - `lib/services/` → push-notification + - `lib/` (그 외) → flutter-app + - `android/.../db/` → database + - `android/.../helpers/` → 파일별 담당 도메인 확인 + - `android/...MainActivity`, `MainApplication`, `AppLifecycleTracker` → android-native + - `android/.../MattermostFirebaseMessagingService`, `ReceiptDelivery`, `NotificationReplyBroadcastReceiver` → push-notification +3. 해당 도메인의 `rules.md`를 로드한다. +4. 경계가 모호하면 후보 도메인을 제시하고 사용자에게 확인한다. + +## 출력 형식 +- 판별된 도메인 이름 +- 로드할 rules.md 경로 +- 경계가 모호한 경우 후보 목록 + +## 금지 사항 +- 도메인을 단정 짓지 말고 모호하면 후보로 제시한다. +- 관련 없는 domain rule을 추가로 로드하지 않는다. diff --git a/agent-ops/skills/router.md b/agent-ops/skills/router.md new file mode 100644 index 0000000..09f9287 --- /dev/null +++ b/agent-ops/skills/router.md @@ -0,0 +1,43 @@ +--- +name: router +description: 작업 유형별 필요한 skill / domain rule 라우팅 +version: 1.0.0 +--- + +# Skill Router + +작업 시작 전 이 파일을 먼저 읽고, 해당하는 skill 또는 domain rule만 로드한다. + +## 라우팅 축 + +### 도메인 판별이 필요할 때 +→ `agent-ops/skills/find-domain/SKILL.md` + +### 코드 변경 작업 +→ `agent-ops/skills/code-change/SKILL.md` +→ 변경 대상 도메인의 `agent-ops/rules/domain//rules.md` + +### 푸시 알림 관련 작업 +→ `agent-ops/rules/domain/push-notification/rules.md` + +### Flutter UI / 화면 / 라우팅 작업 +→ `agent-ops/rules/domain/flutter-app/rules.md` + +### Room DB / 데이터 저장 관련 작업 +→ `agent-ops/rules/domain/database/rules.md` + +### Android 진입점 / 채널 / 생명주기 작업 +→ `agent-ops/rules/domain/android-native/rules.md` + +### 전체 구조 파악 / 스캐폴드 +→ `agent-ops/rules/global.md` +→ 도메인 전체 rules.md 순차 로드 + +## 도메인 맵 + +| 도메인 | 경로 | 유형 | +|--------|------|------| +| push-notification | `agent-ops/rules/domain/push-notification/rules.md` | Core | +| flutter-app | `agent-ops/rules/domain/flutter-app/rules.md` | Core | +| database | `agent-ops/rules/domain/database/rules.md` | Supporting | +| android-native | `agent-ops/rules/domain/android-native/rules.md` | Supporting | diff --git a/push-notification-todo.md b/push-notification-todo.md new file mode 100644 index 0000000..ba33c61 --- /dev/null +++ b/push-notification-todo.md @@ -0,0 +1,432 @@ +# Push Notification Migration TODO + +## 배경 + +mattermost-mobile(React Native) 프로젝트의 안드로이드 푸시 알림 시스템을 이 Flutter 프로젝트(nomadcode)로 마이그레이션한 작업 기록 및 잔여 TODO. + +**패키지명:** `com.tokilabs.mattermost` (google-services.json과 일치) +**원본 참조:** `/config/workspace/mattermost-mobile` + +--- + +## 전체 아키텍처 요약 + +``` +FCM 서버 + │ + ▼ +MattermostFirebaseMessagingService.kt ← 백그라운드/포그라운드 FCM 수신 + │ + ├─ JWT 서명 검증 (CustomPushNotificationHelper.java) + ├─ ReceiptDelivery.java → /api/v4/notifications/ack POST + ├─ PushNotificationDataHelper.kt → Room DB에서 채널/포스트/유저 조회 + ├─ CustomPushNotificationHelper.java → 시스템 알림 표시 + │ + └─ EventChannel (com.tokilabs.mattermost/notifications) + │ + ▼ + push_notification_service.dart ← Flutter에서 이벤트 수신 + │ + ├─ onNotification stream → UI에서 포그라운드 알림 표시 + └─ onNotificationOpened stream → 알림 탭 시 채널/스레드 이동 + │ + ▼ + main.dart (onNavigateToChannel / onNavigateToThread 콜백) +``` + +--- + +## 완료된 작업 (건드리지 말 것) + +### Android 네이티브 레이어 ✅ +| 파일 | 역할 | +|------|------| +| `MattermostFirebaseMessagingService.kt` | FCM 수신, JWT 검증, Flutter EventSink로 전달 | +| `helpers/CustomPushNotificationHelper.java` | 알림 채널 생성, 시스템 알림 빌드, 아바타 처리 | +| `helpers/NotificationHelper.kt` | 알림 ID 관리, 그룹 알림 요약 | +| `helpers/DatabaseHelper.kt` | GlobalDatabase + MattermostDatabase 접근 | +| `helpers/Network.kt` | OkHttp 기반 HTTP 클라이언트 (토큰 캐시 포함) | +| `helpers/db/GlobalDatabase.kt` | deviceToken, Servers 테이블 | +| `helpers/db/MattermostDatabase.kt` | Config, User, System 테이블 (서버별) | +| `helpers/db/GlobalEntities.kt` | GlobalEntity, ServerEntity, DAO 정의 | +| `helpers/db/ServerEntities.kt` | ConfigEntity, UserEntity, SystemEntity, DAO 정의 | +| `ReceiptDelivery.java` | `/api/v4/notifications/ack` ACK 전송 | +| `NotificationReplyBroadcastReceiver.java` | 알림에서 인라인 답장 처리 | +| `NotificationDismissService.java` | 알림 삭제 처리 | +| `AppLifecycleTracker.kt` | 앱 포그라운드/백그라운드 상태 추적 | +| `MainApplication.kt` | DB 초기화, 알림 채널 생성, 라이프사이클 추적 | +| `MainActivity.kt` | EventChannel + MethodChannel 등록 | + +### Flutter 레이어 ✅ +| 파일 | 역할 | +|------|------| +| `lib/services/push_notification_service.dart` | 싱글톤, 채널 수신, 스트림 노출, MethodChannel 호출 | +| `lib/services/push_notification_background.dart` | 백그라운드 FCM 핸들러 | +| `lib/main.dart` | Firebase 초기화, PushNotificationService 초기화, 네비게이션 콜백 등록 | + +### 리소스 ✅ +- `mipmap-*/ic_launcher.png` + `ic_launcher_round.png` + `ic_launcher_background.png` + `ic_launcher_foreground.png` +- `mipmap-anydpi-v26/ic_launcher.xml` + `ic_launcher_round.xml` (Adaptive Icon) +- `drawable-*/ic_notif_action_reply.png` (답장 버튼 아이콘) +- `mipmap-*/ic_notification.png` (알림 상태바 아이콘) +- `AndroidManifest.xml` — 권한, 서비스, 리시버 등록 완료 + +--- + +## 잔여 TODO + +--- + +### [TODO-1] Android: PushNotificationDataHelper.kt — Room DAO 실제 쿼리 구현 + +**파일:** `android/app/src/main/java/com/tokilabs/mattermost/helpers/PushNotificationDataHelper.kt` + +현재 다음 메서드들이 `Log.i(...) return null` stub 상태: + +```kotlin +private fun fetchTeamIfNeeded(db: MattermostDatabase, serverUrl: String, teamId: String): Bundle? +private fun fetchMyChannel(db: MattermostDatabase, channelId: String): Bundle? +private fun fetchPosts(db: MattermostDatabase, serverUrl: String, channelId: String, isCRTEnabled: Boolean, rootId: String?): Bundle? +private fun fetchThread(db: MattermostDatabase, serverUrl: String, rootId: String): Bundle? +private fun fetchNeededUsers(serverUrl: String, channelId: String): List? +private fun saveToDatabase(db: MattermostDatabase, data: Bundle, teamId: String?, channelId: String?, isCRTEnabled: Boolean) +``` + +**구현 방법:** + +1. `ServerEntities.kt`에 아래 Entity + DAO 추가: + +```kotlin +// Channel 테이블 +@Entity(tableName = "Channel") +data class ChannelEntity( + @PrimaryKey val id: String, + val display_name: String? = null, + val type: String? = null, // O=public, P=private, D=DM, G=Group + val team_id: String? = null, + val delete_at: Long = 0 +) + +@Dao +interface ChannelDao { + @Query("SELECT * FROM Channel WHERE id = :id LIMIT 1") + fun getChannel(id: String): ChannelEntity? + + @Insert(onConflict = OnConflictStrategy.REPLACE) + fun upsert(entity: ChannelEntity) +} + +// Post 테이블 +@Entity(tableName = "Post") +data class PostEntity( + @PrimaryKey val id: String, + val channel_id: String? = null, + val root_id: String? = null, + val user_id: String? = null, + val message: String? = null, + val create_at: Long = 0, + val delete_at: Long = 0 +) + +@Dao +interface PostDao { + @Query("SELECT * FROM Post WHERE channel_id = :channelId ORDER BY create_at DESC LIMIT 20") + fun getPostsForChannel(channelId: String): List + + @Query("SELECT * FROM Post WHERE root_id = :rootId ORDER BY create_at ASC") + fun getThreadPosts(rootId: String): List + + @Insert(onConflict = OnConflictStrategy.REPLACE) + fun upsert(entity: PostEntity) + + @Insert(onConflict = OnConflictStrategy.REPLACE) + fun upsertAll(entities: List) +} + +// Team 테이블 +@Entity(tableName = "Team") +data class TeamEntity( + @PrimaryKey val id: String, + val display_name: String? = null, + val name: String? = null +) + +@Dao +interface TeamDao { + @Query("SELECT * FROM Team WHERE id = :id LIMIT 1") + fun getTeam(id: String): TeamEntity? + + @Insert(onConflict = OnConflictStrategy.REPLACE) + fun upsert(entity: TeamEntity) +} +``` + +2. `MattermostDatabase.kt`에 새 DAO 등록: + +```kotlin +@Database( + entities = [ConfigEntity::class, UserEntity::class, SystemEntity::class, + ChannelEntity::class, PostEntity::class, TeamEntity::class], + version = 2, // 버전 올리기 + exportSchema = false +) +abstract class MattermostDatabase : RoomDatabase() { + abstract fun configDao(): ConfigDao + abstract fun userDao(): UserDao + abstract fun systemDao(): SystemDao + abstract fun channelDao(): ChannelDao + abstract fun postDao(): PostDao + abstract fun teamDao(): TeamDao + ... +} +``` + +3. `PushNotificationDataHelper.kt` stub 메서드 구현: + +```kotlin +private fun fetchMyChannel(db: MattermostDatabase, channelId: String): Bundle? { + val channel = db.channelDao().getChannel(channelId) ?: return null + return Bundle().apply { + putString("id", channel.id) + putString("display_name", channel.display_name) + putString("type", channel.type) + putString("team_id", channel.team_id) + } +} + +private fun fetchPosts(db: MattermostDatabase, serverUrl: String, channelId: String, + isCRTEnabled: Boolean, rootId: String?): Bundle? { + val posts = if (isCRTEnabled && rootId != null) + db.postDao().getThreadPosts(rootId) + else + db.postDao().getPostsForChannel(channelId) + if (posts.isEmpty()) return null + return Bundle().apply { + posts.forEachIndexed { i, post -> + putBundle("post_$i", Bundle().apply { + putString("id", post.id) + putString("message", post.message) + putString("user_id", post.user_id) + putLong("create_at", post.create_at) + }) + } + } +} + +private fun fetchTeamIfNeeded(db: MattermostDatabase, serverUrl: String, teamId: String): Bundle? { + val team = db.teamDao().getTeam(teamId) ?: return null + return Bundle().apply { + putString("id", team.id) + putString("display_name", team.display_name) + } +} + +private fun fetchThread(db: MattermostDatabase, serverUrl: String, rootId: String): Bundle? { + val posts = db.postDao().getThreadPosts(rootId) + if (posts.isEmpty()) return null + return Bundle().apply { putInt("reply_count", posts.size) } +} + +private fun fetchNeededUsers(serverUrl: String, channelId: String): List? { + // Note: 현재 DB에 channel_members 테이블 없음 → 추후 구현 + return null +} + +private fun saveToDatabase(db: MattermostDatabase, data: Bundle, teamId: String?, + channelId: String?, isCRTEnabled: Boolean) { + // Flutter 부팅 전 수신된 알림 데이터 저장 + // System 테이블에 "pendingNotification_{channelId}" 키로 JSON 저장 권장 + channelId ?: return + val json = org.json.JSONObject().apply { + data.keySet()?.forEach { key -> put(key, data.getString(key)) } + } + db.systemDao().upsert(SystemEntity("pendingNotification_$channelId", json.toString())) +} +``` + +**주의:** `db.close()` 호출이 `PushNotificationDataHelper.kt` 99번째 줄 finally 블록에 있음. +Room은 싱글톤이므로 `close()` 호출 시 이후 쿼리가 실패함. **해당 줄 제거 필요.** + +```kotlin +// 제거 대상 (PushNotificationDataHelper.kt:99) +db?.close() // ← 삭제 +``` + +--- + +### [TODO-2] Flutter: 앱 라우터 및 화면 구현 + +**파일:** `lib/main.dart` + +현재 상태: +```dart +pushService.onNavigateToChannel = (serverUrl, channelId) { + // TODO: 채널 화면으로 이동 + print('[Nav] Navigate to channel: $channelId on $serverUrl'); +}; +pushService.onNavigateToThread = (serverUrl, rootId) { + // TODO: 스레드 화면으로 이동 + print('[Nav] Navigate to thread: $rootId on $serverUrl'); +}; +``` + +**구현 방향:** + +1. go_router 또는 Navigator 2.0 기반 라우터 설정 +2. 최소 필요 화면: + - `LoginScreen` — 서버 URL 입력 + 로그인 → `pushService.setAuthToken()` 호출 + - `ChannelScreen` — 채널 메시지 목록 + - `ThreadScreen` — 스레드 메시지 목록 + +3. `main.dart` 콜백 연결 예시: +```dart +pushService.onNavigateToChannel = (serverUrl, channelId) { + _navigatorKey.currentState?.pushNamed( + '/channel', + arguments: {'serverUrl': serverUrl, 'channelId': channelId}, + ); +}; +pushService.onNavigateToThread = (serverUrl, rootId) { + _navigatorKey.currentState?.pushNamed( + '/thread', + arguments: {'serverUrl': serverUrl, 'rootId': rootId}, + ); +}; +``` + +--- + +### [TODO-3] Flutter: 로그인 화면 및 setAuthToken 연결 + +알림 답장(NotificationReplyBroadcastReceiver)과 ACK(ReceiptDelivery)가 서버 인증 토큰을 필요로 함. +Flutter 로그인 성공 시 반드시 아래 호출 필요: + +```dart +// 로그인 성공 후 +await PushNotificationService().setAuthToken(serverUrl, bearerToken); + +// 로그아웃 시 +await PushNotificationService().clearAuthToken(serverUrl); +``` + +이 호출이 없으면 인라인 답장 API와 ACK API 요청이 401로 실패함. + +--- + +### [TODO-4] Flutter: 포그라운드 알림 UI + +앱이 포그라운드 상태일 때 FCM 메시지는 시스템 알림 대신 인앱 UI로 표시해야 함. +`push_notification_service.dart`의 `onNotification` 스트림을 구독해서 구현: + +```dart +// 예시: 메인 화면 또는 글로벌 오버레이에서 +PushNotificationService().onNotification.listen((data) { + final type = data['type']; + if (type == 'message') { + // 상단 배너 또는 SnackBar 표시 + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text(data['message'] ?? '새 메시지')), + ); + } +}); +``` + +--- + +### [TODO-5] Flutter: firebase_options.dart 생성 (선택 사항) + +현재 `main.dart`에서 `Firebase.initializeApp()`을 options 없이 호출 중 (google-services.json 사용). +FlutterFire CLI를 사용하면 더 명시적인 초기화 가능: + +```bash +# 프로젝트 루트에서 +dart pub global activate flutterfire_cli +flutterfire configure --project= +``` + +실행 후 생성된 `lib/firebase_options.dart`를 사용: +```dart +await Firebase.initializeApp(options: DefaultFirebaseOptions.currentPlatform); +``` + +현재도 동작에는 문제 없음. 멀티플랫폼(iOS 추가 시) 필수. + +--- + +## Flutter에서 사용 가능한 인터페이스 (push_notification_service.dart) + +```dart +final service = PushNotificationService(); + +// 초기화 (main에서 1회 호출) +service.init(); + +// 인증 토큰 관리 +await service.setAuthToken(serverUrl, token, identifier: serverId); +await service.clearAuthToken(serverUrl); + +// FCM 디바이스 토큰 조회 +final token = await service.getDeviceToken(); + +// 포그라운드 알림 수신 스트림 +service.onNotification.listen((Map data) { ... }); + +// 알림 탭 이벤트 스트림 +service.onNotificationOpened.listen((NotificationOpenedEvent event) { + // event.serverUrl, event.channelId, event.rootId, event.isCRTEnabled +}); + +// 알림 탭 시 네비게이션 콜백 (main.dart builder에서 등록) +service.onNavigateToChannel = (serverUrl, channelId) { ... }; +service.onNavigateToThread = (serverUrl, rootId) { ... }; +``` + +--- + +## MethodChannel 등록 목록 (MainActivity.kt) + +| 메서드명 | 인자 | 설명 | +|---------|------|------| +| `saveDeviceToken` | `token: String` | FCM 토큰 → Room GlobalDB 저장 | +| `getDeviceToken` | 없음 | Room GlobalDB에서 토큰 반환 | +| `setAuthToken` | `serverUrl`, `token`, `identifier?` | Network 캐시 + Room Servers 저장 | +| `clearAuthToken` | `serverUrl` | Network 캐시에서 토큰 제거 | + +--- + +## EventChannel 이벤트 타입 (com.tokilabs.mattermost/notifications) + +네이티브에서 Flutter로 전달되는 Map의 `type` 필드: + +| type | 설명 | 주요 필드 | +|------|------|---------| +| `message` | 새 메시지 알림 | `server_url`, `channel_id`, `post_id`, `root_id`, `message`, `sender_name`, `is_crt_enabled` | +| `clear` | 채널 읽음 처리 (알림 뱃지 초기화) | `server_url`, `channel_id` | +| `session` | 세션 만료 | `server_url` | +| `token_refresh` | FCM 토큰 갱신 | `token` | +| `opened` | 알림 탭으로 앱 진입 | `server_url`, `channel_id`, `root_id`, `is_crt_enabled`, `userInteraction: true` | + +--- + +## 우선순위 정리 + +| 우선순위 | 항목 | 영향 | +|---------|------|------| +| 🔴 필수 | TODO-2: 라우터/화면 | 알림 탭 후 이동 불가 | +| 🔴 필수 | TODO-3: setAuthToken 연결 | 인라인 답장/ACK 인증 실패 | +| 🟡 권장 | TODO-1: Room DAO 구현 | 알림 표시는 되나 채널명/메시지 미리보기 없음 | +| 🟡 권장 | TODO-1의 db.close() 제거 | 간헐적 DB 접근 오류 | +| 🟢 선택 | TODO-4: 포그라운드 알림 UI | 포그라운드에서 알림 표시 없음 | +| 🟢 선택 | TODO-5: firebase_options.dart | iOS 추가 시 필요 | + +--- + +## 참고: 원본 파일 위치 + +| 역할 | 원본 (mattermost-mobile) | +|------|--------------------------| +| FCM 서비스 | `android/app/src/main/java/com/tokilabs/mattermost/CustomPushNotification.kt` | +| 알림 헬퍼 | `android/app/src/main/java/com/mattermost/helpers/CustomPushNotificationHelper.java` | +| 데이터 헬퍼 | `android/app/src/main/java/com/mattermost/helpers/PushNotificationDataHelper.kt` | +| ACK 전송 | `android/app/src/main/java/com/tokilabs/mattermost/ReceiptDelivery.java` | +| Flutter 서비스 | `app/utils/push_notifications.ts` | From 9a39917a2c208572c39f3fb1cf894d0ebad6a305 Mon Sep 17 00:00:00 2001 From: toki Date: Sat, 21 Mar 2026 15:06:19 +0900 Subject: [PATCH 04/18] Add Firebase push notification support and Android Mattermost integration - Android: MainActivity, AppLifecycleTracker, Firebase Messaging Service - Android: Notification handlers (dismiss, reply, receipt delivery) - Android: Custom push notification helper and database support - Android: Drawable resources for notification actions - Android: Mipmap resources with foreground/background/round icons - iOS: Push notification service support - Flutter: Push notification background and foreground services - Web/Windows: Generated plugin registrant updates - Config: Added google-services.json for Firebase --- android/app/build.gradle.kts | 30 +- android/app/google-services.json | 29 + android/app/src/main/AndroidManifest.xml | 31 +- .../mattermost/AppLifecycleTracker.kt | 35 ++ .../com/tokilabs/mattermost/MainActivity.kt | 102 ++++ .../tokilabs/mattermost/MainApplication.kt | 27 + .../MattermostFirebaseMessagingService.kt | 231 ++++++++ .../NotificationDismissService.java | 40 ++ .../NotificationReplyBroadcastReceiver.java | 152 +++++ .../tokilabs/mattermost/ReceiptDelivery.java | 65 +++ .../mattermost/helpers/BitmapCache.kt | 28 + .../helpers/CustomPushNotificationHelper.java | 525 ++++++++++++++++++ .../mattermost/helpers/DatabaseHelper.kt | 102 ++++ .../tokilabs/mattermost/helpers/Network.kt | 116 ++++ .../mattermost/helpers/NotificationHelper.kt | 93 ++++ .../helpers/PushNotificationDataHelper.kt | 145 +++++ .../mattermost/helpers/db/GlobalDatabase.kt | 38 ++ .../mattermost/helpers/db/GlobalEntities.kt | 53 ++ .../helpers/db/MattermostDatabase.kt | 50 ++ .../mattermost/helpers/db/ServerEntities.kt | 71 +++ .../com/nomadcode/nomadcode/MainActivity.kt | 5 - .../drawable-hdpi/ic_notif_action_reply.png | Bin 0 -> 413 bytes .../drawable-mdpi/ic_notif_action_reply.png | Bin 0 -> 348 bytes .../ic_notif_action_reply.png | Bin 0 -> 413 bytes .../ic_notif_action_reply.png | Bin 0 -> 348 bytes .../ic_notif_action_reply.png | Bin 0 -> 610 bytes .../ic_notif_action_reply.png | Bin 0 -> 833 bytes .../ic_notif_action_reply.png | Bin 0 -> 1261 bytes .../drawable-xhdpi/ic_notif_action_reply.png | Bin 0 -> 610 bytes .../drawable-xxhdpi/ic_notif_action_reply.png | Bin 0 -> 833 bytes .../ic_notif_action_reply.png | Bin 0 -> 1261 bytes .../res/mipmap-anydpi-v26/ic_launcher.xml | 5 + .../mipmap-anydpi-v26/ic_launcher_round.xml | 5 + .../src/main/res/mipmap-hdpi/ic_launcher.png | Bin 544 -> 2805 bytes .../mipmap-hdpi/ic_launcher_background.png | Bin 0 -> 2805 bytes .../mipmap-hdpi/ic_launcher_foreground.png | Bin 0 -> 2805 bytes .../res/mipmap-hdpi/ic_launcher_round.png | Bin 0 -> 2805 bytes .../main/res/mipmap-hdpi/ic_notification.png | Bin 0 -> 638 bytes .../src/main/res/mipmap-mdpi/ic_launcher.png | Bin 442 -> 1759 bytes .../mipmap-mdpi/ic_launcher_background.png | Bin 0 -> 1759 bytes .../mipmap-mdpi/ic_launcher_foreground.png | Bin 0 -> 1759 bytes .../res/mipmap-mdpi/ic_launcher_round.png | Bin 0 -> 1759 bytes .../main/res/mipmap-mdpi/ic_notification.png | Bin 0 -> 481 bytes .../src/main/res/mipmap-xhdpi/ic_launcher.png | Bin 721 -> 3784 bytes .../mipmap-xhdpi/ic_launcher_background.png | Bin 0 -> 3784 bytes .../mipmap-xhdpi/ic_launcher_foreground.png | Bin 0 -> 3784 bytes .../res/mipmap-xhdpi/ic_launcher_round.png | Bin 0 -> 3784 bytes .../main/res/mipmap-xhdpi/ic_notification.png | Bin 0 -> 847 bytes .../main/res/mipmap-xxhdpi/ic_launcher.png | Bin 1031 -> 5914 bytes .../mipmap-xxhdpi/ic_launcher_background.png | Bin 0 -> 5914 bytes .../mipmap-xxhdpi/ic_launcher_foreground.png | Bin 0 -> 5914 bytes .../res/mipmap-xxhdpi/ic_launcher_round.png | Bin 0 -> 5914 bytes .../res/mipmap-xxhdpi/ic_notification.png | Bin 0 -> 1313 bytes .../main/res/mipmap-xxxhdpi/ic_launcher.png | Bin 1443 -> 7878 bytes .../mipmap-xxxhdpi/ic_launcher_background.png | Bin 0 -> 7878 bytes .../mipmap-xxxhdpi/ic_launcher_foreground.png | Bin 0 -> 7878 bytes .../res/mipmap-xxxhdpi/ic_launcher_round.png | Bin 0 -> 7878 bytes .../res/mipmap-xxxhdpi/ic_notification.png | Bin 0 -> 1791 bytes android/build.gradle.kts | 6 + lib/main.dart | 133 ++--- .../push_notification_background.dart | 12 + lib/services/push_notification_service.dart | 233 ++++++++ macos/Flutter/GeneratedPluginRegistrant.swift | 6 + pubspec.lock | 191 ++++++- pubspec.yaml | 7 + .../flutter/generated_plugin_registrant.cc | 3 + windows/flutter/generated_plugins.cmake | 1 + 67 files changed, 2469 insertions(+), 101 deletions(-) create mode 100644 android/app/google-services.json create mode 100644 android/app/src/main/java/com/tokilabs/mattermost/AppLifecycleTracker.kt create mode 100644 android/app/src/main/java/com/tokilabs/mattermost/MainActivity.kt create mode 100644 android/app/src/main/java/com/tokilabs/mattermost/MainApplication.kt create mode 100644 android/app/src/main/java/com/tokilabs/mattermost/MattermostFirebaseMessagingService.kt create mode 100644 android/app/src/main/java/com/tokilabs/mattermost/NotificationDismissService.java create mode 100644 android/app/src/main/java/com/tokilabs/mattermost/NotificationReplyBroadcastReceiver.java create mode 100644 android/app/src/main/java/com/tokilabs/mattermost/ReceiptDelivery.java create mode 100644 android/app/src/main/java/com/tokilabs/mattermost/helpers/BitmapCache.kt create mode 100644 android/app/src/main/java/com/tokilabs/mattermost/helpers/CustomPushNotificationHelper.java create mode 100644 android/app/src/main/java/com/tokilabs/mattermost/helpers/DatabaseHelper.kt create mode 100644 android/app/src/main/java/com/tokilabs/mattermost/helpers/Network.kt create mode 100644 android/app/src/main/java/com/tokilabs/mattermost/helpers/NotificationHelper.kt create mode 100644 android/app/src/main/java/com/tokilabs/mattermost/helpers/PushNotificationDataHelper.kt create mode 100644 android/app/src/main/java/com/tokilabs/mattermost/helpers/db/GlobalDatabase.kt create mode 100644 android/app/src/main/java/com/tokilabs/mattermost/helpers/db/GlobalEntities.kt create mode 100644 android/app/src/main/java/com/tokilabs/mattermost/helpers/db/MattermostDatabase.kt create mode 100644 android/app/src/main/java/com/tokilabs/mattermost/helpers/db/ServerEntities.kt delete mode 100644 android/app/src/main/kotlin/com/nomadcode/nomadcode/MainActivity.kt create mode 100644 android/app/src/main/res/drawable-hdpi/ic_notif_action_reply.png create mode 100644 android/app/src/main/res/drawable-mdpi/ic_notif_action_reply.png create mode 100644 android/app/src/main/res/drawable-night-hdpi/ic_notif_action_reply.png create mode 100644 android/app/src/main/res/drawable-night-mdpi/ic_notif_action_reply.png create mode 100644 android/app/src/main/res/drawable-night-xhdpi/ic_notif_action_reply.png create mode 100644 android/app/src/main/res/drawable-night-xxhdpi/ic_notif_action_reply.png create mode 100644 android/app/src/main/res/drawable-night-xxxhdpi/ic_notif_action_reply.png create mode 100644 android/app/src/main/res/drawable-xhdpi/ic_notif_action_reply.png create mode 100644 android/app/src/main/res/drawable-xxhdpi/ic_notif_action_reply.png create mode 100644 android/app/src/main/res/drawable-xxxhdpi/ic_notif_action_reply.png create mode 100644 android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml create mode 100644 android/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml create mode 100644 android/app/src/main/res/mipmap-hdpi/ic_launcher_background.png create mode 100644 android/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.png create mode 100644 android/app/src/main/res/mipmap-hdpi/ic_launcher_round.png create mode 100644 android/app/src/main/res/mipmap-hdpi/ic_notification.png create mode 100644 android/app/src/main/res/mipmap-mdpi/ic_launcher_background.png create mode 100644 android/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.png create mode 100644 android/app/src/main/res/mipmap-mdpi/ic_launcher_round.png create mode 100644 android/app/src/main/res/mipmap-mdpi/ic_notification.png create mode 100644 android/app/src/main/res/mipmap-xhdpi/ic_launcher_background.png create mode 100644 android/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png create mode 100644 android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png create mode 100644 android/app/src/main/res/mipmap-xhdpi/ic_notification.png create mode 100644 android/app/src/main/res/mipmap-xxhdpi/ic_launcher_background.png create mode 100644 android/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png create mode 100644 android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png create mode 100644 android/app/src/main/res/mipmap-xxhdpi/ic_notification.png create mode 100644 android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_background.png create mode 100644 android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png create mode 100644 android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png create mode 100644 android/app/src/main/res/mipmap-xxxhdpi/ic_notification.png create mode 100644 lib/services/push_notification_background.dart create mode 100644 lib/services/push_notification_service.dart diff --git a/android/app/build.gradle.kts b/android/app/build.gradle.kts index 1ab7a8a..b9a85ca 100644 --- a/android/app/build.gradle.kts +++ b/android/app/build.gradle.kts @@ -1,12 +1,14 @@ plugins { id("com.android.application") id("kotlin-android") + id("kotlin-kapt") // The Flutter Gradle Plugin must be applied after the Android and Kotlin Gradle plugins. id("dev.flutter.flutter-gradle-plugin") + id("com.google.gms.google-services") } android { - namespace = "com.nomadcode.nomadcode" + namespace = "com.tokilabs.mattermost" compileSdk = flutter.compileSdkVersion ndkVersion = flutter.ndkVersion @@ -21,7 +23,7 @@ android { defaultConfig { // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). - applicationId = "com.nomadcode.nomadcode" + applicationId = "com.tokilabs.mattermost" // You can update the following values to match your application needs. // For more information, see: https://flutter.dev/to/review-gradle-config. minSdk = flutter.minSdkVersion @@ -42,3 +44,27 @@ android { flutter { source = "../.." } + +dependencies { + // Firebase BOM (버전 통합 관리) + implementation(platform("com.google.firebase:firebase-bom:33.7.0")) + implementation("com.google.firebase:firebase-messaging") + + // JWT 서명 검증 (mattermost와 동일 버전) + implementation("io.jsonwebtoken:jjwt-api:0.12.5") + runtimeOnly("io.jsonwebtoken:jjwt-impl:0.12.5") + runtimeOnly("io.jsonwebtoken:jjwt-orgjson:0.12.5") { + exclude(group = "org.json", module = "json") + } + + // HTTP 클라이언트 + implementation("com.squareup.okhttp3:okhttp:4.12.0") + + // Kotlin Coroutines + implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.3") + + // Room DB (로컬 캐시용) + implementation("androidx.room:room-runtime:2.6.1") + implementation("androidx.room:room-ktx:2.6.1") + kapt("androidx.room:room-compiler:2.6.1") +} diff --git a/android/app/google-services.json b/android/app/google-services.json new file mode 100644 index 0000000..24bda37 --- /dev/null +++ b/android/app/google-services.json @@ -0,0 +1,29 @@ +{ + "project_info": { + "project_number": "1047648748539", + "project_id": "mattermost-6ac08", + "storage_bucket": "mattermost-6ac08.firebasestorage.app" + }, + "client": [ + { + "client_info": { + "mobilesdk_app_id": "1:1047648748539:android:818bf70bfbb3f9d070415e", + "android_client_info": { + "package_name": "com.tokilabs.mattermost" + } + }, + "oauth_client": [], + "api_key": [ + { + "current_key": "AIzaSyAW6j_oPl9MVcm93qS3JgaEPD5ywp-TzZ0" + } + ], + "services": { + "appinvite_service": { + "other_platform_oauth_client": [] + } + } + } + ], + "configuration_version": "1" +} \ No newline at end of file diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index 99c2c49..4350882 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -1,8 +1,14 @@ + + + + + + android:name=".MainApplication" + android:icon="@mipmap/ic_launcher" + android:roundIcon="@mipmap/ic_launcher_round"> + + + + + + + + + + + + + + + + + + + + diff --git a/android/app/src/main/java/com/tokilabs/mattermost/MainActivity.kt b/android/app/src/main/java/com/tokilabs/mattermost/MainActivity.kt index c3809a8..2b59531 100644 --- a/android/app/src/main/java/com/tokilabs/mattermost/MainActivity.kt +++ b/android/app/src/main/java/com/tokilabs/mattermost/MainActivity.kt @@ -69,6 +69,20 @@ class MainActivity : FlutterActivity() { result.error("INVALID_ARG", "serverUrl or token is null", null) } } + // 서버의 JWT 서명 검증용 공개키 저장 + "setSigningKey" -> { + val serverUrl = call.argument("serverUrl") + val signingKey = call.argument("signingKey") + if (serverUrl != null && signingKey != null) { + getSharedPreferences("mattermost_signing", MODE_PRIVATE) + .edit() + .putString("signing_key_$serverUrl", signingKey) + .apply() + result.success(null) + } else { + result.error("INVALID_ARG", "serverUrl or signingKey is null", null) + } + } // 로그아웃 시 토큰 제거 "clearAuthToken" -> { val serverUrl = call.argument("serverUrl") diff --git a/android/app/src/main/java/com/tokilabs/mattermost/MattermostFirebaseMessagingService.kt b/android/app/src/main/java/com/tokilabs/mattermost/MattermostFirebaseMessagingService.kt index ae27a94..fade1aa 100644 --- a/android/app/src/main/java/com/tokilabs/mattermost/MattermostFirebaseMessagingService.kt +++ b/android/app/src/main/java/com/tokilabs/mattermost/MattermostFirebaseMessagingService.kt @@ -4,11 +4,14 @@ import android.app.PendingIntent import android.content.Context import android.content.Intent import android.os.Bundle +import android.os.Handler +import android.os.Looper import android.util.Log import androidx.core.app.NotificationCompat import com.google.firebase.messaging.FirebaseMessagingService import com.google.firebase.messaging.RemoteMessage import com.tokilabs.mattermost.helpers.CustomPushNotificationHelper +import com.tokilabs.mattermost.helpers.DatabaseHelper import com.tokilabs.mattermost.helpers.Network import com.tokilabs.mattermost.helpers.NotificationHelper import com.tokilabs.mattermost.helpers.PushNotificationDataHelper @@ -43,8 +46,10 @@ class MattermostFirebaseMessagingService : FirebaseMessagingService() { override fun onNewToken(token: String) { super.onNewToken(token) Log.i(TAG, "New FCM token: $token") - // Flutter EventChannel이 준비된 경우 토큰 전달 - eventSink?.success(mapOf("type" to "token_refresh", "token" to token)) + // Flutter EventChannel이 준비된 경우 토큰 전달 (Main 스레드에서 실행) + Handler(Looper.getMainLooper()).post { + eventSink?.success(mapOf("type" to "token_refresh", "token" to token)) + } } /** @@ -54,35 +59,65 @@ class MattermostFirebaseMessagingService : FirebaseMessagingService() { @OptIn(DelicateCoroutinesApi::class) override fun onMessageReceived(remoteMessage: RemoteMessage) { super.onMessageReceived(remoteMessage) + Log.i(TAG, ">>> onMessageReceived ENTER, dataSize=${remoteMessage.data.size}") - val data = remoteMessage.data - if (data.isEmpty()) return + try { + val data = remoteMessage.data + if (data.isEmpty()) return - val bundle = Bundle().apply { - data.forEach { (k, v) -> putString(k, v) } - } - - val type = bundle.getString("type") - val ackId = bundle.getString("ack_id") - val postId = bundle.getString("post_id") - val channelId = bundle.getString("channel_id") - val signature = bundle.getString("signature") - val isIdLoaded = bundle.getString("id_loaded") == "true" - val notificationId = NotificationHelper.getNotificationId(bundle) - val serverUrl = bundle.getString("server_url") - - Log.i(TAG, "onMessageReceived type=$type channelId=$channelId ackId=$ackId") - - GlobalScope.launch { - try { - handlePushNotificationInCoroutine( - serverUrl, type, channelId, ackId, - isIdLoaded, notificationId, postId, signature, bundle - ) - } catch (e: Exception) { - Log.e(TAG, "Error handling notification: ${e.message}") - e.printStackTrace() + val bundle = Bundle().apply { + data.forEach { (k, v) -> putString(k, v) } } + + val type = bundle.getString("type") + val ackId = bundle.getString("ack_id") + val postId = bundle.getString("post_id") + val channelId = bundle.getString("channel_id") + val signature = bundle.getString("signature") + val isIdLoaded = bundle.getString("id_loaded") == "true" + val notificationId = try { + NotificationHelper.getNotificationId(bundle) + } catch (e: Exception) { + Log.e(TAG, "getNotificationId failed: ${e.message}") + System.currentTimeMillis().toInt() + } + + // server_url 해석: 직접 포함 → server_id로 DB 조회 → 단일 서버 fallback + var serverUrl = bundle.getString("server_url") + if (serverUrl == null) { + try { + val dbHelper = DatabaseHelper.getInstance() + val serverId = bundle.getString("server_id") + if (serverId != null && dbHelper != null) { + serverUrl = dbHelper.getServerUrlForIdentifier(serverId) + } + if (serverUrl == null) { + serverUrl = dbHelper?.onlyServerUrl + } + } catch (e: Exception) { + Log.e(TAG, "DB lookup failed: ${e.message}") + } + if (serverUrl != null) { + bundle.putString("server_url", serverUrl) + } + } + + Log.i(TAG, "onMessageReceived type=$type channelId=$channelId ackId=$ackId serverUrl=$serverUrl") + + GlobalScope.launch { + try { + handlePushNotificationInCoroutine( + serverUrl, type, channelId, ackId, + isIdLoaded, notificationId, postId, signature, bundle + ) + } catch (e: Exception) { + Log.e(TAG, "Error handling notification: ${e.message}") + e.printStackTrace() + } + } + } catch (e: Exception) { + Log.e(TAG, ">>> onMessageReceived CRASH: ${e.message}") + e.printStackTrace() } } @@ -174,7 +209,9 @@ class MattermostFirebaseMessagingService : FirebaseMessagingService() { private fun notifyReceivedToFlutter(bundle: Bundle) { val data = bundleToMap(bundle) Log.i(TAG, "Sending notification event to Flutter: type=${data["type"]}") - eventSink?.success(data) + Handler(Looper.getMainLooper()).post { + eventSink?.success(data) + } } /** diff --git a/android/app/src/main/java/com/tokilabs/mattermost/ReceiptDelivery.java b/android/app/src/main/java/com/tokilabs/mattermost/ReceiptDelivery.java index eeb25fd..a5918bc 100644 --- a/android/app/src/main/java/com/tokilabs/mattermost/ReceiptDelivery.java +++ b/android/app/src/main/java/com/tokilabs/mattermost/ReceiptDelivery.java @@ -36,7 +36,7 @@ public class ReceiptDelivery { body.put("post_id", postId); body.put("is_id_loaded", isIdLoaded); - try (Response response = Network.INSTANCE.postSync(serverUrl, "api/v4/notifications/ack", body)) { + try (Response response = Network.INSTANCE.postSync(serverUrl, "api/v4/notifications/ack", body, null)) { String responseBody = Objects.requireNonNull(response.body()).string(); JSONObject jsonResponse = new JSONObject(responseBody); return parseAckResponse(jsonResponse); diff --git a/android/app/src/main/java/com/tokilabs/mattermost/helpers/CustomPushNotificationHelper.java b/android/app/src/main/java/com/tokilabs/mattermost/helpers/CustomPushNotificationHelper.java index fe5749e..e445a87 100644 --- a/android/app/src/main/java/com/tokilabs/mattermost/helpers/CustomPushNotificationHelper.java +++ b/android/app/src/main/java/com/tokilabs/mattermost/helpers/CustomPushNotificationHelper.java @@ -144,20 +144,20 @@ public class CustomPushNotificationHelper { } if (serverUrl == null) { - Log.i(TAG, "No server_url for server_id"); - return false; + Log.i(TAG, "No server_url available, skipping signature check"); + return true; } - DatabaseHelper dbHelper = DatabaseHelper.getInstance(); + DatabaseHelper dbHelper = DatabaseHelper.Companion.getInstance(); if (dbHelper == null) { - Log.i(TAG, "Cannot access the database"); - return false; + Log.i(TAG, "Cannot access the database, skipping signature check"); + return true; } MattermostDatabase db = dbHelper.getDatabaseForServer(context, serverUrl); if (db == null) { - Log.i(TAG, "Cannot access the server database"); - return false; + Log.i(TAG, "Cannot access the server database, skipping signature check"); + return true; } if ("NO_SIGNATURE".equals(signature)) { @@ -201,8 +201,13 @@ public class CustomPushNotificationHelper { String signingKey = db.queryConfigSigningKey(); if (signingKey == null) { - Log.i(TAG, "No signing key"); - return false; + // Room DB에 없으면 SharedPreferences에서 조회 + signingKey = context.getSharedPreferences("mattermost_signing", Context.MODE_PRIVATE) + .getString("signing_key_" + serverUrl, null); + } + if (signingKey == null) { + Log.i(TAG, "No signing key, skipping signature check"); + return true; } try { @@ -220,7 +225,7 @@ public class CustomPushNotificationHelper { Log.i(TAG, "Wrong stored device token format"); return false; } - String deviceToken = tokenParts[1].substring(0, tokenParts[1].length() - 1); + String deviceToken = tokenParts[1]; if (deviceToken.isEmpty()) { Log.i(TAG, "Empty stored device token"); return false; @@ -483,7 +488,7 @@ public class CustomPushNotificationHelper { Request request = new Request.Builder().url(urlOverride).build(); response = client.newCall(request).execute(); } else { - DatabaseHelper dbHelper = DatabaseHelper.getInstance(); + DatabaseHelper dbHelper = DatabaseHelper.Companion.getInstance(); if (dbHelper != null) { MattermostDatabase db = dbHelper.getDatabaseForServer(context, serverUrl); if (db != null) { @@ -499,7 +504,7 @@ public class CustomPushNotificationHelper { } bitmapCache.removeBitmap(userId, serverUrl); - String url = serverUrl.trimEnd('/') + "/api/v4/users/" + userId + "/image"; + String url = serverUrl.replaceAll("/+$", "") + "/api/v4/users/" + userId + "/image"; response = Network.INSTANCE.getSync(serverUrl, "api/v4/users/" + userId + "/image", null); } diff --git a/android/app/src/main/java/com/tokilabs/mattermost/helpers/Network.kt b/android/app/src/main/java/com/tokilabs/mattermost/helpers/Network.kt index d846946..286db8c 100644 --- a/android/app/src/main/java/com/tokilabs/mattermost/helpers/Network.kt +++ b/android/app/src/main/java/com/tokilabs/mattermost/helpers/Network.kt @@ -40,7 +40,7 @@ object Network { /** * 동기 POST 요청 (코루틴 내부에서 사용) */ - fun postSync(serverUrl: String, endpoint: String, body: JSONObject, headers: Map = emptyMap()): Response { + fun postSync(serverUrl: String, endpoint: String, body: JSONObject, headers: Map? = null): Response { val url = buildUrl(serverUrl, endpoint) val requestBody = body.toString().toRequestBody(JSON_MEDIA_TYPE) val requestBuilder = Request.Builder() @@ -49,7 +49,7 @@ object Network { .addHeader("Content-Type", "application/json") tokenCache[serverUrl]?.let { requestBuilder.addHeader("Authorization", "Bearer $it") } - headers.forEach { (k, v) -> requestBuilder.addHeader(k, v) } + headers?.forEach { (k, v) -> requestBuilder.addHeader(k, v) } Log.i(TAG, "POST $url") return client.newCall(requestBuilder.build()).execute() @@ -59,7 +59,7 @@ object Network { * 비동기 POST 요청 (BroadcastReceiver 등에서 사용) */ fun post(serverUrl: String, endpoint: String, body: JSONObject, - headers: Map = emptyMap(), + headers: Map? = null, onSuccess: (JSONObject?) -> Unit, onFailure: (Exception) -> Unit) { val url = buildUrl(serverUrl, endpoint) @@ -70,7 +70,7 @@ object Network { .addHeader("Content-Type", "application/json") tokenCache[serverUrl]?.let { requestBuilder.addHeader("Authorization", "Bearer $it") } - headers.forEach { (k, v) -> requestBuilder.addHeader(k, v) } + headers?.forEach { (k, v) -> requestBuilder.addHeader(k, v) } Log.i(TAG, "POST async $url") client.newCall(requestBuilder.build()).enqueue(object : okhttp3.Callback { diff --git a/android/app/src/main/java/com/tokilabs/mattermost/helpers/db/MattermostDatabase.kt b/android/app/src/main/java/com/tokilabs/mattermost/helpers/db/MattermostDatabase.kt index ac59784..463d9d0 100644 --- a/android/app/src/main/java/com/tokilabs/mattermost/helpers/db/MattermostDatabase.kt +++ b/android/app/src/main/java/com/tokilabs/mattermost/helpers/db/MattermostDatabase.kt @@ -31,7 +31,7 @@ abstract class MattermostDatabase : RoomDatabase() { } companion object { - @Volatile private val instances = mutableMapOf() + @Volatile private var instances = mutableMapOf() fun getInstance(context: Context, serverUrl: String): MattermostDatabase { return instances.getOrPut(serverUrl) { diff --git a/android/build.gradle.kts b/android/build.gradle.kts index 3fc4be3..5813b6d 100644 --- a/android/build.gradle.kts +++ b/android/build.gradle.kts @@ -1,4 +1,8 @@ buildscript { + repositories { + google() + mavenCentral() + } dependencies { classpath("com.google.gms:google-services:4.4.2") } diff --git a/ios/Flutter/Debug.xcconfig b/ios/Flutter/Debug.xcconfig index 592ceee..ec97fc6 100644 --- a/ios/Flutter/Debug.xcconfig +++ b/ios/Flutter/Debug.xcconfig @@ -1 +1,2 @@ +#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig" #include "Generated.xcconfig" diff --git a/ios/Flutter/Release.xcconfig b/ios/Flutter/Release.xcconfig index 592ceee..c4855bf 100644 --- a/ios/Flutter/Release.xcconfig +++ b/ios/Flutter/Release.xcconfig @@ -1 +1,2 @@ +#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig" #include "Generated.xcconfig" diff --git a/ios/Podfile b/ios/Podfile new file mode 100644 index 0000000..620e46e --- /dev/null +++ b/ios/Podfile @@ -0,0 +1,43 @@ +# Uncomment this line to define a global platform for your project +# platform :ios, '13.0' + +# CocoaPods analytics sends network stats synchronously affecting flutter build latency. +ENV['COCOAPODS_DISABLE_STATS'] = 'true' + +project 'Runner', { + 'Debug' => :debug, + 'Profile' => :release, + 'Release' => :release, +} + +def flutter_root + generated_xcode_build_settings_path = File.expand_path(File.join('..', 'Flutter', 'Generated.xcconfig'), __FILE__) + unless File.exist?(generated_xcode_build_settings_path) + raise "#{generated_xcode_build_settings_path} must exist. If you're running pod install manually, make sure flutter pub get is executed first" + end + + File.foreach(generated_xcode_build_settings_path) do |line| + matches = line.match(/FLUTTER_ROOT\=(.*)/) + return matches[1].strip if matches + end + raise "FLUTTER_ROOT not found in #{generated_xcode_build_settings_path}. Try deleting Generated.xcconfig, then run flutter pub get" +end + +require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root) + +flutter_ios_podfile_setup + +target 'Runner' do + use_frameworks! + + flutter_install_all_ios_pods File.dirname(File.realpath(__FILE__)) + target 'RunnerTests' do + inherit! :search_paths + end +end + +post_install do |installer| + installer.pods_project.targets.each do |target| + flutter_additional_ios_build_settings(target) + end +end diff --git a/lib/main.dart b/lib/main.dart index ad3fa12..c5f1992 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,10 +1,13 @@ +import 'dart:async'; import 'package:firebase_core/firebase_core.dart'; import 'package:firebase_messaging/firebase_messaging.dart'; import 'package:flutter/material.dart'; +import 'services/mattermost_auth_service.dart'; import 'services/push_notification_background.dart'; import 'services/push_notification_service.dart'; final _navigatorKey = GlobalKey(); +final _pushService = PushNotificationService(); void main() async { WidgetsFlutterBinding.ensureInitialized(); @@ -13,16 +16,21 @@ void main() async { FirebaseMessaging.onBackgroundMessage(firebaseMessagingBackgroundHandler); - final pushService = PushNotificationService(); - pushService.init(); + _pushService.init(); - runApp(MyApp(pushService: pushService)); + // Mattermost 자동 로그인 + FCM 토큰 서버 등록 + final authService = MattermostAuthService(_pushService); + try { + await authService.autoLoginAndRegister(); + } catch (e) { + print('[Main] Mattermost auto-login failed: $e'); + } + + runApp(const MyApp()); } class MyApp extends StatelessWidget { - final PushNotificationService pushService; - - const MyApp({super.key, required this.pushService}); + const MyApp({super.key}); @override Widget build(BuildContext context) { @@ -34,21 +42,10 @@ class MyApp extends StatelessWidget { ), home: const MyHomePage(title: 'Mattermost'), builder: (context, child) { - // 알림 탭 시 네비게이션 콜백 등록 - pushService.onNavigateToChannel = (serverUrl, channelId) { - // TODO: 채널 화면으로 이동 - // _navigatorKey.currentState?.pushNamed( - // '/channel', - // arguments: {'serverUrl': serverUrl, 'channelId': channelId}, - // ); + _pushService.onNavigateToChannel = (serverUrl, channelId) { print('[Nav] Navigate to channel: $channelId on $serverUrl'); }; - pushService.onNavigateToThread = (serverUrl, rootId) { - // TODO: 스레드 화면으로 이동 - // _navigatorKey.currentState?.pushNamed( - // '/thread', - // arguments: {'serverUrl': serverUrl, 'rootId': rootId}, - // ); + _pushService.onNavigateToThread = (serverUrl, rootId) { print('[Nav] Navigate to thread: $rootId on $serverUrl'); }; return child!; @@ -66,6 +63,37 @@ class MyHomePage extends StatefulWidget { } class _MyHomePageState extends State { + StreamSubscription? _notifSub; + + @override + void initState() { + super.initState(); + _notifSub = _pushService.onNotification.listen((data) { + if (data['type'] != 'message') return; + final message = data['message'] as String? ?? ''; + final channel = data['channel_name'] as String? ?? ''; + final sender = data['sender_name'] as String? ?? ''; + if (!mounted) return; + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text( + sender.isNotEmpty ? '[$channel] $sender: $message' : message, + maxLines: 2, + overflow: TextOverflow.ellipsis, + ), + duration: const Duration(seconds: 4), + behavior: SnackBarBehavior.floating, + ), + ); + }); + } + + @override + void dispose() { + _notifSub?.cancel(); + super.dispose(); + } + @override Widget build(BuildContext context) { return Scaffold( diff --git a/lib/services/mattermost_auth_service.dart b/lib/services/mattermost_auth_service.dart new file mode 100644 index 0000000..19ba2f0 --- /dev/null +++ b/lib/services/mattermost_auth_service.dart @@ -0,0 +1,137 @@ +import 'dart:convert'; +import 'package:flutter/services.dart' show rootBundle; +import 'package:http/http.dart' as http; +import 'push_notification_service.dart'; + +/// Mattermost 자동 로그인 + FCM 디바이스 토큰 서버 등록 서비스. +class MattermostAuthService { + final PushNotificationService _pushService; + + String? _serverUrl; + String? _authToken; + String? _userId; + String? _sessionId; + + String? get serverUrl => _serverUrl; + bool get isLoggedIn => _authToken != null; + + MattermostAuthService(this._pushService); + + /// assets/mattermost_credentials.json 로드 → 로그인 → FCM 등록까지 자동 수행. + Future autoLoginAndRegister() async { + final creds = await _loadCredentials(); + _serverUrl = creds['serverUrl']!; + + print('[MattermostAuth] Logging in to $_serverUrl ...'); + await _login(creds['loginId']!, creds['password']!); + + // FCM 토큰이 이미 있으면 바로 등록, 없으면 콜백으로 대기 + final existingToken = await _pushService.getDeviceToken(); + if (existingToken != null && existingToken.isNotEmpty) { + print('[MattermostAuth] FCM token already available, registering ...'); + await _registerDeviceToken(existingToken); + } else { + print('[MattermostAuth] FCM token not ready yet, waiting for callback ...'); + } + + // FCM 토큰 발급/갱신 시 자동으로 서버에 등록 + _pushService.onDeviceTokenReady = (deviceToken) async { + print('[MattermostAuth] FCM token ready, registering with server ...'); + await _registerDeviceToken(deviceToken); + }; + + print('[MattermostAuth] Auto login & FCM registration complete.'); + } + + Future> _loadCredentials() async { + final jsonStr = + await rootBundle.loadString('assets/mattermost_credentials.json'); + final map = json.decode(jsonStr) as Map; + return { + 'serverUrl': map['serverUrl'] as String, + 'loginId': map['loginId'] as String, + 'password': map['password'] as String, + }; + } + + /// POST /api/v4/users/login → Token 헤더에서 인증 토큰 추출. + Future _login(String loginId, String password) async { + final url = Uri.parse('$_serverUrl/api/v4/users/login'); + final response = await http.post( + url, + headers: {'Content-Type': 'application/json'}, + body: json.encode({'login_id': loginId, 'password': password}), + ); + + if (response.statusCode != 200) { + throw Exception( + '[MattermostAuth] Login failed (${response.statusCode}): ${response.body}', + ); + } + + _authToken = response.headers['token']; + if (_authToken == null) { + throw Exception('[MattermostAuth] Login response missing Token header'); + } + + final body = json.decode(response.body) as Map; + _userId = body['id'] as String?; + _sessionId = body['session_id'] as String? ?? body['id'] as String?; + + print('[MattermostAuth] Login OK - userId=$_userId sessionId=$_sessionId'); + + // 네이티브 측에 인증 토큰 저장 (ACK, 답장 등에 사용) + await _pushService.setAuthToken(_serverUrl!, _authToken!); + + // 서버 config에서 signing key 가져와 네이티브에 저장 + await _fetchAndStoreSigningKey(); + } + + /// GET /api/v4/config/client?format=old → AsymmetricSigningPublicKey를 네이티브에 저장. + Future _fetchAndStoreSigningKey() async { + try { + final url = Uri.parse('$_serverUrl/api/v4/config/client?format=old'); + final response = await http.get( + url, + headers: {'Authorization': 'Bearer $_authToken'}, + ); + if (response.statusCode != 200) { + print('[MattermostAuth] Failed to fetch config (${response.statusCode})'); + return; + } + final config = json.decode(response.body) as Map; + final signingKey = config['AsymmetricSigningPublicKey'] as String?; + if (signingKey != null && signingKey.isNotEmpty) { + await _pushService.setSigningKey(_serverUrl!, signingKey); + print('[MattermostAuth] Signing key stored.'); + } else { + print('[MattermostAuth] No signing key in server config.'); + } + } catch (e) { + print('[MattermostAuth] Failed to fetch signing key: $e'); + } + } + + /// PUT /api/v4/users/sessions/device → device_id 등록. + Future _registerDeviceToken(String deviceToken) async { + final url = + Uri.parse('$_serverUrl/api/v4/users/sessions/device'); + final response = await http.put( + url, + headers: { + 'Content-Type': 'application/json', + 'Authorization': 'Bearer $_authToken', + }, + body: json.encode({'device_id': deviceToken}), + ); + + if (response.statusCode == 200) { + print('[MattermostAuth] FCM device token registered successfully.'); + } else { + print( + '[MattermostAuth] FCM registration failed (${response.statusCode}): ${response.body}', + ); + } + } + +} diff --git a/lib/services/push_notification_service.dart b/lib/services/push_notification_service.dart index 3c97dc5..31a575f 100644 --- a/lib/services/push_notification_service.dart +++ b/lib/services/push_notification_service.dart @@ -57,6 +57,9 @@ class PushNotificationService { void Function(String serverUrl, String channelId)? onNavigateToChannel; void Function(String serverUrl, String rootId)? onNavigateToThread; + // FCM 토큰 발급/갱신 시 서버 등록 콜백 + void Function(String deviceToken)? onDeviceTokenReady; + // ─── 초기화 / 해제 ──────────────────────────────────────────────────── void init() { @@ -193,7 +196,7 @@ class PushNotificationService { await _actionChannel.invokeMethod('setAuthToken', { 'serverUrl': serverUrl, 'token': token, - if (identifier != null) 'identifier': identifier, + 'identifier': identifier, }); print('[PushNotification] Auth token saved for $serverUrl'); } catch (e) { @@ -210,14 +213,28 @@ class PushNotificationService { } } - /// FCM 디바이스 토큰 저장 (네이티브 DB에 android-react-native-v2:{token}: 형식으로 저장). + /// 서버의 JWT 서명 검증용 공개키를 네이티브에 저장. + Future setSigningKey(String serverUrl, String signingKey) async { + try { + await _actionChannel.invokeMethod('setSigningKey', { + 'serverUrl': serverUrl, + 'signingKey': signingKey, + }); + } catch (e) { + print('[PushNotification] Failed to save signing key: $e'); + } + } + + /// FCM 디바이스 토큰 저장 (네이티브 DB에 android_rn-v2:{token} 형식으로 저장). Future _saveDeviceToken(String token) async { try { - const prefix = 'android-react-native-v2'; - final formattedToken = '$prefix:$token:'; + const prefix = 'android_rn'; + final formattedToken = '$prefix-v2:$token'; await _actionChannel .invokeMethod('saveDeviceToken', {'token': formattedToken}); print('[PushNotification] Device token saved'); + // 토큰 준비 완료 → 서버 등록 콜백 호출 + onDeviceTokenReady?.call(formattedToken); } catch (e) { print('[PushNotification] Failed to save device token: $e'); } diff --git a/macos/Flutter/Flutter-Debug.xcconfig b/macos/Flutter/Flutter-Debug.xcconfig index c2efd0b..4b81f9b 100644 --- a/macos/Flutter/Flutter-Debug.xcconfig +++ b/macos/Flutter/Flutter-Debug.xcconfig @@ -1 +1,2 @@ +#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig" #include "ephemeral/Flutter-Generated.xcconfig" diff --git a/macos/Flutter/Flutter-Release.xcconfig b/macos/Flutter/Flutter-Release.xcconfig index c2efd0b..5caa9d1 100644 --- a/macos/Flutter/Flutter-Release.xcconfig +++ b/macos/Flutter/Flutter-Release.xcconfig @@ -1 +1,2 @@ +#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig" #include "ephemeral/Flutter-Generated.xcconfig" diff --git a/macos/Podfile b/macos/Podfile new file mode 100644 index 0000000..ff5ddb3 --- /dev/null +++ b/macos/Podfile @@ -0,0 +1,42 @@ +platform :osx, '10.15' + +# CocoaPods analytics sends network stats synchronously affecting flutter build latency. +ENV['COCOAPODS_DISABLE_STATS'] = 'true' + +project 'Runner', { + 'Debug' => :debug, + 'Profile' => :release, + 'Release' => :release, +} + +def flutter_root + generated_xcode_build_settings_path = File.expand_path(File.join('..', 'Flutter', 'ephemeral', 'Flutter-Generated.xcconfig'), __FILE__) + unless File.exist?(generated_xcode_build_settings_path) + raise "#{generated_xcode_build_settings_path} must exist. If you're running pod install manually, make sure \"flutter pub get\" is executed first" + end + + File.foreach(generated_xcode_build_settings_path) do |line| + matches = line.match(/FLUTTER_ROOT\=(.*)/) + return matches[1].strip if matches + end + raise "FLUTTER_ROOT not found in #{generated_xcode_build_settings_path}. Try deleting Flutter-Generated.xcconfig, then run \"flutter pub get\"" +end + +require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root) + +flutter_macos_podfile_setup + +target 'Runner' do + use_frameworks! + + flutter_install_all_macos_pods File.dirname(File.realpath(__FILE__)) + target 'RunnerTests' do + inherit! :search_paths + end +end + +post_install do |installer| + installer.pods_project.targets.each do |target| + flutter_additional_macos_build_settings(target) + end +end diff --git a/pubspec.lock b/pubspec.lock index 4694982..5046551 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -185,7 +185,7 @@ packages: source: sdk version: "0.0.0" http: - dependency: transitive + dependency: "direct main" description: name: http sha256: "87721a4a50b19c7f1d49001e51409bddc46303966ce89a65af4f4e6004896412" diff --git a/pubspec.yaml b/pubspec.yaml index bdf0af2..ee5a5af 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -39,6 +39,9 @@ dependencies: firebase_core: ^3.13.0 firebase_messaging: ^15.2.5 + # HTTP client + http: ^1.3.0 + # Local notifications (foreground display) flutter_local_notifications: ^18.0.1 @@ -64,10 +67,8 @@ flutter: # the material Icons class. uses-material-design: true - # To add assets to your application, add an assets section, like this: - # assets: - # - images/a_dot_burr.jpeg - # - images/a_dot_ham.jpeg + assets: + - assets/mattermost_credentials.json # An image asset can refer to one or more resolution-specific "variants", see # https://flutter.dev/to/resolution-aware-images From 9b8946be49c32ee085f76e0c0cf1d3af424dc8b4 Mon Sep 17 00:00:00 2001 From: toki Date: Sun, 22 Mar 2026 22:28:39 +0900 Subject: [PATCH 07/18] Update AndroidManifest, notification icons, and main.dart --- android/app/src/main/AndroidManifest.xml | 2 +- .../main/res/mipmap-hdpi/ic_notification.png | Bin 638 -> 1111 bytes .../main/res/mipmap-mdpi/ic_notification.png | Bin 481 -> 958 bytes .../main/res/mipmap-xhdpi/ic_notification.png | Bin 847 -> 1331 bytes .../res/mipmap-xxhdpi/ic_notification.png | Bin 1313 -> 1708 bytes .../res/mipmap-xxxhdpi/ic_notification.png | Bin 1791 -> 2124 bytes lib/main.dart | 6 +++--- 7 files changed, 4 insertions(+), 4 deletions(-) diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index c7be139..67a97f0 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -6,7 +6,7 @@ diff --git a/android/app/src/main/res/mipmap-hdpi/ic_notification.png b/android/app/src/main/res/mipmap-hdpi/ic_notification.png index eb89cd8fb840a27c99c78e62c521bc98070f448a..9e82206f37553d54c3d757c68085d2a201f63553 100644 GIT binary patch literal 1111 zcmeAS@N?(olHy`uVBq!ia0vp^Dj>|k1|%Oc%$NbB7>k44ofy`glX(f`RHR0Druq6Z zXaU(A3@nUN46Gm}Ky1e-4QIPCYA`T^#hDlw+A|qgplbYpGzcUBF-Q-DW?sMuRSgtd zzyw#VvVa-E2C8QGcE_3rNO2Z;L>4nJ@ErkR#;MwT(m>NvGD9LtB7A+UlJj%*5>xV% zQuQiw3qZOUY$~jP%-qzHM1_jnoV;SI3R@+x3M(KRB&@Hb09I0xZL1XF8=&BvUzDm~ zre~mMpk&9TprBw=l#*r@P?Wt5Z@Sn2DRmzV368|&p4rRy77T3YHG80i}s z=>k>g7FXt#Bv$C=6)QswftllyTAW;zSx}OhpQivaH!&%{w8U0P31kr*K;4;J0JkWw z80ssa|MV*o3-k^34D{h912x(J{b=P}l$uzQUlfv`pJNAdT4r8$adKioD#(F0`sga4 zR@&%;+=1jSh^N7#KwsN&0d>Mc(2i@<$s|Pv1||+q7srqY&ZA+z+OB~jdu>%*RWv8P zS5ek9UUHA$Ak|@#{DBhzh7%_?+8^+qIa5O7*D04uHa91KNliuJpK+`2t-YML{G9vq zc~N&af8SfZw)Fen=l3S7Jam&?k=AglU`m~jw#I)}w!ayNLcTXW6RlvrbtvH1Mv;JX z@9r>|)`k4Ln!Ml#OU8>M^Uw7!T$R(5)*)-ZOXXyxkk7)pBM&qrKfk!lrg4ucv%F%? z_tJ-#7qWJgb2EQe>J$6s@KVxX_vgk6g;~ssTXfG{xS@POWnH&WpOb3rv$Xbg6P~Z? zw$fTy#xuk1($6{$b&(B{G0)yiFx&C!*%o1?^7bDAPv$sreY!rO?8w&&#y&1}_C*<% z4U+ecYgpU(|49S|G9p1pW3R2x+$?dGfd+jNQB?io%|>K!5 z^WI{Hv>k=7!$ox_*D%KJQp~;v65eT|&n!{J;J$g)0+s*OkM|e;Rl2bB%E>E2LbXPb zeb)<~)a$*{aPqZNJNBmFi*1>$%g+cCXW^0EO>v?jIjO*rjZ&U{d@;r;B)7qTA> z5^SCBFosWfqNz}x_f{?7piae^cQ%UeSmw0tF&3W`7PzHxhe}c4Vc!)?Wgpdb{#JV~ zxX4+z?U&XweYZI$zlf}Qb3sy|cC!Ba9a5dy57JidH?_b literal 638 zcmV-^0)hRBP)J7>UO*dcz*no5b!USc5&`ooRCCK1n`T?cVi zFrP0s9*Iyej{CxPiW8A)PMu(Zi%q+Prw3-y93xH220uWY*TnqeuWx};+a`jLU&Z)Q zM=S-tZs)vaXd099>?oZtf?x_vb*{Gk z4iajNRF2&_#cC39RPU2{0qD=n4rKJF;T?Y3I2~>Z(yjlX`$-j?73^RxM@WJE|Bj_N z!5EM~8|F~K6ws7zi}RrBC}1uV#WW~vbQz2Q%>|gQg9Vr>ph1<;S(6KxiuurQe6164EFJzpCOq&N#aB8wRq_>O=u<5X=vX`pE-nIRD+5xzcF$@#f@i7EL> zsd^Q;1t47vHWgMtW^QUpqC!P(PF}H9g{=};g%ywu64qBz04piUwpEJo4N!2-FG^J~ z(=*UBP_pAvP*AWbN=dT{a&d!d2l8x{GD=Dctn~HE%ggo3jrH=2()A53EiLs8jP#9+ zbb%^#i!1X=5-W7`ij^UTz|3(;Elw`VEGWs$&r<-Io0ybeT4JlD1hNPYpzh2qfLoMT z4D}VzfBF@P1^R}12KsQ5ff{XqezbBfN=+=uFAB-e&#?nJEi*5>I61K(734r0eRLI2 zD{b^a?m%)E#M59=ps($?fI49zXvekbWRfB978;Kw@%&b6kU(ii@a$Yh!C7=6%*JN_WMz>!18+&2WYIB;}znXv^rI__^SPb|KhTK;%1 zbNkJkw#CoRoqcomgTt=16O1_JE(af$4As!xAiC_IQCm`{lW8S<6PTTQ(;=B{l%9k5#J zbnF7Xp3;aApM=WN<%};3tv5bb;NJG}_s-_(N7L`jJH)?%FHiKyjANe|XOzo-N-v0= zTbV7Z(vf{=+8(by=H92$9JX66?Ta!hwqIFx%k-q;f0pg|OkfSM%m!74{ zm6N9NivY#3v#{O^|9~%t8wf4 zoNKhPzF*!=&lpGF|6>5}x55Qz&j)KOt6Exswjche`;(BcBy0{7VPN}W@ej&%9Oe~j zDle}qRLfy)OMBJZjc@WEHFnFL*y-V36P|A^F_HziR_+19M@p;d{0HUQ9N&dY?9cTE X$Y`VS55!g$00000NkvXXu0mjfU7N~C diff --git a/android/app/src/main/res/mipmap-xhdpi/ic_notification.png b/android/app/src/main/res/mipmap-xhdpi/ic_notification.png index 9b97092c553e7699ffed2d1c6f80606446b54b0e..45d5d8c5db85498d9f8455d37a276b3c2419cb69 100644 GIT binary patch literal 1331 zcmeAS@N?(olHy`uVBq!ia0vp^1|ZDA1|-9oezpTC#^NA%Cx&(BWL^R}6{!)PX}-P; zT0k}j0}G=R11m@g5Zf_I!`W_(8Vt-}aV7?a_DlvAs2V>Y4FU;34AKLknHMlZRRhHq zFu_$DEMP{kfvOqUpKkdBq&N#aB8wRq_>O=u<5X=vX`pE-nIRD+5xzcF$@#f@i7EL> zsd^Q;1t47vHWgMtW^QUpqC!P(PF}H9g{=};g%ywu64qBz04piUwpEJo4N!2-FG^J~ z(=*UBP_pAvP*AWbN=dT{a&d!d2l8x{GD=Dctn~HE%ggo3jrH=2()A53EiLs8jP#9+ zbb%^#i!1X=5-W7`ij^UTz|3(;Elw`VEGWs$&r<-Io0ybeT4JlD1hNPYpzh2qfLoMT z4D}VzfBF@P1^R}12KsQ5ff{XqezbBfN=+=uFAB-e&#?nJEi*5>I61K(734r0eRLI2 zD{b^a?m%)E#M59=ps($?fI49zXvekbWRfBS1Je^v7srqc)~hp4ul5a;InIB5_CyiS zLf1)qx{S;(xT*xFHVD->Obpu2!=sWm#VB!+jT)QLWFH%^B~Rq??o4oZI`R4Q)tixT zi_b-6&)-{D{{Hjo_i@?lzeexB|9$(lTdy~=OFdS0-Vw<$>+xf~wbQT8_|52c@XMBE zZ`b-BI2!ta{SGtpg7qodIrYq}3mhh$KJrBDzx0(7Nj53glbh?$AK^dE@a<4(@8Rw1 z?H*JL$TQEfk}R`0c*U;zkm5RqFS2V^+rAb%7`i0h;o4^}`zF=}-xl$I<0?3*{a@q^ z!`A7#24|-Ens?0Jn$G8_JG;Em+varq1}=rCM!VQ2W$zZqEz?}@yK&mt=?bqu*6cdp zmAOjLg5}^(xk=}3OQ(jfZE>u-S>o=Xeo=Jl<}U&}eCB#v%&&L;E93t=D0H&d);trl z%XQHQjzz!P-=xWCyFF^|f{EN-WkEbzWdSqXZt7mheR^_M$DfRELNot4y_c{Mzoc)> zv@iAY*VTU$XGROmjF(z@ee$iCsGrl${wWXm@|X47)5AV}$vZVxD*a+VJ0m&b^t1HC z4DXU}fB95@a>5?BOlyP5iaO<1xneWr?CxgY@GrH>c+CRzRzttT%w736;_g2)O+3ix z{E6$2kJGx`WhJ++Tnby(UYmPAv0Bhz^5TeZf*R4~JHnrBwEie8W5eWH$Lm#Yu&DRl z>E#VOeU3HrrEH97dZ#WRHTUj|D?wkv{o0M<8LW6#C1+{QPI#*PfAX*1)S0t$r35aY zdMf<(g?SB^Vd*r+AMZ}=YLe?zl(@7~tKqUhwS-c5>0eill3#4cH28hjXS|!He4%&A zx~}z$B*T_ii0C~DO>B75Z3+0;N|bIed{MZ-EfyRDZj>+{kwR|_2Mb+Q6@Q{QpD5M&t;ucLK6VF C+x(RP literal 847 zcmV-V1F-ywP)8cFf?|%oSdnk zh(WhPKJ$cNW=R?nVoRkR7EuT66p{XI?xao@BZX(q`--e*{h%^q7XiPs~?o!FnnF#kr2DoEO+{dqE7hzKuVif{aIuoH>x5*^|%E zAn7bKwQc`sAZSjrt-zm&8K|J+5z5oBC3Fedg>{`WB)#?e)Mz#?uTq|{%}ZY?eAK?Z zEMSd6n%gbu$auYx2<7d}idR&#R Zcn3N5n3kYpg9ZQq002ovPDHLkV1lu>gb@G$ diff --git a/android/app/src/main/res/mipmap-xxhdpi/ic_notification.png b/android/app/src/main/res/mipmap-xxhdpi/ic_notification.png index 6ec8dc5f49e62bb184a24f3f0f4245d6dfb89dfa..c97157d0f3b25fc26e9ad557fbb0e50db88f466a 100644 GIT binary patch literal 1708 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 XIWxkFjS4{?00000NkvXXu0mjfDH20# diff --git a/android/app/src/main/res/mipmap-xxxhdpi/ic_notification.png b/android/app/src/main/res/mipmap-xxxhdpi/ic_notification.png index 3da85e8fb37b2b045fe1a220fa068aac641f1e71..7746a7ef1f037aa3a92b09b4ebc532f0907c0b66 100644 GIT binary patch literal 2124 zcmZ8jd03M97XDarlFF^^s+hPWEefVKCT?ivLZP`ZT8;`T7|JG~Vw+g0kCn?zi&Ud& z){NzZ-ZX9Os!&FynWQxsnHR4uqEe<@D>v>(Iop? zus{F+up%TlAOg%}eJnKqzyA_tZ37d87ZKzSh#w=>;6*@<384{*z$UO>3cw*2z!E(K z3?GQ)w>l8A0f2q!LjjNp=nUrVJ9jK##cXTq=dn=0>sE z3?Kb!bXFQKiNc}k7xcpZKVosD7goQ=|LpbE^p`H^+6MtL|9U|m#EplEHUO|>J|w^| zN&wNs#n!ALt(vYeA3WZwdZ0?NY4r!fvx*_XDO0+C5(9-A($?D{R8$ zc)#j`5r1O>J;Z)BAx5pcc&aS8T9PHHKm@3g`e)i@mg$Vm@=P_c+W?CE;0J_Od(~)bR>37l z>v?}e3VVjPII}}45O~$0erIXQ=UV9a{*`$0hFg|DS_`l5m8-Acd$QEG?9yTWgFgtt z<#lF(WT-aIKv8mdp#sb3s$J$sHXgkrNqISCGucKk$&IcWTeHm(Q0(^oL4G!xQ~>ln zww6#-4dUSQ@v_BmcW&R*7!n^la$@9;SotiKMa1M|fZ^PtrlMD|a z3{thjF#MYRX?Dx4F31QJ7oy-_BL+3@K-LpIyJvm77;iV)L3eq!f2TNV`Ek}jIYECO z83-TTF}tn)?qgk1W(l=1ZsrZV9%=;JCVm&)I99vDPvJoMwOq5&?-k7v6D=SV;?c@D z==~yX%~WiJl9}pSCKh5)Ke=h#q_ra7gC6Ek?DhfLP1jUy!@|Fl9a{N9@Y%Mj0#&l| zVc7sZ*FR@hzQog{1h(zn9BE`<812uUD|3USt2szX`rBGLWvXd;A}hQBJ>*FLtk^pO;cq^xp74u)O~8YY_->!m51f(=5S6%@{3Oy>lU)$}KP#iWmG|l6jj06fV1*Kfy|+qrIBdmup>uhT4^EqwXV9pt{lVZ1S0o#;Mr) zTDuprI<_ju`pn2(^%79svQE8Mnxg#t+axe9NBGRgS0NA5n8W?1MIFyVHFR`Y*Rx&= z0n1hT$EL_UU7qex?ki8qnOW}1p*9O@8_V&u5`H}$I=J-BI$}sG1>>><2|G(z{kf>> z9Qld2uYB#!Y=G2Za5Ymqj-HV8uahm~B)$J}V(o`Z@wRx?o>iYHMuneWxsE(cLK(C* zy-F`O`J|{y;kugs{L8Tun>b%#1I#{Y!uCfJ;Q3WAJviq# z3)$-UV7kJ+)<#}9oTGAs^uG>&R4h4pT9x4MY0$H=SCnrQR;Z#idRE+Evbx<2$N>nq z1^)}ik&Rugg)-oBHX?qRy+`^1Gd$UoZ+}i$f*;IzL;^nH@0A%sB{vroNAed$R**p# zk4wxtVUNw2Vy0pEZ~{!hZjpWu`LvdMFnu`0Bo|%THElfaW7arEdS)Up2)aOQeNfz%&KG@2aWTg_p)MyLM?cs94+pV$)q Z0rUFG4B>ZcjsEul2-y)DAoeF8`43usjoknM literal 1791 zcmV86$_>PF1HMZNi0uaQ220?? zGrxcZu;d-7X9gBMByo4|?JX9-0vu6~0B~miT-b7M%g?r)+G2cZc{du3F6oglET}DG zTW)Nr5vKMx;gL;vQGc^#1{t+2h8|gu6{kuNwaW!PvPuDAnPi^}=YBro&|FI)VVMaf zV!wOUsi&etz+Q+cBrJ1HiNH0UI4FVP8m}+NW5&XIfs!6;xSL!bjKIjZ+8Nx<$@Y_Q zQA)k$7?q$7Lwy0x*Vd53mOd$cF}A+j4achlb&S!6u+Upws<6ddb$s8bEi)F>^TL0p z#=gSCz{PB01|_TLh*Cn}YU#VLXqP02%T}v4egM}7gc{eepuUSW-`|bM3F2~)Z%7HO zj~H?VHXG{D6*)oNDU^Fqh>u>zf;!EXdR~wQkT+<_odm%-T?x)-BB=LlOFit0$~Jsh z?~{sLOvX7$Z}>!5|2Arra(jsPaV=9)2J#zTT3}{IC4j7lRzu(CN^f|J$Yo)5EMpyc zkr8B%VT~R1e2^wCb3;PlIz%DT{4wHU%P#71qCLhNq$3vCA9EI{_amhM7hNj!tr&7&r#4oK{6=k=5(4L0F=UZLT6hw7 zA<{-RhMEchVMKA#`J%CZB(dzDq<}wxyLDkq+$F4fU64;C*%@ zRr{Z=z!K|iMx)U^6$Wn1wxS+mue-5-Lf2lkFXjUK$Fib$K9LSrGz>lj-ha}*Xf0S8 z4QNhZld%# zhukC&R2dey57txq;@43;@`>P_1(r5&#nU_e6VZ?^TCw{!e(Z|{Goj!;mon4rGLl_{ zq4G>g5ANE(qq3YveCDh-0$Ty@NY71ZjQqvuc6vf-XvM6*Q+XQFc{-vDGgV-5!x)3L zVxIX8jkijf_?}m3qKqurg0(saexY}%NBXs`uef`FV7&(moDzekbg!eaqKxgI*OUi~ zJ0Trnul-x9z>MnXmlub(G-yahW6M)Tahd0lpZhAE=5X-^D#z@-_gE8wWV>gkz-%gAAsv~WX-aq(jTcx)s$6(`Q$kzyV zd9|(L63eB!49=3_EVo7)H9~C=%I$g9qPihRN#G1D$4!O}3gxYssnK(hk=~$nF|h}m zL5JS?Wt7{gFy0+P$BJ8O^rSsD|>PQ>m+=q{L9ai4`g6cm_&^N~k4!MO!q z_xvm-B`Yd*%1@hXB<2kf&IgGSmx<0zvlU)bemc|8;hExJCCP-MBBf0svW16mS#hsFnr1aGzvt;(UN#Zg|7Tj_Kmdw$@%bcwnvIJ&)&+C@@y@^H& zJTv?-UR)PLbz!9nKG&4KgLY}GU9(~9~Y8Ncq8 zLv|dT0~XtfapBdYIhKXaYYrVb0gv;Zcey}jg}n;A3|4>k_a|BmrY~DxJLt~6t$)a` hHX^sOes6*<^FMcYFnHw@5LN&H002ovPDHLkV1iziRFVJy diff --git a/lib/main.dart b/lib/main.dart index c5f1992..90c7528 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -35,12 +35,12 @@ class MyApp extends StatelessWidget { @override Widget build(BuildContext context) { return MaterialApp( - title: 'Mattermost', + title: 'NomadCode', navigatorKey: _navigatorKey, theme: ThemeData( colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple), ), - home: const MyHomePage(title: 'Mattermost'), + home: const MyHomePage(title: 'NomadCode'), builder: (context, child) { _pushService.onNavigateToChannel = (serverUrl, channelId) { print('[Nav] Navigate to channel: $channelId on $serverUrl'); @@ -102,7 +102,7 @@ class _MyHomePageState extends State { title: Text(widget.title), ), body: const Center( - child: Text('Mattermost Flutter'), + child: Text('NomadCode'), ), ); } From cd86d34d5a31e6d974fbd423231223cdd04d6f0b Mon Sep 17 00:00:00 2001 From: toki Date: Sun, 22 Mar 2026 22:51:58 +0900 Subject: [PATCH 08/18] feat(android): Implement immersive mode for Flutter app - Enable Flutter view to render behind system UI (status bar, navigation bar) - Set transparent status bar and navigation bar colors in styles.xml - Enable immersive sticky mode in Flutter main.dart - MainActivity.kt: Set decor fits system windows to false for full-screen layout --- .../com/tokilabs/mattermost/MainActivity.kt | 3 +++ .../app/src/main/res/values-night/styles.xml | 4 ++++ android/app/src/main/res/values/styles.xml | 4 ++++ lib/main.dart | 19 +++++++++++++++---- 4 files changed, 26 insertions(+), 4 deletions(-) diff --git a/android/app/src/main/java/com/tokilabs/mattermost/MainActivity.kt b/android/app/src/main/java/com/tokilabs/mattermost/MainActivity.kt index 2b59531..4270311 100644 --- a/android/app/src/main/java/com/tokilabs/mattermost/MainActivity.kt +++ b/android/app/src/main/java/com/tokilabs/mattermost/MainActivity.kt @@ -1,6 +1,7 @@ package com.tokilabs.mattermost import android.os.Bundle +import androidx.core.view.WindowCompat import com.tokilabs.mattermost.helpers.DatabaseHelper import com.tokilabs.mattermost.helpers.Network import io.flutter.embedding.android.FlutterActivity @@ -100,6 +101,8 @@ class MainActivity : FlutterActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) + WindowCompat.setDecorFitsSystemWindows(window, false) + // 알림 탭으로 앱 진입 시 Intent 데이터를 Flutter로 전달 intent?.extras?.let { extras -> val data = mutableMapOf() diff --git a/android/app/src/main/res/values-night/styles.xml b/android/app/src/main/res/values-night/styles.xml index 06952be..1922e52 100644 --- a/android/app/src/main/res/values-night/styles.xml +++ b/android/app/src/main/res/values-night/styles.xml @@ -5,6 +5,8 @@ @drawable/launch_background + @android:color/transparent + @android:color/transparent diff --git a/android/app/src/main/res/values/styles.xml b/android/app/src/main/res/values/styles.xml index cb1ef88..7252bfb 100644 --- a/android/app/src/main/res/values/styles.xml +++ b/android/app/src/main/res/values/styles.xml @@ -5,6 +5,8 @@ @drawable/launch_background + @android:color/transparent + @android:color/transparent diff --git a/lib/main.dart b/lib/main.dart index 90c7528..283acbb 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,6 +1,7 @@ import 'dart:async'; import 'package:firebase_core/firebase_core.dart'; import 'package:firebase_messaging/firebase_messaging.dart'; +import 'package:flutter/services.dart'; import 'package:flutter/material.dart'; import 'services/mattermost_auth_service.dart'; import 'services/push_notification_background.dart'; @@ -9,9 +10,20 @@ import 'services/push_notification_service.dart'; final _navigatorKey = GlobalKey(); final _pushService = PushNotificationService(); +Future _applyFullscreenMode() async { + await SystemChrome.setEnabledSystemUIMode(SystemUiMode.immersiveSticky); + SystemChrome.setSystemUIOverlayStyle(const SystemUiOverlayStyle( + statusBarColor: Colors.transparent, + systemNavigationBarColor: Colors.transparent, + systemNavigationBarDividerColor: Colors.transparent, + )); +} + void main() async { WidgetsFlutterBinding.ensureInitialized(); + await _applyFullscreenMode(); + await Firebase.initializeApp(); FirebaseMessaging.onBackgroundMessage(firebaseMessagingBackgroundHandler); @@ -68,6 +80,9 @@ class _MyHomePageState extends State { @override void initState() { super.initState(); + WidgetsBinding.instance.addPostFrameCallback((_) { + _applyFullscreenMode(); + }); _notifSub = _pushService.onNotification.listen((data) { if (data['type'] != 'message') return; final message = data['message'] as String? ?? ''; @@ -97,10 +112,6 @@ class _MyHomePageState extends State { @override Widget build(BuildContext context) { return Scaffold( - appBar: AppBar( - backgroundColor: Theme.of(context).colorScheme.inversePrimary, - title: Text(widget.title), - ), body: const Center( child: Text('NomadCode'), ), From 9c73c41847d48f42b7cd30a6d3525659e06d9f27 Mon Sep 17 00:00:00 2001 From: toki Date: Mon, 23 Mar 2026 18:06:32 +0900 Subject: [PATCH 09/18] chore(vscode): add Flutter device launch configs --- .vscode/launch.json | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/.vscode/launch.json b/.vscode/launch.json index 3ced9ab..fdc156a 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -2,11 +2,18 @@ "version": "0.2.0", "configurations": [ { - "name": "Flutter (Android)", + "name": "Flutter (Pixel 7 Pro)", + "request": "launch", + "type": "dart", + "program": "lib/main.dart", + "deviceId": "emulator-5554" + }, + { + "name": "Flutter (Galaxy Z Fold6)", "request": "launch", "type": "dart", "program": "lib/main.dart", "deviceId": "R3CX80DTM6W" } ] -} +} \ No newline at end of file From 1eff82483b58a06201b031045bbe079ab9578c8d Mon Sep 17 00:00:00 2001 From: toki Date: Tue, 19 May 2026 09:31:32 +0900 Subject: [PATCH 10/18] Rename project to nomadcode-app --- .idea/modules.xml | 4 ++-- README.md | 4 ++-- android/app/src/main/AndroidManifest.xml | 2 +- ...e_android.iml => nomadcode_app_android.iml} | 0 ios/Runner/Info.plist | 4 ++-- lib/main.dart | 6 +++--- lib/services/mattermost_auth_service.dart | 15 ++++++++++++--- linux/CMakeLists.txt | 2 +- linux/runner/my_application.cc | 4 ++-- macos/Runner.xcodeproj/project.pbxproj | 12 ++++++------ .../xcshareddata/xcschemes/Runner.xcscheme | 8 ++++---- macos/Runner/Configs/AppInfo.xcconfig | 2 +- nomadcode.iml => nomadcode_app.iml | 0 pubspec.yaml | 8 +++----- push-notification-todo.md | 2 +- test/widget_test.dart | 18 +++--------------- web/index.html | 6 +++--- web/manifest.json | 6 +++--- windows/CMakeLists.txt | 4 ++-- windows/runner/Runner.rc | 8 ++++---- windows/runner/main.cpp | 2 +- 21 files changed, 56 insertions(+), 61 deletions(-) rename android/{nomadcode_android.iml => nomadcode_app_android.iml} (100%) rename nomadcode.iml => nomadcode_app.iml (100%) diff --git a/.idea/modules.xml b/.idea/modules.xml index b0a1eb4..c241992 100644 --- a/.idea/modules.xml +++ b/.idea/modules.xml @@ -2,8 +2,8 @@ - - + + diff --git a/README.md b/README.md index 4a680c8..0f621ef 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ -# nomadcode +# nomadcode-app -A new Flutter project. +nomadcode-app Flutter project. ## Getting Started diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index 67a97f0..b4856ef 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -6,7 +6,7 @@ diff --git a/android/nomadcode_android.iml b/android/nomadcode_app_android.iml similarity index 100% rename from android/nomadcode_android.iml rename to android/nomadcode_app_android.iml diff --git a/ios/Runner/Info.plist b/ios/Runner/Info.plist index 93cd453..0ad5321 100644 --- a/ios/Runner/Info.plist +++ b/ios/Runner/Info.plist @@ -7,7 +7,7 @@ CFBundleDevelopmentRegion $(DEVELOPMENT_LANGUAGE) CFBundleDisplayName - Nomadcode + nomadcode-app CFBundleExecutable $(EXECUTABLE_NAME) CFBundleIdentifier @@ -15,7 +15,7 @@ CFBundleInfoDictionaryVersion 6.0 CFBundleName - nomadcode + nomadcode-app CFBundlePackageType APPL CFBundleShortVersionString diff --git a/lib/main.dart b/lib/main.dart index 283acbb..ded0e85 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -47,12 +47,12 @@ class MyApp extends StatelessWidget { @override Widget build(BuildContext context) { return MaterialApp( - title: 'NomadCode', + title: 'nomadcode-app', navigatorKey: _navigatorKey, theme: ThemeData( colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple), ), - home: const MyHomePage(title: 'NomadCode'), + home: const MyHomePage(title: 'nomadcode-app'), builder: (context, child) { _pushService.onNavigateToChannel = (serverUrl, channelId) { print('[Nav] Navigate to channel: $channelId on $serverUrl'); @@ -113,7 +113,7 @@ class _MyHomePageState extends State { Widget build(BuildContext context) { return Scaffold( body: const Center( - child: Text('NomadCode'), + child: Text('nomadcode-app'), ), ); } diff --git a/lib/services/mattermost_auth_service.dart b/lib/services/mattermost_auth_service.dart index 19ba2f0..46ad383 100644 --- a/lib/services/mattermost_auth_service.dart +++ b/lib/services/mattermost_auth_service.dart @@ -1,4 +1,5 @@ import 'dart:convert'; +import 'package:flutter/foundation.dart' show FlutterError; import 'package:flutter/services.dart' show rootBundle; import 'package:http/http.dart' as http; import 'push_notification_service.dart'; @@ -20,6 +21,10 @@ class MattermostAuthService { /// assets/mattermost_credentials.json 로드 → 로그인 → FCM 등록까지 자동 수행. Future autoLoginAndRegister() async { final creds = await _loadCredentials(); + if (creds == null) { + print('[MattermostAuth] Credentials asset not found, skipping auto login.'); + return; + } _serverUrl = creds['serverUrl']!; print('[MattermostAuth] Logging in to $_serverUrl ...'); @@ -43,9 +48,13 @@ class MattermostAuthService { print('[MattermostAuth] Auto login & FCM registration complete.'); } - Future> _loadCredentials() async { - final jsonStr = - await rootBundle.loadString('assets/mattermost_credentials.json'); + Future?> _loadCredentials() async { + final String jsonStr; + try { + jsonStr = await rootBundle.loadString('assets/mattermost_credentials.json'); + } on FlutterError { + return null; + } final map = json.decode(jsonStr) as Map; return { 'serverUrl': map['serverUrl'] as String, diff --git a/linux/CMakeLists.txt b/linux/CMakeLists.txt index ea9f018..d980f5e 100644 --- a/linux/CMakeLists.txt +++ b/linux/CMakeLists.txt @@ -4,7 +4,7 @@ project(runner LANGUAGES CXX) # The name of the executable created for the application. Change this to change # the on-disk name of your application. -set(BINARY_NAME "nomadcode") +set(BINARY_NAME "nomadcode-app") # The unique GTK application identifier for this application. See: # https://wiki.gnome.org/HowDoI/ChooseApplicationID set(APPLICATION_ID "com.nomadcode.nomadcode") diff --git a/linux/runner/my_application.cc b/linux/runner/my_application.cc index c2891c0..4c6fb83 100644 --- a/linux/runner/my_application.cc +++ b/linux/runner/my_application.cc @@ -45,11 +45,11 @@ static void my_application_activate(GApplication* application) { if (use_header_bar) { GtkHeaderBar* header_bar = GTK_HEADER_BAR(gtk_header_bar_new()); gtk_widget_show(GTK_WIDGET(header_bar)); - gtk_header_bar_set_title(header_bar, "nomadcode"); + gtk_header_bar_set_title(header_bar, "nomadcode-app"); gtk_header_bar_set_show_close_button(header_bar, TRUE); gtk_window_set_titlebar(window, GTK_WIDGET(header_bar)); } else { - gtk_window_set_title(window, "nomadcode"); + gtk_window_set_title(window, "nomadcode-app"); } gtk_window_set_default_size(window, 1280, 720); diff --git a/macos/Runner.xcodeproj/project.pbxproj b/macos/Runner.xcodeproj/project.pbxproj index b29ac57..8c46b9c 100644 --- a/macos/Runner.xcodeproj/project.pbxproj +++ b/macos/Runner.xcodeproj/project.pbxproj @@ -64,7 +64,7 @@ 331C80D7294CF71000263BE5 /* RunnerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RunnerTests.swift; sourceTree = ""; }; 333000ED22D3DE5D00554162 /* Warnings.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Warnings.xcconfig; sourceTree = ""; }; 335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GeneratedPluginRegistrant.swift; sourceTree = ""; }; - 33CC10ED2044A3C60003C045 /* nomadcode.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "nomadcode.app"; sourceTree = BUILT_PRODUCTS_DIR; }; + 33CC10ED2044A3C60003C045 /* nomadcode-app.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "nomadcode-app.app"; sourceTree = BUILT_PRODUCTS_DIR; }; 33CC10F02044A3C60003C045 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 33CC10F22044A3C60003C045 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; name = Assets.xcassets; path = Runner/Assets.xcassets; sourceTree = ""; }; 33CC10F52044A3C60003C045 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = Base; path = Base.lproj/MainMenu.xib; sourceTree = ""; }; @@ -131,7 +131,7 @@ 33CC10EE2044A3C60003C045 /* Products */ = { isa = PBXGroup; children = ( - 33CC10ED2044A3C60003C045 /* nomadcode.app */, + 33CC10ED2044A3C60003C045 /* nomadcode-app.app */, 331C80D5294CF71000263BE5 /* RunnerTests.xctest */, ); name = Products; @@ -217,7 +217,7 @@ ); name = Runner; productName = Runner; - productReference = 33CC10ED2044A3C60003C045 /* nomadcode.app */; + productReference = 33CC10ED2044A3C60003C045 /* nomadcode-app.app */; productType = "com.apple.product-type.application"; }; /* End PBXNativeTarget section */ @@ -388,7 +388,7 @@ PRODUCT_BUNDLE_IDENTIFIER = com.nomadcode.nomadcode.RunnerTests; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_VERSION = 5.0; - TEST_HOST = "$(BUILT_PRODUCTS_DIR)/nomadcode.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/nomadcode"; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/nomadcode-app.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/nomadcode-app"; }; name = Debug; }; @@ -402,7 +402,7 @@ PRODUCT_BUNDLE_IDENTIFIER = com.nomadcode.nomadcode.RunnerTests; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_VERSION = 5.0; - TEST_HOST = "$(BUILT_PRODUCTS_DIR)/nomadcode.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/nomadcode"; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/nomadcode-app.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/nomadcode-app"; }; name = Release; }; @@ -416,7 +416,7 @@ PRODUCT_BUNDLE_IDENTIFIER = com.nomadcode.nomadcode.RunnerTests; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_VERSION = 5.0; - TEST_HOST = "$(BUILT_PRODUCTS_DIR)/nomadcode.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/nomadcode"; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/nomadcode-app.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/nomadcode-app"; }; name = Profile; }; diff --git a/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme index 566d7ae..838b165 100644 --- a/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme +++ b/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -15,7 +15,7 @@ @@ -31,7 +31,7 @@ @@ -66,7 +66,7 @@ @@ -83,7 +83,7 @@ diff --git a/macos/Runner/Configs/AppInfo.xcconfig b/macos/Runner/Configs/AppInfo.xcconfig index 30f214a..ca5b29b 100644 --- a/macos/Runner/Configs/AppInfo.xcconfig +++ b/macos/Runner/Configs/AppInfo.xcconfig @@ -5,7 +5,7 @@ // 'flutter create' template. // The application's name. By default this is also the title of the Flutter window. -PRODUCT_NAME = nomadcode +PRODUCT_NAME = nomadcode-app // The application's bundle identifier PRODUCT_BUNDLE_IDENTIFIER = com.nomadcode.nomadcode diff --git a/nomadcode.iml b/nomadcode_app.iml similarity index 100% rename from nomadcode.iml rename to nomadcode_app.iml diff --git a/pubspec.yaml b/pubspec.yaml index ee5a5af..ac7c1b2 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,5 +1,6 @@ -name: nomadcode -description: "A new Flutter project." +name: nomadcode_app +description: "nomadcode-app Flutter project." +repository: https://git.toki-labs.com/toki/nomadcode-app.git # The following line prevents the package from being accidentally published to # pub.dev using `flutter pub publish`. This is preferred for private packages. publish_to: 'none' # Remove this line if you wish to publish to pub.dev @@ -67,9 +68,6 @@ flutter: # the material Icons class. uses-material-design: true - assets: - - assets/mattermost_credentials.json - # An image asset can refer to one or more resolution-specific "variants", see # https://flutter.dev/to/resolution-aware-images diff --git a/push-notification-todo.md b/push-notification-todo.md index ba33c61..e66e370 100644 --- a/push-notification-todo.md +++ b/push-notification-todo.md @@ -2,7 +2,7 @@ ## 배경 -mattermost-mobile(React Native) 프로젝트의 안드로이드 푸시 알림 시스템을 이 Flutter 프로젝트(nomadcode)로 마이그레이션한 작업 기록 및 잔여 TODO. +mattermost-mobile(React Native) 프로젝트의 안드로이드 푸시 알림 시스템을 이 Flutter 프로젝트(nomadcode-app)로 마이그레이션한 작업 기록 및 잔여 TODO. **패키지명:** `com.tokilabs.mattermost` (google-services.json과 일치) **원본 참조:** `/config/workspace/mattermost-mobile` diff --git a/test/widget_test.dart b/test/widget_test.dart index d3998ee..84cea8f 100644 --- a/test/widget_test.dart +++ b/test/widget_test.dart @@ -5,26 +5,14 @@ // gestures. You can also use WidgetTester to find child widgets in the widget // tree, read text, and verify that the values of widget properties are correct. -import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; -import 'package:nomadcode/main.dart'; +import 'package:nomadcode_app/main.dart'; void main() { - testWidgets('Counter increments smoke test', (WidgetTester tester) async { - // Build our app and trigger a frame. + testWidgets('App renders project name', (WidgetTester tester) async { await tester.pumpWidget(const MyApp()); - // Verify that our counter starts at 0. - expect(find.text('0'), findsOneWidget); - expect(find.text('1'), findsNothing); - - // Tap the '+' icon and trigger a frame. - await tester.tap(find.byIcon(Icons.add)); - await tester.pump(); - - // Verify that our counter has incremented. - expect(find.text('0'), findsNothing); - expect(find.text('1'), findsOneWidget); + expect(find.text('nomadcode-app'), findsOneWidget); }); } diff --git a/web/index.html b/web/index.html index c562ebf..34e35ac 100644 --- a/web/index.html +++ b/web/index.html @@ -18,18 +18,18 @@ - + - + - nomadcode + nomadcode-app diff --git a/web/manifest.json b/web/manifest.json index c76897c..2b35044 100644 --- a/web/manifest.json +++ b/web/manifest.json @@ -1,11 +1,11 @@ { - "name": "nomadcode", - "short_name": "nomadcode", + "name": "nomadcode-app", + "short_name": "nomadcode-app", "start_url": ".", "display": "standalone", "background_color": "#0175C2", "theme_color": "#0175C2", - "description": "A new Flutter project.", + "description": "nomadcode-app Flutter project.", "orientation": "portrait-primary", "prefer_related_applications": false, "icons": [ diff --git a/windows/CMakeLists.txt b/windows/CMakeLists.txt index 7f79357..e4d035f 100644 --- a/windows/CMakeLists.txt +++ b/windows/CMakeLists.txt @@ -1,10 +1,10 @@ # Project-level configuration. cmake_minimum_required(VERSION 3.14) -project(nomadcode LANGUAGES CXX) +project(nomadcode_app LANGUAGES CXX) # The name of the executable created for the application. Change this to change # the on-disk name of your application. -set(BINARY_NAME "nomadcode") +set(BINARY_NAME "nomadcode-app") # Explicitly opt in to modern CMake behaviors to avoid warnings with recent # versions of CMake. diff --git a/windows/runner/Runner.rc b/windows/runner/Runner.rc index 7eefe46..7cd84b4 100644 --- a/windows/runner/Runner.rc +++ b/windows/runner/Runner.rc @@ -90,12 +90,12 @@ BEGIN BLOCK "040904e4" BEGIN VALUE "CompanyName", "com.nomadcode" "\0" - VALUE "FileDescription", "nomadcode" "\0" + VALUE "FileDescription", "nomadcode-app" "\0" VALUE "FileVersion", VERSION_AS_STRING "\0" - VALUE "InternalName", "nomadcode" "\0" + VALUE "InternalName", "nomadcode-app" "\0" VALUE "LegalCopyright", "Copyright (C) 2026 com.nomadcode. All rights reserved." "\0" - VALUE "OriginalFilename", "nomadcode.exe" "\0" - VALUE "ProductName", "nomadcode" "\0" + VALUE "OriginalFilename", "nomadcode-app.exe" "\0" + VALUE "ProductName", "nomadcode-app" "\0" VALUE "ProductVersion", VERSION_AS_STRING "\0" END END diff --git a/windows/runner/main.cpp b/windows/runner/main.cpp index e62d737..9f82844 100644 --- a/windows/runner/main.cpp +++ b/windows/runner/main.cpp @@ -27,7 +27,7 @@ int APIENTRY wWinMain(_In_ HINSTANCE instance, _In_opt_ HINSTANCE prev, FlutterWindow window(project); Win32Window::Point origin(10, 10); Win32Window::Size size(1280, 720); - if (!window.Create(L"nomadcode", origin, size)) { + if (!window.Create(L"nomadcode-app", origin, size)) { return EXIT_FAILURE; } window.SetQuitOnClose(true); From 30efee61dfc82c6e07e10e7f5418701fcf9bfe69 Mon Sep 17 00:00:00 2001 From: toki Date: Tue, 19 May 2026 10:27:14 +0900 Subject: [PATCH 11/18] chore: sync agent-ops --- .clinerules | 16 + .cursorrules | 16 + AGENTS.md | 23 +- CLAUDE.md | 23 +- GEMINI.md | 23 +- agent-ops/.version | 1 + agent-ops/bin/bump-version.sh | 28 ++ agent-ops/bin/entry-files.sh | 24 ++ agent-ops/bin/init-agent-ops.sh | 104 ++++++ agent-ops/bin/sync.sh | 182 ++++++++++ .../common/_templates/domain-rule-template.md | 29 ++ .../common/_templates/skill-template.md | 57 +++ agent-ops/skills/common/code-review/SKILL.md | 324 ++++++++++++++++++ .../common/code-review/agents/openai.yaml | 4 + .../templates/complete-log-template.md | 31 ++ agent-ops/skills/common/commit-push/SKILL.md | 124 +++++++ .../skills/common/create-domain-rule/SKILL.md | 113 ++++++ agent-ops/skills/common/create-skill/SKILL.md | 95 +++++ .../skills/common/init-agent-ops/SKILL.md | 243 +++++++++++++ agent-ops/skills/common/plan/SKILL.md | 317 +++++++++++++++++ .../skills/common/plan/agents/openai.yaml | 4 + agent-ops/skills/common/router.md | 13 + agent-ops/skills/common/sync-pull/SKILL.md | 36 ++ agent-ops/skills/common/sync-push/SKILL.md | 60 ++++ .../skills/common/update-domain-rule/SKILL.md | 100 ++++++ 25 files changed, 1966 insertions(+), 24 deletions(-) create mode 100644 .clinerules create mode 100644 .cursorrules create mode 100644 agent-ops/.version create mode 100755 agent-ops/bin/bump-version.sh create mode 100644 agent-ops/bin/entry-files.sh create mode 100755 agent-ops/bin/init-agent-ops.sh create mode 100755 agent-ops/bin/sync.sh create mode 100644 agent-ops/rules/common/_templates/domain-rule-template.md create mode 100644 agent-ops/skills/common/_templates/skill-template.md create mode 100644 agent-ops/skills/common/code-review/SKILL.md create mode 100644 agent-ops/skills/common/code-review/agents/openai.yaml create mode 100644 agent-ops/skills/common/code-review/templates/complete-log-template.md create mode 100644 agent-ops/skills/common/commit-push/SKILL.md create mode 100644 agent-ops/skills/common/create-domain-rule/SKILL.md create mode 100644 agent-ops/skills/common/create-skill/SKILL.md create mode 100644 agent-ops/skills/common/init-agent-ops/SKILL.md create mode 100644 agent-ops/skills/common/plan/SKILL.md create mode 100644 agent-ops/skills/common/plan/agents/openai.yaml create mode 100644 agent-ops/skills/common/router.md create mode 100644 agent-ops/skills/common/sync-pull/SKILL.md create mode 100644 agent-ops/skills/common/sync-push/SKILL.md create mode 100644 agent-ops/skills/common/update-domain-rule/SKILL.md diff --git a/.clinerules b/.clinerules new file mode 100644 index 0000000..ae124ac --- /dev/null +++ b/.clinerules @@ -0,0 +1,16 @@ +# 공통 규칙 + +- 기존 구조를 우선한다. 새 파일 생성보다 기존 파일 수정을 우선한다. +- 코드 변경 전 관련 domain rule을 먼저 확인한다. +- 요청 범위를 넘는 변경을 하지 않는다. +- 불확실하면 단정하지 말고 후보를 제시한다. +- `agent-task/archive/**`는 사용자가 명시적으로 요청한 경우에만 읽는다. + +`agent-ops/rules/project/rules.md`와 `agent-ops/rules/private/rules.md`는 세션 최초 1회 읽는다. 파일이 없으면 무시한다. + +아래 성격의 요청은 사용자가 명시적으로 요청한 경우에만 `agent-ops/skills/common/router.md`를 작업 최초 1회 읽고 수행한다. 자동으로 수행하지 않는다. +- agent-ops 초기화 +- domain rule 생성 +- skill 생성 +- git commit / push +- agent-ops 업데이트 / 진입 파일 재적용 diff --git a/.cursorrules b/.cursorrules new file mode 100644 index 0000000..ae124ac --- /dev/null +++ b/.cursorrules @@ -0,0 +1,16 @@ +# 공통 규칙 + +- 기존 구조를 우선한다. 새 파일 생성보다 기존 파일 수정을 우선한다. +- 코드 변경 전 관련 domain rule을 먼저 확인한다. +- 요청 범위를 넘는 변경을 하지 않는다. +- 불확실하면 단정하지 말고 후보를 제시한다. +- `agent-task/archive/**`는 사용자가 명시적으로 요청한 경우에만 읽는다. + +`agent-ops/rules/project/rules.md`와 `agent-ops/rules/private/rules.md`는 세션 최초 1회 읽는다. 파일이 없으면 무시한다. + +아래 성격의 요청은 사용자가 명시적으로 요청한 경우에만 `agent-ops/skills/common/router.md`를 작업 최초 1회 읽고 수행한다. 자동으로 수행하지 않는다. +- agent-ops 초기화 +- domain rule 생성 +- skill 생성 +- git commit / push +- agent-ops 업데이트 / 진입 파일 재적용 diff --git a/AGENTS.md b/AGENTS.md index 94ab725..ae124ac 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -1,9 +1,16 @@ -## 응답 언어 -한국어로 응답한다. +# 공통 규칙 -## 기본 원칙 -- 기존 구조를 우선한다. -- 새 파일 생성보다 기존 파일 수정을 우선한다. -- 작업 전 `agent-ops/skills/router.md`를 참조하여 필요한 skill 또는 domain rule만 로드한다. -- 관련 rule / skill만 최소로 로드한다. -- 세부 규칙은 `agent-ops/` 아래 파일을 따른다. +- 기존 구조를 우선한다. 새 파일 생성보다 기존 파일 수정을 우선한다. +- 코드 변경 전 관련 domain rule을 먼저 확인한다. +- 요청 범위를 넘는 변경을 하지 않는다. +- 불확실하면 단정하지 말고 후보를 제시한다. +- `agent-task/archive/**`는 사용자가 명시적으로 요청한 경우에만 읽는다. + +`agent-ops/rules/project/rules.md`와 `agent-ops/rules/private/rules.md`는 세션 최초 1회 읽는다. 파일이 없으면 무시한다. + +아래 성격의 요청은 사용자가 명시적으로 요청한 경우에만 `agent-ops/skills/common/router.md`를 작업 최초 1회 읽고 수행한다. 자동으로 수행하지 않는다. +- agent-ops 초기화 +- domain rule 생성 +- skill 생성 +- git commit / push +- agent-ops 업데이트 / 진입 파일 재적용 diff --git a/CLAUDE.md b/CLAUDE.md index 94ab725..ae124ac 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -1,9 +1,16 @@ -## 응답 언어 -한국어로 응답한다. +# 공통 규칙 -## 기본 원칙 -- 기존 구조를 우선한다. -- 새 파일 생성보다 기존 파일 수정을 우선한다. -- 작업 전 `agent-ops/skills/router.md`를 참조하여 필요한 skill 또는 domain rule만 로드한다. -- 관련 rule / skill만 최소로 로드한다. -- 세부 규칙은 `agent-ops/` 아래 파일을 따른다. +- 기존 구조를 우선한다. 새 파일 생성보다 기존 파일 수정을 우선한다. +- 코드 변경 전 관련 domain rule을 먼저 확인한다. +- 요청 범위를 넘는 변경을 하지 않는다. +- 불확실하면 단정하지 말고 후보를 제시한다. +- `agent-task/archive/**`는 사용자가 명시적으로 요청한 경우에만 읽는다. + +`agent-ops/rules/project/rules.md`와 `agent-ops/rules/private/rules.md`는 세션 최초 1회 읽는다. 파일이 없으면 무시한다. + +아래 성격의 요청은 사용자가 명시적으로 요청한 경우에만 `agent-ops/skills/common/router.md`를 작업 최초 1회 읽고 수행한다. 자동으로 수행하지 않는다. +- agent-ops 초기화 +- domain rule 생성 +- skill 생성 +- git commit / push +- agent-ops 업데이트 / 진입 파일 재적용 diff --git a/GEMINI.md b/GEMINI.md index 94ab725..ae124ac 100644 --- a/GEMINI.md +++ b/GEMINI.md @@ -1,9 +1,16 @@ -## 응답 언어 -한국어로 응답한다. +# 공통 규칙 -## 기본 원칙 -- 기존 구조를 우선한다. -- 새 파일 생성보다 기존 파일 수정을 우선한다. -- 작업 전 `agent-ops/skills/router.md`를 참조하여 필요한 skill 또는 domain rule만 로드한다. -- 관련 rule / skill만 최소로 로드한다. -- 세부 규칙은 `agent-ops/` 아래 파일을 따른다. +- 기존 구조를 우선한다. 새 파일 생성보다 기존 파일 수정을 우선한다. +- 코드 변경 전 관련 domain rule을 먼저 확인한다. +- 요청 범위를 넘는 변경을 하지 않는다. +- 불확실하면 단정하지 말고 후보를 제시한다. +- `agent-task/archive/**`는 사용자가 명시적으로 요청한 경우에만 읽는다. + +`agent-ops/rules/project/rules.md`와 `agent-ops/rules/private/rules.md`는 세션 최초 1회 읽는다. 파일이 없으면 무시한다. + +아래 성격의 요청은 사용자가 명시적으로 요청한 경우에만 `agent-ops/skills/common/router.md`를 작업 최초 1회 읽고 수행한다. 자동으로 수행하지 않는다. +- agent-ops 초기화 +- domain rule 생성 +- skill 생성 +- git commit / push +- agent-ops 업데이트 / 진입 파일 재적용 diff --git a/agent-ops/.version b/agent-ops/.version new file mode 100644 index 0000000..e9bc149 --- /dev/null +++ b/agent-ops/.version @@ -0,0 +1 @@ +1.1.14 diff --git a/agent-ops/bin/bump-version.sh b/agent-ops/bin/bump-version.sh new file mode 100755 index 0000000..4cc50ef --- /dev/null +++ b/agent-ops/bin/bump-version.sh @@ -0,0 +1,28 @@ +#!/usr/bin/env bash +# bump-version.sh +# patch +1, 999 초과 시 minor 올림. major는 수동 관리. +# 결과 버전을 stdout으로 출력. + +set -euo pipefail + +YELLOW='\033[1;33m'; RESET='\033[0m' + +VERSION="${1:-}" +if [[ -z "$VERSION" ]]; then + echo "사용법: $0 " >&2 + exit 1 +fi + +IFS='.' read -r major minor patch <<< "$VERSION" + +patch=$((patch + 1)) +if [[ $patch -gt 999 ]]; then + patch=0 + minor=$((minor + 1)) +fi +if [[ $minor -gt 999 ]]; then + minor=0 + echo -e "${YELLOW}⚠ minor 버전이 999를 초과했습니다. major 버전을 수동으로 올려주세요.${RESET}" >&2 +fi + +echo "$major.$minor.$patch" diff --git a/agent-ops/bin/entry-files.sh b/agent-ops/bin/entry-files.sh new file mode 100644 index 0000000..dfb6f90 --- /dev/null +++ b/agent-ops/bin/entry-files.sh @@ -0,0 +1,24 @@ +#!/usr/bin/env bash + +# Shared entry-point file list for init/sync scripts. +AGENT_OPS_ENTRY_FILES=("GEMINI.md" "CLAUDE.md" "AGENTS.md" ".cursorrules" ".clinerules") + +apply_agent_ops_entry_files() { + local rules_md="$1" + local target_root="$2" + + if [[ ! -f "$rules_md" ]]; then + if [[ -n "${YELLOW:-}" && -n "${RESET:-}" ]]; then + echo -e "${YELLOW} rules.md 없음, 진입 파일 재적용 건너뜀${RESET}" + else + echo " rules.md 없음, 진입 파일 재적용 건너뜀" + fi + return + fi + + local f + for f in "${AGENT_OPS_ENTRY_FILES[@]}"; do + cp "$rules_md" "$target_root/$f" + echo " 진입 파일 적용: $f" + done +} diff --git a/agent-ops/bin/init-agent-ops.sh b/agent-ops/bin/init-agent-ops.sh new file mode 100755 index 0000000..449c410 --- /dev/null +++ b/agent-ops/bin/init-agent-ops.sh @@ -0,0 +1,104 @@ +#!/bin/bash + +# agent-ops/bin/init-agent-ops.sh +# 프로젝트에 agent-ops 스캐폴드를 초기화하는 스크립트 + +set -e + +SCRIPT_DIR=$(realpath "$(dirname "$0")") +SOURCE_DIR=$(realpath "$SCRIPT_DIR/..") +source "$SCRIPT_DIR/entry-files.sh" + +if [ -z "$1" ]; then + echo "Usage: $0 " + exit 1 +fi + +TARGET_DIR=$(realpath "$1") +ARCHIVE_IGNORE_PATTERN="agent-task/archive/**" + +append_unique_line() { + local file="$1" + local line="$2" + + touch "$file" + if ! grep -qxF "$line" "$file"; then + printf "%s\n" "$line" >> "$file" + fi +} + +echo "Initializing agent-ops in: $TARGET_DIR" +echo "Source agent-ops: $SOURCE_DIR" + +# 1. 대상 폴더 생성 +mkdir -p "$TARGET_DIR/agent-ops/rules/project/domain" +mkdir -p "$TARGET_DIR/agent-ops/rules/private" +mkdir -p "$TARGET_DIR/agent-ops/skills/project" + +# 2. 공통 요소 복사 (프로젝트 전용 설정은 제외) +cp "$SOURCE_DIR/.version" "$TARGET_DIR/agent-ops/" +rm -rf "$TARGET_DIR/agent-ops/bin" +cp -r "$SOURCE_DIR/bin" "$TARGET_DIR/agent-ops/" +cp -r "$SOURCE_DIR/rules/common" "$TARGET_DIR/agent-ops/rules/" +cp -r "$SOURCE_DIR/skills/common" "$TARGET_DIR/agent-ops/skills/" + +# 3. 에이전트 진입 파일 생성 (common/rules.md 복사) +COMMON_RULES="$SOURCE_DIR/rules/common/rules.md" +apply_agent_ops_entry_files "$COMMON_RULES" "$TARGET_DIR" + +# 4. AI ignore / permission 설정 +append_unique_line "$TARGET_DIR/.geminiignore" "$ARCHIVE_IGNORE_PATTERN" +append_unique_line "$TARGET_DIR/.aiexclude" "$ARCHIVE_IGNORE_PATTERN" +append_unique_line "$TARGET_DIR/.cursorignore" "$ARCHIVE_IGNORE_PATTERN" +append_unique_line "$TARGET_DIR/.clineignore" "$ARCHIVE_IGNORE_PATTERN" + +mkdir -p "$TARGET_DIR/.claude" +if [ ! -f "$TARGET_DIR/.claude/settings.json" ]; then + cat > "$TARGET_DIR/.claude/settings.json" <<'EOF' +{ + "$schema": "https://json.schemastore.org/claude-code-settings.json", + "permissions": { + "deny": [ + "Read(./agent-task/archive/**)" + ] + } +} +EOF +elif ! grep -q "agent-task/archive" "$TARGET_DIR/.claude/settings.json"; then + echo "Note: .claude/settings.json exists; add Read(./agent-task/archive/**) manually." +fi + +if [ ! -f "$TARGET_DIR/opencode.json" ]; then + cat > "$TARGET_DIR/opencode.json" <<'EOF' +{ + "$schema": "https://opencode.ai/config.json", + "permission": { + "read": { + "agent-task/archive/**": "deny" + }, + "glob": { + "agent-task/archive/**": "deny" + } + }, + "watcher": { + "ignore": [ + "agent-task/archive/**" + ] + } +} +EOF +elif ! grep -q "agent-task/archive" "$TARGET_DIR/opencode.json"; then + echo "Note: opencode.json exists; add agent-task/archive/** read/glob deny and watcher ignore manually." +fi + +# 5. .gitignore 설정 +TOUCH_GITIGNORE="$TARGET_DIR/.gitignore" +touch "$TOUCH_GITIGNORE" +if ! grep -q "agent-ops/rules/private/" "$TOUCH_GITIGNORE"; then + echo "" >> "$TOUCH_GITIGNORE" + echo "# Agent-Ops Private Rules" >> "$TOUCH_GITIGNORE" + echo "agent-ops/rules/private/" >> "$TOUCH_GITIGNORE" +fi + +echo "Successfully initialized agent-ops in $TARGET_DIR" +echo "Note: agent-ops/rules/project and agent-ops/skills/project are initialized as empty." diff --git a/agent-ops/bin/sync.sh b/agent-ops/bin/sync.sh new file mode 100755 index 0000000..632853d --- /dev/null +++ b/agent-ops/bin/sync.sh @@ -0,0 +1,182 @@ +#!/usr/bin/env bash +set -euo pipefail + +# ── 경로 설정 ──────────────────────────────────────────────────────────────── +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +AGENT_OPS_DIR="$(cd "$SCRIPT_DIR/.." && pwd)" +PROJECT_ROOT="$(cd "$AGENT_OPS_DIR/.." && pwd)" + +# ── 색상 ───────────────────────────────────────────────────────────────────── +RED='\033[0;31m'; YELLOW='\033[1;33m'; GREEN='\033[0;32m'; RESET='\033[0m' + +# ── 진입 파일 공통 관리 ────────────────────────────────────────────────────── +source "$SCRIPT_DIR/entry-files.sh" + +# ── 대상 경로 해석 (폴더명 / 상대경로 / 절대경로) ──────────────────────────── +resolve_target() { + local input="$1" + if [[ "$input" == /* ]]; then + [[ -d "$input" ]] && echo "$input" || echo "" + return + fi + if [[ "$input" == */* ]]; then + local resolved + resolved="$(cd "$input" 2>/dev/null && pwd)" || { echo ""; return; } + echo "$resolved" + return + fi + local sibling="$(dirname "$PROJECT_ROOT")/$input" + [[ -d "$sibling" ]] && echo "$sibling" || echo "" +} + +# ── 버전 비교: v1 > v2 이면 0 반환 ─────────────────────────────────────────── +version_gt() { + local v1="$1" v2="$2" + [[ "$v1" == "$v2" ]] && return 1 + [[ "$(printf '%s\n%s' "$v1" "$v2" | sort -V | head -1)" == "$v2" ]] +} + +# ── 버전 +1 ────────────────────────────────────────────────────────────────── +bump_version() { + bash "$SCRIPT_DIR/bump-version.sh" "$1" +} + +# ── 폴더 동기화 (rules.md 제외, 삭제된 파일도 반영) ───────────────────────── +sync_folder() { + local src="$1" dst="$2" exclude="${3:-}" + mkdir -p "$dst" + # dst에서 src에 없는 항목 제거 (exclude 파일 보존) + find "$dst" -mindepth 1 -maxdepth 1 | while IFS= read -r item; do + local name + name="$(basename "$item")" + [[ -n "$exclude" && "$name" == "$exclude" ]] && continue + if [[ ! -e "$src/$name" ]]; then + rm -rf "$item" + fi + done + # src에서 dst로 복사 (exclude 파일 제외) + find "$src" -mindepth 1 -maxdepth 1 | while IFS= read -r item; do + local name + name="$(basename "$item")" + [[ -n "$exclude" && "$name" == "$exclude" ]] && continue + rm -rf "$dst/$name" + cp -r "$item" "$dst/" + done +} + +sync_common() { + local src="$1" dst="$2" + sync_folder "$src/rules/common" "$dst/rules/common" "rules.md" + sync_folder "$src/skills/common" "$dst/skills/common" + sync_folder "$src/bin" "$dst/bin" +} + +# ── 사용법 ──────────────────────────────────────────────────────────────────── +usage() { + echo "사용법: $0 [--pull] " + echo "" + echo " (기본) 현재 프로젝트 → target(agentic-framework) 으로 push" + echo " --pull target(agentic-framework) → 현재 프로젝트 로 pull" + echo "" + echo " target: 폴더명 (동일 레벨 탐색) | 상대경로 | 절대경로" +} + +# ───────────────────────────────────────────────────────────────────────────── +PULL_MODE="0" +if [[ "${1:-}" == "--pull" ]]; then + PULL_MODE="1" + shift +fi + +TARGET_INPUT="${1:-}" +if [[ -z "$TARGET_INPUT" ]]; then + usage; exit 1 +fi + +TARGET="$(resolve_target "$TARGET_INPUT")" +if [[ -z "$TARGET" ]]; then + echo -e "${RED}Error: 대상을 찾을 수 없습니다: $TARGET_INPUT${RESET}" + exit 1 +fi + +SRC_AO="$AGENT_OPS_DIR" +DST_AO="$TARGET/agent-ops" +IS_FRAMEWORK="$([[ -f "$PROJECT_ROOT/.agent-ops-source" ]] && echo "1" || echo "0")" + +# ── pull 모드: agentic-framework → 현재 프로젝트 ───────────────────────────── +if [[ "$PULL_MODE" == "1" ]]; then + if [[ "$IS_FRAMEWORK" == "1" ]]; then + echo -e "${RED}Error: agentic-framework에서는 --pull을 사용할 수 없습니다.${RESET}" + exit 1 + fi + if [[ ! -f "$TARGET/.agent-ops-source" ]]; then + echo -e "${RED}Error: target이 agentic-framework가 아닙니다 (.agent-ops-source 없음)${RESET}" + exit 1 + fi + SRC_VER="$(cat "$SRC_AO/.version" 2>/dev/null || echo "0.0.0")" + DST_VER="$(cat "$DST_AO/.version" 2>/dev/null || echo "0.0.0")" + echo "▶ $(basename "$TARGET") → $(basename "$PROJECT_ROOT") (pull)" + echo " 현재 버전: $SRC_VER | framework 버전: $DST_VER" + sync_common "$DST_AO" "$SRC_AO" + cp "$DST_AO/.version" "$SRC_AO/.version" + apply_agent_ops_entry_files "$DST_AO/rules/common/rules.md" "$PROJECT_ROOT" + cd "$PROJECT_ROOT" + git add agent-ops/rules/common agent-ops/skills/common agent-ops/bin agent-ops/.version \ + "${AGENT_OPS_ENTRY_FILES[@]}" + git commit -m "sync: pull from agentic-framework v$DST_VER" + git push + echo -e "${GREEN}✓ 완료 (pull v$DST_VER)${RESET}" + exit 0 +fi + +echo "▶ $(basename "$PROJECT_ROOT") → $(basename "$TARGET")" + +# ── agentic-framework에서 다른 프로젝트로 ──────────────────────────────────── +if [[ "$IS_FRAMEWORK" == "1" ]]; then + if [[ ! -d "$DST_AO" ]]; then + echo " 최초 진행: agent-ops 전체 복사" + cp -r "$SRC_AO" "$TARGET/" + apply_agent_ops_entry_files "$SRC_AO/rules/common/rules.md" "$TARGET" + echo -e "${YELLOW} init-agent-ops 스킬로 초기화를 진행하세요.${RESET}" + else + echo " 이후 진행: common/ 동기화" + sync_common "$SRC_AO" "$DST_AO" + cp "$SRC_AO/.version" "$DST_AO/.version" + apply_agent_ops_entry_files "$SRC_AO/rules/common/rules.md" "$TARGET" + fi + echo -e "${GREEN}✓ 완료 → $(basename "$TARGET")${RESET}" + exit 0 +fi + +# ── 일반 프로젝트에서 agentic-framework로 (push) ───────────────────────────── +if [[ ! -f "$TARGET/.agent-ops-source" ]]; then + echo -e "${RED}Error: target이 agentic-framework가 아닙니다 (.agent-ops-source 없음)${RESET}" + exit 1 +fi + +SRC_VER="$(cat "$SRC_AO/.version" 2>/dev/null || echo "0.0.0")" +DST_VER="$(cat "$DST_AO/.version" 2>/dev/null || echo "0.0.0")" +echo " 현재 버전: $SRC_VER | framework 버전: $DST_VER" + +if version_gt "$DST_VER" "$SRC_VER"; then + echo -e "${RED}⚠ 버전 충돌: framework($DST_VER) > current($SRC_VER)${RESET}" + echo -e "${YELLOW} 먼저 sync-pull로 내려받은 뒤 재시도하세요.${RESET}" + exit 1 +fi + +NEW_VER="$(bump_version "$SRC_VER")" +sync_common "$SRC_AO" "$DST_AO" +echo "$NEW_VER" > "$SRC_AO/.version" +echo "$NEW_VER" > "$DST_AO/.version" + +cd "$TARGET" +git add agent-ops/rules/common agent-ops/skills/common agent-ops/bin agent-ops/.version +git commit -m "sync: from $(basename "$PROJECT_ROOT") v$NEW_VER" +git push +cd "$PROJECT_ROOT" + +git add agent-ops/rules/common agent-ops/skills/common agent-ops/bin agent-ops/.version +git commit -m "sync: to agentic-framework v$NEW_VER" +git push + +echo -e "${GREEN}✓ 완료 (v$SRC_VER → v$NEW_VER)${RESET}" diff --git a/agent-ops/rules/common/_templates/domain-rule-template.md b/agent-ops/rules/common/_templates/domain-rule-template.md new file mode 100644 index 0000000..529e10d --- /dev/null +++ b/agent-ops/rules/common/_templates/domain-rule-template.md @@ -0,0 +1,29 @@ +# + +## 목적 / 책임 + +<이 도메인이 담당하는 책임을 1~2문장으로> + +## 포함 경로 + +- `/` — <이 경로가 이 도메인에 속하는 이유> + +## 제외 경로 + +- `/` — <왜 이 도메인이 아닌지> + +## 주요 구성 요소 + +- `` — <역할> + +## 유지할 패턴 + +- <네이밍 규칙 또는 아키텍처 패턴> + +## 다른 도메인과의 경계 + +- **<인접 domain>**: <어디까지가 이 도메인이고 어디서부터 저 도메인인지> + +## 금지 사항 + +- <이 도메인 코드에서 하면 안 되는 것> diff --git a/agent-ops/skills/common/_templates/skill-template.md b/agent-ops/skills/common/_templates/skill-template.md new file mode 100644 index 0000000..2372acb --- /dev/null +++ b/agent-ops/skills/common/_templates/skill-template.md @@ -0,0 +1,57 @@ +--- +name: +version: 1.0.0 +description: <이 skill이 하는 일을 한 줄로 설명. 트리거 키워드 포함 권장> +depends: [] # 선택 — 의존 skill이 없으면 이 줄 삭제 +--- + +# + +## 목적 + +<이 skill이 해결하는 문제를 1~2문장으로 설명> + +## 언제 호출할지 + +- <이 skill을 호출해야 하는 상황 1> +- <이 skill을 호출해야 하는 상황 2> +- <이 skill을 호출해야 하는 상황 3> + +## 입력 + +- ``: <설명> (필수) +- ``: <설명> (선택) + +## 먼저 확인할 것 + +- [ ] <실행 전 반드시 확인해야 할 조건 1> +- [ ] <실행 전 반드시 확인해야 할 조건 2> + +## 실행 절차 + +1. **<단계명>** + - <세부 행동> + - <세부 행동> + +2. **<단계명>** + - <세부 행동> + +3. **결과 보고** + - <출력할 내용> + +## 실행 결과 검증 + +- [ ] <실행 후 확인해야 할 성공 조건 1> +- [ ] <실행 후 확인해야 할 성공 조건 2> +- 검증 실패 시: <실패 시 취할 행동 — 롤백, 사용자 알림, 재시도 등> + +## 출력 형식 + +``` +<출력 예시> +``` + +## 금지 사항 + +- <절대 하면 안 되는 것> +- <절대 하면 안 되는 것> diff --git a/agent-ops/skills/common/code-review/SKILL.md b/agent-ops/skills/common/code-review/SKILL.md new file mode 100644 index 0000000..f2429db --- /dev/null +++ b/agent-ops/skills/common/code-review/SKILL.md @@ -0,0 +1,324 @@ +--- +name: code-review +description: Review completed implementation work in the current repository. Reads PLAN-{build_lane}-GNN.md and CODE_REVIEW-{review_lane}-GNN.md from the active task, reviews the actual changed source files, appends a verdict to the review file, then archives both files as .log. On PASS, writes complete.log summarising the full loop history and moves the completed task under agent-task/archive/YYYY/MM/. If Required or Suggested issues are found (FAIL or WARN), writes new routed plan/review files so the loop continues immediately. Nit-only findings may still PASS. +--- + +# Code Review + +## Purpose + +Review the implementation phase of the plan-code-review loop: + +```text +plan skill -> implementation -> code-review skill + ^ | + +----- issues found: new routed plan/review files +``` + +## Workflow Contract + +Active work must live under `agent-task/{task_name}/` using routed filenames. This is the state protocol shared with the plan skill. + +Filename rules: + +- Plan file: `PLAN-{build_lane}-GNN.md` +- Review file: `CODE_REVIEW-{review_lane}-GNN.md` +- `{lane}` is only `local` or `cloud`; never put model names in filenames. +- `GNN` is a two-digit capability grade from `G01` to `G10`; runtime maps lane+grade to current models externally. + +Multi-plan runtime contract: + +- Multi-plan work is represented as multiple task directories. Each directory owns exactly one normal active plan file and one normal active review file. +- Directory names may encode runtime scheduling metadata. Preserve them verbatim; do not normalize, reinterpret, or choose execution order by agent judgment. +- If the user/runtime names a task directory, review that directory even when other active review files exist. + +Review routing rules: + +- `local`: narrow, low-risk, or first-pass review where tests and scope are clear. +- `cloud`: multi-file, API/call-site impact, meaningful test judgment, plan deviation, weak verification, security/auth, storage/migration, concurrency, protocol/schema, cross-domain, or repeated Required issues. +- `cloud-G07` or higher is mandatory for terminal-agent follow-up work: shell/CLI workflow implementation, bin script orchestration, process control, stdout/stderr parsing, exit-status contracts, long-running command diagnosis, or terminal benchmark-style tasks. Merely running deterministic tests such as `go test` does not make a task terminal-agent work. +- `cloud-G07` or higher is mandatory for follow-up plans when a local implementation failed a real bin/smoke/integration command after unit tests passed, when the success condition depends on an interactive TUI/PTY/browser/external CLI or screen repaint/cursor stream, or when verification trust is Fail because recorded stdout/stderr does not match a rerun. + +Directory states: + +| State | Meaning | +|-------|---------| +| `PLAN-*-G??.md` + `CODE_REVIEW-*-G??.md` stub or placeholders | Implementation is pending/incomplete; review should fail completeness if invoked | +| `PLAN-*-G??.md` + filled `CODE_REVIEW-*-G??.md` | Ready for code-review skill | +| `complete.log` + `*.log` files | Task complete (PASS), before final task-directory archive move | +| `agent-task/archive/YYYY/MM/{task_name}/complete.log` + `*.log` files | Archived completed task (PASS); not active | +| Only `*.log` files (no `complete.log`) | Task terminated mid-loop or abandoned | + +The implementing agent never archives or deletes active files; archiving is this skill's responsibility. + +Finalization invariant: + +- Every review attempt that selects an active review file must end by archiving the active `CODE_REVIEW-*-G??.md` and `PLAN-*-G??.md` files. +- After archiving, exactly one next state is allowed: + - `PASS`: write `complete.log`, move `agent-task/{task_name}/` to `agent-task/archive/YYYY/MM/{task_name}/`, then complete the review-only checklist in the final archive path. + - `WARN` or `FAIL`: write the next active `PLAN-{build_lane}-GNN.md` and `CODE_REVIEW-{review_lane}-GNN.md`; do not write `complete.log`. +- Never stop after appending a verdict. Do not report to the user until archive, required next-state file writes, review-only checklist updates, and PASS task-directory archive moves are complete. +- If the review result feels ambiguous, choose `WARN` or `FAIL` according to the severity rules, archive the current files, and write the next plan/review pair. Ambiguity is not a reason to leave active files unarchived. + +## Step 1 - Find Active Task + +Glob `agent-task/*/CODE_REVIEW-*-G??.md`: + +| Result | Action | +|--------|--------| +| Exactly one | Review that task; exactly one `PLAN-*-G??.md` is expected beside it | +| None | Nothing to review; stop and report | +| Multiple | If the user/runtime named a task directory, review that directory. Otherwise list paths and ask which task to review; do not choose by agent judgment. | + +## Step 2 - Load Context + +Count `agent-task/{task_name}/code_review_*.log`: + +- `0`: first review. Read the active review file, active plan file, every planned source file, related tests, and files importing/imported by changed files up to 2 levels deep. +- `>=1`: follow-up review. Start with `git diff`, `git diff --cached`, and `git log --oneline -5`, then expand to related callers, implementers, tests, and any planned files missing from the diff. + +The diff is the starting point, not the boundary. Follow behavior and API connections far enough to judge correctness. + +## Step 3 - Pre-Review Checklist + +Before writing the verdict: + +- Compare actual source files against every planned checklist item. +- Compare the plan `구현 체크리스트` and review stub `구현 체크리스트`; item text/order must match. +- Confirm the implementation marked the matching checklist items in the active review file, including the final mandatory `CODE_REVIEW-*-G??.md` completion item. +- Treat blank placeholder sections, missing actual implementation notes, missing checklist completion, or missing actual stdout/stderr in the active review file as a completeness or verification-trust failure. +- Grep renamed/removed symbols for stale references. +- Confirm every required test exists, name matches, and assertions are meaningful. +- Cross-check claimed verification output in the active review file against actual code and project commands. +- For follow-up reviews, compare diff against the plan and scan for unplanned changes, debug prints, dead code, TODOs, formatting-only noise, and unrelated edits. + +## Step 4 - Append Verdict + +Append `코드리뷰 결과` to the active `CODE_REVIEW-*-G??.md`. + +Required fields: + +- `종합 판정`: exactly `PASS`, `WARN`, or `FAIL`. +- `차원별 평가`: Pass/Warn/Fail for correctness, completeness, test coverage, API contract, code quality, plan deviation, verification trust. +- `발견된 문제`: `없음`, or bullets using `Required`, `Suggested`, or `Nit` with `file:line` and a concrete fix. +- `다음 단계`: keep only the matching PASS/WARN/FAIL line. + +Do not check archive/next-state items in `코드리뷰 전용 체크리스트` during Step 4. Complete the applicable dedicated checklist items in the archived `code_review_*.log` during Step 7, after archive, next-state writes, and PASS task-directory moves are done. + +Severity semantics: + +| Verdict | Meaning | Follow-up plan | +|---------|---------|----------------| +| `PASS` | No Required/Suggested issues. Nit-only findings may still PASS. | No | +| `WARN` | One or more Suggested issues, zero Required. | Yes | +| `FAIL` | One or more Required issues. | Yes | + +Issue severity: + +- `Required`: correctness, API contract, missing required test, plan-completeness issue, or incomplete/placeholder `CODE_REVIEW-*-G??.md` content required from the implementing agent. +- `Suggested`: useful improvement that should enter the loop but does not block correctness. +- `Nit`: tiny cleanup; may be recorded without forcing WARN. + +Verdict consistency: + +- `PASS` requires all dimensions to be Pass and no Required/Suggested issues. Nit-only findings may still PASS only when every dimension remains Pass. +- Any Fail dimension or any Required issue forces `FAIL`. +- Any Warn dimension or any Suggested issue forces `WARN`, unless the only findings are explicitly Nit and every dimension remains Pass. + +## Step 5 - Archive Active Files + +Archive is mandatory for `PASS`, `WARN`, and `FAIL`. Archive order is fixed: + +1. Count existing `code_review_*.log` as `N`; rename `CODE_REVIEW-{review_lane}-GNN.md` to `code_review_{review_lane}_GNN_N.log`. +2. Count existing `plan_*.log` as `M`; rename `PLAN-{build_lane}-GNN.md` to `plan_{build_lane}_GNN_M.log`. + +After archiving, neither active `.md` file remains unless Step 6 writes a follow-up. + +## Step 6 - Post-Review Actions + +For `PASS`, write `agent-task/{task_name}/complete.log` before reporting. + +Complete log template: + +- Template path: `agent-ops/skills/common/code-review/templates/complete-log-template.md` +- Copy the template's section order and fill every placeholder from the archived plan/review logs and final verdict. +- Do not leave placeholders in `complete.log`. +- Use `없음` for empty `잔여 Nit` or `후속 작업`. +- A PASS `complete.log` must not contain unresolved Required or Suggested issues. Nit-only leftovers may be recorded under `잔여 Nit`. + +After Step 7, report: + +- Verdict. +- Archive filenames. +- Final task archive path under `agent-task/archive/YYYY/MM/`. + +For `WARN` or `FAIL`, write new routed plan/review files using the plan skill format: + +- New plan number is the count of `plan_*.log` after archive. +- Header tag is `REVIEW_`. +- Choose lane/grade again; preserve the prior route only when it was adequate, otherwise raise `GNN` and/or move `local -> cloud`. +- Before choosing the follow-up route, apply this escalation gate: + - If the archived plan was `local-*` and the verdict is `FAIL` for correctness, completeness, test coverage, or verification trust, move the follow-up build lane to `cloud` unless the issue is trivially deterministic and review-detectable without live environment behavior. + - If the follow-up work is terminal-agent work (shell/CLI workflow implementation, bin script orchestration, process control, stdout/stderr parsing, exit-status contracts, long-running command diagnosis, or terminal benchmark-style tasks), use `cloud-G07` or higher. + - If unit tests passed but a real bin/smoke/integration command failed, use `cloud-G07` or higher. + - If the task depends on interactive TUI/PTY/browser/external CLI behavior, screen repaint/cursor stream parsing, or live command-palette state, use `cloud-G07` or higher. + - If recorded verification output was reconstructed, stale, or mismatched on rerun, use `cloud-G07` or higher and make verification trust recovery a plan item. +- `FAIL`: one plan item per Required issue. +- `WARN`: one grouped plan item for Suggested issues, plus related Nit issues if useful. +- Each plan item needs problem, solution with before/after when non-trivial, checklist, test decision, intermediate verification. +- The follow-up plan and review stub must contain matching `구현 체크리스트` item text/order, including the final mandatory `CODE_REVIEW-*-G??.md` completion item. + +Routed review stub template (fill `{…}` placeholders; everything else is fixed and must not be changed by the implementing agent): + +```markdown + + +# Code Review Reference - {TAG} + +> **[IMPLEMENTING AGENT — READ FIRST] Filling in this file is the mandatory final step of implementation.** +> The task is NOT complete until every implementation-owned section below is filled in. +> Complete the `구현 체크리스트`; the final checklist item is mandatory before saving. +> Do not modify or check the `코드리뷰 전용 체크리스트`; it is owned by the review agent only. +> Follow the ownership table at the bottom of this file for which sections you own. + +## 개요 + +date={YYYY-MM-DD} +task={task_name}, plan={N}, tag={TAG} + +## 이 파일을 읽는 리뷰 에이전트에게 + +각 항목의 구현을 실제 소스 파일과 대조하고, `검증 결과` 섹션의 출력이 코드와 일치하는지 확인하세요. +Review 완료 후 반드시 아래 순서로 아카이브하세요. + +1. `CODE_REVIEW-{review_lane}-GNN.md` → `code_review_{review_lane}_GNN_N.log` (N = 기존 code_review_*.log 수) +2. `PLAN-{build_lane}-GNN.md` → `plan_{build_lane}_GNN_M.log` (M = 기존 plan_*.log 수) +3. PASS인 경우 `complete.log` 작성 후 `agent-task/archive/YYYY/MM/{task_name}/`로 task 디렉터리 이동. WARN/FAIL인 경우 새 routed plan + review 스텁 작성. + +어떤 판정에서도 아카이브를 건너뛰지 마세요. PASS/WARN/FAIL 모두 `코드리뷰 결과` append 후 active plan/review 파일을 먼저 아카이브하고, 그 다음 `complete.log` 또는 다음 plan/review 파일을 작성해야 합니다. +PASS에서는 `agent-ops/skills/common/code-review/templates/complete-log-template.md`의 섹션 순서와 필수 항목을 기준으로 `complete.log`를 작성하세요. 작성 후 현재 날짜의 `YYYY/MM` 기준으로 task 디렉터리를 `agent-task/archive/YYYY/MM/{task_name}/`로 이동하고, 최종 archive 경로의 `code_review_*.log`에서 `코드리뷰 전용 체크리스트`를 갱신한 다음 보고하세요. +WARN/FAIL에서는 다음 상태 파일 작성 후 현재 task 경로의 archived `code_review_*.log`에서 적용 가능한 `코드리뷰 전용 체크리스트` 항목을 체크한 다음 보고하세요. + +--- + +## 구현 항목별 완료 여부 + +| 항목 | 완료 여부 | +|------|---------| +| [{TAG}-1] {item description} | [ ] | +| [{TAG}-2] {item description} | [ ] | + +## 구현 체크리스트 + +{copy the follow-up plan's 구현 체크리스트 items exactly, preserving order and checkbox text} + +## 코드리뷰 전용 체크리스트 + +> **[REVIEW AGENT ONLY]** 이 체크리스트는 코드리뷰 에이전트만 사용한다. +> 구현 에이전트는 이 섹션을 수정하거나 체크하지 않는다. + +- [ ] `코드리뷰 결과`에 `PASS`, `WARN`, `FAIL` 중 하나의 판정을 append한다. +- [ ] 판정과 `차원별 평가`, Required/Suggested/Nit 분류가 서로 일치한다. +- [ ] active `CODE_REVIEW-*-G??.md`를 `code_review_{review_lane}_GNN_N.log`로 아카이브한다. +- [ ] active `PLAN-*-G??.md`를 `plan_{build_lane}_GNN_M.log`로 아카이브한다. +- [ ] PASS이면 `agent-ops/skills/common/code-review/templates/complete-log-template.md` 기준으로 `complete.log`를 작성하고 active `.md` 파일을 남기지 않는다. +- [ ] PASS이면 `agent-task/{task_name}/`를 `agent-task/archive/YYYY/MM/{task_name}/`로 이동하고 최종 archive 경로에서 이 체크리스트를 갱신한다. +- [ ] WARN/FAIL이면 다음 active `PLAN-{build_lane}-GNN.md`와 `CODE_REVIEW-{review_lane}-GNN.md`를 작성하고 `complete.log`를 작성하지 않는다. + +## 계획 대비 변경 사항 + +_구현 에이전트가 계획과 다르게 구현한 부분을 이유와 함께 기록한다._ + +## 주요 설계 결정 + +_구현 에이전트가 주요 설계 결정 사항을 기록한다._ + +## 리뷰어를 위한 체크포인트 + +{pre-filled from plan — one bullet per review focus area} + +## 검증 결과 + +_구현 에이전트가 각 중간 검증 및 최종 검증 명령 실행 후 출력을 여기에 붙여 넣는다._ + +필수 규칙: +- 검증 명령은 고정된 계약이다. 임의로 대체하지 않는다. +- 대체가 필요하면 `계획 대비 변경 사항`에 이유와 대체 명령을 기록한다. +- `검증 결과`에는 실제 stdout/stderr를 붙여 넣는다. + +### {TAG}-1 중간 검증 +``` +$ {verification command from plan} +(output) +``` + +### 최종 검증 +``` +$ {final verification command from plan} +(output) +``` + +--- + +> **[IMPLEMENTING AGENT — BEFORE SAVING] Have you filled in every implementation-owned section: completion table, implementation checklist, changes from plan, design decisions, and verification output?** +> If anything is blank, go back and fill it in before saving this file. +> Leave the review-agent-only checklist unchanged. +``` + +Sections and their ownership: + +| 섹션 | 소유자 | 설명 | +|------|--------|------| +| 헤더 주석, 개요(date/task/plan/tag), 리뷰 에이전트 지시 | 스텁 생성 시 고정 | 구현 에이전트가 수정하지 않음 | +| 구현 항목별 완료 여부 (항목명) | 스텁 생성 시 고정 | `[ ]` → `[x]` 체크만 구현 에이전트가 수행 | +| 구현 체크리스트 (항목 텍스트/순서) | follow-up plan에서 복사해 스텁 생성 시 고정 | 구현 에이전트가 `[ ]` → `[x]` 체크만 수행; 마지막 체크박스는 저장 전 필수 | +| 코드리뷰 전용 체크리스트 | Review agent only | Implementing agent must not modify or check this section | +| 계획 대비 변경 사항, 주요 설계 결정 | 구현 에이전트가 채움 | placeholder 텍스트를 실제 내용으로 교체 | +| 리뷰어를 위한 체크포인트 | 스텁 생성 시 고정 | 계획에서 추출한 리뷰 포인트 | +| 검증 결과 (섹션 제목 + 명령) | 스텁 생성 시 고정 | 실행 출력만 구현 에이전트가 채움; 명령 변경은 `계획 대비 변경 사항`에 기록 | +| 코드리뷰 결과 | 리뷰 에이전트가 append | 스텁에 포함하지 않음 | + +## Step 7 - Complete Review-Only Checklist, Move PASS Task, And Report + +After Step 6: + +- If verdict is `PASS`, determine archive month from the current completion date as `YYYY/MM`, create `agent-task/archive/YYYY/MM/` if missing, then move `agent-task/{task_name}/` to `agent-task/archive/YYYY/MM/{task_name}/`. +- Do not overwrite an existing archive directory. If `agent-task/archive/YYYY/MM/{task_name}/` already exists, append the next numeric suffix: `agent-task/archive/YYYY/MM/{task_name}_1/`, `agent-task/archive/YYYY/MM/{task_name}_2/`, and so on. +- For `PASS`, open the moved `agent-task/archive/YYYY/MM/{final_task_dir}/code_review_{review_lane}_GNN_N.log`. +- For `WARN` or `FAIL`, open `agent-task/{task_name}/code_review_{review_lane}_GNN_N.log`. +- Check every applicable item in `코드리뷰 전용 체크리스트`; leave mutually exclusive verdict items unchecked. +- If any applicable item cannot be checked, finish the missing archive, `complete.log`, task-directory move, or follow-up plan/review write first. +- Do not recreate an active review file just to update this checklist; update the archived `code_review_*.log`. +- Only report after the archived review log has the verdict, applicable checked review-only checklist, required next-state files, and for `PASS` the final task archive move. + +Report Required/Suggested counts, archive names, and the final task archive path for `PASS` or new plan path for `WARN`/`FAIL`. + +## Review Dimensions + +| Dimension | Check | +|-----------|-------| +| Correctness | Logic, edge cases, concurrency, errors | +| Completeness | All planned checklist items done, including matching plan/review `구현 체크리스트` completion | +| Test coverage | Required tests present and meaningful | +| API contract | Call sites, compatibility, docs | +| Code quality | No debug prints, dead code, leftover TODOs | +| Plan deviation | Deviations justified, no unrelated risk | +| Verification trust | Reported output matches actual code | + +## Quality Rules + +- Lead with findings; use specific `file:line`. +- Provide a concrete fix for every Required issue. +- Name exact stale symbols or missing tests. +- Do not write vague praise or style opinions without a rule. +- Every dimension gets Pass/Warn/Fail. +- For follow-up plans about verification trust, specify deterministic commands, for example `rg --sort path`, and forbid repo-local tool artifacts. + +## Final Checklist + +- `code_review_{review_lane}_GNN_N.log` exists with verdict appended. +- `plan_{build_lane}_GNN_M.log` exists. +- No active `.md` files remain after PASS. +- PASS: `complete.log` written from `agent-ops/skills/common/code-review/templates/complete-log-template.md`, then task directory moved under `agent-task/archive/YYYY/MM/`. +- WARN/FAIL: new active `PLAN-{build_lane}-GNN.md` and `CODE_REVIEW-{review_lane}-GNN.md` created with matching headers and matching `구현 체크리스트`; no `complete.log`. +- The applicable review-agent-only finalization checklist was completed before reporting. diff --git a/agent-ops/skills/common/code-review/agents/openai.yaml b/agent-ops/skills/common/code-review/agents/openai.yaml new file mode 100644 index 0000000..d45d468 --- /dev/null +++ b/agent-ops/skills/common/code-review/agents/openai.yaml @@ -0,0 +1,4 @@ +interface: + display_name: "Code Review" + short_description: "Review task implementation" + default_prompt: "Use $code-review to review the active repository task and archive or continue the loop." diff --git a/agent-ops/skills/common/code-review/templates/complete-log-template.md b/agent-ops/skills/common/code-review/templates/complete-log-template.md new file mode 100644 index 0000000..a4e8fa0 --- /dev/null +++ b/agent-ops/skills/common/code-review/templates/complete-log-template.md @@ -0,0 +1,31 @@ +# Complete - {task_name} + +## 완료 일시 + +{YYYY-MM-DD or ISO-8601} + +## 요약 + +{one-line task summary, loop count, and final verdict} + +## 루프 이력 + +| Plan | Review | Verdict | 메모 | +|------|--------|---------|------| +| `plan_{build_lane}_GNN_N.log` | `code_review_{review_lane}_GNN_N.log` | PASS/WARN/FAIL | {main outcome or follow-up reason} | + +## 구현/정리 내용 + +- {implemented or cleaned-up change} + +## 최종 검증 + +- `{command}` - {PASS/FAIL/BLOCKED}; {actual output summary or saved output path} + +## 잔여 Nit + +- 없음 + +## 후속 작업 + +- 없음 diff --git a/agent-ops/skills/common/commit-push/SKILL.md b/agent-ops/skills/common/commit-push/SKILL.md new file mode 100644 index 0000000..09e8ace --- /dev/null +++ b/agent-ops/skills/common/commit-push/SKILL.md @@ -0,0 +1,124 @@ +--- +name: commit-push +version: 1.0.0 +description: 변경 사항을 커밋하고 원격에 푸시한다. "커밋해줘", "푸시해줘", "커밋하고 푸시" 요청 시 사용한다. +--- + +# commit-push + +## 목적 + +현재 변경 내용을 분석하여 한국어 커밋 메시지를 작성하고, 커밋 및 푸시를 수행한다. + +## 언제 호출할지 + +- "커밋해줘", "커밋하고 푸시해줘" 요청 시 +- "변경 사항 올려줘", "푸시해줘" 요청 시 +- 코드 작업 완료 후 "반영해줘", "올려줘" 요청 시 + +## 입력 + +- `scope`: 커밋 범위 — all(전체) 또는 특정 파일/경로 (선택, 기본값: all) +- `push`: 푸시 여부 (선택, 기본값: true) + +## 먼저 확인할 것 + +- [ ] 커밋할 변경 사항이 존재하는가 (`git status`로 확인) +- [ ] 현재 브랜치가 올바른 브랜치인가 +- [ ] `.env`, 시크릿 파일 등 민감한 파일이 스테이징에 포함되어 있지 않은가 + +## 실행 절차 + +1. **변경 사항 파악** + - `git status`로 변경/추가/삭제된 파일 목록을 확인한다 + - `git diff`로 구체적인 변경 내용을 확인한다 + - 변경 사항이 없으면 사용자에게 알리고 종료한다 + +2. **민감 파일 검사** + - 스테이징 대상에 아래 패턴이 포함되면 해당 파일을 제외하고 사용자에게 경고한다 + - `.env`, `.env.*` + - `*secret*`, `*credential*`, `*password*` + - `*.pem`, `*.key`, `*.p12` + - `.gitignore`에 이미 등록된 파일은 무시한다 + +3. **커밋 메시지 작성** + - 변경 내용을 분석하여 한국어로 커밋 메시지를 작성한다 + - 아래 메시지 규칙을 따른다 + +4. **사용자 확인** + - 커밋 메시지와 대상 파일 목록을 보여주고 승인을 받는다 + - 사용자가 메시지 수정을 요청하면 반영한다 + +5. **커밋 실행** + - scope에 따라 파일을 스테이징한다 + - all: 변경된 전체 파일을 개별적으로 `git add` + - 특정 경로: 해당 파일만 `git add` + - 승인된 메시지로 `git commit`을 실행한다 + +6. **푸시 실행** + - push가 true인 경우에만 실행한다 + - 현재 브랜치를 원격에 푸시한다 + - 원격 브랜치가 없으면 `-u origin {branch}`로 설정한다 + - 충돌 발생 시 사용자에게 알리고 중단한다 + +7. **결과 보고** + - 커밋 해시, 메시지, 변경 파일 수, 푸시 결과를 출력한다 + +## 커밋 메시지 규칙 + +### 형식 + +``` +<타입>: <변경 요약> + +<본문> (선택) +``` + +### 타입 목록 + +| 타입 | 용도 | +|------|------| +| 기능 | 새로운 기능 추가 | +| 수정 | 버그 수정 | +| 개선 | 기존 기능 개선, 리팩토링 | +| 문서 | 문서 추가/수정 | +| 설정 | 빌드, CI/CD, 설정 파일 변경 | +| 테스트 | 테스트 추가/수정 | +| 정리 | 코드 정리, 불필요 코드 제거 | + +### 작성 원칙 + +- **한국어**로 작성한다 +- 요약은 50자 이내로 간결하게 작성한다 +- 요약은 "~한다" 형태의 서술형으로 끝낸다 (예: "로그인 검증 로직을 추가한다") +- 본문은 **무엇을** 변경했는지가 아니라 **왜** 변경했는지를 쓴다 +- 변경 파일이 3개 이하이면 본문을 생략할 수 있다 +- 여러 성격의 변경이 섞여 있으면 가장 핵심적인 변경을 기준으로 타입을 정한다 + +## 출력 형식 + +``` +## 커밋 완료 + +- 브랜치: {브랜치명} +- 커밋: {해시} — {커밋 메시지 요약} +- 변경 파일: {n}개 + - {파일 경로}: {변경 유형} +- 푸시: {성공/생략/실패} +``` + +## 실행 결과 검증 + +- [ ] `git log -1`로 방금 생성한 커밋이 존재하고, 메시지가 승인된 내용과 일치하는가 +- [ ] 커밋에 포함된 파일 목록이 의도한 scope와 일치하는가 (`git diff-tree --no-commit-id --name-only -r HEAD`) +- [ ] push가 true인 경우, `git status`에서 "Your branch is up to date" 또는 원격과 동기화 상태인가 +- 검증 실패 시: 실패 원인(커밋 누락, 푸시 실패 등)을 사용자에게 알리고 재시도 여부를 확인한다 + +## 금지 사항 + +- `git add -A` 또는 `git add .`을 사용하지 않는다 (민감 파일 혼입 방지) +- `git push --force`를 사용하지 않는다 +- 사용자 확인 없이 커밋하지 않는다 +- 영어로 커밋 메시지를 작성하지 않는다 +- 변경 내용과 무관한 커밋 메시지를 작성하지 않는다 +- `--no-verify` 옵션을 사용하지 않는다 diff --git a/agent-ops/skills/common/create-domain-rule/SKILL.md b/agent-ops/skills/common/create-domain-rule/SKILL.md new file mode 100644 index 0000000..dedb8e2 --- /dev/null +++ b/agent-ops/skills/common/create-domain-rule/SKILL.md @@ -0,0 +1,113 @@ +--- +name: create-domain-rule +version: 1.0.0 +description: 새로운 도메인 rules.md 파일을 생성하기 위한 범용 스킬 +--- + +# Create Domain Rule + +## 목적 + +`agent-ops/rules/project/domain//rules.md` 파일을 올바른 형식으로 생성한다. +실제 프로젝트 폴더 구조를 분석하여 경로와 구성 요소를 자동으로 채운다. +생성 후 `rules/project/rules.md`의 도메인 룰 로딩 안내와 도메인 매핑 테이블을 갱신한다. + +## 언제 호출할지 + +- 새로운 도메인 영역의 코드를 처음 변경하기 전에 +- 기존 domain rule이 없는 경로에 대해 규칙이 필요할 때 +- 사용자가 특정 도메인의 rule 파일을 만들어 달라고 요청할 때 +- agent-ops 초기 scaffold 이후 도메인을 추가 확장할 때 + +## 입력 + +- `domain-name`: 생성할 도메인 이름, kebab-case (필수) +- `domain-type`: `core` / `supporting` / `generic` 중 하나 (필수) +- `path-hints`: 이 도메인에 해당하는 경로 힌트 목록 (선택, 없으면 자동 탐색) + +## 먼저 확인할 것 + +- [ ] `agent-ops/rules/project/domain//rules.md` 가 이미 존재하는지 확인 +- [ ] `agent-ops/rules/common/_templates/domain-rule-template.md` 를 읽어 최신 템플릿 형식 파악 +- [ ] `agent-ops/rules/project/domain/` 하위 기존 domain rule 목록을 확인하여 중복·유사 도메인 여부 판단 + +## 실행 절차 + +1. **중복 확인** + - `agent-ops/rules/project/domain//` 폴더 존재 여부 확인 + - 책임이 겹치는 기존 domain rule 이 있으면 사용자에게 알리고 중단한다 + +2. **경로 탐색** + - `path-hints` 가 제공된 경우: 해당 경로를 기준으로 폴더 구조 확인 + - `path-hints` 가 없는 경우: `domain-name` 과 연관된 폴더명·파일명으로 프로젝트를 탐색 + - 실제로 존재하는 경로만 포함 경로에 기재한다 + - 존재하지 않는 경로는 기재하지 않는다 + +3. **도메인 분석** + - 탐색된 경로의 주요 파일·모듈을 읽어 아래를 도출한다 + - 도메인의 목적 / 책임 한 줄 요약 + - 포함 경로 목록 + - 제외 경로 (인접 도메인과 겹칠 수 있는 경로) + - 주요 구성 요소 (파일, 모듈, 클래스, 함수 등) + - 유지할 패턴 (네이밍 규칙, 아키텍처 패턴 등) + - 다른 도메인과의 경계 + - 금지 사항 + +4. **도메인 rules.md 생성** + - 경로: `agent-ops/rules/project/domain//rules.md` + - `domain-rule-template.md` 형식을 따른다 + - 실제 코드에서 확인된 내용만 기재한다 + - 불확실한 항목은 `` 주석으로 남긴다 + +5. **rules/project/rules.md 업데이트** + - `agent-ops/rules/project/rules.md` 를 읽는다 + - `## 도메인 룰 로딩` 섹션이 없으면 `## 도메인 매핑` 바로 위에 아래 형식으로 추가한다 + ``` + ## 도메인 룰 로딩 + + - 아래 도메인 매핑에 해당하는 작업에서 해당 domain 최초 진입 시 domain rule을 1회 읽는다. + - 이미 읽은 domain rule은 같은 세션에서 반복해서 읽지 않는다. + ``` + - 도메인 매핑 테이블에 아래 형식으로 추가한다 + ``` + | `` | | `agent-ops/rules/project/domain//rules.md` | + ``` + - 경로 패턴은 실제 포함 경로를 기준으로 작성한다 + - 기존 로딩 섹션과 테이블 항목을 삭제하거나 재정렬하지 않는다 + +6. **결과 보고** + - 생성한 파일 경로 + - rules/project/rules.md 에 추가한 항목 + - 불확실하여 TODO로 남긴 항목 (해당 시) + +## 출력 형식 + +``` +## 생성 완료 + +- Domain Rule 경로: agent-ops/rules/project/domain//rules.md +- 도메인 유형: +- 도메인 룰 로딩 섹션: <추가함 | 이미 존재함> +- rules/project/rules.md 추가 경로 패턴: + +## TODO 항목 (확인 필요) +- <불확실하여 직접 확인이 필요한 항목> (해당 시) +``` + +## 실행 결과 검증 + +- [ ] `agent-ops/rules/project/domain//rules.md` 파일이 생성되었는가 +- [ ] 생성된 파일이 `domain-rule-template.md`의 모든 섹션(목적, 포함 경로, 제외 경로, 주요 구성 요소, 유지할 패턴, 경계, 금지 사항)을 포함하는가 +- [ ] `rules/project/rules.md`에 도메인 룰 로딩 섹션이 존재하는가 +- [ ] `rules/project/rules.md`의 도메인 매핑 테이블에 해당 도메인 항목이 추가되었는가 +- [ ] 포함 경로에 기재된 경로가 실제로 프로젝트에 존재하는가 +- 검증 실패 시: 누락된 섹션 또는 잘못된 경로를 사용자에게 알리고 해당 부분만 보완한다 + +## 금지 사항 + +- 이미 존재하는 rules.md 를 덮어쓰지 않는다 +- 실제 존재하지 않는 경로를 포함 경로에 넣지 않는다 +- 추측으로 패턴이나 금지 사항을 채우지 않는다 — 확인된 내용만 기재한다 +- rules/project/rules.md 의 기존 로딩 섹션과 테이블 항목을 삭제하거나 재정렬하지 않는다 +- 하나의 domain rule 에 여러 독립 도메인의 책임을 묶지 않는다 +- 코드 변경을 수행하지 않는다 — 이 skill 은 rule 파일 생성만 담당한다 diff --git a/agent-ops/skills/common/create-skill/SKILL.md b/agent-ops/skills/common/create-skill/SKILL.md new file mode 100644 index 0000000..07bb36c --- /dev/null +++ b/agent-ops/skills/common/create-skill/SKILL.md @@ -0,0 +1,95 @@ +--- +name: create-skill +version: 1.0.0 +description: 새로운 SKILL.md 파일을 생성하기 위한 범용 스킬 +--- + +# Create Skill + +## 목적 + +`agent-ops/skills/` 하위에 올바른 형식의 SKILL.md 파일을 생성한다. +기존 skill-template.md 를 기반으로, 요청 목적에 맞는 내용을 채워 넣는다. +생성 후 라우팅 항목을 추가한다. + +### 생성 위치 결정 +- `.agent-ops-source` 파일이 **있으면** (공통 관리 레포): `agent-ops/skills/common//SKILL.md` +- `.agent-ops-source` 파일이 **없으면** (타겟 프로젝트): `agent-ops/skills/project//SKILL.md` + +## 언제 호출할지 + +- 새로운 반복 작업 패턴이 생겨 skill로 정의해야 할 때 +- 기존 skill이 없는 작업 유형을 처음 수행하기 전에 +- 사용자가 특정 작업을 skill로 만들어 달라고 요청할 때 + +## 입력 + +- `skill-name`: 생성할 skill 이름, kebab-case (필수) +- `purpose`: 이 skill이 해결하는 문제 한 줄 요약 (필수) +- `trigger-cases`: 이 skill을 호출해야 하는 상황 목록 (선택) + +## 먼저 확인할 것 + +- [ ] `agent-ops/skills/common/` 및 `agent-ops/skills/project/` 하위에 동일 이름의 디렉터리가 이미 존재하는지 확인 +- [ ] `agent-ops/rules/common/rules.md` 및 `agent-ops/rules/project/rules.md` 에 이미 유사한 라우팅 항목이 있는지 확인 +- [ ] `agent-ops/skills/common/_templates/skill-template.md` 를 읽어 최신 템플릿 형식 파악 + +## 실행 절차 + +1. **중복 확인** + - `agent-ops/skills/common//` 및 `agent-ops/skills/project//` 폴더 존재 여부 확인 + - 기능이 겹치는 기존 skill이 있으면 사용자에게 알리고 중단한다 + +2. **목적 분석** + - `purpose` 와 `trigger-cases` 를 바탕으로 아래 항목을 도출한다 + - 언제 호출할지 (2~4개) + - 필요한 입력 파라미터 + - 사전 확인 항목 + - 실행 절차 (3~7단계) + - 출력 형식 + - 금지 사항 + +3. **SKILL.md 생성** + - 경로: 생성 위치 결정 규칙에 따라 `common/` 또는 `project/` 하위에 생성 + - `skill-template.md` 형식을 따른다 + - 프로젝트 특화 내용보다 범용 절차를 우선한다 + - 절차는 구체적이되 지나치게 세부 구현을 기술하지 않는다 + +4. **라우팅 업데이트** + - `.agent-ops-source` 마커가 **있으면** (공통 관리 레포): `agent-ops/rules/common/rules.md`에 라우팅 항목 추가 + - `.agent-ops-source` 마커가 **없으면** (타겟 프로젝트): `agent-ops/rules/project/rules.md`의 프로젝트 스킬 라우터 섹션에 라우팅 항목 추가 + - 이 skill이 속할 라우팅 축(구조 분석/코드 변경/흐름 추적 등)을 판단한다 + - 기존 라우팅 구조를 깨지 않는다 + +5. **결과 보고** + - 생성한 파일 경로 + - 라우팅 항목을 추가한 파일과 내용 + - 이 skill이 다루지 않는 범위(필요 시) + +## 출력 형식 + +``` +## 생성 완료 + +- SKILL 경로: agent-ops/skills/{common|project}//SKILL.md +- 라우팅 추가: <대상 파일> → <라우팅 축> → + +## 주의사항 (해당 시) +- <이 skill이 다루지 않는 범위 또는 주의할 점> +``` + +## 실행 결과 검증 + +- [ ] `agent-ops/skills/{common|project}//SKILL.md` 파일이 생성되었는가 +- [ ] 생성된 파일이 `skill-template.md`의 필수 섹션(목적, 언제 호출할지, 실행 절차, 실행 결과 검증, 출력 형식, 금지 사항)을 포함하는가 +- [ ] frontmatter에 name, version, description이 올바르게 기재되었는가 +- [ ] 라우팅 대상 파일에 해당 스킬의 라우팅 항목이 추가되었는가 +- 검증 실패 시: 누락된 섹션 또는 라우팅 항목을 사용자에게 알리고 해당 부분만 보완한다 + +## 금지 사항 + +- 이미 존재하는 skill 을 덮어쓰지 않는다 +- 프로젝트 특화 경로(예: `app/screens/`)를 skill 본문에 하드코딩하지 않는다 +- skill 생성과 무관한 코드 파일을 수정하지 않는다 +- 라우팅 대상 파일의 기존 항목을 삭제하거나 재정렬하지 않는다 +- 하나의 skill 에 여러 독립적인 책임을 묶지 않는다 diff --git a/agent-ops/skills/common/init-agent-ops/SKILL.md b/agent-ops/skills/common/init-agent-ops/SKILL.md new file mode 100644 index 0000000..af2b836 --- /dev/null +++ b/agent-ops/skills/common/init-agent-ops/SKILL.md @@ -0,0 +1,243 @@ +--- +name: init-agent-ops +version: 1.1.1 +description: 프로젝트 상태를 판별하고 agent-ops 기본 스캐폴드를 생성하기 위한 초기 규칙 +--- + +# init-agent-ops + +## 목적 + +프로젝트에 Agent-Ops 구조가 없거나 불완전할 때, +현재 프로젝트 상태를 분석하여 다음을 세팅한다. + +1. 에이전트 진입 파일 +2. AI ignore / permission 기본 설정 +3. agent-ops 기본 폴더 구조 +4. rules/project/rules.md (프로젝트 특화 규칙, 분석 후 생성) +5. 초기 domain rule 초안 +6. 초기 skill (필요 시) + +"현재 프로젝트에 맞는 최소 스캐폴드" 생성을 우선한다. + +## 언제 호출할지 + +- 프로젝트에 agent-ops 구조가 없을 때 +- agent-ops 구조가 불완전하여 재설정이 필요할 때 +- 사용자가 "agent-ops 초기화해줘", "에이전트 설정해줘" 요청 시 + +## 입력 + +- `project-type`: 신규 / 운영중 (선택, 미지정 시 자동 판별) + +## 먼저 확인할 것 + +- [ ] 프로젝트 루트에 기존 agent-ops 관련 파일(`CLAUDE.md`, `agent-ops/` 등)이 있는지 확인 +- [ ] `.agent-ops-source` 파일이 있는지 확인 (공통 관리 레포 여부) +- [ ] 기존 진입 파일(`CLAUDE.md`, `GEMINI.md` 등)이 있는지 확인 +- [ ] 기존 AI ignore / permission 파일(`.geminiignore`, `.aiexclude`, `.claude/settings.json`, `opencode.json` 등)이 있는지 확인 + +## 핵심 원칙 + +- 기존 구조를 우선한다. +- 새 파일 생성은 꼭 필요한 최소 범위로 제한한다. +- 도메인은 발명하지 말고 현재 구조에서 발견한다. +- 처음에는 핵심 도메인만 생성한다. +- 반복되는 작업만 초기 skill로 만든다. +- 불확실한 내용은 후보로 제시한다. + +## 상태 판별 + +### 신규 프로젝트 +- 코드/폴더 구조가 단순하다 +- 도메인 경계가 아직 약하다 +- Agent-Ops 관련 파일이 없다 + +### 운영중 프로젝트 +- 모듈/폴더/패키지 경계가 보인다 +- 반복 작업이 드러난다 +- 핵심 책임 경계가 식별된다 + +## 생성 대상 + +| 파일 | 방법 | +|------|------| +| `GEMINI.md`, `CLAUDE.md`, `AGENTS.md`, `.cursorrules`, `.clinerules` 등 진입 파일 | `rules/common/rules.md`를 `agent-ops/bin/entry-files.sh`의 파일 목록으로 프로젝트 루트에 복사 | +| `agent-ops/.version` | 공통 관리 레포의 VERSION 파일을 그대로 복사 (프레임워크 버전 추적용) | +| `agent-ops/bin/` | 공통 스크립트 전체 복사 (진입 파일 목록의 단일 기준인 `entry-files.sh` 포함) | +| `agent-ops/rules/common/` | 공통 폴더 전체 복사 (수정 금지) | +| `agent-ops/skills/common/` | 공통 폴더 전체 복사 (수정 금지) | +| `agent-ops/rules/project/rules.md` | 프로젝트 분석 후 생성 | +| `agent-ops/rules/project/domain//rules.md` | 도메인 분석 후 생성 | +| `agent-ops/rules/private/` | 폴더만 생성 (내용은 개인이 작성) | +| `.gitignore`에 `agent-ops/rules/private/` 추가 | git 추적 제외 | +| `.geminiignore`, `.aiexclude`, `.cursorignore`, `.clineignore` | `agent-task/archive/**` 한 줄 추가 | +| `.claude/settings.json`, `opencode.json` | 파일이 없으면 `agent-task/archive/**` 읽기/검색 제외 설정 생성, 있으면 덮어쓰지 않고 수동 병합 안내 | + +에이전트별 파일명: + +실제 생성/동기화 대상 목록은 `agent-ops/bin/entry-files.sh`의 `AGENT_OPS_ENTRY_FILES`를 단일 기준으로 사용한다. + +| 에이전트 | 파일명 | +|---------|--------| +| Gemini | `GEMINI.md` | +| Claude | `CLAUDE.md` | +| Kilo Code / OpenCode | `AGENTS.md` | +| Cursor | `.cursorrules` | +| Cline | `.clinerules` | + +## 에이전트 진입 파일 원칙 + +진입 파일은 `rules/common/rules.md` 내용 그대로를 에이전트별 파일명으로 복사한다. +별도 내용을 추가하거나 수정하지 않는다. + +## Rule 구조 원칙 + +### rules/common/rules.md +- 공통 관리 레포에서 제공. 프로젝트에서 직접 수정하지 않는다. +- 실제 사용은 진입점 파일들에서 사용된다. + +포함 항목: +- 기본 원칙 +- 공통 스킬 라우팅 (router.md 참조) +- project/rules.md 로드 지시 + +### rules/project/rules.md +init-agent-ops가 프로젝트를 분석하여 생성한다. + +포함 항목: +- 응답 언어 +- 프로젝트 개요 / 주요 구조 +- 기술 스택 +- 프로젝트 특화 컨벤션 +- 도메인 매핑 테이블 (경로 패턴 → domain rules.md) +- 프로젝트 스킬 라우팅 (해당 시) + +common/rules.md와 내용이 중복되지 않도록 한다. + +#### 도메인 매핑 테이블 형식 + +```markdown +## 도메인 매핑 + +| 경로 패턴 | 도메인 | rules.md | +|----------|--------|----------| +| `src/order/**` | order | `agent-ops/rules/project/domain/order/rules.md` | +| `src/payment/**` | payment | `agent-ops/rules/project/domain/payment/rules.md` | +| `src/common/**` | common | `agent-ops/rules/project/domain/common/rules.md` | +``` + +#### 프로젝트 스킬 라우팅 형식 + +```markdown +## 스킬 라우팅 + +| 요청 키워드 | SKILL.md | +|------------|----------| +| 테스트 실행해줘 | `agent-ops/skills/project/run-test/SKILL.md` | +``` + +### domain rule +각 도메인 rules.md에는 아래만 둔다. + +- 목적 / 책임 +- 포함 경로 +- 제외 경로 +- 주요 구성 요소 +- 유지할 패턴 +- 다른 도메인과의 경계 +- 금지 사항 + +## DDD 기준 + +- **Core Domain**: 핵심 가치와 주요 유즈케이스 +- **Supporting Domain**: 핵심 도메인을 지원 +- **Generic / Common**: 여러 도메인이 공통으로 사용 + +도메인은 실제 폴더, 모듈, 패키지, 책임 경계와 연결되어야 한다. + +## 실행 (Execution) + +지정한 대상 디렉토리에 agent-ops 스캐폴드를 생성한다. + +```bash +./agent-ops/bin/init-agent-ops.sh +``` + +## 실행 절차 + +1. **상태 판별** + - 프로젝트 구조, 모듈 경계, agent-ops 파일 유무를 분석하여 신규/운영중을 판별한다 + +2. **에이전트 진입 파일 생성** + - `rules/common/rules.md`를 `agent-ops/bin/entry-files.sh`의 파일 목록으로 프로젝트 루트에 복사한다 + +3. **AI ignore / permission 기본 설정** + - `.geminiignore`, `.aiexclude`, `.cursorignore`, `.clineignore`에 `agent-task/archive/**`를 추가한다 + - `.claude/settings.json`, `opencode.json`이 없으면 archive 읽기/검색 제외 설정을 생성한다 + - `agent-task/archive/**` 제외는 `.gitignore`에 추가하지 않는다 + +4. **agent-ops 폴더 구조 복사** + - 공통 관리 레포의 `agent-ops/` 공통 폴더(bin, rules/common, skills/common)를 복사한다 + - `agent-ops/.version` 파일을 복사한다 + +5. **rules/project/rules.md 생성** + - 프로젝트를 분석하여 응답 언어, 프로젝트 개요, 기술 스택 등을 채운다 + - common/rules.md와 내용이 중복되지 않도록 한다 + +6. **도메인 분석 및 domain rule 생성** + - 신규 프로젝트: 핵심 domain placeholder 2~4개만 제안한다. domain rules를 과도하게 채우지 않는다 + - 운영중 프로젝트: Core/Supporting/Generic 도메인을 식별하고 실제 경로를 반영한 초안을 생성한다 + +7. **초기 skill 제안** + - 신규 프로젝트: 최소 2개만 제안한다 + - 운영중 프로젝트: 반복 작업 기반으로 필요한 skill을 제안한다 + +8. **결과 보고** + - 상태 판별 결과, 생성된 파일 목록, 도메인 제안, skill 제안, 주의사항을 출력한다 + +## 출력 형식 + +### 상태 판별 +- Agent-Ops 상태: 없음 / 부분 적용 / 운영중 +- 프로젝트 상태: 신규 / 운영중 +- 근거: 짧게 요약 + +### 스캐폴드 계획 +- 생성할 파일 +- 바로 채울 파일 +- placeholder로 둘 파일 + +### 도메인 제안 +- Core Domain +- Supporting Domain +- Generic / Common + +### 초기 skill 제안 +- skill 이름 +- 필요한 이유 + +### 주의사항 +- 지금 만들지 말아야 할 것 +- 아직 확정하면 안 되는 것 + +## 실행 결과 검증 + +- [ ] `agent-ops/bin/entry-files.sh`의 모든 진입 파일이 프로젝트 루트에 존재하고, 내용이 초기화에 사용한 `rules/common/rules.md`와 일치하는가 +- [ ] `agent-ops/rules/project/rules.md`가 생성되었고, 필수 항목(응답 언어, 프로젝트 개요, 기술 스택)이 포함되어 있는가 +- [ ] 생성된 domain rules.md가 `domain-rule-template.md` 형식을 따르는가 +- [ ] `rules/project/rules.md`의 도메인 매핑 테이블에 생성된 도메인이 모두 등록되어 있는가 +- [ ] `.gitignore`에 `agent-ops/rules/private/` 항목이 추가되어 있는가 +- [ ] `.geminiignore`, `.aiexclude`, `.cursorignore`, `.clineignore`에 `agent-task/archive/**`가 포함되어 있는가 +- [ ] `.claude/settings.json`, `opencode.json`에 `agent-task/archive/**` 제외 설정이 있거나, 기존 파일 수동 병합 안내를 출력했는가 +- [ ] `.gitignore`에 `agent-task/archive/**`를 추가하지 않았는가 +- 검증 실패 시: 누락된 파일/항목을 사용자에게 알리고 해당 부분만 보완한다 + +## 금지 사항 + +- 진입 파일에 rules/common/rules.md 외 내용을 추가하지 않는다. +- `agent-task/archive/**` 제외를 `.gitignore`에 추가하지 않는다. +- 실제 구조보다 앞선 추상 구조를 강요하지 않는다. +- 처음부터 많은 domain / skill을 만들지 않는다. +- rules/common/rules.md를 프로젝트에서 직접 수정하지 않는다. +- rules/project/rules.md에 rules/common/rules.md와 중복되는 내용을 넣지 않는다. diff --git a/agent-ops/skills/common/plan/SKILL.md b/agent-ops/skills/common/plan/SKILL.md new file mode 100644 index 0000000..897de37 --- /dev/null +++ b/agent-ops/skills/common/plan/SKILL.md @@ -0,0 +1,317 @@ +--- +name: plan +description: Analyze the current repository and write a detailed PLAN-{build_lane}-GNN.md for implementation work. Also writes the CODE_REVIEW-{review_lane}-GNN.md stub that the implementing agent will fill in after coding. Use for any feature, refactor, bug fix, or follow-up fix that should enter the plan-code-review loop. A separate implementing agent, or the same agent in an implementation pass, reads the plan file and does the coding. The code-review skill archives both files after review and moves PASS tasks under agent-task/archive/YYYY/MM/. +--- + +# Plan + +## Purpose + +Create the planning artifacts for the implementation loop: + +```text +plan skill -> PLAN-{build_lane}-GNN.md + CODE_REVIEW-{review_lane}-GNN.md stub +implementation -> code changes + filled CODE_REVIEW-{review_lane}-GNN.md +code-review skill -> verdict + archive, complete.log, and PASS task-directory archive move or new follow-up plan/review files +``` + +## Workflow Contract + +This skill intentionally uses routed active files under `agent-task/{task_name}/` as the state protocol. Do not change this filename contract unless the paired code-review skill is updated together. + +Filename rules: + +- Plan file: `PLAN-{build_lane}-GNN.md` +- Review stub: `CODE_REVIEW-{review_lane}-GNN.md` +- `{lane}` is only `local` or `cloud`; never put model names in filenames. +- `GNN` is a two-digit capability grade from `G01` to `G10`; the runtime maps lane+grade to current models externally. + +Task directory naming rules: + +- A single-plan task uses `agent-task/{task_name}/` with a short snake_case task name. +- If one plan is not enough, split the work into multiple task directories. Each directory owns exactly one normal active plan file and one normal active review stub. +- Multi-plan output is a set of independent `PLAN-{build_lane}-GNN.md` + `CODE_REVIEW-{review_lane}-GNN.md` pairs across multiple folders, not multiple plan files inside one folder. +- Multi-plan task directory names must start with an execution-order index: `01_{task_name}`, `02_{task_name}`, `03_{task_name}`, and so on. +- If a later task cannot start until a specific current task is complete, mark that dependency immediately after the current task's index with `+`, not `_`: `01+{dependent_task_name}` means this task depends on completion of the `01_...` task. +- When multiple dependent tasks share the same predecessor, order them after the dependency marker: `01+01_{task_name}`, `01+02_{task_name}`. +- Directory names are runtime scheduling metadata. Preserve them verbatim; do not normalize, reinterpret, or choose execution order by agent judgment. +- Every split plan must include `의존 관계 및 구현 순서` and list predecessor task directories that must reach `complete.log` before implementation begins. + +Routing rules: + +- Build defaults to `local` when the plan is explicit, tests are runnable, and failure is review-detectable. +- Use `cloud` for weak tests, broad API/call-site impact, ambiguity, storage/concurrency/protocol/auth risk, or prior local failure. +- Use `cloud-G07` or higher for terminal-agent work: shell/CLI workflow implementation, bin script orchestration, process control, stdout/stderr parsing, exit-status contracts, long-running command diagnosis, or terminal benchmark-style tasks. Merely running deterministic tests such as `go test` does not make a task terminal-agent work. +- Use `cloud-G07` or higher when acceptance depends on a real interactive external tool, TUI, PTY, browser, screen repaint/cursor stream, or bin-level smoke output. This is mandatory when unit tests pass but the real smoke/integration command fails. +- Use `cloud-G07` or higher when a prior review found verification trust failure, reconstructed command output, or claimed stdout/stderr that does not match a rerun. +- Keep high-risk design judgment on `cloud` with a higher grade; the runtime may map `cloud-GNN` to frontier-class models. +- Review may be `local` for narrow/low-risk checks and `cloud` for multi-file/API/test-meaning reviews, security/auth, storage/migration, concurrency, protocol/schema, cross-domain, or repeated Required issues. +- Raise `GNN` with scope, ambiguity, missing tests, irreversible behavior, and blast radius; keep grade model-independent. + +Directory states: + +| State | Meaning | +|-------|---------| +| `PLAN-*-G??.md` only | Invalid; plan skill always writes both active files | +| `PLAN-*-G??.md` + `CODE_REVIEW-*-G??.md` stub | Implementation is pending/in progress | +| `PLAN-*-G??.md` + filled `CODE_REVIEW-*-G??.md` | Ready for code-review skill | +| `complete.log` + `*.log` files | Task complete (PASS), before final task-directory archive move | +| `agent-task/archive/YYYY/MM/{task_name}/complete.log` + `*.log` files | Archived completed task (PASS); not active | +| Only `*.log` files (no `complete.log`) | Task terminated mid-loop or abandoned | + +## Step 1 - Determine Task + +If the user names the task explicitly, use that task name. + +Otherwise, glob `agent-task/*/PLAN-*-G??.md`: + +| Result | Action | +|--------|--------| +| Exactly one | Continue that task | +| None | Create a new task only for a feature/refactor/fix/follow-up that belongs in this workflow | +| Multiple | If the user/runtime named a task directory, use that directory. Otherwise list paths and ask which task to use; do not choose by agent judgment. | + +The routed plan file is the loop entry point. A missing active plan normally means only that no plan has been started for a new task; do not create task files for casual analysis, status, or review requests unless the user explicitly asks for a plan. + +Use short snake_case task names, e.g. `api_refactor`. + +When the work must be split into multiple plans, choose directory names using the task directory naming rules above. Do not put multiple active plan files in one task directory. + +## Step 2 - Analyze Before Writing + +Complete all items below before creating any files. Work through them in order; do not proceed to the next step until every checkbox is done. + +- [ ] **Read all source files in full** — read every source file the change will touch, whole file. No partial reads. +- [ ] **Read all test files in full** — read every test file that exercises the changed behavior. +- [ ] **Assess test coverage** — for each behavior change, explicitly record whether existing tests cover it. +- [ ] **Grep all symbol references** — for any renamed or removed symbol, find every call site and import chain. +- [ ] **Check dependency manifests** — before adding any new package, verify its presence in go.mod / package manifest. +- [ ] **Pre-check compile issues** — identify missing interface implementations, type mismatches, and broken imports. +- [ ] **Verify verification commands** — confirm that the final verification commands actually run in this repository layout. +- [ ] **Stabilize fragile verification** — for search or generated-output checks, choose deterministic commands up front, such as `rg --sort path`, and decide whether cached test output is acceptable or `-count=1` is required. + +## Step 3 - Determine GXX Grade + +GXX is an output of analysis, not an input. Determine lane and grade only after Step 2 is fully complete. + +- [ ] **Assess change scope** — count affected files, interface impact, and call-site count. +- [ ] **Check risk factors** — mark any that apply: concurrency, storage/migration, protocol/schema, auth, irreversible behavior. +- [ ] **Evaluate test confidence** — judge whether existing tests sufficiently verify the changed behavior. +- [ ] **Decide lane** — use `local` if the plan is explicit and tests are sufficient; use `cloud` if any of the following apply: + - Tests are weak or do not cover the changed behavior + - Broad API/call-site impact + - Concurrency, storage, protocol, or auth risk + - Prior local build failure on this task + - Terminal-agent work: shell/CLI workflow implementation, bin script orchestration, process control, stdout/stderr parsing, exit-status contracts, long-running command diagnosis, or terminal benchmark-style tasks + - Real bin/smoke/integration verification failed after unit tests passed + - Interactive TUI/PTY/browser/external CLI automation or screen-rendered output is part of the success condition + - A prior review marked verification trust Fail/Warn because recorded command output did not match a rerun +- [ ] **Decide GNN** — assign a higher grade as scope, ambiguity, irreversibility, and blast radius increase. Grade is complexity-based, not model-name-based. + +## Step 4 - Archive Existing Active Files + +Before writing new active files for the chosen task: + +- Count existing `agent-task/{task_name}/plan_*.log`; call it `N`. If `PLAN-*-G??.md` exists, rename `PLAN-{build_lane}-GNN.md` to `plan_{build_lane}_GNN_N.log`. +- Count existing `agent-task/{task_name}/code_review_*.log`; call it `M`. If `CODE_REVIEW-*-G??.md` exists, rename `CODE_REVIEW-{review_lane}-GNN.md` to `code_review_{review_lane}_GNN_M.log`. + +The new plan number is the count of `plan_*.log` after archiving. + +## Step 5 - Write Plan File + +Header line must be exactly: + +```markdown + +``` + +Required sections: + +- Title. +- `이 파일을 읽는 구현 에이전트에게`: open with a bold warning that filling in implementation-owned sections of `CODE_REVIEW-*-G??.md` is a mandatory final step — the task is NOT complete until every implementation-owned section of that file is filled. Tell the implementer to work from the implementation checklist, complete every checklist item in both the plan and review stub, run intermediate/final verification, and fill implementation-owned `CODE_REVIEW-*-G??.md` sections with actual implementation notes and command output. Explicitly state that the implementing agent must NOT execute the archiving instructions in the review file's `이 파일을 읽는 리뷰 에이전트에게` section and must NOT modify or check the `코드리뷰 전용 체크리스트` — those instructions/checklists are for the code-review skill only. +- `배경`: 2-4 sentences explaining why the work is needed. +- `분석 결과`: record the findings from Step 2 and Step 3. This section is the written output of the analysis — not a summary, but the actual findings that justify the plan's scope and decisions. Must include all of the following subsections: + - `읽은 파일`: list every source and test file read during analysis, with path. + - `테스트 커버리지 공백`: list each behavior change and whether existing tests cover it; explicitly note gaps. + - `심볼 참조`: list renamed/removed symbols and every call site found, or state "none" if no symbols were changed. + - `범위 결정 근거`: state which files or areas were explicitly excluded from this change and why. This is the boundary justification — the implementing agent must not silently expand scope beyond what is recorded here. + - `빌드 등급`: state the decided lane and GNN grade with a one-line rationale. +- `구현 체크리스트`: a top-level checklist the implementing agent must follow while coding. Include one item per plan item, one item for all intermediate/final verification, and make the final item exactly: `- [ ] CODE_REVIEW-*-G??.md의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채운다. 이 항목이 완료되기 전에는 구현이 완료된 것이 아니다.` Copy this checklist into the review stub's `구현 체크리스트` section with the same item text and order. +- One item per change: `### [TAG-1] Title`, `TAG-2`, etc. +- `수정 파일 요약`: table mapping files to item ids. +- `최종 검증`: runnable commands and expected outcome. Commands must be exact and deterministic enough for the reviewer to rerun; use stable ordering for searches and state whether cached test output is acceptable. The final line of this section must read exactly — **"모든 코드 변경 완료 후 반드시 `CODE_REVIEW-*-G??.md`의 구현 에이전트 소유 섹션을 채운다. 이 파일 작성이 구현의 마지막 단계다."** + +Each plan item must include: + +- `문제`: concrete problem with file:line references. +- `해결 방법`: exact approach and before/after code block for non-trivial changes. +- `수정 파일 및 체크리스트`: exhaustive file-level checklist. +- `테스트 작성`: explicit write/skip decision. If writing tests, include path, test name, assertion goal, and fixtures. If skipping, justify. +- `중간 검증`: runnable commands and expected result. + +Include `의존 관계 및 구현 순서` only when order matters. + +For split multi-plan work, `의존 관계 및 구현 순서` is mandatory in every generated plan. If a plan has a `NN+...` directory name, state which `NN_...` predecessor must produce `complete.log` before implementation starts. + +Quality rules: + +- Exact line numbers in every Before snippet. +- Use the language's required override annotation/keyword wherever an abstract method is implemented. +- Include full import statements for new packages. +- List all call sites for renamed/removed symbols. +- Never write "add tests as needed"; decide up front. +- Do not cite files you did not read. +- Be concise. Write the minimum words needed to convey the decision or fact. No preamble, no restatement of context already in the plan, no closing summaries. + +Test policy: + +| Change | Test requirement | +|--------|------------------| +| Bug fix | Regression test required | +| New public API | Normal + boundary tests required | +| API rename | Existing test call-site updates usually enough | +| Internal refactor | Existing tests may be enough | +| Concurrency logic | Race/ordering test recommended | + +Verification fidelity rules: + +- Plan verification commands are a contract. The implementing agent must run them exactly as written. +- If a command must be changed, the implementing agent must record the replacement command and reason in `계획 대비 변경 사항`, then paste the replacement command's actual stdout/stderr. +- Before claiming a tool is unavailable, run and record `command -v ` or the project-equivalent check. +- Do not download, generate, or leave verification tools inside the repository. Temporary tools belong outside the repo, such as under `/tmp`, and must not become task artifacts. +- For search commands whose output order may vary, specify deterministic options in the plan, for example `rg --sort path`. +- `검증 결과` must contain actual stdout/stderr, not summarized or reconstructed output. If output is too long, record the saved output file path and the exact command used to create it. +- If the plan's pass condition says all leftovers must be intentional exceptions, any `변경 필요` item forces FAIL until resolved or explicitly reclassified with evidence. +- Decide in the plan whether Go test cache output is acceptable. If fresh execution matters, use `go test -count=1 ...`. + +## Step 6 - Write Review Stub + +Use the template below exactly. Fill `{…}` placeholders from the plan; everything else is fixed and must not be changed by the implementing agent. + +```markdown + + +# Code Review Reference - {TAG} + +> **[IMPLEMENTING AGENT — READ FIRST] Filling in this file is the mandatory final step of implementation.** +> The task is NOT complete until every implementation-owned section below is filled in. +> Complete the `구현 체크리스트`; the final checklist item is mandatory before saving. +> Do not modify or check the `코드리뷰 전용 체크리스트`; it is owned by the review agent only. +> Follow the ownership table at the bottom of this file for which sections you own. + +## 개요 + +date={YYYY-MM-DD} +task={task_name}, plan={N}, tag={TAG} + +## 이 파일을 읽는 리뷰 에이전트에게 + +각 항목의 구현을 실제 소스 파일과 대조하고, `검증 결과` 섹션의 출력이 코드와 일치하는지 확인하세요. +review 완료 후 반드시 아래 순서로 아카이브하세요. + +1. `CODE_REVIEW-{review_lane}-GNN.md` → `code_review_{review_lane}_GNN_N.log` (N = 기존 code_review_*.log 수) +2. `PLAN-{build_lane}-GNN.md` → `plan_{build_lane}_GNN_M.log` (M = 기존 plan_*.log 수) +3. PASS인 경우 `complete.log` 작성 후 `agent-task/archive/YYYY/MM/{task_name}/`로 task 디렉터리 이동. WARN/FAIL인 경우 새 routed plan + review 스텁 작성. + +어떤 판정에서도 아카이브를 건너뛰지 마세요. PASS/WARN/FAIL 모두 `코드리뷰 결과` append 후 active plan/review 파일을 먼저 아카이브하고, 그 다음 `complete.log` 또는 다음 plan/review 파일을 작성해야 합니다. +PASS에서는 `agent-ops/skills/common/code-review/templates/complete-log-template.md`의 섹션 순서와 필수 항목을 기준으로 `complete.log`를 작성하세요. 작성 후 현재 날짜의 `YYYY/MM` 기준으로 task 디렉터리를 `agent-task/archive/YYYY/MM/{task_name}/`로 이동하고, 최종 archive 경로의 `code_review_*.log`에서 `코드리뷰 전용 체크리스트`를 갱신한 다음 보고하세요. +WARN/FAIL에서는 다음 상태 파일 작성 후 현재 task 경로의 archived `code_review_*.log`에서 적용 가능한 `코드리뷰 전용 체크리스트` 항목을 체크한 다음 보고하세요. + +--- + +## 구현 항목별 완료 여부 + +| 항목 | 완료 여부 | +|------|---------| +| [{TAG}-1] {item description} | [ ] | +| [{TAG}-2] {item description} | [ ] | + +## 구현 체크리스트 + +{copy the plan's 구현 체크리스트 items exactly, preserving order and checkbox text} + +## 코드리뷰 전용 체크리스트 + +> **[REVIEW AGENT ONLY]** 이 체크리스트는 코드리뷰 에이전트만 사용한다. +> 구현 에이전트는 이 섹션을 수정하거나 체크하지 않는다. + +- [ ] `코드리뷰 결과`에 `PASS`, `WARN`, `FAIL` 중 하나의 판정을 append한다. +- [ ] 판정과 `차원별 평가`, Required/Suggested/Nit 분류가 서로 일치한다. +- [ ] active `CODE_REVIEW-*-G??.md`를 `code_review_{review_lane}_GNN_N.log`로 아카이브한다. +- [ ] active `PLAN-*-G??.md`를 `plan_{build_lane}_GNN_M.log`로 아카이브한다. +- [ ] PASS이면 `agent-ops/skills/common/code-review/templates/complete-log-template.md` 기준으로 `complete.log`를 작성하고 active `.md` 파일을 남기지 않는다. +- [ ] PASS이면 `agent-task/{task_name}/`를 `agent-task/archive/YYYY/MM/{task_name}/`로 이동하고 최종 archive 경로에서 이 체크리스트를 갱신한다. +- [ ] WARN/FAIL이면 다음 active `PLAN-{build_lane}-GNN.md`와 `CODE_REVIEW-{review_lane}-GNN.md`를 작성하고 `complete.log`를 작성하지 않는다. + +## 계획 대비 변경 사항 + +_구현 에이전트가 계획과 다르게 구현한 부분을 이유와 함께 기록한다._ + +## 주요 설계 결정 + +_구현 에이전트가 주요 설계 결정 사항을 기록한다._ + +## 리뷰어를 위한 체크포인트 + +{pre-filled from plan — one bullet per review focus area} + +## 검증 결과 + +_구현 에이전트가 각 중간 검증 및 최종 검증 명령 실행 후 출력을 여기에 붙여 넣는다._ + +필수 규칙: +- 검증 명령은 고정된 계약이다. 임의로 대체하지 않는다. +- 대체가 필요하면 `계획 대비 변경 사항`에 이유와 대체 명령을 기록한다. +- `검증 결과`에는 실제 stdout/stderr를 붙여 넣는다. + +### {TAG}-1 중간 검증 +``` +$ {verification command from plan} +(output) +``` + +### 최종 검증 +``` +$ {final verification command from plan} +(output) +``` + +--- + +> **[IMPLEMENTING AGENT — BEFORE SAVING] Have you filled in every implementation-owned section: completion table, implementation checklist, changes from plan, design decisions, and verification output?** +> If anything is blank, go back and fill it in before saving this file. +> Leave the review-agent-only checklist unchanged. +``` + +Sections and their ownership: + +| Section | Owner | Note | +|---------|-------|------| +| Header comment, 개요, 리뷰 에이전트 지시 | Fixed at stub creation | Implementing agent must not modify or execute these (archive, complete.log, and task-directory archive move are review-agent only) | +| 구현 항목별 완료 여부 (item names) | Fixed at stub creation | Implementing agent checks `[ ]` → `[x]` only | +| 구현 체크리스트 (item text/order) | Fixed at stub creation from plan | Implementing agent checks `[ ]` → `[x]` only; final checkbox is mandatory before saving | +| 코드리뷰 전용 체크리스트 | Review agent only | Implementing agent must not modify or check this section | +| 계획 대비 변경 사항, 주요 설계 결정 | Implementing agent | Replace placeholder text with actual content | +| 리뷰어를 위한 체크포인트 | Fixed at stub creation | Pre-filled from plan | +| 검증 결과 (section headings + commands) | Fixed at stub creation | Implementing agent fills in command output only; command changes require a `계획 대비 변경 사항` entry | +| 코드리뷰 결과 | Review agent appends | Not included in stub | + +## Naming + +| Tag | Use for | +|-----|---------| +| `API` | Public API changes | +| `REFACTOR` | Internal refactoring | +| `TEST` | Test additions/fixes | +| `REVIEW_` | Follow-up fixes after review | + +## Final Checklist + +- `PLAN-{build_lane}-GNN.md` and `CODE_REVIEW-{review_lane}-GNN.md` both exist under `agent-task/{task_name}/`. +- Split work, if any, uses one task directory per plan/review pair with `01_...`, `02_...`, or dependency-marked `NN+...` names. +- Both first lines match ``. +- Previous active files, if any, were archived with correct numeric suffixes. +- Every plan item has problem, solution, checklist, test decision, and intermediate verification. +- The plan and review stub have matching `구현 체크리스트` item text/order, and the final checkbox is the mandatory `CODE_REVIEW-*-G??.md` completion item. +- The review stub has a clearly marked `코드리뷰 전용 체크리스트` owned only by the review agent. +- Routed review file completion table lists every plan item. diff --git a/agent-ops/skills/common/plan/agents/openai.yaml b/agent-ops/skills/common/plan/agents/openai.yaml new file mode 100644 index 0000000..751c35e --- /dev/null +++ b/agent-ops/skills/common/plan/agents/openai.yaml @@ -0,0 +1,4 @@ +interface: + display_name: "Plan" + short_description: "Write implementation plans" + default_prompt: "Use $plan to create a task PLAN.md and CODE_REVIEW.md stub for this repository change." diff --git a/agent-ops/skills/common/router.md b/agent-ops/skills/common/router.md new file mode 100644 index 0000000..49ce8d2 --- /dev/null +++ b/agent-ops/skills/common/router.md @@ -0,0 +1,13 @@ +# 공통 스킬 라우터 + +| 요청 키워드 | SKILL.md | +|------------|----------| +| agent-ops 세팅해줘, scaffold 만들어줘, 초기화해줘 | `agent-ops/skills/common/init-agent-ops/SKILL.md` | +| domain rule 만들어줘, rules.md 생성, 새 도메인 규칙 | `agent-ops/skills/common/create-domain-rule/SKILL.md` | +| skill 만들어줘, SKILL.md 생성, 새 스킬 추가 | `agent-ops/skills/common/create-skill/SKILL.md` | +| 계획 세워줘, 구현 계획, PLAN.md, plan | `agent-ops/skills/common/plan/SKILL.md` | +| 코드 리뷰해줘, code review, CODE_REVIEW.md, 리뷰 루프 | `agent-ops/skills/common/code-review/SKILL.md` | +| 커밋해줘, 푸시해줘, commit, push, 반영해줘 | `agent-ops/skills/common/commit-push/SKILL.md` | +| agent-ops 싱크해, agent-ops 동기화해, agentic-framework에 올려줘, agent-ops를 [프로젝트]로 싱크해 | `agent-ops/skills/common/sync-push/SKILL.md` | +| agent-ops pull해, agent-ops 가져와, agentic-framework에서 가져와, agent-ops 내려받아 | `agent-ops/skills/common/sync-pull/SKILL.md` | +| 도메인 업데이트, domain rule 갱신, 도메인 검토, domain 스캔 | `agent-ops/skills/common/update-domain-rule/SKILL.md` | diff --git a/agent-ops/skills/common/sync-pull/SKILL.md b/agent-ops/skills/common/sync-pull/SKILL.md new file mode 100644 index 0000000..adc2c42 --- /dev/null +++ b/agent-ops/skills/common/sync-pull/SKILL.md @@ -0,0 +1,36 @@ +--- +name: sync-pull +description: agentic-framework에서 현재 프로젝트로 agent-ops를 내려받는다. "agent-ops pull해", "agentic-framework에서 가져와" 요청 시 사용한다. +--- + +# sync-pull + +## 목적 + +`agent-ops/bin/sync.sh --pull`을 호출해 agentic-framework → 현재 프로젝트 방향으로 agent-ops를 내려받는다. + +## 언제 호출할지 + +- "agent-ops pull해", "agent-ops 가져와" 요청 시 +- "agentic-framework에서 가져와" 요청 시 +- "agent-ops 내려받아" 요청 시 + +## 실행 절차 + +1. 현재 프로젝트의 **상위 폴더(`../`)** 에 `agentic-framework` 폴더가 있는지 확인한다 (현재 폴더 내부가 아님) +2. **있으면**: `agent-ops/bin/sync.sh --pull agentic-framework` 실행 +3. **없으면**: 사용자에게 agentic-framework 경로 입력을 안내하고, 입력받은 경로로 실행 + +```bash +agent-ops/bin/sync.sh --pull +``` + +## 실행 결과 검증 + +- [ ] sync.sh 가 오류 없이 완료됐는가 +- [ ] 현재 프로젝트 버전이 framework 버전과 일치하는가 +- [ ] `agent-ops/bin/entry-files.sh`의 모든 진입 파일이 갱신됐고, 내용이 framework의 `agent-ops/rules/common/rules.md`와 일치하는가 + +## 금지 사항 + +- sync.sh 를 거치지 않고 직접 파일을 복사하지 않는다 diff --git a/agent-ops/skills/common/sync-push/SKILL.md b/agent-ops/skills/common/sync-push/SKILL.md new file mode 100644 index 0000000..04884a1 --- /dev/null +++ b/agent-ops/skills/common/sync-push/SKILL.md @@ -0,0 +1,60 @@ +--- +name: sync-push +description: 현재 프로젝트의 agent-ops를 agentic-framework로 올리거나, agentic-framework의 공통 agent-ops를 대상 프로젝트로 push한다. "agent-ops 싱크해", "agent-ops 동기화해", "agentic-framework에 올려줘" 요청 시 사용한다. +--- + +# sync-push + +## 목적 + +`agent-ops/bin/sync.sh`를 호출해 agent-ops 공통 파일을 push 방향으로 동기화한다. + +- 일반 프로젝트에서는 현재 프로젝트 → agentic-framework 방향으로 올린다. +- agentic-framework 원본 프로젝트에서는 agentic-framework → 대상 프로젝트 방향으로 보낸다. + +## 언제 호출할지 + +- "agent-ops 싱크해", "agent-ops 동기화해" 요청 시 +- "agentic-framework에 올려줘" 요청 시 +- "agent-ops를 [프로젝트]로 싱크해" 요청 시 + +## 실행 절차 + +### 현 프로젝트가 일반 프로젝트인 경우 (`.agent-ops-source` 없음) + +1. 현재 프로젝트의 **상위 폴더(`../`)** 에 `agentic-framework` 폴더가 있는지 확인한다 (현재 폴더 내부가 아님) +2. **있으면**: `agent-ops/bin/sync.sh agentic-framework` 실행 +3. **없으면**: 사용자에게 agentic-framework 경로 입력을 안내하고, 입력받은 경로로 실행 + +### 현 프로젝트가 agentic-framework인 경우 (`.agent-ops-source` 있음) + +1. 사용자 요청에서 target 프로젝트명 또는 경로를 추출한다 +2. **명시된 경우**: `agent-ops/bin/sync.sh ` 실행 +3. **명시되지 않은 경우**: 사용자에게 대상 프로젝트 입력을 요청한다 +4. `sync.sh`는 현재 agentic-framework의 `agent-ops/rules/common/rules.md` 내용을 대상 프로젝트 루트의 진입 파일에 덮어쓴다 + +덮어쓰기 대상은 `init-agent-ops` 초기 세팅과 동일하며, 실제 목록은 `agent-ops/bin/entry-files.sh`의 `AGENT_OPS_ENTRY_FILES`를 단일 기준으로 사용한다. + +| 에이전트 | 파일명 | +|---------|--------| +| Gemini | `GEMINI.md` | +| Claude | `CLAUDE.md` | +| Kilo Code / OpenCode | `AGENTS.md` | +| Cursor | `.cursorrules` | +| Cline | `.clinerules` | + +대상 프로젝트에 기존 진입 파일이 있어도 보존하거나 병합하지 않고 `agent-ops/rules/common/rules.md` 내용으로 교체한다. + +```bash +agent-ops/bin/sync.sh +``` + +## 실행 결과 검증 + +- [ ] sync.sh 가 오류 없이 완료됐는가 +- [ ] 버전 충돌 경고가 없었는가 — 있었다면 사용자에게 수동 머지 필요함을 알린다 +- [ ] agentic-framework에서 대상 프로젝트로 push한 경우, `agent-ops/bin/entry-files.sh`의 모든 진입 파일 내용이 현재 agentic-framework의 `agent-ops/rules/common/rules.md`와 일치하는가 + +## 금지 사항 + +- sync.sh 를 거치지 않고 직접 파일을 복사하지 않는다 diff --git a/agent-ops/skills/common/update-domain-rule/SKILL.md b/agent-ops/skills/common/update-domain-rule/SKILL.md new file mode 100644 index 0000000..b5beb3e --- /dev/null +++ b/agent-ops/skills/common/update-domain-rule/SKILL.md @@ -0,0 +1,100 @@ +--- +name: update-domain-rule +version: 1.0.0 +description: 기존 도메인 rules.md를 코드 현황에 맞게 갱신. 전체 스캔(full) 또는 지정 도메인(targeted) 두 모드 지원 +--- + +# update-domain-rule + +## 목적 + +프로젝트 코드가 변경되면서 기존 domain rule이 실제 구조와 어긋날 수 있다. +이 스킬은 실제 파일 구조를 탐색하여 기존 `rules.md`와 비교하고, 누락·오류·구식 항목을 수정한다. + +## 언제 호출할지 + +- 대규모 리팩터링 또는 외부 패키지 내재화 후 domain rule 동기화가 필요할 때 +- 특정 도메인 파일 구조가 바뀌어 기존 rule이 맞지 않을 때 +- 사용자가 "도메인 업데이트", "domain rule 갱신", "domain 검토" 등을 요청할 때 +- agent-ops 초기 scaffold 이후 코드가 많이 달라진 경우 + +## 입력 + +- `mode`: `full`(전체 도메인 스캔) | `targeted`(지정 도메인만) (필수) +- `domain-name`: 업데이트할 도메인 이름 — `targeted` 모드에서만 필수 + +## 먼저 확인할 것 + +- [ ] `agent-ops/rules/project/domain/` 하위 기존 도메인 목록 확인 +- [ ] `agent-ops/rules/common/_templates/domain-rule-template.md` 읽어 최신 템플릿 형식 파악 +- [ ] `targeted` 모드이면 `agent-ops/rules/project/domain//rules.md` 존재 여부 확인 + - 존재하지 않으면 `create-domain-rule` 스킬을 사용하도록 안내하고 중단 + +## 실행 절차 + +1. **대상 목록 결정** + - `full`: `agent-ops/rules/project/domain/` 하위 모든 도메인 디렉터리를 대상으로 한다 + - `targeted`: 지정된 `domain-name` 하나만 대상으로 한다 + +2. **도메인별 코드 탐색** + - 기존 `rules.md`의 **포함 경로** 목록을 기준으로 실제 파일 구조 탐색 + - 포함 경로에 없지만 도메인 이름과 연관된 경로도 함께 탐색 + - 탐색 시 실제로 존재하는 경로만 수집한다 + +3. **비교 및 변경 항목 식별** + 다음 항목 각각을 현재 `rules.md`와 비교한다: + - **포함 경로**: 실제로 존재하지 않는 경로 제거, 새로 생긴 경로 추가 + - **주요 구성 요소**: 파일/클래스 삭제·이름 변경·신규 추가 반영 + - **유지할 패턴**: 코드에서 더 이상 사용되지 않는 패턴 제거, 새 패턴 추가 + - **다른 도메인과의 경계**: 도메인 간 import 관계가 바뀐 경우 반영 + - **목적/책임**: 도메인 책임이 실질적으로 변경된 경우에만 수정 + +4. **rules.md 업데이트** + - 변경이 필요한 항목만 수정한다 — 변경 불필요한 섹션은 그대로 둔다 + - 확인된 사실만 기재한다; 불확실하면 `` 주석 처리 + - `domain-rule-template.md`의 섹션 구조를 유지한다 + +5. **도메인 매핑 테이블 검토** (`full` 모드 시) + - `agent-ops/rules/project/rules.md`의 도메인 매핑 테이블과 실제 포함 경로 비교 + - 누락된 경로 패턴은 추가, 존재하지 않는 경로 패턴은 제거 + - 기존 항목 순서는 변경하지 않는다 + +6. **결과 보고** + - 도메인별로 수정한 항목 목록 + - `TODO`로 남긴 항목 (해당 시) + - 도메인 매핑 테이블 변경 내용 (full 모드 시) + +## 실행 결과 검증 + +- [ ] 수정된 `rules.md`의 포함 경로가 모두 실제 프로젝트에 존재하는가 +- [ ] 섹션 구조가 `domain-rule-template.md` 형식을 유지하는가 +- [ ] 변경하지 않아도 되는 섹션이 의도치 않게 바뀌지 않았는가 +- [ ] `full` 모드에서 도메인 매핑 테이블이 실제 포함 경로와 일치하는가 +- 검증 실패 시: 실제 존재하지 않는 경로나 누락된 섹션을 사용자에게 알리고 해당 항목만 보완한다 + +## 출력 형식 + +``` +## 업데이트 완료 + +### +- 변경: <수정된 항목 요약> +- 추가: <새로 추가된 경로/구성 요소> +- 제거: <삭제된 경로/구성 요소> +- 유지: 변경 없음 (해당 섹션) + +### 도메인 매핑 테이블 (full 모드 시) +- 추가: <새 경로 패턴> +- 제거: <삭제된 경로 패턴> + +## TODO 항목 (확인 필요) +- <불확실하여 직접 확인이 필요한 항목> (해당 시) +``` + +## 금지 사항 + +- 실제 존재하지 않는 경로를 포함 경로에 기재하지 않는다 +- 추측으로 패턴·금지 사항을 추가하지 않는다 — 코드에서 확인된 내용만 기재한다 +- 신규 도메인 생성이 필요한 경우 직접 생성하지 않고 `create-domain-rule` 스킬 사용을 안내한다 +- `rules/project/rules.md`의 기존 항목을 삭제하거나 재정렬하지 않는다 +- 코드 파일을 수정하지 않는다 — 이 스킬은 rule 파일 갱신만 담당한다 From 2344782fca9d0631f93d86c90b17114b41b5a31f Mon Sep 17 00:00:00 2001 From: toki Date: Wed, 20 May 2026 09:54:22 +0900 Subject: [PATCH 12/18] update agent-ops files --- agent-ops/.version | 2 +- agent-ops/bin/init-agent-ops.sh | 36 ++++++++++++++++++++------- agent-ops/bin/sync.sh | 22 ++++++++++++++-- agent-ops/skills/common/plan/SKILL.md | 13 ++++++---- 4 files changed, 56 insertions(+), 17 deletions(-) diff --git a/agent-ops/.version b/agent-ops/.version index e9bc149..645377e 100644 --- a/agent-ops/.version +++ b/agent-ops/.version @@ -1 +1 @@ -1.1.14 +1.1.15 diff --git a/agent-ops/bin/init-agent-ops.sh b/agent-ops/bin/init-agent-ops.sh index 449c410..142a9f6 100755 --- a/agent-ops/bin/init-agent-ops.sh +++ b/agent-ops/bin/init-agent-ops.sh @@ -27,20 +27,38 @@ append_unique_line() { fi } +create_project_agent_ops_dirs() { + local agent_ops_dir="$1" + + mkdir -p "$agent_ops_dir/rules/project/domain" + mkdir -p "$agent_ops_dir/rules/private" + mkdir -p "$agent_ops_dir/skills/project" +} + +copy_common_agent_ops() { + local source_dir="$1" + local target_agent_ops_dir="$2" + + mkdir -p "$target_agent_ops_dir/rules" + mkdir -p "$target_agent_ops_dir/skills" + + cp "$source_dir/.version" "$target_agent_ops_dir/" + rm -rf "$target_agent_ops_dir/bin" + rm -rf "$target_agent_ops_dir/rules/common" + rm -rf "$target_agent_ops_dir/skills/common" + cp -r "$source_dir/bin" "$target_agent_ops_dir/" + cp -r "$source_dir/rules/common" "$target_agent_ops_dir/rules/" + cp -r "$source_dir/skills/common" "$target_agent_ops_dir/skills/" +} + echo "Initializing agent-ops in: $TARGET_DIR" echo "Source agent-ops: $SOURCE_DIR" -# 1. 대상 폴더 생성 -mkdir -p "$TARGET_DIR/agent-ops/rules/project/domain" -mkdir -p "$TARGET_DIR/agent-ops/rules/private" -mkdir -p "$TARGET_DIR/agent-ops/skills/project" +# 1. 대상 폴더 생성 (프로젝트 전용 폴더는 내용 복사 없이 폴더만 생성) +create_project_agent_ops_dirs "$TARGET_DIR/agent-ops" # 2. 공통 요소 복사 (프로젝트 전용 설정은 제외) -cp "$SOURCE_DIR/.version" "$TARGET_DIR/agent-ops/" -rm -rf "$TARGET_DIR/agent-ops/bin" -cp -r "$SOURCE_DIR/bin" "$TARGET_DIR/agent-ops/" -cp -r "$SOURCE_DIR/rules/common" "$TARGET_DIR/agent-ops/rules/" -cp -r "$SOURCE_DIR/skills/common" "$TARGET_DIR/agent-ops/skills/" +copy_common_agent_ops "$SOURCE_DIR" "$TARGET_DIR/agent-ops" # 3. 에이전트 진입 파일 생성 (common/rules.md 복사) COMMON_RULES="$SOURCE_DIR/rules/common/rules.md" diff --git a/agent-ops/bin/sync.sh b/agent-ops/bin/sync.sh index 632853d..c32cdb7 100755 --- a/agent-ops/bin/sync.sh +++ b/agent-ops/bin/sync.sh @@ -71,6 +71,24 @@ sync_common() { sync_folder "$src/bin" "$dst/bin" } +create_project_agent_ops_dirs() { + local agent_ops_dir="$1" + mkdir -p "$agent_ops_dir/rules/project/domain" + mkdir -p "$agent_ops_dir/rules/private" + mkdir -p "$agent_ops_dir/skills/project" +} + +copy_common_scaffold() { + local src="$1" dst="$2" + mkdir -p "$dst/rules" "$dst/skills" + cp "$src/.version" "$dst/" + rm -rf "$dst/bin" "$dst/rules/common" "$dst/skills/common" + cp -r "$src/bin" "$dst/" + cp -r "$src/rules/common" "$dst/rules/" + cp -r "$src/skills/common" "$dst/skills/" + create_project_agent_ops_dirs "$dst" +} + # ── 사용법 ──────────────────────────────────────────────────────────────────── usage() { echo "사용법: $0 [--pull] " @@ -134,8 +152,8 @@ echo "▶ $(basename "$PROJECT_ROOT") → $(basename "$TARGET")" # ── agentic-framework에서 다른 프로젝트로 ──────────────────────────────────── if [[ "$IS_FRAMEWORK" == "1" ]]; then if [[ ! -d "$DST_AO" ]]; then - echo " 최초 진행: agent-ops 전체 복사" - cp -r "$SRC_AO" "$TARGET/" + echo " 최초 진행: agent-ops 공통 scaffold 복사" + copy_common_scaffold "$SRC_AO" "$DST_AO" apply_agent_ops_entry_files "$SRC_AO/rules/common/rules.md" "$TARGET" echo -e "${YELLOW} init-agent-ops 스킬로 초기화를 진행하세요.${RESET}" else diff --git a/agent-ops/skills/common/plan/SKILL.md b/agent-ops/skills/common/plan/SKILL.md index 897de37..b254316 100644 --- a/agent-ops/skills/common/plan/SKILL.md +++ b/agent-ops/skills/common/plan/SKILL.md @@ -31,9 +31,12 @@ Task directory naming rules: - A single-plan task uses `agent-task/{task_name}/` with a short snake_case task name. - If one plan is not enough, split the work into multiple task directories. Each directory owns exactly one normal active plan file and one normal active review stub. - Multi-plan output is a set of independent `PLAN-{build_lane}-GNN.md` + `CODE_REVIEW-{review_lane}-GNN.md` pairs across multiple folders, not multiple plan files inside one folder. -- Multi-plan task directory names must start with an execution-order index: `01_{task_name}`, `02_{task_name}`, `03_{task_name}`, and so on. -- If a later task cannot start until a specific current task is complete, mark that dependency immediately after the current task's index with `+`, not `_`: `01+{dependent_task_name}` means this task depends on completion of the `01_...` task. -- When multiple dependent tasks share the same predecessor, order them after the dependency marker: `01+01_{task_name}`, `01+02_{task_name}`. +- Multi-plan task directory names must start with the current task's execution-order index: `01_{task_name}`, `02_{task_name}`, `03_{task_name}`, and so on. The leading number is always the current task index and must increase across sibling task directories. +- Use `_` after the index for a task that can start independently at that point: `01_{task_name}`, `03_{task_name}`. +- Use `+` instead of `_` after the index for a task that depends on an earlier task: `02+{task_name}`. Do not put predecessor numbers in the directory name. +- Record the exact predecessor task directory or directories in the plan's `의존 관계 및 구현 순서` section. +- Example: split a common core plus two app integrations as `01_core`, `02+edge_integration`, `03+node_integration`. In both `02+...` and `03+...`, write that they depend on `agent-task/01_core`. The `+` marker does not mean `03+...` depends on `02+...`; if `03+...` also depends on `02+...`, say so explicitly in `의존 관계 및 구현 순서`. +- Example: split three sequential tasks as `01_schema`, `02+migration`, `03+api`. In `02+migration`, list `agent-task/01_schema` as predecessor. In `03+api`, list `agent-task/02+migration` as predecessor. - Directory names are runtime scheduling metadata. Preserve them verbatim; do not normalize, reinterpret, or choose execution order by agent judgment. - Every split plan must include `의존 관계 및 구현 순서` and list predecessor task directories that must reach `complete.log` before implementation begins. @@ -151,7 +154,7 @@ Each plan item must include: Include `의존 관계 및 구현 순서` only when order matters. -For split multi-plan work, `의존 관계 및 구현 순서` is mandatory in every generated plan. If a plan has a `NN+...` directory name, state which `NN_...` predecessor must produce `complete.log` before implementation starts. +For split multi-plan work, `의존 관계 및 구현 순서` is mandatory in every generated plan. If a plan has a `NN+...` directory name, state which predecessor task directory or directories must produce `complete.log` before implementation starts. Quality rules: @@ -308,7 +311,7 @@ Sections and their ownership: ## Final Checklist - `PLAN-{build_lane}-GNN.md` and `CODE_REVIEW-{review_lane}-GNN.md` both exist under `agent-task/{task_name}/`. -- Split work, if any, uses one task directory per plan/review pair with `01_...`, `02_...`, or dependency-marked `NN+...` names. +- Split work, if any, uses one task directory per plan/review pair with names like `01_core`, `02+edge_integration`, `03+node_integration`; predecessor details live in `의존 관계 및 구현 순서`, not in the directory name. - Both first lines match ``. - Previous active files, if any, were archived with correct numeric suffixes. - Every plan item has problem, solution, checklist, test decision, and intermediate verification. From 66fad46ad2cda58ce8264578a72f1f0ed88be0fa Mon Sep 17 00:00:00 2001 From: toki Date: Wed, 20 May 2026 10:09:42 +0900 Subject: [PATCH 13/18] sync: agent-ops from agentic-framework v1.1.15 --- agent-ops/bin/sync.sh | 160 +++++++++++++++---- agent-ops/skills/common/code-review/SKILL.md | 3 + agent-ops/skills/common/plan/SKILL.md | 1 + agent-ops/skills/common/sync-push/SKILL.md | 18 ++- 4 files changed, 143 insertions(+), 39 deletions(-) diff --git a/agent-ops/bin/sync.sh b/agent-ops/bin/sync.sh index c32cdb7..dbde37f 100755 --- a/agent-ops/bin/sync.sh +++ b/agent-ops/bin/sync.sh @@ -89,14 +89,94 @@ copy_common_scaffold() { create_project_agent_ops_dirs "$dst" } +discover_agent_ops_targets() { + local parent + parent="$(dirname "$PROJECT_ROOT")" + + find "$parent" -mindepth 1 -maxdepth 1 -type d | sort | while IFS= read -r candidate; do + local resolved + resolved="$(cd "$candidate" && pwd)" + [[ "$resolved" == "$PROJECT_ROOT" ]] && continue + [[ -d "$resolved/agent-ops" ]] || continue + echo "$resolved" + done +} + +agent_ops_git_paths() { + printf '%s\n' "agent-ops/.version" + printf '%s\n' "agent-ops/bin" + printf '%s\n' "agent-ops/rules/common" + printf '%s\n' "agent-ops/skills/common" + + local f + for f in "${AGENT_OPS_ENTRY_FILES[@]}"; do + if [[ -e "$f" ]] || git ls-files --error-unmatch "$f" >/dev/null 2>&1; then + printf '%s\n' "$f" + fi + done +} + +commit_and_push_agent_ops_scope() { + local repo="$1" message="$2" + + if ! git -C "$repo" rev-parse --is-inside-work-tree >/dev/null 2>&1; then + echo -e "${RED}Error: git 저장소가 아닙니다: $repo${RESET}" + return 1 + fi + + ( + cd "$repo" + mapfile -t paths < <(agent_ops_git_paths) + git add -- "${paths[@]}" + + if git diff --cached --quiet -- "${paths[@]}"; then + echo " agent-ops 변경 없음: commit 건너뜀" + else + git commit -m "$message" -- "${paths[@]}" + fi + + git push + ) +} + +sync_framework_to_target() { + local target="$1" + local dst_ao="$target/agent-ops" + local src_ver + src_ver="$(cat "$SRC_AO/.version" 2>/dev/null || echo "0.0.0")" + + echo "▶ $(basename "$PROJECT_ROOT") → $(basename "$target")" + + if ! git -C "$target" rev-parse --is-inside-work-tree >/dev/null 2>&1; then + echo -e "${RED}Error: git 저장소가 아닙니다: $target${RESET}" + return 1 + fi + + if [[ ! -d "$dst_ao" ]]; then + echo " 최초 진행: agent-ops 공통 scaffold 복사" + copy_common_scaffold "$SRC_AO" "$dst_ao" + apply_agent_ops_entry_files "$SRC_AO/rules/common/rules.md" "$target" + echo -e "${YELLOW} init-agent-ops 스킬로 초기화를 진행하세요.${RESET}" + else + echo " 이후 진행: common/ 동기화" + sync_common "$SRC_AO" "$dst_ao" + cp "$SRC_AO/.version" "$dst_ao/.version" + apply_agent_ops_entry_files "$SRC_AO/rules/common/rules.md" "$target" + fi + + commit_and_push_agent_ops_scope "$target" "sync: agent-ops from $(basename "$PROJECT_ROOT") v$src_ver" + echo -e "${GREEN}✓ 완료 → $(basename "$target")${RESET}" +} + # ── 사용법 ──────────────────────────────────────────────────────────────────── usage() { - echo "사용법: $0 [--pull] " + echo "사용법: $0 [--pull] [target]" echo "" - echo " (기본) 현재 프로젝트 → target(agentic-framework) 으로 push" + echo " (기본) push 동기화" echo " --pull target(agentic-framework) → 현재 프로젝트 로 pull" echo "" echo " target: 폴더명 (동일 레벨 탐색) | 상대경로 | 절대경로" + echo " agentic-framework에서 target 생략 시 상위 폴더의 agent-ops 적용 프로젝트 전체 동기화" } # ───────────────────────────────────────────────────────────────────────────── @@ -107,22 +187,22 @@ if [[ "${1:-}" == "--pull" ]]; then fi TARGET_INPUT="${1:-}" -if [[ -z "$TARGET_INPUT" ]]; then - usage; exit 1 -fi - -TARGET="$(resolve_target "$TARGET_INPUT")" -if [[ -z "$TARGET" ]]; then - echo -e "${RED}Error: 대상을 찾을 수 없습니다: $TARGET_INPUT${RESET}" - exit 1 -fi SRC_AO="$AGENT_OPS_DIR" -DST_AO="$TARGET/agent-ops" IS_FRAMEWORK="$([[ -f "$PROJECT_ROOT/.agent-ops-source" ]] && echo "1" || echo "0")" # ── pull 모드: agentic-framework → 현재 프로젝트 ───────────────────────────── if [[ "$PULL_MODE" == "1" ]]; then + if [[ -z "$TARGET_INPUT" ]]; then + usage; exit 1 + fi + TARGET="$(resolve_target "$TARGET_INPUT")" + if [[ -z "$TARGET" ]]; then + echo -e "${RED}Error: 대상을 찾을 수 없습니다: $TARGET_INPUT${RESET}" + exit 1 + fi + DST_AO="$TARGET/agent-ops" + if [[ "$IS_FRAMEWORK" == "1" ]]; then echo -e "${RED}Error: agentic-framework에서는 --pull을 사용할 수 없습니다.${RESET}" exit 1 @@ -147,26 +227,47 @@ if [[ "$PULL_MODE" == "1" ]]; then exit 0 fi -echo "▶ $(basename "$PROJECT_ROOT") → $(basename "$TARGET")" - # ── agentic-framework에서 다른 프로젝트로 ──────────────────────────────────── if [[ "$IS_FRAMEWORK" == "1" ]]; then - if [[ ! -d "$DST_AO" ]]; then - echo " 최초 진행: agent-ops 공통 scaffold 복사" - copy_common_scaffold "$SRC_AO" "$DST_AO" - apply_agent_ops_entry_files "$SRC_AO/rules/common/rules.md" "$TARGET" - echo -e "${YELLOW} init-agent-ops 스킬로 초기화를 진행하세요.${RESET}" + if [[ -n "$TARGET_INPUT" ]]; then + TARGET="$(resolve_target "$TARGET_INPUT")" + if [[ -z "$TARGET" ]]; then + echo -e "${RED}Error: 대상을 찾을 수 없습니다: $TARGET_INPUT${RESET}" + exit 1 + fi + sync_framework_to_target "$TARGET" else - echo " 이후 진행: common/ 동기화" - sync_common "$SRC_AO" "$DST_AO" - cp "$SRC_AO/.version" "$DST_AO/.version" - apply_agent_ops_entry_files "$SRC_AO/rules/common/rules.md" "$TARGET" + mapfile -t TARGETS < <(discover_agent_ops_targets) + if [[ "${#TARGETS[@]}" -eq 0 ]]; then + echo -e "${YELLOW}agent-ops가 적용된 sibling 프로젝트가 없습니다.${RESET}" + exit 0 + fi + + STATUS=0 + for TARGET in "${TARGETS[@]}"; do + if ! sync_framework_to_target "$TARGET"; then + STATUS=1 + fi + done + exit "$STATUS" fi - echo -e "${GREEN}✓ 완료 → $(basename "$TARGET")${RESET}" exit 0 fi # ── 일반 프로젝트에서 agentic-framework로 (push) ───────────────────────────── +if [[ -z "$TARGET_INPUT" ]]; then + TARGET_INPUT="agentic-framework" +fi + +TARGET="$(resolve_target "$TARGET_INPUT")" +if [[ -z "$TARGET" ]]; then + echo -e "${RED}Error: 대상을 찾을 수 없습니다: $TARGET_INPUT${RESET}" + exit 1 +fi +DST_AO="$TARGET/agent-ops" + +echo "▶ $(basename "$PROJECT_ROOT") → $(basename "$TARGET")" + if [[ ! -f "$TARGET/.agent-ops-source" ]]; then echo -e "${RED}Error: target이 agentic-framework가 아닙니다 (.agent-ops-source 없음)${RESET}" exit 1 @@ -187,14 +288,7 @@ sync_common "$SRC_AO" "$DST_AO" echo "$NEW_VER" > "$SRC_AO/.version" echo "$NEW_VER" > "$DST_AO/.version" -cd "$TARGET" -git add agent-ops/rules/common agent-ops/skills/common agent-ops/bin agent-ops/.version -git commit -m "sync: from $(basename "$PROJECT_ROOT") v$NEW_VER" -git push -cd "$PROJECT_ROOT" - -git add agent-ops/rules/common agent-ops/skills/common agent-ops/bin agent-ops/.version -git commit -m "sync: to agentic-framework v$NEW_VER" -git push +commit_and_push_agent_ops_scope "$TARGET" "sync: from $(basename "$PROJECT_ROOT") v$NEW_VER" +commit_and_push_agent_ops_scope "$PROJECT_ROOT" "sync: to agentic-framework v$NEW_VER" echo -e "${GREEN}✓ 완료 (v$SRC_VER → v$NEW_VER)${RESET}" diff --git a/agent-ops/skills/common/code-review/SKILL.md b/agent-ops/skills/common/code-review/SKILL.md index f2429db..006174c 100644 --- a/agent-ops/skills/common/code-review/SKILL.md +++ b/agent-ops/skills/common/code-review/SKILL.md @@ -54,11 +54,13 @@ The implementing agent never archives or deletes active files; archiving is this Finalization invariant: - Every review attempt that selects an active review file must end by archiving the active `CODE_REVIEW-*-G??.md` and `PLAN-*-G??.md` files. +- A review is not complete when the verdict is appended. It is complete only after the required archive files and next-state files exist. - After archiving, exactly one next state is allowed: - `PASS`: write `complete.log`, move `agent-task/{task_name}/` to `agent-task/archive/YYYY/MM/{task_name}/`, then complete the review-only checklist in the final archive path. - `WARN` or `FAIL`: write the next active `PLAN-{build_lane}-GNN.md` and `CODE_REVIEW-{review_lane}-GNN.md`; do not write `complete.log`. - Never stop after appending a verdict. Do not report to the user until archive, required next-state file writes, review-only checklist updates, and PASS task-directory archive moves are complete. - If the review result feels ambiguous, choose `WARN` or `FAIL` according to the severity rules, archive the current files, and write the next plan/review pair. Ambiguity is not a reason to leave active files unarchived. +- If you notice that you already reported after only appending `PASS`, `WARN`, or `FAIL`, resume immediately: archive the active files, write `complete.log` or the follow-up plan/review pair, update the archived review checklist, then correct the user-facing report. ## Step 1 - Find Active Task @@ -195,6 +197,7 @@ Review 완료 후 반드시 아래 순서로 아카이브하세요. 2. `PLAN-{build_lane}-GNN.md` → `plan_{build_lane}_GNN_M.log` (M = 기존 plan_*.log 수) 3. PASS인 경우 `complete.log` 작성 후 `agent-task/archive/YYYY/MM/{task_name}/`로 task 디렉터리 이동. WARN/FAIL인 경우 새 routed plan + review 스텁 작성. +판정 append만으로 review를 끝내면 안 됩니다. `PASS`, `WARN`, `FAIL` 모두 active plan/review 파일을 `.log`로 전환한 뒤 다음 상태 파일까지 만든 후에만 보고하세요. 어떤 판정에서도 아카이브를 건너뛰지 마세요. PASS/WARN/FAIL 모두 `코드리뷰 결과` append 후 active plan/review 파일을 먼저 아카이브하고, 그 다음 `complete.log` 또는 다음 plan/review 파일을 작성해야 합니다. PASS에서는 `agent-ops/skills/common/code-review/templates/complete-log-template.md`의 섹션 순서와 필수 항목을 기준으로 `complete.log`를 작성하세요. 작성 후 현재 날짜의 `YYYY/MM` 기준으로 task 디렉터리를 `agent-task/archive/YYYY/MM/{task_name}/`로 이동하고, 최종 archive 경로의 `code_review_*.log`에서 `코드리뷰 전용 체크리스트`를 갱신한 다음 보고하세요. WARN/FAIL에서는 다음 상태 파일 작성 후 현재 task 경로의 archived `code_review_*.log`에서 적용 가능한 `코드리뷰 전용 체크리스트` 항목을 체크한 다음 보고하세요. diff --git a/agent-ops/skills/common/plan/SKILL.md b/agent-ops/skills/common/plan/SKILL.md index b254316..7874d89 100644 --- a/agent-ops/skills/common/plan/SKILL.md +++ b/agent-ops/skills/common/plan/SKILL.md @@ -216,6 +216,7 @@ review 완료 후 반드시 아래 순서로 아카이브하세요. 2. `PLAN-{build_lane}-GNN.md` → `plan_{build_lane}_GNN_M.log` (M = 기존 plan_*.log 수) 3. PASS인 경우 `complete.log` 작성 후 `agent-task/archive/YYYY/MM/{task_name}/`로 task 디렉터리 이동. WARN/FAIL인 경우 새 routed plan + review 스텁 작성. +판정 append만으로 review를 끝내면 안 됩니다. `PASS`, `WARN`, `FAIL` 모두 active plan/review 파일을 `.log`로 전환한 뒤 다음 상태 파일까지 만든 후에만 보고하세요. 어떤 판정에서도 아카이브를 건너뛰지 마세요. PASS/WARN/FAIL 모두 `코드리뷰 결과` append 후 active plan/review 파일을 먼저 아카이브하고, 그 다음 `complete.log` 또는 다음 plan/review 파일을 작성해야 합니다. PASS에서는 `agent-ops/skills/common/code-review/templates/complete-log-template.md`의 섹션 순서와 필수 항목을 기준으로 `complete.log`를 작성하세요. 작성 후 현재 날짜의 `YYYY/MM` 기준으로 task 디렉터리를 `agent-task/archive/YYYY/MM/{task_name}/`로 이동하고, 최종 archive 경로의 `code_review_*.log`에서 `코드리뷰 전용 체크리스트`를 갱신한 다음 보고하세요. WARN/FAIL에서는 다음 상태 파일 작성 후 현재 task 경로의 archived `code_review_*.log`에서 적용 가능한 `코드리뷰 전용 체크리스트` 항목을 체크한 다음 보고하세요. diff --git a/agent-ops/skills/common/sync-push/SKILL.md b/agent-ops/skills/common/sync-push/SKILL.md index 04884a1..12c9c5b 100644 --- a/agent-ops/skills/common/sync-push/SKILL.md +++ b/agent-ops/skills/common/sync-push/SKILL.md @@ -1,6 +1,6 @@ --- name: sync-push -description: 현재 프로젝트의 agent-ops를 agentic-framework로 올리거나, agentic-framework의 공통 agent-ops를 대상 프로젝트로 push한다. "agent-ops 싱크해", "agent-ops 동기화해", "agentic-framework에 올려줘" 요청 시 사용한다. +description: 현재 프로젝트의 agent-ops를 agentic-framework로 올리거나, agentic-framework의 공통 agent-ops를 대상 프로젝트 또는 상위 폴더의 agent-ops 적용 프로젝트 전체로 push하고 푸시한다. "agent-ops 싱크해", "agent-ops 동기화해", "agentic-framework에 올려줘" 요청 시 사용한다. --- # sync-push @@ -10,7 +10,8 @@ description: 현재 프로젝트의 agent-ops를 agentic-framework로 올리거 `agent-ops/bin/sync.sh`를 호출해 agent-ops 공통 파일을 push 방향으로 동기화한다. - 일반 프로젝트에서는 현재 프로젝트 → agentic-framework 방향으로 올린다. -- agentic-framework 원본 프로젝트에서는 agentic-framework → 대상 프로젝트 방향으로 보낸다. +- agentic-framework 원본 프로젝트에서는 agentic-framework → 대상 프로젝트 방향으로 보내고, 대상 repo를 commit/push 한다. +- agentic-framework 원본 프로젝트에서 대상이 명시되지 않으면 현재 프로젝트의 상위 폴더에 있는 agent-ops 적용 프로젝트 전체로 보낸다. ## 언제 호출할지 @@ -23,15 +24,17 @@ description: 현재 프로젝트의 agent-ops를 agentic-framework로 올리거 ### 현 프로젝트가 일반 프로젝트인 경우 (`.agent-ops-source` 없음) 1. 현재 프로젝트의 **상위 폴더(`../`)** 에 `agentic-framework` 폴더가 있는지 확인한다 (현재 폴더 내부가 아님) -2. **있으면**: `agent-ops/bin/sync.sh agentic-framework` 실행 +2. **있으면**: `agent-ops/bin/sync.sh` 실행 또는 `agent-ops/bin/sync.sh agentic-framework` 실행 3. **없으면**: 사용자에게 agentic-framework 경로 입력을 안내하고, 입력받은 경로로 실행 ### 현 프로젝트가 agentic-framework인 경우 (`.agent-ops-source` 있음) 1. 사용자 요청에서 target 프로젝트명 또는 경로를 추출한다 2. **명시된 경우**: `agent-ops/bin/sync.sh ` 실행 -3. **명시되지 않은 경우**: 사용자에게 대상 프로젝트 입력을 요청한다 -4. `sync.sh`는 현재 agentic-framework의 `agent-ops/rules/common/rules.md` 내용을 대상 프로젝트 루트의 진입 파일에 덮어쓴다 +3. **명시되지 않은 경우**: `agent-ops/bin/sync.sh` 실행 +4. target이 없으면 `sync.sh`가 현재 프로젝트 기준 상위 폴더(`../`)의 하위 디렉터리 중 `agent-ops/` 폴더가 있는 프로젝트를 모두 대상으로 삼는다 +5. `sync.sh`는 현재 agentic-framework의 `agent-ops/rules/common/rules.md` 내용을 대상 프로젝트 루트의 진입 파일에 덮어쓴다 +6. 적용 후 각 대상 repo에서 agent-ops 공통 관리 경로와 진입 파일만 stage 하여 commit/push 한다 덮어쓰기 대상은 `init-agent-ops` 초기 세팅과 동일하며, 실제 목록은 `agent-ops/bin/entry-files.sh`의 `AGENT_OPS_ENTRY_FILES`를 단일 기준으로 사용한다. @@ -46,14 +49,17 @@ description: 현재 프로젝트의 agent-ops를 agentic-framework로 올리거 대상 프로젝트에 기존 진입 파일이 있어도 보존하거나 병합하지 않고 `agent-ops/rules/common/rules.md` 내용으로 교체한다. ```bash -agent-ops/bin/sync.sh +agent-ops/bin/sync.sh [target] ``` +푸시 대상 path는 항상 `agent-ops/.version`, `agent-ops/bin`, `agent-ops/rules/common`, `agent-ops/skills/common`과 `AGENT_OPS_ENTRY_FILES`의 진입 파일로 제한한다. + ## 실행 결과 검증 - [ ] sync.sh 가 오류 없이 완료됐는가 - [ ] 버전 충돌 경고가 없었는가 — 있었다면 사용자에게 수동 머지 필요함을 알린다 - [ ] agentic-framework에서 대상 프로젝트로 push한 경우, `agent-ops/bin/entry-files.sh`의 모든 진입 파일 내용이 현재 agentic-framework의 `agent-ops/rules/common/rules.md`와 일치하는가 +- [ ] 대상 repo에 commit/push 된 path가 agent-ops 공통 관리 경로와 진입 파일로 제한되었는가 ## 금지 사항 From a04a0cbd751629c31d13350a117d10eb37401f04 Mon Sep 17 00:00:00 2001 From: toki Date: Wed, 20 May 2026 11:32:53 +0900 Subject: [PATCH 14/18] sync: agent-ops from agentic-framework v1.1.15 --- agent-ops/skills/common/code-review/SKILL.md | 5 +++- agent-ops/skills/common/plan/SKILL.md | 25 ++++++++++++-------- 2 files changed, 19 insertions(+), 11 deletions(-) diff --git a/agent-ops/skills/common/code-review/SKILL.md b/agent-ops/skills/common/code-review/SKILL.md index 006174c..45add84 100644 --- a/agent-ops/skills/common/code-review/SKILL.md +++ b/agent-ops/skills/common/code-review/SKILL.md @@ -29,7 +29,10 @@ Filename rules: Multi-plan runtime contract: - Multi-plan work is represented as multiple task directories. Each directory owns exactly one normal active plan file and one normal active review file. -- Directory names may encode runtime scheduling metadata. Preserve them verbatim; do not normalize, reinterpret, or choose execution order by agent judgment. +- Multi-plan task directory names encode runtime scheduling metadata: + - `NN_{task_name}` has no runtime dependencies. + - `NN+PP[,QQ...]_{task_name}` depends on the listed earlier task indices. +- Directory names are the runtime dependency source of truth. Preserve them verbatim; do not normalize, reinterpret, infer extra dependencies from numeric order, or choose execution order by agent judgment. - If the user/runtime names a task directory, review that directory even when other active review files exist. Review routing rules: diff --git a/agent-ops/skills/common/plan/SKILL.md b/agent-ops/skills/common/plan/SKILL.md index 7874d89..7406350 100644 --- a/agent-ops/skills/common/plan/SKILL.md +++ b/agent-ops/skills/common/plan/SKILL.md @@ -31,14 +31,19 @@ Task directory naming rules: - A single-plan task uses `agent-task/{task_name}/` with a short snake_case task name. - If one plan is not enough, split the work into multiple task directories. Each directory owns exactly one normal active plan file and one normal active review stub. - Multi-plan output is a set of independent `PLAN-{build_lane}-GNN.md` + `CODE_REVIEW-{review_lane}-GNN.md` pairs across multiple folders, not multiple plan files inside one folder. -- Multi-plan task directory names must start with the current task's execution-order index: `01_{task_name}`, `02_{task_name}`, `03_{task_name}`, and so on. The leading number is always the current task index and must increase across sibling task directories. -- Use `_` after the index for a task that can start independently at that point: `01_{task_name}`, `03_{task_name}`. -- Use `+` instead of `_` after the index for a task that depends on an earlier task: `02+{task_name}`. Do not put predecessor numbers in the directory name. -- Record the exact predecessor task directory or directories in the plan's `의존 관계 및 구현 순서` section. -- Example: split a common core plus two app integrations as `01_core`, `02+edge_integration`, `03+node_integration`. In both `02+...` and `03+...`, write that they depend on `agent-task/01_core`. The `+` marker does not mean `03+...` depends on `02+...`; if `03+...` also depends on `02+...`, say so explicitly in `의존 관계 및 구현 순서`. -- Example: split three sequential tasks as `01_schema`, `02+migration`, `03+api`. In `02+migration`, list `agent-task/01_schema` as predecessor. In `03+api`, list `agent-task/02+migration` as predecessor. -- Directory names are runtime scheduling metadata. Preserve them verbatim; do not normalize, reinterpret, or choose execution order by agent judgment. -- Every split plan must include `의존 관계 및 구현 순서` and list predecessor task directories that must reach `complete.log` before implementation begins. +- Multi-plan task directory names must start with a stable two-digit task index. The index must increase across sibling task directories for sorting, but it is not a serial execution dependency. +- Use `NN_{task_name}` for a task with no runtime dependencies, e.g. `01_core`, `04_docs`, `05_ui`. +- Use `NN+PP[,QQ...]_{task_name}` for a task that depends on earlier task indices, e.g. `02+01_db`, `03+01,02_api`, `06+05_integration`. +- Valid independent pattern: `^[0-9]{2}_[a-z0-9_]+$`. +- Valid dependent pattern: `^[0-9]{2}\+[0-9]{2}(,[0-9]{2})*_[a-z0-9_]+$`. +- `NN`, `PP`, and `QQ` are two-digit indices. Every predecessor index after `+` must be lower than `NN` and must refer to a sibling multi-plan task directory. +- The first `_` after the index or dependency list starts `{task_name}`. `{task_name}` stays short snake_case and may contain additional underscores. +- Runtime scheduling reads only the directory name: `_` means `depends_on=[]`; `+` means `depends_on` is the comma-separated index list between `+` and the first `_`. +- Directory names are the source of truth for runtime dependencies. Do not hide extra dependencies only in the plan body, and do not create a bare `NN+{task_name}` without predecessor indices. +- Example: split a common core plus two app integrations as `01_core`, `02+01_edge_integration`, `03+01_node_integration`. Both integrations depend only on `01_core` and may run in parallel after `01_core` has `complete.log`. +- Example: split three sequential tasks as `01_schema`, `02+01_migration`, `03+02_api`. +- Example: split independent docs/UI plus an integration as `01_core`, `02+01_db`, `03+02_api`, `04_docs`, `05_ui`, `06+05_integration`; `01_core`, `04_docs`, and `05_ui` can start together, and `06+05_integration` waits only for `05_ui`. +- Preserve task directory names verbatim; do not normalize, reinterpret, or choose execution order by agent judgment. Routing rules: @@ -154,7 +159,7 @@ Each plan item must include: Include `의존 관계 및 구현 순서` only when order matters. -For split multi-plan work, `의존 관계 및 구현 순서` is mandatory in every generated plan. If a plan has a `NN+...` directory name, state which predecessor task directory or directories must produce `complete.log` before implementation starts. +For split multi-plan work, the directory name is the runtime source of truth. If a plan has a `NN+PP[,QQ...]_...` directory name, `의존 관계 및 구현 순서` must echo the decoded predecessor task directories that must produce `complete.log` before implementation starts, and it must not add dependencies that are absent from the directory name. Quality rules: @@ -312,7 +317,7 @@ Sections and their ownership: ## Final Checklist - `PLAN-{build_lane}-GNN.md` and `CODE_REVIEW-{review_lane}-GNN.md` both exist under `agent-task/{task_name}/`. -- Split work, if any, uses one task directory per plan/review pair with names like `01_core`, `02+edge_integration`, `03+node_integration`; predecessor details live in `의존 관계 및 구현 순서`, not in the directory name. +- Split work, if any, uses one task directory per plan/review pair with names like `01_core`, `02+01_edge_integration`, `03+01_node_integration`; dependency details live in the directory name as `NN+PP[,QQ...]_task_name`. - Both first lines match ``. - Previous active files, if any, were archived with correct numeric suffixes. - Every plan item has problem, solution, checklist, test decision, and intermediate verification. From e95cf0da28e0ca14f4b3b01d164facfdc4f620eb Mon Sep 17 00:00:00 2001 From: toki Date: Wed, 20 May 2026 12:11:08 +0900 Subject: [PATCH 15/18] sync: agent-ops from agentic-framework v1.1.16 --- agent-ops/.version | 2 +- agent-ops/skills/common/code-review/SKILL.md | 42 +++++++------------ .../common/code-review/agents/openai.yaml | 4 +- agent-ops/skills/common/plan/SKILL.md | 14 +++---- agent-ops/skills/common/router.md | 2 +- 5 files changed, 24 insertions(+), 40 deletions(-) diff --git a/agent-ops/.version b/agent-ops/.version index 645377e..63b283b 100644 --- a/agent-ops/.version +++ b/agent-ops/.version @@ -1 +1 @@ -1.1.15 +1.1.16 diff --git a/agent-ops/skills/common/code-review/SKILL.md b/agent-ops/skills/common/code-review/SKILL.md index 45add84..559239f 100644 --- a/agent-ops/skills/common/code-review/SKILL.md +++ b/agent-ops/skills/common/code-review/SKILL.md @@ -1,6 +1,6 @@ --- name: code-review -description: Review completed implementation work in the current repository. Reads PLAN-{build_lane}-GNN.md and CODE_REVIEW-{review_lane}-GNN.md from the active task, reviews the actual changed source files, appends a verdict to the review file, then archives both files as .log. On PASS, writes complete.log summarising the full loop history and moves the completed task under agent-task/archive/YYYY/MM/. If Required or Suggested issues are found (FAIL or WARN), writes new routed plan/review files so the loop continues immediately. Nit-only findings may still PASS. +description: Use for active task review requests such as 리뷰 진행해, 리뷰해줘, 코드 리뷰해줘, code review, CODE_REVIEW.md, or 리뷰 루프. Review the active PLAN/CODE_REVIEW pair, append PASS/WARN/FAIL, archive both active files, and create the required next state: PASS writes complete.log and moves the task to archive; WARN/FAIL immediately writes follow-up PLAN/CODE_REVIEW files. Never stop after verdict append. --- # Code Review @@ -15,6 +15,14 @@ plan skill -> implementation -> code-review skill +----- issues found: new routed plan/review files ``` +## Core Loop Rules + +- Trigger: Korean or English active-task review requests, including `리뷰 진행해` and `리뷰해줘`, must use this skill when `agent-task/*/CODE_REVIEW-*-G??.md` exists. +- Finalize every selected active review: append one verdict, archive the active review and plan files, then create exactly one next state before reporting. +- Next state: `PASS` writes `complete.log` and moves the task under `agent-task/archive/YYYY/MM/`; `WARN` or `FAIL` writes the next active `PLAN-{build_lane}-GNN.md` and `CODE_REVIEW-{review_lane}-GNN.md`. +- Do not ask for confirmation before WARN/FAIL follow-up files. If details are uncertain, write the smallest concrete follow-up plan with file references and verification commands. +- Recovery: if a prior turn appended a verdict without archive or next-state files, resume at Step 5 and finish finalization first. + ## Workflow Contract Active work must live under `agent-task/{task_name}/` using routed filenames. This is the state protocol shared with the plan skill. @@ -54,17 +62,6 @@ Directory states: The implementing agent never archives or deletes active files; archiving is this skill's responsibility. -Finalization invariant: - -- Every review attempt that selects an active review file must end by archiving the active `CODE_REVIEW-*-G??.md` and `PLAN-*-G??.md` files. -- A review is not complete when the verdict is appended. It is complete only after the required archive files and next-state files exist. -- After archiving, exactly one next state is allowed: - - `PASS`: write `complete.log`, move `agent-task/{task_name}/` to `agent-task/archive/YYYY/MM/{task_name}/`, then complete the review-only checklist in the final archive path. - - `WARN` or `FAIL`: write the next active `PLAN-{build_lane}-GNN.md` and `CODE_REVIEW-{review_lane}-GNN.md`; do not write `complete.log`. -- Never stop after appending a verdict. Do not report to the user until archive, required next-state file writes, review-only checklist updates, and PASS task-directory archive moves are complete. -- If the review result feels ambiguous, choose `WARN` or `FAIL` according to the severity rules, archive the current files, and write the next plan/review pair. Ambiguity is not a reason to leave active files unarchived. -- If you notice that you already reported after only appending `PASS`, `WARN`, or `FAIL`, resume immediately: archive the active files, write `complete.log` or the follow-up plan/review pair, update the archived review checklist, then correct the user-facing report. - ## Step 1 - Find Active Task Glob `agent-task/*/CODE_REVIEW-*-G??.md`: @@ -151,16 +148,11 @@ Complete log template: - Use `없음` for empty `잔여 Nit` or `후속 작업`. - A PASS `complete.log` must not contain unresolved Required or Suggested issues. Nit-only leftovers may be recorded under `잔여 Nit`. -After Step 7, report: - -- Verdict. -- Archive filenames. -- Final task archive path under `agent-task/archive/YYYY/MM/`. - For `WARN` or `FAIL`, write new routed plan/review files using the plan skill format: - New plan number is the count of `plan_*.log` after archive. - Header tag is `REVIEW_`. +- Base the follow-up scope directly on the archived review findings. Keep it narrow and actionable. - Choose lane/grade again; preserve the prior route only when it was adequate, otherwise raise `GNN` and/or move `local -> cloud`. - Before choosing the follow-up route, apply this escalation gate: - If the archived plan was `local-*` and the verdict is `FAIL` for correctness, completeness, test coverage, or verification trust, move the follow-up build lane to `cloud` unless the issue is trivially deterministic and review-detectable without live environment behavior. @@ -194,16 +186,12 @@ task={task_name}, plan={N}, tag={TAG} ## 이 파일을 읽는 리뷰 에이전트에게 각 항목의 구현을 실제 소스 파일과 대조하고, `검증 결과` 섹션의 출력이 코드와 일치하는지 확인하세요. -Review 완료 후 반드시 아래 순서로 아카이브하세요. +리뷰 완료는 아래 순서까지 끝난 상태를 의미합니다. -1. `CODE_REVIEW-{review_lane}-GNN.md` → `code_review_{review_lane}_GNN_N.log` (N = 기존 code_review_*.log 수) -2. `PLAN-{build_lane}-GNN.md` → `plan_{build_lane}_GNN_M.log` (M = 기존 plan_*.log 수) -3. PASS인 경우 `complete.log` 작성 후 `agent-task/archive/YYYY/MM/{task_name}/`로 task 디렉터리 이동. WARN/FAIL인 경우 새 routed plan + review 스텁 작성. - -판정 append만으로 review를 끝내면 안 됩니다. `PASS`, `WARN`, `FAIL` 모두 active plan/review 파일을 `.log`로 전환한 뒤 다음 상태 파일까지 만든 후에만 보고하세요. -어떤 판정에서도 아카이브를 건너뛰지 마세요. PASS/WARN/FAIL 모두 `코드리뷰 결과` append 후 active plan/review 파일을 먼저 아카이브하고, 그 다음 `complete.log` 또는 다음 plan/review 파일을 작성해야 합니다. -PASS에서는 `agent-ops/skills/common/code-review/templates/complete-log-template.md`의 섹션 순서와 필수 항목을 기준으로 `complete.log`를 작성하세요. 작성 후 현재 날짜의 `YYYY/MM` 기준으로 task 디렉터리를 `agent-task/archive/YYYY/MM/{task_name}/`로 이동하고, 최종 archive 경로의 `code_review_*.log`에서 `코드리뷰 전용 체크리스트`를 갱신한 다음 보고하세요. -WARN/FAIL에서는 다음 상태 파일 작성 후 현재 task 경로의 archived `code_review_*.log`에서 적용 가능한 `코드리뷰 전용 체크리스트` 항목을 체크한 다음 보고하세요. +1. 판정을 append한다. +2. `CODE_REVIEW-{review_lane}-GNN.md` → `code_review_{review_lane}_GNN_N.log`, `PLAN-{build_lane}-GNN.md` → `plan_{build_lane}_GNN_M.log`로 아카이브한다. +3. PASS이면 `complete.log` 작성 후 task 디렉터리를 `agent-task/archive/YYYY/MM/{task_name}/`로 이동한다. WARN/FAIL이면 다음 active plan/review 파일을 즉시 작성한다. +4. 적용 가능한 `코드리뷰 전용 체크리스트` 항목을 최종 `.log` 위치에서 체크한 뒤 보고한다. --- diff --git a/agent-ops/skills/common/code-review/agents/openai.yaml b/agent-ops/skills/common/code-review/agents/openai.yaml index d45d468..23c51f4 100644 --- a/agent-ops/skills/common/code-review/agents/openai.yaml +++ b/agent-ops/skills/common/code-review/agents/openai.yaml @@ -1,4 +1,4 @@ interface: display_name: "Code Review" - short_description: "Review task implementation" - default_prompt: "Use $code-review to review the active repository task and archive or continue the loop." + short_description: "Review and continue task loops" + default_prompt: "Use $code-review to review the active repository task, archive active files, and create complete.log or follow-up plan/review files." diff --git a/agent-ops/skills/common/plan/SKILL.md b/agent-ops/skills/common/plan/SKILL.md index 7406350..9568111 100644 --- a/agent-ops/skills/common/plan/SKILL.md +++ b/agent-ops/skills/common/plan/SKILL.md @@ -215,16 +215,12 @@ task={task_name}, plan={N}, tag={TAG} ## 이 파일을 읽는 리뷰 에이전트에게 각 항목의 구현을 실제 소스 파일과 대조하고, `검증 결과` 섹션의 출력이 코드와 일치하는지 확인하세요. -review 완료 후 반드시 아래 순서로 아카이브하세요. +리뷰 완료는 아래 순서까지 끝난 상태를 의미합니다. -1. `CODE_REVIEW-{review_lane}-GNN.md` → `code_review_{review_lane}_GNN_N.log` (N = 기존 code_review_*.log 수) -2. `PLAN-{build_lane}-GNN.md` → `plan_{build_lane}_GNN_M.log` (M = 기존 plan_*.log 수) -3. PASS인 경우 `complete.log` 작성 후 `agent-task/archive/YYYY/MM/{task_name}/`로 task 디렉터리 이동. WARN/FAIL인 경우 새 routed plan + review 스텁 작성. - -판정 append만으로 review를 끝내면 안 됩니다. `PASS`, `WARN`, `FAIL` 모두 active plan/review 파일을 `.log`로 전환한 뒤 다음 상태 파일까지 만든 후에만 보고하세요. -어떤 판정에서도 아카이브를 건너뛰지 마세요. PASS/WARN/FAIL 모두 `코드리뷰 결과` append 후 active plan/review 파일을 먼저 아카이브하고, 그 다음 `complete.log` 또는 다음 plan/review 파일을 작성해야 합니다. -PASS에서는 `agent-ops/skills/common/code-review/templates/complete-log-template.md`의 섹션 순서와 필수 항목을 기준으로 `complete.log`를 작성하세요. 작성 후 현재 날짜의 `YYYY/MM` 기준으로 task 디렉터리를 `agent-task/archive/YYYY/MM/{task_name}/`로 이동하고, 최종 archive 경로의 `code_review_*.log`에서 `코드리뷰 전용 체크리스트`를 갱신한 다음 보고하세요. -WARN/FAIL에서는 다음 상태 파일 작성 후 현재 task 경로의 archived `code_review_*.log`에서 적용 가능한 `코드리뷰 전용 체크리스트` 항목을 체크한 다음 보고하세요. +1. 판정을 append한다. +2. `CODE_REVIEW-{review_lane}-GNN.md` → `code_review_{review_lane}_GNN_N.log`, `PLAN-{build_lane}-GNN.md` → `plan_{build_lane}_GNN_M.log`로 아카이브한다. +3. PASS이면 `complete.log` 작성 후 task 디렉터리를 `agent-task/archive/YYYY/MM/{task_name}/`로 이동한다. WARN/FAIL이면 다음 active plan/review 파일을 즉시 작성한다. +4. 적용 가능한 `코드리뷰 전용 체크리스트` 항목을 최종 `.log` 위치에서 체크한 뒤 보고한다. --- diff --git a/agent-ops/skills/common/router.md b/agent-ops/skills/common/router.md index 49ce8d2..daa9cd0 100644 --- a/agent-ops/skills/common/router.md +++ b/agent-ops/skills/common/router.md @@ -6,7 +6,7 @@ | domain rule 만들어줘, rules.md 생성, 새 도메인 규칙 | `agent-ops/skills/common/create-domain-rule/SKILL.md` | | skill 만들어줘, SKILL.md 생성, 새 스킬 추가 | `agent-ops/skills/common/create-skill/SKILL.md` | | 계획 세워줘, 구현 계획, PLAN.md, plan | `agent-ops/skills/common/plan/SKILL.md` | -| 코드 리뷰해줘, code review, CODE_REVIEW.md, 리뷰 루프 | `agent-ops/skills/common/code-review/SKILL.md` | +| 코드 리뷰해줘, 리뷰 진행해, 리뷰해줘, code review, CODE_REVIEW.md, 리뷰 루프 | `agent-ops/skills/common/code-review/SKILL.md` | | 커밋해줘, 푸시해줘, commit, push, 반영해줘 | `agent-ops/skills/common/commit-push/SKILL.md` | | agent-ops 싱크해, agent-ops 동기화해, agentic-framework에 올려줘, agent-ops를 [프로젝트]로 싱크해 | `agent-ops/skills/common/sync-push/SKILL.md` | | agent-ops pull해, agent-ops 가져와, agentic-framework에서 가져와, agent-ops 내려받아 | `agent-ops/skills/common/sync-pull/SKILL.md` | From 8a366c832b6d2ae25a10e401dbc245a8732a7eb5 Mon Sep 17 00:00:00 2001 From: toki Date: Wed, 20 May 2026 21:31:17 +0900 Subject: [PATCH 16/18] sync: agent-ops from agentic-framework v1.1.17 --- agent-ops/.version | 2 +- agent-ops/skills/common/plan/SKILL.md | 16 ++++++++++++++-- 2 files changed, 15 insertions(+), 3 deletions(-) diff --git a/agent-ops/.version b/agent-ops/.version index 63b283b..cbb8cba 100644 --- a/agent-ops/.version +++ b/agent-ops/.version @@ -1 +1 @@ -1.1.16 +1.1.17 diff --git a/agent-ops/skills/common/plan/SKILL.md b/agent-ops/skills/common/plan/SKILL.md index 9568111..56d3a58 100644 --- a/agent-ops/skills/common/plan/SKILL.md +++ b/agent-ops/skills/common/plan/SKILL.md @@ -29,7 +29,8 @@ Filename rules: Task directory naming rules: - A single-plan task uses `agent-task/{task_name}/` with a short snake_case task name. -- If one plan is not enough, split the work into multiple task directories. Each directory owns exactly one normal active plan file and one normal active review stub. +- Prefer splitting before writing. A single-plan task is allowed only when the work is one coherent implementation unit with one primary ownership boundary, no prerequisite subtask, and a reviewable blast radius. +- If the work can be decomposed by dependency, ownership boundary, subsystem, API-vs-call-site phase, test strategy, or independently reviewable risk, split it into multiple task directories. Each directory owns exactly one normal active plan file and one normal active review stub. - Multi-plan output is a set of independent `PLAN-{build_lane}-GNN.md` + `CODE_REVIEW-{review_lane}-GNN.md` pairs across multiple folders, not multiple plan files inside one folder. - Multi-plan task directory names must start with a stable two-digit task index. The index must increase across sibling task directories for sorting, but it is not a serial execution dependency. - Use `NN_{task_name}` for a task with no runtime dependencies, e.g. `01_core`, `04_docs`, `05_ui`. @@ -45,6 +46,15 @@ Task directory naming rules: - Example: split independent docs/UI plus an integration as `01_core`, `02+01_db`, `03+02_api`, `04_docs`, `05_ui`, `06+05_integration`; `01_core`, `04_docs`, and `05_ui` can start together, and `06+05_integration` waits only for `05_ui`. - Preserve task directory names verbatim; do not normalize, reinterpret, or choose execution order by agent judgment. +Split-first gates: + +- Before choosing a single plan, explicitly ask: "Could this be split into smaller independently reviewable implementation units?" If yes, split. +- Split when a plan would require both shared API/foundation changes and broad call-site rollout. Put the API/foundation in an earlier task and the rollout in dependent task(s). +- Split when a plan touches multiple domains or ownership boundaries, unless the change is purely mechanical and trivially reviewable. +- Split when different parts can be verified with different focused tests or different risk profiles. +- Split when a likely failure in one part would force rewriting unrelated parts of the plan. +- Single-plan bias is acceptable only for narrow fixes, one-file/small-file-cluster refactors, or tightly coupled changes where splitting would create artificial coordination overhead. + Routing rules: - Build defaults to `local` when the plan is explicit, tests are runnable, and failure is review-detectable. @@ -83,7 +93,7 @@ The routed plan file is the loop entry point. A missing active plan normally mea Use short snake_case task names, e.g. `api_refactor`. -When the work must be split into multiple plans, choose directory names using the task directory naming rules above. Do not put multiple active plan files in one task directory. +Before writing a single plan, record why the split-first gates did not require multiple task directories. When the work must be split into multiple plans, choose directory names using the task directory naming rules above. Do not put multiple active plan files in one task directory. ## Step 2 - Analyze Before Writing @@ -92,6 +102,7 @@ Complete all items below before creating any files. Work through them in order; - [ ] **Read all source files in full** — read every source file the change will touch, whole file. No partial reads. - [ ] **Read all test files in full** — read every test file that exercises the changed behavior. - [ ] **Assess test coverage** — for each behavior change, explicitly record whether existing tests cover it. +- [ ] **Assess split boundaries** — identify dependency boundaries, ownership boundaries, API-vs-call-site phases, and independently verifiable subwork. If any split-first gate applies, write multiple task directories instead of one plan. - [ ] **Grep all symbol references** — for any renamed or removed symbol, find every call site and import chain. - [ ] **Check dependency manifests** — before adding any new package, verify its presence in go.mod / package manifest. - [ ] **Pre-check compile issues** — identify missing interface implementations, type mismatches, and broken imports. @@ -142,6 +153,7 @@ Required sections: - `읽은 파일`: list every source and test file read during analysis, with path. - `테스트 커버리지 공백`: list each behavior change and whether existing tests cover it; explicitly note gaps. - `심볼 참조`: list renamed/removed symbols and every call site found, or state "none" if no symbols were changed. + - `분할 판단`: state whether the split-first gates were evaluated. For a single plan, explain why the work is still one coherent reviewable unit. For multi-plan output, list each sibling task directory and dependency relationship. - `범위 결정 근거`: state which files or areas were explicitly excluded from this change and why. This is the boundary justification — the implementing agent must not silently expand scope beyond what is recorded here. - `빌드 등급`: state the decided lane and GNN grade with a one-line rationale. - `구현 체크리스트`: a top-level checklist the implementing agent must follow while coding. Include one item per plan item, one item for all intermediate/final verification, and make the final item exactly: `- [ ] CODE_REVIEW-*-G??.md의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채운다. 이 항목이 완료되기 전에는 구현이 완료된 것이 아니다.` Copy this checklist into the review stub's `구현 체크리스트` section with the same item text and order. From 65092691a394a795ff50641b7ef58966ff14fd2e Mon Sep 17 00:00:00 2001 From: toki Date: Wed, 20 May 2026 21:49:41 +0900 Subject: [PATCH 17/18] sync: agent-ops from agentic-framework v1.1.18 --- agent-ops/.version | 2 +- agent-ops/bin/init-agent-ops.sh | 16 ++++++++++++++++ agent-ops/bin/sync.sh | 4 ++-- agent-ops/rules/common/rules.md | 16 ++++++++++++++++ agent-ops/skills/common/init-agent-ops/SKILL.md | 1 + agent-ops/skills/common/sync-push/SKILL.md | 3 ++- 6 files changed, 38 insertions(+), 4 deletions(-) create mode 100644 agent-ops/rules/common/rules.md diff --git a/agent-ops/.version b/agent-ops/.version index cbb8cba..852ed67 100644 --- a/agent-ops/.version +++ b/agent-ops/.version @@ -1 +1 @@ -1.1.17 +1.1.18 diff --git a/agent-ops/bin/init-agent-ops.sh b/agent-ops/bin/init-agent-ops.sh index 142a9f6..7a1f196 100755 --- a/agent-ops/bin/init-agent-ops.sh +++ b/agent-ops/bin/init-agent-ops.sh @@ -51,6 +51,21 @@ copy_common_agent_ops() { cp -r "$source_dir/skills/common" "$target_agent_ops_dir/skills/" } +ensure_common_rules_file() { + local source_dir="$1" + local target_agent_ops_dir="$2" + local source_rules="$source_dir/rules/common/rules.md" + local target_rules="$target_agent_ops_dir/rules/common/rules.md" + + if [ ! -f "$source_rules" ]; then + echo "Error: common rules file not found: $source_rules" >&2 + exit 1 + fi + + mkdir -p "$(dirname "$target_rules")" + cp "$source_rules" "$target_rules" +} + echo "Initializing agent-ops in: $TARGET_DIR" echo "Source agent-ops: $SOURCE_DIR" @@ -63,6 +78,7 @@ copy_common_agent_ops "$SOURCE_DIR" "$TARGET_DIR/agent-ops" # 3. 에이전트 진입 파일 생성 (common/rules.md 복사) COMMON_RULES="$SOURCE_DIR/rules/common/rules.md" apply_agent_ops_entry_files "$COMMON_RULES" "$TARGET_DIR" +ensure_common_rules_file "$SOURCE_DIR" "$TARGET_DIR/agent-ops" # 4. AI ignore / permission 설정 append_unique_line "$TARGET_DIR/.geminiignore" "$ARCHIVE_IGNORE_PATTERN" diff --git a/agent-ops/bin/sync.sh b/agent-ops/bin/sync.sh index dbde37f..790bc1f 100755 --- a/agent-ops/bin/sync.sh +++ b/agent-ops/bin/sync.sh @@ -41,7 +41,7 @@ bump_version() { bash "$SCRIPT_DIR/bump-version.sh" "$1" } -# ── 폴더 동기화 (rules.md 제외, 삭제된 파일도 반영) ───────────────────────── +# ── 폴더 동기화 (삭제된 파일도 반영) ──────────────────────────────────────── sync_folder() { local src="$1" dst="$2" exclude="${3:-}" mkdir -p "$dst" @@ -66,7 +66,7 @@ sync_folder() { sync_common() { local src="$1" dst="$2" - sync_folder "$src/rules/common" "$dst/rules/common" "rules.md" + sync_folder "$src/rules/common" "$dst/rules/common" sync_folder "$src/skills/common" "$dst/skills/common" sync_folder "$src/bin" "$dst/bin" } diff --git a/agent-ops/rules/common/rules.md b/agent-ops/rules/common/rules.md new file mode 100644 index 0000000..ae124ac --- /dev/null +++ b/agent-ops/rules/common/rules.md @@ -0,0 +1,16 @@ +# 공통 규칙 + +- 기존 구조를 우선한다. 새 파일 생성보다 기존 파일 수정을 우선한다. +- 코드 변경 전 관련 domain rule을 먼저 확인한다. +- 요청 범위를 넘는 변경을 하지 않는다. +- 불확실하면 단정하지 말고 후보를 제시한다. +- `agent-task/archive/**`는 사용자가 명시적으로 요청한 경우에만 읽는다. + +`agent-ops/rules/project/rules.md`와 `agent-ops/rules/private/rules.md`는 세션 최초 1회 읽는다. 파일이 없으면 무시한다. + +아래 성격의 요청은 사용자가 명시적으로 요청한 경우에만 `agent-ops/skills/common/router.md`를 작업 최초 1회 읽고 수행한다. 자동으로 수행하지 않는다. +- agent-ops 초기화 +- domain rule 생성 +- skill 생성 +- git commit / push +- agent-ops 업데이트 / 진입 파일 재적용 diff --git a/agent-ops/skills/common/init-agent-ops/SKILL.md b/agent-ops/skills/common/init-agent-ops/SKILL.md index af2b836..5a9807c 100644 --- a/agent-ops/skills/common/init-agent-ops/SKILL.md +++ b/agent-ops/skills/common/init-agent-ops/SKILL.md @@ -224,6 +224,7 @@ common/rules.md와 내용이 중복되지 않도록 한다. ## 실행 결과 검증 - [ ] `agent-ops/bin/entry-files.sh`의 모든 진입 파일이 프로젝트 루트에 존재하고, 내용이 초기화에 사용한 `rules/common/rules.md`와 일치하는가 +- [ ] `agent-ops/rules/common/rules.md`가 대상 프로젝트에 남아 있고 초기화에 사용한 `rules/common/rules.md`와 일치하는가 - [ ] `agent-ops/rules/project/rules.md`가 생성되었고, 필수 항목(응답 언어, 프로젝트 개요, 기술 스택)이 포함되어 있는가 - [ ] 생성된 domain rules.md가 `domain-rule-template.md` 형식을 따르는가 - [ ] `rules/project/rules.md`의 도메인 매핑 테이블에 생성된 도메인이 모두 등록되어 있는가 diff --git a/agent-ops/skills/common/sync-push/SKILL.md b/agent-ops/skills/common/sync-push/SKILL.md index 12c9c5b..390437b 100644 --- a/agent-ops/skills/common/sync-push/SKILL.md +++ b/agent-ops/skills/common/sync-push/SKILL.md @@ -34,7 +34,7 @@ description: 현재 프로젝트의 agent-ops를 agentic-framework로 올리거 3. **명시되지 않은 경우**: `agent-ops/bin/sync.sh` 실행 4. target이 없으면 `sync.sh`가 현재 프로젝트 기준 상위 폴더(`../`)의 하위 디렉터리 중 `agent-ops/` 폴더가 있는 프로젝트를 모두 대상으로 삼는다 5. `sync.sh`는 현재 agentic-framework의 `agent-ops/rules/common/rules.md` 내용을 대상 프로젝트 루트의 진입 파일에 덮어쓴다 -6. 적용 후 각 대상 repo에서 agent-ops 공통 관리 경로와 진입 파일만 stage 하여 commit/push 한다 +6. 적용 후 각 대상 repo에서 agent-ops 공통 관리 경로(`rules/common/rules.md` 포함)와 진입 파일만 stage 하여 commit/push 한다 덮어쓰기 대상은 `init-agent-ops` 초기 세팅과 동일하며, 실제 목록은 `agent-ops/bin/entry-files.sh`의 `AGENT_OPS_ENTRY_FILES`를 단일 기준으로 사용한다. @@ -58,6 +58,7 @@ agent-ops/bin/sync.sh [target] - [ ] sync.sh 가 오류 없이 완료됐는가 - [ ] 버전 충돌 경고가 없었는가 — 있었다면 사용자에게 수동 머지 필요함을 알린다 +- [ ] 대상 repo의 `agent-ops/rules/common/rules.md`가 현재 agentic-framework의 `agent-ops/rules/common/rules.md`와 일치하는가 - [ ] agentic-framework에서 대상 프로젝트로 push한 경우, `agent-ops/bin/entry-files.sh`의 모든 진입 파일 내용이 현재 agentic-framework의 `agent-ops/rules/common/rules.md`와 일치하는가 - [ ] 대상 repo에 commit/push 된 path가 agent-ops 공통 관리 경로와 진입 파일로 제한되었는가 From c7bc4fea567ffd751df2a0cccbb72ec7c5aa10da Mon Sep 17 00:00:00 2001 From: toki Date: Wed, 20 May 2026 22:13:45 +0900 Subject: [PATCH 18/18] sync: agent-ops from agentic-framework v1.1.19 --- agent-ops/.version | 2 +- agent-ops/skills/common/plan/SKILL.md | 34 ++++++++++++++++----------- 2 files changed, 21 insertions(+), 15 deletions(-) diff --git a/agent-ops/.version b/agent-ops/.version index 852ed67..4e03659 100644 --- a/agent-ops/.version +++ b/agent-ops/.version @@ -1 +1 @@ -1.1.18 +1.1.19 diff --git a/agent-ops/skills/common/plan/SKILL.md b/agent-ops/skills/common/plan/SKILL.md index 56d3a58..d998158 100644 --- a/agent-ops/skills/common/plan/SKILL.md +++ b/agent-ops/skills/common/plan/SKILL.md @@ -26,11 +26,26 @@ Filename rules: - `{lane}` is only `local` or `cloud`; never put model names in filenames. - `GNN` is a two-digit capability grade from `G01` to `G10`; the runtime maps lane+grade to current models externally. +Split decision policy: + +- Default to multiple task directories. Before writing any plan, decide whether the work has smaller independently reviewable implementation units. +- If there is any natural dependency boundary, ownership boundary, subsystem boundary, API-vs-call-site phase, test strategy split, or independently reviewable risk, write multiple task directories. +- A single plan is an exception. Use one only when all of these are true: the work is one coherent implementation unit, it has one primary ownership boundary, it has no prerequisite subtask, splitting would create artificial coordination overhead, and the whole change remains easy to review at once. +- When uncertain, split. Do not choose a single plan merely because it is shorter to write. +- Record the split decision in the plan. For a single plan, explicitly state why each applicable split boundary does not require a separate task. For multi-plan output, list each sibling task directory and its dependency relationship. + +Split gates: + +- Split when the work combines shared API/foundation changes with broad call-site rollout. Put the API/foundation in an earlier task and the rollout in dependent task(s). +- Split when the work touches multiple domains or ownership boundaries, unless the change is purely mechanical and trivially reviewable. +- Split when different parts can be verified with different focused tests or have different risk profiles. +- Split when a likely failure in one part would force rewriting unrelated parts of the plan. +- Split when one part can produce a useful `complete.log` before another part starts. + Task directory naming rules: - A single-plan task uses `agent-task/{task_name}/` with a short snake_case task name. -- Prefer splitting before writing. A single-plan task is allowed only when the work is one coherent implementation unit with one primary ownership boundary, no prerequisite subtask, and a reviewable blast radius. -- If the work can be decomposed by dependency, ownership boundary, subsystem, API-vs-call-site phase, test strategy, or independently reviewable risk, split it into multiple task directories. Each directory owns exactly one normal active plan file and one normal active review stub. +- When split gates require decomposition, create multiple task directories. Each directory owns exactly one normal active plan file and one normal active review stub. - Multi-plan output is a set of independent `PLAN-{build_lane}-GNN.md` + `CODE_REVIEW-{review_lane}-GNN.md` pairs across multiple folders, not multiple plan files inside one folder. - Multi-plan task directory names must start with a stable two-digit task index. The index must increase across sibling task directories for sorting, but it is not a serial execution dependency. - Use `NN_{task_name}` for a task with no runtime dependencies, e.g. `01_core`, `04_docs`, `05_ui`. @@ -46,15 +61,6 @@ Task directory naming rules: - Example: split independent docs/UI plus an integration as `01_core`, `02+01_db`, `03+02_api`, `04_docs`, `05_ui`, `06+05_integration`; `01_core`, `04_docs`, and `05_ui` can start together, and `06+05_integration` waits only for `05_ui`. - Preserve task directory names verbatim; do not normalize, reinterpret, or choose execution order by agent judgment. -Split-first gates: - -- Before choosing a single plan, explicitly ask: "Could this be split into smaller independently reviewable implementation units?" If yes, split. -- Split when a plan would require both shared API/foundation changes and broad call-site rollout. Put the API/foundation in an earlier task and the rollout in dependent task(s). -- Split when a plan touches multiple domains or ownership boundaries, unless the change is purely mechanical and trivially reviewable. -- Split when different parts can be verified with different focused tests or different risk profiles. -- Split when a likely failure in one part would force rewriting unrelated parts of the plan. -- Single-plan bias is acceptable only for narrow fixes, one-file/small-file-cluster refactors, or tightly coupled changes where splitting would create artificial coordination overhead. - Routing rules: - Build defaults to `local` when the plan is explicit, tests are runnable, and failure is review-detectable. @@ -93,7 +99,7 @@ The routed plan file is the loop entry point. A missing active plan normally mea Use short snake_case task names, e.g. `api_refactor`. -Before writing a single plan, record why the split-first gates did not require multiple task directories. When the work must be split into multiple plans, choose directory names using the task directory naming rules above. Do not put multiple active plan files in one task directory. +Before choosing plan files or task directory names, apply the split decision policy above. When the policy allows a single plan, record the exception rationale. When the policy requires multiple plans, choose directory names using the task directory naming rules above. Do not put multiple active plan files in one task directory. ## Step 2 - Analyze Before Writing @@ -102,7 +108,7 @@ Complete all items below before creating any files. Work through them in order; - [ ] **Read all source files in full** — read every source file the change will touch, whole file. No partial reads. - [ ] **Read all test files in full** — read every test file that exercises the changed behavior. - [ ] **Assess test coverage** — for each behavior change, explicitly record whether existing tests cover it. -- [ ] **Assess split boundaries** — identify dependency boundaries, ownership boundaries, API-vs-call-site phases, and independently verifiable subwork. If any split-first gate applies, write multiple task directories instead of one plan. +- [ ] **Assess split boundaries first** — identify dependency boundaries, ownership boundaries, API-vs-call-site phases, test strategy splits, risk profile splits, and independently verifiable subwork before selecting plan files. If any split gate applies or the decision is uncertain, write multiple task directories instead of one plan. - [ ] **Grep all symbol references** — for any renamed or removed symbol, find every call site and import chain. - [ ] **Check dependency manifests** — before adding any new package, verify its presence in go.mod / package manifest. - [ ] **Pre-check compile issues** — identify missing interface implementations, type mismatches, and broken imports. @@ -153,7 +159,7 @@ Required sections: - `읽은 파일`: list every source and test file read during analysis, with path. - `테스트 커버리지 공백`: list each behavior change and whether existing tests cover it; explicitly note gaps. - `심볼 참조`: list renamed/removed symbols and every call site found, or state "none" if no symbols were changed. - - `분할 판단`: state whether the split-first gates were evaluated. For a single plan, explain why the work is still one coherent reviewable unit. For multi-plan output, list each sibling task directory and dependency relationship. + - `분할 판단`: state that the split decision policy was evaluated before choosing plan files. For a single plan, explain why each relevant split gate does not apply and why single-plan coordination is safer than splitting. For multi-plan output, list each sibling task directory and dependency relationship. - `범위 결정 근거`: state which files or areas were explicitly excluded from this change and why. This is the boundary justification — the implementing agent must not silently expand scope beyond what is recorded here. - `빌드 등급`: state the decided lane and GNN grade with a one-line rationale. - `구현 체크리스트`: a top-level checklist the implementing agent must follow while coding. Include one item per plan item, one item for all intermediate/final verification, and make the final item exactly: `- [ ] CODE_REVIEW-*-G??.md의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채운다. 이 항목이 완료되기 전에는 구현이 완료된 것이 아니다.` Copy this checklist into the review stub's `구현 체크리스트` section with the same item text and order.