English · Русский
This document describes the Console API v2 contract of the 1C:Enterprise.Element platform (1cmycloud.com), the build file format, and the requirements for the elemctl tool. It holds only facts about the platform interface and requirements for the product: the implementation is designed from scratch.
The elemctl Python package consists of three layers on top of a shared core:
- Library – a programmatic client for Console API v2 and high-level operations: build and deploy. Python standard library only.
- CLI – the
elemctlconsole command, entry pointelemctlin[project.scripts]. - MCP server – the same operations exposed as tools for AI agents, over the stdio transport. The
mcp>=1.2,<3dependency comes as the optional extraelemctl[mcp]. It uses the ergonomic server class of themcppackage:FastMCPin mcp 1.x,MCPServerin mcp 2.x.
Package requirements: name elemctl, version 0.1.0, Python >= 3.10, MIT license, author KeyFire, src/elemctl/ layout, dev extra with pytest. The LICENSE, README.md, .env.example, and .gitignore files are given and are not modified.
Parameters are taken from three sources, in decreasing order of priority:
- explicit arguments (CLI flags
--base-url,--client-id,--client-secret); - environment variables;
- the .env file: path from the
--env-fileflag, or, without it, the.envfile in the current directory, if it exists.
Environment variables:
| Variable | Meaning | Required |
|---|---|---|
ELEMENT_BASE_URL |
platform base URL starting with http:// or https://, e.g. https://1cmycloud.com |
yes |
ELEMENT_CLIENT_ID |
Client-Id for obtaining the token | yes |
ELEMENT_CLIENT_SECRET |
Client-Secret | yes |
ELEMENT_APP_ID |
default application | no |
ELEMENT_PROJECT_ID |
default project | no |
ELEMENT_SPACE_ID |
default space | no |
ELEMENT_CA_FILE |
additional PEM CA bundle for a private cloud | no |
ELEMENT_TLS_STRICT |
strict RFC 5280 certificate checks; true by default |
no |
ELEMENT_TLS_VERIFY |
certificate and hostname verification; true by default |
no |
.env format: KEY=VALUE lines. Empty lines and lines starting with # are skipped. A leading export prefix is allowed, and the value may be wrapped in single or double quotes. The encoding is UTF-8 and a BOM is possible, so read the file as utf-8-sig. A trailing slash in ELEMENT_BASE_URL is trimmed. A nonempty base URL must start with http:// or https://.
Obtaining a token: POST {base}/console/sys/token
- header
Authorization: Basic base64(client_id:client_secret); - body
grant_type=client_credentials, Content-Typeapplication/x-www-form-urlencoded.
The response is a JSON object, and the token sits in the first non-empty of the fields id_token, token, value, access_token. Special case: the access_token value can be the string "Not implemented". That is not a token, so skip the field.
All other requests use the header Authorization: Bearer {token}.
The token lives for about an hour. Cache it in a file in the system temporary directory that tempfile.gettempdir() reports, not in a hardcoded /tmp, because the tool also runs on Windows. The cache lives an hour, and its key must distinguish base_url and client_id pairs. On a 401 response, refresh the token forcibly and retry the request once.
Common prefix: {base}/console/api/v2. Request and response bodies are JSON, apart from the build upload. Field names are in kebab-case.
The newer console reference documents Console API 2.1 as the main version, with the prefix {base}/console/api/v2.1, and keeps 2.0 beside it, working. The client stays on 2.0 and takes 2.1 only for what 2.0 has not got: the extensions applied to an application (section 4.1). A console that has no handler for a path answers 401 rather than 404, with the text Handler of HTTP request "[<method>] <path>" in application "console" not found., and that is how a server older than a method refuses it. Such a 401 is not about the token, and a renewed one gets the same answer, so the client reads the text before the token is renewed: the answer is named as a method the server does not know, and no new token is asked for. Any other 401 renews the token and repeats the request once.
GET /applications– list. Thenamequery parameter exists, but the platform ignores it and returns the full list. That was verified against a live instance, so filtering by name has to happen on the client.GET /applications/{id}– card. Significant response fields:id,status,uri(address of the running application),error(error text, if any),technology-version,date-updated,display-name,publication-context,source(an object with source information, containing among other thingsproject-version– the version of the applied build).POST /applications– create. Body:source– the object{"type": "repository"}plus exactly one of the keys:project-version-id(id of the source build) orimage-id(project id);display-name,publication-context– publication name and path;development-mode– boolean, whether to create a development environment;- optional
space-id,technology-version.
DELETE /applications/{id}– delete.PUT /applications/{id}/status/start– start.PUT /applications/{id}/status/stop– stop.POST /applications/{id}/actions/debug– data for a debug session (ApplicationDebugInfo:{"debug-token": ..., "debug-address": ...}). The request body is empty; requires debugging enabled on the server (config/debug.ymlenabled: true).POST /applications/{id}/project/update– apply a build to the application. Body:{"source": {"type": "repository", "image-id": "<build id>"}}or{"source": {"type": "repository", "project-id": "<id>", "assembly-version": "<version>"}}(assembly-version is optional). A build of an extension goes to the same method and is applied beside the build of the application:source.project-version-idof the card goes on naming the build of the application, and the extension shows up inextension-projectsof the 2.1 method below, with the version of the build inassembly-version. Checked live, on an apply that went through and on two that the platform rolled back.POST /applications/{id}/project/export– the build the application runs. The reference documents the method under 2.0 and 2.1, and a live check got the same archive from both. The request has no body, and the answer is the archive itself,application/octet-streamwith noContent-Disposition: the files of the build,Assembly.yamlamong them, packed anew with entries for the directories. The console takes the archive from the application server, so it is the project the application runs. The extensions applied to it are not in the archive; they have a method of their own (the 2.1 methods below). By the sources of the console, the export of an application in the middle of an update, or of one whose update has failed, answers with the build uploaded for that update, and a live check during an update got exactly that. The manifest is the one the server keeps for the build: a build uploaded into a project comes back with the number the server gave it (section 4.4) and the time of the upload inCreated, while a build that created its project comes back with the manifest of its archive. The handler of 2.1 also takes an undocumentedextension-idquery parameter and then answers with the build of that extension, the way/v2.1/applications/{id}/project/{ExtensionId}/exportdoes; 2.0 ignores the parameter. The controller lets in any authenticated user and reads the application with that user's rights.POST /applications/{id}/dumps– create a dump. Body:include-users,include-binary-data(booleans),description(string).GET /applications/{id}/dumps/{dumpId}– dump status.GET /applications/{id}/users– the users connected to the application, each{user-list-id, user-id, presentation, is-admin, token-access-enabled}. The users of the control panel are among them: the platform connects them itself, and it names such a user by the login.token-access-enabledis the access of the user to the HTTP services of the application by a token. Without it a call of a service with the user's token is refused with a 500 "Token access is denied", whatever the rights of the user.PUT /applications/{id}/users/change-token-access– switch that access. Body:{"user-list-id", "user-id", "enable-access"}, all three required; the answer is the connection as it became. A user who is not connected to the application is refused with a 500 "Can't change user token access" and no word about the cause, so the client finds the connection first and names the user when there is none. The reference documents the method under v2 and v2.1 with the same body, and a live check got the same answer from both; the client takes the v2 prefix, like every other request.GET /me– the user the credentials belong to:id,login,presentation,user-list-id.GET /v2.1/applications/{id}/project– the projects the application runs:application-project, and the extensions applied to it inextension-projects. An extension carriesid,project-id,assembly-id,enabled,order,vendor-name,project-name,project-presentation,project-versionandassembly-version.idis the id of the extension on the application server: theИдof the extension'sПроект.yaml, theconfiguration-idits upload answered with (section 4.4). The console fillsassembly-idof an extension with the id of its project, which a live check and the sources of the console agree on, so that field names no build. The same path of 2.0 answers with the application project alone.POST /v2.1/applications/{id}/project/{ExtensionId}/export– the build of an applied extension. The request has no body, and the answer is the archive itself,application/octet-streamwith noContent-Disposition: the files the build was uploaded with,Assembly.yamlamong them, packed anew. The segment is theidof the extension from the previous method, in either case of letters. The reference calls the parameter the id of the extension project, but theproject-idgets the same bare 500 "Unable to export extension ... from application ..." as an unknown value, with "Contact administrator for details" indetails. A stopped application exports all the same, and a deleted one gets a 404 on the listing. 2.0 has no such method:/v2/...gets the 401 of a missing handler. The controller of the console lets in any authenticated user (PermitAuthenticated) and reads the application with that user's rights.
Application statuses: stable Running, Stopped, Error; transitional Starting, Stopping, Initializing, Updating, Frozen, Creating, Deleting. During transitions the status field may also be empty. The reference describes status as a plain string and lists no values, so this list is what live checks have met. Deleting lasts while the DeleteApplication task runs, and then the application stays in the list under Deleted (section 6.9). UNKNOWN, in capitals, was met on an application whose database files were gone: it could be neither started nor deleted, because a deletion begins with an export of the data. The console gives that status to a server state of the application it has no name for, so nothing says it is final, and the client counts it as neither stable nor transitional: a wait for a status puts up with UNKNOWN for a minute in a row, then stops and names it rather than running out its whole timeout.
- Reading – from the
technology-versionfield of the application card (a dedicated read endpoint is not present in all platform versions – do not use it). - Update:
POST /tasks/group-tasks/update-applications-technology, body{"technology-version": "<version>", "applications": ["<app-id>"]}. Returns a group task; its status –GET /tasks/group-tasks/{taskId}.
GET /spaces– list of spaces.GET /projects– list of projects;GET /projects/{id}– card;DELETE /projects/{id}– delete. The list is answered in full whatever the query, and deleted projects stay in it under thedeletedflag with their former id. Filtering by name and hiding the deleted ones is the client's work, as with applications (section 4.1). A project card carries no status at all: thedeletedflag is the only mark of its state. The kind of a project is inproject-kind:Application,LibraryorExtension, andGroupfor the group of projects the list carries beside its members.
- Uploading a build file – a binary POST (Content-Type
application/octet-stream, body – the file bytes):POST /projects/{id}/assemblies– add a build to an existing project;POST /projects– create a new project from a build, or add the build to the project that carries theИдof itsПроект.yaml;POST /spaces/{space-id}/projects– the same, with a new project created in that space. The space of an upload into a project goes as the optionalspace-idquery parameter, spelled the way the reference spells it. A live check found the server reading it: an id that is not a UUID is refused with a 400 and an unknown space with a 404, while the PascalCaseSpaceIdthe client used to send went through unread.POST /projectsdocuments no space parameter, and the server reads neither spelling there: a new project uploaded withspace-id=not-a-uuidwas created all the same. So the space of a new project goes into the path ofPOST /spaces/{space-id}/projects, which answers an unknown space with a 404 and adds a build whoseИдalready has a project to that project, the wayPOST /projectsdoes. The commit the build was made from goes ascommit-id, a query parameter the reference documents forPOST /projects/{id}/assemblies, and the server puts it on the build card: that is the commit the schema guard of the next deploy compares against (section 7). The reference listsbranch-name,commit-messageandmodifiedfor the same method too, and the client sends none of them. A live check found the server taking them and showing none on the card: itsbranch-namenames a branch of group development, not of git, andmodified=1is refused with a 500. The PascalCaseCommitId,BranchNameandCommitMessageare not parameters of the method at all, and the server ignores them.POST /projectsdocuments no commit parameter, so a build that creates its project carries no commit. What the platform does not keep – the branch, whether the tree had uncommitted changes, the directory of the sources – the client keeps in a local registry of uploads (section 7). The two uploads treat the version of the archive differently. An upload into a project,POST /projects/{id}/assemblies, does not read it at all: the server takes the base from theВерсияof the project descriptor and gives the build the highest number it has ever given in that base plus one. Checked live: an archive of1.0.0-i1became1.0.0-1, the next one,1.0.0-10001, became1.0.0-2, a1.0.0-2already in the project became1.0.0-3, and1.0.1-7became1.0.0-502in a project whose descriptor says1.0.0. A deleted build keeps its number: the build list no longer shows it, and the server counts on from it all the same. With1.0.0-4deleted from the top of the list, the next upload became1.0.0-5, and with1.0.0-50and1.0.0-51deleted, it became1.0.0-52while the list ended at1.0.0-8; a build deleted from the middle of the list changes nothing. The count is nowhere to be read: the project card does not carry it, the build list does not show a deleted build, and the card of a deleted build answers with a 404. That upload answers with the card of the build, and itsassembly-versionis the number the server handed out. An upload without a project id,POST /projects, keeps the version of the archive as it is, a suffix that is not a number included, and answers a version the project already has with a 409 (below). The number of a deleted build is free for it again, and taking it does not wind the count back: an archive of1.0.0-2went in under that number after the earlier1.0.0-2had been deleted, and the next upload into the project became1.0.0-8, not1.0.0-3. So the suffix of a version, the number of a CI run for instance, survives the second way and never the first: what ties a build uploaded into a project to its run is the name of the archive and the commit on the card. The response carries the id of the created build in one of the fieldsimage-id,assembly-idorid, checked in that order. Next to it sits anartifactobject describing the project the build landed in:artifact-idis the project id and opens as a project card,configuration-idis theИдofПроект.yaml, andnameis the project presentation. The console shows a project under the presentation of the build uploaded last, theПредставлениеof itsПроект.yaml, not under the manifestName: builds of oneИдuploaded with a newNameand a new presentation each left the project card named after the presentation every time. So a build uploaded into a project under a different name renames that project and its group, and deleting the build does not undo it. The client refuses such an upload and names the price; uploading a foreign build on purpose takes--force-rename. An upload without a project id renames a project the same way when itsИдleads to a project that has another name (below). Nothing can be compared before such an upload: the server chooses the project, and no method finds a project by itsИд. So the client reads the project list before the upload and holds theartifactof the answer against it: a project the list did not have was created by the upload, and a project it had under another name was renamed by it (section 7,builds upload). The check is best effort: when the names cannot be compared, because the manifest is unreadable or the project card is unreachable, the upload proceeds as before. Not being able to compare is no proof of danger.
- A project is identified by the
ИдofПроект.yaml, theconfiguration-idof the answer (section 6.8).POST /projectstherefore does not always create a project: when a live project carries thatИд, looked for in the space of the upload first and then in any other, the build is added to it and itsartifact-idcomes back in the response, whatever the vendor and the name of the manifest. The vendor and the name are a constraint rather than the key: one project of a space per pair. A live check took the two apart. Builds ofacme/crm,acme/crm-nextandglobex/crmwith oneИдall went to one project, which took the name of each in turn, throughPOST /spaces/{space-id}/projectsas well. A build ofacme/crmwith a freshИдgot a 409. There are two ways to hit a 409ALREADY_EXISTS. The first is uploading a version that is already there, answered with "Версия сборки ... уже присутствует в группе проекта". The second is a freshИдwith a vendor and a name another project of the space already holds, answered with "Сборка с именем поставщика ... уже зарегистрирована в другом проекте": the console starts a new project for theИдand refuses the build it cannot register there, and the new project goes with the refusal. The earlier checks had theИдand the pair agree, so they could not tell the key from the constraint; the source of the console looks an upload without a project id up by theИд(FindProjectById). GET /projects/{id}/assemblies– list of builds. Each element containsassembly-version, a string like1.0-42, and an id inidorimage-id. The response is either an array or an object with the list in theitemsorassembliesfield. The method has no pages and reports no total.limit,size,pageSize,count,top,maxResults,page,pageNumber,offset,skip,fromandstartare all ignored: the answers to every one of them match byte for byte, and neither the headers nor the body carry a counter or a cursor. Verified by live calls on two installations. The platform deletes builds nobody uses, and age has nothing to do with it: the vendor's help calls this automatic deletion of unused builds. The console does it at one moment only, when an application finishes applying a build: a background job then goes over the builds of that application's project with the same base version as the build it now runs. A release build stays, and so do a protected build, the project's default build, a build a live application of the project runs or has had uploaded into it, and the build with the highest number of that base. The rest go, ten at most per apply. A build uploaded without a project id (POST /projects) is protected on arrival, like the build the project's repository was created from, so no apply removes it, while a build uploaded into a project by its id (POST /projects/{id}/assemblies) is not protected. A library build no remaining build depends on goes with the builds of its group. The builds of an extension project are outside the housekeeping altogether. An extension is applied by the same update of the application, and the job that follows starts from the build the card of the application names: it goes over the application's own project and takes the unused builds of that base there, while the extension project keeps every build, the ones no application runs included, until they are deleted by hand or go with the project. A live check agreed with the source: the application ran1.0.0-1of its project,1.0.0-2and1.0.0-3had been uploaded into that project by id, and the extension project held1.0.0-1to1.0.0-4. Applying the extension build1.0.0-2took1.0.0-2of the application project and nothing of the extension project, and applying1.0.0-4after it left the extension builds1.0.0-2and1.0.0-3where they were. So a project nobody applies anything to keeps every build, and a build uploaded for later survives the apply of another one only while its number is the highest. The source of the console in the server distribution says so, and a live check on a project of nine builds agreed with every line of it. Creating an application deleted nothing in two minutes. Applying1.0.0-3took1.0.0-2,1.0.0-4and1.0.0-11away before the apply returned, and left1.0.0-1and1.0.0-10, both uploaded without a project id,1.0.0-3,1.0.0-12, the highest, and both builds of2.0.0. Applying1.0.0-12took1.0.0-3. Applying2.0.0-1took nothing,1.0.0-12included, since that apply looked at2.0.0alone. Seen earlier: seventeen builds of one day's series were made and one survived, the one the application runs, while a build from two months earlier is still listed: it is the project's first, and the upload that creates a project is an upload without a project id. So a listing is not the project's history and not a page of it. It is what survived, and the client must say so.GET /projects/{id}/assemblies/{version}– build card,DELETE .../{version}– delete. The last segment is what the method calls it: the version, which isassembly-versionorproject-version, a string like1.0-42. It is not the id of the card: a UUID there is answered with a 404 "Assembly with version not found". Checked live on two installations of different ages, both behave this way. The pages used to claim the opposite, that only a UUID works and a version gets a 400 "Version is not a valid UUID"; anyone following that claim was left with an unreachable card whichever form they gave. An id is still an address a caller holds: the build list prints it, and an upload answers with one. So the client accepts both forms and looks the value up in the build list to get the version the method takes. A value in no card is named as missing instead of becoming a 404 out of the depths of the platform. When the address is refused with a 400 or a 404, the client tries the id as the segment too: no reachable installation wants it, but the 400 the pages described had to come from somewhere, and the second spelling costs one request. The version of a build uploaded into a project is the number the server gave it, not the one of its archive (the upload above). Deleting a build is rejected with a 500 while an application created from it still exists (section 6.9); once that application is really gone, the same request succeeds. A 500 is an answer, not a misunderstood address: it is raised as it is and never retried with another spelling.
Build versions are compared by the numeric suffix after the last hyphen: 1.0-10 is newer than 1.0-9. Lexicographic comparison gives the wrong order.
GET /branches– list; optional queriesproject-id,name.GET /branches/{id}– card. Fields:name,kind,project,application,source-branch,deletion-mark,version-stamp.POST /branches– create. Body:name,kind: "development",project: {"id": "<id>"}, optionallyapplication: {"id": "<id>"}.PUT /branches/{id}– modify. The platform uses optimistic locking, so read the card first, then send a body assembled from the current values. Those arename,kind,deletion-markandversion-stamp, which has to come back exactly as it was. Collapsesource-branchandapplicationto{"id": ...}, or to{"name": ...}when there is no id. To rebind to an application, replaceapplicationwith{"id": "<new app-id>"}.- Branch changes are accepted by that same
PUT /branches/{id}with an additional body keywrite-parameters: {"merge": true}. DELETE /branches/{id}– delete the branch.
The tool works only with the documented Console API v2. It neither uses nor describes the internal, undocumented APIs of the platform console.
GET /tasks/application-tasks – list of tasks for all applications. There is no server-side filter, so filter on the client. Task fields: id, application-id, status (including Error, Failed), operation-type, error-message, start-date (ISO 8601, may end with Z).
A user list holds either the users of an application, which has a list of its own named after it, or the users of the control panel, which has one list per installation. What the panel calls the sign-in settings lives here.
GET /user-lists– the list of user lists:id,presentation,space-id. There is no server-side name filter, so filter on the client.GET /user-lists/{id}– the full card:self-registration,password-policy,password-policy-enabled,account-services-settings,confirmations, the gateways,include-personal-data-in-messages.GET|PUT /user-lists/{id}/settings/self-registration–{enabled, phone-required, email-required}. This is the panel's "allow users to register themselves". The PUT wants the whole object.GET|POST /user-lists/{id}/settings/account-services-settings,PUT|DELETE .../{account-service-id}– the account services of the list. An entry is{account-service-id, account-service-type, local-id, enabled, create-user-on-auth, additional-settings}. The typeLocalauthenticates by a password, so the panel's "allow signing in with a login and a password" is that entry beingenabled. The other types are external services:OIDC,Cas,ActiveDirectory,Esia. The PUT wants the whole entry back.GET /applications/{id}/userlists– the ids of the lists connected to the application;POSTconnects,DELETEdisconnects. Note the spelling: no dash here. There are no per-connection settings: the link is a set of ids and nothing more.GET /user-lists/{id}/users– the users of the list,idandloginamong their fields. An entry carries the access tokens of the user as well, secrets included, so the client takes nothing but the ids out of it.- The application card names the application's own list in
default-user-list. That is the list of its users, and the panel list connected to it is a separate one.
Two things the panel can do and the API cannot, so both stay manual:
- the composition of an application's authentication forms is not in the API at all;
- the connection setting "users of the list are connected to the application automatically on sign-in" is not represented either. It is in neither
userlists, which is a set of ids, nor the application'saccount-services-settings; on a stand where the setting is on, nothing appears there.
The rules for parsing the response of an account service are accepted in the body of an account service under the key userPropertiesCalculationRules. Those rules are presentation-rule, email-rule, phone-rule and response-kind, all of them JsonPath or XPath. There are two traps. The schema of the reference calls the same thing calculation-rules, and the platform answers 400 to that spelling. And a GET never returns the rules: the setting is write-only, so an API client cannot confirm it applied.
A build file is a ZIP archive (deflate):
-
at the root,
Assembly.yaml– the manifest, flat key-value pairs:ManifestVersion: 1.0 | 1.1 ProjectKind: Application | Library | Extension Vendor: <vendor> Name: <project name> Version: <version, e.g. 1.0-42> Created: <UTC, format YYYY.MM.DD HH:MM:SS> BranchName: <git branch name> CommitId: <commit hash>For a library (
ProjectKind: Library), aRelease:line (empty value) is added at the end; the file extension is.xlib, for an application and an extension –.xasm.An extension (
ProjectKind: Extension) is written withManifestVersion: 1.1, every other kind with1.0. The server picks the reader of the manifest by its version, and the reader of 1.0 knowsApplicationandLibraryalone: an extension under 1.0 is refused on apply with "Unknown project kind: "Extension"". The console writes the same pair for an extension it creates. An extension built this way was applied live throughproject/updateand exported back file for file (section 4.1). -
then the project files at paths
{vendor}/{name}/...relative to the repository root. The project directory must follow the scheme{repo}/{vendor}/{name}/Проект.yaml. For an application and an extension, locally available libraries declared inБиблиотеки/Librariesare included recursively; the console gives an extension its libraries the way it gives them to an application. The projects an extension extends, listed underРасширяемыеПроекты/ExtendedProjects, are not libraries and stay out of the archive. A declared library without a project under the same repository root stays an external platform dependency. Unreferenced sibling projects are not included. Path separators in the archive are forward slashes, on Windows too.
Build file name: {Имя} {Version}.xasm (with a space).
Project metadata comes from Проект.yaml. It is YAML, and parsing the flat top-level "key: value" pairs is enough; skip the nested indented lines. Bilingual sources are a capability the platform declares, and a descriptor written with English keys deploys fine, so every key is read in both spellings: Имя/Name, Поставщик/Vendor, Версия/Version (base, e.g. 1.0) and ВидПроекта/ProjectKind. The value Библиотека or Library means a library, Расширение or Extension an extension; anything else means an application. When both spellings of a key are present, the Russian one wins. The service file names are bilingual too: the platform converter accepts Project.yaml and Проект.yaml, Subsystem.yaml and Подсистема.yaml. So the descriptor is looked up by both names everywhere: project discovery, local library dependencies, archive inspection, the schema guard's availability probe.
When the build version is not set explicitly, it is built as {base version}-{N+1}, where N is the counter from the version of the project's latest build of the same base version. deploy takes N as the higher of the latest build in the list and the highest number the local registry of uploads remembers for the project in that base: the server counts on from the highest number it has ever given, deleted builds included (section 4.4). A project bumped to a new base version starts from -1 again, whatever counters the old base reached. With no last build of that base, the suffix comes from the CI run number in the environment: the first numeric value of CI_PIPELINE_IID, GITHUB_RUN_NUMBER, BUILD_NUMBER, in that order. Otherwise a clean CI checkout would produce -1 every time. With no CI number either, the version is {base version}-1.
Git metadata, meaning the commit hash and the branch name, comes from the git repository that contains the project directory. When git is unavailable, the fields stay empty.
File selection for the archive:
- inside resource directories, whose literal name is
Ресурсы, files of any extension are included, at any level and in their subdirectories too: per the platform documentation a resource is an arbitrary file, such as.pdf,.htm,.mxl,.docxor.xsd; - outside resource directories only these extensions are included:
.yaml .xbsl .xbql .md .txt .json(sources),.png .svg .jpg .jpeg .gif .webp .ico(images),.css .htm .html .js .woff .woff2 .ttf .eot(web resources); - the description files of a SOAP service client are included wherever they lie:
<Client>.Wsdl.<n>and<Client>.Xsd. The platform puts them next to the project element rather than into a resource directory and forbids renaming them, so they are matched by name, not by extension; - the directories
.git,.claude,.github,__pycache__,node_modules,.venvand all hidden ones (starting with a dot) are excluded; - the files
.gitignore,.env,.DS_Storeand any*.xasmor*.xlibare excluded, inside resource directories too.
The reverse of a build: from a .xasm or .xlib file you get the manifest, the project properties from its Проект.yaml inside the archive, and the contents. It is needed to attach a library to a project without unpacking its sources.
The layout inside a project; only the directories tell the truth about the contents:
- a first-level directory is a subsystem.
Подсистема.yamlorSubsystem.yamlis optional, and a library subsystem may have none at all, so it cannot be relied upon when looking for subsystems; - a nested directory of a subsystem is a package. A package has no description file, and every directory contributes a name segment;
- the qualified name of a type:
{vendor}::{name}::{subsystem}[::{package}]::{TypeName}. The same name without the last segment is whatИспользованиеandимпортtake.
Only types with ОбластьВидимости: Глобально, in English VisibilityScope: Global, are visible outside, in the project that attached the library. The default is ВПодсистеме, or InSubsystem, and the global scope is written explicitly. An English descriptor carries English enumeration values as well, so those values are read in both spellings.
Compatibility is checked against the РежимСовместимости property of Проект.yaml. The ВерсияТехнологии property does not exist in Проект.yaml: it belongs to the body of the Console API request that creates an application, not to the project file.
- Silent rollback of build apply. When applying a build to the application fails, a compilation error for instance, the platform silently rolls the application back to the previous build and starts it. The
Runningstatus does not mean success. A reliable check of the result looks like this:- take the application tasks (section 4.6) with status
ErrororFailedwhosestart-dateis not earlier than the moment the deploy started. Old errors from history do not count; - compare the actually applied version, the
source.project-versionof the application card, with the version of the uploaded build; - a build of an extension is compared with the extensions of the application instead,
extension-projectsofGET /v2.1/applications/{id}/project(section 4.1): the card names the build of the application alone, before an extension is applied and after it. The extension of the build's project has to run the version of the build, and it has to beenabled. A server without Console API 2.1 has no such list, so the apply of an extension cannot be verified there, and the check says so rather than report a rollback; - for information, make a check GET against the application
uri. Codes 401 and 403 are normal for closed applications and do not contradict success.
- take the application tasks (section 4.6) with status
- Empty skeleton on creation. On some platform configurations an application created with a "project" source, meaning
image-idset to the project id, comes out empty, with no project data. A reliable source is a specific build inproject-version-id, for example the project's latest build. - Deletion with drafts. If the application's development environment has unpublished edits,
DELETE /applications/{id}returns 400 withFAILED_PRECONDITIONin the body. There is no forced deletion in the API, only the control panel, and the tool must provide a clear hint. - Readiness of a new application. After creation, the application sits in transitional statuses and without a
urifor some time, so provide for waiting until it is ready: aurihas appeared and the status is stable. AnErrorstatus while waiting is an immediate error. A read of the card that breaks off on the network is a missed poll, not the end of the wait, since the application is being created all the same.UNKNOWNheld for a minute in a row ends this wait as well, with an error naming the status (section 4.1): while the task that creates the application runs, the console reportsInitializingwhatever the server says, soUNKNOWNcomes only after that task. The task list of section 4.6 carries the tasks of every application at once and is the read that breaks off most, so a broken read of it is made again, three times in all. - Restart after apply.
project/updatemay restart the application itself. After the call, wait until it leaves the transitional statuses. If the result is notRunning, stop it unless it is alreadyStopped, wait forStopped, start it and wait forRunning. Reasonable waits: about 3 minutes for a stop, about 5 minutes for a start and stabilization, polling every 10 seconds or so. A read of the card that breaks off is a missed poll in these waits too, as in section 6.4.UNKNOWNheld for a minute in a row ends each of them with an error naming the status (section 4.1). Erroris a final status. A stableError, after a failed apply for instance, is an immediate failure: surface the error messages of the application tasks (section 4.6) right away. Do not stop or restart such an application, and do not keep waiting for another status: fromErrorit never moves toStopped, and the wait just burns the whole time budget.- Windows. Temporary files and caches go through
tempfileonly. Switch console output to UTF-8 withreconfigurefor stdout and stderr, otherwise Cyrillic breaks. - The project is identified by its
Ид. A platform project is identified by theИдofПроект.yaml, and theVendor+Namepair of the manifest only has to be free of the other projects of the space. An upload without a project id lands in the live project that carries thatИд, under whatever vendor and name, and renames it after the build; a freshИдwith a pair another project already holds is refused with a 409 (section 4.4). A truly separate project takes a freshИдand a fresh pair together, which is why an isolated compilation check is built around a throwaway application rather than a throwaway project. - Deletion runs in the background and in order.
DELETE /applications/{id}returns immediately, and the application lives on for a while with aDeleteApplicationtask. While it exists, deleting the build it was created from is rejected with a 500. The order for cleanup is: delete the application, wait until its card answers 404 or its status becomesDeleted, and only then delete the build. A read of the card that breaks off during this wait is a missed poll as well, as in section 6.4. - Compilation is the server's, and it happens on apply. A local build only packs an archive. The syntax, the types and the visibility of the sources are checked by the server compiler when a build is applied or an application is created out of it. There is no separate "compile" endpoint, so the only way to check the sources without risking the working application is a throwaway application created from the same build (section 7,
probe). - Signing in to a freshly created application. A new application gets its own empty user list (
default-user-list, section 4.7), password sign-in in it is off, and it has no account service. The accounts used to sign in to other applications therefore do not work here, and connecting another application's user list (POST /applications/{id}/userlists) together with enabling the local sign-in does not change that; this was verified. What works is a control-panel account: the platform connects its users to the application itself, and the sign-in works right away. The tool has to say so out loud when it creates an application (section 7,apps createandapps ensure): the way in does not follow from the card, and it cannot be found by trying, because a user has a failed-attempt counter. - A server that is still starting. After a start or an update the server takes minutes to bring its console up; thirteen of them were seen after one update. All that time every console request, the token request included, is answered with a 404 whose text names the console application:
Application "console" not found. That is the status a missing application or build gets, so only the text tells them apart. The client recognizes the answer in one place, for every request it makes, and raises an error that says what it means: the server is starting, repeat once/consoleanswers 302.deploywaits it out by itself at any step of its cycle, for up to--server-start-timeoutseconds (900 by default, 0 turns the wait off), and announces the start and the end of the wait. A request the console refused was never processed, so asking again is safe whatever its method. The other commands do not wait: they name the cause and stop. - A removal is applied without a question. A build whose sources no longer have a tabular part is applied as it is: the part goes, and its rows go with it. There is no refusal and no warning, the task ends well and the application keeps running; that was seen on a catalog with a row in the part. An attribute of a tabular part and an attribute of the object go the same way, with their values, and so does a catalog whose description is gone from the sources, with its whole table: that was seen on an empty catalog. A narrowed length or a changed type recreates the data of the object instead. What the deploy does about each is in section 7.
Common flags are accepted in any position, after the subcommand too: --base-url, --client-id, --client-secret, --env-file, --timeout (seconds, default 60), --lang, --json, --quiet. --version is the exception and stands before the subcommand.
A key that takes one value cannot be repeated with a different value: the parser refuses such a call before the command runs, with exit code 2 like any other refusal of argparse, and names the key and both values. --output a --output b used to write to b, and nothing said that a had been dropped. The same value given again passes, because a script that assembles a command line out of parts may well repeat a key. --key=value and --key value count as the same key. The values are compared as the command gets them, so --limit 5 and --limit 05 agree. The rule covers the common flags wherever they stand, the keys of every command and the keys a plugin declares (section 10). The keys meant to be repeated (apps list --status, apps token-access --user, probe --cleanup and a multiple argument of a plugin) collect every value, and a flag given twice means the same as a flag given once.
The connection flags are about talking to the platform. build and inspect never do that, so they refuse those flags instead of dropping them quietly. A call carrying --env-file reads as a build bound to a stand, and twice that left a reader asking whether a local build goes to the server after all; silence of that kind is what misleads. Nothing that worked stops working, because the flags changed nothing, and the refusal names the commands that do reach the platform: deploy and builds upload. It covers the flags only. ELEMENT_* variables and a .env next to the project are always around, and whether a build runs must not depend on them.
Output works like this: the result is JSON on stdout (ensure_ascii=False, indent 2), progress of long operations comes as lines on stderr, and an error is JSON with an error field on stderr plus return code 1.
--json makes that a guarantee rather than just a convention: for the duration of the call stdout is redirected to stderr, and the answer alone is written to the real stdout. A caller then parses stdout whole. Without the flag, anything a handler or a plugin prints stays in the stream ahead of the answer. A failure keeps to stderr and leaves stdout empty in this mode too.
--quiet silences the progress stream for the duration of the call: the progress lines, the summaries and the warnings that go to stderr, a plugin command's context.log among them. The answer on stdout and a failure on stderr with its exit code stay, and the form of the answer does not change: --brief and --json are the keys for that. A plugin command written with --quiet after it used to be refused as an unrecognized argument before it did any work.
apps list and builds list print their count and truncation lines after the answer, never before it. Other stderr output can still come first even for these two commands – a warning such as the one ELEMENT_TLS_VERIFY=false prints, or another command's own progress lines – so only stdout read on its own is safe to parse as a whole.
Commands, with the significant flags in parentheses:
token– obtain and print the token.apps list [--name --status --include-deleted --brief],apps get [APP_ID],apps find NAME [--include-deleted],apps create NAME [--project-id --version-id --latest-build --space-id --tech-version --no-dev-mode --wait --verify --no-verify],apps ensure NAME [--project-id --version-id --latest-build --space-id --tech-version --no-dev-mode --wait --verify --no-verify --apply],apps apply [APP_ID] VERSION_ID,apps delete APP_ID,apps start [APP_ID],apps stop [APP_ID],apps users [APP_ID],apps token-access APP_ID [--user --enable --disable],apps export [APP_ID] [--output],apps export-extension [APP_ID] EXTENSION [--output].apps list --namefilters by a case-insensitive name substring, and it does so on the client because the platform ignores the query parameter (section 4.1).--statusselects by the whole status word, several of them separated by commas or given by repeating the key.--briefprints brief cards instead of full ones: id, name, status, uri, applied version.apps listhides the deleted applications. They stay in the platform list under theDeletedstatus, and a stand a few months old answers with hundreds of cards of which a handful are alive.--include-deletedbrings them back, and so does--status deleted: a filter that would answer with nothing is worse than no filter. A cut nobody is told about is a trap of its own, so the command ends with a count line on stderr, "7 live of 324", plus the number shown when a filter narrowed the answer further. Whatever is cut, stdout stays the same JSON array.apps getaddsapplied-buildto the card: the brief card of the build the application runs, with its branch and commit from the build card or from the local registry of uploads, in the shapebuilds list --briefprints. It is null when the card names no applied build.APP_IDofapps get,delete,start,stop,debugandusersis the application id (UUID) or its exact name. A value that is not a UUID is resolved through the list by an exact case-insensitive match, and deleted applications do not count. No match is an error, and several matches are an error listing the ids: destructive commands must not guess.apps findsearches for an exact, case-insensitive name match among the fieldsname,display-nameandpublication-context. Output is{"id": ..., "found": true|false}with return code 0 in both cases: the absence of an application is an answer, not an error. A non-zero return code means the request failed and comes with JSON carrying anerrorfield on stderr. In scripts, check thefoundfield, not the return code.- Deleted applications remain in the platform list with the
Deletedstatus and their formerid.apps findskips them: the found id must be usable, otherwise the caller gets an id on whichapps getanddeployreturn 404. The--include-deletedflag restores the former behaviour, searching among all applications including deleted ones. apps ensureidempotently brings an application with the given name into existence: it searches by theapps findrules, where deleted ones do not count, and creates only if absent. Output is{"id": ..., "created": true|false, "sign-in": ...}, andcreated: falsemeans the application already existed and was left alone. The creation flags are the same as forapps createand take effect only when creation happens. An existing application is never recreated:deleteandcreateproduce a new URL and break external bindings to the former one. Because the creation flags include--version-id, an existing application gets two more fields in the answer:applied, whether it runs the requested assembly, andapplied-version-id, the one it does run. Staying silent about that cost a stand that went to work on the previous build. An assembly the card does not name is looked up among the builds of the extension projects and judged by the extensions of the application, the wayapps applyandverify-deployjudge it (section 6.1): the card names the build of the application alone, andensureused to answerapplied: falsefor an extension that ran the very build asked for. For such a buildapplied-version-idnames the build the extension runs,extension-project-idandextensionname its project and its row ofextension-projects, and a server without Console API 2.1 getsapplied: null, since it cannot tell.--applybrings the application to the assembly in the same call, andapps applydoes it separately.--verifyupgrades the verdict about an application that already runs the requested assembly from that comparison to the full check. An applicationensurehad created used to answerapplied: trueon trust, since it was created from that assembly, and now answers with the checked verdict whenever a check ran.apps apply [APP_ID] VERSION_IDapplies an already uploaded assembly to an application and verifies the result (section 6.6). An apply is not a fact until it is verified: on a failure the platform silently rolls the application back to the previous build and starts it. The output is the verification report, and the exit code is 1 when the assembly did not land. Until this command, applying was reachable through the MCP tool alone, while long operations are the ones that want a CLI run in the background. The assembly of an extension is applied the same way and verified by the extensions of the application (section 6.1): the card goes on naming the build of the application, and comparing with it called every extension apply a rollback, the successful ones included.apps createandapps ensureend by saying how to sign in to the application (section 6.11). That is thesign-infield of the output, shaped{"url", "account": "control-panel", "hint", "note"}, plus the same two sentences on stderr.urlis the application address out of the card, and it isnullwhile the application has none yet, which is the case without--wait; the hint then says where to take it from.accountis a code, not a text: the way in is a control-panel account, and the note says why the accounts used to sign in to other applications do not work here.--latest-builduses the project's latest build as the source and protects against an empty skeleton (section 6.2).--waitwaits until ready (section 6.4), verifies the result (section 6.6) and outputs the final card.- A source given by
--version-idis looked up in the build list of the project before anything is created. The project is--project-id, orELEMENT_PROJECT_IDwhen the flag is absent. The platform deletes the builds nobody uses and cannot create an application from a deleted one: it answers with a bare 400 "Can't create application", which reads like a limit on the number of applications. So the command refuses by itself. A build the project does not list is looked for in the other projects of the stand first: a build uploaded without a project id sits in the project the server chose for it, whileELEMENT_PROJECT_IDnames the project the stand works with. No method finds a build by its id alone, so the search reads the build lists: the project the local registry of uploads remembers for the build first, then the others, newest first. A project fromELEMENT_PROJECT_IDis a default, and it gives way to the project of the build, with a line on stderr. A project named by--project-idis not replaced behind the caller's back: the refusal names the project the build is in and the flag to pass. Such a build used to be refused as a deleted one, and the refusal offered a build of the project from the environment instead. When no project lists the build, the refusal names the cause and offers the build that a running application of the project runs, or--latest-buildwhen no application runs one. When the other projects cannot be read, the refusal says that the project came fromELEMENT_PROJECT_ID, because the build may belong to another project. Without a project there is no list to look in, and the create goes as before. - Waiting means verifying. A failed apply is rolled back by the platform to the previous build, and the application comes up
Runningall the same, so a card handed back after a wait is not evidence that the build asked for is the one running.--waittherefore ends with the same checkapps applyandverify-deploydo: the applied build id against the requested one, the application tasks that failed since the creation started, the uri. It puts the report into theverifyfield of the output and answers with exit code 1 when the check does not pass.--verifyasks for the check on its own, and waits too, because there is nothing to check on an application still being created.--no-verifybrings back the plain wait. Without either flag nothing is waited for and nothing is checked, as before. - The application exists once the create has answered, so a wait or a check that breaks off does not lose it. The output still carries the id: the card of
apps create, theidofapps ensurewithapplied: null, since nothing was checked. Thewait-errorfield holds the error that ended the wait, stderr names theverify-deploycall that checks the build later, and the exit code is 1. The answer used to be a bare network error without the id, and the id of an application that came up minutes later had to be looked up by name. apps users [APP_ID]prints the users connected to the application the wayGET /applications/{id}/usersgives them (section 4.1):user-list-id,user-id,presentation,is-admin,token-access-enabled. There is no login among them: the platform names a user of the control panel by the login inpresentationand any other user by its presentation. The command only reads, so the application defaults toELEMENT_APP_ID, and it ends with a count line on stderr: how many users are connected, how many of them administer the application and how many reach its HTTP services by a token. Until the command the list was seen only in a refusal ofapps token-access.apps token-access APP_IDshows or switches the access of a user to the HTTP services of the application by a token (section 4.1). The application is named explicitly, by the argument or by--app-id:ELEMENT_APP_IDnames the working application, and a switch that opens its services must not land there by default.--useris a login, a presentation or a user id, and without it the command speaks about the account elemctl itself signs in with (GET /me). Without--enableor--disableit only reads; both at once is an error. A switch reads the connection back, sotoken-access-enabledin the output is what the platform keeps after the change, and a flag that did not move is an error with exit code 1. The output is{"app-id", "user", "user-id", "user-list-id", "token-access-enabled", "changed"}, wherechangedsays whether this very call altered anything: the state that is already there sends no request. A user who is not connected to the application is named as such, with the users that are, before anything is sent.--usermay be repeated, and then every user is read or switched in turn. The output becomes{"ok", "app-id", "users"}with an entry per user in the order given: the report above with"ok": true, or{"ok": false, "user", "error"}for a user that could not be found or switched. Such a user does not stop the others, and the return code is 1 when any entry failed. The same user given twice counts once, and a single user keeps the output above.apps export [APP_ID]saves the build the application runs to a file (section 4.1).--outputis the file to write or a directory for it. Without it the file lands in the current directory under the name{Name} {Version}.xasmtaken from the manifest of the archive, the name a build of elemctl gets; for a build uploaded into a project that is the number the server gave it. The extensions applied to the application are not in the archive,apps export-extensionsaves them. The command only reads the server, so the application defaults toELEMENT_APP_IDthe wayapps getdoes. The output is{app-id, file, size, manifest}, wherefileis the full path andmanifestis theAssembly.yamlof the archive. An answer that is not a build archive is refused, and no file is written. A server without the method is refused with the reason and the answer its console gave (status 401, section 4).apps export-extension [APP_ID] EXTENSIONsaves the build of an extension applied to the application to a file (section 4.1, Console API 2.1).EXTENSIONis the id of the extension, the id of its project, the name or the presentation of the project, compared exactly and without regard to case. The value is looked up among the extensions of the application first, because the export answers a miss with a 500 that names no cause: no match is an error naming the extensions the application has, and several matches are an error listing them.--outputis the file to write or a directory for it. Without it the file lands in the current directory under the name{Name} {Version}.xasmtaken from the manifest of the archive, the name a build of elemctl gets. The command only reads the server, so the application defaults toELEMENT_APP_IDthe wayapps getdoes. The output is{app-id, extension-id, project-id, vendor-name, project-name, assembly-version, enabled, file, size, manifest}, wherefileis the full path andmanifestis theAssembly.yamlof the archive. An answer that is not a build archive is refused, and no file is written. A server without the method is refused with the reason and the answer its console gave (status 401, section 4), rather than with what reads as a failed sign-in.
spaces list.user-lists list [--name],user-lists get [LIST] [--app],user-lists self-registration [LIST] [--app --enable --disable],user-lists password-login [LIST] [--app --enable --disable]– user lists and their sign-in settings (section 4.7). The target is theLISTargument, an id or the exact presentation, resolved like an application name: no match is an error, several matches are an error listing the ids. The other way to give a target is--app, the application's own list out of itsdefault-user-list; giving both is an error. Without--enableor--disablethe two setting commands only read the current state, so the same command answers "how is it now". Both flags at once is an error.password-loginworks on the account service of typeLocal. The output is{"list-id", "enabled", "changed"}, whereenabled: nullmeans the list has no such service at all and nothing signs in by password, andchangedsays whether this very call altered anything: switching to the state that is already there sends no request.projects list [--name --include-deleted],projects get [PROJECT_ID],projects delete PROJECT_ID.projects list --namefilters by a case-insensitive name substring, and it does so on the client because the platform answers the full list (section 4.3). Projects marked deleted are hidden unless--include-deletedis given: a stand a few months old keeps hundreds of them in the list against a handful of live ones, and a check for a project name must not cost the full listing.
builds list [--project-id --limit --brief],builds get VERSION [--project-id],builds upload FILE [--project-id --new-project --force-rename --space-id],builds delete VERSION [--project-id].builds uploadsends the commit the manifest of the archive names ascommit-id(section 4.4), names on stderr a build the server numbered otherwise than its archive (section 4.4) and reports the target in the output throughproject-idandproject-id-source:flagorenvfor the project the call named,serverfor the project the server chose, and a note on stderr says when the target comes fromELEMENT_PROJECT_ID.--new-projectignores the environment binding and uploads without a project id: the server puts the build into the live project that carries itsИд, or creates one when there is none (section 6.8); it is mutually exclusive with--project-id. Such an upload names that project in theprojectfield,{id, name, configuration-id, created, renamed-from, found-by}, and in a line on stderr. The project comes from theartifactof the answer (section 4.4), and an answer without one sends the client through the build lists (found-byisresponseorbuild-list).createdandrenamed-fromare judged against the project list read before the upload: an id the list did not have is a project the upload created, and an id it had under another name is a project the upload renamed. The rename is a warning on stderr, since deleting the build does not bring the name back. The answer used to printproject-id: null, and a project the server had created for the build was then looked up by hand. A list that cannot be read leavescreatednull and does not stop the upload.--force-renameallows uploading an assembly whose name differs, which renames the target project.builds listshows the ten newest builds, and--limit 0lifts the cut. It ends with a count line on stderr saying which of the two cuts the reader is looking at: the tool's--limit, or the platform's housekeeping (section 4.4: it deletes the builds nobody uses, whatever their age). "30 of 30" without that line was read as the project's whole history. The housekeeping is judged by the facts of the answer, not by the length of the listing: the platform hands out the numbers of a base version one after another, so a hole in the numbering is a build already taken away, and the line says there are holes without counting them. A hole can also be a jump: an upload without a project id keeps the number of its archive, and the next upload into the project counts on from it. Seen live,1.0.0-3was followed by1.0.0-500and1.0.0-501, and the numbers between had never existed. The created stamps of the neighbours cannot tell a jump from a loss, since the server handed out four numbers in a third of a second, so the local registry of uploads does: a hole under a build this machine uploaded without a project id is named as a jump and is no evidence of the housekeeping. A build uploaded that way from elsewhere is not in the registry, and its hole still reads as a deletion. A hole can be a jump and a deletion at once. Seen live, the listing went from1.0.0-8straight to1.0.0-52after1.0.0-50, uploaded without a project id, and1.0.0-51were deleted, and the line called the hole a deletion alone; a jump to1.0.0-10over1.0.0-4, which the housekeeping had taken, was called a jump and no deletion. So a hole under a jump of this machine is a jump only while the registry knows no upload of the project inside it. A hole with such an upload inside, under a jump or with the jumped build itself inside, is named as both: the build that brought its number from the archive and the uploads of this machine the listing no longer has, the first three of them and how many more. It counts as a deletion. An empty listing says the project has no builds, and a listing without a single numbered build says there is no numbering to judge by, instead of calling the numbering unbroken. An extension project is outside the housekeeping altogether (section 4.4), and its count line says so: a hole there is a build deleted by hand, and the builds nobody needs stay untilbuilds deletetakes them. The kind comes from the project card, and a card that cannot be read leaves the line any other project gets.--briefprints the id, the versions, the date, the branch and the commit of a build. The branch and the commit come from the card when the platform filled them, otherwise from the local registry of uploads, andbranch-name-sourceandcommit-id-sourcesay which answered:platform,registry, or null when neither knows. The registry alone knowsdirty, whether the tree had uncommitted changes, andproject-dir, the directory the build was made from.- The local registry of uploads. Every upload elemctl makes –
deploy,builds upload,probe– appends a JSON line touploads.jsonlin the data directory of the user:ELEMCTL_DATA_DIRwhen it is set, otherwise%LOCALAPPDATA%\elemctlon Windows and$XDG_STATE_HOME/elemctl(~/.local/state/elemctl) elsewhere. A line holds the build id, the project id, the version, the branch, the commit, thedirtyflag, the directory of the sources, the archive, the stand, the command, the application the upload was made for (the target of a deploy, the throwaway application of a probe), the route of the upload (projectinto a project by its id,no-project-idwithout one; the lines written before the route got that name carryvendor-nameand are read the same way) and the time. The platform keeps the commit alone, and with several sessions deploying from one machine a build on an application could not be traced to the working tree it came from. The registry is local: a build uploaded from another machine, from CI or by an elemctl that had no registry yet is not in it. A registry that cannot be written is a warning on stderr, never a failure – the build is on the server by then – and one that cannot be read reads as empty. The schema guard ofdeploytakes the commit of an applied build from here when the card of that build carries none.deploycounts the number of the next build from here as well: the version a build of the project got stays in the registry after the build is deleted (section 5). The registry keeps the newest 1000 uploads,ELEMCTL_REGISTRY_LIMITsets another number and0keeps every one: once the file has grown past the limit by a tenth, the next upload cuts it back to the newest lines. The file is rewritten beside itself and swapped in by a rename, and a line another process appended meanwhile is carried over; a swap the system refuses leaves the file as it was until the next upload. build [--project-dir --output --build-version --last-build --commit --branch --kind {application,library,extension} --require-clean]– build the archive locally. Output:file,name,vendor,version,version-source(flag,last-build, the CI variable name ordefault),kind,branch,commitanddirty, which says whether the project directory has uncommitted changes and is null when git is unavailable. The version is a field of its own so CI does not parse the file name. Without--project-dir, the project directory is found automatically: the first directory withПроект.yamlwhen descending from the current one.--kinddefaults based onВидПроекта.--require-cleanaborts before building when the project directory has uncommitted changes; git being unavailable also aborts, because there is nothing to confirm a clean tree with.inspect FILE– parse a prebuilt archive (section 5.1). Local as well: the platform is not called, and the connection flags are refused the same way.deploy [--app-id --project-id --project-dir --output --build-version --branch --commit --dry-run --require-clean --allow-data-loss --server-start-timeout]– the full cycle: build -> upload -> apply -> restart -> verification of the actual apply (section 6.1). Output – a JSON report with fields:app-id,uri,status,version,assembly-id,assembly-version(the version the server gave the uploaded build),renumbered(true when that is not the version of the archive, null when the upload did not say),applied-version,applied(true, false or null, where null means the actual version could not be determined),uri-status,problems(list of strings, the platform's texts as they came),problems-lines(the same broken into plain lines: JSON escapes a multi-line refusal intoandexactly where it has to be read),ok(boolean),dirtyanddirty-files(uncommitted changes of the project directory at build time),extension-project-idandextension(below). The build captures the current disk state, so the divergence from HEAD must be visible; a warning also goes to stderr, and null means git was unavailable.hintpoints at the log of the server when a task was refused without a compilation error in its text. The platform may answer with "Contact administrator for details" alone, and the cause then sits in the server log: the lastCaused byline, withSrcPath:beside it naming the file the apply stopped at. An apply that leaves the application inErrorends the deploy with an error, and that error carries the same hint. The server numbers a build uploaded into a project itself (section 4.4), so an explicit--build-versionit will not keep – a suffix that is not a number, no suffix at all, a base other than theВерсияof the project – is named on stderr before the build, and a build the server numbered otherwise than its archive is named right after the upload. Without--build-versionthe deploy counts the number on from the higher of the build list and the local registry of uploads (section 5): the server does not give the number of a deleted build again, and the registry remembers the numbers the uploads of this machine got. A build uploaded from elsewhere and deleted since is in neither of them, so the count can still fall short, and the line after the upload then says that the number is the server's, without a warning. A build of an extension project,ProjectKind: Extensionin the manifest, is verified by the extensions of the application (section 6.1):extension-project-idnames the project,extensionis the entry ofextension-projectsthe verdict rests on, null when the application has no such extension, andapplied-versionandapplied-version-idname the build the extension runs rather than the build on the card. Return code 0 only whenok.--dry-runbuilds and stops there, and--require-cleanaborts before building on a dirty tree.--server-start-timeoutis how many seconds a server that is still starting is waited out (section 6.12). Before anything is built, the schema guard reads the sources against the commit the applied build was made from: thecommit-idof its card (section 4.4), or, when the card carries none, the commit the local registry of uploads remembers for that build. A build that created its project has no commit on its card, and the registry knows one when the build was uploaded from this machine; a line on stderr says the commit came from there, and whether that build was made from a tree with uncommitted changes, which the comparison cannot see.schema-commitnames the commit compared against andschema-commit-sourcewhere it came from,platformorregistry. The project directory is found the way the build finds it. A narrowed length or a changed type of an attribute, a dimension or a resource, the attributes of a tabular part included, and a removed dimension recreate or break the data, so the deploy refuses them unless--allow-data-lossis given. So does an element that keeps data of its own – a catalog, a document, an information or accumulation register, a constants set, an exchange plan or a settings storage – whose description is gone from the sources: its whole table goes with every row. The disk cannot tell of a file that is not there, so the guard lists the files of the applied commit with git and follows an element by itsИд. A description moved to another subsystem or renamed keeps its data and is compared with its old text; an element without anИдcannot be told from a moved one and is left alone. A file still in its place that carries anotherИдdescribes another element: the description was made anew, and the element it used to describe is followed the same way. Checked live, a catalog described anew under its old path and name was applied without a question and came out empty, so such an element is refused as a removal, and the refusal names the formerИдto give back when the element is meant to be the same. The fields of the two descriptions are not compared with each other: they belong to different elements. A removed attribute, resource, tabular part or attribute of a tabular part is a deliberate edit the server applies without asking (section 6.13): the deploy names each one on stderr and in theschema-warningsfield and goes on. Fields are matched byИд, so a rename keeps its data, and across a translation of the description only anИдcan tell that an element is gone.schema-checksays what the guard did:clean,warned(removals only),allowed, orskipped:<reason>when there was nothing to compare against:no-project-dir,read-failed,no-applied-build,no-applied-extension,applied-build-not-listed,no-commit-idorcommit-unavailable, each explained in words of its own. The sources of an extension are compared with the build the extension runs, found through the extensions of the application, and an extension the application does not have yet isno-applied-extension: its first apply has nothing to compare against. A skipped check is said once more after the verdict: a passed verification alone read as if the schema had been checked too.verify-deploy [APP_ID] [--app-id --version-id --expected-version --since-minutes]– the verification of section 6.1 on its own, deploying nothing. It looks at the application tasks in an error status raised over the last--since-minutesminutes, whoseerror-messagecarries the file and the position of a compilation error. Then it compares the applied build with the expected one:--version-idis the id of the uploaded build and the reliable comparison,--expected-versionis the version string and the backup. It finishes with a control GET on the address. The report is the same asdeploygives, and the return code is 0 only whenok. It is what a CI script needs afterapps create: a build that failed to apply is rolled back silently, and a status ofRunningproves nothing. A--version-idthe card does not name is looked up among the builds of the extension projects of the stand (project-kind, section 4.3): a build found there is an extension build and is verified by the extensions of the application, the waydeployverifies one, and any other build keeps the verdict of the card. The lookup costs the listing of the projects and one build listing per extension project, and it runs only when the card names another build.--expected-versionalone leads to no project, so an extension is verified by its id.probe [--project-dir --output --build-version --name --space-id --keep --require-clean],probe --cleanup APP_ID– an isolated compilation check of the sources: build -> upload -> a throwaway application (that is the compilation, section 6.10) -> errors with file and position -> cleanup. Before the build the manifest is checked for what the server needs to take a probe:Представление(Presentation), without which the console refuses the upload with a bare 500;ЯзыкРазработки(DevelopmentLanguage), a required property without which no application is created; andЯзыкиЛокализации(LocalizationLanguages) wheneverЯзыкПоУмолчанию(DefaultLanguage) is set, since the server refuses that pair without naming the cause. A missing key stops the probe before anything is built or uploaded, and the error names every such key with the line to add, in the spelling the manifest uses.ELEMENT_APP_IDandELEMENT_PROJECT_IDare deliberately left unused: the probe must not be able to reach the working application, and the target project is chosen by the platform out of theИдof the project descriptor (section 6.8). The default build version is{base}-probe-{token}. It has to be a new one every time, because a repeat is a 409, and it must not look like the project's latest build: the counter is numeric, and a non-numeric suffix keeps a probe build out of that comparison. Cleanup runs whether the compilation passed or failed, in the order of section 6.9: the application, then the build, then the project, and the project only if the probe itself created it. Output:ok,project-dir,vendor,name,file,version,project-id,project-created,project-renamed-from(the former name of a project the probe build renamed: the build goes into the project that carries theИдof the sources, and a presentation edited in them renames that project; a line on stderr says so, the cleanup does not bring the name back, and the field is null when nothing was renamed),assembly-id,app-id,app-name,status,errors(a list of{file, entry, line, column, environment, message}, wherefileis the path relative to the project directory),messages(the platform texts verbatim, so nothing is lost when the failure is not a compilation one),cleanup(kept,app-deleted,assembly-deleted,project-deleted,problems,command,steps) andhint, the pointer to the server log that thedeployreport carries too.hintis filled when the server refused and named no file; a wait that ran out of time leaves it empty. A stand that does not know the compatibility mode of the project refuses the whole project and then complains about types and properties of that mode in files the change never touched. That refusal is recognized, the parsing stops there,compatibility-refusednames the mode andmessages-droppedcounts what followed from it, so a verdict about the stand cannot read as a verdict about the code. Return code 0 only whenok. A failed cleanup is a problem in the report and on stderr; it does not change the compilation verdict.--keepleaves the application and the build in place for a hands-on look. What a probe does leave behind is a tombstone: the platform keeps deleted applications in the list with theDeletedstatus and their former id, and there is no API to remove them. Whatever a probe leaves, on purpose with--keepor through a cleanup step that failed, comes with the commands that remove it, incleanupand on stderr:commandis the one command,probe --cleanup APP_ID, andstepsare the same by hand in the order of section 6.9, with the build addressed in the probe's own project. A barebuilds deletelooks in the project ofELEMENT_PROJECT_ID, where the probe build is not, and a build deleted right after its application is refused with a 500 until the application is gone. Both lines name the--env-filethe probe was run with.probe --cleanup APP_IDremoves a probe left on the stand, starting from its application, an id or a name. The application card names the project and the build it was created from, so nothing has to be remembered between the runs. Only a probe's application is touched: its name starts withelemctl-probe-, or the build it runs carries-probe-in its version, which covers a probe named with--name, or the local registry of uploads remembers a probe of this machine creating it, which covers a probe given both--nameand--build-versionand one that got deploys of its own since. Any other application is refused with the reason, and so is the oneELEMENT_APP_IDnames. The order is that of section 6.9: the application, a wait until it is gone, the build of this very probe (its token in the version, or the build the registry remembers the probe uploading) together with every build the registry remembers uploading for this application, and the project last. A build uploaded for the application that another live application runs now stays, and its entry inbuildsnames that application inkept. The project goes only when no build is left in it and no live application runs it, and never when it is the project ofELEMENT_PROJECT_ID: a probe usually lands in the project that owns its sources, the working one, while a project the probe created ends up empty. The builds other runs uploaded into the probe's application from another machine, which the registry does not know, are left to the platform, which deletes the builds nobody uses. A second run finishes what the first one left: an application that is already a tombstone is not deleted twice, since the list keeps its card, and a project deleted already is not deleted again. The run builds nothing, so the flags of a probe run are refused beside--cleanup. Output:ok,app-id,app-name,project-id,app-deleted,builds({id, version, deleted}each),project-deleted,project-kept(why the project stayed) andproblems; return code 0 only whenok.--cleanupmay be repeated, and then the probes are removed in turn, one after another: two probes of the same sources share a project, and it can go only after the last of their builds. The output becomes{"ok", "cleanups"}: the report above for each application in the order given, or{"ok": false, "app-id", "error"}for one that was refused (not a probe's, the working one, not found), withapp-idas it was given. A refusal does not stop the others.okand the return code 0 need every cleanup to finish. The same application given twice counts once, and a single one keeps the output above.branches list [--project-id --name],branches get ID,branches create NAME [--project-id --app-id],branches update ID [--app-id],branches delete ID,branches merge ID.dumps create [APP_ID] [--description],dumps get APP_ID DUMP_ID.tasks list [--app-id],tasks get-group TASK_ID.tech get [APP_ID],tech set APP_ID VERSION.debug-adapter– the path to the platform debug adapter directory supplied by a plugin (theelemctl.debug_adapterentry-point group, section 10). Output{"path": ..., "found": true, "adapter-class": ...}when present or{"path": null, "found": false}; exit code 0 in both cases. Thepathis a ready value for the VS Code extension'sxbsl.debug.adapterPath(a directory with arepo/subdirectory).plugins– diagnostics: what the plugins bring.debug-adapterholds the declared adapter directories and whether each of them holds jars.commandsholds the commands of the plugins with the entry point they arrived through and the name of their MCP tool, which isnullwhen the command stays out of MCP.failuresholds what was left out: a plugin that did not load, or a command that would have taken over a name of the core, each with the entry point and the reason. The answer looks like{"debug-adapter": [{"path": ..., "has-jars": true|false}], "commands": [{"name": ..., "source": ..., "mcp": ...}], "failures": [{"source": ..., "error": ...}]}.- The subcommands the plugins bring (section 10, the
elemctl.commandsgroup) stand alongside the commands of the core and are listed by--help. They may not take over a name of the core; the command reference describes the core alone. A plugin that fails to load is left out and does not take the CLI down (section 10): every command names it on stderr, and a call to a command that is missing while plugins failed is refused with JSON on stderr, the failures in itsplugin-failuresfield. self-update [--version X] [--stop-holders[=all]]– update the installed elemctl by unpacking the wheel from PyPI into site-packages, without touching busy exe files. Plain pipx or pip breaks the install whenelemctl.exeis held by a running MCP server. Here only the package files are updated, and the exe stub calls the new code. The command also fixespipx_metadata.json. Output is{updated, from, to}. Without--versionthe latest release is asked of every source PyPI has. Both listings of releases, the simple index and the JSON summary, are cached node by node and may name the previous release for minutes after a new one is out, so the command reads both, takes the newer answer and then asks for the pages of the three next numbers (0.44.1,0.45.0and1.0.0after0.44.0): the page of a new release is there as soon as it is published. When the sources disagree, a line on stderr says what each of them named. An installation newer than every source is left as it is rather than replaced with the release before. A version given with--versionthat the index does not list yet is looked up on its own page, and only a 404 there means the version does not exist. When the files of the package are held, the command names the holders by pid and command line and refuses; the previous installation stays as it was.--stop-holdersends the servers first, sinceelemctl mcpwould keep running the old code anyway. The running commands of other sessions are named and left alone: each is somebody's work in progress, such as a wait for a pipeline. When one of them keeps the files busy, the refusal says to wait for it and repeat.--stop-holders=allstops the commands too, and a stopped command leaves no result.mcp– start the MCP server; without the extra installed – a clear error with the hintpip install "elemctl[mcp]".
Positional APP_ID and PROJECT_ID marked as optional above are taken from the configuration when absent, from ELEMENT_APP_ID and ELEMENT_PROJECT_ID. If those are empty too, the command errors out.
Server name elemctl, stdio transport, credentials from the same environment variables and .env. In the server instructions, warn about the silent rollback of build apply (section 6.1). The tools, whose docstrings are short and in Russian:
list_apps(name="", status="", include_deleted=False) – the filters of apps list
(section 7): name by a case-insensitive substring on the client (section 4.1), status
by the whole status word, and the deleted applications hidden unless asked for. The answer
is an object with total, live, shown, a summary line and applications, so that
what was hidden is stated rather than guessed at; get_app(app_id) – the card with
applied-build, as apps get prints it (section 7), find_app(name),
create_app(name, project_id="", version_id="", space_id="", development_mode=True, verify=False) – when only project_id is given, the project's latest
build is automatically used as the source (section 6.2); verify=True waits for the
application and checks that the build asked for is the one it runs, putting the report into
the verify field of the answer (the same check ensure_app(verify=True) makes, for a
created application as well as for one that was found); a wait that breaks off keeps the id
of the created application in the answer, beside a wait-error field with the reason, and
ensure_app then answers applied: null; over an application that already exists,
ensure_app judges an extension build by the extensions of the application, as
apps ensure does (section 7); a version_id is looked up before the create, as in the
CLI (section 7), with the project taken from project_id or from the stand's
ELEMENT_PROJECT_ID: a build of another project is found there, the stand's project gives
way to it, an explicit project_id is refused with the project the build is in, and a
build no project lists is refused as deleted; create_app and ensure_app add a
sign-in field to their answer – the way into the application (section 6.11) in the
same shape the CLI prints: an agent sees only the JSON, so the hint has to live there;
start_app(app_id), stop_app(app_id), debug_info(app_id) – data for a
debug session (requires debugging enabled on the server), debug_adapter() –
the path to the platform debug adapter from a plugin (section 10; a local
operation that does not call the platform), delete_app(app_id)
(the docstring – a warning about irreversibility and URL change), list_spaces(),
app_id of get_app/delete_app/start_app/stop_app/debug_info is the
id (UUID) or the exact application name (resolved like the CLI does),
list_projects(name="", include_deleted=False) – the filters of projects list (section 7),
list_builds(project_id, limit=10, brief=True) – an object {total, shown, summary, builds}:
the listing has to say whether it is the whole store, judged by the gaps in the build
numbering (section 4.4) and naming a jump the local registry explains (section 7), the
summary of an extension project says that no housekeeping takes its builds, and a
brief card names the source of its branch and commit, the
card or the local registry of uploads, the way builds list --brief does (section 7),
get_build(project_id, version) – the whole card of one build, addressed by the build
version (section 4.4; an id is accepted and resolved through the listing),
build_assembly(project_dir="", output_dir="", version=""),
inspect_assembly(file) – parsing of a built archive (section 5.1; a local operation),
deploy(app_id, project_id, project_dir="", version="", branch="", allow_data_loss=False, server_start_timeout=900) – returns the deploy report plus a log field with progress
lines, and waits out a server that is still starting (section 6.12); allow_data_loss=True
lets through what the schema guard refuses, the way --allow-data-loss does in the CLI
(section 7), and without it the refusal comes back as an error before anything is built; probe(project_dir="", space_id="", keep=False) – an isolated
compilation check that does not touch the working application (section 7),
the report plus a log field; probe_cleanup(app_id) – probe --cleanup (section 7),
the removal of a probe left on the stand, the report plus a log field; app_id may be
a list, answered the way a repeated --cleanup is, {ok, cleanups} plus log; apply_build(app_id, version_id), verify_deploy(app_id, expected_version="", since_minutes=30) – verification of the apply per section 6.1, an
extension build included (section 7, verify-deploy);
list_app_tasks(app_id=""), list_branches(project_id="", name=""),
merge_branch(branch_id), list_user_lists(name="") and
configure_user_list(list_id="", app_id="", self_registration=None, password_login=None) –
the sign-in settings of a user list (section 4.7) in one call: the list is given by id, by
presentation or by the application whose own list it is; both flags are optional, and
without them the tool only reports the state (self-registration-enabled,
password-login-enabled, changed); list_app_users(app_id) – the users connected to
the application as apps users shows them (section 7), in an object
{app-id, total, summary, users}; token_access(app_id, user="", enabled=None) – the
access of a user to the HTTP services of the application by a token, as apps token-access
shows and switches it (section 7): app_id is required, an empty user means the account
elemctl signs in with, and without enabled the tool only reads; user may be a list,
answered the way a repeated --user is, {ok, app-id, users} with an entry per user;
export_app(app_id, output="") – the build the application runs saved to a file, as
apps export does it (section 7); an empty output is the current directory of the server
process;
export_extension(app_id, extension, output="") – the build of an extension applied to the
application saved to a file, as apps export-extension does it (section 7); an empty
output is the current directory of the server process.
The tools the plugins bring (section 10, the elemctl.commands group) are registered
alongside these: the schema is built out of the declared arguments, the description is the
help of the command, and the core adds an env_file parameter of its own. A name already
taken by a tool of the core is not taken over: that command is left out, and so is a plugin
that fails to load. The server names them on stderr, which a client keeps as its log, and
starts with the rest.
- pytest tests without network access: .env parsing and configuration priorities,
file selection and build archive contents (including the manifest), auto-increment
and numeric comparison of versions, deploy outcome logic (
ok/applied), the hint onFAILED_PRECONDITION, application search by name, plugin discovery through entry points and debug-adapter path resolution (stubbed entry points, directories in temp folders), adapter extraction from a tiny .car, self-update by unpacking a wheel (urllib mocked, wheel and site-packages in temp folders). - Docstrings, comments and identifiers – English, tests and tools included: the project is
public and international. Russian stays where it faces the user: the i18n message catalog,
argparse help, user-facing strings, MCP tool descriptions and the server instructions, plus
platform identifiers quoted as they are (
Проект.yaml,Ресурсы,Имя,Поставщик) and the Russian data of test fixtures. Straight quotes"in text, dashes – en dash–(not em dash), ellipsis – three dots.... - The library never prints to stdout or stderr itself: it delivers progress through a callback the caller passes in.
- API errors are raised as a dedicated exception carrying JSON-serializable details of the server response.
elemctl discovers external packages through importlib.metadata.entry_points. The core declares nothing about plugins in its own pyproject.toml: it is a consumer that reads the entry points on demand. Non-publishable vendor artifacts, the proprietary 1C jars, then live in a separate package while the public core stays clean.
The elemctl.debug_adapter group. The entry-point value is a path, as a Path or a str, or a zero-argument callable returning one (() -> Path | str). The path points to the platform debug adapter directory, meaning a directory with a repo/ subdirectory holding the adapter jars, com.e1c.g5rt.debugger.adapter*.jar among them. This is a ready value for the VS Code extension's xbsl.debug.adapterPath.
Declaration in a plugin package:
[project.entry-points."elemctl.debug_adapter"]
name = "my_package:adapter_root"The elemctl.commands group. The entry-point value is a Command, a list of them, or a zero-argument callable returning either. One declaration serves both surfaces: the core builds a CLI subcommand and an MCP tool out of it and knows nothing about what the command does. This is where a command belongs when it knows about someone's own environment: internal circuits, neighbouring systems, private stands. A public core is no place for it.
[project.entry-points."elemctl.commands"]
name = "my_package.commands:commands"The declaration types are exported from elemctl.plugins:
Argument(name, help="", type=str, default=None, required=False, choices=(), cli_alias="", multiple=False)–nameis"--stand"for an option or"stand"for a positional argument. The value name,dest, is the name without the leading dashes and with the inner ones replaced by underscores, exactly as argparse does it. The types arestr,int,floatandbool. Aboolmeans a flag,store_truein the CLI and a boolean defaulting tofalsein MCP, so it cannot be positional.requiredworks for both kinds: a positional argument is optional unlessrequired=Truemakes it required.cli_aliasgives a positional argument a CLI-only key synonym:Argument("page", cli_alias="--page")accepts bothwiki-get 123andwiki-get --page 123. The MCP tool schema keeps the onepageparameter it always had – the alias is a parser convenience, not a second declared argument. The CLI builds the two forms as a mutually exclusive pair sharing onedest: both at once, or neither of a required argument, is a parser refusal. An option cannot declarecli_alias– it already has a name to call it by.multiple=Truelets an argument take several values: the handler gets the list of the values in the order of the command line, the declared default as a list when none is given,[]without one, and the MCP tool gets an array of the declared type. An option may then be given more than once, and the CLI builds it withaction="append". A positional argument takes its values one after another, withnargs="+"when it is required andnargs="*"otherwise, and itscli_aliaskey is built withaction="append"like an option:wiki-get 123 456andwiki-get --page 123 --page 456hand over the same list, and the two forms stay a mutually exclusive pair. The CLI keeps the default out of the parser, so the command line replaces the default rather than adding to it. An argument without the field takes one value: the parser refuses its option given again with a different value, as it refuses any key of one value (section 7), and lets the same value through. A flag cannot be multiple, and the default of a multiple argument isNone, a list or a tuple. A plugin that also runs on an older core checkshasattr(Argument, "multiple")before declaring a multiple option, since the older dataclass refuses the keyword and the plugin is left out. A multiple positional argument needsPOSITIONAL_MULTIPLEofelemctl.pluginsas well, read asgetattr(plugins, "POSITIONAL_MULTIPLE", False): a core that knowsmultiplefor options alone refuses it on a positional argument with aPluginError, and thehasattrcheck holds there too.Command(name, help, handler, arguments=[], mcp=True, mcp_name="")–nameis the CLI subcommand. The MCP tool is namedmcp_name, or the same name with dashes turned into underscores.mcp=Falseleaves the command in the CLI only.sourceis filled in by discovery with the name of the entry point.CommandContext– what the handler gets.configis the assembled connection configuration.clientis a platform client built on first use and cached, so a command that never reaches the platform does not demand credentials.log(message)takes progress lines: the CLI prints them to stderr as they come, and the MCP tool collects them into thelogfield of its answer.surfacenames the caller:"cli"for a subcommand,"mcp"for a tool, the values ofSURFACE_CLIandSURFACE_MCP. It isNonein a context built outside the core, by a test or a library caller. A plugin used to tell the two apart by the shape oflogalone, which is the core's to change. A plugin that also runs on an older core reads it withgetattr(context, "surface", None), since there the attribute is missing.
The handler is called as handler(context, **values), the values keyed by dest. Its result must be JSON-serializable: the CLI prints it, the MCP tool returns it. The CLI exit code is taken from the result:
- a dict result whose
exit-codefield holds an integer from 0 to 255 ends with that code. Abooldoes not count as an integer here, and the range is what a process returns portably: POSIX keeps only the low eight bits of an exit status, so 256 would arrive as 0; - otherwise a dict result with
"ok": falseends with 1, the same convention thedeployandprobereports follow, and every other result ends with 0; - a valid
exit-codewins overok, so{"ok": false, "exit-code": 0}ends with 0. A value of another type or out of the range is ignored, andokdecides.
The name of the field is exported as elemctl.plugins.EXIT_CODE_FIELD; a plugin that also runs on an older core can tell by its absence that the process code there follows ok alone. The MCP tool returns the exit-code field untouched, with the rest of the result. The handler does not end the process itself: the same function serves the MCP server, where a SystemExit leaves the call unanswered and stops the server. To the MCP tool the core adds an env_file parameter, as every core tool has, and, for a dict result, a log field with the progress lines.
Discovery behavior:
- entry points are sorted by name;
debug_adapter_path()returns the first directory that actually holds the adapter jars (a directory withoutrepo/or without the adapter jar is skipped), otherwiseNone; - a failing entry point is an error,
PluginError, a subclass ofElemctlError, rather than a silent skip: a tool that silently drops a plugin would leave the user without debugging and without an explanation. Both loading an entry point and calling the function it names are guarded. A plugin written for a newer core fails in that call, with aTypeErrorabout a field the installed core does not know, and the error names the installed version; - a command declaration is validated at discovery time, not when the command is run: an empty name, a handler that is not callable, an unsupported argument type, a boolean positional argument, duplicate value names, a
cli_aliason an option, one that does not start with a dash, one that collides with another argument's own flag, a multiple flag and a multiple argument whose default is not a list are allPluginError; - an argument may not take a common flag of section 7, one the CLI accepts in any position: not as an option, not as the value name of a positional argument and not as a
cli_alias. The CLI moves such a flag in front of the subcommand and parses it itself, and a subcommand writes its values where the root parser keeps its own, so the plugin's value either never reached the command or replaced the value of the core. Anenv_fileof the plugin's own would also be a second one on its MCP tool. The set is exactly the flags the CLI moves, so a flag the core adds is closed to plugins at once, and such a declaration is aPluginErrorlike the ones above; - nor may an argument take a value name the CLI itself keeps in the parse of a command –
command,handler,plugin_command,plugin_commands,plugin_failures– or an option the subparser answers itself,-hand--help. A positionalhandlerreplaced the function the CLI calls with the string the user typed, and the call ended in aTypeError; an option--helpstopped the parser of the whole CLI from being built, with a bareargparse.ArgumentError. Both lists are read off the parser the CLI builds, so a name the core starts to keep is closed to plugins at once, and such a declaration is aPluginErroras well; - a failure stays with its plugin.
discover_commands()returns the commands that loaded and aPluginFailurefor each entry point that did not,{source, error}in itsto_dict(); one bad command leaves its whole entry point out.plugin_commands()is the strict form and raises the first failure. The CLI and the MCP server are built fromdiscover_commands(): the plugin that failed is left out, and the core and the other plugins keep working. One broken plugin used to stop the parser from being built, so no command worked, the core ones included, and the answer was a Python traceback; - a plugin may not take over a name the core already occupies – neither a CLI subcommand nor an MCP tool. The clashing command is left out and reported like a plugin that did not load, while the core keeps its own;
- the
ELEMCTL_NO_PLUGINS=1environment variable disables discovery, leaving the core capabilities alone. The command reference generator sets it, so the reference describes the core alone.
Surfaces using the mechanism: the CLI debug-adapter/plugins (section 7) and the subcommands of the plugins, the MCP tool debug_adapter (section 8) and the tools of the plugins, and the VS Code extension, which requests the path from elemctl debug-adapter when the adapterPath setting is empty.
The adapter itself is extracted from the platform distribution by tools/extract_adapter.py, which is clean code and is not shipped in the package distribution (prune). It copies the data/ide/theia/plugins/@1c-appengine-plugin/bin/debugger/ directory from the .car into <output>/<version>/ and updates index.json. The proprietary jars stay out of the public package; a separate plugin package ships them.