pi-revit 0.2.7 → 0.2.9
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/CHANGELOG.md +44 -0
- package/README.md +26 -4
- package/extensions/pi-revit/index.ts +64 -20
- package/package.json +1 -1
- package/scripts/setup-workspace.ps1 +80 -80
- package/scripts/uninstall.ps1 +146 -146
- package/skills/pi-revit/SKILL.md +2 -1
- package/src/Revit/Tools/ExecuteCsharp.cs +6 -0
- package/src/Revit/Tools/GetElementDetails.cs +26 -6
- package/src/Revit/Tools/GetElements.cs +12 -5
- package/src/Revit/Tools/SearchApiDocs.cs +13 -5
- package/src/Revit/Tools/SetParameters.cs +6 -0
- package/src/Revit/Tools/ToolSupport.cs +301 -261
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,50 @@ Format follows [Keep a Changelog](https://keepachangelog.com/); version headers
|
|
|
7
7
|
Every published version gets an entry with **Added** / **Changed** / **Fixed** sections
|
|
8
8
|
describing what the user will notice — not internal refactors.
|
|
9
9
|
|
|
10
|
+
## [0.2.9] - 2026-07-21
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
- Self-healing tool discovery: when pi starts before Revit, the extension now keeps
|
|
14
|
+
retrying tool discovery in the background (every 15s) and also re-discovers on a
|
|
15
|
+
successful `ping` — no more sessions stuck with only `ping` registered until a fresh
|
|
16
|
+
pi start. When tools arrive mid-session, `ping`'s result says so.
|
|
17
|
+
- `set_parameters` and `execute_csharp` accept an optional `expected_document` (the model
|
|
18
|
+
title): if the active document differs — e.g. the user switched models mid-session —
|
|
19
|
+
the write fails cleanly instead of landing in the wrong model.
|
|
20
|
+
- `get_element_details.parameter_names` now also matches language-independent
|
|
21
|
+
BuiltInParameter enum names (e.g. `ALL_MODEL_MARK`), so filtering works in non-English
|
|
22
|
+
Revit UIs where display names are localized.
|
|
23
|
+
|
|
24
|
+
### Fixed
|
|
25
|
+
- `get_element_details` no longer reports a misleading "0 params" when a
|
|
26
|
+
`parameter_names` filter simply matched nothing — it now reports "N of M params
|
|
27
|
+
matched parameter_names" so localization misses are visible. (This explains the
|
|
28
|
+
earlier "0 params vs 38 params" reports: different filter arguments, not flaky reads.)
|
|
29
|
+
- `get_elements`: a display-name filter rule in an **unscoped** query (no category /
|
|
30
|
+
of_class) is no longer promoted to a pinned collector filter based on a 50-element
|
|
31
|
+
probe — it stays on the per-element post-scan path, so categories beyond the probe
|
|
32
|
+
window can't be silently dropped when the same parameter name maps to different ids.
|
|
33
|
+
|
|
34
|
+
### Changed
|
|
35
|
+
- SKILL.md: guidance on localized parameter names (prefer BuiltInParameter enum names)
|
|
36
|
+
and on using `expected_document` for long sessions / multiple open models.
|
|
37
|
+
- README: new "Safety model" section stating explicitly what the add-in enforces and
|
|
38
|
+
that write-confirmation UX is a client-side decision.
|
|
39
|
+
|
|
40
|
+
Requires redeploying the Revit add-in (`scripts\deploy.ps1` with Revit closed, then
|
|
41
|
+
restart Revit).
|
|
42
|
+
|
|
43
|
+
## [0.2.8] - 2026-07-21
|
|
44
|
+
|
|
45
|
+
### Fixed
|
|
46
|
+
- `search_api_docs`: overload-targeted queries no longer fail on comma spacing.
|
|
47
|
+
`Wall.Create(Document,Curve` (no space) and `Wall.Create( Document, Curve` now match the
|
|
48
|
+
same overloads as `Wall.Create(Document, Curve` — the query's spacing around commas and
|
|
49
|
+
parentheses is normalized to the rendered signature style before matching.
|
|
50
|
+
|
|
51
|
+
Requires redeploying the Revit add-in (`scripts\deploy.ps1` with Revit closed, then restart
|
|
52
|
+
Revit).
|
|
53
|
+
|
|
10
54
|
## [0.2.7] - 2026-07-21
|
|
11
55
|
|
|
12
56
|
### Added
|
package/README.md
CHANGED
|
@@ -29,10 +29,32 @@ headless Revit add-in ← no ribbon, no panels; just a bridge
|
|
|
29
29
|
Revit API ← reads run directly; writes run in one named transaction
|
|
30
30
|
```
|
|
31
31
|
|
|
32
|
-
The extension discovers its tools from the bridge at startup (
|
|
33
|
-
tool list always matches what the add-in serves. Everything between Pi and
|
|
34
|
-
local-machine only; note that Pi sends conversation context and tool results to your
|
|
35
|
-
LLM provider, like any Pi session.
|
|
32
|
+
The extension discovers its tools from the bridge at startup (retrying in the background until
|
|
33
|
+
Revit is up), so the tool list always matches what the add-in serves. Everything between Pi and
|
|
34
|
+
Revit is local-machine only; note that Pi sends conversation context and tool results to your
|
|
35
|
+
selected LLM provider, like any Pi session.
|
|
36
|
+
|
|
37
|
+
## Safety model
|
|
38
|
+
|
|
39
|
+
Be deliberate about pointing an LLM at a real project model. The add-in enforces what it can
|
|
40
|
+
enforce mechanically, and is honest about what it cannot:
|
|
41
|
+
|
|
42
|
+
- Every write tool is flagged `write: true` — that flag is the machine-readable signal a client
|
|
43
|
+
can gate on. Whether a write needs human confirmation is a **client-side decision**: the
|
|
44
|
+
add-in cannot know your policy, so confirmation UX belongs in the Pi client/agent layer, not
|
|
45
|
+
here.
|
|
46
|
+
- All writes run in one named transaction: committed on success, rolled back on failure, always
|
|
47
|
+
visible in Revit's undo history. Commit-time warnings are reported back (`commitWarnings`);
|
|
48
|
+
error-severity failures roll back with Revit's failure text.
|
|
49
|
+
- `execute_csharp` is an unrestricted escape hatch by design — scripts have full CLR access.
|
|
50
|
+
Treat it like giving the agent a macro editor, on a model you have saved or can restore.
|
|
51
|
+
- Blocking popups are auto-answered so Revit can never hang behind a dialog; unrecognized
|
|
52
|
+
dialogs get the dismissive answer (Cancel/Close/No), never a blind OK.
|
|
53
|
+
- Writes accept an optional `expected_document` check so a queued write cannot silently land in
|
|
54
|
+
a different model than intended.
|
|
55
|
+
|
|
56
|
+
Practical advice: work on saved models, keep worksharing backups/central protection as usual,
|
|
57
|
+
and review the agent's summary of what changed after any write session.
|
|
36
58
|
|
|
37
59
|
## Requirements
|
|
38
60
|
|
|
@@ -275,7 +275,7 @@ async function announceUpdateOnce(notify: (message: string, level: "info") => vo
|
|
|
275
275
|
}
|
|
276
276
|
}
|
|
277
277
|
|
|
278
|
-
function registerPing(pi: ExtensionAPI) {
|
|
278
|
+
function registerPing(pi: ExtensionAPI, onBridgeAlive?: () => Promise<"ready" | "registered" | "failed">) {
|
|
279
279
|
pi.registerTool({
|
|
280
280
|
name: "ping",
|
|
281
281
|
label: "Ping Revit Bridge",
|
|
@@ -287,18 +287,71 @@ function registerPing(pi: ExtensionAPI) {
|
|
|
287
287
|
async execute(_toolCallId, _params, signal) {
|
|
288
288
|
const payload = await bridgeRequest("/ping", { method: "GET" }, signal, 10_000);
|
|
289
289
|
const warning = versionMismatch((payload as { addinVersion?: string }).addinVersion);
|
|
290
|
+
// The bridge is alive: if this session started before Revit and only has
|
|
291
|
+
// ping, register the bridge tools now and tell the model they arrived.
|
|
292
|
+
let registrationNote = "";
|
|
293
|
+
if (onBridgeAlive) {
|
|
294
|
+
const state = await onBridgeAlive();
|
|
295
|
+
if (state === "registered")
|
|
296
|
+
registrationNote = "\nNOTE: The Revit bridge tools (get_elements, set_parameters, execute_csharp, ...) were just registered in this session and are available from now on.";
|
|
297
|
+
else if (state === "failed")
|
|
298
|
+
registrationNote = "\nNOTE: Bridge tool discovery failed even though ping succeeded; retry ping or restart pi.";
|
|
299
|
+
}
|
|
290
300
|
return {
|
|
291
|
-
content: [{ type: "text", text: JSON.stringify(payload) + (warning ? `\nWARNING: ${warning}` : "") }],
|
|
301
|
+
content: [{ type: "text", text: JSON.stringify(payload) + (warning ? `\nWARNING: ${warning}` : "") + registrationNote }],
|
|
292
302
|
details: payload,
|
|
293
303
|
};
|
|
294
304
|
},
|
|
295
305
|
});
|
|
296
306
|
}
|
|
297
307
|
|
|
308
|
+
const REDISCOVERY_INTERVAL_MS = 15_000;
|
|
309
|
+
|
|
298
310
|
export default async function revitConnector(pi: ExtensionAPI) {
|
|
311
|
+
// Self-healing discovery: when pi starts before Revit is ready, the initial
|
|
312
|
+
// GET /tools fails and only ping is registered. Rather than requiring a
|
|
313
|
+
// fresh pi start (/reload does not reliably re-run async registration), a
|
|
314
|
+
// background retry keeps probing until the bridge appears, and a successful
|
|
315
|
+
// ping also triggers an immediate attempt.
|
|
316
|
+
let bridgeToolsRegistered = false;
|
|
317
|
+
let discoveryInFlight: Promise<boolean> | null = null;
|
|
318
|
+
|
|
319
|
+
async function discoverAndRegister(): Promise<boolean> {
|
|
320
|
+
if (bridgeToolsRegistered) return true;
|
|
321
|
+
if (discoveryInFlight) return discoveryInFlight;
|
|
322
|
+
discoveryInFlight = (async () => {
|
|
323
|
+
try {
|
|
324
|
+
const payload = (await bridgeRequest("/tools", { method: "GET" }, undefined, DISCOVERY_TIMEOUT_MS)) as {
|
|
325
|
+
tools?: BridgeToolDescriptor[];
|
|
326
|
+
};
|
|
327
|
+
const descriptors = Array.isArray(payload?.tools) ? payload.tools : [];
|
|
328
|
+
if (descriptors.length === 0) return false;
|
|
329
|
+
for (const descriptor of descriptors) {
|
|
330
|
+
if (!descriptor || typeof descriptor.name !== "string" || !descriptor.name) continue;
|
|
331
|
+
if (descriptor.name === "ping") continue;
|
|
332
|
+
registerBridgeTool(pi, descriptor);
|
|
333
|
+
}
|
|
334
|
+
bridgeToolsRegistered = true;
|
|
335
|
+
return true;
|
|
336
|
+
} catch {
|
|
337
|
+
// Bridge down (Revit closed, still starting, stale bridge.json):
|
|
338
|
+
// stay on ping only and try again later.
|
|
339
|
+
return false;
|
|
340
|
+
} finally {
|
|
341
|
+
discoveryInFlight = null;
|
|
342
|
+
}
|
|
343
|
+
})();
|
|
344
|
+
return discoveryInFlight;
|
|
345
|
+
}
|
|
346
|
+
|
|
299
347
|
// ping is hard-coded: it must work (and report clearly) even when the
|
|
300
|
-
// bridge is down, so it is never part of /tools discovery.
|
|
301
|
-
|
|
348
|
+
// bridge is down, so it is never part of /tools discovery. A successful
|
|
349
|
+
// ping doubles as a re-discovery trigger — the natural first call in a
|
|
350
|
+
// session that finds itself without bridge tools.
|
|
351
|
+
registerPing(pi, async () => {
|
|
352
|
+
if (bridgeToolsRegistered) return "ready";
|
|
353
|
+
return (await discoverAndRegister()) ? "registered" : "failed";
|
|
354
|
+
});
|
|
302
355
|
|
|
303
356
|
// Surface an incomplete update (see versionMismatch) once per session, right
|
|
304
357
|
// where the user lands after running `pi update --extensions`. Bridge down at
|
|
@@ -314,21 +367,12 @@ export default async function revitConnector(pi: ExtensionAPI) {
|
|
|
314
367
|
}
|
|
315
368
|
});
|
|
316
369
|
|
|
317
|
-
|
|
318
|
-
try {
|
|
319
|
-
const payload = (await bridgeRequest("/tools", { method: "GET" }, undefined, DISCOVERY_TIMEOUT_MS)) as {
|
|
320
|
-
tools?: BridgeToolDescriptor[];
|
|
321
|
-
};
|
|
322
|
-
descriptors = Array.isArray(payload?.tools) ? payload.tools : [];
|
|
323
|
-
} catch {
|
|
324
|
-
// Bridge down at startup (Revit closed, stale bridge.json, ...): keep
|
|
325
|
-
// only ping registered and never block pi startup. /reload re-discovers.
|
|
326
|
-
return;
|
|
327
|
-
}
|
|
370
|
+
if (await discoverAndRegister()) return;
|
|
328
371
|
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
}
|
|
372
|
+
// Never block pi startup on Revit: keep retrying quietly in the background
|
|
373
|
+
// and stop the moment discovery succeeds.
|
|
374
|
+
const timer = setInterval(async () => {
|
|
375
|
+
if (await discoverAndRegister()) clearInterval(timer);
|
|
376
|
+
}, REDISCOVERY_INTERVAL_MS);
|
|
377
|
+
timer.unref?.();
|
|
334
378
|
}
|
package/package.json
CHANGED
|
@@ -1,80 +1,80 @@
|
|
|
1
|
-
#Requires -Version 5.1
|
|
2
|
-
<#
|
|
3
|
-
Sets up the pi-revit user environment on this PC:
|
|
4
|
-
|
|
5
|
-
1. Workspace at Documents\pi-revit (AGENTS.md conventions, Models\ output tree,
|
|
6
|
-
double-click launcher). An existing AGENTS.md is never overwritten.
|
|
7
|
-
2. Global `pi-revit` command installed next to the `pi` command, so any terminal can run:
|
|
8
|
-
pi-revit -> Pi in the workspace
|
|
9
|
-
pi-revit -c -> continue the last session
|
|
10
|
-
Model output sorts itself: the Revit tools file each export under
|
|
11
|
-
Models\<model title>\ automatically, keyed by the document it came from.
|
|
12
|
-
|
|
13
|
-
Idempotent — safe to re-run. Usage:
|
|
14
|
-
scripts\setup-workspace.ps1
|
|
15
|
-
scripts\setup-workspace.ps1 -WorkspaceDir "D:\work\pi-revit"
|
|
16
|
-
#>
|
|
17
|
-
param(
|
|
18
|
-
[string]$WorkspaceDir = (Join-Path ([Environment]::GetFolderPath('MyDocuments')) 'pi-revit')
|
|
19
|
-
)
|
|
20
|
-
|
|
21
|
-
$ErrorActionPreference = 'Stop'
|
|
22
|
-
$templates = Join-Path (Split-Path $PSScriptRoot -Parent) 'workspace'
|
|
23
|
-
|
|
24
|
-
# 1. Workspace folders. Models\ is where the Revit tools file per-model output.
|
|
25
|
-
New-Item -ItemType Directory -Force -Path (Join-Path $WorkspaceDir 'Models') | Out-Null
|
|
26
|
-
|
|
27
|
-
# Conventions / notes: copy only when missing so user edits survive re-runs.
|
|
28
|
-
$workspaceAgents = Join-Path $WorkspaceDir 'AGENTS.md'
|
|
29
|
-
if (-not (Test-Path $workspaceAgents)) {
|
|
30
|
-
Copy-Item (Join-Path $templates 'AGENTS.md') $workspaceAgents
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
# Launcher is code, not user data: always refresh.
|
|
34
|
-
Copy-Item (Join-Path $templates 'pi-revit-here.cmd') (Join-Path $WorkspaceDir 'pi-revit.cmd') -Force
|
|
35
|
-
|
|
36
|
-
Write-Host "Workspace ready: $WorkspaceDir" -ForegroundColor Green
|
|
37
|
-
|
|
38
|
-
# 2. Global pi-revit command next to pi (that directory is on PATH by definition).
|
|
39
|
-
# The template's workspace path is rewritten to honor -WorkspaceDir.
|
|
40
|
-
# Under `npx pi-revit`, npx prepends its ephemeral cache's node_modules\.bin to
|
|
41
|
-
# PATH, and that folder holds a pi shim pulled in via peerDependencies. A launcher
|
|
42
|
-
# written next to that shim lands in a folder that is neither on the user's own
|
|
43
|
-
# PATH nor long-lived, so skip pi entries inside this package's node_modules tree
|
|
44
|
-
# or any npx cache and use the user's permanent pi instead.
|
|
45
|
-
function Get-PermanentPiCommand {
|
|
46
|
-
# Long-form both sides of the prefix comparison: PATH entries may carry 8.3
|
|
47
|
-
# short names (
|
|
48
|
-
# mismatch would silently defeat the own-tree exclusion.
|
|
49
|
-
function Resolve-LongPath([string]$p) {
|
|
50
|
-
try { (Get-Item -LiteralPath $p -ErrorAction Stop).FullName } catch { $p }
|
|
51
|
-
}
|
|
52
|
-
$ownTree = $null
|
|
53
|
-
$repoRoot = Resolve-LongPath (Split-Path $PSScriptRoot -Parent)
|
|
54
|
-
$idx = $repoRoot.LastIndexOf('\node_modules\', [StringComparison]::OrdinalIgnoreCase)
|
|
55
|
-
if ($idx -ge 0) { $ownTree = $repoRoot.Substring(0, $idx + '\node_modules\'.Length) }
|
|
56
|
-
Get-Command pi -All -ErrorAction SilentlyContinue | Where-Object {
|
|
57
|
-
if (-not $_.Source) { return $false }
|
|
58
|
-
$src = Resolve-LongPath $_.Source
|
|
59
|
-
($src -notmatch '\\_npx\\') -and
|
|
60
|
-
(-not $ownTree -or -not $src.StartsWith($ownTree, [StringComparison]::OrdinalIgnoreCase))
|
|
61
|
-
} | Select-Object -First 1
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
$pi = Get-PermanentPiCommand
|
|
65
|
-
if ($pi) {
|
|
66
|
-
$binDir = Split-Path $pi.Source -Parent
|
|
67
|
-
# Literal line swap (no regex): the workspace path may contain characters
|
|
68
|
-
# like $ that a -replace replacement string would misinterpret.
|
|
69
|
-
$globalCmd = Get-Content (Join-Path $templates 'pi-revit-global.cmd') | ForEach-Object {
|
|
70
|
-
if ($_ -like 'set "WORKSPACE=*') { 'set "WORKSPACE=' + $WorkspaceDir + '"' } else { $_ }
|
|
71
|
-
}
|
|
72
|
-
# cmd.exe parses .cmd files in the console's OEM code page: write the launcher
|
|
73
|
-
# in that encoding so workspace paths with non-ASCII characters (user names,
|
|
74
|
-
# localized folder names) stay intact.
|
|
75
|
-
Set-Content -Path (Join-Path $binDir 'pi-revit.cmd') -Value $globalCmd -Encoding Oem
|
|
76
|
-
Write-Host "Global command installed: $(Join-Path $binDir 'pi-revit.cmd')" -ForegroundColor Green
|
|
77
|
-
Write-Host 'Run pi-revit from any terminal (pi-revit -c continues the last session).'
|
|
78
|
-
} else {
|
|
79
|
-
Write-Warning 'No permanent pi command found on PATH. Install Pi first (npm install -g --ignore-scripts @earendil-works/pi-coding-agent), then run npx.cmd -y pi-revit again (or re-run this script) to get the global pi-revit command.'
|
|
80
|
-
}
|
|
1
|
+
#Requires -Version 5.1
|
|
2
|
+
<#
|
|
3
|
+
Sets up the pi-revit user environment on this PC:
|
|
4
|
+
|
|
5
|
+
1. Workspace at Documents\pi-revit (AGENTS.md conventions, Models\ output tree,
|
|
6
|
+
double-click launcher). An existing AGENTS.md is never overwritten.
|
|
7
|
+
2. Global `pi-revit` command installed next to the `pi` command, so any terminal can run:
|
|
8
|
+
pi-revit -> Pi in the workspace
|
|
9
|
+
pi-revit -c -> continue the last session
|
|
10
|
+
Model output sorts itself: the Revit tools file each export under
|
|
11
|
+
Models\<model title>\ automatically, keyed by the document it came from.
|
|
12
|
+
|
|
13
|
+
Idempotent — safe to re-run. Usage:
|
|
14
|
+
scripts\setup-workspace.ps1
|
|
15
|
+
scripts\setup-workspace.ps1 -WorkspaceDir "D:\work\pi-revit"
|
|
16
|
+
#>
|
|
17
|
+
param(
|
|
18
|
+
[string]$WorkspaceDir = (Join-Path ([Environment]::GetFolderPath('MyDocuments')) 'pi-revit')
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
$ErrorActionPreference = 'Stop'
|
|
22
|
+
$templates = Join-Path (Split-Path $PSScriptRoot -Parent) 'workspace'
|
|
23
|
+
|
|
24
|
+
# 1. Workspace folders. Models\ is where the Revit tools file per-model output.
|
|
25
|
+
New-Item -ItemType Directory -Force -Path (Join-Path $WorkspaceDir 'Models') | Out-Null
|
|
26
|
+
|
|
27
|
+
# Conventions / notes: copy only when missing so user edits survive re-runs.
|
|
28
|
+
$workspaceAgents = Join-Path $WorkspaceDir 'AGENTS.md'
|
|
29
|
+
if (-not (Test-Path $workspaceAgents)) {
|
|
30
|
+
Copy-Item (Join-Path $templates 'AGENTS.md') $workspaceAgents
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
# Launcher is code, not user data: always refresh.
|
|
34
|
+
Copy-Item (Join-Path $templates 'pi-revit-here.cmd') (Join-Path $WorkspaceDir 'pi-revit.cmd') -Force
|
|
35
|
+
|
|
36
|
+
Write-Host "Workspace ready: $WorkspaceDir" -ForegroundColor Green
|
|
37
|
+
|
|
38
|
+
# 2. Global pi-revit command next to pi (that directory is on PATH by definition).
|
|
39
|
+
# The template's workspace path is rewritten to honor -WorkspaceDir.
|
|
40
|
+
# Under `npx pi-revit`, npx prepends its ephemeral cache's node_modules\.bin to
|
|
41
|
+
# PATH, and that folder holds a pi shim pulled in via peerDependencies. A launcher
|
|
42
|
+
# written next to that shim lands in a folder that is neither on the user's own
|
|
43
|
+
# PATH nor long-lived, so skip pi entries inside this package's node_modules tree
|
|
44
|
+
# or any npx cache and use the user's permanent pi instead.
|
|
45
|
+
function Get-PermanentPiCommand {
|
|
46
|
+
# Long-form both sides of the prefix comparison: PATH entries may carry 8.3
|
|
47
|
+
# short names (SOMEUS~1.NAM) while $PSScriptRoot is normalized, and a form
|
|
48
|
+
# mismatch would silently defeat the own-tree exclusion.
|
|
49
|
+
function Resolve-LongPath([string]$p) {
|
|
50
|
+
try { (Get-Item -LiteralPath $p -ErrorAction Stop).FullName } catch { $p }
|
|
51
|
+
}
|
|
52
|
+
$ownTree = $null
|
|
53
|
+
$repoRoot = Resolve-LongPath (Split-Path $PSScriptRoot -Parent)
|
|
54
|
+
$idx = $repoRoot.LastIndexOf('\node_modules\', [StringComparison]::OrdinalIgnoreCase)
|
|
55
|
+
if ($idx -ge 0) { $ownTree = $repoRoot.Substring(0, $idx + '\node_modules\'.Length) }
|
|
56
|
+
Get-Command pi -All -ErrorAction SilentlyContinue | Where-Object {
|
|
57
|
+
if (-not $_.Source) { return $false }
|
|
58
|
+
$src = Resolve-LongPath $_.Source
|
|
59
|
+
($src -notmatch '\\_npx\\') -and
|
|
60
|
+
(-not $ownTree -or -not $src.StartsWith($ownTree, [StringComparison]::OrdinalIgnoreCase))
|
|
61
|
+
} | Select-Object -First 1
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
$pi = Get-PermanentPiCommand
|
|
65
|
+
if ($pi) {
|
|
66
|
+
$binDir = Split-Path $pi.Source -Parent
|
|
67
|
+
# Literal line swap (no regex): the workspace path may contain characters
|
|
68
|
+
# like $ that a -replace replacement string would misinterpret.
|
|
69
|
+
$globalCmd = Get-Content (Join-Path $templates 'pi-revit-global.cmd') | ForEach-Object {
|
|
70
|
+
if ($_ -like 'set "WORKSPACE=*') { 'set "WORKSPACE=' + $WorkspaceDir + '"' } else { $_ }
|
|
71
|
+
}
|
|
72
|
+
# cmd.exe parses .cmd files in the console's OEM code page: write the launcher
|
|
73
|
+
# in that encoding so workspace paths with non-ASCII characters (user names,
|
|
74
|
+
# localized folder names) stay intact.
|
|
75
|
+
Set-Content -Path (Join-Path $binDir 'pi-revit.cmd') -Value $globalCmd -Encoding Oem
|
|
76
|
+
Write-Host "Global command installed: $(Join-Path $binDir 'pi-revit.cmd')" -ForegroundColor Green
|
|
77
|
+
Write-Host 'Run pi-revit from any terminal (pi-revit -c continues the last session).'
|
|
78
|
+
} else {
|
|
79
|
+
Write-Warning 'No permanent pi command found on PATH. Install Pi first (npm install -g --ignore-scripts @earendil-works/pi-coding-agent), then run npx.cmd -y pi-revit again (or re-run this script) to get the global pi-revit command.'
|
|
80
|
+
}
|