Repository navigation
Commit bc254be
fix(cloud-connection,cli): the install-local ledger follows OS_HOME, so os dev --fresh starts without earlier installs (#30351)
<!-- claude-projects-attribution: {"github_login":"os-bill"} -->
_Requested by **Bill** · [project
thread](https://claude.ai/code/project/chan_016iMXWSZDzqWRWvWzdNo8wg?thread=cmsg_016iMXWSZDzqWRWvWzdNo8wg8wW3DTJphDWvE6eMmCmuPe)_
domain:server#3 · os-bill · session_01Utp6CY4LWqfjLCYqpZhNCs
Fixes #30217
Clause-②: yes (widening)
Metadata-protocol: none
## 维护者速读
- **改了什么:** 本地安装账本(install-local 缓存目录)不再固定在工作目录:设了 `OS_HOME` 就放
`$OS_HOME/installed-packages`,没设仍在
`CWD/.objectstack/installed-packages`。旧位置的已有安装在新目录第一次出现时复制一次过去(旧文件不删),`os
dev --fresh` 不复制;`os doctor` 读运行时同一个目录。
- **为什么改:** `os dev --fresh` 或换一个 `OS_HOME`
的第二个实例,库是新的,却把上一次装过的包又加载回来,测试互相污染。
- **风险与回滚:** 不设 `OS_HOME` 的 `os dev`、项目模式的 `os
start`(`OS_HOME=CWD/.objectstack`)路径不变;设了别的 `OS_HOME`
的部署首次启动会复制旧账本并打一行警告。复制失败打 error,下次启动重试,旧文件始终不动。回滚即撤 PR。
- **先看:**
`packages/cloud-connection/src/local-manifest-source.ts`(`resolveInstalledPackagesDir`、`adoptLegacyEntries`),`marketplace-install-local-plugin.ts`
的 `adoptLegacyLedger`,`packages/cli/src/commands/doctor.ts` 的
`readInstalledPackageEntries`。
- **两分钟验证:** `pnpm --filter @objectstack/cloud-connection exec vitest
run src/marketplace-install-local-storage-dir.test.ts`,预期 10 个测试全过(含
`--fresh` 不复制、设 `OS_HOME` 复制一次、卸载后不复活)。
- **需要你做的:** 无,席位落地。
## What changes
**`@objectstack/cloud-connection`**
- `resolveInstalledPackagesDir({ env?, cwd? })` is the one answer to
where the ledger lives: `$OS_HOME/installed-packages` when `OS_HOME` is
set (a leading `~` expanded the way the home resolver does),
`CWD/.objectstack/installed-packages` when it is not. It also returns
`legacyDir`: the working-directory ledger, whenever the answer moved
away from it, else `null`.
- Unset stays in the project on purpose. `os dev` without `OS_HOME`
keeps its database in the project, and the ledger has to sit beside the
database whose sample-data rows its entries describe; `~/.objectstack`
would make one ledger serve every project on the machine. `os start` in
project mode sets `OS_HOME` to `CWD/.objectstack`, so its ledger is the
same directory as before.
- `LocalManifestSource()` with no `storageDir` uses that answer and
keeps `legacyDir`; an explicit `storageDir` adopts nothing.
- `LocalManifestSource.adoptLegacyEntries()` copies the legacy
directory's `.json` files byte for byte into the ledger directory, once:
only while the ledger directory does not exist yet. The copy goes to a
sibling staging directory that is renamed into place, so a failure
leaves no half-made ledger and the next boot copies again. The legacy
files are left in place, so an `OS_HOME` pointed at a throwaway
directory never takes a project's installs with it, and they are not
read again, so an uninstall in the new place sticks. `list()` and
`read()` never adopt: a reader such as `os doctor` must not write.
- `MarketplaceInstallLocalPlugin.rehydrate` calls it before the ledger
read. A copy is a `warn` naming both directories and asking for the old
one to be removed; a failed copy is an `error` (the old ledger lists
packages this runtime does not load, and the boot looks normal
otherwise), naming both directories and the fix.
- Under `os dev --fresh` nothing is adopted: the dev parent names its
ephemeral home on the internal channel `OS_INTERNAL_FRESH_HOME`, and the
resolver skips the adoption only when that value is the current
`OS_HOME`, so an inherited copy speaks for no other run. The channel is
between `os dev` and its serve child only; it is not an operator setting
and is not documented as one.
- Only the constructor/`storageDir` doc and the adoption before
`rehydrate`'s read change in `marketplace-install-local-plugin.ts`;
`runInlineSeed` and the install/uninstall doors are untouched.
**`@objectstack/cli`**
- `os dev` sets `OS_INTERNAL_FRESH_HOME` on its serve child under
`--fresh` and removes an inherited copy otherwise
(`applyFreshHomeChannel`).
- `os doctor` takes the ledger directory from
`resolveInstalledPackagesDir` (under its `.env*` overlay, as `os serve`
loads that cascade before the ledger resolves `OS_HOME`), instead of
joining `cwd` with `DEFAULT_INSTALLED_PACKAGES_DIR`. Without this, with
`OS_HOME` set, doctor and the runtime would read two directories and
neither would say so. `OS_HOME` joins `DOCTOR_ENV_INPUTS`, so its
provenance is reported. A `@objectstack/cloud-connection` without the
resolver is the existing "does not declare the ledger directory" row,
now naming `resolveInstalledPackagesDir`.
- `serve.ts` is unchanged: its two `new
MarketplaceInstallLocalPlugin(...)` sites pass no `storageDir`, and the
default now resolves the right directory.
**The widening.** New exports `resolveInstalledPackagesDir`,
`INSTALLED_PACKAGES_HOME_SUBDIR`, `FRESH_HOME_CHANNEL_ENV` and the types
`InstalledPackagesLocation` / `LegacyLedgerAdoption`; new members
`LocalManifestSource.legacyDir` and `adoptLegacyEntries()`. Nothing is
removed or narrowed; nothing an author writes changes.
## Tests
- `local-manifest-source.test.ts`: two new describes. The resolver's
table (unset and blank, set, `~`, project-mode `os start`, the fresh
channel for its own home and for another), the default constructor;
`adoptLegacyEntries` copies `.json` byte for byte (an unreadable entry
too, reported by `list()` from its new place), leaves the legacy files,
leaves no staging directory, adopts nothing once the directory exists,
answers `null` without touching the disk, and on a failed copy throws
and leaves no ledger directory.
- `marketplace-install-local-storage-dir.test.ts`: a new describe
booting the plugin with no `storageDir` — installs land under
`$OS_HOME/installed-packages`; a second instance under another `OS_HOME`
does not rehydrate the first one's install; the cwd ledger is adopted
once and rehydrated, the old file kept, and an uninstall in the new
place is not undone on the next boot; a fresh home adopts nothing; a
fresh channel naming another home does not stop the adoption; with
`OS_HOME` unset the cwd ledger is read with no adoption; a failed copy
logs the error naming both directories. The existing cwd-default case
now pins `OS_HOME` unset.
- `doctor-ledger-read-failure.test.ts`: with `OS_HOME` set doctor
reports the entry under `$OS_HOME/installed-packages` and nothing from a
stale cwd ledger; the `#6643` case computes its expectation from the
resolver.
- `doctor-ledger-dir-authority.test.ts`: the authority is the resolver
function (`not a function`).
- `dev-fresh-home-channel.test.ts` (new): the CLI's spelling equals the
package's; set under `--fresh`, removed otherwise.
## Acceptance notes
- **A brand-new non-fresh home adopts the pre-upgrade ledger once.** Two
instances started under two new `OS_HOME`s (neither `--fresh`) after
this upgrade each copy the working-directory entries that existed before
it. Installs made after the upgrade never leak; the old entries stop
leaking once the old directory is removed, which the boot warning asks
for.
- **Copy, not move.** A move would converge sooner, but any `OS_HOME`
pointed at a temporary directory (a test harness, a one-off run) would
then take the project's installs into a directory that gets deleted.
- **`os doctor` before the first boot.** With `OS_HOME` set and the new
directory not created yet, doctor reads the new (absent) directory and
reports nothing installed; the next boot adopts the old entries. Doctor
does not write, by design.
## Verification
Real boots of `examples/app-todo` through `os dev --no-watch --log-level
info` with one pre-seeded working-directory ledger entry
(`com.probe.ledger`, standing for an earlier install), reading the
plugin's boot lines and the ledger directories afterwards. Before =
`origin/main` sources of the five changed source files restored and both
packages rebuilt; after = this branch; ablation = this branch with the
fresh-channel return in `resolveInstalledPackagesDir` disabled through
`scripts/ablation-replace.mjs`, `ablation-dist-preflight.mjs` confirming
the marker in 2 built files, then restored (blob equal to HEAD, `git
diff HEAD` empty, `--absent` over 6 built files).
<details><summary>Before / after / ablation, three scenarios</summary>
| Scenario | Before (`main`) | After (this PR) | Ablation (fresh guard
off) |
|---|---|---|---|
| `OS_HOME` unset | `rehydrated com.probe.ledger@1.0.0`, storage
`CWD/.objectstack/installed-packages` | same | same |
| `os dev --fresh` | `rehydrated com.probe.ledger@1.0.0`, storage
`CWD/.objectstack/installed-packages` — the defect | no rehydrate,
storage `/tmp/objectstack-dev-…/installed-packages`, cwd ledger
untouched | `copied 1 installed package(s) … into
/tmp/objectstack-dev-…/installed-packages`, `rehydrated
com.probe.ledger@1.0.0` — the defect is back |
| `OS_HOME=/tmp/os30217-home-…` | `rehydrated`, storage `CWD/…`, nothing
under the home | `copied 1 installed package(s) from CWD/… into
HOME_DIR/installed-packages`, `rehydrated`, storage
`HOME_DIR/installed-packages`; cwd file kept | same as after |
</details>
Tests and gates, on head `0947c7af` (after merging `origin/main` at
`c1240a38`; the closure rebuilt after the merge). The final head
`299ca6dd` changes only the changeset's wording (path spellings without
angle brackets); the nine changeset- and text-reading families were
re-run there, all exit 0: `check-adr-0087-registration`,
`check-changeset-no-major`, `check-empty-changeset`,
`check:changeset-gate-self-tests`, `check:doc-authoring`,
`check:issue-citations` (both forms), `check:nul-bytes`,
`check-closing-keyword-parity`.
- `pnpm --filter @objectstack/cloud-connection test`: Test Files 70
passed (70), Tests 863 passed (863); `typecheck` (both programs) exit 0.
`marketplace-install-local-storage-dir.test.ts` re-run after the last
commit: 10/10.
- `pnpm --filter @objectstack/cli exec vitest run --project unit`: 382
of 385 files passed on the first head; the two `published-subpath-*`
pins failed only because `packages/cli` was not built yet (their own
preflight says so) and pass after the build, and `doctor-env-provenance`
named `OS_HOME` as an undeclared doctor input, which the second commit
declares. Re-run after the fixes: every `doctor*` file, 14 files / 212
tests, plus `dev-fresh-home-channel` and both `published-subpath` pins,
all passed; `pnpm --filter @objectstack/cli typecheck` exit 0. The
`integration` tier is CI's.
- `eslint --no-inline-config` over the ten changed source and test
files: 0 problems.
- `node scripts/pm/dispatch-gates.mjs --commands` derives 63 commands;
each was run with its exit code captured before any pipe, and `--ran`
reconciles 63 derived, 61 run, 2 NOT MEASURED, 0 unrun.
<details><summary>The 63 derived commands and their exit codes</summary>
```text
node scripts/check-adr-0087-registration.mjs --base origin/main :: exit 0
node scripts/check-adr-0087-registration.mjs --self-test :: exit 0
node scripts/check-changeset-no-major.mjs --base origin/main :: exit 0
node scripts/check-closing-keyword-parity.mjs :: exit 0
node scripts/check-closing-keyword-parity.mjs --self-test :: exit 0
node scripts/check-comment-mask-adoption.mjs :: exit 0
node scripts/check-comment-mask-adoption.mjs --self-test :: exit 0
node scripts/check-comment-mask-corpus.mjs :: exit 0
node scripts/check-dts-emitted.mjs --self-test :: exit 0
node scripts/check-empty-changeset.mjs --base origin/main :: exit 0
node scripts/check-empty-changeset.mjs --self-test :: exit 0
node scripts/check-issue-citations.mjs :: exit 0
node scripts/check-plugin-teardown-shape.mjs :: exit 0
node scripts/check-plugin-teardown-shape.mjs --self-test :: exit 0
node scripts/ci/assert-turbo-remote-cache.mjs --self-test :: exit 0
node scripts/ci/plan-test-core-shards.mjs --self-test :: exit 0
node scripts/ci/prune-turbo-cache.mjs --self-test :: exit 0
node scripts/docs-audit/check-affected-docs.mjs :: exit 0
node scripts/docs-audit/check-drift-comment.mjs :: exit 0
node scripts/pm/release-rehearsal-clone.mjs --self-test :: exit 0
node scripts/release-pending-publish.mjs --self-test :: exit 0
pnpm --filter @objectstack/spec run check:duration-unit-keys :: exit 0
pnpm check:changeset-gate-self-tests :: exit 0
pnpm check:ci-filter-parity :: exit 0
pnpm check:closed-ceremony-text :: exit 0
pnpm check:cross-package-test-inputs :: exit 0
pnpm check:dispatcher-error-vocabulary :: exit 0
pnpm check:doc-authoring :: exit 0
pnpm check:driver-memory-census :: exit 0
pnpm check:dts-closure :: exit 0
pnpm check:dual-build-cjs-loads :: exit 3
pnpm check:durability-log-level :: exit 0
pnpm check:engine-double-contract :: exit 0
pnpm check:error-status-conformance :: exit 0
pnpm check:gitlink-declared :: exit 0
pnpm check:i18n :: exit 0
pnpm check:i18n-coverage :: exit 3
pnpm check:i18n-walk-parity :: exit 0
pnpm check:issue-citations :: exit 0
pnpm check:keyed-text-bounds :: exit 0
pnpm check:lean-entry-closure :: exit 0
pnpm check:logger-receiver-detach :: exit 0
pnpm check:nul-bytes :: exit 0
pnpm check:objectql-double-limit :: exit 0
pnpm check:org-identifier :: exit 0
pnpm check:page-declaration-shape :: exit 0
pnpm check:platform-object-tenancy-census :: exit 0
pnpm check:published-files :: exit 0
pnpm check:query-options-erasure :: exit 0
pnpm check:refd-timer-probe :: exit 0
pnpm check:registry-log-declared :: exit 0
pnpm check:rest-log-spy-declared :: exit 0
pnpm check:route-envelope :: exit 0
pnpm check:slot-lookup :: exit 0
pnpm check:sourcemap-no-sources-content :: exit 0
pnpm check:system-context-census :: exit 0
pnpm check:test-source-alias :: exit 0
pnpm check:tier-file-adoption :: exit 0
pnpm check:type-check-coverage :: exit 0
pnpm check:type-check-debt :: exit 0
pnpm check:undeclared-dep-imports :: exit 0
pnpm check:watch-hint-literal :: exit 0
pnpm check:where-matcher :: exit 0
```
NOT MEASURED, both prerequisite refusals (exit 3), left to CI's `Lint &
Repo Gates`: `check:dual-build-cjs-loads` (packages outside this card's
closure have no `dist/` in this worktree, e.g. `@objectstack/studio`,
`@objectstack/client-react`) and `check:i18n-coverage`
(`examples/app-showcase`'s `@objectstack/connector-slack` has no build
output here).
</details>
Dogfood: n/a — no `!` in the title and no `(narrowing)` arm.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
https://claude.ai/code/session_01Utp6CY4LWqfjLCYqpZhNCs
---
_Generated by [Claude
Code](https://claude.ai/code/session_01Utp6CY4LWqfjLCYqpZhNCs)_
---------
Co-authored-by: Claude <noreply@anthropic.com>1 parent fdad178 commit bc254be
13 files changed
Lines changed: 651 additions & 72 deletions
File tree
- .changeset
- packages
- cli
- src/commands
- test
- cloud-connection/src
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
251 | 251 | | |
252 | 252 | | |
253 | 253 | | |
| 254 | + | |
| 255 | + | |
| 256 | + | |
| 257 | + | |
| 258 | + | |
| 259 | + | |
| 260 | + | |
| 261 | + | |
| 262 | + | |
| 263 | + | |
| 264 | + | |
| 265 | + | |
| 266 | + | |
| 267 | + | |
| 268 | + | |
| 269 | + | |
| 270 | + | |
| 271 | + | |
| 272 | + | |
| 273 | + | |
| 274 | + | |
| 275 | + | |
254 | 276 | | |
255 | 277 | | |
256 | 278 | | |
| |||
676 | 698 | | |
677 | 699 | | |
678 | 700 | | |
| 701 | + | |
| 702 | + | |
| 703 | + | |
679 | 704 | | |
680 | 705 | | |
681 | 706 | | |
| |||
810 | 835 | | |
811 | 836 | | |
812 | 837 | | |
| 838 | + | |
813 | 839 | | |
814 | 840 | | |
815 | 841 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
5 | 5 | | |
6 | 6 | | |
7 | 7 | | |
8 | | - | |
| 8 | + | |
| 9 | + | |
9 | 10 | | |
10 | 11 | | |
11 | 12 | | |
| |||
114 | 115 | | |
115 | 116 | | |
116 | 117 | | |
117 | | - | |
| 118 | + | |
118 | 119 | | |
119 | 120 | | |
120 | | - | |
| 121 | + | |
121 | 122 | | |
122 | 123 | | |
123 | 124 | | |
| |||
136 | 137 | | |
137 | 138 | | |
138 | 139 | | |
139 | | - | |
| 140 | + | |
140 | 141 | | |
141 | 142 | | |
142 | 143 | | |
| |||
145 | 146 | | |
146 | 147 | | |
147 | 148 | | |
148 | | - | |
| 149 | + | |
149 | 150 | | |
150 | 151 | | |
151 | 152 | | |
| |||
182 | 183 | | |
183 | 184 | | |
184 | 185 | | |
185 | | - | |
| 186 | + | |
186 | 187 | | |
187 | 188 | | |
188 | 189 | | |
| |||
271 | 272 | | |
272 | 273 | | |
273 | 274 | | |
274 | | - | |
| 275 | + | |
275 | 276 | | |
276 | 277 | | |
277 | 278 | | |
| |||
312 | 313 | | |
313 | 314 | | |
314 | 315 | | |
315 | | - | |
| 316 | + | |
316 | 317 | | |
317 | 318 | | |
318 | | - | |
319 | | - | |
320 | | - | |
321 | | - | |
| 319 | + | |
| 320 | + | |
| 321 | + | |
| 322 | + | |
322 | 323 | | |
323 | 324 | | |
324 | 325 | | |
| |||
333 | 334 | | |
334 | 335 | | |
335 | 336 | | |
336 | | - | |
| 337 | + | |
337 | 338 | | |
338 | 339 | | |
339 | 340 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
479 | 479 | | |
480 | 480 | | |
481 | 481 | | |
482 | | - | |
| 482 | + | |
483 | 483 | | |
484 | 484 | | |
485 | | - | |
| 485 | + | |
486 | 486 | | |
487 | 487 | | |
488 | 488 | | |
| |||
673 | 673 | | |
674 | 674 | | |
675 | 675 | | |
| 676 | + | |
| 677 | + | |
| 678 | + | |
| 679 | + | |
| 680 | + | |
| 681 | + | |
| 682 | + | |
| 683 | + | |
| 684 | + | |
| 685 | + | |
| 686 | + | |
| 687 | + | |
| 688 | + | |
| 689 | + | |
| 690 | + | |
| 691 | + | |
| 692 | + | |
| 693 | + | |
| 694 | + | |
| 695 | + | |
| 696 | + | |
| 697 | + | |
| 698 | + | |
| 699 | + | |
| 700 | + | |
| 701 | + | |
| 702 | + | |
| 703 | + | |
676 | 704 | | |
677 | 705 | | |
678 | 706 | | |
| |||
0 commit comments