@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.
@@ -6,18 +6,21 @@
6
6
  "asset": "assets/bootstrap-migrations/wsl-setup/setup-wsl.py",
7
7
  "source": {
8
8
  "component": "bootstrap",
9
- "revision": "043b3b2a0e3ba03b8d18666bb3fef59c588bfa16",
9
+ "revision": "6b6a2252434d310b21150fcbb5ce7270919ad9d0",
10
10
  "artifact": "runtime/dev_environment_wsl.py"
11
11
  },
12
- "targetGitBlob": "0b959b4e85508016f6e8c12d403a94b2eed46c1f",
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 or not command_path("code"):
316
- return extensions if not command_path("code") else []
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
- return missing
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
- missing_extensions = ensure_extensions(config.get("vscodeExtensions", []), args.check)
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
 
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "schemaVersion": 2,
3
3
  "package": "@mriekki/project-forge",
4
- "version": "4.15.0",
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": "b51cb62bcc9630748168c9fd1900c6109cafb587",
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.0",
4
+ "version": "4.15.1",
5
5
  "components": {
6
6
  "core": {
7
- "revision": "b51cb62bcc9630748168c9fd1900c6109cafb587",
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": "ca79843e59e5f50785c3621b6bf8ecb7413307aefce05fec737321589792c3a9"
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": "190015b44611b5b6f0f21bff05923262abd9d9e16e9916dbe71755fb753d8fbc"
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": "54963b69bef2d9f763a6c92ad2c2b3105d3f10dd69542f72167de7e57db18982"
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 .......... READY / NOT INSTALLED / ERROR
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 decorator then refreshes automatically and also exposes:
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 default_extensions_root() -> Path:
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
- return Path.home() / ".vscode" / "extensions"
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
- ready = target.is_dir() and all(
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": "ready" if ready else "not-installed",
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"] == "ready":
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))
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mriekki/project-forge",
3
- "version": "4.15.0",
3
+ "version": "4.15.1",
4
4
  "description": "Public consumer distribution and updater for Project Forge engineering assets.",
5
5
  "type": "module",
6
6
  "bin": {