Metadata-Version: 2.4
Name: sync2pod
Version: 0.1.3
Summary: Synchronize local project trees into running Kubernetes pods
Author: sync2pod contributors
License-Expression: Apache-2.0
License-File: LICENSE
Classifier: Development Status :: 3 - Alpha
Classifier: License :: OSI Approved :: Apache Software License
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Requires-Python: <3.14,>=3.10
Requires-Dist: loguru<1,>=0.7
Requires-Dist: watchdog<7,>=4
Description-Content-Type: text/markdown

# sync2pod

`sync2pod` manages persistent local [DevSpace](https://devspace.sh/) sync
tasks. It writes a task definition, starts `devspace sync` in an independent
background process group, records its runtime identity and log, and provides
safe lifecycle, inspection, log, and cleanup commands.

This project is alpha software. Its configuration and command behavior may
change.

## Installation

Install from PyPI:

```bash
python3 -m pip install --upgrade sync2pod
hash -r
sync2pod --help
```

Run the same command later to upgrade to the latest published release. To
confirm the installed package version, use:

```bash
python3 -m pip show sync2pod
```

Build and install a local wheel from source:

```bash
./scripts/build-package.sh
python3 -m pip install --upgrade dist/*.whl
```

### Global command installation

Use `pipx` to install the latest PyPI release as an isolated global command on
macOS:

```bash
brew install pipx
pipx ensurepath
pipx install sync2pod
```

Open a new terminal, then verify the command is on `PATH`:

```bash
sync2pod --help
```

Upgrade the global PyPI installation with:

```bash
pipx upgrade sync2pod
```

To replace the global command with a locally built wheel instead:

```bash
uv build --offline
pipx install --force dist/*.whl
```

Python 3.10 through 3.13 are supported. The wheel installs two equivalent
console commands:

```bash
sync2pod --help
sync_local_to_pod --help
```

The `sync_local_to_pod` name preserves compatibility with existing scripts.

## DevSpace task manager

The built-in `devspace-upload` template manages a local-to-pod DevSpace sync
session. It starts the equivalent of `devspace sync --upload-only
--initial-sync mirrorLocal` in the background, then records the task state and
combined DevSpace output locally.

`create` requires DevSpace and checks that `devspace sync` is available. It
then verifies Kubernetes access before writing the task, using the supplied
`--kubeconfig` explicitly (with its context and namespace). Run the same
minimal connectivity check yourself before creating a task:

```bash
kubectl \
  --kubeconfig ~/.kube/config \
  --context my-context \
  get namespace development \
  --request-timeout=10s
```

`start` and `restart` recheck that DevSpace is available locally. Also verify
that the label selector resolves to the intended container. `mirrorLocal` is authoritative: files
deleted locally can be deleted from the remote target. The remote root
directory (`/`) and double-slash paths are rejected to avoid mirroring into an
unsafe target.

Create a task as shown below. `create` prints a fixed 12-character task
ID; every later DevSpace command uses that ID, never the task name.

```bash
sync2pod create my-project \
  --local-path /Users/me/src/my-project \
  --remote-path /workspace/my-project \
  --kubeconfig ~/.kube/config \
  --kube-context my-context \
  --namespace development \
  --label-selector app=my-project \
  --container app \
  --exclude .git \
  --exclude .DS_Store

sync2pod start <task_id>
sync2pod list
sync2pod info <task_id>
sync2pod info
```

By default, `create` prints a human-readable task summary. With `--json`, it
prints JSON similar to:

```json
{
  "config_path": "/Users/me/.sync2pod/tasks/my-project/task.json",
  "id": "a1b2c3d4e5f6",
  "task_name": "my-project"
}
```

Keep the returned ID. Task names are only used when creating the definition;
all lifecycle operations use the fixed task ID.

### Output formats

All task commands except `logs` default to a human-readable display. Add
`--json` for one machine-readable JSON value on standard output:

```bash
sync2pod list
sync2pod list --json
sync2pod info <task_id> --json
sync2pod start <task_id> --json
```

### Diagnostics

Run `doctor` before creating or starting tasks to diagnose the local
environment and every registered task:

```bash
sync2pod doctor
sync2pod doctor --json
```

Doctor checks that `devspace` and `kubectl` are available. For every registered
task, it checks that the local path still exists, selects exactly one Running
Pod using the task's kubeconfig, context, namespace, and label selector, and
verifies that the configured container exists. It does not modify task files,
Kubernetes resources, or DevSpace helper state. Doctor exits with status `1`
when any check fails after reporting all available results.

`logs` is a streaming terminal command. Use `--tail` to select the initial
line count, and `--follow` to print new DevSpace output as it arrives.

The generated command is equivalent to:

```bash
devspace sync \
  --kubeconfig ~/.kube/config \
  --kube-context my-context \
  --namespace development \
  --label-selector app=my-project \
  --container app \
  --path /Users/me/src/my-project:/workspace/my-project \
  --upload-only \
  --initial-sync mirrorLocal \
  --exclude .git \
  --exclude .DS_Store \
  --debug
```

Task files are stored under `~/.sync2pod/tasks/<name>/`:

- `task.json`: the DevSpace task template and its fixed `task_id`.
- `runtime.json`: runner PID, process identity, command, lifecycle state, and
  timing information.
- `logs/current.log`: current combined stdout and stderr of the background
  process.
- `logs/*.log`: rotated logs from previous runs or rotations.

`list` prints the task ID, task name, and local upload path. `info <task_id>` prints its full
configuration, configuration-file path, and state (`running`, `stopped`, or
`failed`); `info` prints those details for every task. Runtime details can also
include the PID, human-readable start/end times, exit code, and log path.
`runtime.json` persists `sync_state` (`initializing`, `syncing`, `synced`, or
`failed`), `initial_sync` (`pending` or `completed`), `pending_changes`,
`last_sync_at`, `last_change_at`, and `last_error`. `synced` means DevSpace has
reported completion for the current known changes; it does not imply that the
background task has stopped watching for later local changes. A second `start` for
a running task is refused. If the existing process identity cannot be verified,
`start`, `stop`, `restart`, and `delete` fail safely instead of risking a second sync
process or deleting an active task. Task names are immutable: delete an old
task before creating a replacement with the same name.

Stop, restart, observe, or delete a task by ID:

```bash
sync2pod stop <task_id>
sync2pod restart <task_id>
sync2pod logs <task_id> --tail 50
sync2pod logs <task_id> --follow
sync2pod gc --dry-run
sync2pod gc
sync2pod delete <task_id>
```

`stop`, `restart`, and `delete` send `SIGTERM` to the isolated DevSpace process
group, so helper processes stop with the parent. `stop` preserves the task
definition and log so it can be started again. `delete` waits for a running
task to stop before removing its task definition, runtime state, and log.
Current logs rotate at 50 MiB; up to three archives are retained. `gc` removes
logs for stopped or failed tasks seven days after their recorded end time while
retaining `task.json`. It never removes logs of a running task.

### Task lifecycle reference

| Command | Behavior |
| --- | --- |
| `sync2pod create <name> ...` | Verifies DevSpace and the explicitly supplied kubeconfig, then persists a task definition and prints its task ID. It does not start synchronization. |
| `sync2pod start <task_id>` | Rechecks DevSpace, then starts the managed runner in the background. |
| `sync2pod stop <task_id>` | Stops a running task but preserves its definition and log. |
| `sync2pod list [--verbose]` | Prints registered task IDs, names, and local upload paths. `--verbose` also shows runtime state, sync state, remote path, and last sync time. |
| `sync2pod info [task_id]` | Prints one task's details, or every task's details when the ID is omitted. |
| `sync2pod restart <task_id>` | Rechecks DevSpace, stops the current process group, and starts a new background runner. |
| `sync2pod logs <task_id> [--tail N] [--follow]` | Prints the current task log, optionally continuing as new lines arrive. |
| `sync2pod gc [--dry-run]` | Lists or removes expired logs for stopped or failed tasks. |
| `sync2pod doctor [--json]` | Read-only check of local dependencies and every registered task's local path, target Pod, and container. |
| `sync2pod delete <task_id>` | Stops a running task and removes its local task definition, state, and log. |

These commands do not create or delete Kubernetes API resources. `start` and
`restart` run `devspace sync`, which may inject DevSpace's normal sync helper
into the target container for the life of the local sync connection. `logs`
only reads the local task log.

## Legacy native engine quick start

The archive-based native engine remains available through the legacy commands
and Python API for compatibility. Create a filesystem-backed project
configuration, inspect it, and run a dry run before connecting to a pod:

```bash
sync2pod --init-config --local-path /path/to/project --project demo
$EDITOR ~/.sync2pod/demo/sync_config.json
sync2pod --list-projects
sync2pod --project demo --dry-run
sync2pod --project demo
```

Without `--project`, the project name defaults to the final component of
`--local-path` during initialization.

## Configuration

Each normal CLI project is stored as
`~/.sync2pod/<project>/sync_config.json`. Existing JSON project objects remain
compatible with the standalone package. Unknown legacy fields are ignored.

A standard `kubectl` configuration looks like this:

```json
{
  "cluster": "development",
  "namespace": "workspace",
  "pod_label": "app=my-service",
  "remote_parent_path": "/workspace",
  "local_path": "/Users/me/src/my-service",
  "container": "app",
  "kubectl_command": ["kubectl", "--context", "development"],
  "follow_sync": [],
  "exclude_paths": [".git", "node_modules", "*.log"],
  "max_workers": 10,
  "debounce_seconds": 1.0,
  "no_watch": false,
  "skip_verify": false,
  "prune": true,
  "upload_chunk_count": 6,
  "target_chunk_size_mb": 64
}
```

Wrapper commands are represented as an argument array, not a shell string.
`{cluster}` is replaced with the configured `cluster` value:

```json
{
  "cluster": "development",
  "kubectl_command": ["cluster-wrapper", "--target={cluster}", "kubectl"]
}
```

The complete schema is:

| Field | Required | Default | Meaning |
| --- | --- | --- | --- |
| `cluster` | yes | - | Cluster identifier available to wrapper commands. |
| `namespace` | yes | - | Namespace containing the target pod. |
| `pod_label` | yes | - | Label selector that must match exactly one running pod. |
| `remote_parent_path` | yes | - | Absolute POSIX parent; the local directory name is appended. |
| `local_path` | yes | - | Existing primary source directory. |
| `container` | no | `null` | Explicit target container for every `exec` and `cp`; omit it for kubectl's default container. |
| `kubectl_command` | no | `["kubectl"]` | Non-empty argument array used before every Kubernetes operation. |
| `follow_sync` | no | `[]` | Additional source directories mapped below the remote project. |
| `exclude_paths` | no | `[]` | Path-name and glob exclusions applied to full and sparse sync. |
| `compress_threshold` | no | `100` | Positive legacy-compatible tuning value. |
| `max_workers` | no | `10` | Maximum sparse-watch worker count. |
| `debug` | no | `false` | Legacy-compatible diagnostic setting. |
| `show_concurrency` | no | `false` | Legacy-compatible concurrency display setting. |
| `no_watch` | no | `false` | Exit after the startup full mirror. |
| `skip_verify` | no | `false` | Skip the interactive configuration confirmation. |
| `debounce_seconds` | no | `1.0` | Positive watch-event debounce interval. |
| `prune` | no | `true` | Legacy-compatible setting; startup remains an authoritative mirror. |
| `upload_chunk_count` | no | `6` | Upper bound used when scheduling archive shards. |
| `target_chunk_size_mb` | no | `64` | Positive target size for startup archive shards. |

`cluster`, `namespace`, `pod_label`, `remote_parent_path`, and `local_path`
must be present and non-empty. Boolean and positive-number fields are validated
strictly.

### Follow mappings and managed symlinks

`follow_sync` maps directories outside the primary tree into named children of
the remote project:

```json
{
  "follow_sync": [
    {
      "local_path": "/Users/me/src/shared-library",
      "alias": "shared",
      "create_symlink": true
    }
  ]
}
```

The remote mapping is `/workspace/my-service/shared` in this example. When
`alias` is omitted or blank, the follow directory name is used. Aliases must be
single safe path segments and must be unique.

With `create_symlink: true`, sync2pod also manages
`<local_path>/shared -> /Users/me/src/shared-library` before synchronization.
It reuses an exact existing symlink but refuses to overwrite a file, directory,
or conflicting symlink. Set `create_symlink` to `false` to keep only the remote
mapping.

## Synchronization behavior

Every normal start performs an authoritative compressed full mirror before
watch mode becomes active. Files are archived locally, copied in a bounded
number of shards, verified, extracted into staging directories, and switched
into place only after all mappings are ready.

Warning: the remote project path is a mirror target, not a merge destination.
Startup replacement and later delete events remove remote content that is not
present locally. Do not point `remote_parent_path/local-directory-name` at
valuable pod data.

While watching, sync2pod batches filesystem events and applies sparse file,
subtree, and deletion actions. Independent work can run concurrently up to
`max_workers`; conflicting ancestor and descendant operations remain ordered.
The selected pod is fixed for the session, so restart the command after the pod
is replaced.

Each project also has a local single-run manager. A normal sync holds the
persistent nonblocking lock `~/.sync2pod/<project>/manager.lock` and
atomically publishes its PID, selected pod, target settings, and random owner
token in `state.json`. A second run for the same project is refused. This is a
local safeguard only; it does not coordinate across machines.

Unconfigured symlinks are archived as symlinks and are never followed outside
the configured roots. Sparse updates reject symlink paths that could escape a
source directory. Device files and other unsupported special files are not a
supported synchronization mechanism.

## Command-line interface

Both console names accept the same legacy-compatible flags:

| Flag | Behavior |
| --- | --- |
| `--init-config` | Create a project configuration. Requires `--local-path`. |
| `--list-projects` | List configured projects. |
| `--project NAME` | Select the project configuration. |
| `--local-path PATH` | Source directory used during initialization. |
| `--force` | Accepted for compatibility; startup already performs a full mirror. |
| `--skip-verify` | Skip the interactive confirmation for this invocation. |
| `--dry-run` | Build local archives and report mappings, counts, bytes, shards, and target without pod operations or symlink mutation. |
| `--status` | Print the local manager state for the selected project without loading its sync configuration. |
| `--stop` | Create a cooperative stop request for the selected project's active local manager. |

For unattended runs, set `skip_verify` in the project configuration or pass
`--skip-verify`.

`--status` and `--stop` are useful even if the configuration was removed while
a previous sync is still running:

```bash
sync2pod --project demo --status
sync2pod --project demo --stop
```

`--stop` is cooperative: it requests that the active sync instance stop itself
and run its normal cleanup. It never signals a PID from a state file.

Pod selection fails before local managed symlinks or remote work are changed
when the selector matches zero or more than one Running Pod. Narrow the label
selector until it identifies exactly one target.

## Pod requirements

The local machine must provide the configured Kubernetes command and permission
to list pods, execute commands, and copy files in the selected namespace. The
target container must provide:

```text
sh bash find tar md5sum rm mkdir mv cat
```

The process also needs write permission for the remote parent, staging paths,
and final project path.

## mtools and embedded backends

mtools can keep project JSON in its own database instead of
`~/.sync2pod`. It launches the compatibility command with:

```text
SYNC2POD_CONFIG_BACKEND=utools-db
SYNC2POD_CONFIGS_JSON={"project-name": { ... project configuration ... }}
```

When this backend saves configuration, stdout contains one machine-readable
line prefixed with `__SYNC2POD_DB_UPDATE__ `. Watch progress is emitted on
stderr with `__SYNC2POD_PROGRESS__ ` and the JSON keys `pending`, `uploading`,
`session_done`, and `session_failed`. Embedders should consume these control
lines and keep them out of user-facing logs.

The public Python entry point is also available:

```python
from sync2pod import run

run(project_configuration)
```

## Safety boundaries and non-goals

- sync2pod synchronizes into one already-running pod; it does not deploy,
  restart, or supervise workloads.
- It does not provide bidirectional synchronization, conflict resolution, or a
  durable cross-machine journal.
- It does not automatically reselect pods during a watch session.
- It does not interpret shell strings in `kubectl_command`.
- It does not promise preservation of remote-only files inside the mirror
  target.

## Troubleshooting

- **No running pod found:** verify `namespace`, `pod_label`, and the configured
  command by running its equivalent `get pods` request manually.
- **Missing required commands:** add the named tools to the target container;
  minimal images often omit `bash`, `tar`, or `md5sum`.
- **Permission denied:** verify local read access and remote write access,
  including the configured parent directory.
- **Configuration confirmation blocks automation:** pass `--skip-verify` or set
  `skip_verify` to `true`.
- **Follow symlink conflict:** remove or rename the conflicting local path, or
  set `create_symlink` to `false` for that follow mapping.
- **Pod restarted:** stop and relaunch sync2pod so it selects the replacement
  running pod and performs a fresh authoritative mirror.

## Development

```bash
uv sync --all-groups
uv run pytest -m "not performance" --cov=sync2pod --cov-report=term-missing
uv run pytest -m performance
uv build
```

## License

Licensed under the Apache License 2.0.
