Merge remote-tracking branch 'origin/codex/invariant-service-seam' into codex/invariant-package-registration-gate

# Conflicts:
#	.agents/notes/implemented/architecture/2026-07-19-package-owned-invariant-service.i18n.yaml
#	docs/core-data-structures/session.md
#	docs/module-graph.md
#	package.json
#	packages/sdk/scripts/tsconfig.json
This commit is contained in:
Tianyi Cui
2026-07-20 00:35:06 +08:00
677 changed files with 6993 additions and 2418 deletions

View File

@@ -8,6 +8,7 @@ The `dsh-sdk` launcher owns SDK project startup and configuration.
| `dsh-sdk dev [target] [-- args…]` | Register TypeScript and local-workspace source resolution, then use the start path |
| `dsh-sdk build [args…]` | Invoke the project's installed tsdown with the project arguments |
| `dsh-sdk config` | Open one interactive edit session, review accumulated changes, commit once, and install once when NPM dependencies changed |
| `dsh-sdk create <source>` | Add an external Cordis plugin from a native package-manager source (`pkg@version` or `github:owner/repo#ref`): confirm, `<pm> add <source>`, then mount the resolved dependency in `cordis.yml`. No giget/pacote; the package manager resolves and pins the source (github deps build via their own `prepare` under the manager's policy) |
`ProjectBuild(tsdownConfig)` and `PluginBuild(tsdownConfig)` are exported only from `@deepseek-ai/dsh-scripts/dev/tsdown-config`. Development and production read the same `cordis.yml`.
@@ -25,6 +26,10 @@ The root library exports `startSDK`, `runSDK`, and the `SdkBootArgs`/`SdkBootCon
Indirectly, through the project `cordis.yml` tree loaded by `start` or `dev`.
#### KV Cache effect
No direct invalidation; the named consumer owns any request-prefix changes.
## Known Limitations and Deferred Work
- **Launcher arguments are schema-free** — `start` and `dev` preserve Node `parseArgs()` output rather than validating project-specific flags.

View File

@@ -37,6 +37,7 @@
"license": "BSD-3-Clause",
"dependencies": {
"@deepseek-ai/dsh-helper": "workspace:^",
"@deepseek-ai/dsh-telemetry": "workspace:^",
"commander": "^15.0.0",
"node-addon-require-builtin": "^0.1.0"
},

View File

@@ -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')

View File

@@ -7,7 +7,9 @@
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 { reportCommandTelemetry, type CommandTelemetryEvent } from './telemetry.ts'
import { DSH_SDK_TEMPLATES } from './templates/dsh-sdk-templates.ts'
/** Injectable process and command boundaries used by the dsh-sdk bin. */
@@ -19,6 +21,8 @@ export interface DshSdkCommandContext extends ConfigCommandContext {
run?: typeof runSDK
build?: typeof runProjectBuild
config?: typeof runConfigCommand
createPlugin?: typeof runCreatePluginCommand
telemetry?: (event: CommandTelemetryEvent) => Promise<void>
}
/** Run one parsed dsh-sdk command and return its process exit code. */
@@ -31,28 +35,42 @@ export async function runDshSdkCommand(
stderr: process.stderr,
},
): Promise<number> {
const startedAt = Date.now()
let command: string | undefined
let success = true
try {
const args = parseDshSdkArgs(argv)
if (args.help || !args.command) {
context.stdout.write(DSH_SDK_TEMPLATES.usage.render({}))
return 0
}
command = args.command
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
case 'build': await build(args.forwarded, context.cwd); break
case 'config': {
const result = await config(context)
if (result.installError) return 1
if (result.installError) { success = false; 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) {
success = false
context.stderr.write(`dsh-sdk: ${error instanceof Error ? error.message : String(error)}\n`)
return 1
} finally {
if (command !== undefined) {
/* v8 ignore next -- production telemetry wiring is exercised by the built-bin smoke */
const telemetry = context.telemetry ?? reportCommandTelemetry
await telemetry({ command, cwd: context.cwd, durationMs: Date.now() - startedAt, success })
}
}
}

View File

@@ -28,6 +28,17 @@ export interface ConfigWorkflowResult {
installError?: Error
}
/**
* Non-interactive desired end-state for a config run: the complete set of enabled
* features, with options and any secrets/values a newly installed feature needs.
* Features not listed are reconciled to disabled, exactly as an interactive tree
* selection would be. Custom (non-feature) cordis plugins keep their current state;
* toggling them headlessly is not yet supported.
*/
export interface ConfigPlan {
features: readonly FeatureSelection[]
}
function featureTarget(feature: Feature): string {
return `feature:${feature.id}`
}
@@ -66,48 +77,58 @@ export class ConfigWorkflow {
}
/** Select desired state, reconcile the working copy, review, and apply. */
async run(project: SdkProject, registry: FeatureRegistry): Promise<ConfigWorkflowResult> {
async run(project: SdkProject, registry: FeatureRegistry, plan?: ConfigPlan): Promise<ConfigWorkflowResult> {
const edit = project.edit(registry)
const configurator = new FeatureConfigurator(this.port)
const features = registry.all().filter(feature => feature.isApplicable(project.profile))
const inspections = new Map(edit.inspections().map(item => [item.id, item]))
const custom = edit.cordisConfigEntries().filter(entry => !registry.ownerOfPackage(entry.name, project.profile))
const desired = requireAnswer(await this.port.nestedMultiselect<string, string>({
message: 'Configure the project',
showChanges: true,
options: [
...features.map((feature) => {
const installation = inspections.get(feature.id)
/* v8 ignore next -- inspections() is built from this exact feature registry */
if (!installation) throw new Error(`feature inspection is missing: ${feature.id}`)
const inconsistent = installation.state === 'inconsistent'
const selectedOptions = new Set(installation.options.length > 0
? installation.options
: feature.defaultOptions(project.profile))
return {
value: featureTarget(feature),
label: feature.summary,
required: feature.required,
default: feature.required || installation.state === 'enabled' || inconsistent,
disabled: inconsistent,
...inconsistent ? { warning: installation.diagnostics.join('; ') } : {},
...feature.mode === 'single' ? {} : {
choiceMode: feature.mode,
choices: feature.options.map(option => ({
value: option.id,
label: option.label,
default: selectedOptions.has(option.id),
})),
},
}
}),
...custom.map(entry => ({
value: pluginTarget(entry.id),
label: `${entry.name} [custom]`,
default: !entry.disabled,
const desired = plan
? [
...plan.features.map(selection => ({
value: featureTarget(registry.get(selection.id)),
choices: selection.options,
})),
],
}))
...custom
.filter(entry => !entry.disabled)
.map(entry => ({ value: pluginTarget(entry.id), choices: [] as readonly string[] })),
]
: requireAnswer(await this.port.nestedMultiselect<string, string>({
message: 'Configure the project',
showChanges: true,
options: [
...features.map((feature) => {
const installation = inspections.get(feature.id)
/* v8 ignore next -- inspections() is built from this exact feature registry */
if (!installation) throw new Error(`feature inspection is missing: ${feature.id}`)
const inconsistent = installation.state === 'inconsistent'
const selectedOptions = new Set(installation.options.length > 0
? installation.options
: feature.defaultOptions(project.profile))
return {
value: featureTarget(feature),
label: feature.summary,
required: feature.required,
default: feature.required || installation.state === 'enabled' || inconsistent,
disabled: inconsistent,
...inconsistent ? { warning: installation.diagnostics.join('; ') } : {},
...feature.mode === 'single' ? {} : {
choiceMode: feature.mode,
choices: feature.options.map(option => ({
value: option.id,
label: option.label,
default: selectedOptions.has(option.id),
})),
},
}
}),
...custom.map(entry => ({
value: pluginTarget(entry.id),
label: `${entry.name} [custom]`,
default: !entry.disabled,
})),
],
}))
const desiredByTarget = new Map(desired.map(item => [item.value, item]))
const targetProfile = {
...project.profile,
@@ -117,6 +138,9 @@ export class ConfigWorkflow {
if (!feature.isApplicable(targetProfile)) desiredByTarget.delete(featureTarget(feature))
}
const plannedById = new Map<FeatureSelection['id'], FeatureSelection>(
(plan?.features ?? []).map(selection => [selection.id, selection]),
)
for (const feature of features) {
const installation = inspections.get(feature.id)
/* v8 ignore next -- inspections() is built from this exact feature registry */
@@ -124,7 +148,7 @@ export class ConfigWorkflow {
if (installation.state === 'inconsistent') continue
const choice = desiredByTarget.get(featureTarget(feature))
if (!choice && !feature.required) continue
await this.enableOrConfigure(feature, installation, choice, project, edit, configurator)
await this.enableOrConfigure(feature, installation, choice, project, edit, configurator, plannedById.get(feature.id))
}
for (const feature of [...features].reverse()) {
@@ -176,6 +200,7 @@ export class ConfigWorkflow {
project: SdkProject,
edit: ReturnType<SdkProject['edit']>,
configurator: FeatureConfigurator,
planned?: FeatureSelection,
): Promise<void> {
const options = choice?.choices.length
? choice.choices
@@ -183,7 +208,9 @@ export class ConfigWorkflow {
? installation.options
: feature.defaultOptions(project.profile)
if (installation.state === 'absent') {
const selection = await configurator.configure(feature, project.profile, undefined, options)
const selection = await configurator.configure(
feature, project.profile, undefined, options, planned?.secrets ?? {}, planned?.values ?? {},
)
edit.installFeature(feature, selection)
return
}
@@ -191,10 +218,7 @@ export class ConfigWorkflow {
if (!installation.selection) throw new Error(`feature ${feature.id} has no readable selection`)
if (!sameOptions(installation.options, options)) {
const selection: FeatureSelection = await configurator.configure(
feature,
project.profile,
installation.selection,
options,
feature, project.profile, installation.selection, options, planned?.secrets ?? {}, planned?.values ?? {},
)
edit.configureFeature(feature, selection)
}

View 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
}

View File

@@ -0,0 +1,63 @@
/**
* Launcher-side telemetry wiring: resolve consent and send one fire-and-forget
* event around each dsh-sdk command. Best-effort — never affects the command's
* outcome or exit code.
*
* @module @deepseek-ai/dsh-scripts/telemetry
*/
import {
ConsentResolver,
TelemetryReporter,
buildTelemetryPayload,
type ConsentDecision,
} from '@deepseek-ai/dsh-telemetry'
/** One command's telemetry lifecycle facts. */
export interface CommandTelemetryEvent {
/** The dsh-sdk command that ran. */
command: string
/** Project directory whose consent, `cordis.yml`, and `package.json` are read. */
cwd: string
/** Wall-clock duration in milliseconds. */
durationMs: number
/** Whether the command completed without error. */
success: boolean
}
/** Injectable consent and delivery seams for tests. */
export interface CommandTelemetryDeps {
resolve?: (cwd: string) => Promise<ConsentDecision>
reporter?: Pick<TelemetryReporter, 'report' | 'flush'>
}
/**
* Resolve consent for the project and, when allowed, assemble and send one
* telemetry event, draining in-flight sends before returning. Swallows every
* error so telemetry can never change a command's result.
* @param event - the command lifecycle facts.
* @param deps - consent and delivery seams; defaults hit the real endpoint.
*/
export async function reportCommandTelemetry(
event: CommandTelemetryEvent,
deps: CommandTelemetryDeps = {},
): Promise<void> {
try {
/* v8 ignore next -- the production ConsentResolver is exercised by the built-bin smoke */
const resolve = deps.resolve ?? (cwd => new ConsentResolver().resolve(cwd))
const consent = await resolve(event.cwd)
if (!consent.allowed) return
const payload = await buildTelemetryPayload({
command: event.command,
durationMs: event.durationMs,
success: event.success,
projectDir: event.cwd,
})
/* v8 ignore next -- the production TelemetryReporter is exercised by the built-bin smoke */
const reporter = deps.reporter ?? new TelemetryReporter()
reporter.report(payload, consent)
await reporter.flush()
} catch {
// Telemetry is best-effort; a consent, payload, or delivery fault never reaches the command.
}
}

View File

@@ -5,3 +5,4 @@ Commands:
dev [target] [-- args...] Start with TypeScript and local-plugin source resolution
build [args...] Run the project's installed tsdown
config Interactively edit project features
create <source> Add an external plugin dependency (pkg@version or github:owner/repo#ref) and mount it in cordis.yml

View File

@@ -5,6 +5,7 @@ import { PassThrough, Writable } from 'node:stream'
import { fileURLToPath, pathToFileURL } from 'node:url'
import { afterEach, describe, expect, expectTypeOf, it, vi } from 'vitest'
import {
HeadlessPromptPort,
LocalPluginBlueprint,
NpmPackageManager,
SdkProject,
@@ -29,7 +30,9 @@ import { parseDshSdkArgs, parseSdkBootArgs } from '../src/args.ts'
import { PluginBuild, ProjectBuild, runProjectBuild } from '../src/build.ts'
import { runDshSdkCommand, type DshSdkCommandContext } from '../src/command.ts'
import { runConfigCommand } from '../src/config.ts'
import { ConfigWorkflow } from '../src/config/config-workflow.ts'
import { ConfigWorkflow, type ConfigPlan } from '../src/config/config-workflow.ts'
import { runCreatePluginCommand } from '../src/create-plugin.ts'
import { reportCommandTelemetry, type CommandTelemetryEvent } from '../src/telemetry.ts'
import { initialize, resolve as resolveLocalPlugin } from '../src/local-plugin-loader-hooks.ts'
const temporary: string[] = []
@@ -165,6 +168,7 @@ describe('Commander launcher arguments', () => {
await expect(runDshSdkCommand(['unknown'], context)).resolves.toBe(1)
await expect(runDshSdkCommand([], context)).resolves.toBe(0)
expect(context.readStdout()).toContain('Usage: dsh-sdk')
expect(context.readStdout()).toContain('create <source>')
const defaults = commandContext(root)
await writeFile(join(root, 'main.mjs'), 'export function main() { return "ok" }\n')
@@ -399,6 +403,28 @@ describe('ConfigWorkflow', () => {
expect(output.read()).toContain('Disable feature: todo')
})
it('reconciles a headless plan without prompting and preserves custom plugins', async () => {
const project = await committedProject([], [new LocalPluginBlueprint('plugin', 'plugin')])
const registry = createBuiltinRegistry(project.profile)
const output = outputBuffer()
let installs = 0
const plan: ConfigPlan = {
features: [
{ id: featureId('bash'), options: ['local'] },
{ id: featureId('persistence'), options: ['jsonl'] },
{ id: featureId('todo'), options: ['default'] },
{ id: featureId('web'), options: ['exa'], secrets: { apiKey: 'exa-key' } },
],
}
const result = await new ConfigWorkflow(
new HeadlessPromptPort(), output.stream, async () => { installs += 1 },
).run(project, registry, plan)
expect(result.commit?.project.cordis.entry('tool-todo')).toBeDefined()
// the unlisted custom local plugin keeps its enabled state (not nuked by the plan)
expect(result.commit?.project.cordis.entry('plugin')?.disabled).toBeFalsy()
expect(installs).toBe(1)
})
it('installs once after NPM dependency changes and keeps committed files on install failure', async () => {
const project = await committedProject()
const registry = createBuiltinRegistry(project.profile)
@@ -536,3 +562,108 @@ describe('ConfigWorkflow', () => {
expect(output.read()).toContain('Disable feature: ask-user')
})
})
describe('dsh-sdk create', () => {
const writeDependency = (name: string) => async (_m: unknown, spec: string, cwd: string): Promise<void> => {
const path = join(cwd, 'package.json')
const manifest = JSON.parse(await readFile(path, 'utf8')) as { dependencies?: Record<string, string> }
manifest.dependencies = { ...manifest.dependencies, [name]: spec }
await writeFile(path, JSON.stringify(manifest, null, 2))
}
it('adds a dependency and mounts it after confirmation', async () => {
const project = await committedProject()
const context = { ...commandContext(project.root), port: new QueuePort([true]), add: writeDependency('my-ext-plugin') }
const result = await runCreatePluginCommand('github:o/r#sha', context)
expect(result?.project.cordis.entry('my-ext-plugin')?.name).toBe('my-ext-plugin')
expect(context.readStdout()).toContain('Mounted my-ext-plugin')
})
it('derives the cordis id from a scoped package name', async () => {
const project = await committedProject()
const context = { ...commandContext(project.root), port: new QueuePort([true]), add: writeDependency('@acme/cool-plugin') }
const result = await runCreatePluginCommand('@acme/cool-plugin@1.0.0', context)
expect(result?.project.cordis.entry('cool-plugin')?.name).toBe('@acme/cool-plugin')
})
it('returns undefined and adds nothing when declined', async () => {
const project = await committedProject()
let added = false
const context = {
...commandContext(project.root),
port: new QueuePort([false]),
add: async () => { added = true },
}
await expect(runCreatePluginCommand('pkg@1.0.0', context)).resolves.toBeUndefined()
expect(added).toBe(false)
})
it('rejects an empty source, a non-TTY session, and a no-op add', async () => {
const project = await committedProject()
await expect(runCreatePluginCommand(' ', { ...commandContext(project.root), port: new QueuePort([]) }))
.rejects.toThrow('requires a plugin source')
const noTty = commandContext(project.root)
noTty.stdin.isTTY = false
noTty.stdout.isTTY = false
await expect(runCreatePluginCommand('pkg@1.0.0', noTty)).rejects.toThrow('interactive TTY')
const noOutTty = commandContext(project.root)
noOutTty.stdout.isTTY = false
await expect(runCreatePluginCommand('pkg@1.0.0', noOutTty)).rejects.toThrow('interactive TTY')
await expect(runCreatePluginCommand('pkg@1.0.0', {
...commandContext(project.root), port: new QueuePort([true]), add: async () => {},
})).rejects.toThrow('added no new dependency')
})
it('dispatches create through the launcher', async () => {
const project = await committedProject()
const context = commandContext(project.root)
context.createPlugin = async () => undefined
await expect(runDshSdkCommand(['create', 'pkg@1.0.0'], context)).resolves.toBe(0)
})
})
describe('command telemetry', () => {
it('reports when consent allows and skips when denied or faulting', async () => {
const dir = await mkdtemp(join(tmpdir(), 'dsh-telemetry-'))
temporary.push(dir)
const sent: unknown[] = []
const reporter = { report: () => { sent.push(1) }, flush: async () => {} }
await reportCommandTelemetry(
{ command: 'build', cwd: dir, durationMs: 5, success: true },
{ resolve: async () => ({ allowed: true, reason: 'absent' }), reporter },
)
expect(sent).toHaveLength(1)
await reportCommandTelemetry(
{ command: 'build', cwd: dir, durationMs: 5, success: true },
{ resolve: async () => ({ allowed: false, reason: 'disabled' }), reporter },
)
expect(sent).toHaveLength(1)
await expect(reportCommandTelemetry(
{ command: 'build', cwd: dir, durationMs: 5, success: true },
{ resolve: async () => { throw new Error('boom') }, reporter },
)).resolves.toBeUndefined()
expect(sent).toHaveLength(1)
})
it('emits a telemetry event carrying each command outcome', async () => {
const project = await committedProject()
const events: CommandTelemetryEvent[] = []
const context = commandContext(project.root)
context.telemetry = async (event) => { events.push(event) }
context.build = async () => {}
await expect(runDshSdkCommand(['build'], context)).resolves.toBe(0)
expect(events).toHaveLength(1)
expect(events[0]).toMatchObject({ command: 'build', cwd: project.root, success: true })
await runDshSdkCommand([], context)
expect(events).toHaveLength(1)
context.build = async () => { throw new Error('boom') }
await expect(runDshSdkCommand(['build'], context)).resolves.toBe(1)
expect(events[1]).toMatchObject({ command: 'build', success: false })
context.config = async () => ({ installError: new Error('offline') })
await expect(runDshSdkCommand(['config'], context)).resolves.toBe(1)
expect(events.at(-1)).toMatchObject({ command: 'config', success: false })
})
})

View File

@@ -6,17 +6,10 @@
},
"include": ["src"],
"references": [
{
"path": "../helper"
},
{
"path": "../../ui/app-boot"
},
{
"path": "../../../vendor/cordis"
},
{
"path": "../../support/invariants"
}
{ "path": "../helper" },
{ "path": "../telemetry" },
{ "path": "../../ui/app-boot" },
{ "path": "../../../vendor/cordis" },
{ "path": "../../support/invariants" }
]
}