Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
80 commits
Select commit Hold shift + click to select a range
9f69050
feat: 1C.0: bootstrap :cli module with Picocli + Quarkus skeleton
May 4, 2026
c1dd312
docs(dial-unified-config): mark slice 1C.0 merged with B1 keystore de…
May 4, 2026
8286973
feat: 1C.1: dial-cli env list / current / use / check
May 5, 2026
b051774
docs(dial-unified-config): mark slice 1C.1 merged with config-only en…
May 5, 2026
8b03ad2
feat: 1C.2: dial-cli model get / list + get-models alias + HTTP client
May 5, 2026
849aec0
docs(dial-unified-config): mark slice 1C.2 merged with HTTP-pattern d…
May 5, 2026
c31291e
feat: 1C.3: extend dial-cli read to 8 admin-config types
May 5, 2026
f7c8f01
feat: 1C.4: dial-cli export streams /v1/admin/export to stdout / file
May 5, 2026
63012c4
feat: 1C.5: dial-cli diff --source --target structural diff
May 5, 2026
10dce52
feat: 1C.6: dial-cli completion bash | zsh | fish
May 5, 2026
ca1dace
feat: 2C.0: dial-cli model add (POST) with --dry-run
May 5, 2026
655ffd3
feat: 2C.1: dial-cli model update (PUT) with --set + --if-match
May 5, 2026
aa1999f
feat: 2C.2: dial-cli model delete with --if-match
May 5, 2026
3394c63
feat: 2C.3: dial-cli model validate against POST /v1/admin/validate
May 5, 2026
993464b
feat: 2C.4: dial-cli model promote --from --to (as-is mode)
May 5, 2026
eff7491
feat: 2C.5: dial-cli model diff --source --target (single-type)
May 5, 2026
ff13725
feat: 3C.0: dial-cli write commands for all admin-config entity types
May 5, 2026
5f50e2a
docs(dial-unified-config): mark slice 3C.0 merged
May 5, 2026
e36e1af
fix: declare cli lombok task ordering vs Quarkus generated-sources co…
May 5, 2026
66be190
feat: 4C.0: dial-cli apply -f for fully-resolved manifests
May 5, 2026
31778c5
docs(dial-unified-config): mark slice 4C.0 merged
May 5, 2026
d00571b
chore: Dist.1: bundle dial-cli uber-jar into ai-dial-core image
May 5, 2026
7de3eb1
docs(dial-unified-config): file Cli.1 + Cli.2 follow-ons surfaced by …
May 5, 2026
0c0993c
fix: Cli.1: honor DIAL_CLI_CONFIG env var in ProfileLoader
May 5, 2026
6a975e5
fix: Cli.2: wire dial-cli --version via @Command version attribute
May 5, 2026
b97d019
chore: Dist.2: sample/dial-cli/ newcomer playground
May 5, 2026
a6ad1dd
docs: Dist.2: fix docker-alias profile path, expand README with verb …
May 6, 2026
bf51051
feat: Cli.3: -o INHERIT scope + friendly HTTP errors + strip-projecti…
May 8, 2026
df03988
feat: Cli.4: --from-file accepts manifest envelopes on add/validate
May 9, 2026
288192b
docs(dial-unified-config): promote 4C.1–4C.5 + 4C.7 into MVP scope
May 9, 2026
0f6e18f
feat: 4C.1: template DSL (extends/includes/!if/!for + functions + pla…
May 10, 2026
98c07c5
docs(dial-unified-config): mark slice 4C.1 merged with retrospective
May 10, 2026
0497334
feat: 4C.7: dial-cli apply -f accepts a directory
May 10, 2026
507924d
docs(dial-unified-config): mark slice 4C.7 merged with retrospective
May 10, 2026
b7f20e4
feat: 4C.4: ${SECRET:*} resolver via System.getenv + ${ENV_VAR} fallback
May 10, 2026
2162cc7
docs(dial-unified-config): mark slice 4C.4 merged with retrospective
May 10, 2026
67e5fc0
feat: 4C.2: dial-cli apply --overlay env-overlay support
May 11, 2026
e793e61
docs(dial-unified-config): mark slice 4C.2 merged with retrospective
May 11, 2026
a4d1c3f
docs(dial-cli): expand sample playground to cover landed Phase-4 feat…
May 14, 2026
2604275
fix: Cli.5: route overlay kinds in base manifests to --overlay hint
May 14, 2026
1c5dc0d
fix: Cli.6: resolve placeholders in quoted !if expressions
May 14, 2026
31f0404
feat: 4C.3: dial-cli apply expands kind: Bundle manifests
May 14, 2026
5ff3ba8
docs(dial-unified-config): mark slice 4C.3 merged with retrospective
May 14, 2026
eccfe90
update cli samples
May 14, 2026
1d5d965
feat: 4C.5: dial-cli promote --template <name|auto> + hostname warning
May 14, 2026
de017f7
docs(dial-unified-config): mark slice 4C.5 merged with retrospective
May 14, 2026
64c88a3
docs(dial-unified-config): escape pipes in 4C.1/4C.5/4C.7 register rows
May 15, 2026
2524e9c
docs(dial-cli): cover 4C.5 promote --template in sample README
May 15, 2026
481d621
feat: extract env resolution into separate EnvResolver
KirylKurnosenka May 15, 2026
f7b49a4
feat: extract render logic from EntityReader into per-format renderers
KirylKurnosenka May 15, 2026
f1d61ab
feat: remove PLAN file
KirylKurnosenka May 15, 2026
13f85d7
feat: replace Auth.type String with AuthType enum
KirylKurnosenka May 15, 2026
3c1872c
feat: code cleanup
KirylKurnosenka May 15, 2026
cb0fb51
feat: fix URI encoding in CliHttpClient
KirylKurnosenka May 15, 2026
62f2000
feat: honour defaults.output from profile config in output format res…
KirylKurnosenka May 18, 2026
07f8741
feat: preserve config file formatting on env use
KirylKurnosenka May 18, 2026
64aaa61
feat: delegate missing-arg validation to picocli and introduce SINGLE…
KirylKurnosenka May 18, 2026
692ce4c
feat: delegate missing-arg validation to picocli and introduce SINGLE…
KirylKurnosenka May 18, 2026
b7366bd
feat: simplify CompletionCommand — drop unused parent field, delegate…
KirylKurnosenka May 18, 2026
3066bb7
feat: remove export and diff commands — server dropping /v1/admin/export
KirylKurnosenka May 19, 2026
58a6679
feat: remove unused --verbose option from DialCli
KirylKurnosenka May 19, 2026
68a0f7f
feat: force HTTP/1.1 in CliHttpClient to fix 505 responses
KirylKurnosenka May 19, 2026
8b595fc
feat: align CLI with unified-config-server API changes (U.0 + U.1)
KirylKurnosenka May 25, 2026
c19b197
docs: align CLI docs with current source after verbose/diff/export re…
KirylKurnosenka May 25, 2026
ad1165c
docs: align dial-unified-config-lite docs with current CLI source
KirylKurnosenka May 26, 2026
50dbe04
fix: make --source enum case-insensitive and update option descriptions
KirylKurnosenka May 26, 2026
1c590d3
refactor: decouple EntityWriter error handling via CliException + IEx…
KirylKurnosenka May 28, 2026
f4c13c7
refactor: remove CommandSpec coupling from EnvResolver by throwing Cl…
KirylKurnosenka May 28, 2026
37eb235
refactor: make CliConfigException extend CliException for uniform err…
KirylKurnosenka May 28, 2026
067d580
refactor: replace DialCliCommandLineFactory subclass with producer-ba…
KirylKurnosenka May 28, 2026
5b3715f
refactor: replace manual --param parsing with picocli map option + Pa…
KirylKurnosenka May 28, 2026
3de8018
refactor: extract JsonPatcher + move --set to Map<String, JsonNode> w…
KirylKurnosenka May 28, 2026
70cc3f9
refactor: change addEntity/updateEntity/deleteEntity return type to void
KirylKurnosenka May 28, 2026
8f996dd
refactor: replace ifMatch/ifNoneMatch params with Map<String, String>…
KirylKurnosenka May 29, 2026
104a8a7
chore: remove non-existent streamingSupported field from samples and …
KirylKurnosenka Jun 1, 2026
5549357
fix: Key.key is required — remove auto-generation claim and add ${SEC…
KirylKurnosenka Jun 1, 2026
d3c1278
fix: JsonPatcher — escape ~ and / in --set path segments (RFC 6901)
KirylKurnosenka Jun 3, 2026
29beb1a
fix: strip projection fields from GET response before promote apply e…
KirylKurnosenka Jun 3, 2026
a3eacb1
fix: env list/current ignore --env override, show profile default only
KirylKurnosenka Jun 3, 2026
8efbb8f
refactor: consolidate env-name resolution into EnvResolver
KirylKurnosenka Jun 3, 2026
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
8 changes: 8 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ COPY --chown=gradle:gradle . /home/gradle/src
WORKDIR /home/gradle/src
RUN --mount=type=secret,id=GPR_USERNAME,env=GPR_USERNAME --mount=type=secret,id=GPR_PASSWORD,env=GPR_PASSWORD gradle --no-daemon build --stacktrace -PdisableCompression=true -x test
RUN mkdir /build && tar -xf /home/gradle/src/server/build/distributions/server*.tar --strip-components=1 -C /build
RUN cp /home/gradle/src/cli/build/cli-*-runner.jar /tmp/dial-cli.jar

FROM eclipse-temurin:21-jdk-alpine

Expand All @@ -24,6 +25,13 @@ RUN adduser -u 1001 --disabled-password --gecos "" appuser
COPY --from=builder --chown=appuser:appuser /build/ .
RUN chown -R appuser:appuser /app

# dial-cli alpha distribution: same image doubles as a CLI runner via
# `docker run <img> dial-cli …`; docker-entrypoint.sh exec's non-empty argv as-is.
RUN mkdir -p /opt/cli && chown appuser:appuser /opt/cli
COPY --from=builder --chown=appuser:appuser /tmp/dial-cli.jar /opt/cli/dial-cli.jar
RUN printf '#!/bin/sh\nexec java -jar /opt/cli/dial-cli.jar "$@"\n' > /usr/local/bin/dial-cli \
&& chmod +x /usr/local/bin/dial-cli

COPY --chown=appuser:appuser docker-entrypoint.sh /usr/local/bin/
RUN chmod +x /usr/local/bin/docker-entrypoint.sh

Expand Down
38 changes: 38 additions & 0 deletions cli/build.gradle
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
plugins {
id 'io.quarkus' version "3.16.4"
}

dependencies {
// Quarkus BOM — pins Picocli, Jackson, Arc, JUnit 5 versions transitively.
implementation enforcedPlatform("io.quarkus.platform:quarkus-bom:3.16.4")

implementation "io.quarkus:quarkus-picocli"
implementation "io.quarkus:quarkus-arc"
implementation "com.fasterxml.jackson.core:jackson-databind"
implementation "com.fasterxml.jackson.dataformat:jackson-dataformat-yaml"

// Reuse DIAL Core data classes (Config, Model, Application, …) — design 05 §6.
implementation project(':config')

testImplementation "org.junit.jupiter:junit-jupiter"
testImplementation "org.assertj:assertj-core:3.27.3"
testImplementation "org.mockito:mockito-core"
testImplementation "org.mockito:mockito-junit-jupiter"
testRuntimeOnly "org.junit.jupiter:junit-jupiter-engine"
}

application {
mainClass = 'com.epam.aidial.cli.DialCli'
}

test {
useJUnitPlatform()
}

// Quarkus registers `build/classes/java/quarkus-generated-sources/{grpc,avdl,avpr,avsc}` as
// source dirs of the main source set, and `compileQuarkusGeneratedSourcesJava` outputs into
// the same locations that `generateEffectiveLombokConfig` walks. Without this dependency
// Gradle 8.10+ flags an implicit-dependency validation error and the CI build fails.
tasks.matching { it.name == 'generateEffectiveLombokConfig' }.configureEach {
mustRunAfter('compileQuarkusGeneratedSourcesJava')
}
206 changes: 206 additions & 0 deletions cli/src/main/java/com/epam/aidial/cli/ApplicationCommand.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,206 @@
package com.epam.aidial.cli;

import com.fasterxml.jackson.databind.JsonNode;
import picocli.CommandLine.Command;
import picocli.CommandLine.Model.CommandSpec;
import picocli.CommandLine.Option;
import picocli.CommandLine.Parameters;
import picocli.CommandLine.ParentCommand;
import picocli.CommandLine.Spec;
import picocli.CommandLine.UseDefaultConverter;

import java.nio.file.Path;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.Callable;

@Command(
name = "application",
description = "Manage DIAL application entities.",
mixinStandardHelpOptions = true,
subcommands = {ApplicationCommand.Get.class, ApplicationCommand.List.class,
ApplicationCommand.Add.class, ApplicationCommand.Update.class,
ApplicationCommand.Delete.class, ApplicationCommand.Validate.class,
ApplicationCommand.Promote.class, ApplicationCommand.Diff.class}
)
public class ApplicationCommand {

static final String TYPE = "applications";
static final String BUCKET = "public";
static final String KIND = "Application";
static final String CANONICAL_PREFIX = TYPE + "/" + BUCKET + "/";

@ParentCommand
DialCli parent;

@Command(name = "get", description = "Get a single application by name (or canonical id).")
static class Get implements Callable<Integer> {
@ParentCommand
ApplicationCommand cmd;
@Spec
CommandSpec spec;
@Parameters(index = "0", description = "Application name or canonical id (applications/<bucket>/<name>).")
String name;
@Option(names = "--source", description = "Config source: ${COMPLETION-CANDIDATES} (default: ${DEFAULT-VALUE}).", defaultValue = "api")
ConfigSource source;

@Override
public Integer call() {
return source == ConfigSource.FILE
? EntityReader.readConfigFileEntity(cmd.parent, spec, TYPE, name)
: EntityReader.readEntity(cmd.parent, spec, TYPE, name);
}
}

@Command(name = "list", description = "List applications in the public bucket.")
static class List implements Callable<Integer> {
@ParentCommand
ApplicationCommand cmd;
@Spec
CommandSpec spec;
@Option(names = "--source", description = "Config source: ${COMPLETION-CANDIDATES} (default: ${DEFAULT-VALUE}).", defaultValue = "api")
ConfigSource source;

@Override
public Integer call() {
return source == ConfigSource.FILE
? EntityReader.listConfigFileEntities(cmd.parent, spec, TYPE)
: EntityReader.listEntities(cmd.parent, spec, TYPE);
}
}

@Command(name = "add", description = "Create an application (PUT with If-None-Match: *). Fails with exit 5 if it already exists.")
static class Add implements Callable<Integer> {
@ParentCommand
ApplicationCommand cmd;
@Spec
CommandSpec spec;
@Option(names = "--name", required = true,
description = "Canonical id (applications/public/<name>).")
String name;
@Option(names = "--from-file", required = true,
description = "JSON or YAML file with the application spec (.yaml/.yml parsed as YAML).")
Path fromFile;
@Option(names = "--template", description = "Template name from CLI profile to apply.")
String template;
@Option(names = "--param", description = "Template parameter 'key=value' (repeatable).", converter = {UseDefaultConverter.class, ParamValueConverter.class})
Map<String, Object> params = new HashMap<>();

@Override
public Integer call() {
EntityWriter.addEntity(cmd.parent, spec, TYPE, KIND, BUCKET, name, fromFile, template, params);
return 0;
}
}

@Command(name = "update",
description = "Update an application (PUT). Fails with exit 4 if it does not exist, 6 on stale ETag.")
static class Update implements Callable<Integer> {
@ParentCommand
ApplicationCommand cmd;
@Spec
CommandSpec spec;
@Parameters(index = "0", description = "Canonical id (applications/public/<name>).")
String name;
@Option(names = "--set", description = "Field override 'dot.path=value' (repeatable). Keys are dot-paths; values are JSON-coerced.",
converter = {UseDefaultConverter.class, JsonNodeValueConverter.class})
Map<String, JsonNode> sets;
@Option(names = "--if-match", description = "ETag for optimistic concurrency. Defaults to the GET response's ETag.")
String ifMatch;

@Override
public Integer call() {
EntityWriter.updateEntity(cmd.parent, spec, TYPE, BUCKET, name, sets, ifMatch);
return 0;
}
}

@Command(name = "delete", description = "Delete an application (DELETE). Fails with exit 4 if missing, 6 on stale ETag.")
static class Delete implements Callable<Integer> {
@ParentCommand
ApplicationCommand cmd;
@Spec
CommandSpec spec;
@Parameters(index = "0", description = "Canonical id (applications/public/<name>).")
String name;
@Option(names = "--if-match", description = "ETag for optimistic concurrency.")
String ifMatch;

@Override
public Integer call() {
EntityWriter.deleteEntity(cmd.parent, spec, TYPE, BUCKET, name, ifMatch);
return 0;
}
}

@Command(name = "validate",
description = "Validate a proposed application spec via POST /v1/admin/validate (no write).")
static class Validate implements Callable<Integer> {
@ParentCommand
ApplicationCommand cmd;
@Spec
CommandSpec spec;
@Option(names = "--name", required = true,
description = "Canonical id (applications/public/<name>).")
String name;
@Option(names = "--from-file", required = true,
description = "JSON or YAML file with the application spec (.yaml/.yml parsed as YAML).")
Path fromFile;
@Option(names = "--template", description = "Template name from CLI profile to apply.")
String template;
@Option(names = "--param", description = "Template parameter 'key=value' (repeatable).", converter = {UseDefaultConverter.class, ParamValueConverter.class})
Map<String, Object> params = new HashMap<>();

@Override
public Integer call() {
return EntityWriter.validateEntity(cmd.parent, spec, TYPE, KIND, BUCKET, name, fromFile, template, params);
}
}

@Command(name = "promote",
description = "Promote an application from one environment to another via POST /v1/admin/apply.")
static class Promote implements Callable<Integer> {
@ParentCommand
ApplicationCommand cmd;
@Spec
CommandSpec spec;
@Option(names = "--from", required = true, description = "Source environment.")
String fromEnv;
@Option(names = "--to", required = true, description = "Target environment.")
String toEnv;
@Option(names = "--name", required = true,
description = "Canonical id (applications/public/<name>).")
String name;
@Option(names = "--template",
description = "Template name from CLI profile, or 'auto' to reverse-match against the source entity.")
String template;
@Option(names = "--param", description = "Template parameter 'key=value' (repeatable).", converter = {UseDefaultConverter.class, ParamValueConverter.class})
Map<String, Object> params = new HashMap<>();

@Override
public Integer call() {
return EntityWriter.promoteEntity(cmd.parent, spec, TYPE, KIND, BUCKET, name, fromEnv, toEnv, template, params);
}
}

@Command(name = "diff",
description = "Structural diff of a single application (with --name) or all applications between two environments.")
static class Diff implements Callable<Integer> {

@ParentCommand
ApplicationCommand cmd;
@Spec
CommandSpec spec;
@Option(names = "--source", required = true, description = "Source environment.")
String sourceEnv;
@Option(names = "--target", required = true, description = "Target environment.")
String targetEnv;
@Option(names = "--name", description = "Optional canonical id (applications/public/<name>) for a single-entity diff.")
String name;

@Override
public Integer call() {
return EntityDiff.run(cmd.parent, spec, TYPE, BUCKET, CANONICAL_PREFIX, sourceEnv, targetEnv, name);
}
}
}
Loading