Skip to content

feat: dial-unified-config — dial-cli (Picocli + Quarkus CLI)#1531

Open
siarhei-fedziukovich wants to merge 80 commits into
feature/unified-config-serverfrom
feature/unified-config-cli
Open

feat: dial-unified-config — dial-cli (Picocli + Quarkus CLI)#1531
siarhei-fedziukovich wants to merge 80 commits into
feature/unified-config-serverfrom
feature/unified-config-cli

Conversation

@siarhei-fedziukovich
Copy link
Copy Markdown
Contributor

  • feat: 1C.0: bootstrap :cli module with Picocli + Quarkus skeleton
  • docs(dial-unified-config): mark slice 1C.0 merged with B1 keystore deferral
  • feat: 1C.1: dial-cli env list / current / use / check
  • docs(dial-unified-config): mark slice 1C.1 merged with config-only env-check note
  • feat: 1C.2: dial-cli model get / list + get-models alias + HTTP client
  • docs(dial-unified-config): mark slice 1C.2 merged with HTTP-pattern decisions
  • feat: 1C.3: extend dial-cli read to 8 admin-config types
  • docs(dial-unified-config): mark slice 1C.3 merged with Reading A scope narrowing
  • feat: 1C.4: dial-cli export streams /v1/admin/export to stdout / file
  • docs(dial-unified-config): mark slice 1C.4 merged with format-negotiation note
  • feat: 1C.5: dial-cli diff --source --target structural diff
  • docs(dial-unified-config): mark slice 1C.5 merged with structural-diff note
  • feat: 1C.6: dial-cli completion bash | zsh | fish
  • docs(dial-unified-config): mark slice 1C.6 merged with fish caveat
  • feat: 2C.0: dial-cli model add (POST) with --dry-run
  • docs(dial-unified-config): mark slice 2C.0 merged with design-call notes
  • feat: 2C.1: dial-cli model update (PUT) with --set + --if-match
  • docs(dial-unified-config): mark slice 2C.1 merged with design-call notes
  • feat: 2C.2: dial-cli model delete with --if-match
  • docs(dial-unified-config): mark slice 2C.2 merged with design-call notes
  • feat: 2C.3: dial-cli model validate against POST /v1/admin/validate
  • docs(dial-unified-config): mark slice 2C.3 merged with design-call notes
  • feat: 2C.4: dial-cli model promote --from --to (as-is mode)
  • docs(dial-unified-config): mark slice 2C.4 merged with Option A narrowing
  • feat: 2C.5: dial-cli model diff --source --target (single-type)
  • docs(dial-unified-config): mark slice 2C.5 merged with design-call notes
  • feat: 3C.0: dial-cli write commands for all admin-config entity types
  • docs(dial-unified-config): mark slice 3C.0 merged
  • fix: declare cli lombok task ordering vs Quarkus generated-sources compile
  • feat: 4C.0: dial-cli apply -f for fully-resolved manifests
  • docs(dial-unified-config): mark slice 4C.0 merged
  • chore: Dist.1: bundle dial-cli uber-jar into ai-dial-core image
  • docs(dial-unified-config): file Cli.1 + Cli.2 follow-ons surfaced by Dist.1
  • fix: Cli.1: honor DIAL_CLI_CONFIG env var in ProfileLoader
  • fix: Cli.2: wire dial-cli --version via @command version attribute
  • chore: Dist.2: sample/dial-cli/ newcomer playground
  • docs: Dist.2: fix docker-alias profile path, expand README with verb cookbook
  • feat: Cli.3: -o INHERIT scope + friendly HTTP errors + strip-projection-on-PUT
  • docs(dial-unified-config): backfill Cli.3 commit SHA
  • feat: Cli.4: --from-file accepts manifest envelopes on add/validate
  • docs(dial-unified-config): backfill Cli.4 commit SHA
  • docs(dial-unified-config): promote 4C.1–4C.5 + 4C.7 into MVP scope
  • feat: 4C.1: template DSL (extends/includes/!if/!for + functions + placeholders)
  • docs(dial-unified-config): mark slice 4C.1 merged with retrospective
  • feat: 4C.7: dial-cli apply -f accepts a directory
  • docs(dial-unified-config): mark slice 4C.7 merged with retrospective
  • feat: 4C.4: ${SECRET:*} resolver via System.getenv + ${ENV_VAR} fallback
  • docs(dial-unified-config): mark slice 4C.4 merged with retrospective
  • feat: 4C.2: dial-cli apply --overlay env-overlay support
  • docs(dial-unified-config): mark slice 4C.2 merged with retrospective

Applicable issues

  • fixes #

Description of changes

Checklist

By submitting this pull request, I confirm that my contribution is made under the terms of the Apache 2.0 license.

@siarhei-fedziukovich
Copy link
Copy Markdown
Contributor Author

siarhei-fedziukovich commented May 11, 2026

/deploy-review

GitHub actions run: 25695681701

Stage Status
deploy-review Success ✅
chat Success ✅

Comment thread cli/src/main/java/com/epam/aidial/cli/config/Auth.java Outdated
Comment thread cli/src/main/java/com/epam/aidial/cli/config/Defaults.java Outdated
@siarhei-fedziukovich
Copy link
Copy Markdown
Contributor Author

siarhei-fedziukovich commented May 14, 2026

/deploy-review

GitHub actions run: 25872894570

Stage Status
deploy-review Success ✅
chat Success ✅

@siarhei-fedziukovich
Copy link
Copy Markdown
Contributor Author

siarhei-fedziukovich commented May 15, 2026

/deploy-review

GitHub actions run: 25920268735

Stage Status
deploy-review Success ✅
chat Failed ❌

Comment thread cli/src/main/java/com/epam/aidial/cli/EnvCommand.java Outdated
Comment thread cli/src/main/java/com/epam/aidial/cli/DiffCommand.java Outdated
Comment thread cli/src/main/java/com/epam/aidial/cli/ExportCommand.java Outdated
@KirylKurnosenka KirylKurnosenka force-pushed the feature/unified-config-cli branch 2 times, most recently from ee00e6c to a2400d6 Compare May 25, 2026 15:21
@KirylKurnosenka
Copy link
Copy Markdown

KirylKurnosenka commented May 26, 2026

/deploy-review

GitHub actions run: 26446894825

Stage Status
deploy-review Success ✅
chat Queued ⏳

@KirylKurnosenka KirylKurnosenka force-pushed the feature/unified-config-cli branch from d1b1754 to a4fa152 Compare June 1, 2026 19:06
@KirylKurnosenka
Copy link
Copy Markdown

KirylKurnosenka commented Jun 2, 2026

/deploy-review

GitHub actions run: 26817130712

Stage Status
deploy-review Success ✅
chat Failed ❌

@KirylKurnosenka
Copy link
Copy Markdown

KirylKurnosenka commented Jun 2, 2026

/deploy-review

GitHub actions run: 26851620705

Stage Status
deploy-review Success ✅
chat Success ✅

@KirylKurnosenka
Copy link
Copy Markdown

KirylKurnosenka commented Jun 3, 2026

/deploy-review

GitHub actions run: 26873338963

Stage Status
deploy-review Success ✅
chat Failed ❌

@KirylKurnosenka
Copy link
Copy Markdown

KirylKurnosenka commented Jun 3, 2026

/deploy-review

GitHub actions run: 26883450725

Stage Status
deploy-review Success ✅
chat Failed ❌

SiarheiFedziukovich and others added 12 commits June 3, 2026 19:52
Adds the foundational :cli sibling Gradle module: Quarkus 3.16.4 + Picocli
@topcommand entrypoint with design 05 §1 global flags, env/get stub
subcommands, ~/.dial-cli/config.yaml profile loader (Jackson YAML, snake_case),
and API-key resolution chain (env var → --api-key-file → no-echo prompt).
Keystore tier deferred to post-MVP per slice 1C.0 row B1 — ships with auth
login once OIDC lands. JVM-mode only per IMPLEMENTATION.md §3.4.

Design anchors: 05 §1, §2, §6
Tests: cli/src/test/.../{DialCliSmokeTest,ProfileLoaderTest,ApiKeyResolverTest}.java

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…ferral

Slice 1C.0 (`feat: 1C.0: bootstrap :cli module with Picocli + Quarkus
skeleton`, ff2ae5d) ships JVM-mode CLI bootstrap. Row updated: status
✅, commit pinned, scope-narrowing footnote added explaining the
architect-plan halt — Ambiguity A (chain order = design 06 §2.1) and
Ambiguity B1 (keystore tier deferred until `auth login` ships post-MVP).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Wires four Phase-1 env subcommands on the 1C.0 skeleton: list (sorted,
current marked '*'), current (--env > defaults.env), use (validates env
exists, atomic-rename save), check (config-only — reachability probe
deferred to 1C.2). Adds ProfileLoader.save + ApiKeyResolver.describeSource.

Design anchors: 05 §1
Tests: cli/src/test/.../EnvCommandTest.java + extensions

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…v-check note

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Establishes HTTP read pattern: CliHttpClient (JDK, redirects normal,
Api-Key header), inline output formatter (json/yaml/table — NAME+ENDPOINT
cols), ModelCommand Get/List + GetCommand dispatch. identifierToPath
rejects ambiguous partial canonical IDs and URL-encodes simple names.
HTTP→exit codes per 06 §2.8 (401/403→3, 404→4, 5xx→1).

Design anchors: 05 §1; 06 §2.2
Tests: ModelCommandTest, CliHttpClientTest

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…ecisions

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Extracts EntityReader from ModelCommand; adds 8 per-type command classes
(applications, toolsets, interceptors, roles, keys, routes, schemas,
settings) as thin shims. Per-type bucket map (public/ vs platform/)
+ per-type table shape (NAME-only default, models keep NAME+ENDPOINT).
SettingsCommand is singleton — Get only, no name arg. GetCommand alias
extended to all 9 plurals. listEntities warns when hasMore=true.
identifierToPath rejects unknown types and ambiguous partial canonical IDs.
Files/prompts/conversations deferred (not in 1C.3 dep set).

Design anchors: 05 §1
Tests: EntityReaderTypesTest (13), DialCliSmokeTest extensions (3 new)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds ExportCommand using EntityReader.resolveEnv (promoted package-private)
+ CliHttpClient.get(path, accept) overload. Format negotiation via global
-o flag: yaml→application/yaml, json/table→application/json. -f/--output-file
writes to file (creates parent dirs, rejects directory paths). UTF-8
charset pinned in BodyHandlers.ofString to avoid mojibake on non-ASCII
entity names. HTTP error → exit code per 06 §2.8.

Design anchors: 05 §1; 03 §1
Tests: ExportCommandTest (8 cases)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Two GETs to /v1/admin/export per env, structural Jackson-tree diff with
dotted path notation. JsonDiff utility (added/removed/changed; arrays
opaque). DiffCommand uses EntityReader.resolveEnv(parent, spec, explicitEnv)
overload — global --api-url override is ignored when explicit env is given
(prevents diff hitting the same URL twice). Path-only output; operators
use 'dial-cli get -o yaml' for values.

Design anchors: 05 §1
Tests: JsonDiffTest (10), DiffCommandTest (9)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
CompletionCommand uses Picocli's AutoComplete.bash() for bash and zsh
(zsh-bash compat), rejects fish with exit 2 (Picocli has no fish
generator; documented for follow-up). Unknown shell or missing arg
returns exit 2 with helpful message.

Design anchors: 05 §1
Tests: CompletionCommandTest (6 cases — bash, zsh, all-subcommands, fish, unknown, missing)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds the first CLI write command. EntityWriter sibling helper handles
canonical-id parsing (rejects simple names per design 05 §1), JSON/YAML
file loading via extension-sniff, --dry-run local preview, and POST
exit-code mapping (0/5/2/3 per 06 §2.8). CliHttpClient gains post()
plus 409->5 / 400->2 mappings; Response record exposes ETag for future
--if-match (2C.1).

Design anchors: 05 §1, 06 §2.4, §2.8
Tests: cli/src/test/.../ModelCommandTest.java, CliHttpClientTest.java

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds Update subcommand to ModelCommand. EntityWriter.updateEntity does
GET → local-merge → PUT, auto-threading the GET ETag as If-Match (operator
override via --if-match). --set k=v parses dotted paths, JSON-coerces
values, deep-merges into the GET body. CliHttpClient gains put(...) and
toExitCode now maps 412→6 per 06 §2.8.

Design anchors: 05 §1 lines 58, 88-95; 06 §2.4 lines 326-381
Tests: cli/src/test/java/com/epam/aidial/cli/{ModelCommandTest,http/CliHttpClientTest}.java

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
KirylKurnosenka and others added 27 commits June 3, 2026 19:53
Introduces AuthType enum (API_KEY) so auth type is validated at
deserialization time instead of relying on raw strings. Configures
YAMLMapper with case-insensitive enum reads and lowercase enum writes to
stay compatible with existing YAML profiles. Test deps de-versioned to
resolve from the BOM.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
buildUri now uses the 7-arg URI constructor so path segments and query
values are percent-encoded correctly (spaces as %20, not +).

All HTTP methods gain no-query overloads so callers without query params
need not pass null. Methods that do carry a query (entity listing with
limit=100) pass it as a separate argument instead of embedding it in
the path string.

URLEncoder.encode() removed from EntityReader and EntityWriter — encoding
is now CliHttpClient's responsibility exclusively.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…olution

Remove the hard-coded Picocli defaultValue so --output is null when
unset, then resolve via OutputFormatResolver: CLI flag > defaults.output
in config > TABLE fallback.

Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
Switching the active environment via `dial env use` previously
round-tripped the config through Jackson serialization, which stripped
comments, blank lines, and !if/!for custom tags from the templates
section.

Introduce YamlPatcher, a text-level YAML patcher that walks the raw
file line-by-line using an indentation-based state machine and updates
only the exact dot-path requested (e.g. defaults.env), leaving every
other byte unchanged. ProfileLoader.saveDefaultEnv delegates to it,
and the old full-profile save() is removed since it is no longer called
from production code.

Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
…TON_TYPES in GetCommand

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…TON_TYPES in GetCommand

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
… missing-arg check to picocli, use enhanced switch

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
--verbose was declared but never referenced anywhere in the codebase.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Java's HttpClient defaults to HTTP/2; servers that only support HTTP/1.1
reject the upgrade with 505. Pinning to HTTP_1_1 fixes all CLI requests.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- CliHttpClient: add 5-param put() overload with If-None-Match support
- EntityWriter: POST → PUT with If-None-Match:* for addEntity; map 412 → exit 5; drop 'source' from projection fields
- EntityReader: listings moved to /v1/metadata/{type}/{bucket}/ (nextToken pagination); add readConfigFileEntity, listConfigFileEntities, readConfigFileSingleton for /v1/admin/config/file/* surface
- EntityRenderer: split renderList → renderMetadataList + renderFileList; TableEntityRenderer uses url field for metadata items, name field for file-config items; SOURCE column retired
- All entity commands (model/application/toolset/interceptor/role/key/route/schema/settings/get): --source option changed from String to ConfigSource enum
- Tests: update listing mock URLs to /v1/metadata/..., response shape to {url:...}, hasMore → nextToken

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…movals

- Remove --verbose global flag (dropped in 3594e4c)
- Replace top-level `dial-cli diff` with per-entity `<type> diff` syntax
  (cross-type diff dropped alongside /v1/admin/export in 6667899)
- Add --source {API|FILE} flag documentation to list/get commands (U.1, f2ca7d9)
- Fix settings reset → settings delete (no reset alias in SettingsCommand)
- Remove OS keystore from credential chain (not implemented; 3-step chain only)
- Add fish-not-supported caveat to shell completion docs

Touches: 05-cli-design.md, 06-cli-user-guide.md, 03-api-reference.md,
07-migration-and-rollout.md, IMPLEMENTATION.md, sample/dial-cli/README.md,
sample/dial-cli/manifests/base/09-settings.yaml

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Fix settings reset → settings delete (no reset alias in SettingsCommand)
- Remove OS keystore from credential chain (not implemented)
- Fix dial-cli promote → dial-cli <type> promote (no top-level promote command)
- Add --source {API|FILE} flag to list/get rows
- Completion: fish not yet supported
- Remove OS keystore from ${SECRET:} namespace description

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Add @jsonvalue toString() to ConfigSource returning lowercase so Picocli
matches user input (api/file) via toString() fallback, mirroring OutputFormat.
Update all --source option descriptions to use ${COMPLETION-CANDIDATES} and
${DEFAULT-VALUE}, and change defaultValue to lowercase "api" for consistency.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…ecutionExceptionHandler

Introduce CliException (unchecked, carries exit code) and DialExceptionHandler
(picocli IExecutionExceptionHandler) so EntityWriter throws on all failure paths
instead of catching exceptions and writing to stderr itself — analogous to
Spring's @RestControllerAdvice. NetworkException now extends CliException(exitCode=1).
DialCliCommandLineFactory registers the handler for the Quarkus production path;
test run() helpers install it directly on CommandLine.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…iException

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…or handling

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…sed CDI wiring

Removes the DefaultPicocliCommandLineFactory subclass that caused an
AmbiguousResolutionException at startup (two beans satisfying the
PicocliCommandLineFactory injection point). Introduces CliConfiguration
with a @produces CommandLine method and DialCliFactory as a plain static
utility for test use.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…ramValueConverter

Removes the hand-rolled parseParams/parseParamValue loop (indexOf-based key=value
splitting) in EntityWriter. picocli now owns key=value splitting via Map<String, Object>
with an explicit two-converter declaration:
  converter = {UseDefaultConverter.class, ParamValueConverter.class}
UseDefaultConverter handles keys (String identity); ParamValueConverter handles values,
converting [a,b,c] bracket notation to List<String> and leaving plain strings as-is.

All nine entity-command classes and ApplyCommand updated to Map<String, Object> params
initialised to new HashMap<>() so callers always receive a non-null map without
defensive guards inside EntityWriter.

Integration tests added to TemplateResolutionTest covering: string param substitution,
multiple params, param value containing '=', and invalid param format (no '=').

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…ith picocli converter

- Add JsonNodeValueConverter: ITypeConverter<JsonNode> that tries JSON parse,
  falls back to TextNode — mirrors ParamValueConverter pattern
- Extract applySets logic into JsonPatcher.apply(ObjectNode, Map<String,JsonNode>);
  refactor traversal to use Jackson JsonPointer + withObject() instead of manual
  segment loop
- Add JsonPatcherTest covering null/empty no-ops, scalar/array/object values,
  nested and deep auto-created paths, and error cases (empty segment, non-object
  intermediate)
- Change --set option in all 9 Update subcommands from List<String> to
  Map<String,JsonNode> with {UseDefaultConverter, JsonNodeValueConverter};
  value parsing now happens at picocli parse time rather than in application code
- Remove applySets and parseSetValue from EntityWriter

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
These methods either succeed or throw CliException — they never return a
meaningful non-zero value. Changing to void removes the IDE warning
"Method always returns 0" and makes the contract explicit. Callers now
invoke the method as a statement and return 0 themselves.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
… headers in CliHttpClient

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…docs

Features.java has no streamingSupported field; Jackson would silently
ignore it. Removed from config.yaml, 06-model.json, and both doc files.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…RET:} placeholders

The server rejects a blank Key.key with 400 ("Key.key must be provided
explicitly"); auto-generation was never implemented despite OQ-12 saying
so, and --reveal-secrets does not exist (retired in U.4).

- Replace incorrect comment in 04-key.yaml; add key: "${SECRET:DIAL_CI_KEY}"
- Add key field to 04-key.json, 00-batch-array.json, and the bundle Key entry
- Correct OQ-12 to reflect that key is required, not server-generated
- Update README Caveats with the three env vars callers must export

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Dot-path segments were naively joined with '/' to build a JsonPointer,
so a key like 'models/public/gpt-4' was split into three segments by
JsonPointer. Fix: split on dot, then escape '~'→'~0' and '/'→'~1'
per RFC 6901 before compiling the pointer. Setting a path to null now
removes the field instead of writing a JSON null.

Adds tests for slash-in-key, tilde-in-key, both combined, and all null
removal cases. Updates README and sandbox design docs to reflect the
corrected dot-path syntax and null-removal behaviour.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…nvelope

Name, status, and validationWarnings leaked into the manifest spec sent to
/v1/admin/apply, causing Jackson to fail with "Failed to parse entity at
line -1, column -1". Mirror the same remove(PROJECTION_FIELDS) pattern used
in updateEntity.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@KirylKurnosenka KirylKurnosenka force-pushed the feature/unified-config-cli branch from 38e00fe to 29beb1a Compare June 3, 2026 16:54
KirylKurnosenka and others added 2 commits June 3, 2026 20:29
--env on list/current was misleading — it could print a non-existent
environment name as the active one. Both commands now read exclusively
from defaults.env in the profile; resolveCurrent (which honours --env)
is still used by env check where the override is intentional.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Move resolveDefault/resolveCurrent from EnvCommand into EnvResolver so
both the command layer and resolveEnv share one implementation. Replace
the explicitEnv == null guard in resolveEnv with a named crossEnv boolean
to make the api-url override intent explicit.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants