gdkbox 0.1.8 → 0.1.10

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: e1c7b818ffd457d8f32f3d80073585222be0140ba3ae5c32a76b587bae5a98f5
4
- data.tar.gz: 35b56df278015bbf0ad44cf175665c481cddd3939bc2130a5b72d193aec44c90
3
+ metadata.gz: 75c5c8ed1f89cb877856f68e45c39f4b5460131ea5a312e5af10e7cb7de2688b
4
+ data.tar.gz: a0eb3697bb1b630665be5799d33ffb8bb4aec0999858e075861e3469bd312431
5
5
  SHA512:
6
- metadata.gz: d0fd71a1460e658cd975298666fc7afaa1110a2b154a0da005bc80c7c4e9bdb21545ab544091c5a2f2773eab48e2e9520311e24b39c16962a7ae2416b9024bcf
7
- data.tar.gz: 44638a681108d58ddafdb1a9d4703f5a1f84cd6da764dae608d25ead66a3ebba8f4bb9057d57c38176ecd4f3c49e5c6809636c159c5c8ebbd0803c4b0ff2415d
6
+ metadata.gz: 344bba38a105dcf4bf899893d18eb7ef9f82ef03ef6d2229af810346e27f18b24c46e09c02c7f83e406255f79e8d033ff37c159ced868aad284e540c6cf7948c
7
+ data.tar.gz: ce33e8b7bd1aa403ea5fe77127b7c46fd1c4eb7079736689cc6447b6b0649bd270b880db82d31a33bb0d090f818366b1e2de847e6b3d70f918dbae257200014a
data/README.md CHANGED
@@ -90,6 +90,8 @@ Or run it straight from the checkout without installing:
90
90
  | `gdkbox claude NAME` | Install Claude Code inside the box. |
91
91
  | `gdkbox set-key NAME` | Seed/rotate the Anthropic API key in the box for unattended dispatch. |
92
92
  | `gdkbox set-host` | Add the `gdk.local` entry to your `/etc/hosts` so box web UIs resolve (`--remove` to undo). Also runs during `up`. |
93
+ | `gdkbox set-git NAME` | Seed your git identity (`user.name`/`user.email`) into the box so `git commit` works. Also runs during `up`. |
94
+ | `gdkbox set-remote NAME [REMOTE]` | Point the box's GitLab checkout at a different remote (URL or `namespace/project`, e.g. `gitlab-org/gitlab`); default from config.yml. |
93
95
  | `gdkbox start NAME` | Start a stopped box (and re-enable SSH). |
94
96
  | `gdkbox stop NAME` | Stop a running box. |
95
97
  | `gdkbox rm NAME` | Remove a box: container, metadata, and SSH entry. |
@@ -110,6 +112,7 @@ Or run it straight from the checkout without installing:
110
112
  | `--harness ID` | `claude` (or config.yml) | Agent harness to install: claude, codex, opencode, pi. |
111
113
  | `--no-agent` | (agent installed) | Skip installing the agent harness. |
112
114
  | `--skill NAME [NAME...]` | (none) | Seed skill(s) into the box at creation for dispatched agents. |
115
+ | `--gitlab-remote REMOTE` | config.yml, else image default | Point the GitLab checkout at this remote (URL or `namespace/project`); the image ships pointing at the community mirror. |
113
116
 
114
117
  ## Typical workflow
115
118
 
data/lib/gdkbox/box.rb CHANGED
@@ -137,6 +137,15 @@ module GDKBox
137
137
  )
138
138
  @store.save(@data)
139
139
 
140
+ # Best-effort: a host without a git identity (or a transient exec
141
+ # failure) should not abort the box; `gdkbox set-git` can seed it later.
142
+ begin
143
+ git_name, git_email = host_git_identity
144
+ provisioner.setup_git_identity(cname, name: git_name, email: git_email) if git_name || git_email
145
+ rescue StandardError
146
+ nil
147
+ end
148
+
140
149
  provisioner.setup_agent(cname, harness) if install_agent
141
150
  @data["agent_installed"] = install_agent
142
151
 
@@ -166,6 +175,41 @@ module GDKBox
166
175
  @store.delete(name)
167
176
  end
168
177
 
178
+ # The full git URL for a remote given as a URL or a "namespace/project"
179
+ # shorthand (expanded against gitlab.com over SSH, so pushes ride the
180
+ # forwarded agent).
181
+ def self.expand_remote(token)
182
+ return token if token.include?("://") || token.include?(":")
183
+
184
+ "git@gitlab.com:#{token}.git"
185
+ end
186
+
187
+ # Point the box's GitLab checkout at a different remote (URL or
188
+ # "namespace/project"). Returns the URL that origin now uses.
189
+ def set_gitlab_remote!(remote)
190
+ url = self.class.expand_remote(remote)
191
+ Provisioner.new(docker: @docker, config: @config)
192
+ .setup_gitlab_remote(container_name, url)
193
+ url
194
+ end
195
+
196
+ # Seed a git identity into the box so `git commit` works there. Falls back
197
+ # to the host's `git config` when name/email are not given; raises when
198
+ # neither source has anything to seed. Returns the [name, email] seeded.
199
+ def set_git_identity!(git_name: nil, git_email: nil)
200
+ host_name, host_email = host_git_identity
201
+ git_name ||= host_name
202
+ git_email ||= host_email
203
+ if git_name.nil? && git_email.nil?
204
+ raise Error, "No git identity found. Pass --name/--email or set " \
205
+ "`git config --global user.name/user.email` on the host."
206
+ end
207
+
208
+ Provisioner.new(docker: @docker, config: @config)
209
+ .setup_git_identity(container_name, name: git_name, email: git_email)
210
+ [git_name, git_email]
211
+ end
212
+
169
213
  # (Re)install this box's agent harness inside the container.
170
214
  def install_agent!
171
215
  Provisioner.new(docker: @docker, config: @config).setup_agent(container_name, harness)
@@ -267,6 +311,15 @@ module GDKBox
267
311
 
268
312
  private
269
313
 
314
+ # The host's git identity as [name, email], each nil when unset.
315
+ def host_git_identity
316
+ read = lambda do |key|
317
+ value = @shell.run("git", "config", "--get", key).stdout.strip
318
+ value.empty? ? nil : value
319
+ end
320
+ [read.call("user.name"), read.call("user.email")]
321
+ end
322
+
270
323
  # Choose free host ports and persist a preliminary record claiming them,
271
324
  # all while holding an exclusive lock so concurrent `create!` calls in
272
325
  # separate processes serialize and cannot pick the same ports. Returns the
data/lib/gdkbox/cli.rb CHANGED
@@ -37,6 +37,9 @@ module GDKBox
37
37
  desc: "Skill name(s) or path(s) to install into the box for dispatched agents"
38
38
  option :default_skills, type: :boolean, default: true,
39
39
  desc: "Also install the default skills from ~/.gdkbox/config.yml"
40
+ option :gitlab_remote, type: :string,
41
+ desc: "Point the GitLab checkout at this remote (URL or namespace/project; " \
42
+ "default from config.yml, else the image's community mirror)"
40
43
  def up(name)
41
44
  ensure_docker!
42
45
  box = build_box(name)
@@ -55,6 +58,7 @@ module GDKBox
55
58
  )
56
59
  rewrite_ssh_config
57
60
  seed_skills(box)
61
+ repoint_gitlab_remote(box)
58
62
 
59
63
  if options[:json]
60
64
  puts JSON.generate(box.summary)
@@ -236,6 +240,54 @@ module GDKBox
236
240
  end
237
241
  map "set-key" => :set_key
238
242
 
243
+ desc "set-git NAME", "Seed your git identity (user.name/email) into the box"
244
+ long_desc <<~DESC
245
+ Copies your git identity into the box's global git config so `git
246
+ commit` works there. Defaults to the host's `git config user.name` and
247
+ `user.email`; override either with --name/--email. Runs automatically
248
+ during `gdkbox up`, so this is mainly for boxes created before that or
249
+ for changing the identity later.
250
+
251
+ Pushing uses your host's ssh-agent, forwarded into interactive sessions
252
+ (`gdkbox ssh` / VS Code) by the generated SSH config — no keys or tokens
253
+ are stored in the box.
254
+ DESC
255
+ option :name, type: :string, desc: "git user.name to seed (defaults to the host's)"
256
+ option :email, type: :string, desc: "git user.email to seed (defaults to the host's)"
257
+ def set_git(name)
258
+ box = load_box!(name)
259
+ git_name, git_email = box.set_git_identity!(
260
+ git_name: options[:name], git_email: options[:email]
261
+ )
262
+ say "Seeded git identity into '#{name}': #{[git_name, git_email].compact.join(' <')}#{'>' if git_email}", :green
263
+ end
264
+ map "set-git" => :set_git
265
+
266
+ desc "set-remote NAME [REMOTE]", "Point the box's GitLab checkout at a different remote"
267
+ long_desc <<~DESC
268
+ The GDK-in-a-box image clones GitLab from the community mirror
269
+ (gitlab-community/gitlab-org/gitlab). This repoints the checkout's
270
+ origin — pass a full git URL or a namespace/project shorthand (expanded
271
+ to git@gitlab.com:namespace/project.git), or set `gitlab_remote:` in
272
+ ~/.gdkbox/config.yml and omit REMOTE. Runs `git remote set-url` plus a
273
+ fetch inside the box; `gdkbox up` does the same automatically when a
274
+ remote is configured. Pushing uses your forwarded ssh-agent and your own
275
+ permissions — no credentials are stored in the box.
276
+ DESC
277
+ def set_remote(name, remote = nil)
278
+ box = load_box!(name)
279
+ remote ||= config.default_gitlab_remote
280
+ unless remote
281
+ raise Error, "No remote. Pass REMOTE (URL or namespace/project) " \
282
+ "or set `gitlab_remote:` in ~/.gdkbox/config.yml."
283
+ end
284
+
285
+ say "Pointing '#{name}' GitLab checkout at #{Box.expand_remote(remote)} (fetching)...", :green
286
+ url = box.set_gitlab_remote!(remote)
287
+ say "origin now #{url}.", :green
288
+ end
289
+ map "set-remote" => :set_remote
290
+
239
291
  desc "set-host", "Add (default) or remove the gdk.local entry in /etc/hosts"
240
292
  long_desc <<~DESC
241
293
  GDK generates URLs and redirects that use the `gdk.local` hostname, so
@@ -412,6 +464,23 @@ module GDKBox
412
464
  end
413
465
  end
414
466
 
467
+ # Point the new box's GitLab checkout at the configured remote, when one
468
+ # is set (--gitlab-remote wins over config.yml; no setting keeps the
469
+ # image's community mirror). Best-effort like seed_skills: the box is
470
+ # already up, so a bad remote should warn, not abort. Muted in JSON mode.
471
+ def repoint_gitlab_remote(box)
472
+ remote = options[:gitlab_remote] || config.default_gitlab_remote
473
+ return unless remote
474
+
475
+ url = box.set_gitlab_remote!(remote)
476
+ say "Pointed the GitLab checkout at #{url}.", :green unless options[:json]
477
+ rescue StandardError => e
478
+ unless options[:json]
479
+ say "Could not repoint the GitLab checkout: #{e.message}", :yellow
480
+ say " Retry later with: gdkbox set-remote #{box.name} #{remote}", :yellow
481
+ end
482
+ end
483
+
415
484
  # Ensure gdk.local resolves on the host so the box's web UI is reachable
416
485
  # (GDK redirects to that hostname). Adding the entry needs sudo, so warn
417
486
  # before any password prompt appears. Failures are non-fatal: the box is
@@ -11,7 +11,8 @@ module GDKBox
11
11
  SHELLS = %w[bash zsh].freeze
12
12
 
13
13
  # Subcommands whose first positional argument is an existing box.
14
- BOX_COMMANDS = %w[status dispatch ssh code install-agent set-key start stop rm add-skill].freeze
14
+ BOX_COMMANDS = %w[status dispatch ssh code install-agent set-key set-git set-remote start
15
+ stop rm add-skill].freeze
15
16
 
16
17
  def initialize(cli_class = CLI)
17
18
  @cli = cli_class
data/lib/gdkbox/config.rb CHANGED
@@ -14,6 +14,7 @@ module GDKBox
14
14
  #
15
15
  # # ~/.gdkbox/config.yml
16
16
  # image: registry.example.com/my-gdk:latest # default image for `up`
17
+ # gitlab_remote: gitlab-org/gitlab # repoint the GitLab checkout
17
18
  # skills: # transferred into every box
18
19
  # - gdkbox-fleet
19
20
  # - code-history
@@ -102,6 +103,19 @@ module GDKBox
102
103
  key.empty? ? Harness.default : key
103
104
  end
104
105
 
106
+ # The git remote to point each box's GitLab checkout at, from config.yml
107
+ # (`gitlab_remote:`), or nil to keep the image's default (the community
108
+ # mirror). A URL or a "namespace/project" shorthand.
109
+ def default_gitlab_remote
110
+ remote = file_settings["gitlab_remote"].to_s.strip
111
+ remote.empty? ? nil : remote
112
+ end
113
+
114
+ # Where the GitLab repository lives inside the box.
115
+ def gitlab_checkout_path
116
+ File.join(remote_path, "gitlab")
117
+ end
118
+
105
119
  # An API key read from config.yml under `config_key` (e.g.
106
120
  # "anthropic_api_key", "openai_api_key"), or nil. This is a plaintext
107
121
  # secret on disk, so it is the lowest-priority source (a flag or the
@@ -131,6 +131,27 @@ module GDKBox
131
131
  done
132
132
  BASH
133
133
 
134
+ # Seeds the GDK user's global git identity so `git commit` works inside
135
+ # the box. Values arrive via the environment (dodging shell quoting) and
136
+ # blanks are skipped, so a partial host config seeds what it can.
137
+ GIT_IDENTITY_SETUP = <<~'BASH'
138
+ set -e
139
+ [ -n "$GDKBOX_GIT_NAME" ] && git config --global user.name "$GDKBOX_GIT_NAME"
140
+ [ -n "$GDKBOX_GIT_EMAIL" ] && git config --global user.email "$GDKBOX_GIT_EMAIL"
141
+ true
142
+ BASH
143
+
144
+ # Points the GitLab checkout's origin at a different repository. The image
145
+ # hardcodes the community mirror (gitlab-community/gitlab-org/gitlab);
146
+ # team members typically want the canonical gitlab-org/gitlab. The mirror
147
+ # shares its history, so a set-url plus fetch is all it takes (the
148
+ # checkout is treeless, keeping the fetch cheap).
149
+ GITLAB_REMOTE_SETUP = <<~'BASH'
150
+ set -e
151
+ git remote set-url origin "$GDKBOX_GITLAB_REMOTE"
152
+ git fetch --quiet origin
153
+ BASH
154
+
134
155
  def initialize(docker:, config:)
135
156
  @docker = docker
136
157
  @config = config
@@ -168,6 +189,26 @@ module GDKBox
168
189
  )
169
190
  end
170
191
 
192
+ def setup_git_identity(container_name, name:, email:)
193
+ @docker.exec(
194
+ container_name, GIT_IDENTITY_SETUP,
195
+ user: @config.ssh_user,
196
+ env: {
197
+ "GDKBOX_GIT_NAME" => name.to_s,
198
+ "GDKBOX_GIT_EMAIL" => email.to_s
199
+ }
200
+ )
201
+ end
202
+
203
+ def setup_gitlab_remote(container_name, url)
204
+ @docker.exec(
205
+ container_name, GITLAB_REMOTE_SETUP,
206
+ user: @config.ssh_user,
207
+ workdir: @config.gitlab_checkout_path,
208
+ env: { "GDKBOX_GITLAB_REMOTE" => url }
209
+ )
210
+ end
211
+
171
212
  def setup_api_key(container_name, api_key, key_env)
172
213
  @docker.exec(
173
214
  container_name, API_KEY_SETUP,
@@ -32,6 +32,11 @@ module GDKBox
32
32
  lines << " User #{box['ssh_user']}"
33
33
  lines << " IdentityFile #{@config.private_key_path}"
34
34
  lines << " IdentitiesOnly yes"
35
+ # Forward the host's ssh-agent so `git push` inside the box uses the
36
+ # user's own keys — which never enter the container, and are only
37
+ # reachable while a human session is connected (dispatched agents run
38
+ # via `docker exec` and never see the agent).
39
+ lines << " ForwardAgent yes"
35
40
  lines << " StrictHostKeyChecking no"
36
41
  lines << " UserKnownHostsFile /dev/null"
37
42
  lines << ""
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module GDKBox
4
- VERSION = "0.1.8"
4
+ VERSION = "0.1.10"
5
5
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: gdkbox
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.8
4
+ version: 0.1.10
5
5
  platform: ruby
6
6
  authors:
7
7
  - jotolo