feat(dsh-sdk): create <source> — add external plugin as native PM dependency + cordis mount
dsh-sdk create <source> adds a github (github:owner/repo#ref) or npm (pkg@version) plugin as a package-manager-native dependency, then mounts the resolved dependency in cordis.yml through ProjectEditSession. Adds PackageManager.add(spec) and ProjectEditSession.addExternalPlugin(id, packageName). No giget/pacote. Per-file 100% coverage on the new/changed files.
This commit is contained in:
@@ -8,12 +8,13 @@ import { parseArgs as parseNodeArgs } from 'node:util'
|
||||
import { Command } from 'commander'
|
||||
|
||||
/** Commands implemented by the dsh-sdk launcher. */
|
||||
type DshSdkCommand = 'start' | 'dev' | 'build' | 'config'
|
||||
type DshSdkCommand = 'start' | 'dev' | 'build' | 'config' | 'create'
|
||||
|
||||
/** Parsed dsh-sdk invocation. */
|
||||
export interface DshSdkArgs {
|
||||
command?: DshSdkCommand
|
||||
target?: string
|
||||
source?: string
|
||||
forwarded: readonly string[]
|
||||
help: boolean
|
||||
}
|
||||
@@ -60,6 +61,9 @@ export function parseDshSdkArgs(argv: readonly string[]): DshSdkArgs {
|
||||
program.command('config').helpOption(false).action(() => {
|
||||
parsed = { command: 'config', forwarded: [], help: false }
|
||||
})
|
||||
program.command('create <source>').helpOption(false).action((source: string) => {
|
||||
parsed = { command: 'create', source, forwarded: [], help: false }
|
||||
})
|
||||
program.parse([...launcherArgv], { from: 'user' })
|
||||
/* v8 ignore next -- every registered Commander action above assigns parsed or Commander throws */
|
||||
if (!parsed) throw new Error('dsh-sdk command did not resolve')
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
import { parseDshSdkArgs } from './args.ts'
|
||||
import { runProjectBuild } from './build.ts'
|
||||
import { runConfigCommand, type ConfigCommandContext } from './config.ts'
|
||||
import { runCreatePluginCommand } from './create-plugin.ts'
|
||||
import { runSDK } from './runtime.ts'
|
||||
import { DSH_SDK_TEMPLATES } from './templates/dsh-sdk-templates.ts'
|
||||
|
||||
@@ -19,6 +20,7 @@ export interface DshSdkCommandContext extends ConfigCommandContext {
|
||||
run?: typeof runSDK
|
||||
build?: typeof runProjectBuild
|
||||
config?: typeof runConfigCommand
|
||||
createPlugin?: typeof runCreatePluginCommand
|
||||
}
|
||||
|
||||
/** Run one parsed dsh-sdk command and return its process exit code. */
|
||||
@@ -40,6 +42,7 @@ export async function runDshSdkCommand(
|
||||
const run = context.run ?? runSDK
|
||||
const build = context.build ?? runProjectBuild
|
||||
const config = context.config ?? runConfigCommand
|
||||
const createPlugin = context.createPlugin ?? runCreatePluginCommand
|
||||
switch (args.command) {
|
||||
case 'start': await run(args.target, { cwd: context.cwd, argv: args.forwarded }); break
|
||||
case 'dev': await run(args.target, { cwd: context.cwd, dev: true, argv: args.forwarded }); break
|
||||
@@ -49,6 +52,8 @@ export async function runDshSdkCommand(
|
||||
if (result.installError) return 1
|
||||
break
|
||||
}
|
||||
/* v8 ignore next -- Commander requires <source>, so create never dispatches without it */
|
||||
case 'create': await createPlugin(args.source ?? '', context); break
|
||||
}
|
||||
return 0
|
||||
} catch (error) {
|
||||
|
||||
91
packages/sdk/scripts/src/create-plugin.ts
Normal file
91
packages/sdk/scripts/src/create-plugin.ts
Normal file
@@ -0,0 +1,91 @@
|
||||
/**
|
||||
* dsh-sdk create command: add an external Cordis plugin (github or npm) as a
|
||||
* native package-manager dependency and mount it in cordis.yml.
|
||||
*
|
||||
* @module @deepseek-ai/dsh-scripts/create-plugin
|
||||
*/
|
||||
|
||||
import { readFile } from 'node:fs/promises'
|
||||
import { join } from 'node:path'
|
||||
import {
|
||||
ClackPromptPort,
|
||||
ConfirmQuestion,
|
||||
SdkProject,
|
||||
createBuiltinRegistry,
|
||||
requireAnswer,
|
||||
type PackageManager,
|
||||
type ProjectCommitResult,
|
||||
type PromptPort,
|
||||
} from '@deepseek-ai/dsh-helper'
|
||||
|
||||
/** Process and interaction slice required by dsh-sdk create. */
|
||||
export interface CreatePluginContext {
|
||||
cwd: string
|
||||
stdin: NodeJS.ReadStream
|
||||
stdout: NodeJS.WriteStream
|
||||
port?: PromptPort
|
||||
add?: (manager: PackageManager, spec: string, cwd: string) => Promise<void>
|
||||
}
|
||||
|
||||
/** Result of a create run; `undefined` when the confirmation was declined. */
|
||||
export type CreatePluginResult = ProjectCommitResult<SdkProject> | undefined
|
||||
|
||||
/** Derive a stable cordis entry id from a package name's last path segment. */
|
||||
function pluginId(packageName: string): string {
|
||||
const base = packageName.startsWith('@') ? packageName.slice(packageName.indexOf('/') + 1) : packageName
|
||||
const id = base.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, '')
|
||||
/* v8 ignore next -- a valid npm package name always yields a non-empty id */
|
||||
if (!id) throw new Error(`cannot derive a plugin id from package name: ${packageName}`)
|
||||
return id
|
||||
}
|
||||
|
||||
/** Read the direct dependency names declared in a project's package.json. */
|
||||
async function dependencyNames(cwd: string): Promise<Set<string>> {
|
||||
const manifest = JSON.parse(await readFile(join(cwd, 'package.json'), 'utf8')) as {
|
||||
dependencies?: Record<string, unknown>
|
||||
}
|
||||
/* v8 ignore next -- generated projects always declare a dependencies map */
|
||||
return new Set(Object.keys(manifest.dependencies ?? {}))
|
||||
}
|
||||
|
||||
/**
|
||||
* Add one external plugin dependency to the current project and mount it.
|
||||
* @param source - a package-manager-native source (`pkg@version` or `github:owner/repo#ref`).
|
||||
* @param context - process, interaction, and dependency-add boundaries.
|
||||
* @returns the commit result, or `undefined` when the confirmation was declined.
|
||||
*/
|
||||
export async function runCreatePluginCommand(
|
||||
source: string,
|
||||
context: CreatePluginContext,
|
||||
): Promise<CreatePluginResult> {
|
||||
const spec = source.trim()
|
||||
if (!spec) throw new Error('dsh-sdk create requires a plugin source (pkg@version or github:owner/repo#ref)')
|
||||
if (!context.port && (!context.stdin.isTTY || !context.stdout.isTTY)) {
|
||||
throw new Error('dsh-sdk create requires an interactive TTY')
|
||||
}
|
||||
const project = await SdkProject.open(context.cwd)
|
||||
/* v8 ignore next -- production TTY wiring is exercised by the built-bin smoke */
|
||||
const port = context.port ?? new ClackPromptPort(context.stdin, context.stdout)
|
||||
const confirmed = requireAnswer(await new ConfirmQuestion({
|
||||
id: 'create.confirm',
|
||||
message: `Add plugin '${spec}' as a dependency and mount it in cordis.yml?`,
|
||||
initialValue: true,
|
||||
}).resolve(port))
|
||||
if (!confirmed) return undefined
|
||||
|
||||
const before = await dependencyNames(context.cwd)
|
||||
/* v8 ignore next -- production package-manager wiring is exercised by the built-bin smoke */
|
||||
const add = context.add ?? ((manager, source, cwd) => manager.add(source, cwd))
|
||||
await add(project.profile.packageManager, spec, context.cwd)
|
||||
const after = await dependencyNames(context.cwd)
|
||||
const added = [...after].filter(name => !before.has(name))
|
||||
if (added.length === 0) throw new Error(`dsh-sdk create: '${spec}' added no new dependency`)
|
||||
|
||||
const reopened = await SdkProject.open(context.cwd)
|
||||
const registry = createBuiltinRegistry(reopened.profile)
|
||||
const edit = reopened.edit(registry)
|
||||
for (const packageName of added) edit.addExternalPlugin(pluginId(packageName), packageName)
|
||||
const commit = await edit.commit()
|
||||
context.stdout.write(`Mounted ${added.join(', ')} in cordis.yml.\n`)
|
||||
return commit
|
||||
}
|
||||
Reference in New Issue
Block a user