PCLI2
1.21.0 - 2026-09-04
Fixed
-
The active tenant follows the environment - One tenant selection was shared by every environment, so
env use stagingleft production's tenant UUID active and every command failed with "Tenant not found" untiltenant usewas run again. Each environment now remembers its own tenant (older versions still read the top-level value). Folder and metadata caches are keyed by environment too, so a staging tenant cloned from production with the same UUID can no longer be served production's folder tree. -
Per-asset failure messages survive
--progress- During a folder match the reasons for individual failures, and the "stopping after repeated authentication failures" explanation, were written underneath the redrawing progress display and painted over (the same defect fixed for the scan spinner in 1.18.3). They are now printed with the display lifted. -
env reset,env removeand overwriting withenv addask first -env resetwiped every environment with no prompt;env addsilently replaced an existing one. All three honour the global--yes, and refuse rather than assume when no prompt can be shown. -
env addrejects a URL that is not one ---api-url 'not a url'was saved and surfaced later as an opaque request error. -
env remove --name nopesays the environment does not exist (exit 67) instead of "missing value for property". -
--format JSONworks - The value is matched case-insensitively, asPCLI2_FORMATalready was. -
asset list --folder-uuid- Every other folder command accepted a UUID; this one only took a path. -
user listshows itslsalias in help like every other list command. -
--verboseshows pcli2's own debug output without the HTTP stack's;RUST_LOGstill opens everything. -
Folder trees and lists sort case-insensitively ("alpha" before "Zeta").
-
Configuration and formatting errors show the cause as a sentence rather than a Rust debug dump.
-
asset deleteandfolder deletework again - The request-path rewrite in this release cycle sent the relative API path as the whole URL for DELETE; caught before release, with a regression test. -
asset metadata create-batchfetches the field registry once and lists each parent folder once - Both happened per row: a 5,000-row batch into a 10,000-asset folder was about a quarter of a million requests. A registry fetch that fails is now an error rather than "no fields exist", which used to turn every value into a new text field and report the API's rejection as a type conflict in the user's CSV. -
Cache files are written atomically and carry a schema version - Two pcli2 processes (a script under
xargs -P) could read a half-written cache and refetch the whole hierarchy; a cache written by another version is now discarded instead of decoded wrongly. A cache timestamp from the future no longer panics a debug build. -
Asset listing stops if the server echoes a stale page number - The page loop had no guard against being asked for the same page forever.
-
Listings that hit the safety page cap say so - The truncation of very large user and asset listings was logged at debug level only.
-
--files a,b,crejects a path that does not exist - It was dropped silently, so a typo uploaded one file fewer. Directories matched by a glob are skipped and counted instead of becoming per-file failures. -
asset metadata create --type numberno longer saturates large values -1e20was stored as 9223372036854775807.--type booleanwith a value that is not a boolean is sent as-is so the API rejects it, instead of silently writingfalse. -
asset metadata getprints{}(or CSV headers) for an asset without metadata - stdout used to be empty for a successful command, and JSON output was wrapped in an internal{"meta": ...}object. -
folder download --folder-uuidandfolder thumbnail --folder-uuidno longer write into a directory nameduntitled- The folder's name is used. -
folder uploadno longer copies every file through the temp directory - The copy used a non-unique name, so two uploads of files calledpart.stlfrom different directories could clobber each other mid-upload. -
asset create --overridetreats only "absent" as absent - A network or session failure while checking for the existing asset used to be read as "does not exist". -
A batch row whose folder does not exist is counted as missing, not as a permission problem.
-
A configured API URL with a trailing slash no longer produces
//tenantsURLs; user IDs are URL-encoded. -
tenant useinteractive selection no longer panics on a tenant name containing a colon. -
Excel report sorting parses each cell once instead of once per comparison (tens of millions of parses on a million-row report).
-
--tenantaccepts a tenant UUID, as its help text promised - Only the short name worked. A tenant missing from the cached list is also looked up once more from the API before it is reported as not found, so a tenant granted since the cache was written is usable withoutcache clear. -
PCLI2_FORMATis honoured byenv list,env getandconfig get---formathas a default value, so the branch that consulted the variable was unreachable. -
In-app advice no longer refers to a
contextcommand that does not exist - "Run 'pcli2 context set tenant'" is now "Run 'pcli2 tenant use'". -
Uploads go to the folder that was asked for - The upload endpoint places a file by the path string it is sent and creates any folder it does not know, and three callers sent it the wrong string:
folder upload --folder-uuidsent a placeholder/(every file landed at the tenant root),asset create --folder-uuidfell back to the bare folder name when the hierarchy fetch failed (a nested folderA/B/Cbecame a new top-levelC), and--folder-pathwas sent as typed, so/Home/Partscould create a literalHomefolder and/partsa second folder besideParts. The destination is now the folder's canonical path from the hierarchy, whichever way the folder was named. -
Uploading to the tenant root works -
asset create --folder-path /,asset create-batch --folder-path /andfolder upload --folder-path /failed with "Folder '/' not found" after fetching the whole hierarchy to suggest alternatives. The root has no UUID and needs none. -
Typed metadata compares equal to itself in match reports - A numeric or boolean field was rendered as text on the reference side and blanked on the candidate side, so the Excel diff painted every such field as "missing on one side" and CSV rows showed
42against an empty cell. Both sides now render values the same way (JSON text for non-strings, empty for null). -
Folder match JSON no longer invents reference asset fields -
referenceAssetreportedstate: "active"(not a Physna state),type: "asset",isAssembly: falseand empty timestamps regardless of the real asset. The real values are used. -
Single-asset text match CSV no longer doubles the asset URL - The stored URL was already an asset URL, and the CSV writer appended
/asset/<uuid>to it again. -
Excel reports stop at the worksheet hyperlink limit - Excel allows 65,530 hyperlinks per sheet; a report between that and the 1,048,574-row cap produced a workbook Excel "repairs" by stripping every link. Comparison URLs past the limit are written as plain text, with one warning.
-
Comma-separated values work where the help text promised them -
--name a,bonasset metadata deleteandasset metadata inference, and--folder-path a,bonfolder geometric-match,part-match,visual-matchandasset dependencies, were sent as one value containing a comma; a delete ofMaterial,Weightdeleted nothing and exited 0. -
Metadata batch CSV: the classic header is checked and short rows are reported - A file with columns in another order was applied positionally (paths and names swapped);
metadata get --format csvoutput (NAME,VALUE) was accepted and silently did nothing because every row was too short. The header must now readASSET_PATH,NAME,VALUE[,TYPE]; rows with fewer than three columns are counted and reported with line numbers; an empty NAME is an error. -
Downloaded names cannot escape the output directory - A folder or asset named
.., or a name containing a path separator, is refused instead of written outside the directory the user chose. -
One missing field no longer fails a whole page of assets -
type,createdAt,updatedAt,state,isAssemblyandmetadataare optional when deserializing, as are the page index fields. -
Ambiguous folder paths are reported - Two sibling folders that differ only by case (or not at all) both match a path; the first was used silently. A warning now names both UUIDs.
-
Batch commands exit non-zero when any item failed -
asset create-batchlogged failed files at debug level only and printed the successes with exit 0;asset metadata create-batch --continue-on-errorexited 0 with every row failed;folder upload,folder downloadandfolder thumbnailprinted "Operation completed successfully!" whatever had happened, and a missing thumbnail counted as a success. Every failure is now named on stderr and the command exits 69 (temporary failure) with a one-line count.--continue-on-errorstill decides whether the rest of the batch is attempted; it no longer decides whether the exit code tells the truth. -
folder downloadwithout--continue-on-errorstops at the first failure - It used to attempt every remaining asset anyway (each with its own 5-10 second retry sleep) and only then report an error; an expired session on file 1 of 5,000 cost 5,000 failing attempts. Remaining downloads are now cancelled and counted as "not attempted". -
Concurrent commands share one API client -
folder download,folder thumbnail,folder uploadandasset create-batchbuilt a fresh client inside every task: one credential-file read, one connection pool and one private token slot per asset, so a token expiring mid-run was renewed once per remaining asset (up to 22,000 auth-server calls in the largest known run) instead of once. Tasks now clone the caller's client, which shares the token, the renewal lock and the connection pool. -
folder uploadlists the destination folder once instead of once per file - Each task listed the entire folder (every page) to check whether its file already existed: 2,000 files into a folder of 10,000 assets was about 100,000 requests before the first upload. A failed listing was also treated as "does not exist", so--skip-existingcould re-upload on a transient error; it is now a hard error. -
folder downloadandfolder thumbnailno longer fetch every subfolder a second time - The subfolder listing already carries the name the walk needed; one request per subfolder is gone. -
Network and session failures while loading the folder hierarchy are no longer reported as "Folder not found" - The lookup swallowed them and every caller turned the empty answer into a path problem, sending users to check a path that was right. They now surface as what they are, and
--recursiveno longer suggestscache clearfor a failure that clearing the cache cannot fix. -
asset listandfolder listuse the folder cache - Both rebuilt the tenant's whole hierarchy from the API on every call (asset list --recursivetwice), andfolder list --reloadrefreshed nothing because the listing bypassed the cache. A path missing from the cached hierarchy triggers one refresh before it is reported absent, so a folder created since the cache was written is still found. -
Downloads are streamed to disk through a temporary file -
asset downloadandfolder downloadheld the whole file in memory (ten concurrent multi-gigabyte assemblies meant ten files in RAM), wrote directly to the final name so an interrupted transfer left a truncated file that looked complete, and accepted an empty body as success. The file is now streamed into<name>.partand renamed into place only when whole; an empty response is an error. The blind "sleep and retry any error, including 404" wrappers are gone: the client retries transient failures itself. -
Folder match runs count every failed task - A task that failed outside the search itself was dropped from the tally, so "Searched X of Y" and the 10% threshold used a smaller denominator. A run stopped by repeated authentication failures now always exits non-zero, even when the stop happened past the 90% mark.
-
Match runs read the configuration once - The UI base URL was loaded and parsed from disk for every match row: 1.29 million times on the largest known report.
-
"No assets to download" is a warning, not an error with exit 0 -
asset download-folderandfolder downloadprinted a red error and exited 0 for an empty folder. -
A server outage mid-run no longer produces a "complete" report with exit 0 - Every HTTP status other than 401/403/404 was wrapped in the same
Conflicterror the search endpoint uses for an asset that is not indexed, so a 500, 503 or 429 during a folder match was counted as "not searchable" - a property of the tenant, excluded from the failure threshold - and the run finished with a short report and a clean exit. Errors now carry their status: only a real 409 is "not searchable", and everything else counts as a failure that can fail the run. -
Transient failures are retried on every request, not only on DELETE - The API client built its requests on the raw HTTP client and skipped the retry helper entirely, so
PCLI2_MAX_RETRIES, the backoff, andRetry-Afterapplied to nothing the user cared about; one 503 in a 5,000-page match run aborted it. Every request now goes through one path with proactive token renewal, transient retry (connection errors, 408/429/502/503/504,Retry-Afterin seconds or HTTP-date form), and a single renew-and-retry on 401/403. Uploads re-open the file for each attempt; timed-out non-idempotent requests are still never resent. -
Exit codes follow the documented contract - Authentication failures exit 100, network failures 101, API errors 102, not-found 67, missing input files 66, an incomplete report 69, and usage errors from the argument parser 64 (previously 2). Every one of these used to exit 70 "internal software error" (or 0, see below), which made the
case $?examples in the README impossible to hit. -
Failures that exited 0 now exit non-zero -
auth getandauth expirationwith no token,env usewith no selection,env getwith no active or an unknown environment,tenant usewith no or an unknown tenant,config validatewhen validation fails, andconfig import, which was an unimplemented stub that reported success having done nothing and now says so. -
Error messages are no longer rewritten by substring - A folder named
401-serieswas reported as an authentication failure, an unknown environment name was replaced by "Resource not found", a file calledconnection-bracket.stlbecame a network error. Messages are now printed as the code produced them, with one hint chosen from what the error actually is (a login hint for authentication failures, a role hint for a 403 that survived token renewal, a connectivity hint for network errors). The OAuth error codes from the auth server keep their explanations, appended rather than substituted. -
Errors are printed once - Login and token failures were reported with remediation steps and then printed a second time on exit as a bare "Security error"; the incomplete-report error was printed twice with different counts.
-
A 404 no longer triggers a token renewal - Every not-found answer (a deleted asset, a wrong UUID) cost an auth-server call, a credential-file write and a retried request, and with a token-only login was then reported as an expired token. A 404 is now a not-found.
-
Automatic re-authentication failures say why - A rotated secret, a rate-limited auth endpoint and a DNS failure all printed the same "log in again"; the cause is now included.
-
Concurrent 401s collapse into one renewal - The check for "someone else already renewed" compared against the token current at the moment of the check rather than the token the failed request had used, so a task that read the shared slot after another task's renewal renewed again anyway.
-
Timeouts - A stalled connection used to wait out the full 30-minute request timeout even for a small JSON call, and the auth client had no timeout at all while holding the renewal lock, which hung every concurrent task with it. Connections now time out after 15 seconds and a read after 5 minutes of silence; the total timeout (
PCLI2_TIMEOUT, still 30 minutes by default) only bounds genuine long transfers. -
Every request identifies itself - The
User-Agentheader (nowPCLI2/<version>) was only sent on DELETE. -
Unsupported file types are no longer retried five times by
--override- The "Invalid path extension" upload rejection was classified as a conflict, which the override path treats as "the previous asset may still be deleting" and retries with backoff. It is now an invalid-parameter error, reported the same way whether or not the first attempt also hit an expired token. -
Thumbnail downloads on a Viewer account no longer make three auth-server calls per asset - The per-asset retry helper renewed the token up to three more times on any 403; the client's own renew-and-retry is enough.
Documentation
- The install guide had pointed at the wrong repository and listed a Rust toolchain as a prerequisite for the binary install; it now covers the shell and PowerShell installers, the Homebrew tap, the MSI,
pcli2-update, and says plainly that the crate is not on crates.io. The README no longer claims credentials are kept in the OS keychain: they live indev_credentials.jsonunder the configuration directory, owner-only on macOS and Linux.pcli2 config environment ...becamepcli2 env ...some releases ago and the README and quick start now say so;asset geometric-match-folderisfolder geometric-match; thecontextcommand istenant use/get/clear;--pathonasset listandfolder downloadis--folder-path; examples use/Home/..., the name Physna shows for the root, instead of/Root/.... Three environment variables (PCLI2_API_BASE_URL,PCLI2_UI_BASE_URL,PCLI2_AUTH_BASE_URL) that the code never read are gone from the docs, and every variable that is read is now listed in one table (README and Cross-Platform Configuration). The exit-code tables drop the code that is never emitted (68) and explain 64 for parser errors and 69 for partial batches. The documented multi-level inference propagation and theconfig importexample are removed with the features that did not exist.
Removed
- Dead dependencies -
bincode,exitcode,url,mime,tokio-utilandasync-recursion(recursion now usesBox::pin, stable since Rust 1.77);tempfilemoves to the test dependencies andtokio-testis dropped. Two integration test files that compiled to no tests (one was a single line of escaped text; the other asserted nothing) are removed and the test README describes what actually exists. The committed mdBook output underdocs/book/and the stale top-leveldocs/*.mdcopies are removed; the published site builds fromdocs/src/. The site no longer advertisescargo install pcli2.
Changed
-
CI runs
cargo auditand a minimum-supported-Rust check (1.88, set by transitive dependencies);rust-versionis declared inCargo.toml.Cargo.lockis no longer listed in.gitignore(it was tracked all along). -
asset metadata inference --recursive- The flag was accepted and never read; the documented multi-level propagation did not exist.
Changed
- Batch status lines go to stderr - The statistics reports of
folder upload,folder downloadandfolder thumbnail, and the per-file "Successfully uploaded" / "Skipping existing asset" lines, no longer share stdout with command output.