feat(ailogic): add ailogic:templates CLI commands - #10853
Conversation
Add `firebase ailogic:providers:{enable,disable,list}` to manage the
Gemini API providers (Gemini Developer API and Agent Platform Gemini API)
for Firebase AI Logic from the CLI.
- providers:enable enables the Firebase AI Logic API and the selected
provider's underlying API; agent-platform-gemini-api requires the Blaze
(pay-as-you-go) plan.
- providers:disable prompts for confirmation (--force to skip) and turns
off the proxy when no providers remain enabled.
- providers:list reports which providers are enabled.
- Adds a shared interactive enablement flow (used when the API is not yet
enabled) that checks enable permission up front before prompting.
- Adds `firebase help <namespace>` listing of subcommands under a prefix
so ailogic commands are discoverable.
Requires the firebasevertexai and serviceusage IAM permissions; commands
support --json and --non-interactive.
- Rename provider id agent-platform-gemini-api -> gemini-agent-platform-api for symmetry with gemini-developer-api (paulb777). - Centralize provider-type validation in ailogic.parseProviderType/isProviderType and reuse it in the enable/disable commands instead of copying the union (christhompsongoogle). - Add AILOGIC_LOGGING_PREFIX constant; replace the repeated 'ailogic' literals. - Move enablement-cache invalidation into serviceusage.disableServiceAndPoll and drop the per-callsite uncache calls; rename its 'prefix' param to loggingPrefix. - Drop the redundant non-interactive guard in disable; confirm() already enforces --force in non-interactive mode. - Gate the ailogic commands behind a new 'ailogic' experiment until API-council approval. - Document the help.ts namespace-listing blocks. - Update/extend unit tests accordingly.
# Conflicts: # CHANGELOG.md
Add `firebase ailogic:config:get [path]` and `ailogic:config:set <path> <value>` to read and write Firebase AI Logic service configuration from the CLI. - config:get prints the full config (providers, security, monitoring) or a single value by path; it is read-only and reports "not enabled" gracefully rather than forcing API enablement. - config:set updates one setting. Tightening security.auth-only or security.template-only from false to true prompts for confirmation (--force to skip); monitoring.sample-rate-percentage is validated as an integer 1-100. - Developer-facing paths map onto the underlying config resource (trafficFilter.*, telemetryConfig.*), with sample rate stored as a fraction. Supports --json and --non-interactive.
- Bring config commands under the ailogic experiment gate (via cherry-pick of the providers review fixes) and inherit the provider rename + centralized validation. - config:get/config:set: single source of valid paths (fixes get's validate-vs-error mismatch); rename provider id to gemini-agent-platform-api. - config:set: drop the redundant non-interactive guard (confirm() enforces --force), use utils.logSuccess, extract a bool parser, validate the path before the API-enablement flow. - gcp: GLOBAL_LOCATION constant for the config resource; AILOGIC_LOGGING_PREFIX in isAILogicApiEnabled. - Add ailogic-config-get.spec.ts and ailogic-config-set.spec.ts.
- config:set validates the value before the API-enablement flow (fail-fast) and
returns a {path, value} result so --json produces output.
- config:get path traversal uses an isRecord type guard instead of an `as` cast;
provider keys/paths derive from ailogic.PROVIDER_TYPES (no duplicated literals).
- Add tests: monitoring.state=false -> NONE, template-only tightening, nested-path
get, and fail-fast-before-enablement.
- Critical: register ailogic:config commands INSIDE the experiment gate; a rebase had left them outside, which crashed CLI startup for users without the ailogic experiment (client.ailogic was undefined). - parseBool accepts case-insensitive true/false. - listProviders runs its two independent enablement checks in parallel.
- Remove the unused security-rules layer (generateRulesContent, getSecurityRules, updateSecurityRules, the rules import, and their tests); nothing references it. - Single source for config paths: WRITABLE_CONFIG_PATHS exported from gcp/ailogic, READABLE derived from it; shared assertKnownConfigPath error helper; shared samplingRateToPercent/percentToSamplingRate codec (both directions were hand-coded in each command). - config:get validates the path before any API call and only checks provider enablement when the requested path needs it. - config:set: buildUpdate is now the single per-path decision site (folds in the confirmation message and current-value read), the switch is exhaustive so a new path cannot silently fall into the sample-rate branch, the percentage requires a plain decimal integer (Number() also accepted hex and scientific notation), and the --json result echoes the normalized value. - ensureAILogicApiEnabled reuses isAILogicApiEnabled instead of inlining the check.
- config:set preflights serviceusage.services.get (ensureAILogicApiEnabled reads
enablement state via Service Usage; without it a cold cache surfaced a raw 403
mid-command despite passing requirePermissions).
- Echo a normalized sample-rate value ('007' -> '7') so the success message and
--json output match what was stored.
- ensureAILogicApiEnabled honors --force for its enable confirmation and the
provider selection offers a cancel choice (the Spark-plan retry loop had no
exit besides Ctrl+C).
- Strengthen provider specs: pin each provider to its own API via withArgs (a
swapped destructure passed before) and assert the disable cross-check consults
the other provider's API.
- Add detailed .help() text to ailogic:config:get/set and ailogic:providers:enable/disable documenting allowed values - Drop CHANGELOG entries while the feature is behind the experiment flag - Remove empty JSDoc blocks in gcp/ailogic.ts - Inline single-use sampling-rate conversions instead of exporting helpers
- listProviders now reports a provider as enabled only when the Firebase AI Logic API (firebasevertexai.googleapis.com) is also enabled, so providers:list can no longer disagree with other ailogic commands about whether AI Logic is enabled on the project - enableProvider enables the AI Logic API before the provider's service API, so a partial failure cannot land in that inconsistent state
…ive help The custom namespace walk in the help command predates #10772, which auto-registers a commander command for every namespace. getCommand now always resolves namespaces to those commands, so the walk was unreachable (and mishandled function-valued intermediate nodes like client.ext, as flagged by review). firebase help ailogic:providers and firebase help ext:dev are both served by progressive help. Also remove two empty JSDoc blocks.
…by progressive help
Exposes the Firebase AI Logic templates command surface under the `firebase ailogic` namespace: - `templates:deploy`, `templates:list`, `templates:get`, `templates:delete`, `templates:lock`, and `templates:unlock` to version control and manage server prompt templates. - Template validations, lock checks, and pruning configurations. - Full unit test coverage for templates deploy command.
- Fix templates:deploy --dir: drop the Commander default so an explicit missing --dir errors while a missing default dir is a graceful no-op (was dead code). - Reuse src/fsutils (dirExistsSync/listFiles/readFile) instead of raw fs. - Add templateIdFromName helper; GLOBAL_LOCATION constant; PROMPT_FILE_EXT / DEFAULT_PROMPTS_DIR constants (no magic strings). - 404 -> friendly 'does not exist' on templates:get/lock/unlock (matching delete). - Drop redundant non-interactive guards in deploy/delete (confirm() enforces --force). - utils.logSuccess for success output. - Gate the template commands behind the ailogic experiment. - Add specs for list/get/delete/lock/unlock; update the deploy prune test.
… fix review findings
- New src/ailogic/templates.ts lib (mirrors the functions release planner and
remoteconfig lib layout): validatePromptFile, readPromptDirectory, and a pure
planTemplateDeploy -> {creates, updates, deletes, lockedViolations}, unit-tested
without stubs. Also positions a future `deploy --only ailogic` target as a
thin wrapper.
- Fix: declare -f/--force on templates:deploy; the prune confirmation consumed
options.force but the flag was never declared, so --prune --force was rejected
as an unknown option (unusable in CI).
- Fix: frontmatter delimiters must be on their own line; '---' inside a quoted
YAML value no longer breaks validation. Non-mapping frontmatter is rejected.
- Fix: a directory named *.prompt is reported as a validation error instead of
crashing with a raw EISDIR; filename-derived template ids are validated
(URL-safe charset) during the validation pass, closing a partial-deploy hole.
- deploy validates all local input before the API-enablement flow (fail-fast) and
warns when --prune is ignored because no local prompt files exist.
- gcp: shared withTemplate404 helper replaces the 404 try/catch copied across
get/delete/lock/unlock; byte-identical lockTemplate/unlockTemplate collapsed
into setTemplateLocked; templateName() helper; the always-global location param
dropped from template functions (matching getConfig).
- Commands return values for --json (deploy {deployed, pruned}; delete/lock/unlock
return the template).
- deploy.spec: reset command.befores like every sibling spec (tests no longer
depend on ambient credentials or make live IAM calls) and stub the fsutils seam
the command actually uses.
- Add detailed .help() text to all six templates commands documenting template ids, lock semantics, and the deploy flow - Resolve --dir against the project root instead of the cwd, falling back to the cwd outside a project - Match the .prompt extension case-insensitively so files from case-insensitive filesystems are not silently skipped
- Validate <templateId> as URL-safe in get/delete/lock/unlock before any API call; an id like 'welcome#old' or '..' would otherwise be spliced raw into the REST path and silently address a different template. The same regex is now shared with the filename validation in deploy. - Preflight all permissions deploy actually uses (get/update/delete plus serviceusage.services.get) so a partial deploy cannot start with permissions that only cover part of the plan; add the Service Usage read permission to the other five commands for parity with config - Report case-variant files that collapse to one template id instead of letting the last file win - Tolerate a 404 while pruning (template already deleted concurrently)
Wiz Scan Summary
To detect these findings earlier in the dev lifecycle, try the Wiz Code extension for VS Code, JetBrains, or Visual Studio. |
There was a problem hiding this comment.
Code Review
This pull request introduces the firebase ailogic command surface, gated behind the ailogic experiment, which allows users to manage Gemini API providers, configurations, and server prompt templates. Key feedback on these changes includes: ensuring that CRLF line endings are fully stripped in template previews to prevent garbled terminal output, refining YAML frontmatter validation to correctly reject arrays (which also evaluate to type 'object'), and defensively using optional chaining when accessing API response bodies to avoid potential runtime crashes.
| clc.bold(ailogic.templateIdFromName(t.name)), | ||
| t.displayName || "", | ||
| t.locked ? "Yes" : "No", | ||
| preview.replace(/\n/g, " "), |
There was a problem hiding this comment.
Since prompt files can have CRLF (\r\n) line endings (which is explicitly supported and tested), replacing only \n with a space leaves \r in the preview string. Printing a carriage return \r to the terminal resets the cursor to the beginning of the line, which will cause subsequent table columns to overwrite the start of the line and completely garble the terminal output. Replacing \r?\n ensures proper rendering across all platforms.
| preview.replace(/\n/g, " "), | |
| preview.replace(/\r?\n/g, " "), |
There was a problem hiding this comment.
Good catch, fixed. The preview now strips \r?\n, and there is a test asserting no carriage return survives into the table output.
| if (parsed !== undefined && parsed !== null && typeof parsed !== "object") { | ||
| return "Frontmatter must be a YAML mapping."; | ||
| } |
There was a problem hiding this comment.
The current check typeof parsed !== "object" is insufficient to verify that the parsed frontmatter is a YAML mapping. In JavaScript, arrays also have a typeof value of "object". If a user provides a YAML sequence (array) instead of a mapping, it will bypass this validation. Adding Array.isArray(parsed) ensures that only valid YAML mappings are accepted.
| if (parsed !== undefined && parsed !== null && typeof parsed !== "object") { | |
| return "Frontmatter must be a YAML mapping."; | |
| } | |
| if (parsed !== undefined && parsed !== null && (typeof parsed !== "object" || Array.isArray(parsed))) { | |
| return "Frontmatter must be a YAML mapping."; | |
| } |
There was a problem hiding this comment.
Done. validatePromptFile now rejects arrays with an Array.isArray check, and the frontmatter test covers a YAML sequence.
| if (res.body.templates) { | ||
| templates.push(...res.body.templates); | ||
| } | ||
| pageToken = res.body.nextPageToken; |
There was a problem hiding this comment.
To be defensive and prevent potential runtime TypeErrors, use optional chaining when accessing res.body. If the API returns a successful response with an empty body (or if res.body is null/undefined), accessing res.body.templates directly will crash the CLI.
| if (res.body.templates) { | |
| templates.push(...res.body.templates); | |
| } | |
| pageToken = res.body.nextPageToken; | |
| if (res.body?.templates) { | |
| templates.push(...res.body.templates); | |
| } | |
| pageToken = res.body?.nextPageToken; |
References
- TypeScript: Use strict null checks and handle
undefined/nullexplicitly. (link)
There was a problem hiding this comment.
Done, switched to optional chaining on the list response body.
…ates
Verified against the live v1beta API (discovery doc + probes):
- 'locked' is output-only on PromptTemplate; a PATCH with
updateMask=locked is rejected with 400 INVALID_ARGUMENT. lock/unlock
now call POST templates/{id}:modifyLock, which the server accepts and
enforces (PATCH/DELETE on a locked template fail FAILED_PRECONDITION).
- The API enforces etags when provided: PATCH with a mismatched body
etag and DELETE with a mismatched ?etag= both return 409 ABORTED.
templates:delete now passes the etag from its pre-confirmation read,
and deploy passes each template's listed etag on updates and prunes,
so a template modified after planning fails with a clear re-run
message instead of being silently overwritten or deleted.
Live-tested all six commands against a real project: create/update
deploy, get, list, lock, deploy and delete blocked while locked,
unlock, prune guard, delete, and the friendly 404.
|
Heads up on a fix pushed in aacf0a5: while verifying the API's concurrency semantics against a live project, I found that 'locked' is output-only on PromptTemplate, so the original lock/unlock implementation (PATCH with updateMask=locked) was rejected by the server with 400 INVALID_ARGUMENT. They now use the dedicated POST :modifyLock RPC. Same commit adds etag preconditions, since the API turned out to fully enforce them: templates:delete passes the etag from its pre-confirmation read, and deploy passes each template's listed etag on updates and prunes. A template modified between planning and apply now fails with a clear re-run message (409) instead of being silently overwritten. All six commands were exercised end to end against a real project, including lock enforcement on deploy/delete (the server rejects writes to locked templates with FAILED_PRECONDITION, so the CLI-side checks are a fail-fast nicety on top of real server enforcement). |
…ntics - Strip CRLF from list previews; a raw carriage return in a table cell resets the terminal cursor and garbles the rendered table - Reject YAML sequences as frontmatter; arrays are typeof object but are not mappings - Optional-chain the list response body defensively - Mask deploy updates to templateString: probing the live API showed an unmasked PATCH replaces the whole resource, clearing a displayName set outside the CLI; under the mask the body etag is still enforced as a precondition (verified stale -> 409, current -> 200). displayName is now only set as a friendly default on creates - Skip templates whose content already matches the remote: no updateTime/etag churn, an all-unchanged deploy reports 'already up to date', and a lock on an identical template no longer blocks the deploy
|
Seems like all the previous changes are showing up in the PR as well - can you go submit those and sync this to main, and that should get us to a cleaner diff to review? |
# Conflicts: # src/commands/index.ts # src/gcp/ailogic.spec.ts # src/gcp/ailogic.ts
Description
Adds the
firebase ailogic:templates:*commands to manage Firebase AI Logic server prompt templates from the CLI, gated behind theailogicexperiment (same asailogic:providersandailogic:config).ailogic:templates:deploy [--dir <path>] [--prune]deploys every.promptfile in the prompts directory (defaultprompts/, resolved relative to the project root). The file name becomes the template id: new ids are created, existing ids are updated, templates whose content already matches the remote are skipped ("already up to date"), and--prunedeletes remote templates with no local file after confirmation. An empty directory with--pruneis refused rather than interpreted as "delete everything".ailogic:templates:listlists deployed templates (id, display name, lock state, content preview).ailogic:templates:get <templateId>prints one template in full.ailogic:templates:delete <templateId>deletes after confirmation (--forceto skip).ailogic:templates:lock/ailogic:templates:unlock <templateId>toggle a lock that blocks updates and deletes. Locked templates block a deploy during planning with a message pointing atunlock;--forcedoes not override a lock.Design notes (the concurrency behavior was verified against the live v1beta API, not just assumed from the docs):
src/ailogic/templates.tsas a pure planner (planTemplateDeploy): local files plus remote state in, a creates/updates/unchanged/deletes/lockedViolations plan out. No I/O in the planner, so the set arithmetic is directly unit-testable (mirrors the functions release planner and thesrc/remoteconfiglayout).:modifyLockRPC;lockedis output-only on the resource, and the server enforces locks on writes (FAILED_PRECONDITION), so the CLI-side lock checks are a fail-fast nicety on top of real server enforcement.templates:deletepasses the etag from its pre-confirmation read, and deploy passes each template's listed etag on updates and prunes. A template modified between planning and apply fails with a clear "re-run" message (409) instead of being silently overwritten or deleted.templateString(an unmasked PATCH replaces the whole resource), so adisplayNameset outside the CLI survives deploys;displayNamedefaults to the template id on creates only.<templateId>args in get/delete/lock/unlock) since they are spliced into REST resource paths.---delimiters, so---inside YAML values or the prompt body is not mistaken for a delimiter.globallocation, return structured values for--json, and follow the same read/write split as the config commands: read-only commands report when the AI Logic API is disabled, mutating commands run the interactive enablement flow.Builds on the now-merged #10849 (providers) and #10850 (config); the branch is synced with main, so the diff is the template-only changes (16 files, pure additions).
Scenarios Tested
src/ailogic/templates.spec.ts(validator, directory reader, planner) and one spec per command. Covers frontmatter edge cases (unclosed block,---in YAML values, CRLF, sequences), invalid/duplicate/directory-shaped prompt files reported in one pass, locked-template blocking for deploy and delete, unchanged-content skipping, prune confirmation and the--force/non-interactive paths, etag passthrough and 409 conflict mapping on delete/update/prune, concurrent-deletion (404) tolerance during prune, fail-fast template id validation, and--dirresolution against the project root.npm run build, lint, andtsc --noEmitare clean; full ailogic suite passes (97 tests).firebase help ailogic:templateslists all six).:modifyLock, deploy and delete correctly blocked while locked, unlock, prune guard on an empty directory, delete with etag, friendly 404 after deletion, and adisplayNameset outside the CLI surviving a content deploy.Sample Commands