Merge remote-tracking branch 'origin/master' into stack/agent-profiles-1-seam
# Conflicts: # packages/README.i18n.yaml # packages/README.zh.md # packages/boot/app-boot/README.i18n.yaml # packages/boot/app-boot/README.zh.md # scripts/gen-cordis-catalog.ts # scripts/verify-package-readme-model-experience.ts
This commit is contained in:
@@ -166,7 +166,7 @@ function readEnvLayer(
|
||||
* Load the product CLI's inherited > invoking-directory `.env` > Harness-home
|
||||
* `.env` snapshot. The Harness home resolves before either file; both files
|
||||
* are checked before either is applied, and accepted values are materialized
|
||||
* without replacing inherited ones. The snapshot preserves source provenance.
|
||||
* without replacing inherited ones. The snapshot preserves which layer supplied each value.
|
||||
* @param binName - the diagnostic prefix on the diagnostics.
|
||||
* @param cwd - the invoking directory whose `.env` is the project layer.
|
||||
* @param warn - sink for the one-line misconfiguration diagnostics.
|
||||
@@ -336,9 +336,9 @@ function parsePatchList(
|
||||
return parsed as PatchOptions[]
|
||||
}
|
||||
|
||||
/** One overlay patch list with the label provenance comments print for it. */
|
||||
/** One overlay patch list with the source label printed in dump comments. */
|
||||
export interface ConfigDumpLayer {
|
||||
/** Source name shown in provenance comments (a file basename or path). */
|
||||
/** Source name shown in dump comments (a file basename or path). */
|
||||
label: string
|
||||
/** The layer's patches, from {@link loadOverlayPatches} / {@link loadOptionalPatches}. */
|
||||
patches: PatchOptions[]
|
||||
@@ -354,10 +354,10 @@ export interface ConfigDumpLayer {
|
||||
* sees) compose identically — then render the result as YAML in the same
|
||||
* dialect (`!!js` expressions print verbatim, unevaluated).
|
||||
*
|
||||
* Every run of rows with the same provenance is preceded by a `# ==` comment
|
||||
* Every run of rows from the same file and patch layers is preceded by a `# ==` comment
|
||||
* naming the file that contributed the rows and any layers that patched them,
|
||||
* so the output stays a loadable YAML document while showing which section
|
||||
* comes from which file. Provenance is derived from single-call prefix
|
||||
* comes from which file. The file and patch labels are derived from single-call prefix
|
||||
* snapshots (base + layers 1..k), diffed positionally: the patch algorithm
|
||||
* only rewrites rows in place or appends, so a top-level index identifies one
|
||||
* row across snapshots, and a layer whose addition changes the row (config
|
||||
@@ -373,7 +373,7 @@ export interface ConfigDumpLayer {
|
||||
* @param layers - overlay layers in application order (later wins).
|
||||
* @param warn - sink for skipped-patch diagnostics; defaults to stderr.
|
||||
* @returns the composed entry list rendered as a YAML document with
|
||||
* provenance comment separators.
|
||||
* source comment separators.
|
||||
*/
|
||||
export function renderConfigDump(
|
||||
binName: string,
|
||||
@@ -440,7 +440,7 @@ export function renderConfigDump(
|
||||
return groupedDump(composed, provenance)
|
||||
}
|
||||
|
||||
/** Render the composed rows grouped under one provenance comment per contiguous run. */
|
||||
/** Render the composed rows grouped under one source-and-patches comment per contiguous run. */
|
||||
function groupedDump(
|
||||
composed: readonly unknown[],
|
||||
provenance: readonly { origin: string; patchedBy: string[] }[],
|
||||
@@ -456,7 +456,7 @@ function groupedDump(
|
||||
}
|
||||
for (let index = 0; index < composed.length; index += 1) {
|
||||
const record = provenance[index]
|
||||
/* v8 ignore next -- provenance is index-aligned with composed by construction */
|
||||
/* v8 ignore next -- this array is index-aligned with composed by construction */
|
||||
if (record === undefined) continue
|
||||
const label = record.patchedBy.length === 0
|
||||
? record.origin
|
||||
|
||||
@@ -114,6 +114,11 @@ export function resolveProfileDir(name: string, home: string = resolveDshHome())
|
||||
/** The shipped profile templates auto-initialized on first use, by name. */
|
||||
export const PROFILE_TEMPLATES: Record<string, readonly string[]> = {
|
||||
web: ['@deepseek-ai/dsh-base', '@deepseek-ai/dsh-web-app'],
|
||||
headless: ['@deepseek-ai/dsh-base', '@deepseek-ai/dsh-headless'],
|
||||
}
|
||||
|
||||
/** Installation-owned bundle tuples normalized to the shipped template. */
|
||||
const INSTALLATION_OWNED_PROFILE_TUPLES: Record<string, readonly string[]> = {
|
||||
headless: ['@deepseek-ai/dsh-base', '@deepseek-ai/dsh-web-app', '@deepseek-ai/dsh-headless'],
|
||||
}
|
||||
|
||||
@@ -203,10 +208,10 @@ function ensureSymlink(link: string, target: string): void {
|
||||
* directory after the profile's own `node_modules`, so every in-box plugin
|
||||
* resolves without pnpm ever managing it — the exact "bundles come from the
|
||||
* installation" contract. The closure (not just direct dependencies) is
|
||||
* required for out-of-tree plugins: their peer dependencies name seam
|
||||
* packages (`dsh-compact`, `dsh-invariants`, ...) that the app reaches only
|
||||
* through its implementation packages. Symlinked packages resolve their own
|
||||
* dependencies from their real directories (Node's default
|
||||
* required for out-of-tree plugins: their peer dependencies name Service
|
||||
* Definition packages (`dsh-compact`, `dsh-invariants`, ...) that the app
|
||||
* reaches only through its Service provider packages. Symlinked packages
|
||||
* resolve their own dependencies from their real directories (Node's default
|
||||
* symlink-following), so each package needs only its one flat link.
|
||||
* Idempotent: correct links are kept and moved installations are
|
||||
* re-pointed; a stale link to a vanished package stays until its name is
|
||||
@@ -226,7 +231,7 @@ export function healProfilesModuleFallback(installAnchor: string, home: string =
|
||||
// map itself (first resolution wins, matching Node's own nearest-wins).
|
||||
const queue: { anchor: string; manifest: ProfileManifest }[] = [{ anchor: installAnchor, manifest: appManifest }]
|
||||
for (let next = queue.shift(); next !== undefined; next = queue.shift()) {
|
||||
// Peer dependencies participate: seam packages (dsh-subprocess,
|
||||
// Peer dependencies participate: Service Definition packages (dsh-subprocess,
|
||||
// dsh-compact, ...) are peers of their implementations, never plain
|
||||
// dependencies, yet out-of-tree plugins import them directly.
|
||||
/* v8 ignore next -- a real app manifest always declares dependencies */
|
||||
@@ -279,6 +284,32 @@ export function writeProfileManifest(dir: string, manifest: ProfileManifest): vo
|
||||
writeFileSync(join(dir, 'package.json'), JSON.stringify(manifest, undefined, 2) + '\n')
|
||||
}
|
||||
|
||||
/** Return whether two bundle lists have the same values in the same order. */
|
||||
function sameBundles(left: readonly string[], right: readonly string[]): boolean {
|
||||
return left.length === right.length && left.every((value, index) => value === right[index])
|
||||
}
|
||||
|
||||
/**
|
||||
* Normalize an exact installation-owned bundle tuple to its shipped template
|
||||
* while preserving every other manifest field. Any other list is user-owned.
|
||||
*/
|
||||
function normalizeShippedProfile(name: string, dir: string, manifest: ProfileManifest): ProfileManifest {
|
||||
const installationOwned = INSTALLATION_OWNED_PROFILE_TUPLES[name]
|
||||
const current = PROFILE_TEMPLATES[name]
|
||||
const bundles = manifest.dsh?.profile?.bundles
|
||||
if (installationOwned === undefined || current === undefined || bundles === undefined
|
||||
|| !sameBundles(bundles, installationOwned)) return manifest
|
||||
const normalized: ProfileManifest = {
|
||||
...manifest,
|
||||
dsh: {
|
||||
...manifest.dsh,
|
||||
profile: { ...manifest.dsh?.profile, bundles: [...current] },
|
||||
},
|
||||
}
|
||||
writeProfileManifest(dir, normalized)
|
||||
return normalized
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve a package's root directory from one anchor without depending on the
|
||||
* package exporting `./package.json` (`require.resolve` would need that):
|
||||
@@ -350,7 +381,7 @@ export function loadProfile(
|
||||
}
|
||||
initProfile(dir, template)
|
||||
}
|
||||
const manifest = readProfileManifest(binName, dir)
|
||||
const manifest = normalizeShippedProfile(name, dir, readProfileManifest(binName, dir))
|
||||
// A hand-written profile manifest may omit the dsh section entirely.
|
||||
const bundles = manifest.dsh?.profile?.bundles ?? []
|
||||
const layers = bundles.map((packageName): ProfileLayer => {
|
||||
|
||||
Reference in New Issue
Block a user