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.
- devin_devkit-0.1.0/PKG-INFO +129 -0
- devin_devkit-0.1.0/README.md +117 -0
- devin_devkit-0.1.0/pyproject.toml +31 -0
- devin_devkit-0.1.0/setup.cfg +4 -0
- devin_devkit-0.1.0/src/devin_devkit/__init__.py +1 -0
- devin_devkit-0.1.0/src/devin_devkit/cli.py +128 -0
- devin_devkit-0.1.0/src/devin_devkit/installer.py +213 -0
- devin_devkit-0.1.0/src/devin_devkit/manifest.json +1201 -0
- devin_devkit-0.1.0/src/devin_devkit/updater.py +262 -0
- devin_devkit-0.1.0/src/devin_devkit.egg-info/PKG-INFO +129 -0
- devin_devkit-0.1.0/src/devin_devkit.egg-info/SOURCES.txt +17 -0
- devin_devkit-0.1.0/src/devin_devkit.egg-info/dependency_links.txt +1 -0
- devin_devkit-0.1.0/src/devin_devkit.egg-info/entry_points.txt +2 -0
- devin_devkit-0.1.0/src/devin_devkit.egg-info/requires.txt +6 -0
- devin_devkit-0.1.0/src/devin_devkit.egg-info/top_level.txt +1 -0
- devin_devkit-0.1.0/tests/test_cli.py +79 -0
- devin_devkit-0.1.0/tests/test_installer.py +226 -0
- devin_devkit-0.1.0/tests/test_smoke.py +4 -0
- devin_devkit-0.1.0/tests/test_updater.py +233 -0
|
@@ -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
|
+

|
|
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
|
+

|
|
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 @@
|
|
|
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"]}
|