ask-local 0.1.0 → 0.2.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/CHANGELOG.md +73 -32
- data/README.md +58 -9
- data/bin/askl +6 -0
- data/lib/ask/local/cli/boot.rb +150 -159
- data/lib/ask/local/cli/routes.rb +17 -13
- data/lib/ask/local/cli/system.rb +272 -2
- data/lib/ask/local/cli.rb +7 -3
- data/lib/ask/local/config.rb +342 -59
- data/lib/ask/local/procfile.rb +139 -0
- data/lib/ask/local/proxy_control.rb +38 -13
- data/lib/ask/local/resolver.rb +62 -94
- data/lib/ask/local/runner.rb +8 -3
- data/lib/ask/local/version.rb +1 -1
- data/lib/ask/skills/ask-local/SKILL.md +57 -33
- data/lib/ask-local.rb +1 -0
- metadata +4 -1
data/lib/ask/local/resolver.rb
CHANGED
|
@@ -2,124 +2,92 @@
|
|
|
2
2
|
|
|
3
3
|
module Ask
|
|
4
4
|
module Local
|
|
5
|
-
# Resolve
|
|
6
|
-
#
|
|
7
|
-
#
|
|
8
|
-
#
|
|
5
|
+
# Resolve hostnames from config/local.yml — the ONLY source of truth.
|
|
6
|
+
# No inference, no Procfile fallback, no ENV for naming. If the file
|
|
7
|
+
# is missing, resolve raises ConfigError telling the user to run
|
|
8
|
+
# `ask-local init`.
|
|
9
9
|
module Resolver
|
|
10
10
|
module_function
|
|
11
11
|
|
|
12
|
-
Result = Struct.new(:app, :
|
|
12
|
+
Result = Struct.new(:app, :tld, :host, :processes, :secrets,
|
|
13
|
+
:sources, :variant, keyword_init: true)
|
|
13
14
|
|
|
14
|
-
def resolve(dir = Dir.pwd,
|
|
15
|
-
|
|
16
|
-
config
|
|
17
|
-
|
|
18
|
-
|
|
15
|
+
def resolve(dir = Dir.pwd, variant: nil, tld: nil, host: nil)
|
|
16
|
+
config = Config.load(dir, variant: variant)
|
|
17
|
+
unless config
|
|
18
|
+
raise ConfigError, Config.missing_message(dir)
|
|
19
|
+
end
|
|
19
20
|
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
[app_cfg["name"], config_source],
|
|
24
|
-
[Inference.infer(dir), :infer]
|
|
25
|
-
)
|
|
26
|
-
app, app_source = app_source == :infer ? app : [app, app_source]
|
|
21
|
+
service = config.service
|
|
22
|
+
proxy = config.proxy_config
|
|
23
|
+
processes = config.processes
|
|
27
24
|
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
[ENV["ASK_LOCAL_SERVICE"], "ASK_LOCAL_SERVICE"],
|
|
31
|
-
[app_cfg["service"], config_source]
|
|
32
|
-
)
|
|
25
|
+
variant_name = (variant || ENV["ASK_LOCAL_VARIANT"])&.strip
|
|
26
|
+
variant_name = nil if variant_name&.empty?
|
|
33
27
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
[app_cfg["variant"], config_source]
|
|
39
|
-
)
|
|
28
|
+
# host or tld: config proxy.host wins over proxy.tld; flags override.
|
|
29
|
+
if host || proxy["host"]
|
|
30
|
+
effective_host = (host || proxy["host"]).to_s.strip.downcase
|
|
31
|
+
effective_tld = nil
|
|
40
32
|
else
|
|
41
|
-
|
|
33
|
+
effective_tld = (tld || proxy["tld"] || Hostname::DEFAULT_TLD).to_s.strip.downcase
|
|
34
|
+
effective_tld = effective_tld.gsub(/\A\./, "")
|
|
35
|
+
unless Sanitize.valid_tld?(effective_tld)
|
|
36
|
+
raise ConfigError, "Invalid tld #{effective_tld.inspect} in #{config.path} proxy.tld"
|
|
37
|
+
end
|
|
38
|
+
effective_host = nil
|
|
42
39
|
end
|
|
43
40
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
tld_list = tld_list.map(&:downcase).uniq
|
|
41
|
+
sources = {
|
|
42
|
+
app: config.path.to_s,
|
|
43
|
+
tld: proxy["tld"] ? "#{config.path} proxy.tld" : "default (localhost)",
|
|
44
|
+
host: proxy["host"] ? "#{config.path} proxy.host" : nil,
|
|
45
|
+
variant: variant_name ? "config/local.#{variant_name}.yml" : nil
|
|
46
|
+
}
|
|
51
47
|
|
|
52
48
|
Result.new(
|
|
53
|
-
app: Sanitize.hostname_label(
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
49
|
+
app: Sanitize.hostname_label(service),
|
|
50
|
+
tld: effective_tld,
|
|
51
|
+
host: effective_host,
|
|
52
|
+
processes: processes,
|
|
53
|
+
secrets: config.secrets,
|
|
54
|
+
sources: sources,
|
|
55
|
+
variant: variant_name
|
|
58
56
|
)
|
|
59
57
|
end
|
|
60
58
|
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
59
|
+
# Hostname for one process: primary (first proxy:true) is bare;
|
|
60
|
+
# others are proc.app.tld; proxy:false have none.
|
|
61
|
+
def hostname_for(result, proc_name)
|
|
62
|
+
entry = result.processes[proc_name]
|
|
63
|
+
return nil unless entry
|
|
64
|
+
return nil if entry["proxy"] == false
|
|
65
65
|
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
def find_config(dir)
|
|
70
|
-
current = File.expand_path(dir)
|
|
71
|
-
fallback = nil
|
|
72
|
-
loop do
|
|
73
|
-
begin
|
|
74
|
-
loaded = Config.load(current)
|
|
75
|
-
if loaded
|
|
76
|
-
if loaded.data["apps"].is_a?(Hash)
|
|
77
|
-
return [loaded, current]
|
|
78
|
-
else
|
|
79
|
-
fallback ||= [loaded, current]
|
|
80
|
-
end
|
|
81
|
-
end
|
|
82
|
-
rescue ConfigError
|
|
83
|
-
nil
|
|
84
|
-
end
|
|
85
|
-
parent = File.dirname(current)
|
|
86
|
-
break if parent == current
|
|
66
|
+
base = result.host ? result.host : "#{result.app}.#{result.tld}"
|
|
67
|
+
proc_name.to_s == primary_proc(result) ? base : "#{proc_name}.#{base}"
|
|
68
|
+
end
|
|
87
69
|
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
fallback || [nil, nil]
|
|
70
|
+
def primary_proc(result)
|
|
71
|
+
result.processes.find { |_, v| v["proxy"] != false }&.first
|
|
91
72
|
end
|
|
92
73
|
|
|
93
|
-
|
|
94
|
-
|
|
74
|
+
# All HTTP hostnames (for route registration / doctor).
|
|
75
|
+
def hostnames(result)
|
|
76
|
+
result.processes.filter_map { |name, entry|
|
|
77
|
+
next if entry["proxy"] == false
|
|
95
78
|
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
.relative_path_from(Pathname.new(File.expand_path(config_dir))).to_s
|
|
99
|
-
rescue ArgumentError
|
|
100
|
-
"."
|
|
79
|
+
hostname_for(result, name)
|
|
80
|
+
}
|
|
101
81
|
end
|
|
102
82
|
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
pairs.each do |value, source|
|
|
107
|
-
if source == :infer
|
|
108
|
-
inferred, from = value
|
|
109
|
-
return [inferred, from] unless inferred.nil? || inferred.to_s.empty?
|
|
110
|
-
elsif !value.nil? && !value.to_s.strip.empty?
|
|
111
|
-
return [value.to_s.strip, source]
|
|
112
|
-
end
|
|
113
|
-
end
|
|
114
|
-
[nil, nil]
|
|
83
|
+
def url_for(result, proc_name, port:, tls:)
|
|
84
|
+
host = hostname_for(result, proc_name)
|
|
85
|
+
host && Hostname.url(host, port: port, tls: tls)
|
|
115
86
|
end
|
|
116
87
|
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
parts = value.to_s.split(",").map(&:strip).reject(&:empty?)
|
|
122
|
-
parts.empty? ? nil : parts
|
|
88
|
+
# Effective URL list for display (status): primary bare, others prefixed.
|
|
89
|
+
def urls(result, port:, tls:)
|
|
90
|
+
hostnames(result).map { |h| Hostname.url(h, port: port, tls: tls) }
|
|
123
91
|
end
|
|
124
92
|
end
|
|
125
93
|
end
|
data/lib/ask/local/runner.rb
CHANGED
|
@@ -91,9 +91,10 @@ module Ask
|
|
|
91
91
|
end
|
|
92
92
|
|
|
93
93
|
# Run an arbitrary command with PORT + ASK_LOCAL_URL. Returns App.
|
|
94
|
-
def boot_run(name:, hostname:, url:, dir:, command:, port: nil, force: false
|
|
94
|
+
def boot_run(name:, hostname:, url:, dir:, command:, port: nil, force: false,
|
|
95
|
+
rails_dev_host: nil)
|
|
95
96
|
port ||= Ports.find_free
|
|
96
|
-
env = child_env(dir, url: url, port: port)
|
|
97
|
+
env = child_env(dir, url: url, port: port, rails_dev_host: rails_dev_host)
|
|
97
98
|
pid = with_clean_env { spawn(env, *command, chdir: dir) }
|
|
98
99
|
Process.detach(pid)
|
|
99
100
|
target = "127.0.0.1:#{port}"
|
|
@@ -103,12 +104,16 @@ module Ask
|
|
|
103
104
|
target: target, kind: "tcp", command: command)
|
|
104
105
|
end
|
|
105
106
|
|
|
106
|
-
def child_env(dir, url:, port:)
|
|
107
|
+
def child_env(dir, url:, port:, rails_dev_host: nil)
|
|
107
108
|
env = { "ASK_LOCAL_URL" => url }
|
|
108
109
|
env["PORT"] = port.to_s if port
|
|
109
110
|
env["HOST"] = "127.0.0.1"
|
|
110
111
|
ca = File.join(Certs.state_dir, "ca.pem")
|
|
111
112
|
env["NODE_EXTRA_CA_CERTS"] = ca if File.file?(ca)
|
|
113
|
+
# Rails blocks unknown Host headers in development. Allow the
|
|
114
|
+
# proxied hostname so Rails apps boot behind ask-local with zero
|
|
115
|
+
# config — this replaces the ask-local-rails hosts patch.
|
|
116
|
+
env["RAILS_DEVELOPMENT_HOSTS"] = rails_dev_host if rails_dev_host
|
|
112
117
|
env
|
|
113
118
|
end
|
|
114
119
|
|
data/lib/ask/local/version.rb
CHANGED
|
@@ -1,81 +1,105 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: ask-local
|
|
3
|
-
description: Run Ruby apps through ask-local for stable named .localhost URLs (e.g. https://myapp.localhost instead of http://localhost:3000). Use when booting dev servers (Rails, Rack, Roda, Sinatra, Jekyll
|
|
3
|
+
description: Run Ruby apps through ask-local for stable named .localhost URLs (e.g. https://myapp.localhost instead of http://localhost:3000). Use when booting dev servers (Rails, Rack, Roda, Sinatra, Jekyll), wiring frontend to API, configuring OAuth callbacks or webhooks, debugging port conflicts, or working in git worktrees.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# Local Dev with ask-local
|
|
7
7
|
|
|
8
8
|
Never invent ports. Never parse them from logs. Every app has a stable URL.
|
|
9
9
|
|
|
10
|
-
##
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
10
|
+
## One file, one command
|
|
11
|
+
|
|
12
|
+
Every app declares `config/local.yml` — the single source of truth:
|
|
13
|
+
|
|
14
|
+
```yaml
|
|
15
|
+
service: myapp
|
|
16
|
+
proxy:
|
|
17
|
+
tld: localhost
|
|
18
|
+
processes:
|
|
19
|
+
web:
|
|
20
|
+
cmd: bundle exec puma -b tcp://127.0.0.1:$PORT config.ru
|
|
21
|
+
proxy: true # gets https://myapp.localhost
|
|
22
|
+
worker:
|
|
23
|
+
cmd: bundle exec sidekiq
|
|
24
|
+
proxy: false # background, supervised, no URL
|
|
25
|
+
env:
|
|
26
|
+
clear:
|
|
27
|
+
RAILS_ENV: development
|
|
17
28
|
```
|
|
18
29
|
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
## Lifecycle
|
|
30
|
+
If the file is missing, ask-local prints `Run ask-local init`. Generate it
|
|
31
|
+
with `ask-local init` (migrates an existing Procfile). Rails apps need
|
|
32
|
+
no extra gem — the proxied hostname is allowed automatically via
|
|
33
|
+
`RAILS_DEVELOPMENT_HOSTS`.
|
|
24
34
|
|
|
25
35
|
```bash
|
|
26
|
-
ask-local
|
|
27
|
-
ask-local
|
|
28
|
-
ask-local
|
|
36
|
+
ask-local start # setup if needed, then boot every process
|
|
37
|
+
ask-local # same as start
|
|
38
|
+
ask-local stop # stop this app's backend + routes
|
|
39
|
+
ask-local status # show service, processes, and URLs
|
|
40
|
+
ask-local log [-f] # tail the web process log
|
|
29
41
|
```
|
|
30
42
|
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
43
|
+
`$PORT` and `ASK_LOCAL_URL` are injected per process; HTTP processes get
|
|
44
|
+
stable URLs, background ones are supervised without routes. A process
|
|
45
|
+
that exits cleans up the whole tree.
|
|
34
46
|
|
|
35
47
|
## Cross-service wiring
|
|
36
48
|
|
|
37
49
|
```bash
|
|
38
50
|
ask-local get backend # -> https://backend.localhost
|
|
51
|
+
ask-local get backend --variant demo
|
|
39
52
|
```
|
|
40
53
|
|
|
41
54
|
Use `get` output for frontend-to-API URLs, Cable URLs, and webhook
|
|
42
55
|
targets. Do not guess ports.
|
|
43
56
|
|
|
44
|
-
## Variants
|
|
57
|
+
## Variants are files
|
|
58
|
+
|
|
59
|
+
A variant is a file overlay, Kamal-style: `config/local.<variant>.yml`
|
|
60
|
+
deep-merges over `config/local.yml`. Select it with `ASK_LOCAL_VARIANT`
|
|
61
|
+
or `--variant`. Worktrees get a branch prefix automatically.
|
|
62
|
+
|
|
63
|
+
```bash
|
|
64
|
+
ASK_LOCAL_VARIANT=fix-ui ask-local # boots with config/local.fix-ui.yml merged
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
## First time on a machine
|
|
45
68
|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
69
|
+
Run `ask-local start` — it does the one-shot CA trust, port 443, and
|
|
70
|
+
hosts sync if anything is missing, then boots. Prefer `ask-local setup`
|
|
71
|
+
for workstation setup without booting. If any command fails with a
|
|
72
|
+
privileged-port error, do not work around it with `-p` — run
|
|
73
|
+
`ask-local setup` instead. A `:port` suffix in a URL means someone
|
|
74
|
+
explicitly opted into it.
|
|
49
75
|
|
|
50
76
|
## OAuth and webhooks
|
|
51
77
|
|
|
52
|
-
Build callback URLs from `ASK_LOCAL_URL
|
|
78
|
+
Build callback URLs from `ASK_LOCAL_URL` (injected into every HTTP
|
|
79
|
+
process):
|
|
53
80
|
|
|
54
81
|
```ruby
|
|
55
82
|
callback = "#{Ask::Local::Rails.url}/auth/google/callback"
|
|
56
83
|
```
|
|
57
84
|
|
|
58
85
|
Strict providers (Google, Apple) reject `.localhost`. Serve the app on a
|
|
59
|
-
domain you own instead — no code change:
|
|
86
|
+
domain you own instead — no code change, just config:
|
|
60
87
|
|
|
61
|
-
```
|
|
62
|
-
|
|
88
|
+
```yaml
|
|
89
|
+
proxy:
|
|
90
|
+
host: myapp.local.example.com # instead of tld: localhost
|
|
63
91
|
```
|
|
64
92
|
|
|
65
93
|
## Troubleshooting
|
|
66
94
|
|
|
67
95
|
```bash
|
|
68
96
|
ask-local doctor # read-only: proxy, routes, DNS, CA trust
|
|
69
|
-
ask-local list
|
|
97
|
+
ask-local list --json # routes as stable JSON
|
|
70
98
|
ask-local prune # clear stale routes from crashed sessions
|
|
71
99
|
```
|
|
72
100
|
|
|
73
|
-
Prefer `list --json`, `status --json`, and `doctor --json` when parsing
|
|
74
|
-
output programmatically — stable keys, no prose scraping.
|
|
75
|
-
|
|
76
101
|
If a hostname does not resolve: `ask-local hosts sync`. If the browser
|
|
77
|
-
warns about TLS: `ask-local trust`.
|
|
78
|
-
alongside ask-local — they bypass routing and reintroduce conflicts.
|
|
102
|
+
warns about TLS: `ask-local trust`.
|
|
79
103
|
|
|
80
104
|
## When NOT to use ask-local
|
|
81
105
|
|
data/lib/ask-local.rb
CHANGED
|
@@ -22,6 +22,7 @@ require_relative "ask/local/resolver"
|
|
|
22
22
|
require_relative "ask/local/trust"
|
|
23
23
|
require_relative "ask/local/doctor"
|
|
24
24
|
require_relative "ask/local/cli/context"
|
|
25
|
+
require_relative "ask/local/procfile"
|
|
25
26
|
require_relative "ask/local/cli/boot"
|
|
26
27
|
require_relative "ask/local/cli/routes"
|
|
27
28
|
require_relative "ask/local/cli/system"
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: ask-local
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.2.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Kaka Ruto
|
|
@@ -73,6 +73,7 @@ email:
|
|
|
73
73
|
- kaka@myrrlabs.com
|
|
74
74
|
executables:
|
|
75
75
|
- ask-local
|
|
76
|
+
- askl
|
|
76
77
|
extensions: []
|
|
77
78
|
extra_rdoc_files: []
|
|
78
79
|
files:
|
|
@@ -80,6 +81,7 @@ files:
|
|
|
80
81
|
- LICENSE
|
|
81
82
|
- README.md
|
|
82
83
|
- bin/ask-local
|
|
84
|
+
- bin/askl
|
|
83
85
|
- lib/ask-local.rb
|
|
84
86
|
- lib/ask/local/certs.rb
|
|
85
87
|
- lib/ask/local/cli.rb
|
|
@@ -97,6 +99,7 @@ files:
|
|
|
97
99
|
- lib/ask/local/log.rb
|
|
98
100
|
- lib/ask/local/ownership.rb
|
|
99
101
|
- lib/ask/local/ports.rb
|
|
102
|
+
- lib/ask/local/procfile.rb
|
|
100
103
|
- lib/ask/local/proxy.rb
|
|
101
104
|
- lib/ask/local/proxy_control.rb
|
|
102
105
|
- lib/ask/local/resolver.rb
|