app_manager 0.1.0 → 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.github/workflows/ruby.yml +5 -6
- data/Gemfile +2 -2
- data/Gemfile.lock +16 -24
- data/README.md +119 -13
- data/app/controllers/app_manager/application_controller.rb +7 -0
- data/app/controllers/app_manager/banners_controller.rb +7 -2
- data/app/controllers/app_manager/charges_controller.rb +122 -0
- data/app/controllers/app_manager/plans_controller.rb +157 -0
- data/app/controllers/concerns/app_manager/authenticate.rb +10 -0
- data/app_manager-0.1.0.gem +0 -0
- data/app_manager.gemspec +3 -3
- data/config/routes.rb +14 -1
- data/lib/app_manager/actions.rb +10 -0
- data/lib/app_manager/api_cache_handler.rb +67 -0
- data/lib/app_manager/client/banners.rb +1 -1
- data/lib/app_manager/client/connection.rb +37 -3
- data/lib/app_manager/client/plans.rb +40 -0
- data/lib/app_manager/client.rb +15 -7
- data/lib/app_manager/config.rb +82 -0
- data/lib/app_manager/fail_safe.rb +324 -0
- data/lib/app_manager/graphql_helper.rb +125 -0
- data/lib/app_manager/model.rb +98 -0
- data/lib/app_manager/protection.rb +21 -0
- data/lib/app_manager/railtie.rb +10 -0
- data/lib/app_manager/response_cache.rb +40 -0
- data/lib/app_manager/tasks/sync/local_app_manager.rake +12 -0
- data/lib/app_manager/version.rb +1 -1
- data/lib/app_manager.rb +22 -2
- data/lib/generators/app_manager/install/install_generator.rb +8 -0
- data/lib/generators/app_manager/install/templates/app_manager.rb.tt +61 -0
- metadata +46 -2
@@ -0,0 +1,125 @@
|
|
1
|
+
module AppManager
|
2
|
+
class GraphqlHelper
|
3
|
+
|
4
|
+
def initialize(shopify_domain,shopify_token)
|
5
|
+
@api_key = AppManager.configuration.shopify_api_key || nil
|
6
|
+
@api_version = AppManager.configuration.shopify_api_version || nil
|
7
|
+
@shopify_domain = shopify_domain
|
8
|
+
@shopify_token = shopify_token
|
9
|
+
end
|
10
|
+
|
11
|
+
def rest_client
|
12
|
+
|
13
|
+
end
|
14
|
+
|
15
|
+
def api_call(query,is_json=false)
|
16
|
+
require 'uri'
|
17
|
+
require 'net/http'
|
18
|
+
if !@api_key.nil? && !@api_version.nil? && !@shopify_domain.nil? && !@shopify_token.nil?
|
19
|
+
url = URI("https://#{@api_key}:#{@shopify_token}@#{@shopify_domain}/admin/api/#{@api_version}/graphql.json")
|
20
|
+
http = Net::HTTP.new(url.host, url.port)
|
21
|
+
http.use_ssl = true
|
22
|
+
http.verify_mode = OpenSSL::SSL::VERIFY_PEER
|
23
|
+
request = Net::HTTP::Post.new(url)
|
24
|
+
request["X-Shopify-Access-Token"] = @shopify_token
|
25
|
+
request.body = query
|
26
|
+
request["Accept"] = 'application/json'
|
27
|
+
if is_json.present?
|
28
|
+
request["Content-Type"] = 'application/json'
|
29
|
+
else
|
30
|
+
request["Content-Type"] = 'application/graphql'
|
31
|
+
end
|
32
|
+
response = http.request(request)
|
33
|
+
response = ActiveSupport::JSON.decode(response.read_body) rescue nil
|
34
|
+
return response
|
35
|
+
else
|
36
|
+
Rails.logger.info "=== params missing from any of these api_key, api_version, shopify_domain, shopify_token ==="
|
37
|
+
return []
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
def run_graph_api(query,variables = '',is_retrieve_all = false,after = '',is_json = true)
|
42
|
+
query = query.dup.force_encoding("UTF-8")
|
43
|
+
if variables.present?
|
44
|
+
query = {"query": "#{query}","variables": variables }
|
45
|
+
end
|
46
|
+
if is_json
|
47
|
+
response = api_call(query.to_json,true)
|
48
|
+
else
|
49
|
+
response = api_call(query,false)
|
50
|
+
end
|
51
|
+
return response
|
52
|
+
end
|
53
|
+
|
54
|
+
|
55
|
+
def recurring_charge_api_call(plan,return_url,shop)
|
56
|
+
plan_test = nil
|
57
|
+
shop_plan_field = AppManager.configuration.field_names['shopify_plan'] rescue nil
|
58
|
+
if !plan['affiliate'].nil? && plan['affiliate'].any? && !shop_plan_field.nil? && plan['affiliate'].map{|e| e['value']}.include?(shop[shop_plan_field])
|
59
|
+
plan_test = true
|
60
|
+
end
|
61
|
+
trial_days = plan['trial_days'] || 0
|
62
|
+
|
63
|
+
if shop && shop.shopify_domain && trial_days
|
64
|
+
trial_activated_at_field = AppManager.configuration.field_names['trial_activated_at'] rescue nil
|
65
|
+
trial_activated_at = shop[trial_activated_at] rescue nil
|
66
|
+
plan_field = AppManager.configuration.plan_id_or_name_field rescue nil
|
67
|
+
plan_id_field = shop[@plan_field] rescue nil
|
68
|
+
remaining_obj = AppManager::Client.new
|
69
|
+
remaining = remaining_obj.get_remaining_days(shop.shopify_domain,trial_activated_at,plan_id_field)
|
70
|
+
trial_days = !remaining.nil? ? remaining : trial_days
|
71
|
+
end
|
72
|
+
|
73
|
+
discount_type = plan['discount_type'] || "percentage"
|
74
|
+
discount_val = discount_type == "percentage" ? (plan['discount'].to_f/ 100) : plan['discount']
|
75
|
+
plan_discount = plan['discount'] ? { "durationLimitInIntervals" => (plan['discount_interval'] || nil), "value" => {"#{discount_type}" => discount_val} } : {}
|
76
|
+
|
77
|
+
price_details = {
|
78
|
+
"price": { "amount": plan['price'], "currencyCode": 'USD' },
|
79
|
+
"interval": plan['interval']['value']
|
80
|
+
}
|
81
|
+
price_details << plan_discount if plan_discount.any?
|
82
|
+
|
83
|
+
query = 'mutation(
|
84
|
+
$name: String!,
|
85
|
+
$returnUrl: URL!,
|
86
|
+
$trialDays: Int,
|
87
|
+
$test: Boolean,
|
88
|
+
$lineItems: [AppSubscriptionLineItemInput!]!
|
89
|
+
) {
|
90
|
+
appSubscriptionCreate(
|
91
|
+
name: $name,
|
92
|
+
returnUrl: $returnUrl,
|
93
|
+
trialDays: $trialDays,
|
94
|
+
test: $test,
|
95
|
+
lineItems: $lineItems
|
96
|
+
) {
|
97
|
+
userErrors {
|
98
|
+
field
|
99
|
+
message
|
100
|
+
}
|
101
|
+
confirmationUrl
|
102
|
+
appSubscription {
|
103
|
+
id
|
104
|
+
}
|
105
|
+
}
|
106
|
+
}'
|
107
|
+
|
108
|
+
variables = {
|
109
|
+
"name": plan['name'],
|
110
|
+
"returnUrl": return_url,
|
111
|
+
"trialDays": trial_days,
|
112
|
+
"test": plan_test,
|
113
|
+
"lineItems": [{
|
114
|
+
"plan": {
|
115
|
+
"appRecurringPricingDetails": price_details
|
116
|
+
}
|
117
|
+
}]
|
118
|
+
}
|
119
|
+
data = run_graph_api(query,variables)
|
120
|
+
return data
|
121
|
+
end
|
122
|
+
|
123
|
+
end
|
124
|
+
end
|
125
|
+
|
@@ -0,0 +1,98 @@
|
|
1
|
+
module AppManager
|
2
|
+
module Model
|
3
|
+
extend ActiveSupport::Concern
|
4
|
+
|
5
|
+
|
6
|
+
|
7
|
+
def has_plan
|
8
|
+
if !self[AppManager.configuration.plan_id_or_name_field]
|
9
|
+
return false;
|
10
|
+
end
|
11
|
+
plan_id = self.plan_id
|
12
|
+
if !plan_id
|
13
|
+
Rails.logger.info "Plan id found nil or not set"
|
14
|
+
return false;
|
15
|
+
end
|
16
|
+
remaining_days = self.get_remaining_days
|
17
|
+
if remaining_days > 0
|
18
|
+
return true
|
19
|
+
end
|
20
|
+
shopify_fields = @field_names = AppManager.configuration.field_names
|
21
|
+
plan_obj = AppManager::Client.new
|
22
|
+
active_charge = plan_obj.get_charge(shopify_fields['name']) rescue nil
|
23
|
+
return active_charge ? true : false
|
24
|
+
end
|
25
|
+
|
26
|
+
|
27
|
+
|
28
|
+
def plan_features
|
29
|
+
plan_id = self.plan_id
|
30
|
+
if !plan_id
|
31
|
+
raise "Plan id not found"
|
32
|
+
end
|
33
|
+
plan_obj = AppManager::Client.new
|
34
|
+
plans = plan_obj.get_plan(plan_id)
|
35
|
+
if plans && plans['features'].blank?
|
36
|
+
return []
|
37
|
+
end
|
38
|
+
features_by_plan = plans['features'].map { |h| h.slice('value', 'feature_id') }
|
39
|
+
all_features = AppManager.configuration.plan_features
|
40
|
+
feature_plan_ids = features_by_plan.map{|c| c['feature_id']}
|
41
|
+
pf = all_features.select{|x| feature_plan_ids.include?(x['uuid'])}.each{|g| g["value"] = features_by_plan.find{|e| e['feature_id'] == g['uuid']}['value'] }
|
42
|
+
return pf
|
43
|
+
end
|
44
|
+
|
45
|
+
|
46
|
+
|
47
|
+
def has_feature(slug)
|
48
|
+
self.plan_features.select{|x| x['slug'].to_s == slug }.size > 0
|
49
|
+
end
|
50
|
+
|
51
|
+
|
52
|
+
def get_feature(slug)
|
53
|
+
plan_features = self.plan_features
|
54
|
+
features = plan_features.select{|x| x['slug'].to_s == slug }
|
55
|
+
if features.any?
|
56
|
+
feature = features.first
|
57
|
+
return casted_value(feature['value'],feature['value_type'])
|
58
|
+
else
|
59
|
+
nil
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
def get_remaining_days
|
64
|
+
shop_domain = self[AppManager.configuration.shopify_domain_field]
|
65
|
+
trial_activated_at_field = AppManager.configuration.field_names['trial_activated_at']
|
66
|
+
trial_activated_at_val = self[trial_activated_at_field]
|
67
|
+
plan_field = AppManager.configuration.field_names['plan_id']
|
68
|
+
plan_id = self[plan_field]
|
69
|
+
plan_obj = AppManager::Client.new
|
70
|
+
shop_domain ? plan_obj.get_remaining_days(shop_domain,trial_activated_at_val,plan_id) : 0
|
71
|
+
end
|
72
|
+
|
73
|
+
private
|
74
|
+
|
75
|
+
def casted_value(value,value_type)
|
76
|
+
case value_type
|
77
|
+
when "integer"
|
78
|
+
value.to_i
|
79
|
+
when "boolean"
|
80
|
+
case value
|
81
|
+
when true, 'true', 1, '1'
|
82
|
+
return true
|
83
|
+
when false, 'false', 0, '0'
|
84
|
+
return false
|
85
|
+
end
|
86
|
+
when "string"
|
87
|
+
value.to_s
|
88
|
+
else
|
89
|
+
value
|
90
|
+
end
|
91
|
+
end
|
92
|
+
|
93
|
+
|
94
|
+
|
95
|
+
|
96
|
+
|
97
|
+
end
|
98
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
module AppManager
|
2
|
+
module Protection
|
3
|
+
extend ActiveSupport::Concern
|
4
|
+
def authorize_request
|
5
|
+
if request.headers['token'].present? && ENV['APP_MANAGER_ACCESS_TOKEN']
|
6
|
+
if request.headers['token'] === ENV['APP_MANAGER_ACCESS_TOKEN']
|
7
|
+
else
|
8
|
+
render json: {
|
9
|
+
error: "Invalid Token ",
|
10
|
+
status: 401
|
11
|
+
}, status: 401
|
12
|
+
end
|
13
|
+
else
|
14
|
+
render json: {
|
15
|
+
error: "Missing Token in request",
|
16
|
+
status: 401
|
17
|
+
}, status: 401
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,40 @@
|
|
1
|
+
module AppManager
|
2
|
+
module Actions
|
3
|
+
class ResponseCache
|
4
|
+
def initialize(cache_path)
|
5
|
+
@cache_path = cache_path
|
6
|
+
@expires_in = AppManager.configuration.expires_in || 1.day
|
7
|
+
end
|
8
|
+
|
9
|
+
def present?
|
10
|
+
cached_response.present?
|
11
|
+
end
|
12
|
+
|
13
|
+
def body
|
14
|
+
cached_response['body']
|
15
|
+
end
|
16
|
+
|
17
|
+
def status
|
18
|
+
cached_response['status']
|
19
|
+
end
|
20
|
+
|
21
|
+
def headers
|
22
|
+
cached_response['headers']
|
23
|
+
end
|
24
|
+
|
25
|
+
def cached_response
|
26
|
+
@cached_response ||= Rails.cache.read(@cache_path)
|
27
|
+
end
|
28
|
+
|
29
|
+
def write_cache(response)
|
30
|
+
cache_object = {
|
31
|
+
body: response.body,
|
32
|
+
status: response.status,
|
33
|
+
headers: response.headers
|
34
|
+
}.as_json
|
35
|
+
Rails.cache.write(@cache_path, cache_object, expires_in: @expires_in)
|
36
|
+
end
|
37
|
+
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
@@ -0,0 +1,12 @@
|
|
1
|
+
namespace :sync do
|
2
|
+
desc "This task sync local pending charge with app manager portal"
|
3
|
+
task local_app_manager: :environment do
|
4
|
+
begin
|
5
|
+
@fs = AppManager::FailSafe.new
|
6
|
+
@fs.sync_app_manager
|
7
|
+
puts "app manager sync successfully"
|
8
|
+
rescue Exception => e
|
9
|
+
Rails.logger.info "APP MANAGER SYNC TAKS FAILED #{e.inspect}"
|
10
|
+
end
|
11
|
+
end
|
12
|
+
end
|
data/lib/app_manager/version.rb
CHANGED
data/lib/app_manager.rb
CHANGED
@@ -2,11 +2,31 @@
|
|
2
2
|
|
3
3
|
require "httparty"
|
4
4
|
require "rails"
|
5
|
+
require 'sqlite3'
|
5
6
|
require "app_manager/version"
|
6
7
|
require "app_manager/engine"
|
7
8
|
require "app_manager/client"
|
8
|
-
|
9
|
+
require "app_manager/actions"
|
10
|
+
require "app_manager/api_cache_handler"
|
11
|
+
require "app_manager/graphql_helper"
|
12
|
+
require "app_manager/response_cache"
|
13
|
+
require "app_manager/config"
|
14
|
+
require "app_manager/protection"
|
15
|
+
require "app_manager/model"
|
16
|
+
require "app_manager/fail_safe"
|
17
|
+
require 'app_manager/railtie' if defined?(Rails)
|
9
18
|
|
10
19
|
module AppManager
|
11
|
-
|
20
|
+
def self.configure
|
21
|
+
yield configuration
|
22
|
+
end
|
23
|
+
|
24
|
+
def self.configuration
|
25
|
+
@configuration ||= AppManager::Config.new
|
26
|
+
end
|
27
|
+
|
28
|
+
def self.clear_cache
|
29
|
+
Rails.cache.delete_matched('api-response-cache/*')
|
30
|
+
end
|
31
|
+
|
12
32
|
end
|
@@ -8,6 +8,14 @@ module AppManager
|
|
8
8
|
route("mount AppManager::Engine, at: '/'")
|
9
9
|
end
|
10
10
|
|
11
|
+
# def create_initializer_file
|
12
|
+
# copy_file("plan_features.rb", "config/initializers/plan_features.rb")
|
13
|
+
# end
|
14
|
+
|
15
|
+
def create_app_manager_initializer
|
16
|
+
template("app_manager.rb", "config/initializers/app_manager.rb")
|
17
|
+
end
|
18
|
+
|
11
19
|
end
|
12
20
|
end
|
13
21
|
end
|
@@ -0,0 +1,61 @@
|
|
1
|
+
AppManager.configure do |config|
|
2
|
+
config.enable_caching = false # Optional, True to enable app-manager api response caching, default is enabled from gem
|
3
|
+
config.expires_in = 1.days # Optional, Example: 30.seconds, 5.minutes or 2.days Default caching is for 1.days from gem
|
4
|
+
config.app_url = '' # App URL like https://volumediscount.hulkapps.dev/ or #https://5044-2409-4052-209a-69da-9d7-925a-9418-a9c3.ngrok.io
|
5
|
+
config.shopify_api_key = '' # Shopify api key of app
|
6
|
+
config.shopify_api_version = '' # Must be 2022-04 or latest
|
7
|
+
config.shopify_table_name = 'shops' # Table name which is generated by shopify mostly it is 'shops'
|
8
|
+
config.shopify_domain_field = 'shopify_domain' #shopify domain field
|
9
|
+
config.plan_id_or_name_field = 'plan_id'
|
10
|
+
config.field_names = {
|
11
|
+
'name' => 'shopify_domain', # demo-rahul-tiwari.myshopify.com
|
12
|
+
'shopify_email' => 'email', # rahul.t@hulkapps.com
|
13
|
+
'shopify_token' => 'shopify_token',
|
14
|
+
'shopify_plan' => 'plan_name', # partner_test
|
15
|
+
'plan_id' => 'plan_id', # 1. t
|
16
|
+
'created_at' => 'created_at', # 2022-04-15 10:43:05
|
17
|
+
'trial_activated_at' => 'trial_activated_at' # field name that stores trial start/activated date
|
18
|
+
}
|
19
|
+
config.plan_features = [
|
20
|
+
{
|
21
|
+
"uuid" => "b48a3a6c-c1fb-11ec-9d64-0242ac120002",
|
22
|
+
"name" => "Features 1",
|
23
|
+
"slug" => "feature-1",
|
24
|
+
"description" => "Feature Description",
|
25
|
+
"value_type" => "integer",
|
26
|
+
"format" => "count",
|
27
|
+
"display_order" => 1
|
28
|
+
},
|
29
|
+
{
|
30
|
+
"uuid" => "9f18f95a-bfaf-11ec-9d64-0242ac120002",
|
31
|
+
"name" => "Features 2",
|
32
|
+
"slug" => "feature-2",
|
33
|
+
"description" => "Feature Description",
|
34
|
+
"value_type" => "boolean",
|
35
|
+
"format" => "percentage",
|
36
|
+
"display_order" => 2
|
37
|
+
},
|
38
|
+
{
|
39
|
+
"uuid" => "9f190a26-bfaf-11ec-9d64-0242ac120002",
|
40
|
+
"name" => "Features 3",
|
41
|
+
"slug" => "feature-3",
|
42
|
+
"description" => "Feature Description",
|
43
|
+
"value_type" => "string",
|
44
|
+
"format" => "string",
|
45
|
+
"display_order" => 3
|
46
|
+
},
|
47
|
+
{
|
48
|
+
"uuid" => "9f191340-bfaf-11ec-9d64-0242ac12000",
|
49
|
+
"name" => "Features 4",
|
50
|
+
"slug" => "feature-4",
|
51
|
+
"description" => "Feature Description",
|
52
|
+
"value_type" => "array",
|
53
|
+
"values" => [
|
54
|
+
"val 1",
|
55
|
+
"val 2"
|
56
|
+
],
|
57
|
+
"format" => "string",
|
58
|
+
"display_order" => 4
|
59
|
+
}
|
60
|
+
] #Required, Values type : integer, boolean, string, array
|
61
|
+
end
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: app_manager
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version:
|
4
|
+
version: 1.0.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Hulkapps
|
8
8
|
autorequire:
|
9
9
|
bindir: exe
|
10
10
|
cert_chain: []
|
11
|
-
date: 2022-
|
11
|
+
date: 2022-05-26 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: httparty
|
@@ -38,6 +38,34 @@ dependencies:
|
|
38
38
|
- - ">="
|
39
39
|
- !ruby/object:Gem::Version
|
40
40
|
version: 5.2.0
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: kaminari
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - ">="
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: 0.16.3
|
48
|
+
type: :runtime
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - ">="
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: 0.16.3
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
name: sqlite3
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - "~>"
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: 1.3.0
|
62
|
+
type: :runtime
|
63
|
+
prerelease: false
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - "~>"
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: 1.3.0
|
41
69
|
description: Provides helper function to access AppManager API
|
42
70
|
email: rahul.t@hulkapps.com
|
43
71
|
executables: []
|
@@ -54,17 +82,33 @@ files:
|
|
54
82
|
- Rakefile
|
55
83
|
- app/controllers/app_manager/application_controller.rb
|
56
84
|
- app/controllers/app_manager/banners_controller.rb
|
85
|
+
- app/controllers/app_manager/charges_controller.rb
|
86
|
+
- app/controllers/app_manager/plans_controller.rb
|
87
|
+
- app/controllers/concerns/app_manager/authenticate.rb
|
88
|
+
- app_manager-0.1.0.gem
|
57
89
|
- app_manager.gemspec
|
58
90
|
- bin/console
|
59
91
|
- bin/setup
|
60
92
|
- config/routes.rb
|
61
93
|
- lib/app_manager.rb
|
94
|
+
- lib/app_manager/actions.rb
|
95
|
+
- lib/app_manager/api_cache_handler.rb
|
62
96
|
- lib/app_manager/client.rb
|
63
97
|
- lib/app_manager/client/banners.rb
|
64
98
|
- lib/app_manager/client/connection.rb
|
99
|
+
- lib/app_manager/client/plans.rb
|
100
|
+
- lib/app_manager/config.rb
|
65
101
|
- lib/app_manager/engine.rb
|
102
|
+
- lib/app_manager/fail_safe.rb
|
103
|
+
- lib/app_manager/graphql_helper.rb
|
104
|
+
- lib/app_manager/model.rb
|
105
|
+
- lib/app_manager/protection.rb
|
106
|
+
- lib/app_manager/railtie.rb
|
107
|
+
- lib/app_manager/response_cache.rb
|
108
|
+
- lib/app_manager/tasks/sync/local_app_manager.rake
|
66
109
|
- lib/app_manager/version.rb
|
67
110
|
- lib/generators/app_manager/install/install_generator.rb
|
111
|
+
- lib/generators/app_manager/install/templates/app_manager.rb.tt
|
68
112
|
homepage: https://www.hulkapps.com
|
69
113
|
licenses: []
|
70
114
|
metadata: {}
|