mirror of
https://github.com/hansjone/netxops.git
synced 2026-10-20 08:50:46 +08:00
Restore Desktop Netx Ops preset via dual-path install.
Compose from host standard for web directories and sync a correctly indented declarative insert into the desktop profile patch (0.1.48). Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
parent
2d86f55bf9
commit
7a289cf409
7 changed files with 674 additions and 109 deletions
|
|
@ -1,11 +1,14 @@
|
|||
/**
|
||||
* Install Netx Ops as a user agent-preset by composing from the host's shipped
|
||||
* `standard` preset + Netx Ops overlays (persona / tools / skills).
|
||||
* Install Netx Ops agent preset for both DSH host styles:
|
||||
*
|
||||
* DSH has no preset inheritance (`extends`). Copying `standard` at install time
|
||||
* is the supported way to stay aligned with the running Harness version without
|
||||
* embedding a full standard composition (or `@deepseek-ai/dsh-agent-preset`)
|
||||
* inside this package.
|
||||
* - Web / source CLI (`@deepseek-ai/dsh-agent-presets`): directory under
|
||||
* `$DSH_HOME/.agent-presets/netxops`.
|
||||
* - Desktop 0.2 (`@deepseek-ai/dsh-agent-preset` + registry): declarative
|
||||
* insert row in `presets/netxops.preset.patch.yml`, synced into the desktop
|
||||
* profile `cordis.patch.yml` on apply (not the package bundle — keeps web safe).
|
||||
*
|
||||
* Both are composed from the host shipped `standard` + Netx Ops overlays
|
||||
* (DSH has no preset `extends`).
|
||||
*/
|
||||
|
||||
import {
|
||||
|
|
@ -24,6 +27,9 @@ import type { Context } from '@deepseek-ai/cordis'
|
|||
|
||||
export const NETXOPS_PRESET_ID = 'netxops'
|
||||
|
||||
const DESKTOP_PATCH_BEGIN = '# BEGIN dsh-netxops-preset (managed)'
|
||||
const DESKTOP_PATCH_END = '# END dsh-netxops-preset (managed)'
|
||||
|
||||
function packageRoot(): string {
|
||||
return join(dirname(fileURLToPath(import.meta.url)), '..')
|
||||
}
|
||||
|
|
@ -41,6 +47,8 @@ export function resolveStandardPresetDir(): string | null {
|
|||
join(dshHome, 'profiles', 'node_modules', '@deepseek-ai', 'dsh-agent-presets', 'presets', 'standard'),
|
||||
join(dshHome, 'profiles', 'web', 'node_modules', '@deepseek-ai', 'dsh-agent-presets', 'presets', 'standard'),
|
||||
join(dshHome, 'profiles', 'desktop', 'node_modules', '@deepseek-ai', 'dsh-agent-presets', 'presets', 'standard'),
|
||||
// Source checkout used by `dsh` CLI / local Harness.
|
||||
join(dirname(fileURLToPath(import.meta.url)), '..', '..', 'DeepSeekHarness', 'packages', 'preset', 'agent-presets', 'presets', 'standard'),
|
||||
]
|
||||
for (const dir of candidates) {
|
||||
if (existsSync(join(dir, 'agent.cordis.yml'))) return dir
|
||||
|
|
@ -89,7 +97,6 @@ function ensureNetxopsToolsRow(cordis: string): string {
|
|||
if (/^- id: netxops-tools$/m.test(cordis)) return cordis
|
||||
const row = '- id: netxops-tools\n name: dsh-netxops/tools'
|
||||
if (/^- id: tool-skill$/m.test(cordis)) {
|
||||
// Insert immediately after the tool-skill entry.
|
||||
const lines = cordis.replace(/\r\n/g, '\n').split('\n')
|
||||
const start = lines.findIndex((line) => line === '- id: tool-skill')
|
||||
let end = start + 1
|
||||
|
|
@ -101,7 +108,6 @@ function ensureNetxopsToolsRow(cordis: string): string {
|
|||
}
|
||||
|
||||
function personaEntryFromMarkdown(md: string): string {
|
||||
// Drop the human-facing H1 (`# Netx Ops 人设…`); the cordis prefix starts at the body.
|
||||
const body = md.replace(/\r\n/g, '\n').replace(/^#[^\n]*\n+/u, '').replace(/\n+$/u, '')
|
||||
const indented = body.split('\n').map((line) => ` ${line}`).join('\n')
|
||||
return [
|
||||
|
|
@ -109,70 +115,179 @@ function personaEntryFromMarkdown(md: string): string {
|
|||
" name: '@deepseek-ai/dsh-persona'",
|
||||
' config:',
|
||||
' suffix: Your working directory is {{cwd}}.',
|
||||
' # Body from presets/netxops/PERSONA.md (composed onto standard at install).',
|
||||
' # Body from presets/netxops/PERSONA.md (composed onto standard).',
|
||||
' prefix: |-',
|
||||
indented,
|
||||
].join('\n')
|
||||
}
|
||||
|
||||
/**
|
||||
* Build `~/.dsh/.agent-presets/netxops` from the host `standard` preset plus
|
||||
* Netx Ops persona / tools / skills. Safe to call on every Host apply.
|
||||
*/
|
||||
export function ensureAgentPresetInstalled(logger: Context['logger']): void {
|
||||
export interface NetxopsPresetArtifacts {
|
||||
standardDir: string
|
||||
name: string
|
||||
description: string
|
||||
order: string
|
||||
/** Full agent.cordis.yml body (standard + overlays). */
|
||||
agentCordis: string
|
||||
/** Declarative `@deepseek-ai/dsh-agent-preset` patch YAML. */
|
||||
declarativePatch: string
|
||||
}
|
||||
|
||||
function readPresetMeta(metaPath: string): { name: string; description: string; order: string } {
|
||||
const metaText = readFileSync(metaPath, 'utf8')
|
||||
return {
|
||||
name: (metaText.match(/^name:\s*(.+)$/m) || [])[1]?.trim() || 'Netx Ops',
|
||||
description: (metaText.match(/^description:\s*(.+)$/m) || [])[1]?.trim() || '',
|
||||
order: (metaText.match(/^order:\s*(\d+)/m) || [])[1] || '50',
|
||||
}
|
||||
}
|
||||
|
||||
/** Compose standard + Netx Ops overlays into directory + declarative artifacts. */
|
||||
export function composeNetxopsPresetArtifacts(): NetxopsPresetArtifacts {
|
||||
const overlayRoot = join(packageRoot(), 'presets', NETXOPS_PRESET_ID)
|
||||
const personaPath = join(overlayRoot, 'PERSONA.md')
|
||||
const metaPath = join(overlayRoot, 'preset.yml')
|
||||
if (!existsSync(personaPath) || !existsSync(metaPath)) {
|
||||
logger.warn('netxops: bundled preset overlay missing under %s — skip install', overlayRoot)
|
||||
return
|
||||
throw new Error(`bundled preset overlay missing under ${overlayRoot}`)
|
||||
}
|
||||
|
||||
const standardDir = resolveStandardPresetDir()
|
||||
if (standardDir === null) {
|
||||
logger.warn(
|
||||
'netxops: cannot find shipped standard preset (@deepseek-ai/dsh-agent-presets) — skip user-preset install',
|
||||
)
|
||||
return
|
||||
throw new Error('cannot find shipped standard preset (@deepseek-ai/dsh-agent-presets)')
|
||||
}
|
||||
const meta = readPresetMeta(metaPath)
|
||||
let agentCordis = replaceTopLevelEntry(
|
||||
readFileSync(join(standardDir, 'agent.cordis.yml'), 'utf8'),
|
||||
'persona',
|
||||
personaEntryFromMarkdown(readFileSync(personaPath, 'utf8')),
|
||||
)
|
||||
agentCordis = ensureNetxopsToolsRow(agentCordis)
|
||||
if (!agentCordis.endsWith('\n')) agentCordis += '\n'
|
||||
|
||||
// Match dsh-web-app/presets/*.patch.yml: plugins items are indented 10 spaces
|
||||
// under ` plugins:` (8). Same-level `- id:` makes YAML treat them as
|
||||
// siblings of `plugins` and the declaration fails to mount.
|
||||
const pluginsBody = agentCordis
|
||||
.replace(/\r\n/g, '\n')
|
||||
.split('\n')
|
||||
.map((line) => (line.length === 0 ? '' : ` ${line}`))
|
||||
.join('\n')
|
||||
|
||||
const declarativePatch = `# Declarative Netx Ops preset for Desktop 0.2 (@deepseek-ai/dsh-agent-preset).
|
||||
# Generated from host \`standard\` + presets/netxops overlays — do not hand-edit.
|
||||
# Regenerate: bun run scripts/gen-preset-patch.mjs
|
||||
|
||||
- insert:
|
||||
- id: preset-netxops
|
||||
name: '@deepseek-ai/dsh-agent-preset'
|
||||
config:
|
||||
id: netxops
|
||||
order: ${meta.order}
|
||||
name: ${JSON.stringify(meta.name)}
|
||||
description: ${JSON.stringify(meta.description)}
|
||||
plugins:
|
||||
${pluginsBody}
|
||||
`
|
||||
return { standardDir, ...meta, agentCordis, declarativePatch }
|
||||
}
|
||||
|
||||
function installDirectoryPreset(artifacts: NetxopsPresetArtifacts, logger: Context['logger']): void {
|
||||
const overlayRoot = join(packageRoot(), 'presets', NETXOPS_PRESET_ID)
|
||||
const destParent = join(resolveDshHome(), '.agent-presets')
|
||||
const dest = join(destParent, NETXOPS_PRESET_ID)
|
||||
try {
|
||||
mkdirSync(destParent, { recursive: true })
|
||||
if (existsSync(dest)) rmSync(dest, { recursive: true, force: true })
|
||||
// Real copy: DSH discovery skips Windows junctions.
|
||||
cpSync(standardDir, dest, { recursive: true })
|
||||
mkdirSync(destParent, { recursive: true })
|
||||
if (existsSync(dest)) rmSync(dest, { recursive: true, force: true })
|
||||
cpSync(artifacts.standardDir, dest, { recursive: true })
|
||||
writeFileSync(join(dest, 'preset.yml'), readFileSync(join(overlayRoot, 'preset.yml'), 'utf8'))
|
||||
writeFileSync(join(dest, 'PERSONA.md'), readFileSync(join(overlayRoot, 'PERSONA.md'), 'utf8'))
|
||||
writeFileSync(join(dest, 'agent.cordis.yml'), artifacts.agentCordis)
|
||||
const skillsSrc = join(overlayRoot, 'skills')
|
||||
if (existsSync(skillsSrc)) cpSync(skillsSrc, join(dest, 'skills'), { recursive: true })
|
||||
writeFileSync(
|
||||
join(dest, '.dsh-netxops-managed'),
|
||||
[
|
||||
'composed-from: standard',
|
||||
`standard-source: ${artifacts.standardDir}`,
|
||||
`at: ${new Date().toISOString()}`,
|
||||
'',
|
||||
].join('\n'),
|
||||
'utf8',
|
||||
)
|
||||
logger.info('netxops: directory preset composed from standard → %s', dest)
|
||||
}
|
||||
|
||||
writeFileSync(join(dest, 'preset.yml'), readFileSync(metaPath, 'utf8'))
|
||||
writeFileSync(join(dest, 'PERSONA.md'), readFileSync(personaPath, 'utf8'))
|
||||
/**
|
||||
* Sync declarative preset into Desktop's cordis.patch.yml.
|
||||
* Desktop 0.2 profile roots are empty `[]` (bundles + patch compose the tree),
|
||||
* so we key off the profile name — never web's directory roster.
|
||||
*/
|
||||
function syncDeclarativeIntoProfilePatch(
|
||||
profileName: string,
|
||||
declarativePatch: string,
|
||||
logger: Context['logger'],
|
||||
): void {
|
||||
if (profileName !== 'desktop') return
|
||||
const profileDir = join(resolveDshHome(), 'profiles', profileName)
|
||||
const patchPath = join(profileDir, 'cordis.patch.yml')
|
||||
if (!existsSync(profileDir)) return
|
||||
|
||||
const standardCordis = readFileSync(join(standardDir, 'agent.cordis.yml'), 'utf8')
|
||||
let composed = replaceTopLevelEntry(
|
||||
standardCordis,
|
||||
'persona',
|
||||
personaEntryFromMarkdown(readFileSync(personaPath, 'utf8')),
|
||||
)
|
||||
composed = ensureNetxopsToolsRow(composed)
|
||||
writeFileSync(join(dest, 'agent.cordis.yml'), composed.endsWith('\n') ? composed : `${composed}\n`, 'utf8')
|
||||
let patch = existsSync(patchPath) ? readFileSync(patchPath, 'utf8') : '[]\n'
|
||||
// Drop previous managed blocks (literal markers — do not RegExp `(managed)`).
|
||||
patch = stripManagedPresetBlocks(patch)
|
||||
// Drop a bare enable stub left by older installs.
|
||||
patch = patch.replace(/^- id: preset-netxops\n(?: .*\n)*/m, '')
|
||||
if (patch.trim() === '' || patch.trim() === '[]') patch = ''
|
||||
if (!patch.endsWith('\n') && patch.length > 0) patch += '\n'
|
||||
|
||||
const skillsSrc = join(overlayRoot, 'skills')
|
||||
if (existsSync(skillsSrc)) {
|
||||
cpSync(skillsSrc, join(dest, 'skills'), { recursive: true })
|
||||
const managed = `${DESKTOP_PATCH_BEGIN}\n${declarativePatch.trimEnd()}\n${DESKTOP_PATCH_END}\n`
|
||||
writeFileSync(patchPath, `${patch}${managed}`, 'utf8')
|
||||
logger.info('netxops: declarative preset synced into profiles/%s/cordis.patch.yml (restart Desktop to load)', profileName)
|
||||
}
|
||||
|
||||
/** Remove every BEGIN…END managed block using literal index search (markers contain `()`). */
|
||||
function stripManagedPresetBlocks(patch: string): string {
|
||||
let out = patch
|
||||
for (;;) {
|
||||
const start = out.indexOf(DESKTOP_PATCH_BEGIN)
|
||||
if (start < 0) break
|
||||
const end = out.indexOf(DESKTOP_PATCH_END, start)
|
||||
if (end < 0) {
|
||||
out = out.slice(0, start)
|
||||
break
|
||||
}
|
||||
let cut = end + DESKTOP_PATCH_END.length
|
||||
if (out[cut] === '\r') cut += 1
|
||||
if (out[cut] === '\n') cut += 1
|
||||
out = `${out.slice(0, start)}${out.slice(cut)}`
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
writeFileSync(
|
||||
join(dest, '.dsh-netxops-managed'),
|
||||
[
|
||||
`composed-from: standard`,
|
||||
`standard-source: ${standardDir}`,
|
||||
`at: ${new Date().toISOString()}`,
|
||||
'',
|
||||
].join('\n'),
|
||||
'utf8',
|
||||
)
|
||||
logger.info('netxops: agent preset composed from standard → %s', dest)
|
||||
/**
|
||||
* Build directory preset (web) + sync declarative patch into Desktop profiles.
|
||||
*/
|
||||
export function ensureAgentPresetInstalled(logger: Context['logger']): void {
|
||||
let artifacts: NetxopsPresetArtifacts
|
||||
try {
|
||||
artifacts = composeNetxopsPresetArtifacts()
|
||||
} catch (error) {
|
||||
logger.error('netxops: failed to install agent preset: %s', error)
|
||||
logger.warn('netxops: skip preset install: %s', error)
|
||||
return
|
||||
}
|
||||
try {
|
||||
installDirectoryPreset(artifacts, logger)
|
||||
} catch (error) {
|
||||
logger.error('netxops: failed to install directory preset: %s', error)
|
||||
}
|
||||
try {
|
||||
// Refresh shipped declarative file inside the package for next bundle consumers.
|
||||
writeFileSync(join(packageRoot(), 'presets', 'netxops.preset.patch.yml'), artifacts.declarativePatch)
|
||||
} catch {
|
||||
// package may be read-only in some installs
|
||||
}
|
||||
try {
|
||||
for (const profile of ['desktop', 'web']) {
|
||||
syncDeclarativeIntoProfilePatch(profile, artifacts.declarativePatch, logger)
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error('netxops: failed to sync declarative preset into profile patch: %s', error)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue