proctolog 0.2.0 → 0.3.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 +34 -0
- data/README.md +18 -15
- data/app/controllers/proctolog/application_controller.rb +1 -1
- data/app/models/proctolog/doctor.rb +14 -11
- data/app/models/proctolog/localhost.rb +17 -0
- data/app/models/proctolog/server.rb +1 -7
- data/app/models/proctolog/setup.rb +190 -0
- data/bin/proctolog +70 -29
- data/completions/_proctolog +3 -2
- data/docs/manual/pr-format.md +1 -1
- data/docs/manual/setup.md +59 -0
- data/docs/manual/sharing.md +1 -1
- data/docs/manual/updates.md +11 -4
- data/lib/proctolog/changelog/news.rb +67 -0
- data/lib/proctolog/changelog.rb +35 -10
- data/lib/proctolog/installation.rb +19 -0
- data/lib/proctolog/version.rb +1 -1
- metadata +5 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 7e70671fa7a287f7273aa19d34e110411abdaf6eaa53af119c538e6f9f558b83
|
|
4
|
+
data.tar.gz: 715e723cad15056e1c5e878e21f89895e10a8a970550a6eb84183e9ccfc2c84a
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: ab6e65d6dffbb0cf8cfa2b1dd228879e3aae55f32f5118a77873c5e69fab28e840dfce9240cf44d88efc30e59a7cb4b8e271d800e987126e6c3514d60a718109
|
|
7
|
+
data.tar.gz: 38aa55282ff812f1eb8ff1fe22b6dbff1493f0debfe592c6b57f6e21a0cd6a2b7343ad86d45363202cfa3318070f9116a71039bffec371866a60c2cc8d1680f4
|
data/CHANGELOG.md
CHANGED
|
@@ -6,6 +6,40 @@ adds, what it changes and what it fixes. Versions are semantic
|
|
|
6
6
|
something of you says what under **You do**. Unreleased is what main has that
|
|
7
7
|
no gem has yet.
|
|
8
8
|
|
|
9
|
+
## 0.3.0
|
|
10
|
+
|
|
11
|
+
### Added
|
|
12
|
+
|
|
13
|
+
- `proctolog setup` is the one command to remember: it takes you from
|
|
14
|
+
wherever you are to an open desk, a step at a time. It links PRoctolog's
|
|
15
|
+
command, completion and skills, then goes through doctor's checks in the
|
|
16
|
+
order you meet them, each saying why it matters, checked again when you
|
|
17
|
+
press Enter, and stamped done or left for later. Outside a checkout, it
|
|
18
|
+
asks for one. Its sharing step says what Tailscale and ngrok each give, and
|
|
19
|
+
where to get them. Of anyone else's, it installs nothing
|
|
20
|
+
([docs/manual/setup.md](docs/manual/setup.md)). A bare `proctolog`, and
|
|
21
|
+
`serve` outside a checkout, point to it.
|
|
22
|
+
- `proctolog update` says what's new since the version you had, from this
|
|
23
|
+
changelog: each change by its first sentence, under Added, Changed and
|
|
24
|
+
Fixed, and in full what you must do. It starts with the update after this
|
|
25
|
+
one, as the version you leave is the one that runs it.
|
|
26
|
+
|
|
27
|
+
### Changed
|
|
28
|
+
|
|
29
|
+
- `proctolog install` is another name for `setup`: it links as before, then
|
|
30
|
+
takes you through the rest. After an update it only links, and says what's
|
|
31
|
+
new.
|
|
32
|
+
- `doctor` says how to install Chrome when it is missing.
|
|
33
|
+
|
|
34
|
+
### Fixed
|
|
35
|
+
|
|
36
|
+
- `proctolog update` said it pulled a checkout forward, and to restart the
|
|
37
|
+
desk, when origin had nothing newer. It says the checkout is the latest now,
|
|
38
|
+
and changes nothing.
|
|
39
|
+
- On macOS 27 the desk took every port on this Mac for answering: Try it
|
|
40
|
+
showed the app up and never started it, `doctor` said port 4040 was taken,
|
|
41
|
+
and `proctolog share start` shared a desk that was not there.
|
|
42
|
+
|
|
9
43
|
## 0.2.0
|
|
10
44
|
|
|
11
45
|
### Added
|
data/README.md
CHANGED
|
@@ -12,16 +12,16 @@ it works.
|
|
|
12
12
|
|
|
13
13
|
## The short version
|
|
14
14
|
|
|
15
|
-
|
|
16
|
-
whose PRs you review:
|
|
15
|
+
With [rv](https://rv.dev), from a checkout of the repo whose PRs you review:
|
|
17
16
|
|
|
18
17
|
```sh
|
|
19
|
-
rvx proctolog install
|
|
20
18
|
cd path/to/your-repo
|
|
21
|
-
proctolog
|
|
22
|
-
proctolog serve # then open http://proctolog.localhost:4040
|
|
19
|
+
rvx proctolog setup # a step at a time, to http://proctolog.localhost:4040
|
|
23
20
|
```
|
|
24
21
|
|
|
22
|
+
`setup` is the one command to remember: it links PRoctolog's own pieces, says
|
|
23
|
+
what to do about the rest, and opens the desk. `install` is another name for it.
|
|
24
|
+
|
|
25
25
|
Pick a PR, a stack, or a few ticked ones, and press `?` once you're in. When
|
|
26
26
|
your agent opens PRs, tell it to "write these PRs for PRoctolog", so they come
|
|
27
27
|
with decisions to review. Everything below is the long version.
|
|
@@ -41,11 +41,10 @@ brew install diffle imagemagick ffmpeg
|
|
|
41
41
|
With rv, whatever Ruby your shell or your repo runs:
|
|
42
42
|
|
|
43
43
|
```sh
|
|
44
|
-
rvx proctolog
|
|
45
|
-
proctolog doctor
|
|
44
|
+
rvx proctolog setup
|
|
46
45
|
```
|
|
47
46
|
|
|
48
|
-
rv gives PRoctolog a Ruby 4 and gems of its own, and `
|
|
47
|
+
rv gives PRoctolog a Ruby 4 and gems of its own, and `setup` puts a
|
|
49
48
|
`proctolog` in `~/.local/bin` that runs it through `rvx`. The apps you review
|
|
50
49
|
keep their own Ruby: what the desk starts for them never sees PRoctolog's.
|
|
51
50
|
rv installs from gem.coop, which mirrors RubyGems.
|
|
@@ -54,20 +53,24 @@ With Ruby 4.0 already in your shell:
|
|
|
54
53
|
|
|
55
54
|
```sh
|
|
56
55
|
gem install proctolog
|
|
57
|
-
proctolog
|
|
58
|
-
proctolog doctor
|
|
56
|
+
proctolog setup
|
|
59
57
|
```
|
|
60
58
|
|
|
61
|
-
Either way `
|
|
62
|
-
and the agent skills (below) into `~/.claude/skills` and `~/.agents/skills`.
|
|
63
|
-
|
|
59
|
+
Either way `setup` first links the zsh completion into `~/.zsh/completions`
|
|
60
|
+
and the agent skills (below) into `~/.claude/skills` and `~/.agents/skills`.
|
|
61
|
+
Then it takes the rest a step at a time: gh, the checkout, a first burst, what
|
|
62
|
+
turns features on, sharing the desk with your phone. Each step says why it
|
|
63
|
+
matters, checks again once you've acted, and is stamped done or left for
|
|
64
|
+
later; the last one opens the desk ([setting up](docs/manual/setup.md)). Run it
|
|
65
|
+
again whenever: what's done is stamped and passed. `doctor` asks the same
|
|
66
|
+
things all at once, a line each with the fix: `!!` for what the desk can't do
|
|
64
67
|
without, `--` for what only turns a feature off.
|
|
65
68
|
|
|
66
69
|
When a newer PRoctolog is out, the desk says so, and `proctolog update` takes
|
|
67
70
|
it ([versions and updates](docs/manual/updates.md)).
|
|
68
71
|
|
|
69
72
|
Working on PRoctolog itself? Run it from a checkout instead: clone it, then
|
|
70
|
-
`bundle install &&
|
|
73
|
+
`bundle install && bin/proctolog setup`, which also puts a `proctolog` that runs
|
|
71
74
|
the checkout in `~/.local/bin`. Keep the checkout where it is, since
|
|
72
75
|
everything links back to it. The desk tells a checkout of a newer release
|
|
73
76
|
too, and `proctolog update` pulls it forward.
|
|
@@ -104,7 +107,7 @@ Your review is saved as you go, on your Mac under `~/.local/share/proctolog/`.
|
|
|
104
107
|
|
|
105
108
|
PRoctolog finds decisions under a `## Decisions for the owner` heading, one
|
|
106
109
|
checkbox line each. The `proctolog-pr-writing` skill teaches your agent the
|
|
107
|
-
format, and `proctolog
|
|
110
|
+
format, and `proctolog setup` already put it where the agent looks. Tell it to
|
|
108
111
|
"write these PRs for PRoctolog", or "rewrite this PR's description for
|
|
109
112
|
PRoctolog" for one that's open. Check a description before publishing it:
|
|
110
113
|
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
module Proctolog
|
|
4
4
|
class ApplicationController < ActionController::Base
|
|
5
|
-
rescue_from(Github::Unreachable) { render plain: "GitHub did not answer.\n\n#{it.message}", status: :service_unavailable }
|
|
5
|
+
rescue_from(Github::Unreachable) { render plain: "GitHub did not answer.\n\n#{it.message}\n\nIf gh is not set up on this Mac yet, proctolog setup takes you through it.", status: :service_unavailable }
|
|
6
6
|
rescue_from(ActionController::InvalidAuthenticityToken) do
|
|
7
7
|
redirect_back_or_to root_path, alert: "The desk did not know this page any more, so nothing was saved. Do it once more."
|
|
8
8
|
end
|
|
@@ -11,6 +11,7 @@ module Proctolog
|
|
|
11
11
|
MARKS = {fine: "ok", missing: "!!", off: "--"}.freeze
|
|
12
12
|
|
|
13
13
|
Finding = Data.define(:state, :line) do
|
|
14
|
+
def fine? = state == :fine
|
|
14
15
|
def missing? = state == :missing
|
|
15
16
|
def to_s = "#{MARKS.fetch(state)} #{line}"
|
|
16
17
|
end
|
|
@@ -23,7 +24,16 @@ module Proctolog
|
|
|
23
24
|
@project, @home, @path = project, Pathname(home), path.split(File::PATH_SEPARATOR)
|
|
24
25
|
end
|
|
25
26
|
|
|
26
|
-
def findings =
|
|
27
|
+
def findings = (NEEDS + NICETIES).filter_map { self[it] }
|
|
28
|
+
|
|
29
|
+
# One check's finding, by its name, asked once: setup asks a step's
|
|
30
|
+
# checks alone. Nil for one not asked here, as the repository is not
|
|
31
|
+
# without gh.
|
|
32
|
+
def [](check)
|
|
33
|
+
raise ArgumentError, "Doctor checks no #{check}" unless (NEEDS + NICETIES).include?(check)
|
|
34
|
+
|
|
35
|
+
(@checked ||= {}).fetch(check) { @checked[check] = send(check) }
|
|
36
|
+
end
|
|
27
37
|
|
|
28
38
|
def fine? = findings.none?(&:missing?)
|
|
29
39
|
|
|
@@ -72,7 +82,7 @@ module Proctolog
|
|
|
72
82
|
def skills
|
|
73
83
|
folders = %w[.claude/skills .agents/skills].select { |folder| SKILLS.keys.all? { @home.join(folder, it).exist? } }
|
|
74
84
|
missing = SKILLS.reject { |skill, _| %w[.claude/skills .agents/skills].any? { @home.join(it, skill).exist? } }.values
|
|
75
|
-
if missing.any? then off("The #{missing.to_sentence} #{"skill".pluralize(missing.size)} #{missing.one? ? "is" : "are"} not linked: proctolog
|
|
85
|
+
if missing.any? then off("The #{missing.to_sentence} #{"skill".pluralize(missing.size)} #{missing.one? ? "is" : "are"} not linked: proctolog setup")
|
|
76
86
|
else fine("The #{SKILLS.values.to_sentence} skills, in ~/#{folders.join(" and ~/")}")
|
|
77
87
|
end
|
|
78
88
|
end
|
|
@@ -82,7 +92,7 @@ module Proctolog
|
|
|
82
92
|
|
|
83
93
|
def chrome
|
|
84
94
|
require "ferrum"
|
|
85
|
-
Ferrum::Browser::Options::Chrome.options.detect_path ? fine("Chrome, for browser tests and scenarios") : off("No Chrome
|
|
95
|
+
Ferrum::Browser::Options::Chrome.options.detect_path ? fine("Chrome, for browser tests and scenarios") : off("No Chrome, so browser tests skip and scenarios cannot open: brew install --cask google-chrome")
|
|
86
96
|
end
|
|
87
97
|
|
|
88
98
|
def diffle = command?("diffle") ? fine("Diffle, for d on a pull request") : off("No Diffle, so d opens no code: brew install diffle")
|
|
@@ -112,7 +122,7 @@ module Proctolog
|
|
|
112
122
|
off("Nothing shares the desk beyond this Mac. #{unready.map { "#{it.unready}." }.join(" ")}")
|
|
113
123
|
end
|
|
114
124
|
|
|
115
|
-
def port = answers?(4040) ? off("Port 4040 answers already: a desk may be open there, or serve --port 4041") : fine("Port 4040 is free")
|
|
125
|
+
def port = Localhost.answers?(4040, within: 1) ? off("Port 4040 answers already: a desk may be open there, or serve --port 4041") : fine("Port 4040 is free")
|
|
116
126
|
|
|
117
127
|
def fine(line) = Finding.new(:fine, line)
|
|
118
128
|
def missing(line) = Finding.new(:missing, line)
|
|
@@ -124,12 +134,5 @@ module Proctolog
|
|
|
124
134
|
def runnable?(program) = program.include?("/") ? @project.root.join(program).executable? : command?(program)
|
|
125
135
|
|
|
126
136
|
def succeeds?(*command) = Open3.capture2e({"PATH" => @path.join(File::PATH_SEPARATOR)}, *command).last.success?
|
|
127
|
-
|
|
128
|
-
def answers?(port)
|
|
129
|
-
Socket.tcp("localhost", port, connect_timeout: 1, &:close)
|
|
130
|
-
true
|
|
131
|
-
rescue SystemCallError
|
|
132
|
-
false
|
|
133
|
-
end
|
|
134
137
|
end
|
|
135
138
|
end
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "socket"
|
|
4
|
+
|
|
5
|
+
module Proctolog
|
|
6
|
+
# This Mac, as a program on it is reached: by a port something answers on.
|
|
7
|
+
module Localhost
|
|
8
|
+
# TCPSocket, not Socket.tcp: on macOS 27, Socket.tcp with a connect
|
|
9
|
+
# timeout takes a port nothing listens on for one that answers.
|
|
10
|
+
def self.answers?(port, within: 0.5)
|
|
11
|
+
TCPSocket.new("localhost", port, connect_timeout: within).close
|
|
12
|
+
true
|
|
13
|
+
rescue SystemCallError, IO::TimeoutError
|
|
14
|
+
false
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
@@ -35,7 +35,7 @@ module Proctolog
|
|
|
35
35
|
end
|
|
36
36
|
|
|
37
37
|
# Every port the app listens on answers.
|
|
38
|
-
def up? = app.listens(port).all? {
|
|
38
|
+
def up? = app.listens(port).all? { Localhost.answers?(it) }
|
|
39
39
|
|
|
40
40
|
def state
|
|
41
41
|
return :up if up?
|
|
@@ -179,12 +179,6 @@ module Proctolog
|
|
|
179
179
|
|
|
180
180
|
def logs = Project.current.cache.join("servers")
|
|
181
181
|
|
|
182
|
-
def listening?(port)
|
|
183
|
-
Socket.tcp("localhost", port, connect_timeout: 0.5) { true }
|
|
184
|
-
rescue SystemCallError
|
|
185
|
-
false
|
|
186
|
-
end
|
|
187
|
-
|
|
188
182
|
def git(*) = Open3.capture2e("git", "-C", checkout.to_s, *)
|
|
189
183
|
|
|
190
184
|
def tip(name)
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Proctolog
|
|
4
|
+
# From nothing to an open desk, a step at a time: PRoctolog's own links,
|
|
5
|
+
# then doctor's checks in the order a newcomer meets them, each step saying
|
|
6
|
+
# why it matters. It explains, links to what to install and checks again
|
|
7
|
+
# once the owner says a step is done; of anyone else's, it installs
|
|
8
|
+
# nothing. `proctolog setup` walks it, and `install` is another name for it.
|
|
9
|
+
#
|
|
10
|
+
# A step is done when every one of its checks is fine. A nicety that is
|
|
11
|
+
# off never holds it up: the owner may leave it for later. A need that is
|
|
12
|
+
# missing does, unless the step says what does its work until then, as the
|
|
13
|
+
# desk's first page starts a burst.
|
|
14
|
+
class Setup
|
|
15
|
+
# first names what the setup does itself before checking the step; asks,
|
|
16
|
+
# what the owner may type in place of Enter: the step's folder.
|
|
17
|
+
Step = Data.define(:title, :why, :checks, :advice, :until_then, :first, :asks) do
|
|
18
|
+
def initialize(title:, why:, checks:, advice: [], until_then: nil, first: nil, asks: nil) = super
|
|
19
|
+
|
|
20
|
+
def findings(doctor) = checks.filter_map { doctor[it] }
|
|
21
|
+
|
|
22
|
+
# Whether the owner may go on without it: nothing it needs is missing,
|
|
23
|
+
# or something does that work meanwhile.
|
|
24
|
+
def leavable?(findings) = until_then.present? || findings.none?(&:missing?)
|
|
25
|
+
|
|
26
|
+
def question(findings) = [asks, "Enter checks again", ("l leaves it for later" if leavable?(findings)), "q stops"].compact.join(", ")
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
class << self
|
|
30
|
+
def steps = @steps ||= []
|
|
31
|
+
|
|
32
|
+
private
|
|
33
|
+
|
|
34
|
+
def step(title, **) = steps << Step.new(title:, **)
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
step "PRoctolog on this Mac", checks: %i[ruby launcher skills release], first: :link,
|
|
38
|
+
why: "From wherever this PRoctolog lives, setup links the proctolog command, zsh's completion, and the skills your agents write pull requests and carry out your review with.",
|
|
39
|
+
until_then: "This setup goes on as it is; a new terminal finds proctolog once your PATH has it."
|
|
40
|
+
|
|
41
|
+
step "GitHub, through gh", checks: %i[gh],
|
|
42
|
+
why: "The desk reads pull requests, their stacks and their checks with your own gh, and writes to GitHub only when you say so."
|
|
43
|
+
|
|
44
|
+
step "The checkout you review", checks: %i[checkout repository], asks: "type its folder",
|
|
45
|
+
why: "The desk opens the pull requests of one checkout, from its GitHub origin, which your gh must be able to read."
|
|
46
|
+
|
|
47
|
+
step "A first burst", checks: %i[bursts],
|
|
48
|
+
why: "A burst is a body of pull requests you review together, a part at a time; the desk keeps your marks by it.",
|
|
49
|
+
until_then: "Review on the desk's first page starts one."
|
|
50
|
+
|
|
51
|
+
step "What turns features on", checks: %i[chrome diffle imagemagick ffmpeg app],
|
|
52
|
+
why: "Each of these turns one feature on. The desk opens without any of them.",
|
|
53
|
+
until_then: "Each feature waits for its own."
|
|
54
|
+
|
|
55
|
+
step "Your phone, through a share", checks: %i[share],
|
|
56
|
+
why: "Shared, the desk is yours from your phone or a computer elsewhere, while this Mac keeps it and an agent works on.",
|
|
57
|
+
advice: [
|
|
58
|
+
"Tailscale keeps the desk off the internet: only the devices signed in to your tailnet reach it, your phone among them once it has the Tailscale app. Install it from https://tailscale.com/download/mac.",
|
|
59
|
+
"ngrok puts the desk on the internet, behind its sign-in with Google, for your email alone; its free plan carries 1 GB a month. Install it from https://ngrok.com/download, then copy your authtoken from https://dashboard.ngrok.com/get-started/your-authtoken and run ngrok config add-authtoken TOKEN.",
|
|
60
|
+
"Either one is enough. Once the desk is open, proctolog share shares it."
|
|
61
|
+
]
|
|
62
|
+
|
|
63
|
+
INDENT = " " * 8
|
|
64
|
+
WIDTH = 78
|
|
65
|
+
|
|
66
|
+
# The folder setup takes the checkout from: where it started, or the one
|
|
67
|
+
# the owner typed at its step.
|
|
68
|
+
attr_reader :directory
|
|
69
|
+
|
|
70
|
+
# installation links PRoctolog's own files. doctor gives a fresh Doctor
|
|
71
|
+
# for a folder each time it is called, so that a step checked again sees
|
|
72
|
+
# what the owner just did.
|
|
73
|
+
def initialize(installation:, directory: Dir.pwd, input: $stdin, output: $stdout, &doctor)
|
|
74
|
+
@installation, @directory, @input, @output, @doctor = installation, Pathname(directory), input, output, doctor
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def steps = self.class.steps
|
|
78
|
+
|
|
79
|
+
# Asks at each step that is not done yet, and checks it again on Enter.
|
|
80
|
+
# :serve when the owner asked for the desk to open now, :finished when
|
|
81
|
+
# it is left for later or one answers already, :stopped when they
|
|
82
|
+
# stopped on the way.
|
|
83
|
+
def walk
|
|
84
|
+
say "PRoctolog, a step at a time: each says why it matters, and is stamped DONE", "once it is met. Of anyone else's, nothing is installed for you: a step says", "what to do, and Enter checks it again once you have."
|
|
85
|
+
steps.each.with_index(1) { |step, number| taken(step, number) or return :stopped }
|
|
86
|
+
desk
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
# Every step as it stands, asking nothing: for an agent, or a pipe.
|
|
90
|
+
# True when the desk can open: no step waits on a need.
|
|
91
|
+
def show
|
|
92
|
+
shown = steps.each.with_index(1).map { |step, number| shown(step, number) }
|
|
93
|
+
the_desk
|
|
94
|
+
lines [doctor[:port]]
|
|
95
|
+
say(*wrapped("Run proctolog setup in a terminal to take it a step at a time, or proctolog serve to open the desk."))
|
|
96
|
+
shown.all?
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
private
|
|
100
|
+
|
|
101
|
+
def doctor = @doctor.call(directory)
|
|
102
|
+
|
|
103
|
+
def taken(step, number)
|
|
104
|
+
begun(step, number)
|
|
105
|
+
advised = false
|
|
106
|
+
loop do
|
|
107
|
+
findings = step.findings(doctor)
|
|
108
|
+
lines findings
|
|
109
|
+
return stamp("DONE") if findings.all?(&:fine?)
|
|
110
|
+
|
|
111
|
+
advise(step) unless advised
|
|
112
|
+
advised = true
|
|
113
|
+
answer = ask(step.question(findings))
|
|
114
|
+
case answer&.downcase
|
|
115
|
+
in "q" | nil then return stopped(number)
|
|
116
|
+
in "l" then (return stamp("LATER", step.until_then) if step.leavable?(findings))
|
|
117
|
+
in "" then next
|
|
118
|
+
else @directory = Pathname(File.expand_path(answer, directory)) if step.asks
|
|
119
|
+
end
|
|
120
|
+
end
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
def shown(step, number)
|
|
124
|
+
begun(step, number)
|
|
125
|
+
findings = step.findings(doctor)
|
|
126
|
+
lines findings
|
|
127
|
+
return stamp("DONE") if findings.all?(&:fine?)
|
|
128
|
+
|
|
129
|
+
advise(step)
|
|
130
|
+
stamp("TO DO", step.until_then)
|
|
131
|
+
step.leavable?(findings)
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
def begun(step, number)
|
|
135
|
+
heading(step.title, number, why: step.why)
|
|
136
|
+
send(step.first) if step.first
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
# PRoctolog's own links, made fresh before its first step is checked.
|
|
140
|
+
# One in the way that is someone else's stays, and is said.
|
|
141
|
+
def link
|
|
142
|
+
@installation.install.each { say "#{INDENT}#{it.gsub(Dir.home, "~")}" }
|
|
143
|
+
rescue Installation::Refused => e
|
|
144
|
+
say(*wrapped("!! #{e.message}"))
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
# The last step: the desk itself, opened by the owner's Enter.
|
|
148
|
+
def desk
|
|
149
|
+
the_desk
|
|
150
|
+
port = doctor[:port]
|
|
151
|
+
lines [port]
|
|
152
|
+
return :finished unless port.fine?
|
|
153
|
+
|
|
154
|
+
(ask("Enter opens the desk, q leaves it for proctolog serve") == "") ? :serve : :finished
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
def the_desk = heading("The desk", steps.size + 1, why: "It opens on this Mac at #{Desk.address(4040)}; after the first visit, typing procto in the address bar finds it.")
|
|
158
|
+
|
|
159
|
+
def stopped(number)
|
|
160
|
+
say "", "Stopped at step #{number}. proctolog setup starts again from the first, and stamps each step that is done."
|
|
161
|
+
false
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
def heading(title, number, why: nil)
|
|
165
|
+
say "", format("%-8s%s", "#{number} of #{steps.size + 1}", title)
|
|
166
|
+
say(*wrapped(why)) if why
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
def lines(findings) = findings.each { say "#{INDENT}#{it}" }
|
|
170
|
+
|
|
171
|
+
def advise(step) = step.advice.each { say(*wrapped(it)) }
|
|
172
|
+
|
|
173
|
+
def stamp(word, after = nil)
|
|
174
|
+
say "#{INDENT}[ #{word} ]#{" #{after}" if after}"
|
|
175
|
+
true
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
def ask(question)
|
|
179
|
+
@output.print "#{INDENT}#{question.upcase_first} > "
|
|
180
|
+
@output.flush
|
|
181
|
+
@input.gets&.strip
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
# A long line broken at spaces under the step's title; a word too long
|
|
185
|
+
# for the line, an address, stays whole.
|
|
186
|
+
def wrapped(text) = text.scan(/\S.{0,#{WIDTH - INDENT.size - 1}}(?=\s|\z)|\S+/).map { INDENT + it }
|
|
187
|
+
|
|
188
|
+
def say(*lines) = @output.puts(*lines)
|
|
189
|
+
end
|
|
190
|
+
end
|
data/bin/proctolog
CHANGED
|
@@ -157,7 +157,7 @@ module Proctolog
|
|
|
157
157
|
option :allow, type: :string, repeatable: true, desc: "An email that may sign in through ngrok, in place of your Git email"
|
|
158
158
|
def start
|
|
159
159
|
boot_desk!
|
|
160
|
-
abort "No desk answers on :#{port}. Start one with the share: proctolog serve --share#{port_option}" unless answers?
|
|
160
|
+
abort "No desk answers on :#{port}. Start one with the share: proctolog serve --share#{port_option}" unless Localhost.answers?(port, within: 1)
|
|
161
161
|
puts Share.open(port, via: options[:via], allow: options[:allow])
|
|
162
162
|
puts "Stop it with: proctolog share stop#{port_option}"
|
|
163
163
|
rescue Share::Refused => e
|
|
@@ -188,13 +188,6 @@ module Proctolog
|
|
|
188
188
|
def port = options[:port]
|
|
189
189
|
|
|
190
190
|
def port_option = (" --port #{port}" unless port == 4040)
|
|
191
|
-
|
|
192
|
-
def answers?
|
|
193
|
-
Socket.tcp("localhost", port, connect_timeout: 1, &:close)
|
|
194
|
-
true
|
|
195
|
-
rescue SystemCallError
|
|
196
|
-
false
|
|
197
|
-
end
|
|
198
191
|
end
|
|
199
192
|
end
|
|
200
193
|
|
|
@@ -205,6 +198,14 @@ module Proctolog
|
|
|
205
198
|
|
|
206
199
|
class_option :project, type: :string, aliases: "-C", desc: "The Git checkout whose bursts to open"
|
|
207
200
|
|
|
201
|
+
no_commands do
|
|
202
|
+
# Bare, proctolog shows its commands under the one to start with.
|
|
203
|
+
def help(command = nil, subcommand = false)
|
|
204
|
+
puts "New here? proctolog setup takes you to the desk, a step at a time.", "" unless command
|
|
205
|
+
super
|
|
206
|
+
end
|
|
207
|
+
end
|
|
208
|
+
|
|
208
209
|
desc "burst SUBCOMMAND", "Start a burst of your own, or list the bursts there are"
|
|
209
210
|
subcommand "burst", BurstCommands
|
|
210
211
|
|
|
@@ -233,7 +234,7 @@ module Proctolog
|
|
|
233
234
|
abort "Port #{options[:port]} is taken. If that is the desk, it is open at #{Desk.address(options[:port])}; " \
|
|
234
235
|
"for a second one: proctolog serve --port #{options[:port] + 1}"
|
|
235
236
|
rescue Project::Missing => e
|
|
236
|
-
abort e.message
|
|
237
|
+
abort "#{e.message} New here? proctolog setup takes you to the desk, a step at a time."
|
|
237
238
|
end
|
|
238
239
|
|
|
239
240
|
desc "route [BURST]", "Print the order to take a burst in"
|
|
@@ -318,25 +319,20 @@ module Proctolog
|
|
|
318
319
|
map %w[-v --version] => :version
|
|
319
320
|
def version = puts("PRoctolog #{VERSION}")
|
|
320
321
|
|
|
321
|
-
desc "install", "
|
|
322
|
+
desc "install", "Another name for setup: link this PRoctolog, then set it up a step at a time"
|
|
322
323
|
long_desc <<~TEXT
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
`update` runs it for the version it installs.
|
|
324
|
+
Does what `proctolog setup` does, whichever of the two you remember: links the proctolog command, zsh's
|
|
325
|
+
completion and the agent skills to this PRoctolog, then takes you through the rest a step at a time.
|
|
326
|
+
`proctolog help setup` says how.
|
|
327
327
|
TEXT
|
|
328
|
-
def install
|
|
329
|
-
puts Installation.here.install
|
|
330
|
-
rescue Installation::Refused => e
|
|
331
|
-
abort e.message
|
|
332
|
-
end
|
|
328
|
+
def install = setup
|
|
333
329
|
|
|
334
330
|
desc "update", "Install the latest PRoctolog, and link to it"
|
|
335
331
|
long_desc <<~TEXT
|
|
336
332
|
Asks RubyGems for the latest PRoctolog and, when it is newer than this one, installs it the way this one
|
|
337
333
|
was: with `rv tool install` for one rvx runs, with `gem install` otherwise. A checkout is pulled
|
|
338
334
|
forward instead, when it is clean and on origin's own branch, and its bundle installed. Then runs the
|
|
339
|
-
new one's `
|
|
335
|
+
new one's `setup`, which links it. A desk open now runs on as it was: restart it to run the new one.
|
|
340
336
|
TEXT
|
|
341
337
|
def update
|
|
342
338
|
installation = Installation.here
|
|
@@ -367,7 +363,7 @@ module Proctolog
|
|
|
367
363
|
system(environment, "rv", "tool", "install", "proctolog@#{latest}", "--gem-server", "#{Release.server}/", exception: true)
|
|
368
364
|
puts taken(latest)
|
|
369
365
|
# rvx runs the newest it holds, which is now this one; rv 0.7 runs no proctolog@VERSION.
|
|
370
|
-
exec(environment, "rvx", "--no-install", "proctolog", "
|
|
366
|
+
exec(environment.merge(Installation.updating_from(VERSION)), "rvx", "--no-install", "proctolog", "setup")
|
|
371
367
|
end
|
|
372
368
|
end
|
|
373
369
|
|
|
@@ -376,17 +372,20 @@ module Proctolog
|
|
|
376
372
|
system(RbConfig.ruby, File.join(RbConfig::CONFIG["bindir"], "gem"), "install", "proctolog", "--version", latest.version, "--clear-sources", "--source", Release.server, exception: true)
|
|
377
373
|
puts taken(latest)
|
|
378
374
|
# RubyGems' launcher takes the version to run as its first word.
|
|
379
|
-
exec(RbConfig.ruby, File.join(Gem.bindir, "proctolog"), "_#{latest}_", "
|
|
375
|
+
exec(Installation.updating_from(VERSION), RbConfig.ruby, File.join(Gem.bindir, "proctolog"), "_#{latest}_", "setup")
|
|
380
376
|
end
|
|
381
377
|
|
|
382
378
|
# The checkout's own bundle, on this Ruby, as bin/proctolog runs it.
|
|
379
|
+
# Nothing to take when origin has nothing newer, as for a gem.
|
|
383
380
|
def take_with_git(installation)
|
|
384
|
-
|
|
381
|
+
pull = installation.pull
|
|
382
|
+
return puts("PRoctolog #{VERSION} in #{installation.root} is the latest: origin has nothing newer.") unless pull.moved?
|
|
383
|
+
|
|
385
384
|
Bundler.with_unbundled_env do
|
|
386
385
|
system(RbConfig.ruby, Gem.bin_path("bundler", "bundle"), "install", "--quiet", chdir: installation.root.to_s, exception: true)
|
|
387
386
|
end
|
|
388
387
|
puts "Pulled #{installation.root} forward. Restart the desk to run it."
|
|
389
|
-
exec(RbConfig.ruby, installation.root.join("bin/install").to_s)
|
|
388
|
+
exec(Installation.updating_from(VERSION), RbConfig.ruby, installation.root.join("bin/install").to_s)
|
|
390
389
|
end
|
|
391
390
|
|
|
392
391
|
def taken(latest) = "Installed PRoctolog #{latest}. Restart the desk to run it: one open now still runs #{Release.running}."
|
|
@@ -402,13 +401,55 @@ module Proctolog
|
|
|
402
401
|
Exits 0 when every need is met, 1 when one is not.
|
|
403
402
|
TEXT
|
|
404
403
|
def doctor
|
|
405
|
-
project =
|
|
406
|
-
|
|
404
|
+
project = discovered
|
|
405
|
+
boot_desk!
|
|
406
|
+
Doctor.new(project).tap { puts it }.then { exit(it.fine? ? 0 : 1) }
|
|
407
|
+
end
|
|
408
|
+
|
|
409
|
+
desc "setup", "Set PRoctolog up, a step at a time, from wherever you are to an open desk"
|
|
410
|
+
long_desc <<~TEXT
|
|
411
|
+
First links the proctolog command, zsh's completion and the agent skills to this PRoctolog: from the gem,
|
|
412
|
+
or the checkout, which also gets its launcher in ~/.local/bin. Anything of the same name that is not
|
|
413
|
+
PRoctolog's stays as it is. Then takes doctor's checks in the order you meet them: gh signed in, the
|
|
414
|
+
checkout you review and its GitHub repository, a first burst, what turns features on, sharing the desk
|
|
415
|
+
with your phone, and the desk itself. Each step says why it matters and what to do; Enter checks it again
|
|
416
|
+
once you have, and it is stamped DONE when it is met, or LATER when you leave it. Outside a checkout, its
|
|
417
|
+
step asks for the folder of one. Of anyone else's, nothing is installed for you. The last step opens the
|
|
418
|
+
desk with `proctolog serve`. `install` is another name for it, and `update` runs it for the version it
|
|
419
|
+
installs, which then only links and says what is new.
|
|
420
|
+
|
|
421
|
+
Outside a terminal it asks nothing: it prints each step as it stands, and exits 0 when the desk can
|
|
422
|
+
open, 1 when a step still waits on something the desk needs.
|
|
423
|
+
TEXT
|
|
424
|
+
def setup
|
|
425
|
+
installation = Installation.here
|
|
426
|
+
# An update hands over to the version it installed, to link it and say
|
|
427
|
+
# what is new: the owner asked for an update, not for a setup.
|
|
428
|
+
return puts(installation.install, "", installation.news) if installation.news
|
|
429
|
+
|
|
430
|
+
discovered
|
|
431
|
+
boot_desk!
|
|
432
|
+
setup = Setup.new(installation:, directory: options[:project] || Dir.pwd) { Doctor.new(discovered(it)) }
|
|
433
|
+
exit(setup.show ? 0 : 1) unless $stdin.tty?
|
|
434
|
+
|
|
435
|
+
case setup.walk
|
|
436
|
+
# The command that runs now, as a new terminal may not find proctolog yet.
|
|
437
|
+
in :serve then exec(RbConfig.ruby, $PROGRAM_NAME, "serve", *(["--project", Project.current.root.to_s] if Project.current))
|
|
438
|
+
in :finished then exit 0
|
|
439
|
+
in :stopped then exit 1
|
|
440
|
+
end
|
|
441
|
+
rescue Installation::Refused => e
|
|
442
|
+
abort e.message
|
|
443
|
+
end
|
|
444
|
+
|
|
445
|
+
no_commands do
|
|
446
|
+
# The checkout --project names, or the one this runs in, found again
|
|
447
|
+
# each time: one a setup step made is there the next time it checks.
|
|
448
|
+
def discovered(directory = options[:project] || Dir.pwd)
|
|
449
|
+
Project.current = Project.discover(directory)
|
|
407
450
|
rescue Project::Missing
|
|
408
|
-
nil
|
|
451
|
+
Project.current = nil
|
|
409
452
|
end
|
|
410
|
-
Desk.initialize! unless Desk.initialized?
|
|
411
|
-
Doctor.new(project).tap { puts it }.then { exit(it.fine? ? 0 : 1) }
|
|
412
453
|
end
|
|
413
454
|
end
|
|
414
455
|
end
|
data/completions/_proctolog
CHANGED
|
@@ -58,8 +58,9 @@ _proctolog() {
|
|
|
58
58
|
'check:Check a description as the desk reads it, of a pull request or a file'
|
|
59
59
|
'themes:Print the themes a decision may name, with what each means'
|
|
60
60
|
'doctor:Say what this Mac and this checkout lack, a line each with the fix'
|
|
61
|
+
'setup:Set PRoctolog up, a step at a time, from wherever you are to an open desk'
|
|
61
62
|
'version:Print which PRoctolog this is'
|
|
62
|
-
'install:
|
|
63
|
+
'install:Another name for setup: link this PRoctolog, then set it up a step at a time'
|
|
63
64
|
'update:Install the latest PRoctolog, and link to it'
|
|
64
65
|
'tree:Print a tree of all available commands'
|
|
65
66
|
'help:Describe the commands, or one of them'
|
|
@@ -157,7 +158,7 @@ _proctolog() {
|
|
|
157
158
|
unopened=(${(f)"$(_proctolog_unopened "$root")"})
|
|
158
159
|
_arguments $project '(-f --file)'{-f,--file}'[A local Markdown file to check before it is published]:file:_files' \
|
|
159
160
|
"1:pull request:{_describe -t pull-requests 'open pull request' pull_requests; _describe -t unopened 'not opened yet' unopened}" ;;
|
|
160
|
-
themes|doctor|version|install|update) _arguments $project ;;
|
|
161
|
+
themes|doctor|setup|version|install|update) _arguments $project ;;
|
|
161
162
|
help) _describe -t commands 'command' commands ;;
|
|
162
163
|
esac
|
|
163
164
|
;;
|
data/docs/manual/pr-format.md
CHANGED
|
@@ -21,7 +21,7 @@ which only renders and keeps nothing.
|
|
|
21
21
|
## PR format
|
|
22
22
|
|
|
23
23
|
The skill in `skills/proctolog-pr-writing/` teaches an agent this format, with a
|
|
24
|
-
whole example in its `references/example-description.md`. `proctolog
|
|
24
|
+
whole example in its `references/example-description.md`. `proctolog setup`
|
|
25
25
|
links it into `~/.claude/skills/` and `~/.agents/skills/`. Keep the project's
|
|
26
26
|
template (Summary, Changes, Verification) and add:
|
|
27
27
|
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
# Setting up, a step at a time
|
|
2
|
+
|
|
3
|
+
`proctolog setup` is the one command to remember. Run it the first time, with
|
|
4
|
+
`rvx proctolog setup` or after `gem install proctolog`, and again whenever
|
|
5
|
+
you're unsure something is in place. It takes you from wherever you are to an
|
|
6
|
+
open desk, one step at a time. `proctolog install` is another name for it, so
|
|
7
|
+
whichever you remember works.
|
|
8
|
+
|
|
9
|
+
1. **PRoctolog on this Mac**: setup links the `proctolog` command, zsh's
|
|
10
|
+
completion and the agent skills to this PRoctolog, then checks Ruby, the
|
|
11
|
+
command on your PATH, and whether a newer PRoctolog is out.
|
|
12
|
+
2. **GitHub, through gh**: installed and signed in.
|
|
13
|
+
3. **The checkout you review**: a Git checkout whose origin is on GitHub,
|
|
14
|
+
and which your `gh` can read. Outside one, type its folder here.
|
|
15
|
+
4. **A first burst**: the pull requests you review together.
|
|
16
|
+
5. **What turns features on**: Chrome, Diffle, ImageMagick, ffmpeg and the
|
|
17
|
+
checkout's `.proctolog/app.yml`.
|
|
18
|
+
6. **Your phone, through a share**: Tailscale or ngrok, what each gives, and
|
|
19
|
+
where to get them ([sharing](sharing.md)).
|
|
20
|
+
7. **The desk**: Enter opens it with `proctolog serve`.
|
|
21
|
+
|
|
22
|
+
Each step says why it matters and shows doctor's lines for it, each with the
|
|
23
|
+
fix. Do what it says in another terminal, then press Enter there: the step
|
|
24
|
+
checks itself again. A step that is met is stamped `[ DONE ]` and the next
|
|
25
|
+
one begins, so a second run passes over what you already have, stamping each
|
|
26
|
+
step as it goes. The links in the first step are PRoctolog's own, made again
|
|
27
|
+
each time; anything of the same name that is someone else's stays, and is
|
|
28
|
+
said. Nothing else is installed for you: a step explains, links and checks.
|
|
29
|
+
|
|
30
|
+
Some steps can wait. Type `l` and Enter to leave one for later, and it is
|
|
31
|
+
stamped `[ LATER ]` with what does its work meanwhile:
|
|
32
|
+
|
|
33
|
+
- A step whose lines are only `--` is a nicety: the desk opens without it.
|
|
34
|
+
- The first step, when the command is linked but your PATH doesn't have it
|
|
35
|
+
yet: setup goes on as it is, and a new terminal finds `proctolog` once
|
|
36
|
+
`~/.zshrc` puts it there.
|
|
37
|
+
- A first burst: **Review** on the desk's first page starts one, so the step
|
|
38
|
+
needn't hold you up.
|
|
39
|
+
- The features step: each feature waits for its own program, and the rest of
|
|
40
|
+
the desk works.
|
|
41
|
+
|
|
42
|
+
gh and the checkout can't wait, as the desk opens nothing without them;
|
|
43
|
+
there, `l` only checks again. `q` stops anywhere, and `proctolog setup`
|
|
44
|
+
starts again from the first step.
|
|
45
|
+
|
|
46
|
+
If a desk already answers on port 4040, the last step says so instead of
|
|
47
|
+
opening a second one.
|
|
48
|
+
|
|
49
|
+
## After an update
|
|
50
|
+
|
|
51
|
+
`proctolog update` runs the new version's `setup`, which then only links it
|
|
52
|
+
and says what's new: you asked for an update, not to go through setup again.
|
|
53
|
+
|
|
54
|
+
## For an agent
|
|
55
|
+
|
|
56
|
+
Outside a terminal, in a pipe or an agent's shell, `setup` asks nothing. It
|
|
57
|
+
links, prints every step as it stands, stamping the unfinished ones
|
|
58
|
+
`[ TO DO ]`, and exits 0 when the desk can open, 1 when a step still waits on
|
|
59
|
+
something the desk needs.
|
data/docs/manual/sharing.md
CHANGED
|
@@ -34,7 +34,7 @@ in. It goes through whichever of these this Mac has:
|
|
|
34
34
|
and run `ngrok config add-authtoken TOKEN`. On the free plan, ngrok shows a
|
|
35
35
|
warning page of its own before the desk, and carries 1 GB a month.
|
|
36
36
|
|
|
37
|
-
Neither is installed for you. `proctolog doctor` and `proctolog share status`
|
|
37
|
+
Neither is installed for you. `proctolog doctor`, `proctolog setup` and `proctolog share status`
|
|
38
38
|
say what each one still needs.
|
|
39
39
|
|
|
40
40
|
### Turning it on
|
data/docs/manual/updates.md
CHANGED
|
@@ -39,10 +39,17 @@ proctolog update
|
|
|
39
39
|
|
|
40
40
|
It asks RubyGems, installs the newer version the way yours was installed
|
|
41
41
|
(`rv tool install` for a PRoctolog that `rvx` runs, `gem install` otherwise),
|
|
42
|
-
and runs that version's `proctolog
|
|
43
|
-
skills
|
|
44
|
-
bundle installed.
|
|
45
|
-
`proctolog
|
|
42
|
+
and runs that version's `proctolog setup`, which after an update only links
|
|
43
|
+
zsh's completion and the agent skills to it, and walks no steps. A checkout of PRoctolog is pulled forward instead, and its
|
|
44
|
+
bundle installed. When there's nothing newer, it says so and changes nothing.
|
|
45
|
+
A desk that's open keeps running the old one: stop it and run `proctolog
|
|
46
|
+
serve` again.
|
|
47
|
+
|
|
48
|
+
Then it says what's new since the version you had, from the changelog: each
|
|
49
|
+
change by its first sentence, under Added, Changed and Fixed, and in full
|
|
50
|
+
what you must do. Past a dozen changes it counts the rest, and it ends with
|
|
51
|
+
where the whole changelog is. An update from 0.2.0 or earlier says none of
|
|
52
|
+
this, as the version you leave is the one that runs the update.
|
|
46
53
|
|
|
47
54
|
The old version stays installed. `gem cleanup proctolog` removes the ones
|
|
48
55
|
you no longer run; with rv, `rv tool list` shows them and
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "io/console"
|
|
4
|
+
|
|
5
|
+
module Proctolog
|
|
6
|
+
class Changelog
|
|
7
|
+
# What an update brings, as the console says it once the new version is
|
|
8
|
+
# in: each change by the first sentence of its line, under its heading,
|
|
9
|
+
# and what to do in full. Plain text, as a terminal reads no markdown.
|
|
10
|
+
class News
|
|
11
|
+
# Past this many changes, the rest are counted rather than shown.
|
|
12
|
+
SHOWN = 12
|
|
13
|
+
|
|
14
|
+
# As wide as the terminal it is said in, within reason.
|
|
15
|
+
def self.columns = (IO.console&.winsize&.last || 80).clamp(40, 100)
|
|
16
|
+
|
|
17
|
+
def initialize(since:, sections:, path:)
|
|
18
|
+
@since, @sections, @path = since, sections, path
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def to_s(width: self.class.columns)
|
|
22
|
+
shown = changes.first(SHOWN)
|
|
23
|
+
more = changes.size - shown.size
|
|
24
|
+
told = @sections.flat_map { told(it, shown:, width:) }
|
|
25
|
+
["What's new since PRoctolog #{@since}:", *told, "", more.positive? ? "#{more} more, and all of it: #{@path}" : "All of it: #{@path}"].join("\n")
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
private
|
|
29
|
+
|
|
30
|
+
# Every change, in the order written: its section, its heading and its line.
|
|
31
|
+
def changes = @sections.flat_map { |section| section.changes.flat_map { |heading, lines| lines.map { [section, heading, it] } } }
|
|
32
|
+
|
|
33
|
+
# A section's changes that are shown, and what it asks, which always is.
|
|
34
|
+
def told(section, shown:, width:)
|
|
35
|
+
headed = section.changes.flat_map do |heading, lines|
|
|
36
|
+
kept = lines.select { shown.include?([section, heading, it]) }
|
|
37
|
+
kept.any? ? [" #{heading}", *kept.map { " - #{headline(it, room: width - 6)}" }] : []
|
|
38
|
+
end
|
|
39
|
+
asked = section.you_do&.then { laid("You do#{plain(it)}", width:) } || []
|
|
40
|
+
(headed.any? || asked.any?) ? ["", section.name, *headed, *asked] : []
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# The first sentence of a line. One longer than the room ends at its last
|
|
44
|
+
# clause that fits, when that keeps half the room, or else at a word.
|
|
45
|
+
def headline(line, room:)
|
|
46
|
+
sentence = plain(line).then { it[/\A.+?[.!?](?=\s|\z)/] || it }
|
|
47
|
+
return sentence if sentence.size <= room
|
|
48
|
+
|
|
49
|
+
cut = sentence[0, room]
|
|
50
|
+
clause = cut[/\A.*(?=[,;:]\s)/]
|
|
51
|
+
"#{(clause && clause.size >= room / 2) ? clause : cut[/\A.*(?=\s)/]}…"
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
# In lines no wider than the console, the first two spaces in, the rest four.
|
|
55
|
+
def laid(text, width:)
|
|
56
|
+
text.split.each_with_object([+" "]) do |word, lines|
|
|
57
|
+
lines << +" " if lines.last.size + word.size >= width && !lines.last.strip.empty?
|
|
58
|
+
lines.last << (lines.last.strip.empty? ? word : " #{word}")
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
# Without markdown's marks: **bold**, <links> and [text](links) keep
|
|
63
|
+
# their words. Backticks stay, as the console writes commands in them.
|
|
64
|
+
def plain(text) = text.gsub(/\*\*(.+?)\*\*/, '\1').gsub(/<(\S+?)>/, '\1').gsub(/\[([^\]]+)\]\([^)]+\)/, '\1')
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
end
|
data/lib/proctolog/changelog.rb
CHANGED
|
@@ -1,23 +1,42 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require_relative "changelog/news"
|
|
4
|
+
|
|
3
5
|
module Proctolog
|
|
4
|
-
# CHANGELOG.md as a release
|
|
5
|
-
# first, under ## Unreleased what main has that no gem has
|
|
6
|
-
# as bin/release
|
|
6
|
+
# CHANGELOG.md as a release and an update read it: a section to each
|
|
7
|
+
# version, newest first, under ## Unreleased what main has that no gem has
|
|
8
|
+
# yet. Plain Ruby, as bin/release, the Release workflow and bin/install
|
|
9
|
+
# read it before any bundle.
|
|
7
10
|
class Changelog
|
|
8
11
|
PATH = File.expand_path("../../CHANGELOG.md", __dir__)
|
|
9
12
|
|
|
10
|
-
# One ## section, its lines under ### Added, Changed and Fixed
|
|
13
|
+
# One ## section, its lines under ### Added, Changed and Fixed, and what
|
|
14
|
+
# to do after **You do**.
|
|
11
15
|
Section = Data.define(:name, :text) do
|
|
12
16
|
def adds? = text.match?(/^### Added$/)
|
|
13
17
|
def lines? = text.match?(/^- /)
|
|
14
18
|
def notes = text.strip
|
|
19
|
+
|
|
20
|
+
def unreleased? = name == "Unreleased"
|
|
21
|
+
def version = (Gem::Version.new(name) if Gem::Version.correct?(name))
|
|
22
|
+
|
|
23
|
+
# Its lines by heading, in the order written, each on one line.
|
|
24
|
+
def changes
|
|
25
|
+
text.scan(/^### ([^\n]+)\n(.*?)(?=^### |^\*\*You do\*\*|\z)/m).to_h do |heading, lines|
|
|
26
|
+
[heading, lines.split(/^- /).drop(1).map { it.split.join(" ") }]
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# What it asks, on one line, as it goes on after **You do**: ": nothing",
|
|
31
|
+
# or ", coming from a checkout: …".
|
|
32
|
+
def you_do = text[/^\*\*You do\*\*(.*)/m, 1]&.split&.join(" ")
|
|
15
33
|
end
|
|
16
34
|
|
|
17
35
|
# It writes ⌘ and ↩, so it is read as UTF-8 whatever the locale.
|
|
18
|
-
def self.read(path = PATH) = new(File.read(path, encoding: "UTF-8"))
|
|
36
|
+
def self.read(path = PATH) = new(File.read(path, encoding: "UTF-8"), path:)
|
|
19
37
|
|
|
20
|
-
def initialize(text)
|
|
38
|
+
def initialize(text, path: PATH)
|
|
39
|
+
@path = path
|
|
21
40
|
@sections = text.split(/^## /).drop(1).map do
|
|
22
41
|
name, body = it.split("\n", 2)
|
|
23
42
|
Section.new(name: name.strip, text: body.to_s)
|
|
@@ -28,6 +47,15 @@ module Proctolog
|
|
|
28
47
|
|
|
29
48
|
def unreleased = self["Unreleased"]
|
|
30
49
|
|
|
50
|
+
# What an update from this version brings: every section newer than it,
|
|
51
|
+
# Unreleased too in a checkout. Nil when it brings nothing written here.
|
|
52
|
+
def since(version)
|
|
53
|
+
return unless Gem::Version.correct?(version)
|
|
54
|
+
|
|
55
|
+
newer = @sections.select { it.unreleased? || it.version&.>(Gem::Version.new(version)) }
|
|
56
|
+
News.new(since: version, sections: newer, path: @path) if newer.any?
|
|
57
|
+
end
|
|
58
|
+
|
|
31
59
|
# Adding something makes a minor (docs/manual/updates.md): the minor this
|
|
32
60
|
# version owes when it adds something but is only a patch on the one
|
|
33
61
|
# before it.
|
|
@@ -42,9 +70,6 @@ module Proctolog
|
|
|
42
70
|
# too.
|
|
43
71
|
def adds?(version) = [self[version], unreleased].compact.any?(&:adds?)
|
|
44
72
|
|
|
45
|
-
def released_before(version)
|
|
46
|
-
older = @sections.drop_while { it.name != version }.drop(1)
|
|
47
|
-
older.map(&:name).find { Gem::Version.correct?(it) }&.then { Gem::Version.new(it) }
|
|
48
|
-
end
|
|
73
|
+
def released_before(version) = @sections.drop_while { it.name != version }.drop(1).find(&:version)&.version
|
|
49
74
|
end
|
|
50
75
|
end
|
|
@@ -4,6 +4,7 @@ require "fileutils"
|
|
|
4
4
|
require "open3"
|
|
5
5
|
require "rbconfig"
|
|
6
6
|
require "shellwords"
|
|
7
|
+
require_relative "changelog"
|
|
7
8
|
|
|
8
9
|
module Proctolog
|
|
9
10
|
# Where this PRoctolog lives, and what it puts on this Mac for zsh and the
|
|
@@ -22,8 +23,20 @@ module Proctolog
|
|
|
22
23
|
|
|
23
24
|
SKILL_FOLDERS = %w[.claude/skills .agents/skills].freeze
|
|
24
25
|
|
|
26
|
+
# What a pull did: the commit the checkout was at, and the one it is at.
|
|
27
|
+
Pull = Data.define(:from, :to) do
|
|
28
|
+
def moved? = from != to
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# `proctolog update` hands the new version's install the version it
|
|
32
|
+
# leaves, in the environment, for that install to say what is new since.
|
|
33
|
+
UPDATED_FROM = "PROCTOLOG_UPDATED_FROM"
|
|
34
|
+
|
|
25
35
|
def self.here = new(File.expand_path("../..", __dir__))
|
|
26
36
|
|
|
37
|
+
# The environment update starts the new version's install in.
|
|
38
|
+
def self.updating_from(version) = {UPDATED_FROM => version.to_s}
|
|
39
|
+
|
|
27
40
|
attr_reader :root
|
|
28
41
|
|
|
29
42
|
def initialize(root, home: Dir.home, env: ENV)
|
|
@@ -47,9 +60,15 @@ module Proctolog
|
|
|
47
60
|
branch, main = git("branch", "--show-current"), origin_branch
|
|
48
61
|
raise Refused, "#{root} is on #{branch}, not #{main}: switch to #{main}, or pull #{branch} yourself." unless branch == main
|
|
49
62
|
|
|
63
|
+
from = git("rev-parse", "HEAD")
|
|
50
64
|
git("pull", "--ff-only")
|
|
65
|
+
Pull.new(from:, to: git("rev-parse", "HEAD"))
|
|
51
66
|
end
|
|
52
67
|
|
|
68
|
+
# What is new in this PRoctolog since the one an update left, or nil when
|
|
69
|
+
# no update brought it here.
|
|
70
|
+
def news = @env[UPDATED_FROM]&.then { Changelog.read(root.join("CHANGELOG.md")).since(it) }
|
|
71
|
+
|
|
53
72
|
def launcher = @home.join(".local/bin/proctolog")
|
|
54
73
|
|
|
55
74
|
def completion = @home.join(".zsh/completions/_proctolog")
|
data/lib/proctolog/version.rb
CHANGED
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: proctolog
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.3.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Gill Abarbanel
|
|
@@ -193,6 +193,7 @@ files:
|
|
|
193
193
|
- app/models/proctolog/handover/worklist.rb
|
|
194
194
|
- app/models/proctolog/housekeeping.rb
|
|
195
195
|
- app/models/proctolog/kit.rb
|
|
196
|
+
- app/models/proctolog/localhost.rb
|
|
196
197
|
- app/models/proctolog/marks.rb
|
|
197
198
|
- app/models/proctolog/membership.rb
|
|
198
199
|
- app/models/proctolog/mentions.rb
|
|
@@ -217,6 +218,7 @@ files:
|
|
|
217
218
|
- app/models/proctolog/rules.rb
|
|
218
219
|
- app/models/proctolog/scenario.rb
|
|
219
220
|
- app/models/proctolog/server.rb
|
|
221
|
+
- app/models/proctolog/setup.rb
|
|
220
222
|
- app/models/proctolog/share.rb
|
|
221
223
|
- app/models/proctolog/share/ngrok.rb
|
|
222
224
|
- app/models/proctolog/share/settings.rb
|
|
@@ -306,10 +308,12 @@ files:
|
|
|
306
308
|
- docs/manual/handover.md
|
|
307
309
|
- docs/manual/pr-format.md
|
|
308
310
|
- docs/manual/reading.md
|
|
311
|
+
- docs/manual/setup.md
|
|
309
312
|
- docs/manual/sharing.md
|
|
310
313
|
- docs/manual/unopened.md
|
|
311
314
|
- docs/manual/updates.md
|
|
312
315
|
- lib/proctolog/changelog.rb
|
|
316
|
+
- lib/proctolog/changelog/news.rb
|
|
313
317
|
- lib/proctolog/installation.rb
|
|
314
318
|
- lib/proctolog/version.rb
|
|
315
319
|
- public/asking.js
|