slipway 0.1.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.
Files changed (124) hide show
  1. checksums.yaml +7 -0
  2. data/.yardopts +7 -0
  3. data/CHANGELOG.md +45 -0
  4. data/LICENSE.txt +21 -0
  5. data/README.md +1013 -0
  6. data/exe/slipway +10 -0
  7. data/lib/slipway/cli/builtins.rb +241 -0
  8. data/lib/slipway/cli/completer.rb +158 -0
  9. data/lib/slipway/cli/completion_scripts.rb +163 -0
  10. data/lib/slipway/cli/context.rb +67 -0
  11. data/lib/slipway/cli/errors.rb +19 -0
  12. data/lib/slipway/cli/globals.rb +27 -0
  13. data/lib/slipway/cli/help_renderer.rb +135 -0
  14. data/lib/slipway/cli/manpage.rb +226 -0
  15. data/lib/slipway/cli/parser.rb +45 -0
  16. data/lib/slipway/cli/registry.rb +191 -0
  17. data/lib/slipway/cli/runner.rb +186 -0
  18. data/lib/slipway/cli/style.rb +82 -0
  19. data/lib/slipway/cli/theme.rb +85 -0
  20. data/lib/slipway/cli/validator.rb +61 -0
  21. data/lib/slipway/cli.rb +22 -0
  22. data/lib/slipway/command_line.rb +22 -0
  23. data/lib/slipway/commands/api_resources.rb +82 -0
  24. data/lib/slipway/commands/apply.rb +172 -0
  25. data/lib/slipway/commands/base.rb +50 -0
  26. data/lib/slipway/commands/config.rb +73 -0
  27. data/lib/slipway/commands/create.rb +218 -0
  28. data/lib/slipway/commands/delete.rb +82 -0
  29. data/lib/slipway/commands/describe.rb +74 -0
  30. data/lib/slipway/commands/diff.rb +122 -0
  31. data/lib/slipway/commands/edit.rb +130 -0
  32. data/lib/slipway/commands/explain.rb +97 -0
  33. data/lib/slipway/commands/fetch.rb +112 -0
  34. data/lib/slipway/commands/from_dir.rb +141 -0
  35. data/lib/slipway/commands/get.rb +167 -0
  36. data/lib/slipway/commands/label.rb +114 -0
  37. data/lib/slipway/commands/manual.rb +67 -0
  38. data/lib/slipway/commands/options.rb +73 -0
  39. data/lib/slipway/commands/results.rb +57 -0
  40. data/lib/slipway/commands/rollout.rb +114 -0
  41. data/lib/slipway/commands/rollout_spec.rb +99 -0
  42. data/lib/slipway/commands/rollout_undo.rb +126 -0
  43. data/lib/slipway/commands/scope.rb +156 -0
  44. data/lib/slipway/commands/sync.rb +140 -0
  45. data/lib/slipway/commands.rb +54 -0
  46. data/lib/slipway/drift.rb +87 -0
  47. data/lib/slipway/editor.rb +71 -0
  48. data/lib/slipway/error.rb +27 -0
  49. data/lib/slipway/fetcher.rb +99 -0
  50. data/lib/slipway/field_selector.rb +86 -0
  51. data/lib/slipway/git/branch_name.rb +32 -0
  52. data/lib/slipway/git/commit.rb +13 -0
  53. data/lib/slipway/git/distance.rb +13 -0
  54. data/lib/slipway/git/errors.rb +125 -0
  55. data/lib/slipway/git/fake.rb +147 -0
  56. data/lib/slipway/git/fast_forward.rb +12 -0
  57. data/lib/slipway/git/fast_forwarding.rb +148 -0
  58. data/lib/slipway/git/fetch_result.rb +22 -0
  59. data/lib/slipway/git/move_back.rb +12 -0
  60. data/lib/slipway/git/reflog.rb +25 -0
  61. data/lib/slipway/git/repository.rb +288 -0
  62. data/lib/slipway/git/rolling_back.rb +98 -0
  63. data/lib/slipway/git/runner.rb +175 -0
  64. data/lib/slipway/git/status.rb +110 -0
  65. data/lib/slipway/git/url.rb +95 -0
  66. data/lib/slipway/git.rb +20 -0
  67. data/lib/slipway/inspector.rb +103 -0
  68. data/lib/slipway/labels.rb +126 -0
  69. data/lib/slipway/manifest.rb +265 -0
  70. data/lib/slipway/names.rb +22 -0
  71. data/lib/slipway/outcome.rb +45 -0
  72. data/lib/slipway/output/age.rb +70 -0
  73. data/lib/slipway/output/describe.rb +71 -0
  74. data/lib/slipway/output/explain.rb +75 -0
  75. data/lib/slipway/output/serializer.rb +35 -0
  76. data/lib/slipway/output/table.rb +67 -0
  77. data/lib/slipway/output.rb +28 -0
  78. data/lib/slipway/paths.rb +65 -0
  79. data/lib/slipway/plan.rb +227 -0
  80. data/lib/slipway/pool.rb +94 -0
  81. data/lib/slipway/resources.rb +91 -0
  82. data/lib/slipway/rollback.rb +236 -0
  83. data/lib/slipway/rollout_history.rb +69 -0
  84. data/lib/slipway/runtime.rb +65 -0
  85. data/lib/slipway/scanner.rb +54 -0
  86. data/lib/slipway/schema.rb +128 -0
  87. data/lib/slipway/selector.rb +146 -0
  88. data/lib/slipway/settings.rb +174 -0
  89. data/lib/slipway/state.rb +82 -0
  90. data/lib/slipway/store.rb +170 -0
  91. data/lib/slipway/syncer.rb +139 -0
  92. data/lib/slipway/version.rb +5 -0
  93. data/lib/slipway/views/group.rb +35 -0
  94. data/lib/slipway/views/project.rb +148 -0
  95. data/lib/slipway/views.rb +10 -0
  96. data/lib/slipway/yaml.rb +14 -0
  97. data/lib/slipway.rb +32 -0
  98. data/man/man1/slipway-api-resources.1 +53 -0
  99. data/man/man1/slipway-apply.1 +45 -0
  100. data/man/man1/slipway-completion.1 +29 -0
  101. data/man/man1/slipway-config-path.1 +20 -0
  102. data/man/man1/slipway-config-view.1 +25 -0
  103. data/man/man1/slipway-config.1 +22 -0
  104. data/man/man1/slipway-create.1 +89 -0
  105. data/man/man1/slipway-delete.1 +46 -0
  106. data/man/man1/slipway-describe.1 +95 -0
  107. data/man/man1/slipway-diff.1 +120 -0
  108. data/man/man1/slipway-edit.1 +34 -0
  109. data/man/man1/slipway-explain.1 +36 -0
  110. data/man/man1/slipway-fetch.1 +77 -0
  111. data/man/man1/slipway-get.1 +155 -0
  112. data/man/man1/slipway-help.1 +19 -0
  113. data/man/man1/slipway-label.1 +53 -0
  114. data/man/man1/slipway-man.1 +36 -0
  115. data/man/man1/slipway-rollout-history.1 +28 -0
  116. data/man/man1/slipway-rollout-pause.1 +21 -0
  117. data/man/man1/slipway-rollout-resume.1 +21 -0
  118. data/man/man1/slipway-rollout-undo.1 +73 -0
  119. data/man/man1/slipway-rollout-unpin.1 +21 -0
  120. data/man/man1/slipway-rollout.1 +36 -0
  121. data/man/man1/slipway-sync.1 +90 -0
  122. data/man/man1/slipway-version.1 +17 -0
  123. data/man/man1/slipway.1 +243 -0
  124. metadata +173 -0
@@ -0,0 +1,139 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'command_line'
4
+ require_relative 'drift'
5
+ require_relative 'git'
6
+ require_relative 'outcome'
7
+ require_relative 'plan'
8
+ require_relative 'rollout_history'
9
+
10
+ module Slipway
11
+ # Brings projects to their manifests by the one move that cannot lose work: each is fetched,
12
+ # planned by Plan.for, and its checked-out branch fast-forwarded when the plan says so. It
13
+ # observes and plans on the workers and moves branches on the calling thread, one project at a
14
+ # time, so no two of its writes run at once.
15
+ class Syncer
16
+ # The plan for a project whose fetch went through, made from what git answered after it.
17
+ Observed = Data.define(:project, :inspection, :fetched, :plan)
18
+
19
+ FAST_FORWARDED = 'fast-forwarded'
20
+ REACHED = [Outcome::FETCHED, Outcome::UNCHANGED].freeze
21
+ ABBREV = Git::Porcelain::ABBREVIATION
22
+ TO_PIN = '(to the pinned revision)'
23
+ # Git leaves the branch where it was on each refusal. A lock may belong to a git that is
24
+ # still running, so none is ever removed.
25
+ ADVICE = { 'Busy' => 'sync never removes a lock', 'WouldOverwrite' => 'move them and run sync again',
26
+ 'WouldLoseChanges' => 'commit or move them and run sync again',
27
+ 'NotFastForward' => 'sync never merges or rebases' }.freeze
28
+
29
+ # Dry-run projects that no fetch has reached, counted as they settle.
30
+ attr_reader :unfetched
31
+
32
+ # `group` is the one a command without -n selects, so the undo command can leave it out; nil
33
+ # when -n was typed, so every undo command names its group.
34
+ def initialize(runtime, fetcher, dry_run:, group:)
35
+ @runtime = runtime
36
+ @fetcher = fetcher
37
+ @dry_run = dry_run
38
+ @group = group
39
+ @unfetched = 0
40
+ end
41
+
42
+ # Runs on a worker thread: an Outcome when nothing is left to do, else an Observed.
43
+ def observe(project)
44
+ return Outcome.new(project:, word: Outcome::PAUSED) if project.paused
45
+
46
+ inspection = @runtime.inspector.examine(project)
47
+ return unreadable(inspection) if inspection.error
48
+
49
+ fetched = @fetcher.fetch(project, inspection)
50
+ return fetched unless REACHED.include?(fetched.word)
51
+
52
+ inspection = @runtime.inspector.examine(project) unless @dry_run
53
+ return unreadable(inspection) if inspection.error
54
+
55
+ Observed.new(project:, inspection:, fetched:, plan: Plan.for(inspection))
56
+ end
57
+
58
+ # Runs on the calling thread, in the order the projects are listed.
59
+ def settle(step)
60
+ return step unless step.is_a?(Observed)
61
+
62
+ @unfetched += 1 if @dry_run && step.inspection.fetched_at.nil?
63
+ plan = step.plan
64
+ return skipped(step) unless plan.skips.empty?
65
+ return move(step) if plan.fast_forward?
66
+
67
+ still(step)
68
+ end
69
+
70
+ private
71
+
72
+ # The checks inside the fast-forward read the repository again right before git merges,
73
+ # and --ff-only is git's own last word.
74
+ def move(step)
75
+ project = step.project
76
+ plan = step.plan
77
+ return outcome(project, FAST_FORWARDED, drift(plan.items)) if @dry_run
78
+
79
+ onto = plan.to_revision? ? project.revision : Git::Repository::UPSTREAM
80
+ forward = @fetcher.exclusively(project) { @runtime.git.fast_forward(@fetcher.path(project), onto:) }
81
+ return still(step) unless forward.moved?
82
+
83
+ outcome(project, FAST_FORWARDED, [moved(step, forward), *declared(plan)])
84
+ rescue Git::Blocked => e
85
+ refused(step, e)
86
+ rescue Git::Error => e
87
+ Outcome.failure(project, e)
88
+ end
89
+
90
+ def moved(step, forward)
91
+ range = "#{step.inspection.status.branch} #{forward.from[0, ABBREV]}..#{forward.to[0, ABBREV]}"
92
+ return "#{range} #{TO_PIN}" if step.plan.to_revision?
93
+
94
+ gained = forward.count
95
+ undo = RolloutHistory.command('undo', step.project, @group)
96
+ "#{range} (#{gained} commit#{'s' unless gained == 1}); undo with '#{undo}'"
97
+ end
98
+
99
+ def refused(step, error)
100
+ reason = error.reason
101
+ detail = [error.message.delete_prefix("#{error.path}: "), ADVICE[reason]].compact.join('; ')
102
+ command = CommandLine.git(step.project, 'status') unless reason == 'Busy'
103
+ outcome(step.project, Outcome::SKIPPED, [detail, command, *declared(step.plan)], reason:)
104
+ end
105
+
106
+ def skipped(step)
107
+ blocker = step.plan.skips.first
108
+ outcome(step.project, Outcome::SKIPPED, [blocker.message, blocker.command, *declared(step.plan)],
109
+ reason: blocker.type)
110
+ end
111
+
112
+ # Nothing moved, so the plan's drift says why. Only a FetchOnly project reports what its fetch
113
+ # brought; for any other, sync reports the branch.
114
+ def still(step)
115
+ fetched = step.fetched if step.project.sync_policy == SyncPolicy::FETCH_ONLY
116
+ outcome(step.project, fetched&.word || Outcome::UNCHANGED,
117
+ [*fetched&.details, held(step), *drift(step.plan.reports)])
118
+ end
119
+
120
+ def held(step)
121
+ "held at #{step.project.revision[0, ABBREV]} by spec.revision" if step.inspection.at_pin?
122
+ end
123
+
124
+ # Worded as diff words it, with the command that shows or resolves it.
125
+ def unreadable(inspection)
126
+ item = Plan.for(inspection).items.first
127
+ outcome(inspection.project, Outcome::SKIPPED, [item.message, item.command], reason: item.type)
128
+ end
129
+
130
+ # A move or a blocker already says where the branch stands against its upstream or its pin.
131
+ def declared(plan) = drift(plan.reports.reject { Drift::MOVES.include?(it.type) })
132
+
133
+ def drift(items) = items.map { "#{it.type}: #{it.message}" }
134
+
135
+ def outcome(project, word, details, reason: nil)
136
+ Outcome.new(project:, word:, reason:, details: details.compact)
137
+ end
138
+ end
139
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Slipway
4
+ VERSION = '0.1.0'
5
+ end
@@ -0,0 +1,35 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../labels'
4
+ require_relative '../output'
5
+
6
+ module Slipway
7
+ module Views
8
+ module Group
9
+ # See Views::Project::FIELDS.
10
+ FIELDS = { 'metadata.name' => '' }.freeze
11
+
12
+ def self.headers(wide: false, labels: false)
13
+ columns = %w[NAME PROJECTS AGE]
14
+ columns << 'DESCRIPTION' if wide
15
+ columns << 'LABELS' if labels
16
+ columns
17
+ end
18
+
19
+ def self.row(group, count:, now:, wide: false, labels: false)
20
+ cells = [group.name, count.to_s, Output::Age.humanize(group.created_at, now)]
21
+ cells << group.description if wide
22
+ cells << Labels.format(group.labels) if labels
23
+ cells
24
+ end
25
+
26
+ def self.describe(group, count:, now:)
27
+ [['Name', group.name], ['Labels', group.labels], ['Created', group.created_at],
28
+ ['Age', Output::Age.humanize(group.created_at, now)], ['Description', group.description],
29
+ ['Projects', count]]
30
+ end
31
+
32
+ def self.object(group, count:) = group.to_manifest.merge('status' => { 'projects' => count })
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,148 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../git/status'
4
+ require_relative '../git/url'
5
+ require_relative '../labels'
6
+ require_relative '../plan'
7
+ require_relative '../resources'
8
+ require_relative '../state'
9
+ require_relative '../output'
10
+
11
+ module Slipway
12
+ module Views
13
+ module Project
14
+ DETACHED = '(detached)'
15
+ # A repository git could not read shows <none> in FETCHED, like its other cells, so
16
+ # <never> always means git answered and FETCH_HEAD records no fetch.
17
+ NEVER = '<never>'
18
+ ROLES = lambda do |header, value|
19
+ case header
20
+ when 'STATUS' then State.role(value)
21
+ when 'FETCHED' then :muted if value == NEVER
22
+ end
23
+ end
24
+ # The paths of #object a field selector may name, each with the value it compares as when
25
+ # the object leaves it out: kubectl compares an unset field as the empty string, while a
26
+ # missing lastFetch means no fetch is on record, which FETCHED shows as <never>.
27
+ FIELDS = { 'metadata.name' => '', 'metadata.group' => '', 'spec.path' => '', 'status.state' => '',
28
+ 'status.branch' => '', 'status.lastFetch' => 'never' }.freeze
29
+
30
+ def self.headers(wide: false, group: false, labels: false)
31
+ columns = [*(['GROUP'] if group), 'NAME', 'BRANCH', 'STATUS', 'FETCHED', 'AGE']
32
+ columns.push('PATH', 'HEAD', 'LAST-COMMIT', 'DRIFT') if wide
33
+ columns << 'LABELS' if labels
34
+ columns
35
+ end
36
+
37
+ # nil cells are fine: Table prints them as <none>.
38
+ def self.row(inspection, now:, wide: false, group: false, labels: false)
39
+ project = inspection.project
40
+ cells = [*([project.group] if group), project.name, branch(inspection.status), inspection.state,
41
+ fetched(inspection, now), Output::Age.humanize(project.created_at, now)]
42
+ if wide
43
+ cells.push(project.path, inspection.status&.head, last_commit_age(inspection.commit, now),
44
+ drift_types(inspection))
45
+ end
46
+ cells << Labels.format(project.labels) if labels
47
+ cells
48
+ end
49
+
50
+ def self.describe(inspection, now:)
51
+ project = inspection.project
52
+ [['Name', project.name], ['Group', project.group], ['Labels', project.labels],
53
+ ['Created', project.created_at], ['Age', Output::Age.humanize(project.created_at, now)],
54
+ ['Path', project.path], ['Description', project.description], *desired(project),
55
+ ['Status', Output::Painted.new(role: State.role(inspection.state), text: inspection.state)],
56
+ ['Repository', repository(inspection)], ['Last Commit', last_commit(inspection.commit)],
57
+ ['Drift', drift(inspection).map { [it.type, it.message] }]]
58
+ end
59
+
60
+ # Fields git could not answer are left out, as kubectl leaves out unset fields.
61
+ def self.object(inspection)
62
+ status = inspection.status
63
+ inspection.project.to_manifest.merge(
64
+ 'status' => plain(
65
+ 'branch' => status&.branch, 'head' => status&.head, 'upstream' => status&.upstream,
66
+ 'ahead' => status&.ahead, 'behind' => status&.behind, 'staged' => status&.staged,
67
+ 'unstaged' => status&.unstaged, 'untracked' => status&.untracked,
68
+ 'conflicted' => status&.conflicted, 'stashes' => status&.stashes,
69
+ 'state' => inspection.state, 'lastFetch' => Resources.timestamp(inspection.fetched_at),
70
+ 'drift' => drift_object(inspection),
71
+ 'lastCommit' => commit_object(inspection.commit)
72
+ ).compact
73
+ )
74
+ end
75
+
76
+ def self.desired(project)
77
+ [['Remote', project.remote&.then { Git::Url.redact(it) }], ['Branch', project.branch],
78
+ ['Revision', project.revision&.then { "#{it[0, Git::Porcelain::ABBREVIATION]} (pinned)" }],
79
+ ['Sync Policy', project.sync_policy], ['Paused', project.paused]]
80
+ end
81
+
82
+ def self.branch(status)
83
+ return nil if status.nil?
84
+
85
+ status.detached? ? DETACHED : status.branch
86
+ end
87
+
88
+ def self.fetched(inspection, now)
89
+ return nil if inspection.status.nil?
90
+
91
+ inspection.fetched_at ? Output::Age.humanize(inspection.fetched_at, now) : NEVER
92
+ end
93
+
94
+ def self.last_commit_age(commit, now) = commit && Output::Age.humanize(commit.time, now)
95
+
96
+ def self.drift(inspection) = Plan.for(inspection).items
97
+
98
+ def self.drift_types(inspection)
99
+ types = drift(inspection).map(&:type)
100
+ types.join(',') unless types.empty?
101
+ end
102
+
103
+ def self.drift_object(inspection)
104
+ drift(inspection).map { plain('type' => it.type, 'message' => it.message, 'blocker' => it.blocker) }
105
+ end
106
+
107
+ def self.repository(inspection)
108
+ status = inspection.status
109
+ return failure(inspection.error) if status.nil?
110
+
111
+ [['Branch', branch(status)], ['Head', status.head], ['Upstream', status.upstream],
112
+ ['Ahead', status.ahead], ['Behind', status.behind], ['Staged', status.staged],
113
+ ['Unstaged', status.unstaged], ['Untracked', status.untracked],
114
+ ['Conflicted', status.conflicted], ['Stashes', status.stashes],
115
+ ['Remote', inspection.remote&.then { Git::Url.redact(it) }], ['Last Fetch', last_fetch(inspection)]]
116
+ end
117
+
118
+ def self.last_fetch(inspection) = inspection.fetched_at || Output::Painted.new(role: :muted, text: NEVER)
119
+
120
+ # The Path line already shows the path, so it is cut from git's message.
121
+ def self.failure(error)
122
+ reason = error.message.delete_prefix("#{error.path}: ")
123
+ error.respond_to?(:hint) && error.hint ? [reason, error.hint] : reason
124
+ end
125
+
126
+ def self.last_commit(commit)
127
+ return nil if commit.nil?
128
+
129
+ [['Hash', commit.sha], ['Author', "#{commit.author} <#{commit.email}>"],
130
+ ['Date', Resources.timestamp(commit.time)], ['Subject', commit.subject]]
131
+ end
132
+
133
+ def self.commit_object(commit)
134
+ return nil if commit.nil?
135
+
136
+ plain('hash' => commit.sha, 'author' => commit.author, 'email' => commit.email,
137
+ 'date' => Resources.timestamp(commit.time), 'subject' => commit.subject)
138
+ end
139
+
140
+ # json and yaml escape what their syntax needs, not what a terminal obeys, and `jq -r`
141
+ # prints a field as it is, so git's text is made plain here as it is in a table.
142
+ def self.plain(fields) = fields.transform_values { it.is_a?(String) ? Output.plain(it) : it }
143
+
144
+ private_class_method :desired, :branch, :fetched, :last_commit_age, :drift, :drift_types, :drift_object,
145
+ :repository, :last_fetch, :failure, :last_commit, :commit_object, :plain
146
+ end
147
+ end
148
+ end
@@ -0,0 +1,10 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'views/project'
4
+ require_relative 'views/group'
5
+
6
+ module Slipway
7
+ # Resources shaped for the renderers, one module per kind and no I/O.
8
+ module Views
9
+ end
10
+ end
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'psych'
4
+
5
+ module Slipway
6
+ # The one YAML writer: manifests, `get -o yaml` and `config view` print the same dialect.
7
+ module Yaml
8
+ DOCUMENT_START = "---\n"
9
+
10
+ # line_width: -1 turns off Psych's line folding. The document marker is dropped, the way
11
+ # kubectl prints objects.
12
+ def self.dump(hash) = Psych.safe_dump(hash, line_width: -1).delete_prefix(DOCUMENT_START)
13
+ end
14
+ end
data/lib/slipway.rb ADDED
@@ -0,0 +1,32 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'slipway/version'
4
+ require_relative 'slipway/cli'
5
+ require_relative 'slipway/paths'
6
+ require_relative 'slipway/settings'
7
+ require_relative 'slipway/editor'
8
+ require_relative 'slipway/names'
9
+ require_relative 'slipway/labels'
10
+ require_relative 'slipway/selector'
11
+ require_relative 'slipway/resources'
12
+ require_relative 'slipway/schema'
13
+ require_relative 'slipway/manifest'
14
+ require_relative 'slipway/store'
15
+ require_relative 'slipway/git'
16
+ require_relative 'slipway/state'
17
+ require_relative 'slipway/output'
18
+ require_relative 'slipway/runtime'
19
+ require_relative 'slipway/inspector'
20
+ require_relative 'slipway/views'
21
+ require_relative 'slipway/commands'
22
+
23
+ # A kubectl-style registry of the git repositories on your machine.
24
+ module Slipway
25
+ # Shared by exe/slipway and the tests, so it returns the exit status instead of exiting.
26
+ # `runtime_factory` is called only once a verb runs, with the context and the parsed options.
27
+ def self.run(argv, context: CLI::Context.system, runtime_factory: Runtime.method(:build))
28
+ registry = Commands.registry(runtime_factory)
29
+ color, theme = Runtime.color_defaults(context, argv)
30
+ CLI::Runner.new(registry, context, color:, theme:).run(argv)
31
+ end
32
+ end
@@ -0,0 +1,53 @@
1
+ .\" Generated by slipway 0.1.0. Do not edit.
2
+ .TH "SLIPWAY-API-RESOURCES" "1" "2026-09-30" "slipway 0.1.0" "Slipway Manual"
3
+ .SH NAME
4
+ slipway\-api\-resources \- Print the supported resource types
5
+ .SH SYNOPSIS
6
+ .SY "slipway api\-resources"
7
+ .RI [ flags ]
8
+ .YS
9
+ .SH DESCRIPTION
10
+ Print the supported resource types.
11
+ .PP
12
+ Prints a table of the resource types slipway knows, sorted by name. A command accepts a type by its NAME, its singular or one of its SHORTNAMES. Type words are case\-insensitive. Use \-o wide to add the verbs that act on each type, and \-o name to print only the names.
13
+ .SH OPTIONS
14
+ .TP
15
+ \fB\-o\fR, \fB\-\-output\fR \fIFORMAT\fR
16
+ Output format. One of: table, wide, name. (default "table")
17
+ .TP
18
+ \fB\-\-no\-headers\fR
19
+ When using the default output format, don't print headers.
20
+ .SH COLUMNS
21
+ Every type shows NAME, SHORTNAMES, KIND and GROUPED, and \-o wide adds VERBS. A type without aliases reads <none> in SHORTNAMES.
22
+ .TP
23
+ \fBNAME\fR
24
+ The name of the type, in the plural.
25
+ .TP
26
+ \fBSHORTNAMES\fR
27
+ The aliases of the type, besides its singular.
28
+ .TP
29
+ \fBKIND\fR
30
+ The kind a manifest of the type declares.
31
+ .TP
32
+ \fBGROUPED\fR
33
+ Whether resources of the type live in a group, so that \-n and \-A scope them. kubectl's NAMESPACED column says the same about namespaces.
34
+ .TP
35
+ \fBVERBS\fR
36
+ The commands that act on resources of the type.
37
+ .SH EXAMPLES
38
+ .EX
39
+ # Print the supported resource types
40
+ slipway api\-resources
41
+ .EE
42
+ .PP
43
+ .EX
44
+ # Print the supported resource types with more information
45
+ slipway api\-resources \-o wide
46
+ .EE
47
+ .PP
48
+ .EX
49
+ # Print the names of the supported resource types
50
+ slipway api\-resources \-o name
51
+ .EE
52
+ .SH "SEE ALSO"
53
+ .BR slipway (1)
@@ -0,0 +1,45 @@
1
+ .\" Generated by slipway 0.1.0. Do not edit.
2
+ .TH "SLIPWAY-APPLY" "1" "2026-09-30" "slipway 0.1.0" "Slipway Manual"
3
+ .SH NAME
4
+ slipway\-apply \- Apply a configuration to a resource by file name or stdin
5
+ .SH SYNOPSIS
6
+ .SY "slipway apply"
7
+ .B \-f
8
+ .I FILE
9
+ .RI [ flags ]
10
+ .YS
11
+ .SH DESCRIPTION
12
+ Apply a configuration to a resource by file name or stdin.
13
+ .PP
14
+ The resource name must be specified in the manifest. A resource is created when it does not exist yet, configured when its manifest differs from the one stored, and reported unchanged otherwise. A directory applies every *.yaml and *.yml file it holds, sorted by name and without descending into subdirectories. A project manifest without metadata.group lands in the current group.
15
+ .PP
16
+ YAML is accepted, with several documents per file, and a document of kind List stands for each of its items. Documents are applied in order, so a group may be created by the document before the projects that use it.
17
+ .SH OPTIONS
18
+ .TP
19
+ \fB\-f\fR, \fB\-\-filename\fR \fIFILE\fR
20
+ The file that contains the manifests to apply; may be repeated. A directory reads its *.yaml and *.yml files, '\-' reads stdin. (required)
21
+ .TP
22
+ \fB\-\-dry\-run\fR
23
+ Print what would change and write nothing.
24
+ .SH EXAMPLES
25
+ .EX
26
+ # Apply the configuration in hldr.yaml to a project
27
+ slipway apply \-f hldr.yaml
28
+ .EE
29
+ .PP
30
+ .EX
31
+ # Apply every manifest in a directory
32
+ slipway apply \-f ./projects
33
+ .EE
34
+ .PP
35
+ .EX
36
+ # Apply the YAML passed into stdin
37
+ slipway apply \-f \- < hldr.yaml
38
+ .EE
39
+ .PP
40
+ .EX
41
+ # Show what would change without writing anything
42
+ slipway apply \-f hldr.yaml \-\-dry\-run
43
+ .EE
44
+ .SH "SEE ALSO"
45
+ .BR slipway (1)
@@ -0,0 +1,29 @@
1
+ .\" Generated by slipway 0.1.0. Do not edit.
2
+ .TH "SLIPWAY-COMPLETION" "1" "2026-09-30" "slipway 0.1.0" "Slipway Manual"
3
+ .SH NAME
4
+ slipway\-completion \- Output shell completion code for the specified shell (bash, zsh, fish)
5
+ .SH SYNOPSIS
6
+ .SY "slipway completion"
7
+ .I SHELL\&
8
+ .RI [ flags ]
9
+ .YS
10
+ .SH DESCRIPTION
11
+ Output shell completion code for the specified shell (bash, zsh, fish).
12
+ The shell code must be evaluated to provide interactive completion of commands, resource types and names.
13
+ .SH EXAMPLES
14
+ .EX
15
+ # Install bash completions where bash\-completion loads them
16
+ slipway completion bash > "${XDG_DATA_HOME:\-$HOME/.local/share}/bash\-completion/completions/slipway"
17
+ .EE
18
+ .PP
19
+ .EX
20
+ # Install zsh completions in a directory on your fpath
21
+ slipway completion zsh > ~/.zfunc/_slipway
22
+ .EE
23
+ .PP
24
+ .EX
25
+ # Install fish completions
26
+ slipway completion fish > ~/.config/fish/completions/slipway.fish
27
+ .EE
28
+ .SH "SEE ALSO"
29
+ .BR slipway (1)
@@ -0,0 +1,20 @@
1
+ .\" Generated by slipway 0.1.0. Do not edit.
2
+ .TH "SLIPWAY-CONFIG-PATH" "1" "2026-09-30" "slipway 0.1.0" "Slipway Manual"
3
+ .SH NAME
4
+ slipway\-config\-path \- Display the path of the configuration file
5
+ .SH SYNOPSIS
6
+ .SY "slipway config path"
7
+ .RI [ flags ]
8
+ .YS
9
+ .SH DESCRIPTION
10
+ Display the path of the configuration file.
11
+ .PP
12
+ Prints the file that slipway reads: \-\-config, then SLIPWAY_CONFIG, then $XDG_CONFIG_HOME/slipway/config.yaml. When the default file does not exist, its path is still printed and a note on stderr says so; a file named by \-\-config or SLIPWAY_CONFIG must exist.
13
+ .SH EXAMPLES
14
+ .EX
15
+ # Print the path of the configuration file
16
+ slipway config path
17
+ .EE
18
+ .SH "SEE ALSO"
19
+ .BR slipway (1),
20
+ .BR slipway\-config (1)
@@ -0,0 +1,25 @@
1
+ .\" Generated by slipway 0.1.0. Do not edit.
2
+ .TH "SLIPWAY-CONFIG-VIEW" "1" "2026-09-30" "slipway 0.1.0" "Slipway Manual"
3
+ .SH NAME
4
+ slipway\-config\-view \- Display the configuration in effect
5
+ .SH SYNOPSIS
6
+ .SY "slipway config view"
7
+ .RI [ flags ]
8
+ .YS
9
+ .SH DESCRIPTION
10
+ Display the configuration in effect.
11
+ .PP
12
+ Prints every setting as YAML after applying the precedence flag, then SLIPWAY_* environment variable, then configuration file, then built\-in default. The first line names the configuration file that was consulted, and says (not found) when the default file does not exist. A file named by \-\-config or SLIPWAY_CONFIG must exist.
13
+ .SH EXAMPLES
14
+ .EX
15
+ # Show the settings in effect
16
+ slipway config view
17
+ .EE
18
+ .PP
19
+ .EX
20
+ # Show the settings another file would give
21
+ slipway config view \-\-config ~/work/slipway.yaml
22
+ .EE
23
+ .SH "SEE ALSO"
24
+ .BR slipway (1),
25
+ .BR slipway\-config (1)
@@ -0,0 +1,22 @@
1
+ .\" Generated by slipway 0.1.0. Do not edit.
2
+ .TH "SLIPWAY-CONFIG" "1" "2026-09-30" "slipway 0.1.0" "Slipway Manual"
3
+ .SH NAME
4
+ slipway\-config \- Inspect the configuration in effect
5
+ .SH SYNOPSIS
6
+ .SY "slipway config"
7
+ .I COMMAND
8
+ .RI [ flags ]
9
+ .YS
10
+ .SH DESCRIPTION
11
+ Inspect the configuration that slipway resolved from flags, environment variables and the configuration file.
12
+ .SH COMMANDS
13
+ .TP 6
14
+ \fBview\fR
15
+ Display the configuration in effect
16
+ .TP 6
17
+ \fBpath\fR
18
+ Display the path of the configuration file
19
+ .SH "SEE ALSO"
20
+ .BR slipway (1),
21
+ .BR slipway\-config\-view (1),
22
+ .BR slipway\-config\-path (1)
@@ -0,0 +1,89 @@
1
+ .\" Generated by slipway 0.1.0. Do not edit.
2
+ .TH "SLIPWAY-CREATE" "1" "2026-09-30" "slipway 0.1.0" "Slipway Manual"
3
+ .SH NAME
4
+ slipway\-create \- Create a resource by name
5
+ .SH SYNOPSIS
6
+ .SY "slipway create"
7
+ .I "(TYPE NAME | project \-\-from\-dir DIR)"\&
8
+ .RI [ flags ]
9
+ .YS
10
+ .SH DESCRIPTION
11
+ Create a resource by name.
12
+ .PP
13
+ A project registers the git repository at \-\-path in the current group, which must exist unless it is the default group. A group is created empty and holds the projects you register with \-\-group later.
14
+ .PP
15
+ With \-\-from\-dir DIR in place of NAME, create project registers every git repository at DIR or under it, down to \-\-depth levels. Each project is named after its directory, lowercased with each run of characters other than ASCII letters, digits and dashes made one dash, and records its path and the URL of its origin without any credentials; the checked\-out branch is not recorded. A path the group already holds is reported unchanged, so running it again adds only new clones.
16
+ .PP
17
+ Use \-\-dry\-run to check the arguments without writing anything, and \-o yaml with it to print the manifest instead, ready for apply \-f.
18
+ .PP
19
+ Resource types: projects (project, proj) and groups (group). Type words are case\-insensitive.
20
+ .SH OPTIONS
21
+ .TP
22
+ \fB\-\-path\fR \fIDIR\fR
23
+ Directory of the git repository to register; required for a project given by NAME. A relative directory is stored resolved against the current directory, a ~ path as written.
24
+ .TP
25
+ \fB\-\-description\fR \fITEXT\fR
26
+ A short description of the resource.
27
+ .TP
28
+ \fB\-\-label\fR \fIKEY=VALUE\fR
29
+ A label to set on the new resource; may be repeated.
30
+ .TP
31
+ \fB\-\-remote\fR \fIURL\fR
32
+ The URL the origin remote is expected to have, written to spec.remote. A URL that embeds credentials is refused; use a credential helper.
33
+ .TP
34
+ \fB\-\-branch\fR \fINAME\fR
35
+ The branch the project is expected to have checked out, written to spec.branch.
36
+ .TP
37
+ \fB\-\-from\-dir\fR \fIDIR\fR
38
+ Register every git repository at or under DIR as a project, in place of NAME.
39
+ .TP
40
+ \fB\-\-depth\fR \fIN\fR
41
+ How many directory levels under \-\-from\-dir to search, from 1 to 8. The search stops at a repository and never follows a symbolic link under \-\-from\-dir. (default 1)
42
+ .TP
43
+ \fB\-\-dry\-run\fR
44
+ Print what would change and write nothing.
45
+ .TP
46
+ \fB\-o\fR, \fB\-\-output\fR \fIFORMAT\fR
47
+ Output format; without it, each resource prints a result line such as project/hldr created. One of: json, yaml, name.
48
+ .SH EXAMPLES
49
+ .EX
50
+ # Register the repository at ~/dev/hldr as a project in the current group
51
+ slipway create project hldr \-\-path '~/dev/hldr'
52
+ .EE
53
+ .PP
54
+ .EX
55
+ # Register a project in the work group with two labels
56
+ slipway create project api \-\-path '~/work/api' \-n work \-\-label lang=go \-\-label tier=api
57
+ .EE
58
+ .PP
59
+ .EX
60
+ # Register a project with the remote and the branch it is expected to have
61
+ slipway create project hldr \-\-path '~/dev/hldr' \-\-remote git@github.com:hvpaiva/hldr.git \-\-branch main
62
+ .EE
63
+ .PP
64
+ .EX
65
+ # Create a group with a description
66
+ slipway create group work \-\-description "Projects for the day job"
67
+ .EE
68
+ .PP
69
+ .EX
70
+ # Check the arguments without writing the project
71
+ slipway create project hldr \-\-path '~/dev/hldr' \-\-dry\-run
72
+ .EE
73
+ .PP
74
+ .EX
75
+ # Print the manifest of a project without registering it
76
+ slipway create project hldr \-\-path '~/dev/hldr' \-\-dry\-run \-o yaml
77
+ .EE
78
+ .PP
79
+ .EX
80
+ # Register every repository in ~/dev/personal in the personal group
81
+ slipway create project \-\-from\-dir ~/dev/personal \-n personal
82
+ .EE
83
+ .PP
84
+ .EX
85
+ # Write the manifests of the repositories two levels under ~/work to a file
86
+ slipway create project \-\-from\-dir ~/work \-\-depth 2 \-\-dry\-run \-o yaml > work.yaml
87
+ .EE
88
+ .SH "SEE ALSO"
89
+ .BR slipway (1)