@mriekki/project-forge 4.15.0 → 4.15.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/assets/bootstrap-migrations/wsl-setup/setup-wsl.manifest.json +7 -4
- package/assets/bootstrap-migrations/wsl-setup/setup-wsl.py +43 -6
- package/assets/components.json +2 -2
- package/assets/generated/ASSEMBLY.json +5 -5
- package/assets/generated/core/core/skills/work-item-status/SKILL.md +20 -4
- package/assets/generated/core/tools/vscode_work_item_status/extension/README.md +8 -0
- package/assets/generated/core/tools/vscode_work_item_status/install.py +59 -7
- package/package.json +1 -1
|
@@ -6,18 +6,21 @@
|
|
|
6
6
|
"asset": "assets/bootstrap-migrations/wsl-setup/setup-wsl.py",
|
|
7
7
|
"source": {
|
|
8
8
|
"component": "bootstrap",
|
|
9
|
-
"revision": "
|
|
9
|
+
"revision": "6b6a2252434d310b21150fcbb5ce7270919ad9d0",
|
|
10
10
|
"artifact": "runtime/dev_environment_wsl.py"
|
|
11
11
|
},
|
|
12
|
-
"targetGitBlob": "
|
|
12
|
+
"targetGitBlob": "a557150b7333f83cfd342b4983037f5ddf8c6748",
|
|
13
13
|
"supportedPredecessorGitBlobs": [
|
|
14
14
|
"6731910246fb99887c58b9f3a8a14fec1bf02c01",
|
|
15
|
-
"883f7bbadd2de4bc0dcb601ee2285f88a0085b52"
|
|
15
|
+
"883f7bbadd2de4bc0dcb601ee2285f88a0085b52",
|
|
16
|
+
"0b959b4e85508016f6e8c12d403a94b2eed46c1f"
|
|
16
17
|
],
|
|
17
18
|
"supportedBootstrapRevisions": [
|
|
18
19
|
"0c45eb332a815ad79ea061443f337500b9dc5712",
|
|
19
20
|
"173203baaf36935d04eb81c89f1d016131a40442",
|
|
20
|
-
"9f0f7fd4e0e2220250f6fd5b035706a7d15834af"
|
|
21
|
+
"9f0f7fd4e0e2220250f6fd5b035706a7d15834af",
|
|
22
|
+
"043b3b2a0e3ba03b8d18666bb3fef59c588bfa16",
|
|
23
|
+
"1e656d533a259428bbfb21aada548429462ba190"
|
|
21
24
|
],
|
|
22
25
|
"allowMissing": true
|
|
23
26
|
}
|
|
@@ -311,19 +311,48 @@ def ensure_tool(entry: dict[str, Any], check_only: bool) -> tuple[bool, str, str
|
|
|
311
311
|
return False, detail or f"No WSL installer is defined for {tool_id}", "MISSING"
|
|
312
312
|
|
|
313
313
|
|
|
314
|
-
def ensure_extensions(extensions: list[str], check_only: bool) -> list[str]:
|
|
315
|
-
if not extensions
|
|
316
|
-
return
|
|
314
|
+
def ensure_extensions(extensions: list[str], check_only: bool) -> tuple[list[str], bool, str]:
|
|
315
|
+
if not extensions:
|
|
316
|
+
return [], True, "not applicable"
|
|
317
|
+
if not command_path("code"):
|
|
318
|
+
return extensions, False, "VS Code's WSL command bridge is unavailable"
|
|
319
|
+
|
|
320
|
+
ipc_hook = os.environ.get("VSCODE_IPC_HOOK_CLI")
|
|
321
|
+
if not ipc_hook:
|
|
322
|
+
return [], False, (
|
|
323
|
+
"this shell is not bound to an active Remote - WSL VS Code host. Open the repository in "
|
|
324
|
+
"VS Code Remote - WSL and rerun setup from that window's integrated terminal; a bare `code` "
|
|
325
|
+
"command from an external WSL shell can target the Windows-local extension set instead"
|
|
326
|
+
)
|
|
327
|
+
|
|
317
328
|
result = run(["code", "--list-extensions"])
|
|
329
|
+
if result.returncode != 0:
|
|
330
|
+
detail = "\n".join(filter(None, [(result.stdout or "").strip(), (result.stderr or "").strip()]))
|
|
331
|
+
return [], False, detail or "the active Remote - WSL host did not list extensions successfully"
|
|
332
|
+
|
|
318
333
|
installed = {line.strip().lower() for line in (result.stdout or "").splitlines() if line.strip()}
|
|
319
334
|
missing = [value for value in extensions if value.lower() not in installed]
|
|
320
335
|
if not check_only:
|
|
336
|
+
install_failures: list[str] = []
|
|
321
337
|
for extension in missing:
|
|
322
|
-
run(["code", "--install-extension", extension, "--force"], capture=False)
|
|
338
|
+
install_result = run(["code", "--install-extension", extension, "--force"], capture=False)
|
|
339
|
+
if install_result.returncode != 0:
|
|
340
|
+
install_failures.append(extension)
|
|
323
341
|
result = run(["code", "--list-extensions"])
|
|
342
|
+
if result.returncode != 0:
|
|
343
|
+
detail = "\n".join(filter(None, [(result.stdout or "").strip(), (result.stderr or "").strip()]))
|
|
344
|
+
return install_failures, False, detail or "the active Remote - WSL host could not verify extensions"
|
|
324
345
|
installed = {line.strip().lower() for line in (result.stdout or "").splitlines() if line.strip()}
|
|
325
346
|
missing = [value for value in extensions if value.lower() not in installed]
|
|
326
|
-
|
|
347
|
+
for extension in install_failures:
|
|
348
|
+
if extension not in missing:
|
|
349
|
+
missing.append(extension)
|
|
350
|
+
|
|
351
|
+
agent_folder = os.environ.get("VSCODE_AGENT_FOLDER")
|
|
352
|
+
detail = "active Remote - WSL extension host"
|
|
353
|
+
if agent_folder:
|
|
354
|
+
detail += f" ({agent_folder})"
|
|
355
|
+
return missing, True, detail
|
|
327
356
|
|
|
328
357
|
|
|
329
358
|
def restore_project(commands: list[list[str]], check_only: bool) -> list[str]:
|
|
@@ -377,9 +406,17 @@ def main() -> int:
|
|
|
377
406
|
if not ok:
|
|
378
407
|
(failed_required if required else failed_optional).append(entry["id"])
|
|
379
408
|
|
|
380
|
-
|
|
409
|
+
declared_extensions = config.get("vscodeExtensions", [])
|
|
410
|
+
missing_extensions, extension_host_verified, extension_host_detail = ensure_extensions(
|
|
411
|
+
declared_extensions, args.check
|
|
412
|
+
)
|
|
413
|
+
if declared_extensions:
|
|
414
|
+
host_state = "PASS" if extension_host_verified else "UNVERIFIED"
|
|
415
|
+
print(f"{host_state} VS Code WSL extension host — {extension_host_detail}")
|
|
381
416
|
for extension in missing_extensions:
|
|
382
417
|
print(f"MISSING extension {extension}")
|
|
418
|
+
if declared_extensions and not extension_host_verified:
|
|
419
|
+
failed_required.append("VS Code extension host")
|
|
383
420
|
if missing_extensions:
|
|
384
421
|
failed_required.append("VS Code extensions")
|
|
385
422
|
|
package/assets/components.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"schemaVersion": 2,
|
|
3
3
|
"package": "@mriekki/project-forge",
|
|
4
|
-
"version": "4.15.
|
|
4
|
+
"version": "4.15.1",
|
|
5
5
|
"defaultProfile": "greenfield-project",
|
|
6
6
|
"profiles": {
|
|
7
7
|
"greenfield-project": {
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
},
|
|
14
14
|
"components": {
|
|
15
15
|
"core": {
|
|
16
|
-
"revision": "
|
|
16
|
+
"revision": "6e31941ac4b6c2490b0470b1b4d25b7289b97c98",
|
|
17
17
|
"version": "4.0.0",
|
|
18
18
|
"distribution": "greenfield-workspace",
|
|
19
19
|
"descriptor": "assets/core/distribution.json",
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
{
|
|
2
2
|
"schemaVersion": 2,
|
|
3
3
|
"package": "@mriekki/project-forge",
|
|
4
|
-
"version": "4.15.
|
|
4
|
+
"version": "4.15.1",
|
|
5
5
|
"components": {
|
|
6
6
|
"core": {
|
|
7
|
-
"revision": "
|
|
7
|
+
"revision": "6e31941ac4b6c2490b0470b1b4d25b7289b97c98",
|
|
8
8
|
"version": "4.0.0",
|
|
9
9
|
"distribution": "greenfield-workspace",
|
|
10
10
|
"files": [
|
|
@@ -96,7 +96,7 @@
|
|
|
96
96
|
{
|
|
97
97
|
"source": "core/skills/work-item-status/SKILL.md",
|
|
98
98
|
"destination": ".github/skills/work-item-status/SKILL.md",
|
|
99
|
-
"sha256": "
|
|
99
|
+
"sha256": "b09fc1b9666a81f5c5a85dc691a6db9869097fe560d45fc14dec71256eabdc52"
|
|
100
100
|
},
|
|
101
101
|
{
|
|
102
102
|
"source": "core/skills/cleanup-branches/SKILL.md",
|
|
@@ -331,7 +331,7 @@
|
|
|
331
331
|
{
|
|
332
332
|
"source": "tools/vscode_work_item_status/install.py",
|
|
333
333
|
"destination": "tools/project-forge-vscode/work-item-status/install.py",
|
|
334
|
-
"sha256": "
|
|
334
|
+
"sha256": "1f19a8d47b916bd8b21669c50a418010807832db879993a39c3a74d49aa662e1"
|
|
335
335
|
},
|
|
336
336
|
{
|
|
337
337
|
"source": "tools/vscode_work_item_status/extension/package.json",
|
|
@@ -346,7 +346,7 @@
|
|
|
346
346
|
{
|
|
347
347
|
"source": "tools/vscode_work_item_status/extension/README.md",
|
|
348
348
|
"destination": "tools/project-forge-vscode/work-item-status/extension/README.md",
|
|
349
|
-
"sha256": "
|
|
349
|
+
"sha256": "b5f1e2d6051fa7b62f724fcfbf9413ff5bbef61437c780bee566be66475f0855"
|
|
350
350
|
},
|
|
351
351
|
{
|
|
352
352
|
"source": "tools/package/check_baseline.py",
|
|
@@ -82,10 +82,23 @@ A useful result is:
|
|
|
82
82
|
WORK-ITEM EXPLORER STATUS
|
|
83
83
|
|
|
84
84
|
Local work-item folders .... PRESENT / NOT PRESENT
|
|
85
|
-
VS Code decorator
|
|
85
|
+
VS Code decorator files .... FILES PRESENT / NOT INSTALLED / ERROR
|
|
86
|
+
Host activation ............ VERIFIED BY HUMAN / UNVERIFIED / NOT APPLICABLE
|
|
86
87
|
Project workflow authority . UNCHANGED
|
|
87
88
|
```
|
|
88
89
|
|
|
90
|
+
`FILES PRESENT` means only that the expected extension files exist in the resolved extension-host directory. It does not prove host discovery or activation. The installer must not report a generic `ready` result merely from file presence.
|
|
91
|
+
|
|
92
|
+
For Remote - WSL, resolve the extension root in this precedence order:
|
|
93
|
+
|
|
94
|
+
1. explicit `--extensions-dir`;
|
|
95
|
+
2. `VSCODE_EXTENSIONS`;
|
|
96
|
+
3. the active VS Code Server root exposed by `VSCODE_AGENT_FOLDER`;
|
|
97
|
+
4. an unambiguous existing stable (`~/.vscode-server`) or Insiders (`~/.vscode-server-insiders`) server root;
|
|
98
|
+
5. the normal stable WSL server root when no server root exists yet.
|
|
99
|
+
|
|
100
|
+
If both stable and Insiders roots exist and no active/explicit root identifies the intended host, stop with an actionable ambiguity instead of guessing. Do not generalize the WSL default to SSH, Dev Containers, custom server roots, or other remote hosts.
|
|
101
|
+
|
|
89
102
|
## 4. Enable
|
|
90
103
|
|
|
91
104
|
Outside `/setup`, `/work-item-status enable` is explicit authorization for one bounded local editor change: install/update the Project Forge-owned extension in this developer's VS Code extensions directory. Normal `/setup` and `/setup local` already authorize the same bounded install when local `docs/work-items` folders are present, so do not ask for a second confirmation in that flow.
|
|
@@ -98,7 +111,9 @@ python tools/project-forge-vscode/work-item-status/install.py install --json
|
|
|
98
111
|
|
|
99
112
|
The installer may replace older versions of **this same Project Forge extension only**. It must not install unrelated extensions, alter Project files, change work-item docs, or change Git/GitHub state.
|
|
100
113
|
|
|
101
|
-
If installation changes the extension, tell the developer to reload VS Code once. The
|
|
114
|
+
If installation changes the extension, tell the developer to reload VS Code once. The install result may confirm file placement and `reloadRequired: true`, but activation remains `unverified` until the actual editor host discovers the extension after reload. When activation matters, confirm command discovery in the active VS Code window rather than inferring it from file existence or from a `code --list-extensions` invocation whose host binding is unknown.
|
|
115
|
+
|
|
116
|
+
The decorator then refreshes automatically and also exposes:
|
|
102
117
|
|
|
103
118
|
```text
|
|
104
119
|
Project Forge: Refresh Work Item Status
|
|
@@ -123,9 +138,10 @@ Do not remove any other extension. If files were removed, tell the developer to
|
|
|
123
138
|
## 6. Guided ending
|
|
124
139
|
|
|
125
140
|
Return:
|
|
126
|
-
- decorator installation state;
|
|
141
|
+
- decorator file-installation state;
|
|
142
|
+
- host activation state when actually verified;
|
|
127
143
|
- whether local `docs/work-items` folders are present;
|
|
128
144
|
- a reminder that badges are evidence-derived and non-authoritative;
|
|
129
145
|
- the smallest relevant next action.
|
|
130
146
|
|
|
131
|
-
Do not turn this editor convenience into a source of Project workflow authority.
|
|
147
|
+
Do not turn this editor convenience into a source of Project workflow authority.
|
|
@@ -2,6 +2,14 @@
|
|
|
2
2
|
|
|
3
3
|
This optional local VS Code extension decorates Project Forge work-item folders in the Explorer from repository evidence and provides read-only helpers for Project Forge human-review surfaces.
|
|
4
4
|
|
|
5
|
+
## Installation and activation
|
|
6
|
+
|
|
7
|
+
Project Forge installs this extension into the VS Code extension-host directory selected by `tools/project-forge-vscode/work-item-status/install.py`.
|
|
8
|
+
|
|
9
|
+
For Remote - WSL, the installer preserves explicit `--extensions-dir` and `VSCODE_EXTENSIONS` overrides, prefers the active VS Code Server root exposed by `VSCODE_AGENT_FOLDER`, and otherwise distinguishes stable from Insiders server roots without silently choosing between ambiguous existing hosts.
|
|
10
|
+
|
|
11
|
+
Installer status describes file placement only. `FILES PRESENT` does not prove that the active VS Code host has discovered or activated the extension. After an install/update reports `reloadRequired: true`, reload the actual VS Code window and verify a Project Forge command from that window when activation evidence is required.
|
|
12
|
+
|
|
5
13
|
## Work-item status
|
|
6
14
|
|
|
7
15
|
- `●` — in progress, supported by the current work-item branch or an open pull request.
|
|
@@ -29,11 +29,51 @@ def extension_identity() -> tuple[str, str]:
|
|
|
29
29
|
return f"{publisher}.{name}", version
|
|
30
30
|
|
|
31
31
|
|
|
32
|
-
def
|
|
32
|
+
def is_wsl() -> bool:
|
|
33
|
+
if os.environ.get("WSL_DISTRO_NAME") or os.environ.get("WSL_INTEROP"):
|
|
34
|
+
return True
|
|
35
|
+
try:
|
|
36
|
+
release = Path("/proc/sys/kernel/osrelease").read_text(encoding="utf-8")
|
|
37
|
+
except OSError:
|
|
38
|
+
return False
|
|
39
|
+
return "microsoft" in release.lower()
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def resolve_extensions_root(explicit: Path | None = None) -> tuple[Path, str]:
|
|
43
|
+
if explicit is not None:
|
|
44
|
+
return explicit.expanduser(), "command-line"
|
|
45
|
+
|
|
33
46
|
override = os.environ.get("VSCODE_EXTENSIONS")
|
|
34
47
|
if override:
|
|
35
|
-
return Path(override).expanduser()
|
|
36
|
-
|
|
48
|
+
return Path(override).expanduser(), "VSCODE_EXTENSIONS"
|
|
49
|
+
|
|
50
|
+
if not is_wsl():
|
|
51
|
+
return Path.home() / ".vscode" / "extensions", "native-default"
|
|
52
|
+
|
|
53
|
+
agent_folder = os.environ.get("VSCODE_AGENT_FOLDER")
|
|
54
|
+
if agent_folder:
|
|
55
|
+
return Path(agent_folder).expanduser() / "extensions", "VSCODE_AGENT_FOLDER"
|
|
56
|
+
|
|
57
|
+
home = Path.home()
|
|
58
|
+
candidates = [
|
|
59
|
+
(home / ".vscode-server" / "extensions", "wsl-stable"),
|
|
60
|
+
(home / ".vscode-server-insiders" / "extensions", "wsl-insiders"),
|
|
61
|
+
]
|
|
62
|
+
existing = [(path, source) for path, source in candidates if path.parent.is_dir()]
|
|
63
|
+
if len(existing) == 1:
|
|
64
|
+
return existing[0]
|
|
65
|
+
if len(existing) > 1:
|
|
66
|
+
roots = ", ".join(str(path) for path, _ in existing)
|
|
67
|
+
raise RuntimeError(
|
|
68
|
+
"Multiple VS Code Server roots are present in WSL "
|
|
69
|
+
f"({roots}). Set --extensions-dir or VSCODE_EXTENSIONS to select the active host explicitly."
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
return candidates[0][0], "wsl-stable-default"
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def default_extensions_root() -> Path:
|
|
76
|
+
return resolve_extensions_root()[0]
|
|
37
77
|
|
|
38
78
|
|
|
39
79
|
def extension_target(root: Path) -> Path:
|
|
@@ -44,15 +84,18 @@ def extension_target(root: Path) -> Path:
|
|
|
44
84
|
def status(root: Path) -> dict[str, Any]:
|
|
45
85
|
extension_id, version = extension_identity()
|
|
46
86
|
target = extension_target(root)
|
|
47
|
-
|
|
87
|
+
files_present = target.is_dir() and all(
|
|
48
88
|
(target / name).is_file() for name in ("package.json", "extension.js", "README.md")
|
|
49
89
|
)
|
|
50
90
|
return {
|
|
51
91
|
"extensionId": extension_id,
|
|
52
92
|
"version": version,
|
|
53
|
-
"status": "
|
|
93
|
+
"status": "files-present" if files_present else "not-installed",
|
|
54
94
|
"path": str(target),
|
|
55
95
|
"reloadRequired": False,
|
|
96
|
+
"activationStatus": "unverified" if files_present else "not-applicable",
|
|
97
|
+
"activationVerified": False,
|
|
98
|
+
"verificationRequired": files_present,
|
|
56
99
|
}
|
|
57
100
|
|
|
58
101
|
|
|
@@ -65,7 +108,7 @@ def install(root: Path) -> dict[str, Any]:
|
|
|
65
108
|
raise RuntimeError(f"Missing Project Forge VS Code extension source: {SOURCE}")
|
|
66
109
|
|
|
67
110
|
existing = status(root)
|
|
68
|
-
if existing["status"] == "
|
|
111
|
+
if existing["status"] == "files-present":
|
|
69
112
|
return existing
|
|
70
113
|
|
|
71
114
|
with tempfile.TemporaryDirectory(prefix="project-forge-vscode-", dir=str(root)) as tmp:
|
|
@@ -101,6 +144,9 @@ def uninstall(root: Path) -> dict[str, Any]:
|
|
|
101
144
|
"status": "not-installed",
|
|
102
145
|
"removed": removed,
|
|
103
146
|
"reloadRequired": bool(removed),
|
|
147
|
+
"activationStatus": "not-applicable",
|
|
148
|
+
"activationVerified": False,
|
|
149
|
+
"verificationRequired": False,
|
|
104
150
|
}
|
|
105
151
|
|
|
106
152
|
|
|
@@ -112,8 +158,12 @@ def emit(result: dict[str, Any], as_json: bool) -> None:
|
|
|
112
158
|
print(f"Extension: {result['extensionId']}@{result['version']}")
|
|
113
159
|
if result.get("path"):
|
|
114
160
|
print(f"Path: {result['path']}")
|
|
161
|
+
if result.get("extensionsRootSource"):
|
|
162
|
+
print(f"Extensions root: {result['extensionsRootSource']}")
|
|
115
163
|
if result.get("removed"):
|
|
116
164
|
print(f"Removed: {len(result['removed'])}")
|
|
165
|
+
if result.get("verificationRequired"):
|
|
166
|
+
print("Extension files are present; VS Code host discovery/activation is not verified by this installer.")
|
|
117
167
|
if result.get("reloadRequired"):
|
|
118
168
|
print("Reload VS Code to activate the changed extension installation.")
|
|
119
169
|
|
|
@@ -127,14 +177,16 @@ def main() -> int:
|
|
|
127
177
|
parser.add_argument("--json", action="store_true")
|
|
128
178
|
args = parser.parse_args()
|
|
129
179
|
|
|
130
|
-
root = (args.extensions_dir or default_extensions_root()).expanduser().resolve()
|
|
131
180
|
try:
|
|
181
|
+
root, root_source = resolve_extensions_root(args.extensions_dir)
|
|
182
|
+
root = root.resolve()
|
|
132
183
|
if args.action == "install":
|
|
133
184
|
result = install(root)
|
|
134
185
|
elif args.action == "uninstall":
|
|
135
186
|
result = uninstall(root)
|
|
136
187
|
else:
|
|
137
188
|
result = status(root)
|
|
189
|
+
result["extensionsRootSource"] = root_source
|
|
138
190
|
except (OSError, RuntimeError, ValueError, json.JSONDecodeError) as exc:
|
|
139
191
|
if args.json:
|
|
140
192
|
print(json.dumps({"status": "error", "error": str(exc)}, sort_keys=True))
|