Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion SKILL/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,9 @@ v8-runner init
`--sources` on `yaxunit` or `client-mcp` to download `.cfe` artifacts when
`builder=DESIGNER`.
- Vanessa Automation debugging or scenario authoring: use `v8-runner launch mcp va --wait-ready ...` to start the client MCP server with VA loaded and verify the VA MCP tools before driving `.feature` workflows.
- Extension properties need synchronization: use `v8-runner extensions` or `extensions --name <SOURCE_SET>`.
- Extension properties need synchronization: use `v8-runner extensions`, `extensions --name <SOURCE_SET>`,
or CLI-only `extensions --extension <PLATFORM_NAME>` for an installed extension that is not selected
through a configured source-set. Do not combine `--name` and `--extension`.
- Infobase changes need to become Git-visible files: check `git status`, then run the relevant `v8-runner dump ...` command.
- Source files need conversion between Designer and EDT: use `v8-runner convert`; this is CLI-only and does not use the infobase.
- Existing `.cf` or `.cfe` artifacts need to be applied to an infobase: use `v8-runner load ...`.
Expand Down
8 changes: 8 additions & 0 deletions SKILL/references/command-selection.md
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,14 @@ Update selected extension source-sets:
v8-runner extensions --name <SOURCE_SET>
```

Update installed extensions that are not selected through `v8project.yaml`:

```bash
v8-runner extensions --extension <PLATFORM_NAME>
```

`--extension` is CLI-only, can be repeated, and cannot be combined with `--name`.

## Dump, Convert, Load, And Artifacts

Bring infobase changes back into Git-visible files:
Expand Down
4 changes: 3 additions & 1 deletion SKILL/references/config-and-backends.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,9 @@ settings before CLI overrides.
- `format=DESIGNER`, `builder=IBCMD`: supports init, build, extensions, and dump for file infobases and server infobases with `infobase.dbms`.
- `format=EDT`, `builder=DESIGNER`: supports init, build through EDT export to Designer files, EDT syntax checks, extensions, and tests.
- `format=EDT`, `builder=IBCMD`: supports init and build through EDT export to Designer files followed by IBCMD import/apply; requires a file infobase.
- `extensions` supports Designer and EDT projects, but only extension `source-set` entries are actionable.
- `extensions` supports Designer and EDT projects. Without a selector, it updates configured extension
`source-set` entries; CLI-only `--extension <PLATFORM_NAME>` updates an installed platform extension
directly and cannot be combined with `--name`.
- `syntax designer-config` and `syntax designer-modules` require Designer format with Designer backend.
- `syntax edt` requires EDT format with Designer backend.
- IBCMD dump uses project-local standalone-server data under `workPath/ibcmd-data`.
Expand Down
4 changes: 4 additions & 0 deletions SKILL/references/project-workflows.md
Original file line number Diff line number Diff line change
Expand Up @@ -117,8 +117,12 @@ Do not replace extension-specific synchronization with a full rebuild unless the
```bash
v8-runner extensions
v8-runner extensions --name <SOURCE_SET>
v8-runner extensions --extension <PLATFORM_NAME>
```

`--name` selects configured extension source-sets. CLI-only `--extension` selects installed
platform extensions by their exact names and can be repeated; it cannot be combined with `--name`.

## Launch

Prefer runner launch commands over raw `1cv8` command construction:
Expand Down
9 changes: 6 additions & 3 deletions docs/CAPABILITIES.md
Original file line number Diff line number Diff line change
Expand Up @@ -157,11 +157,14 @@ v8-runner tools download client-mcp [--sources] [--force]
### `extensions`

```bash
v8-runner extensions [--name <SOURCE_SET>...]
v8-runner extensions [--name <SOURCE_SET>... | --extension <PLATFORM_NAME>...]
```

- Работает только с `source-set`, у которых `type=EXTENSION`.
- Без `--name` обрабатывает все extension `source-set` из конфига.
- Без прямого selector работает только с `source-set`, у которых `type=EXTENSION`.
- Без селектора обрабатывает все extension `source-set` из конфига.
- `--name` выбирает configured extension `source-set`; `--extension` выбирает установленное в ИБ
platform extension по его точному имени, без lookup в конфиге. Эти селекторы взаимоисключающие;
`--extension` можно повторять, а пустые, control-character и повторяющиеся значения отклоняются.
- Возвращает пошаговый результат по каждому целевому расширению.

### `build`
Expand Down
43 changes: 41 additions & 2 deletions src/cli/args.rs
Original file line number Diff line number Diff line change
Expand Up @@ -236,8 +236,12 @@ pub struct LoadArgs {
#[command(next_help_heading = "Command options")]
pub struct ExtensionsArgs {
/// Extension source-set name to update. Repeat to target multiple extensions.
#[arg(long = "name")]
#[arg(long = "name", conflicts_with = "extensions")]
pub names: Vec<String>,

/// Installed platform extension name to update. Repeat to target multiple extensions.
#[arg(long = "extension", conflicts_with = "names")]
pub extensions: Vec<String>,
}

#[derive(Args, Debug)]
Expand Down Expand Up @@ -626,13 +630,48 @@ mod tests {
.expect("parse");

match cli.command {
Command::Extensions(ExtensionsArgs { names }) => {
Command::Extensions(ExtensionsArgs { names, .. }) => {
assert_eq!(names, vec!["client_mcp", "tests"]);
}
_ => panic!("unexpected command"),
}
}

#[test]
fn parses_extensions_command_with_platform_extension_names_in_requested_order() {
let cli = Cli::try_parse_from([
"v8-runner",
"extensions",
"--extension",
"SalesAddon",
"--extension",
"TestsAddon",
])
.expect("parse");

match cli.command {
Command::Extensions(ExtensionsArgs { extensions, .. }) => {
assert_eq!(extensions, vec!["SalesAddon", "TestsAddon"]);
}
_ => panic!("unexpected command"),
}
}

#[test]
fn extensions_command_rejects_mixing_source_set_and_platform_extension_selectors() {
let error = Cli::try_parse_from([
"v8-runner",
"extensions",
"--name",
"client_mcp",
"--extension",
"SalesAddon",
])
.expect_err("selectors must conflict");

assert!(error.to_string().contains("cannot be used with"));
}

#[test]
fn parses_load_command_with_default_mode() {
let cli = Cli::try_parse_from(["v8-runner", "load", "--path", "dist/main.cf"])
Expand Down
76 changes: 69 additions & 7 deletions src/cli/execute.rs
Original file line number Diff line number Diff line change
Expand Up @@ -287,7 +287,8 @@ fn execute_extensions(
clean_before_execution: bool,
cancellation: CancellationToken,
) -> Result<(), UseCaseError> {
let request = map_extensions_request(args);
let request = map_extensions_request(args)
.map_err(|error| render_pre_dispatch_error(presenter, CommandName::Extensions, error))?;
let context = cli_context(config, CommandName::Extensions, cancellation);
with_cli_workspace_lock(
config,
Expand Down Expand Up @@ -833,10 +834,25 @@ fn map_build_request(args: &BuildArgs) -> BuildRequest {
}
}

fn map_extensions_request(args: &ExtensionsArgs) -> ConfigureExtensionsRequest {
ConfigureExtensionsRequest {
names: args.names.clone(),
fn map_extensions_request(
args: &ExtensionsArgs,
) -> Result<ConfigureExtensionsRequest, UseCaseError> {
if !args.names.is_empty() && !args.extensions.is_empty() {
return Err(UseCaseError::new(
UseCaseErrorKind::Validation,
"--name cannot be used with --extension",
));
}

let selector = if !args.extensions.is_empty() {
crate::use_cases::request::ExtensionSelector::platform_names(args.extensions.clone())?
} else if !args.names.is_empty() {
crate::use_cases::request::ExtensionSelector::SourceSets(args.names.clone())
} else {
crate::use_cases::request::ExtensionSelector::ConfiguredAll
};

Ok(ConfigureExtensionsRequest { selector })
}

fn map_tools_download_target(args: &ToolsDownloadArgs) -> ToolDownloadTarget {
Expand Down Expand Up @@ -2628,9 +2644,11 @@ mod tests {
assert_eq!(
map_extensions_request(&ExtensionsArgs {
names: vec!["client_mcp".to_owned()],
extensions: vec![],
})
.names,
vec!["client_mcp"]
.expect("request")
.selector,
crate::use_cases::request::ExtensionSelector::SourceSets(vec!["client_mcp".to_owned()])
);
assert_eq!(
map_dump_request(&DumpArgs {
Expand Down Expand Up @@ -2772,6 +2790,47 @@ mod tests {
assert_eq!(artifacts.extension.as_deref(), Some("SalesAddon"));
}

#[test]
fn maps_extensions_request_to_direct_platform_selector_without_normalizing_name() {
let request = map_extensions_request(&ExtensionsArgs {
names: vec![],
extensions: vec![" SalesAddon ".to_owned(), "TestsAddon".to_owned()],
})
.expect("valid direct selector");

assert!(matches!(
request.selector,
crate::use_cases::request::ExtensionSelector::PlatformNames(names)
if names == [" SalesAddon ", "TestsAddon"]
));
}

#[test]
fn maps_extensions_request_rejects_duplicate_direct_platform_names() {
let error = map_extensions_request(&ExtensionsArgs {
names: vec![],
extensions: vec!["SalesAddon".to_owned(), "SalesAddon".to_owned()],
})
.expect_err("duplicate direct selector");

assert_eq!(error.kind(), UseCaseErrorKind::Validation);
assert!(error.message().contains("duplicate --extension"));
}

#[test]
fn maps_extensions_request_rejects_blank_or_control_character_direct_platform_names() {
for name in [" ", "Sales\nAddon"] {
let error = map_extensions_request(&ExtensionsArgs {
names: vec![],
extensions: vec![name.to_owned()],
})
.expect_err("invalid direct selector");

assert_eq!(error.kind(), UseCaseErrorKind::Validation);
assert!(error.message().contains("invalid --extension"));
}
}

#[test]
fn maps_artifacts_request_keeps_blank_extension_in_cfe_mode() {
let artifacts = map_artifacts_request_with_config(
Expand Down Expand Up @@ -2907,7 +2966,10 @@ mod tests {
fn resolves_command_name() {
assert_eq!(command_name(&Command::Init), CommandName::Init);
assert_eq!(
command_name(&Command::Extensions(ExtensionsArgs { names: vec![] })),
command_name(&Command::Extensions(ExtensionsArgs {
names: vec![],
extensions: vec![],
})),
CommandName::Extensions
);
assert_eq!(
Expand Down
50 changes: 27 additions & 23 deletions src/use_cases/configure_extensions.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ use crate::use_cases::extension_identity::platform_extension_name;
use crate::use_cases::ibcmd_diagnostics::format_ibcmd_failure_details;
use crate::use_cases::interruption;
use crate::use_cases::progress::log_live_stage;
use crate::use_cases::request::ConfigureExtensionsRequest;
use crate::use_cases::request::{ConfigureExtensionsRequest, ExtensionSelector};
use crate::use_cases::result::{UseCaseFailure, UseCaseResult};
use tracing::{debug, info};

Expand Down Expand Up @@ -230,23 +230,27 @@ fn resolve_targets(
})
.collect::<Vec<_>>();

if args.names.is_empty() {
return Ok(available.into_iter().map(|(_, name)| name).collect());
}

let mut targets = Vec::new();
for requested in &args.names {
let Some((_, resolved)) = available
.iter()
.find(|(name, _)| *name == requested.as_str())
else {
return Err(AppError::Validation(format!(
"unknown extension source-set '{requested}'"
)));
};
targets.push(resolved.clone());
match &args.selector {
ExtensionSelector::ConfiguredAll => {
Ok(available.into_iter().map(|(_, name)| name).collect())
}
ExtensionSelector::SourceSets(requested_names) => {
let mut targets = Vec::new();
for requested in requested_names {
let Some((_, resolved)) = available
.iter()
.find(|(name, _)| *name == requested.as_str())
else {
return Err(AppError::Validation(format!(
"unknown extension source-set '{requested}'"
)));
};
targets.push(resolved.clone());
}
Ok(targets)
}
ExtensionSelector::PlatformNames(names) => Ok(names.clone()),
}
Ok(targets)
}

#[cfg(test)]
Expand Down Expand Up @@ -328,8 +332,8 @@ mod tests {
.expect("project file");
let config = sample_config(dir.path(), dir.path(), Path::new("/tmp/ibcmd"));

let targets = resolve_targets(&config, &ConfigureExtensionsRequest { names: vec![] })
.expect("targets");
let targets =
resolve_targets(&config, &ConfigureExtensionsRequest::default()).expect("targets");

assert_eq!(targets, vec!["client_mcp"]);
}
Expand Down Expand Up @@ -377,7 +381,7 @@ mod tests {
let result = execute(
&ExecutionContext::cli(CommandName::Extensions),
&config,
&ConfigureExtensionsRequest { names: vec![] },
&ConfigureExtensionsRequest::default(),
)
.expect("execute");

Expand Down Expand Up @@ -416,7 +420,7 @@ mod tests {
let result = execute(
&ExecutionContext::cli(CommandName::Extensions),
&config,
&ConfigureExtensionsRequest { names: vec![] },
&ConfigureExtensionsRequest::default(),
)
.expect("execute");

Expand Down Expand Up @@ -448,7 +452,7 @@ mod tests {
let failure = execute(
&ExecutionContext::cli(CommandName::Extensions),
&config,
&ConfigureExtensionsRequest { names: vec![] },
&ConfigureExtensionsRequest::default(),
)
.expect_err("failure");

Expand Down Expand Up @@ -486,7 +490,7 @@ mod tests {
let failure = execute(
&ExecutionContext::cli(CommandName::Extensions).with_cancellation(cancellation),
&config,
&ConfigureExtensionsRequest { names: vec![] },
&ConfigureExtensionsRequest::default(),
)
.expect_err("interrupted execution");
let payload = failure.payload.expect("payload");
Expand Down
38 changes: 36 additions & 2 deletions src/use_cases/request.rs
Original file line number Diff line number Diff line change
Expand Up @@ -640,11 +640,45 @@ pub struct LaunchRequest {
#[derive(Debug, Clone, PartialEq, Eq, Default)]
pub struct InitRequest;

/// Explicit extension target selection for extension property updates.
#[derive(Debug, Clone, PartialEq, Eq, Default)]
pub enum ExtensionSelector {
/// Updates all configured extension source-sets.
#[default]
ConfiguredAll,
/// Updates extension source-sets selected by their configuration names.
SourceSets(Vec<String>),
/// Updates installed platform extensions selected by their platform names.
PlatformNames(Vec<String>),
}

impl ExtensionSelector {
/// Builds a direct platform-name selector without altering user-provided names.
pub fn platform_names(names: Vec<String>) -> Result<Self, UseCaseError> {
let mut seen = std::collections::HashSet::new();
for name in &names {
if name.trim().is_empty() || name.chars().any(char::is_control) {
return Err(UseCaseError::new(
UseCaseErrorKind::Validation,
"invalid --extension value: platform extension name must be nonblank and contain no control characters",
));
}
if !seen.insert(name.as_str()) {
return Err(UseCaseError::new(
UseCaseErrorKind::Validation,
format!("duplicate --extension value '{name}'"),
));
}
}
Ok(Self::PlatformNames(names))
}
}

/// Transport-neutral request for extension property updates.
#[derive(Debug, Clone, PartialEq, Eq, Default)]
pub struct ConfigureExtensionsRequest {
/// Optional source-set names to update. Empty means all extension source-sets.
pub names: Vec<String>,
/// Requested extension target selection.
pub selector: ExtensionSelector,
}

#[cfg(test)]
Expand Down
Loading