Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -137,3 +137,6 @@ scripts/audit-modules-prompt.md
# Go engine build artifacts
go-engine/endstate.exe


# OMC operational state (runtime artifacts, never committed)
.omc/
58 changes: 57 additions & 1 deletion docs/contracts/cli-json-contract.md
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,7 @@ When `success` is `false`, the `error` field contains:
| `ROLLBACK_FAILED` | The backend rollback failed (non-systemic). Raw backend text is confined to `error.detail`. Additive in schema 1.x. |
| `CONVERGENCE_UNSUPPORTED` | `apply --prune` was requested on a backend that cannot safely remove installed-but-undeclared packages (the winget driver, or any host with no realizer). Nothing is removed. Additive in schema 1.x. |
| `CONFIRMATION_REQUIRED` | `rebuild` was invoked for a live run (restore on, not `--dry-run`) without `--confirm`. Raised before any mutation, so the refusal has no side effects. Additive in schema 1.x. |
| `NOT_SUPPORTED` | The requested operation is not supported on the current platform (e.g. `schedule enable` on non-Windows), or the input mode is unsupported (e.g. `rebuild --from <URL>`). Additive in schema 1.x. |
| `NOT_SUPPORTED` | The requested operation is not supported on the current platform (e.g. `schedule enable` on non-Windows), or the input mode is unsupported (e.g. `rebuild --from <URL>`, or `import --from <source>` for an unrecognised source). Additive in schema 1.x. |
| `TASK_REGISTRATION_FAILED` | `schedule enable` could not register the Windows Scheduled Task via `schtasks.exe`. Additive in schema 1.x. |

#### Hosted Backup error codes
Expand Down Expand Up @@ -533,6 +533,62 @@ endstate rebuild --from ./machine.jsonc --no-restore --json

---

## Command: `import`

Imports an external package list into an Endstate manifest. The only supported source in v0 is `unigetui` — a [UniGetUI](https://github.com/marticliment/UniGetUI) `.ubundle` backup/bundle (JSON, `export_version: 3`). Winget-source packages become manifest apps; every other package (chocolatey, scoop, pip, ...) and every `incompatible_packages` entry is reported, never silently dropped. Import is a **pure transform**: no installs, no network access, deterministic (byte-identical) output. It imports the package list only — UniGetUI's own settings are not imported; config restore comes from Endstate's module catalog on a later `plan`/`apply`.

```powershell
endstate import --from unigetui --path "backup.ubundle" --json
endstate import --from unigetui --path "backup.ubundle" --pin --out ./machine.jsonc --json
```

### Flags

| Flag | Behavior |
|------|----------|
| `--from <source>` | Required. The source format. Only `unigetui` is supported; any other value returns `NOT_SUPPORTED` with remediation. An empty value returns `MANIFEST_VALIDATION_ERROR`. |
| `--path <file>` | Required. The input bundle file. An empty value returns `MANIFEST_VALIDATION_ERROR`; a missing path returns `MANIFEST_NOT_FOUND`; a file that is not valid JSON returns `MANIFEST_PARSE_ERROR`; a well-formed JSON file that is not a UniGetUI bundle (no `export_version` and no `packages`) returns `MANIFEST_VALIDATION_ERROR`. |
| `--out <path>` | Output manifest path. Defaults to `manifests/local/imported-unigetui.jsonc` (gitignored); outside a repo checkout the default lands next to the input bundle instead. The emitted JSONC must load through the manifest loader before it is written; a load failure returns `MANIFEST_VALIDATION_ERROR` and writes nothing. An existing file at the output path is replaced (the write is atomic: staged to a temp file, then renamed into place). A write failure returns `MANIFEST_WRITE_FAILED`. |
| `--pin` | Record a `version` on each imported app: the package's `InstallationOptions.Version` pin when present (authored intent), otherwise the observed `Version`. Off by default (no version written). |
| `--json` | Emit the standard envelope to stdout. |

### Response

```json
{
"schemaVersion": "1.0",
"cliVersion": "0.1.0",
"command": "import",
"runId": "20241220-143052",
"timestampUtc": "2024-12-20T14:30:52Z",
"success": true,
"data": {
"source": "unigetui",
"input": "C:\\Users\\me\\backup.ubundle",
"output": "C:\\repo\\manifests\\local\\imported-unigetui.jsonc",
"exportVersion": 3,
"pinned": false,
"counts": { "imported": 3, "skipped": 3, "incompatible": 1 },
"imported": [
{ "id": "visualstudiocode", "ref": "Microsoft.VisualStudioCode", "displayName": "Microsoft Visual Studio Code" }
],
"skipped": [
{ "id": "nodejs", "name": "Node.js", "manager": "Chocolatey", "reason": "unsupported package manager (Endstate installs via winget on Windows)" }
],
"incompatible": [
{ "id": "Contoso.LocalOnlyApp", "name": "Contoso Local Only App", "version": "1.0.0", "source": "Local PC" }
]
},
"error": null
}
```

- `imported`, `skipped`, and `incompatible` together account for every entry in the bundle — nothing is silently dropped. `counts` is their length summary.
- `imported[].version` is present only under `--pin`. `warnings` carries the parser's version-mismatch note (a non-3 `export_version`) and any app-id collision de-duplication; it is omitted when empty.
- Additive in schema 1.x; no schema bump.

---

## Command: `verify`

Verifies current machine state against manifest.
Expand Down
23 changes: 22 additions & 1 deletion go-engine/cmd/endstate/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ Commands:
capabilities Report CLI capabilities (GUI handshake)
apply Execute provisioning plan
rebuild Rebuild a machine from a bundle or manifest (install + restore + verify)
import Import an external package list into a manifest (--from unigetui)
verify Verify machine state against manifest
capture Capture current machine state
plan Generate execution plan
Expand Down Expand Up @@ -67,6 +68,8 @@ Per-command flags:
--export <path> Export directory path (restore, export-config, validate-export)
--restore-filter <e> Filter restore entries by module ID (restore, apply)
--from <path> Bundle (.zip) or manifest (.jsonc) to rebuild from (rebuild)
--from <source> External package-list source to import (import; e.g. unigetui)
--path <file> Source bundle file to import (import)
--no-restore Install without restoring configuration (rebuild)
--latest Most recent run (report)
--last <n> Last N runs (report)
Expand Down Expand Up @@ -117,9 +120,12 @@ type parsedArgs struct {
restoreFilter string // --restore-filter <expr>

// Rebuild flags
from string // rebuild --from <bundle.zip|manifest.jsonc>
from string // rebuild --from <bundle.zip|manifest.jsonc>; import --from <source>
noRestore bool // rebuild --no-restore: install without restoring configuration

// Import flags
path string // import --path <bundle.ubundle>: source file to import

// Capture flags
out string
name string
Expand Down Expand Up @@ -308,6 +314,11 @@ func parseArgs(args []string) parsedArgs {
p.from = args[i+1]
i++
}
case "--path":
if i+1 < len(args) {
p.path = args[i+1]
i++
}
case "--email":
if i+1 < len(args) {
p.email = args[i+1]
Expand Down Expand Up @@ -371,6 +382,8 @@ func commandUsage(cmd string) string {
return "Usage: endstate apply [--manifest <path>] [--dry-run] [--enable-restore] [--only <id[,id...]>] [--prune] [--repin] [--confirm] [--bootstrap-backends] [--no-bootstrap] [--json] [--events jsonl]\n\nExecute provisioning plan. With --only, limit the run to the comma-separated list of manifest app ids (filtering happens before planning so only the selected apps are installed, restored, and verified). With --prune, converge the engine-managed set to exactly the manifest by removing installed-but-undeclared packages (realizer backends only, e.g. Nix on Linux/macOS). With --repin, reinstall a declared app version when the installed version has drifted from it (winget only). --prune and --repin both require --confirm to execute; use --dry-run to preview what would change. --only and --prune cannot be combined. On macOS/Linux, when a needed package backend is absent, --bootstrap-backends authorizes the engine to install it via its official installer; --no-bootstrap forces skipping it. Without either flag the engine skips the lane and requests consent.\n"
case "rebuild":
return "Usage: endstate rebuild --from <bundle.zip|manifest.jsonc> [--dry-run] [--confirm] [--no-restore] [--json] [--events jsonl]\n\nRebuild a machine from a capture bundle (.zip) or a bare manifest (.jsonc): install the declared apps, restore configuration, then verify. Restore is ON by default, so a live run (not --dry-run, not --no-restore) requires --confirm. Use --dry-run to preview the plan without changing anything, or --no-restore to install and verify without touching configuration. Overwritten files are backed up first and can be undone with 'endstate revert'. Local file input only — URL input is not supported.\n"
case "import":
return "Usage: endstate import --from unigetui --path <file> [--out <path>] [--pin] [--json]\n\nImport an external package list into an Endstate manifest. The only supported source is 'unigetui' (a UniGetUI .ubundle backup/bundle, JSON). Winget-source packages become manifest apps; every non-winget package (chocolatey, scoop, pip, ...) and every incompatible entry is reported, never silently dropped. With --pin, the app version is recorded (a package's InstallationOptions.Version pin wins over the observed version). Output defaults to manifests/local/imported-unigetui.jsonc (gitignored). This is a pure transform: no installs, no network. Scope: the package list only — UniGetUI's own settings are not imported; Endstate's module catalog restores app config on plan/apply.\n"
case "verify":
return "Usage: endstate verify [--manifest <path>] [--json] [--events jsonl]\n\nVerify machine state against manifest.\n"
case "capture":
Expand Down Expand Up @@ -512,6 +525,14 @@ func dispatch(p parsedArgs) (interface{}, *envelope.Error) {
Events: p.events,
})

case "import":
return commands.RunImport(commands.ImportFlags{
From: p.from,
Path: p.path,
Out: p.out,
Pin: p.pin,
})

case "verify":
return commands.RunVerify(commands.VerifyFlags{
Manifest: p.manifest,
Expand Down
4 changes: 4 additions & 0 deletions go-engine/internal/commands/capabilities.go
Original file line number Diff line number Diff line change
Expand Up @@ -165,6 +165,10 @@ func RunCapabilities() (interface{}, *envelope.Error) {
Supported: true,
Flags: []string{"--from", "--dry-run", "--confirm", "--no-restore", "--json", "--events"},
},
"import": {
Supported: true,
Flags: []string{"--from", "--path", "--out", "--pin", "--json"},
},
},
Features: FeaturesInfo{
Streaming: false,
Expand Down
Loading
Loading