@pablozaiden/devbox 0.1.3 → 0.1.5

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.
package/README.md CHANGED
@@ -12,6 +12,7 @@ It does not modify the original `devcontainer.json`. Instead, it generates a der
12
12
  - Publishes the same TCP port on host and container.
13
13
  - Mounts the current directory into the container as the workspace.
14
14
  - Shares a usable SSH agent socket with the container and copies `known_hosts` into the container.
15
+ - Seeds the container user's global Git `user.name` and `user.email` from the host when available.
15
16
  - Runs the [`ssh-server-runner`](https://github.com/PabloZaiden/ssh-server-runner) one-liner inside the devcontainer.
16
17
  - Persists the runner credentials on the mounted workspace as a local `.sshcred` file, and keeps SSH host keys in `.devbox-ssh-host-keys/`, so they survive `down` / `rebuild`.
17
18
 
@@ -108,6 +109,7 @@ cd examples/smoke-workspace
108
109
  - When Docker Desktop host services are available, `devbox` can share the SSH agent without relying on a host-shell `SSH_AUTH_SOCK`.
109
110
  - On Docker Desktop, `devbox` prefers the Docker-provided SSH agent socket over the host `SSH_AUTH_SOCK`, which avoids macOS launchd socket mount issues.
110
111
  - `--allow-missing-ssh` starts the workspace without mounting an SSH agent and prints a warning instead of failing.
112
+ - When the host already has Git author identity configured, `devbox` copies it into the container user's global Git config if the container does not already define those values.
111
113
 
112
114
  ## Releasing to npm
113
115
 
package/dist/devbox.js CHANGED
@@ -1305,12 +1305,22 @@ function formatDevcontainerProgressLine(line) {
1305
1305
  return null;
1306
1306
  }
1307
1307
  if (type === "raw" && text === "Container started") {
1308
- return "Container started.";
1308
+ return "Container started. Finishing devcontainer setup...";
1309
1309
  }
1310
1310
  if (text.startsWith("workspace root: ")) {
1311
1311
  return `Workspace: ${text.slice("workspace root: ".length)}`;
1312
1312
  }
1313
- if (text === "No user features to update" || text === "Inspecting container" || text.startsWith("Run: ") || text.startsWith("Run in container: ") || text.startsWith("userEnvProbe") || text.startsWith("LifecycleCommandExecutionMap:") || text.startsWith("Exit code ")) {
1313
+ if (text === "Inspecting container") {
1314
+ return "Inspecting container...";
1315
+ }
1316
+ if (text.startsWith("userEnvProbe")) {
1317
+ return "Checking container environment...";
1318
+ }
1319
+ const lifecycleProgress = formatDevcontainerLifecycleProgress(text);
1320
+ if (lifecycleProgress) {
1321
+ return lifecycleProgress;
1322
+ }
1323
+ if (text === "No user features to update" || text.startsWith("Run: ") || text.startsWith("Run in container: ") || text.startsWith("Exit code ")) {
1314
1324
  return null;
1315
1325
  }
1316
1326
  if (level !== undefined && level >= 2) {
@@ -1340,6 +1350,25 @@ function buildAssertConfiguredSshAuthSockScript() {
1340
1350
  ].join(`
1341
1351
  `);
1342
1352
  }
1353
+ function buildConfigureGitIdentityScript(input) {
1354
+ const commands = [];
1355
+ if (input.gitUserName) {
1356
+ commands.push('current_git_user_name="$(git config --global --get user.name 2>/dev/null || true)"', 'if [ -z "$current_git_user_name" ]; then', ` git config --global user.name ${quoteShell(input.gitUserName)}`, "fi");
1357
+ }
1358
+ if (input.gitUserEmail) {
1359
+ commands.push('current_git_user_email="$(git config --global --get user.email 2>/dev/null || true)"', 'if [ -z "$current_git_user_email" ]; then', ` git config --global user.email ${quoteShell(input.gitUserEmail)}`, "fi");
1360
+ }
1361
+ if (commands.length === 0) {
1362
+ return null;
1363
+ }
1364
+ return [
1365
+ "if ! command -v git >/dev/null 2>&1; then",
1366
+ " exit 0",
1367
+ "fi",
1368
+ ...commands
1369
+ ].join(`
1370
+ `);
1371
+ }
1343
1372
  function buildInteractiveShellScript() {
1344
1373
  return [
1345
1374
  "if command -v bash >/dev/null 2>&1; then",
@@ -1405,13 +1434,21 @@ async function ensureHostEnvironment(options) {
1405
1434
  }
1406
1435
  const hostEnvSshAuthSock = process.env.SSH_AUTH_SOCK?.trim() || undefined;
1407
1436
  const hostEnvSockExists = hostEnvSshAuthSock ? await pathExists(hostEnvSshAuthSock) : false;
1408
- const dockerDesktopHostServiceAvailable = await hasDockerDesktopHostService();
1409
- return resolveSshAuthSockSource({
1410
- hostEnvSshAuthSock,
1411
- hostEnvSockExists,
1412
- dockerDesktopHostServiceAvailable,
1413
- allowMissingSsh: options.allowMissingSsh
1414
- });
1437
+ const [dockerDesktopHostServiceAvailable, gitUserName, gitUserEmail] = await Promise.all([
1438
+ hasDockerDesktopHostService(),
1439
+ tryGetGitConfig(options.workspacePath, "user.name"),
1440
+ tryGetGitConfig(options.workspacePath, "user.email")
1441
+ ]);
1442
+ return {
1443
+ ...resolveSshAuthSockSource({
1444
+ hostEnvSshAuthSock,
1445
+ hostEnvSockExists,
1446
+ dockerDesktopHostServiceAvailable,
1447
+ allowMissingSsh: options.allowMissingSsh
1448
+ }),
1449
+ gitUserName,
1450
+ gitUserEmail
1451
+ };
1415
1452
  }
1416
1453
  async function ensureGeneratedConfigIgnored(workspacePath, generatedConfigPath) {
1417
1454
  await ensurePathIgnored(workspacePath, generatedConfigPath);
@@ -1517,6 +1554,13 @@ async function copyKnownHosts(containerId) {
1517
1554
  const script = `if [ -f ${quoteShell(KNOWN_HOSTS_TARGET)} ]; then umask 077 && mkdir -p ~/.ssh && cp ${quoteShell(KNOWN_HOSTS_TARGET)} ~/.ssh/known_hosts && chmod 600 ~/.ssh/known_hosts; fi`;
1518
1555
  await devcontainerExec(containerId, script, { quiet: true });
1519
1556
  }
1557
+ async function configureGitIdentity(containerId, gitUserName, gitUserEmail) {
1558
+ const script = buildConfigureGitIdentityScript({ gitUserName, gitUserEmail });
1559
+ if (!script) {
1560
+ return;
1561
+ }
1562
+ await devcontainerExec(containerId, script, { quiet: true });
1563
+ }
1520
1564
  async function stopManagedSshd(containerId) {
1521
1565
  await devcontainerExec(containerId, buildStopManagedSshdScript(), { quiet: true });
1522
1566
  }
@@ -1642,6 +1686,22 @@ async function tryGetGitTopLevel(workspacePath) {
1642
1686
  return null;
1643
1687
  }
1644
1688
  }
1689
+ async function tryGetGitConfig(workspacePath, key) {
1690
+ if (!isExecutableAvailable("git")) {
1691
+ return null;
1692
+ }
1693
+ const result = await execute(["git", "config", "--get", key], {
1694
+ cwd: workspacePath,
1695
+ stdoutMode: "capture",
1696
+ stderrMode: "capture",
1697
+ allowFailure: true
1698
+ });
1699
+ if (result.exitCode !== 0) {
1700
+ return null;
1701
+ }
1702
+ const trimmed = result.stdout.trim();
1703
+ return trimmed.length > 0 ? trimmed : null;
1704
+ }
1645
1705
  async function findListeningPids(port) {
1646
1706
  if (isExecutableAvailable("lsof")) {
1647
1707
  const result = await execute(["lsof", "-nP", `-iTCP:${port}`, "-sTCP:LISTEN", "-t"], {
@@ -1713,6 +1773,7 @@ async function consumeStream(stream, mode, useStderr) {
1713
1773
  stream.setEncoding("utf8");
1714
1774
  let captured = "";
1715
1775
  let buffered = "";
1776
+ let lastRenderedProgressLine = null;
1716
1777
  for await (const chunk of stream) {
1717
1778
  const text = typeof chunk === "string" ? chunk : String(chunk);
1718
1779
  captured += text;
@@ -1729,13 +1790,13 @@ async function consumeStream(stream, mode, useStderr) {
1729
1790
  while (newlineIndex >= 0) {
1730
1791
  const line = buffered.slice(0, newlineIndex);
1731
1792
  buffered = buffered.slice(newlineIndex + 1);
1732
- renderDevcontainerJsonLine(line, writer);
1793
+ lastRenderedProgressLine = renderDevcontainerJsonLine(line, writer, lastRenderedProgressLine);
1733
1794
  newlineIndex = buffered.indexOf(`
1734
1795
  `);
1735
1796
  }
1736
1797
  }
1737
1798
  if (mode === "devcontainer-json" && buffered.length > 0) {
1738
- renderDevcontainerJsonLine(buffered, writer);
1799
+ renderDevcontainerJsonLine(buffered, writer, lastRenderedProgressLine);
1739
1800
  }
1740
1801
  return captured;
1741
1802
  }
@@ -1771,12 +1832,14 @@ function isExecutablePath(candidate) {
1771
1832
  return false;
1772
1833
  }
1773
1834
  }
1774
- function renderDevcontainerJsonLine(line, writer) {
1835
+ function renderDevcontainerJsonLine(line, writer, previousLine) {
1775
1836
  const formatted = formatDevcontainerProgressLine(line);
1776
- if (formatted) {
1837
+ if (formatted && formatted !== previousLine) {
1777
1838
  writer.write(`${formatted}
1778
1839
  `);
1840
+ return formatted;
1779
1841
  }
1842
+ return previousLine;
1780
1843
  }
1781
1844
  function parseDevcontainerOutcome(stdout) {
1782
1845
  const lines = stdout.split(/\r?\n/).map((line) => line.trim()).filter(Boolean);
@@ -1806,6 +1869,16 @@ ${details}`;
1806
1869
  function stripAnsi(text) {
1807
1870
  return text.replace(/\u001B\[[0-9;]*m/g, "");
1808
1871
  }
1872
+ function formatDevcontainerLifecycleProgress(text) {
1873
+ if (!text.startsWith("LifecycleCommandExecutionMap:")) {
1874
+ return null;
1875
+ }
1876
+ const commandMatch = text.match(/\b(initializeCommand|onCreateCommand|updateContentCommand|postCreateCommand|postStartCommand|postAttachCommand)\b/);
1877
+ if (commandMatch) {
1878
+ return `Running ${commandMatch[1]}...`;
1879
+ }
1880
+ return "Running devcontainer lifecycle commands...";
1881
+ }
1809
1882
  function looksLikeDevcontainerUserEnvProbeDump(text) {
1810
1883
  const match = text.match(/^([0-9a-f]{8}(?:-[0-9a-f]{4}){3}-[0-9a-f]{12})([\s\S]*)\1$/i);
1811
1884
  if (!match) {
@@ -1840,7 +1913,7 @@ async function main() {
1840
1913
  await handleUpLike(parsed.command, workspacePath, state, parsed.port, parsed.allowMissingSsh, parsed.devcontainerSubpath);
1841
1914
  }
1842
1915
  async function handleUpLike(command, workspacePath, state, explicitPort, allowMissingSsh, devcontainerSubpath) {
1843
- const environment = await ensureHostEnvironment({ allowMissingSsh });
1916
+ const environment = await ensureHostEnvironment({ allowMissingSsh, workspacePath });
1844
1917
  const port = resolvePort(command, explicitPort, state);
1845
1918
  const knownHostsPath = await getKnownHostsPath();
1846
1919
  const discovered = await discoverDevcontainerConfig(workspacePath, devcontainerSubpath);
@@ -1899,6 +1972,10 @@ async function handleUpLike(command, workspacePath, state, explicitPort, allowMi
1899
1972
  await assertConfiguredSshAuthSockAvailable(upResult.containerId);
1900
1973
  }
1901
1974
  await copyKnownHosts(upResult.containerId);
1975
+ if (environment.gitUserName || environment.gitUserEmail) {
1976
+ console.log("Syncing Git author identity from the host into the devcontainer...");
1977
+ await configureGitIdentity(upResult.containerId, environment.gitUserName, environment.gitUserEmail);
1978
+ }
1902
1979
  await stopManagedSshd(upResult.containerId);
1903
1980
  await restoreRunnerHostKeys(upResult.containerId, remoteWorkspaceFolder);
1904
1981
  console.log("Installing and starting the SSH server inside the container (first run can take a bit)...");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pablozaiden/devbox",
3
- "version": "0.1.3",
3
+ "version": "0.1.5",
4
4
  "type": "module",
5
5
  "description": "CLI to run and expose a devcontainer with SSH agent sharing and a forwarded ssh-server-runner port.",
6
6
  "repository": {