devin-devkit 0.1.0__tar.gz

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.
@@ -0,0 +1,129 @@
1
+ Metadata-Version: 2.4
2
+ Name: devin-devkit
3
+ Version: 0.1.0
4
+ Summary: Install public Devin CLI profiles through uv
5
+ Author: Icaro0310
6
+ License-Expression: MIT
7
+ Requires-Python: >=3.10
8
+ Description-Content-Type: text/markdown
9
+ Requires-Dist: tomli>=2.0; python_version < "3.11"
10
+ Provides-Extra: dev
11
+ Requires-Dist: pytest>=8; extra == "dev"
12
+
13
+ <div align="center">
14
+
15
+ <a href="https://github.com/Icaro0310/devin-devkit/actions/workflows/test-devkit.yml"><img src="https://github.com/Icaro0310/devin-devkit/actions/workflows/test-devkit.yml/badge.svg" alt="ci"/></a>
16
+
17
+ <a href="https://scorecard.dev/viewer/?uri=github.com/Icaro0310/devin-devkit"><img src="https://api.scorecard.dev/projects/github.com/Icaro0310/devin-devkit/badge" alt="OpenSSF Scorecard"/></a>
18
+ <a href="LICENSE"><img src="https://img.shields.io/badge/license-MIT-green" alt="License: MIT"/></a>
19
+ <a href="https://www.python.org/"><img src="https://img.shields.io/badge/python-3.10%2B-blue" alt="Python 3.10+"/></a>
20
+ <a href="https://github.com/Icaro0310/devin-devkit"><img src="https://img.shields.io/github/stars/Icaro0310/devin-devkit" alt="GitHub stars"/></a>
21
+ <a href="https://github.com/Icaro0310/devin-devkit/commits/main"><img src="https://img.shields.io/github/last-commit/Icaro0310/devin-devkit" alt="Last commit"/></a>
22
+ <a href="https://github.com/Icaro0310/awesome-devin"><img src="https://img.shields.io/badge/part%20of-devin--*-ecosystem-7c3aed" alt="devin-* ecosystem"/></a>
23
+ <a href="https://github.com/Icaro0310/devin-devkit/issues"><img src="https://img.shields.io/badge/PRs-welcome-brightgreen" alt="PRs welcome"/></a>
24
+ </div>
25
+
26
+ # devin-devkit
27
+
28
+ > Unofficial community project. Not affiliated with, endorsed by, or sponsored by Cognition AI. Devin is a Cognition AI trademark.
29
+
30
+ **[Linux](README.linux.md)** · **[Personal Windows](README.windows.md)** · **[Corporate Windows](README.corporate-windows.md)** · **[Compatibility](COMPATIBILITY.md)** · **[macOS status](README.macos.md)**
31
+
32
+ Part of the [awesome-devin](https://github.com/Icaro0310/awesome-devin) ecosystem: the curated hub for the devin-* tools.
33
+
34
+ `devin-devkit` turns the public `devin-powerups` registry into selectable installation profiles. It is an **installer/distribution tool**, not a meta-package: each Python app gets its own isolated `uv` environment, and the Node bridge uses npm separately.
35
+
36
+ ![devin-devkit demo: list, outdated, doctor check](assets/demo.gif)
37
+
38
+ ## Source → tools → profiles
39
+
40
+ - `devin-powerups/registry.json` is the catalog and profile source of truth for the first-party `devin-*` tools, this separate DevKit distribution, the maintainer hub, and related artifacts.
41
+ - `tools/export_devkit_manifest.py` builds this repo's bundled manifest from the registry, including artifact, interface, audience, platform and environment metadata. The exporter rejects private or unregistered tools and verifies immutable Git commit references.
42
+ - `src/devin_devkit/cli.py` previews and installs the selected profile under an explicit execution environment. It does not edit Devin settings, inspect session databases, or install Slack/Obsidian/VM services.
43
+
44
+ ## Execution environments
45
+
46
+ One `devin-devkit` distribution supports three environments. Linux and Personal Windows use the extended runtime: local execution plus optional delegated workloads through Devin VM or QwenPaw when the selected tool supports them. Corporate Windows is intentionally self-contained: local execution only, with no VM, QwenPaw, Slack dependency, external compute, or workload delegation.
47
+
48
+ The installer defaults to `linux` on Linux and `personal-windows` on Windows. Corporate Windows is never inferred from the operating system; select it explicitly so restricted machines fail closed:
49
+
50
+ ```bash
51
+ devin-devkit install qa --environment linux
52
+ devin-devkit install qa --environment personal-windows
53
+ devin-devkit install qa --environment corporate-windows
54
+ ```
55
+
56
+ Environment entry points: [Linux](environments/linux/README.md) · [Personal Windows](environments/personal-windows/README.md) · [Corporate Windows](environments/corporate-windows/README.md).
57
+
58
+ The generated [compatibility matrix](COMPATIBILITY.md) is rendered from `registry.json`; unsupported entries carry an explicit registry reason rather than guessed compatibility.
59
+
60
+ ## Profiles
61
+
62
+ | Profile | Contents |
63
+ |---|---|
64
+ | `qa` | internals spec, QA pack, evals, metrics, doctor |
65
+ | `evaluation` | internals spec, evals, metrics, synthetic task fixtures |
66
+ | `security` | redact, bridge, janitor |
67
+ | `memory` / `agent` | memory, graph, search, poordjaevin |
68
+ | `operations` | backup, history, doctor, metrics, janitor, PM, orchestrator, switch and skill catalog |
69
+ | `full` / `all` | every automatically installable profile tool; `devin-office` remains a manual source-only service |
70
+
71
+ QwenPaw is an optional add-on and is not installed by these profiles. macOS is planned, not tested.
72
+
73
+ ## Use
74
+
75
+ Install the current GitHub source archive or use a local checkout; the package has no runtime dependencies:
76
+
77
+ ```bash
78
+ uv tool install "git+https://github.com/Icaro0310/devin-devkit.git#subdirectory=packages/devkit"
79
+ ```
80
+
81
+ From a local checkout, use `uv tool install .`. Then inspect the available profiles and preview before applying:
82
+
83
+ ```bash
84
+ devin-devkit profiles
85
+ devin-devkit list
86
+ devin-devkit install qa
87
+ devin-devkit install qa --apply
88
+ devin-devkit install full --apply
89
+ ```
90
+
91
+ Install specs are pinned (PyPI versions or GitHub archive SHAs), so an install never drifts on its own. To move forward:
92
+
93
+ ```bash
94
+ devin-devkit outdated # which installed tools have newer pins
95
+ devin-devkit update # preview the update plan
96
+ devin-devkit update --apply # reinstall outdated tools
97
+ devin-devkit update --force --apply # reinstall everything
98
+ ```
99
+
100
+ `outdated`/`update` read the manifest published in this repository, which a weekly workflow re-exports from the maintainer registry, so the pins are never more than about a week behind upstream.
101
+
102
+ To run from a checkout without installing the DevKit itself:
103
+
104
+ ```bash
105
+ uv run --project . python -m devin_devkit.cli install qa
106
+ ```
107
+
108
+ After a PyPI package release, the installer can also be run with `uvx devin-devkit ...`.
109
+
110
+ The manifest pins PyPI versions where available and GitHub source archives to immutable commit SHAs otherwise. Before it starts, the installer checks platform support, registry-declared environment compatibility, required managers, Node.js for `devin-bridge`, and existing command collisions. It never overwrites an existing command; commands already on `PATH` are reported as pre-existing but are not version-verified, and a partial collision blocks the profile before any installs run.
111
+
112
+ ## Runtime requirements
113
+
114
+ - Python tools: Python 3.10 or newer, installed in isolated environments managed by `uv`.
115
+ - `devin-bridge`: Node.js 20 or newer and npm. Its pinned GitHub archive is downloaded over HTTPS.
116
+ - Current installable profiles do not need Git. Git is needed only for manual source checkouts such as `devin-office`; package archives use HTTPS.
117
+ - Supported environments: Linux, Personal Windows and Corporate Windows. Linux and Personal Windows may use optional delegated runtime; Corporate Windows rejects registry entries that require external runtime, delegation or external integrations. macOS is planned but not claimed as tested.
118
+
119
+ ## Development and synchronization
120
+
121
+ ```bash
122
+ uv run --extra dev pytest
123
+ python ../devin-powerups/tools/validate_registry.py
124
+ python ../devin-powerups/tools/export_devkit_manifest.py
125
+ python ../devin-powerups/tools/export_devkit_manifest.py --check
126
+ python ../devin-powerups/tools/render_catalog.py --check ../Icaro0310/README.md
127
+ ```
128
+
129
+ The DevKit package has no runtime dependencies. New catalog entries and profiles belong in the registry; regenerate the manifest instead of hand-editing its generated copy. For local sibling development, install dependencies editably in a virtual environment (examples in the OS guides); keep relative file paths out of publishable package metadata.
@@ -0,0 +1,117 @@
1
+ <div align="center">
2
+
3
+ <a href="https://github.com/Icaro0310/devin-devkit/actions/workflows/test-devkit.yml"><img src="https://github.com/Icaro0310/devin-devkit/actions/workflows/test-devkit.yml/badge.svg" alt="ci"/></a>
4
+
5
+ <a href="https://scorecard.dev/viewer/?uri=github.com/Icaro0310/devin-devkit"><img src="https://api.scorecard.dev/projects/github.com/Icaro0310/devin-devkit/badge" alt="OpenSSF Scorecard"/></a>
6
+ <a href="LICENSE"><img src="https://img.shields.io/badge/license-MIT-green" alt="License: MIT"/></a>
7
+ <a href="https://www.python.org/"><img src="https://img.shields.io/badge/python-3.10%2B-blue" alt="Python 3.10+"/></a>
8
+ <a href="https://github.com/Icaro0310/devin-devkit"><img src="https://img.shields.io/github/stars/Icaro0310/devin-devkit" alt="GitHub stars"/></a>
9
+ <a href="https://github.com/Icaro0310/devin-devkit/commits/main"><img src="https://img.shields.io/github/last-commit/Icaro0310/devin-devkit" alt="Last commit"/></a>
10
+ <a href="https://github.com/Icaro0310/awesome-devin"><img src="https://img.shields.io/badge/part%20of-devin--*-ecosystem-7c3aed" alt="devin-* ecosystem"/></a>
11
+ <a href="https://github.com/Icaro0310/devin-devkit/issues"><img src="https://img.shields.io/badge/PRs-welcome-brightgreen" alt="PRs welcome"/></a>
12
+ </div>
13
+
14
+ # devin-devkit
15
+
16
+ > Unofficial community project. Not affiliated with, endorsed by, or sponsored by Cognition AI. Devin is a Cognition AI trademark.
17
+
18
+ **[Linux](README.linux.md)** · **[Personal Windows](README.windows.md)** · **[Corporate Windows](README.corporate-windows.md)** · **[Compatibility](COMPATIBILITY.md)** · **[macOS status](README.macos.md)**
19
+
20
+ Part of the [awesome-devin](https://github.com/Icaro0310/awesome-devin) ecosystem: the curated hub for the devin-* tools.
21
+
22
+ `devin-devkit` turns the public `devin-powerups` registry into selectable installation profiles. It is an **installer/distribution tool**, not a meta-package: each Python app gets its own isolated `uv` environment, and the Node bridge uses npm separately.
23
+
24
+ ![devin-devkit demo: list, outdated, doctor check](assets/demo.gif)
25
+
26
+ ## Source → tools → profiles
27
+
28
+ - `devin-powerups/registry.json` is the catalog and profile source of truth for the first-party `devin-*` tools, this separate DevKit distribution, the maintainer hub, and related artifacts.
29
+ - `tools/export_devkit_manifest.py` builds this repo's bundled manifest from the registry, including artifact, interface, audience, platform and environment metadata. The exporter rejects private or unregistered tools and verifies immutable Git commit references.
30
+ - `src/devin_devkit/cli.py` previews and installs the selected profile under an explicit execution environment. It does not edit Devin settings, inspect session databases, or install Slack/Obsidian/VM services.
31
+
32
+ ## Execution environments
33
+
34
+ One `devin-devkit` distribution supports three environments. Linux and Personal Windows use the extended runtime: local execution plus optional delegated workloads through Devin VM or QwenPaw when the selected tool supports them. Corporate Windows is intentionally self-contained: local execution only, with no VM, QwenPaw, Slack dependency, external compute, or workload delegation.
35
+
36
+ The installer defaults to `linux` on Linux and `personal-windows` on Windows. Corporate Windows is never inferred from the operating system; select it explicitly so restricted machines fail closed:
37
+
38
+ ```bash
39
+ devin-devkit install qa --environment linux
40
+ devin-devkit install qa --environment personal-windows
41
+ devin-devkit install qa --environment corporate-windows
42
+ ```
43
+
44
+ Environment entry points: [Linux](environments/linux/README.md) · [Personal Windows](environments/personal-windows/README.md) · [Corporate Windows](environments/corporate-windows/README.md).
45
+
46
+ The generated [compatibility matrix](COMPATIBILITY.md) is rendered from `registry.json`; unsupported entries carry an explicit registry reason rather than guessed compatibility.
47
+
48
+ ## Profiles
49
+
50
+ | Profile | Contents |
51
+ |---|---|
52
+ | `qa` | internals spec, QA pack, evals, metrics, doctor |
53
+ | `evaluation` | internals spec, evals, metrics, synthetic task fixtures |
54
+ | `security` | redact, bridge, janitor |
55
+ | `memory` / `agent` | memory, graph, search, poordjaevin |
56
+ | `operations` | backup, history, doctor, metrics, janitor, PM, orchestrator, switch and skill catalog |
57
+ | `full` / `all` | every automatically installable profile tool; `devin-office` remains a manual source-only service |
58
+
59
+ QwenPaw is an optional add-on and is not installed by these profiles. macOS is planned, not tested.
60
+
61
+ ## Use
62
+
63
+ Install the current GitHub source archive or use a local checkout; the package has no runtime dependencies:
64
+
65
+ ```bash
66
+ uv tool install "git+https://github.com/Icaro0310/devin-devkit.git#subdirectory=packages/devkit"
67
+ ```
68
+
69
+ From a local checkout, use `uv tool install .`. Then inspect the available profiles and preview before applying:
70
+
71
+ ```bash
72
+ devin-devkit profiles
73
+ devin-devkit list
74
+ devin-devkit install qa
75
+ devin-devkit install qa --apply
76
+ devin-devkit install full --apply
77
+ ```
78
+
79
+ Install specs are pinned (PyPI versions or GitHub archive SHAs), so an install never drifts on its own. To move forward:
80
+
81
+ ```bash
82
+ devin-devkit outdated # which installed tools have newer pins
83
+ devin-devkit update # preview the update plan
84
+ devin-devkit update --apply # reinstall outdated tools
85
+ devin-devkit update --force --apply # reinstall everything
86
+ ```
87
+
88
+ `outdated`/`update` read the manifest published in this repository, which a weekly workflow re-exports from the maintainer registry, so the pins are never more than about a week behind upstream.
89
+
90
+ To run from a checkout without installing the DevKit itself:
91
+
92
+ ```bash
93
+ uv run --project . python -m devin_devkit.cli install qa
94
+ ```
95
+
96
+ After a PyPI package release, the installer can also be run with `uvx devin-devkit ...`.
97
+
98
+ The manifest pins PyPI versions where available and GitHub source archives to immutable commit SHAs otherwise. Before it starts, the installer checks platform support, registry-declared environment compatibility, required managers, Node.js for `devin-bridge`, and existing command collisions. It never overwrites an existing command; commands already on `PATH` are reported as pre-existing but are not version-verified, and a partial collision blocks the profile before any installs run.
99
+
100
+ ## Runtime requirements
101
+
102
+ - Python tools: Python 3.10 or newer, installed in isolated environments managed by `uv`.
103
+ - `devin-bridge`: Node.js 20 or newer and npm. Its pinned GitHub archive is downloaded over HTTPS.
104
+ - Current installable profiles do not need Git. Git is needed only for manual source checkouts such as `devin-office`; package archives use HTTPS.
105
+ - Supported environments: Linux, Personal Windows and Corporate Windows. Linux and Personal Windows may use optional delegated runtime; Corporate Windows rejects registry entries that require external runtime, delegation or external integrations. macOS is planned but not claimed as tested.
106
+
107
+ ## Development and synchronization
108
+
109
+ ```bash
110
+ uv run --extra dev pytest
111
+ python ../devin-powerups/tools/validate_registry.py
112
+ python ../devin-powerups/tools/export_devkit_manifest.py
113
+ python ../devin-powerups/tools/export_devkit_manifest.py --check
114
+ python ../devin-powerups/tools/render_catalog.py --check ../Icaro0310/README.md
115
+ ```
116
+
117
+ The DevKit package has no runtime dependencies. New catalog entries and profiles belong in the registry; regenerate the manifest instead of hand-editing its generated copy. For local sibling development, install dependencies editably in a virtual environment (examples in the OS guides); keep relative file paths out of publishable package metadata.
@@ -0,0 +1,31 @@
1
+ [build-system]
2
+ requires = ["setuptools>=77"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "devin-devkit"
7
+ version = "0.1.0"
8
+ description = "Install public Devin CLI profiles through uv"
9
+ readme = "README.md"
10
+ license = "MIT"
11
+ requires-python = ">=3.10"
12
+ authors = [{ name = "Icaro0310" }]
13
+ dependencies = [
14
+ "tomli>=2.0; python_version < '3.11'",
15
+ ]
16
+
17
+ [project.optional-dependencies]
18
+ dev = ["pytest>=8"]
19
+
20
+ [project.scripts]
21
+ devin-devkit = "devin_devkit.cli:main"
22
+
23
+ [tool.setuptools.packages.find]
24
+ where = ["src"]
25
+
26
+ [tool.setuptools.package-data]
27
+ devin_devkit = ["manifest.json"]
28
+
29
+ [tool.pytest.ini_options]
30
+ testpaths = ["tests"]
31
+ pythonpath = ["src"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1 @@
1
+ __version__ = "0.1.0"
@@ -0,0 +1,128 @@
1
+ from __future__ import annotations
2
+
3
+ import argparse
4
+ import json
5
+ import sys
6
+ from pathlib import Path
7
+ from typing import Any
8
+
9
+ from devin_devkit.installer import DevKitError, apply_plan, build_plan, load_manifest
10
+ from devin_devkit.updater import (
11
+ apply_update_plan,
12
+ build_update_plan,
13
+ fetch_remote_manifest,
14
+ freshness_hint,
15
+ )
16
+
17
+
18
+ def _print_plan(plan: dict[str, Any], as_json: bool) -> None:
19
+ if as_json:
20
+ print(json.dumps(plan, ensure_ascii=False, indent=2))
21
+ return
22
+ print(f"Profile: {plan['profile']} ({plan['platform']}; environment={plan['environment']}; runtime={plan['runtime']})")
23
+ for item in plan["actions"]:
24
+ command = " ".join(item.get("command", []))
25
+ detail = item.get("detail", "")
26
+ suffix = f" — {detail}" if detail else ""
27
+ print(f"- {item['tool']}: {item['action']}{suffix}{': ' + command if command else ''}")
28
+ for error in plan["errors"]:
29
+ print(f"BLOCKED: {error}", file=sys.stderr)
30
+ if not plan["errors"] and any(item["action"] == "install" for item in plan["actions"]):
31
+ print("Dry run only. Re-run with --apply to install.")
32
+
33
+
34
+ def _build_parser() -> argparse.ArgumentParser:
35
+ parser = argparse.ArgumentParser(prog="devin-devkit", description="Install curated Devin tool profiles with isolated uv/npm environments.")
36
+ parser.add_argument("--manifest", type=Path, help=argparse.SUPPRESS)
37
+ sub = parser.add_subparsers(dest="action", required=True)
38
+ sub.add_parser("profiles", help="list available installation profiles")
39
+ sub.add_parser("list", help="list the tools in the manifest")
40
+ install = sub.add_parser("install", help="preview a profile; pass --apply to install")
41
+ install.add_argument("profile")
42
+ install.add_argument("--apply", action="store_true", help="install the selected profile")
43
+ install.add_argument("--json", action="store_true", help="print the plan as JSON")
44
+ install.add_argument(
45
+ "--environment",
46
+ choices=["linux", "personal-windows", "corporate-windows"],
47
+ help="execution environment; defaults to linux on Linux and personal-windows on Windows",
48
+ )
49
+ outdated = sub.add_parser("outdated", help="list installed tools with a newer version in the remote registry")
50
+ outdated.add_argument("--json", action="store_true", help="print the check as JSON")
51
+ update = sub.add_parser("update", help="preview tool updates; pass --apply to install them")
52
+ update.add_argument("--apply", action="store_true", help="reinstall outdated tools")
53
+ update.add_argument("--force", action="store_true", help="reinstall every installed tool")
54
+ update.add_argument("--json", action="store_true", help="print the plan as JSON")
55
+ return parser
56
+
57
+
58
+ def main(argv: list[str] | None = None) -> int:
59
+ parser = _build_parser()
60
+ args = parser.parse_args(argv)
61
+ try:
62
+ manifest = load_manifest(args.manifest)
63
+ if args.action in {"outdated", "update"}:
64
+ remote = fetch_remote_manifest()
65
+ plan = build_update_plan(remote, force=args.action == "update" and args.force)
66
+ if args.json:
67
+ print(json.dumps(plan, ensure_ascii=False, indent=2))
68
+ else:
69
+ print(f"Remote registry v{plan['registry_version']} (generated {plan['generated']})")
70
+ for item in plan["actions"]:
71
+ if item["action"] == "update":
72
+ print(f"- {item['tool']}: {item['from']} -> {item['to']}")
73
+ elif item["action"] == "current":
74
+ print(f"- {item['tool']}: up to date ({item['version']})")
75
+ elif item["action"] == "blocked":
76
+ print(f"- {item['tool']}: BLOCKED — {item['detail']}")
77
+ if args.action == "outdated":
78
+ return 0
79
+ pending = [a for a in plan["actions"] if a["action"] == "update"]
80
+ if not args.apply:
81
+ if pending:
82
+ print("Dry run only. Re-run with --apply to update.")
83
+ return 0
84
+ result = apply_update_plan(plan)
85
+ print(f"Updated: {', '.join(result['updated']) or 'nothing'}")
86
+ for name in result["failed"]:
87
+ print(f"FAILED: {name}", file=sys.stderr)
88
+ return 1 if result["failed"] else 0
89
+ if args.action == "profiles":
90
+ for name, profile in manifest["profiles"].items():
91
+ alias = f" (alias of {profile['alias_of']})" if profile.get("alias_of") else ""
92
+ print(f"{name}: {profile['label']}{alias} — {profile['description']}")
93
+ return 0
94
+ if args.action == "list":
95
+ cat = manifest["catalog"]
96
+ pl = lambda n: "" if n == 1 else "s" # noqa: E731
97
+ print(f"{cat['tool_count']} first-party Devin tools; {cat['distribution_count']} distribution layer{pl(cat['distribution_count'])}; {cat['hub_count']} maintainer hub{pl(cat['hub_count'])}; {cat['related_count']} related project{pl(cat['related_count'])}")
98
+ for tool in manifest["tools"]:
99
+ print(f"{tool['id']}: {tool['package']} {tool['version']} [{tool['manager']}/{tool['source']}/{tool['status']}]")
100
+ hint = freshness_hint(manifest)
101
+ if hint:
102
+ print(hint, file=sys.stderr)
103
+ return 0
104
+
105
+ plan = build_plan(manifest, args.profile, environment=args.environment)
106
+ if not args.apply:
107
+ _print_plan(plan, args.json)
108
+ hint = freshness_hint(manifest)
109
+ if hint:
110
+ print(hint, file=sys.stderr)
111
+ return 2 if plan["errors"] else 0
112
+ if plan["errors"]:
113
+ _print_plan(plan, args.json)
114
+ return 2
115
+ result = apply_plan(plan)
116
+ print(json.dumps(result, ensure_ascii=False, indent=2) if args.json else f"Installed: {', '.join(result['installed']) or 'nothing new'}")
117
+ for item in result["actions"]:
118
+ if item["action"] in {"preexisting", "manual", "unsupported"}:
119
+ detail = item.get("detail", ", ".join(item.get("commands", [])))
120
+ print(f"- {item['tool']}: {item['action']} — {detail}")
121
+ return 0
122
+ except DevKitError as exc:
123
+ print(f"error: {exc}", file=sys.stderr)
124
+ return 2
125
+
126
+
127
+ if __name__ == "__main__":
128
+ raise SystemExit(main())
@@ -0,0 +1,213 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ import platform as platform_module
5
+ import re
6
+ import shutil
7
+ import subprocess
8
+ from importlib.resources import files
9
+ from pathlib import Path
10
+ from typing import Any, Callable
11
+
12
+
13
+ class DevKitError(RuntimeError):
14
+ pass
15
+
16
+
17
+ def load_manifest(path: Path | None = None) -> dict[str, Any]:
18
+ source = path if path is not None else files("devin_devkit").joinpath("manifest.json")
19
+ try:
20
+ text = Path(source).read_text(encoding="utf-8") if path else source.read_text(encoding="utf-8")
21
+ return json.loads(text)
22
+ except (OSError, json.JSONDecodeError) as exc:
23
+ raise DevKitError(f"cannot load manifest {source}: {exc}") from exc
24
+
25
+
26
+ def platform_name(system: str | None = None) -> str:
27
+ value = (system or platform_module.system()).lower()
28
+ if value in {"nt", "windows"}:
29
+ return "windows"
30
+ if value in {"posix", "linux"}:
31
+ return "linux"
32
+ if value in {"darwin", "macos"}:
33
+ return "macos"
34
+ return value
35
+
36
+
37
+ _ENVIRONMENT_ALIASES = {
38
+ "linux": "linux",
39
+ "personal-windows": "personal_windows",
40
+ "personal_windows": "personal_windows",
41
+ "windows": "personal_windows",
42
+ "corporate-windows": "corporate_windows",
43
+ "corporate_windows": "corporate_windows",
44
+ "corporate": "corporate_windows",
45
+ }
46
+
47
+
48
+ def normalize_environment(environment: str | None, host: str) -> str:
49
+ if environment is None:
50
+ return "personal_windows" if host == "windows" else host
51
+ normalized = _ENVIRONMENT_ALIASES.get(environment.lower())
52
+ if normalized is None:
53
+ raise ValueError(f"unsupported environment: {environment}")
54
+ expected_host = "windows" if normalized.endswith("_windows") else normalized
55
+ if host != expected_host:
56
+ raise ValueError(f"environment {normalized} cannot run on host platform {host}")
57
+ return normalized
58
+
59
+
60
+ def resolve_profile(manifest: dict[str, Any], name: str) -> tuple[str, dict[str, Any]]:
61
+ profiles = manifest.get("profiles", {})
62
+ if name not in profiles:
63
+ raise DevKitError(f"unknown profile {name!r}; choose from: {', '.join(sorted(profiles))}")
64
+ current = name
65
+ seen: set[str] = set()
66
+ while profiles[current].get("alias_of"):
67
+ if current in seen:
68
+ raise DevKitError(f"profile alias cycle at {current!r}")
69
+ seen.add(current)
70
+ current = profiles[current]["alias_of"]
71
+ if current not in profiles:
72
+ raise DevKitError(f"profile {name!r} points to missing alias target {current!r}")
73
+ return current, profiles[current]
74
+
75
+
76
+ def _node_major(runner: Callable[..., Any], which: Callable[[str], str | None]) -> int | None:
77
+ if which("node") is None:
78
+ return None
79
+ try:
80
+ result = runner(["node", "--version"], capture_output=True, text=True, check=False)
81
+ except OSError:
82
+ return None
83
+ match = re.search(r"v?(\d+)", result.stdout or "")
84
+ return int(match.group(1)) if match else None
85
+
86
+
87
+ def build_plan(
88
+ manifest: dict[str, Any],
89
+ profile_name: str,
90
+ *,
91
+ system: str | None = None,
92
+ environment: str | None = None,
93
+ which: Callable[[str], str | None] = shutil.which,
94
+ runner: Callable[..., Any] = subprocess.run,
95
+ ) -> dict[str, Any]:
96
+ resolved_name, profile = resolve_profile(manifest, profile_name)
97
+ host = platform_name(system)
98
+ if host not in manifest.get("supported_platforms", []):
99
+ planned = ", ".join(manifest.get("planned_platforms", [])) or "none"
100
+ raise DevKitError(f"platform {host!r} is not supported; planned platforms: {planned}")
101
+ try:
102
+ selected_environment = normalize_environment(environment, host)
103
+ except ValueError as exc:
104
+ raise DevKitError(str(exc)) from exc
105
+ if selected_environment not in manifest.get("environments", {}):
106
+ raise DevKitError(f"manifest does not define environment {selected_environment!r}")
107
+
108
+ tool_map = {tool["id"]: tool for tool in manifest.get("tools", [])}
109
+ actions: list[dict[str, Any]] = []
110
+ errors: list[str] = []
111
+ node_major = None
112
+
113
+ for tool_id in profile.get("tools", []):
114
+ tool = tool_map.get(tool_id)
115
+ if tool is None:
116
+ errors.append(f"profile {resolved_name!r} references unknown tool {tool_id!r}")
117
+ continue
118
+ environment_meta = tool.get("environments", {}).get(selected_environment)
119
+ if environment_meta is None:
120
+ errors.append(f"{tool_id} has no {selected_environment} environment metadata")
121
+ actions.append({"tool": tool_id, "action": "unsupported", "detail": f"no metadata for {selected_environment}"})
122
+ continue
123
+ if not environment_meta.get("supported"):
124
+ reason = environment_meta.get("reason", f"not listed for {selected_environment}")
125
+ errors.append(f"{tool_id} is unsupported in {selected_environment}: {reason}")
126
+ actions.append({"tool": tool_id, "action": "unsupported", "detail": reason})
127
+ continue
128
+ if selected_environment == "corporate_windows" and (
129
+ environment_meta.get("external_dependencies") or environment_meta.get("delegation") in {"optional", "supported", "required"}
130
+ ):
131
+ reason = "external runtime or delegation is disabled in corporate Windows"
132
+ errors.append(f"{tool_id} violates the corporate Windows local-only guarantee")
133
+ actions.append({"tool": tool_id, "action": "blocked", "detail": reason})
134
+ continue
135
+ if host not in tool.get("platforms", []):
136
+ actions.append({"tool": tool_id, "action": "unsupported", "detail": f"not listed for {host}"})
137
+ continue
138
+ if tool.get("status") == "manual" or tool.get("manager") == "manual":
139
+ actions.append({"tool": tool_id, "action": "manual", "detail": tool.get("manual_note", "manual setup required")})
140
+ continue
141
+
142
+ manager = tool["manager"]
143
+ commands = tool.get("commands", [])
144
+ present = [command for command in commands if which(command) is not None]
145
+ if present:
146
+ if len(present) == len(commands):
147
+ actions.append({"tool": tool_id, "action": "preexisting", "detail": "commands are on PATH; versions are not verified", "commands": present})
148
+ else:
149
+ missing = [command for command in commands if command not in present]
150
+ errors.append(f"{tool_id} has a partial PATH collision; present={present}, missing={missing}")
151
+ actions.append({"tool": tool_id, "action": "blocked", "detail": "partial command collision"})
152
+ continue
153
+
154
+ manager_binary = "uv" if manager == "uv" else "npm"
155
+ if which(manager_binary) is None:
156
+ errors.append(f"{tool_id} requires {manager_binary} on PATH")
157
+ actions.append({"tool": tool_id, "action": "blocked", "detail": f"missing {manager_binary}"})
158
+ continue
159
+ if tool.get("requires_git") and which("git") is None:
160
+ errors.append(f"{tool_id} has a Git dependency and requires git on PATH")
161
+ actions.append({"tool": tool_id, "action": "blocked", "detail": "missing git"})
162
+ continue
163
+ if manager == "npm":
164
+ if node_major is None:
165
+ node_major = _node_major(runner, which)
166
+ if node_major is None or node_major < 20:
167
+ errors.append(f"{tool_id} requires Node.js >=20")
168
+ actions.append({"tool": tool_id, "action": "blocked", "detail": "Node.js >=20 is required"})
169
+ continue
170
+
171
+ spec = tool.get("install_spec")
172
+ if not spec:
173
+ errors.append(f"{tool_id} has no install specification")
174
+ actions.append({"tool": tool_id, "action": "blocked", "detail": "missing install specification"})
175
+ continue
176
+ command = ["uv", "tool", "install", spec] if manager == "uv" else ["npm", "install", "--global", spec]
177
+ actions.append({"tool": tool_id, "action": "install", "command": command})
178
+
179
+ manual = [
180
+ {"tool": name, "action": "manual", "detail": tool_map.get(name, {}).get("manual_note", "manual setup required")}
181
+ for name in profile.get("manual", [])
182
+ ]
183
+ actions.extend(manual)
184
+ return {
185
+ "profile": profile_name,
186
+ "resolved_profile": resolved_name,
187
+ "platform": host,
188
+ "environment": selected_environment,
189
+ "runtime": manifest["environments"][selected_environment].get("runtime"),
190
+ "dry_run": True,
191
+ "actions": actions,
192
+ "errors": errors,
193
+ }
194
+
195
+
196
+ def apply_plan(plan: dict[str, Any], runner: Callable[..., Any] = subprocess.run) -> dict[str, Any]:
197
+ if plan["errors"]:
198
+ raise DevKitError("preflight failed: " + "; ".join(plan["errors"]))
199
+ completed: list[str] = []
200
+ for action in plan["actions"]:
201
+ if action["action"] != "install":
202
+ continue
203
+ try:
204
+ result = runner(action["command"], check=False)
205
+ except OSError as exc:
206
+ raise DevKitError(f"failed to start {action['tool']}: {exc}") from exc
207
+ if result.returncode:
208
+ raise DevKitError(
209
+ f"install failed for {action['tool']} (exit {result.returncode}); "
210
+ f"completed before failure: {completed}"
211
+ )
212
+ completed.append(action["tool"])
213
+ return {"profile": plan["profile"], "installed": completed, "actions": plan["actions"]}