@backifydev/cli 0.3.2 → 0.3.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/README.md +7 -4
  2. package/dist/index.js +197 -54
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -9,12 +9,13 @@ npx @backifydev/cli sync
9
9
  npx @backifydev/cli login
10
10
  npx @backifydev/cli status
11
11
  npx @backifydev/cli report --check
12
+ npx @backifydev/cli pull
12
13
  npx @backifydev/cli ship
13
14
  ```
14
15
 
15
16
  `login` opens the Backify portal in your browser. Sign in and select an organization; the portal returns the organization and access token to the CLI through a random, short-lived callback bound to `127.0.0.1`. The CLI stores them in `~/.backify.json`. Set `portalUrl` in that file to use another portal, or `BACKIFY_PORTAL_URL` in the environment. `login <token> [organizationId]` remains available for scripted development.
16
-
17
- `init` installs `@backifydev/sdk`, writes `backify.json`, and maintains the Backify blocks in `AGENTS.md`, `CLAUDE.md`, and `.github/copilot-instructions.md`. It preserves other instructions in those files.
17
+
18
+ `init` installs `@backifydev/sdk`, writes `backify.json` and `BACKIFY.md`, and maintains only the selected coding tool's Backify block. Use `--agent codex` (`AGENTS.md`), `--agent claude` (`CLAUDE.md`), `--agent copilot` (`.github/copilot-instructions.md`), or `--agent gemini` (`GEMINI.md`). The CLI remembers the selection for `init`, `sync`, and `doctor`; new projects default to Codex. For any other tool, pass `--instruction-file <relative-path>` to use its native instruction or rule file, including nested paths; this is remembered too. It preserves other instructions and does not create or modify other tools' files.
18
19
 
19
20
  The app configuration stores `instructionVersion`, matching the version declared in each managed block and its immutable guide at `https://agent.backify.dev/v<number>`. The agent root always serves the latest guide as plain text. `doctor` detects version/content drift and malformed markers. Use `npx @backifydev/cli@latest sync` to explicitly upgrade all blocks and the configuration together; unrelated configuration and unmanaged instructions remain unchanged.
20
21
 
@@ -22,9 +23,11 @@ You can start from an empty folder without a Git repository. `init` creates a mi
22
23
 
23
24
  See the [agent setup guide](https://agent.backify.dev/).
24
25
 
25
- Copy the application AI prompt in Backify to receive an application-scoped, single-use pairing command. Codes expire after 15 minutes; redeemed sessions expire after seven days. Add `--clone` to the connection command only in an empty directory. Credentials are stored in the user's home configuration, not the project. The default API is `https://api.backify.dev/api`; local development can override it with `BACKIFY_API_URL` or the copied command's `--api` option. HTTPS is required outside loopback.
26
+ Copy the application AI prompt in Backify to receive the application ID, organization ID, and an application-scoped, single-use pairing code; the agent guide supplies the connection and initialization commands. Codes expire after 15 minutes; redeemed sessions expire after seven days. Add `--clone` even when the current folder contains AI coding-tool scaffolding. Empty directories of any name are allowed. Hidden tool-metadata directories and files, Markdown instruction files, and default `work/` and `outputs/` directories may contain tool settings or artifacts. The CLI clones into a temporary directory, validates collisions, then merges without deleting or overwriting existing tool files. Existing Git metadata, root application source, and symlinked tool directories are rejected; file conflicts stop before repository files are installed. Credentials are stored in the user's home configuration, not the project. The default API is `https://api.backify.dev/api`; local development can override it with `BACKIFY_API_URL` or the connection command's `--api` option. HTTPS is required outside loopback.
27
+
28
+ `init` adds a persistent delivery guide linked from the existing agent instructions. Before each application-building request, the agent sends only the user's own text through stdin to `backify prompt record --stdin --id <UUID>`, optionally adding repeated `--filename <basename>` arguments. Reuse the UUID on retries. The command never reads attachments; their bodies, extracted text, full paths, setup/pairing prompts, and credentials must not be submitted. History is displayed oldest-first under the application. This is an explicit agent workflow, not an automatic interceptor for every coding tool. Record the model, AI coding tool, and effective access for each request with --model, --ai-tool, and --access-level whenever known. Access is a short description of the tool’s actual filesystem, network, and approval permissions at the time of the request, not its Backify role. Use only exposed session information; never guess unknown values or inspect credentials to discover them. Omit unknown fields; older or incomplete records display “Not recorded”. These values are self-reported metadata, not verified authorization. Reuse the same metadata on retries. Model and tool names are limited to 200 characters each; access descriptions to 500. Never include secrets, tokens, full paths, or configuration file contents.
26
29
 
27
- `init` adds a persistent delivery guide linked from the existing agent instructions. Before each application-building request, the agent sends only the user's own text through stdin to `backify prompt record --stdin --id <UUID>`, optionally adding repeated `--filename <basename>` arguments. Reuse the UUID on retries. The command never reads attachments; their bodies, extracted text, full paths, setup/pairing prompts, and credentials must not be submitted. History is displayed oldest-first under the application. This is an explicit agent workflow, not an automatic interceptor for every coding tool.
30
+ `pull` checks the connected application, repository, branch, and repository root, then fetches GitHub using a short-lived scoped credential. It fast-forwards only a clean, non-diverged branch. Incoming updates blocked by local changes, untracked files, or diverged history return a blocked result and a nonzero exit code; nothing is stashed, reset, rebased, or force-pushed. Connecting an existing repository runs this update automatically, and the agent guide requires it before editing each new user request. It is not a background watcher.
28
31
 
29
32
  `ship` runs configured checks, verifies the repository/branch, and pushes already-committed source using a short-lived repository-scoped GitHub App credential. It never stages, commits, force-pushes, or fabricates preview testing. It waits for the exact healthy preview, then publishes when policy and permissions allow. Required testing/approval returns a pending state and preview URL, not a false successful production deployment. Use `--preview-only` to stop at preview, or `--timeout <seconds>` to change the default 1200-second wait. Applications must currently be at the repository root; Backify Cloud needs static index.html output, and customer Azure needs a Dockerfile serving port 8080.
30
33
 
package/dist/index.js CHANGED
@@ -769,35 +769,43 @@ async function packageFunctions(entry, output) {
769
769
  }
770
770
 
771
771
  // src/development.ts
772
- import { existsSync as existsSync4, readFileSync as readFileSync3, readdirSync as readdirSync2, writeFileSync as writeFileSync4, renameSync, rmSync } from "node:fs";
773
- import { join as join3, resolve as resolve3 } from "node:path";
772
+ import { existsSync as existsSync4, readFileSync as readFileSync3, readdirSync as readdirSync2, writeFileSync as writeFileSync4, renameSync, rmSync, mkdtempSync, lstatSync as lstatSync2, cpSync } from "node:fs";
773
+ import { dirname as dirname4, join as join3, resolve as resolve3 } from "node:path";
774
774
  import { spawnSync as spawnSync2 } from "node:child_process";
775
775
  import { randomUUID } from "node:crypto";
776
776
 
777
777
  // src/setup.ts
778
- import { existsSync as existsSync3, readFileSync as readFileSync2, writeFileSync as writeFileSync3, mkdirSync as mkdirSync2, readdirSync } from "node:fs";
779
- import { basename, dirname as dirname2, join as join2, resolve as resolve2 } from "node:path";
778
+ import { existsSync as existsSync3, readFileSync as readFileSync2, writeFileSync as writeFileSync3, mkdirSync as mkdirSync2, readdirSync, lstatSync } from "node:fs";
779
+ import { basename, dirname as dirname3, join as join2, resolve as resolve2, isAbsolute } from "node:path";
780
780
  import { spawnSync } from "node:child_process";
781
781
 
782
782
  // ../platform/dist/delivery.js
783
783
  var deliveryInstructions = `# Backify application delivery and prompt history
784
784
 
785
- Instruction version: 3
786
- Versioned guide: https://agent.backify.dev/v3/delivery.md
785
+ Instruction version: 4
786
+ Versioned guide: https://agent.backify.dev/v4/delivery.md
787
787
 
788
788
  These instructions are for AI coding tools building applications on Backify, not the Backify platform implementation. Complete the application and deliver a verified deployment, not just SDK setup. Respect the coding tool's own permissions and confirmation settings; preserve unrelated work and applicable repository instructions.
789
789
 
790
790
  ## Connect and initialize
791
791
 
792
- Use the connection command from the copied Backify prompt. The application ID, organization, API address, and single-use pairing code are already supplied there; never invent them. Pairing expires after 15 minutes. Never record the code or setup prompt in project files, Git, prompt history, or logs. The CLI stores an application-scoped session outside the repository. If access expires, obtain a new setup prompt or use CLI browser login.
792
+ The copied Backify prompt supplies App Id, Organization, and, when available, Pair, plus this guide reference and a privacy warning. Substitute those values for <app-id>, <organization-id>, and <pairing-code> below; never invent them. Use npx @backifydev/cli@latest so tool-specific setup and metadata-preserving cloning are available. The public API URL is https://api.backify.dev/api. For a nonproduction environment, replace the --api value with an explicitly trusted endpoint, or omit --api when using a trusted BACKIFY_API_URL override; never send a pairing code to an unverified endpoint.
793
793
 
794
- Inspect the workspace before connecting. In an empty project folder, add --clone to the supplied connect command to clone the connected repository without a separate GitHub login. In an existing repository, verify it matches the connected repository and branch; never overwrite unrelated files or force-push. Do not clone into a non-empty folder.
794
+ npx @backifydev/cli@latest connect --app <app-id> --organization <organization-id> --api https://api.backify.dev/api --pair <pairing-code>
795
795
 
796
- Run the initialization command supplied in the prompt. Read the installed AGENTS.md and BACKIFY-WORKFLOW.md before changing application code. Initialization installs this workflow alongside the persistent SDK instructions. Follow https://agent.backify.dev/v3 for instruction version 3; use the guide matching backify.json in an existing application. Do not silently upgrade its managed instruction version or overwrite instructions outside the managed block.
796
+ If Pair is absent, omit --pair and use CLI browser login. Pairing expires after 15 minutes. Never record the code or setup prompt in project files, Git, prompt history, or logs. The CLI stores an application-scoped session outside the repository. If access expires, obtain a new setup prompt or use CLI browser login.
797
+
798
+ Before connecting, inspect the current folder. If it is not a Git repository, is missing its .git directory or file, or has no Git remote, add --clone to the connect command. Default AI coding-tool directories and instruction files do not make a workspace an existing application. Run connect --clone in the current folder even when tool scaffolding exists, including configuration, rules, agents, skills, commands, prompts, and work/output directories. This is tool-independent: preserve the current tool's metadata and instructions regardless of its name; never delete or overwrite them to make the folder empty. The CLI recognizes empty directories of any name, hidden tool-metadata directories and files, Markdown instruction files, and default work/outputs directories, including their contents. It clones into a temporary folder, checks for file conflicts, and safely merges the repository into the current workspace. If actual application source or an existing .git is present, or repository files conflict with existing entries, stop and reconcile the workspace or use a new folder; never overwrite user work. In an existing repository with a remote, verify it matches the connected repository and branch; never overwrite unrelated files or force-push.
799
+
800
+ Initialize using App Id from the copied prompt and the current tool selection:
801
+
802
+ npx @backifydev/cli@latest init --app <app-id> --agent <current-tool>
803
+
804
+ For a tool without a built-in preset, replace --agent <current-tool> with --instruction-file <relative-path>. Read BACKIFY.md and the instruction file used by the current AI coding tool before changing application code. Create or update only instruction files used by that tool; never create files for other coding tools. Use --agent for a built-in tool preset or --instruction-file <relative-path> for the current tool's native instruction file, as described in the setup guide. Initialization installs this workflow alongside the persistent SDK instructions. Follow https://agent.backify.dev/v4 for instruction version 4; use the guide matching backify.json in an existing application. Do not silently upgrade its managed instruction version or overwrite instructions outside the managed block.
797
805
 
798
806
  Run the following to discover the connected hosting target, repository, branch, reports, releases, and deployment failures:
799
807
 
800
- npx @backifydev/cli@0.3.0 status
808
+ npx @backifydev/cli@latest status
801
809
 
802
810
  Connection and initialization may proceed without application requirements. Then ask what to build if none were supplied; never invent requirements or record this setup text as a user requirement.
803
811
 
@@ -805,9 +813,19 @@ Connection and initialization may proceed without application requirements. Then
805
813
 
806
814
  For EVERY user request to build or modify this application, including the initial requirements supplied alongside or after setup, record only the user's own requirement text before changing application code. Submit the text through standard input to:
807
815
 
808
- npx @backifydev/cli@0.3.0 prompt record --stdin --id <new-UUID> [--filename <basename> ...]
816
+ npx @backifydev/cli@latest prompt record --stdin --id <new-UUID> [--filename <basename> ...] [--model <model>] [--ai-tool <tool>] [--access-level <access>]
817
+
818
+ Record requests separately and in order. Generate a new UUID per request and reuse that UUID on retries. Add one --filename argument per attachment, using only its basename. Never send attachment bodies, extracted text, images, full paths, assistant responses, tool output, secrets, connection commands, or this setup/pairing block. Filename arguments are metadata only; never read attachment bodies to submit them. If recording fails, retry or report the blocker; never silently claim a request was recorded. Record the model, AI coding tool, and effective access for each request with --model, --ai-tool, and --access-level whenever known. Access is a short description of the tool\u2019s actual filesystem, network, and approval permissions at the time of the request, not its Backify role. Use only exposed session information; never guess unknown values or inspect credentials to discover them. Omit unknown fields; older or incomplete records display \u201CNot recorded\u201D. These values are self-reported metadata, not verified authorization. Reuse the same metadata on retries. Model and tool names are limited to 200 characters each; access descriptions to 500. Never include secrets, tokens, full paths, or configuration file contents.
809
819
 
810
- Record requests separately and in order. Generate a new UUID per request and reuse that UUID on retries. Add one --filename argument per attachment, using only its basename. Never send attachment bodies, extracted text, images, full paths, assistant responses, tool output, secrets, connection commands, or this setup/pairing block. Filename arguments are metadata only; never read attachment bodies to submit them. If recording fails, retry or report the blocker; never silently claim a request was recorded.
820
+ ## Keep local source current
821
+
822
+ Automatically run the following before editing code for every new build or modification request, after recording the user requirement. Connecting an existing Git repository also runs this safe update automatically. A new clone already contains the connected branch's current source.
823
+
824
+ npx @backifydev/cli@latest pull
825
+
826
+ The CLI verifies the connected application, repository, branch, and repository root, fetches GitHub changes with a short-lived scoped credential, and fast-forwards only when the local working tree is clean and history has not diverged. It never switches branches, stashes, resets, rebases, discards files, or force-pushes automatically. If local or untracked changes would block an incoming update, or local commits diverge from GitHub, stop and explain the blocker; obtain explicit direction to reconcile it. Never continue editing against known stale source or claim a blocked pull succeeded. Already-current or locally-ahead source is left unchanged.
827
+
828
+ This is automatic at connection and at the start of each agent task, not a background watcher. Do not pull during ongoing edits or overwrite unrelated local work.
811
829
 
812
830
  ## Build for the connected hosting target
813
831
 
@@ -821,21 +839,31 @@ Do not claim unsupported hosted server functions or SDK execution paths work. Re
821
839
 
822
840
  Run:
823
841
 
824
- npx @backifydev/cli@0.3.0 report --check
842
+ npx @backifydev/cli@latest report --check
825
843
 
826
844
  Fix relevant failures, then commit only application changes for this request using a meaningful message. Never commit credentials or unrelated user changes. Then run:
827
845
 
828
- npx @backifydev/cli@0.3.0 ship
846
+ npx @backifydev/cli@latest ship
829
847
 
830
848
  The CLI verifies the repository and branch, runs checks, pushes committed source using a short-lived repository-scoped credential, waits for the exact source's healthy preview, and publishes through existing Backify permissions and policy. It never stages or commits files, force-pushes, changes hosting, fabricates preview testing, or bypasses approval. Use --preview-only only when the user requests a preview.
831
849
 
832
850
  If testing or approval is required, return the preview URL and explain the remaining action. Otherwise return the verified production URL and actual check results only after healthy activation. Inspect failures with the status command, fix them, and retry without asking the user to perform routine deployment steps. Never invent a successful check or deployment.
833
851
 
852
+ ## User-facing handoff
853
+
854
+ After setup or a delivery attempt, give the user concise instructions: explain what is ready, how to open and use the application, and any sign-in, testing, approval, or other next steps. Summarize actual checks and blockers without exposing credentials or asking the user to perform routine deployment work the coding tool can complete.
855
+
856
+ Include separate Preview URL and Production URL lines, using the actual URLs returned by CLI status or deployment results. Include both URLs when available. If either is unavailable, say not deployed or unavailable and explain why; never invent a URL or claim an unverified deployment is live. When production still serves an earlier release, label it as the existing production version and explain that the new changes are preview-only or awaiting approval.
857
+
858
+ If application requirements are missing after setup, finish the response with this exact question as its final line, without any text after it:
859
+
860
+ What should the application do, and what should it look like?
861
+
834
862
  These instructions are an explicit agent workflow, not a universal chat interceptor. Request capture depends on the coding tool following this guide or an integration invoking the recording command.
835
863
  `;
836
864
 
837
865
  // ../platform/dist/index.js
838
- var INSTRUCTION_VERSION = 3;
866
+ var INSTRUCTION_VERSION = 4;
839
867
  var SDK_PACKAGE = "@backifydev/sdk";
840
868
  var DOCS_URL = "https://docs.backify.dev";
841
869
  var MAIL_LIMITS = Object.freeze({
@@ -908,7 +936,7 @@ Run relevant tests, typechecking, lint, and builds using the repository's script
908
936
 
909
937
  ## Connected application delivery
910
938
 
911
- For connected applications, follow \`BACKIFY-WORKFLOW.md\` and https://agent.backify.dev/v${INSTRUCTION_VERSION}/delivery.md before changing application code. Record every user build or modification request, including initial requirements, with attachment filenames only; never submit attachment contents or setup credentials. Verify checks and publish through the connected CLI workflow without bypassing testing, approval, or coding-tool permissions. Report only verified deployment URLs.
939
+ For connected applications, follow \`BACKIFY.md\` and https://agent.backify.dev/v${INSTRUCTION_VERSION}/delivery.md before changing application code. Record every user build or modification request, including initial requirements, with attachment filenames only; never submit attachment contents or setup credentials. Verify checks and publish through the connected CLI workflow without bypassing testing, approval, or coding-tool permissions. Report only verified deployment URLs.
912
940
 
913
941
  ## Instruction maintenance
914
942
 
@@ -954,69 +982,105 @@ function validateConfig(value) {
954
982
  errors.push("appId must use the app_<id> format");
955
983
  if (!Number.isInteger(x.instructionVersion) || Number(x.instructionVersion) < 1)
956
984
  errors.push("instructionVersion must be a positive integer");
985
+ if (x.agent !== void 0 && !["codex", "claude", "copilot", "gemini", "custom"].includes(x.agent))
986
+ errors.push("agent must be codex, claude, copilot, gemini, or custom");
987
+ if (x.instructionFile !== void 0 && (typeof x.instructionFile !== "string" || !x.instructionFile.trim()))
988
+ errors.push("instructionFile must be a nonempty relative path");
957
989
  if (x.instructions?.preferBackifyCapabilities !== true)
958
990
  errors.push("instructions.preferBackifyCapabilities must be true");
959
991
  if (x.instructions?.allowDirectCloudInfrastructure !== false)
960
992
  errors.push("instructions.allowDirectCloudInfrastructure must be false");
961
993
  return errors;
962
994
  }
963
- var BACKIFY_SCHEMA = { $schema: "https://json-schema.org/draft/2020-12/schema", $id: `${DOCS_URL}/schema/backify.json`, type: "object", additionalProperties: false, required: ["$schema", "platform", "sdk", "appId", "instructionVersion", "docs", "instructions"], properties: { $schema: { const: `${DOCS_URL}/schema/backify.json` }, platform: { const: "backify" }, sdk: { const: SDK_PACKAGE }, appId: { type: "string", pattern: "^app_[A-Za-z0-9_-]+$" }, instructionVersion: { type: "integer", minimum: 1 }, docs: { type: "string", format: "uri" }, instructions: { type: "object", additionalProperties: false, required: ["preferBackifyCapabilities", "allowDirectCloudInfrastructure"], properties: { preferBackifyCapabilities: { const: true }, allowDirectCloudInfrastructure: { const: false } } } } };
995
+ var BACKIFY_SCHEMA = { $schema: "https://json-schema.org/draft/2020-12/schema", $id: `${DOCS_URL}/schema/backify.json`, type: "object", additionalProperties: false, required: ["$schema", "platform", "sdk", "appId", "instructionVersion", "docs", "instructions"], properties: { $schema: { const: `${DOCS_URL}/schema/backify.json` }, platform: { const: "backify" }, sdk: { const: SDK_PACKAGE }, appId: { type: "string", pattern: "^app_[A-Za-z0-9_-]+$" }, instructionVersion: { type: "integer", minimum: 1 }, agent: { enum: ["codex", "claude", "copilot", "gemini", "custom"] }, instructionFile: { type: "string", minLength: 1 }, docs: { type: "string", format: "uri" }, instructions: { type: "object", additionalProperties: false, required: ["preferBackifyCapabilities", "allowDirectCloudInfrastructure"], properties: { preferBackifyCapabilities: { const: true }, allowDirectCloudInfrastructure: { const: false } } } } };
964
996
 
965
997
  // src/delivery.ts
966
998
  import { existsSync as existsSync2, readFileSync, writeFileSync as writeFileSync2 } from "node:fs";
967
- import { join } from "node:path";
999
+ import { dirname as dirname2, join, relative as relativePath } from "node:path";
968
1000
  import { createHash } from "node:crypto";
969
- var previousGeneratedWorkflowHashes = /* @__PURE__ */ new Set(["6eb280ae7bdb14c390011686c41400f59c023304c3015d8440b7e80aaafa1c11", "545e03a1d5b996ade179e0e60ed5c86accdadc1982c1c25efd094e34f72f47ee"]);
1001
+ var previousGeneratedWorkflowHashes = /* @__PURE__ */ new Set(["33e962274e8493f7cbac2afbac05a4e96afeec49ee08cf2bf82f8add9081b28b", "578dfb4c771c623fd04454e8a604efd701d5563d11e11191324862369bd24718", "6eb280ae7bdb14c390011686c41400f59c023304c3015d8440b7e80aaafa1c11", "545e03a1d5b996ade179e0e60ed5c86accdadc1982c1c25efd094e34f72f47ee"]);
970
1002
  function validateDeliveryInstructions(root) {
971
- const path = join(root, "BACKIFY-WORKFLOW.md");
972
- if (existsSync2(path)) {
973
- const existing = readFileSync(path, "utf8").replace(/\r\n/g, "\n");
974
- if (existing !== deliveryInstructions && !previousGeneratedWorkflowHashes.has(createHash("sha256").update(existing).digest("hex"))) throw new Error("BACKIFY-WORKFLOW.md contains different instructions. Review it before initializing; it will not be overwritten.");
1003
+ for (const filename of ["BACKIFY.md", "BACKIFY-WORKFLOW.md"]) {
1004
+ const path = join(root, filename);
1005
+ if (existsSync2(path)) {
1006
+ const existing = readFileSync(path, "utf8").replace(/\r\n/g, "\n");
1007
+ if (existing !== deliveryInstructions && !previousGeneratedWorkflowHashes.has(createHash("sha256").update(existing).digest("hex"))) throw new Error(filename + " contains different instructions. Review it before initializing; it will not be overwritten.");
1008
+ }
975
1009
  }
976
1010
  }
977
1011
  function installDeliveryInstructions(root, instructionFiles2) {
978
1012
  validateDeliveryInstructions(root);
979
- const path = join(root, "BACKIFY-WORKFLOW.md");
980
- writeFileSync2(path, deliveryInstructions);
1013
+ writeFileSync2(join(root, "BACKIFY.md"), deliveryInstructions);
981
1014
  for (const relative of instructionFiles2) {
982
1015
  const instructionPath = join(root, relative), content = readFileSync(instructionPath, "utf8");
983
- if (content.includes("## Backify delivery")) continue;
984
- const link = relative.startsWith(".github") ? "../BACKIFY-WORKFLOW.md" : "BACKIFY-WORKFLOW.md";
985
- writeFileSync2(instructionPath, content.trimEnd() + "\n\n## Backify delivery\n\nFor application delivery and recording each user request, follow [BACKIFY-WORKFLOW.md](" + link + ").\n");
1016
+ const link = relativePath(dirname2(instructionPath), join(root, "BACKIFY.md")).replaceAll("\\", "/");
1017
+ const legacyLink = relativePath(dirname2(instructionPath), join(root, "BACKIFY-WORKFLOW.md")).replaceAll("\\", "/");
1018
+ const legacy = "For application delivery and recording each user request, follow [BACKIFY-WORKFLOW.md](" + legacyLink + ").";
1019
+ const guidance = "For application delivery and recording each user request, follow [BACKIFY.md](" + link + ").";
1020
+ if (content.includes(legacy)) writeFileSync2(instructionPath, content.replace(legacy, guidance));
1021
+ else if (!content.includes("## Backify delivery")) writeFileSync2(instructionPath, content.trimEnd() + "\n\n## Backify delivery\n\n" + guidance + "\n");
986
1022
  }
987
1023
  }
988
1024
 
989
1025
  // src/setup.ts
990
- var instructionFiles = ["AGENTS.md", "CLAUDE.md", join2(".github", "copilot-instructions.md")];
1026
+ var instructionFiles = ["AGENTS.md"];
1027
+ var agentInstructionFiles = { codex: "AGENTS.md", claude: "CLAUDE.md", copilot: join2(".github", "copilot-instructions.md"), gemini: "GEMINI.md" };
1028
+ function instructionSelection(root, agent, instructionFile) {
1029
+ const configPath2 = join2(root, "backify.json"), configured = existsSync3(configPath2) ? JSON.parse(readFileSync2(configPath2, "utf8")) : {};
1030
+ const custom = instructionFile ?? (agent === void 0 ? configured.instructionFile : void 0);
1031
+ const selected = instructionFile !== void 0 ? "custom" : agent ?? configured.agent ?? "codex";
1032
+ if (typeof selected !== "string" || selected !== "custom" && !Object.hasOwn(agentInstructionFiles, selected)) throw new Error("Select the current tool with --agent or --instruction-file <relative-path>.");
1033
+ const filename = custom ?? agentInstructionFiles[selected];
1034
+ if (typeof filename !== "string" || !filename.trim()) throw new Error("Select the current tool with --agent codex, claude, copilot, or gemini, or --instruction-file <relative-path> for any other tool.");
1035
+ const normalized = filename.replaceAll("\\", "/"), parts = normalized.split("/");
1036
+ if (isAbsolute(filename) || /^[A-Za-z]:/.test(filename) || parts.some((part) => !part || part === "." || part === ".." || part.toLowerCase() === ".git") || ["backify.md", "backify-workflow.md", "backify.json", "package.json", "package-lock.json"].includes(normalized.toLowerCase())) throw new Error("Instruction file must be a relative tool-instruction path inside the project, not Git metadata or a Backify configuration file.");
1037
+ for (let index = 1; index <= parts.length; index++) {
1038
+ const entry = join2(root, ...parts.slice(0, index));
1039
+ try {
1040
+ const metadata = lstatSync(entry);
1041
+ if (metadata.isSymbolicLink() || index < parts.length && !metadata.isDirectory() || index === parts.length && !metadata.isFile()) throw new Error("Instruction file path cannot traverse symlinks or replace a directory.");
1042
+ } catch (reason) {
1043
+ if (reason.code !== "ENOENT") throw reason;
1044
+ }
1045
+ }
1046
+ return { agent: selected, instructionFile: normalized, files: [normalized] };
1047
+ }
1048
+ function selectedInstructionFiles(root = process.cwd(), agent, instructionFile) {
1049
+ return instructionSelection(root, agent, instructionFile).files;
1050
+ }
1051
+ function isToolWorkspaceEntry(entry, root = process.cwd()) {
1052
+ if (entry.name.toLowerCase() === ".git" || entry.name.toLowerCase() === ".env" || entry.name.toLowerCase().startsWith(".env.")) return false;
1053
+ return entry.isDirectory() && (entry.name.startsWith(".") || ["work", "outputs"].includes(entry.name) || readdirSync(join2(root, entry.name)).length === 0) || entry.isFile() && (entry.name.startsWith(".") || /\.(md|mdc)$/i.test(entry.name));
1054
+ }
991
1055
  function detectPackageManager(root = process.cwd()) {
992
1056
  for (const [file, name] of [["pnpm-lock.yaml", "pnpm"], ["yarn.lock", "yarn"], ["bun.lockb", "bun"], ["bun.lock", "bun"], ["package-lock.json", "npm"]]) if (existsSync3(join2(root, file))) return name;
993
1057
  return "npm";
994
1058
  }
995
- function updateInstructions(root = process.cwd()) {
996
- const updates = instructionFiles.map((relative) => {
1059
+ function updateInstructions(root = process.cwd(), agent, instructionFile) {
1060
+ const updates = selectedInstructionFiles(root, agent, instructionFile).map((relative) => {
997
1061
  const path = join2(root, relative);
998
1062
  return { path, content: updateManagedInstructions(existsSync3(path) ? readFileSync2(path, "utf8") : "") };
999
1063
  });
1000
1064
  for (const { path, content } of updates) {
1001
- mkdirSync2(dirname2(path), { recursive: true });
1065
+ mkdirSync2(dirname3(path), { recursive: true });
1002
1066
  writeFileSync3(path, content);
1003
1067
  }
1004
1068
  }
1005
- function synchronize(root = process.cwd()) {
1069
+ function synchronize(root = process.cwd(), agent, instructionFile) {
1006
1070
  const path = join2(root, "backify.json");
1007
1071
  if (!existsSync3(path)) throw new Error("backify.json is missing; run backify init --app app_xxxxx");
1008
1072
  const value = JSON.parse(readFileSync2(path, "utf8")), issues = validateConfig(value);
1009
1073
  if (issues.length) throw new Error(issues.join("; "));
1010
1074
  if (value.instructionVersion > INSTRUCTION_VERSION) throw new Error("This app uses newer instructions. Install a matching or newer CLI before syncing.");
1075
+ const selected = instructionSelection(root, agent, instructionFile);
1011
1076
  validateDeliveryInstructions(root);
1012
- updateInstructions(root);
1013
- installDeliveryInstructions(root, instructionFiles);
1077
+ updateInstructions(root, selected.agent, selected.instructionFile);
1078
+ installDeliveryInstructions(root, selected.files);
1079
+ value.agent = selected.agent;
1080
+ value.instructionFile = selected.instructionFile;
1014
1081
  value.instructionVersion = INSTRUCTION_VERSION;
1015
1082
  writeFileSync3(path, JSON.stringify(value, null, 2) + "\n");
1016
1083
  }
1017
- function writeConfig(appId, root = process.cwd()) {
1018
- writeFileSync3(join2(root, "backify.json"), JSON.stringify(createConfig(appId), null, 2) + "\n");
1019
- }
1020
1084
  function sdkInstalled(root = process.cwd()) {
1021
1085
  try {
1022
1086
  const pkg = JSON.parse(readFileSync2(join2(root, "package.json"), "utf8"));
@@ -1033,20 +1097,24 @@ function installSdk(root = process.cwd()) {
1033
1097
  function ensurePackageManifest(root) {
1034
1098
  const path = join2(root, "package.json");
1035
1099
  if (existsSync3(path)) return;
1036
- const setupFiles = /* @__PURE__ */ new Set([".git", ".gitignore", ".github", "AGENTS.md", "CLAUDE.md", "README.md", "LICENSE", "backify.json"]);
1037
- if (readdirSync(root).some((file) => !setupFiles.has(file))) throw new Error("package.json is missing. Run backify init from the existing application's JavaScript/TypeScript package directory, or start in an empty folder. See https://agent.backify.dev/");
1100
+ const setupFiles = /* @__PURE__ */ new Set([".git", ".gitignore", ".github", "AGENTS.md", "CLAUDE.md", "GEMINI.md", "BACKIFY.md", "BACKIFY-WORKFLOW.md", "README.md", "LICENSE", "backify.json"]);
1101
+ if (readdirSync(root, { withFileTypes: true }).some((entry) => !setupFiles.has(entry.name) && !isToolWorkspaceEntry(entry, root))) throw new Error("package.json is missing. Run backify init from the existing application's JavaScript/TypeScript package directory, or start in an empty folder. See https://agent.backify.dev/");
1038
1102
  const name = basename(resolve2(root)).toLowerCase().replace(/[^a-z0-9-]+/g, "-").replace(/^-+|-+$/g, "") || "backify-app";
1039
1103
  writeFileSync3(path, JSON.stringify({ name, version: "0.0.0", private: true, type: "module" }, null, 2) + "\n", { flag: "wx" });
1040
1104
  }
1041
- function initialize(appId, root = process.cwd(), install = true) {
1105
+ function initialize(appId, root = process.cwd(), install = true, agent, instructionFile) {
1042
1106
  if (!/^app_[A-Za-z0-9_-]+$/.test(appId)) throw new Error("Provide an application ID using --app app_xxxxx");
1107
+ const selected = instructionSelection(root, agent, instructionFile), files = selected.files;
1108
+ validateDeliveryInstructions(root);
1109
+ for (const relative of files) if (existsSync3(join2(root, relative))) updateManagedInstructions(readFileSync2(join2(root, relative), "utf8"));
1043
1110
  ensurePackageManifest(root);
1044
1111
  if (install) installSdk(root);
1045
- writeConfig(appId, root);
1046
- updateInstructions(root);
1047
- installDeliveryInstructions(root, instructionFiles);
1112
+ const configPath2 = join2(root, "backify.json"), existing = existsSync3(configPath2) ? JSON.parse(readFileSync2(configPath2, "utf8")) : {};
1113
+ writeFileSync3(configPath2, JSON.stringify({ ...existing, ...createConfig(appId), agent: selected.agent, instructionFile: selected.instructionFile }, null, 2) + "\n");
1114
+ updateInstructions(root, selected.agent, selected.instructionFile);
1115
+ installDeliveryInstructions(root, files);
1048
1116
  }
1049
- function doctor(root = process.cwd()) {
1117
+ function doctor(root = process.cwd(), agent, instructionFile) {
1050
1118
  const issues = [], warnings = [];
1051
1119
  if (!sdkInstalled(root)) issues.push(`${SDK_PACKAGE} is not installed`);
1052
1120
  let config;
@@ -1057,7 +1125,13 @@ function doctor(root = process.cwd()) {
1057
1125
  issues.push("backify.json is missing or invalid JSON");
1058
1126
  }
1059
1127
  if (config && config.instructionVersion !== INSTRUCTION_VERSION) issues.push(`Repository instruction version ${config.instructionVersion}; CLI version ${INSTRUCTION_VERSION}. Use the matching CLI or run backify sync to upgrade.`);
1060
- for (const relative of instructionFiles) {
1128
+ let files = instructionFiles;
1129
+ try {
1130
+ files = selectedInstructionFiles(root, agent, instructionFile);
1131
+ } catch (reason) {
1132
+ issues.push(reason instanceof Error ? reason.message : String(reason));
1133
+ }
1134
+ for (const relative of files) {
1061
1135
  const path = join2(root, relative);
1062
1136
  if (!existsSync3(path)) {
1063
1137
  issues.push(`${relative} is missing`);
@@ -1075,6 +1149,19 @@ function doctor(root = process.cwd()) {
1075
1149
  }
1076
1150
 
1077
1151
  // src/development.ts
1152
+ function promptMetadata(args2) {
1153
+ const fields = [["--model", "model", 200], ["--ai-tool", "aiTool", 200], ["--access-level", "accessLevel", 500]];
1154
+ const metadata = {};
1155
+ for (const [flag, field, maxLength] of fields) {
1156
+ const index = args2.indexOf(flag);
1157
+ if (index < 0) continue;
1158
+ const value = args2[index + 1]?.trim();
1159
+ if (!value || value.startsWith("--") || value.length > maxLength || /[\u0000-\u001f\u007f-\u009f]|bfcli_[0-9a-f]{64}|--pair\s+[0-9a-f]{64}/i.test(value))
1160
+ throw new Error(`Provide ${flag} with a nonempty value of at most ${maxLength} characters without control characters or credentials.`);
1161
+ metadata[field] = value;
1162
+ }
1163
+ return metadata;
1164
+ }
1078
1165
  function applicationGuid(appId) {
1079
1166
  if (/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(appId)) return appId.toLowerCase();
1080
1167
  const match = /^app_([0-9a-f]{32})$/i.exec(appId);
@@ -1138,13 +1225,57 @@ function gitEnvironment(repository, token) {
1138
1225
  if (!/^[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+$/.test(repository)) throw new Error("Invalid repository.");
1139
1226
  return { ...process.env, GIT_TERMINAL_PROMPT: "0", GIT_CONFIG_COUNT: "3", GIT_CONFIG_KEY_0: "http.https://github.com/" + repository + ".git.extraheader", GIT_CONFIG_VALUE_0: "Authorization: Basic " + Buffer.from("x-access-token:" + token).toString("base64"), GIT_CONFIG_KEY_1: "credential.helper", GIT_CONFIG_VALUE_1: "", GIT_CONFIG_KEY_2: "http.followRedirects", GIT_CONFIG_VALUE_2: "false" };
1140
1227
  }
1228
+ function validateCloneMerge(source, target) {
1229
+ for (const entry of readdirSync2(source, { withFileTypes: true })) {
1230
+ const destination = join3(target, entry.name);
1231
+ let existing;
1232
+ try {
1233
+ existing = lstatSync2(destination);
1234
+ } catch (reason) {
1235
+ if (reason.code === "ENOENT") continue;
1236
+ throw reason;
1237
+ }
1238
+ if (entry.isDirectory() && existing.isDirectory() && !existing.isSymbolicLink()) validateCloneMerge(join3(source, entry.name), destination);
1239
+ else throw new Error("Clone conflicts with existing workspace entry: " + destination + ". Existing files are never overwritten; reconcile the conflict or use a new folder.");
1240
+ }
1241
+ }
1141
1242
  async function cloneProject(call2, appId, root = process.cwd()) {
1142
1243
  const status = await call2("/apps/" + applicationGuid(appId) + "/developer");
1143
1244
  if (!status.repository || !status.branch || !status.workflowReady) throw new Error("Connect the repository in Backify and wait for workflow setup.");
1144
- if (readdirSync2(root).length) throw new Error("--clone requires an empty directory. Existing files are never overwritten.");
1245
+ if (readdirSync2(root, { withFileTypes: true }).some((entry) => !isToolWorkspaceEntry(entry, root))) throw new Error("--clone requires an empty directory or a workspace containing only AI tool metadata and default work/outputs directories. Existing application files and Git repositories are never overwritten.");
1145
1246
  const credential = await call2("/apps/" + applicationGuid(appId) + "/developer/git-credential", { method: "POST" });
1146
1247
  if (credential.repository.toLowerCase() !== status.repository.toLowerCase()) throw new Error("Repository changed. Run connect again.");
1147
- git(["clone", "--single-branch", "--branch", status.branch, "--", "https://github.com/" + status.repository + ".git", "."], root, gitEnvironment(status.repository, credential.token));
1248
+ const staging = mkdtempSync(join3(dirname4(resolve3(root)), ".backify-clone-"));
1249
+ try {
1250
+ git(["clone", "--single-branch", "--branch", status.branch, "--", "https://github.com/" + status.repository + ".git", "."], staging, gitEnvironment(status.repository, credential.token));
1251
+ validateCloneMerge(staging, root);
1252
+ for (const entry of readdirSync2(staging)) cpSync(join3(staging, entry), join3(root, entry), { recursive: true, force: false, errorOnExist: true, dereference: false });
1253
+ } finally {
1254
+ if (dirname4(staging) !== dirname4(resolve3(root)) || !staging.startsWith(join3(dirname4(resolve3(root)), ".backify-clone-"))) throw new Error("Refusing to clean up an unexpected clone staging path.");
1255
+ rmSync(staging, { recursive: true, force: true });
1256
+ }
1257
+ }
1258
+ async function pullProject(call2, appId, root = process.cwd()) {
1259
+ if (existsSync4(join3(root, "backify.json")) && applicationGuid(projectApplication(root)) !== applicationGuid(appId)) throw new Error("Local application ID does not match the connected application.");
1260
+ const prefix = "/apps/" + applicationGuid(appId);
1261
+ const status = await call2(prefix + "/developer");
1262
+ if (!status.repository || !status.branch) throw new Error("Connect the repository and branch in Backify before pulling.");
1263
+ if (resolve3(git(["rev-parse", "--show-toplevel"], root)) !== resolve3(root)) throw new Error("Run pull from the connected repository root.");
1264
+ if (repositoryName(git(["remote", "get-url", "origin"], root)) !== status.repository.toLowerCase()) throw new Error("Local origin does not match the repository connected in Backify.");
1265
+ if (git(["branch", "--show-current"], root) !== status.branch) throw new Error("Local branch does not match the branch connected in Backify. The CLI never switches branches automatically.");
1266
+ git(["check-ref-format", "--branch", status.branch], root);
1267
+ const credential = await call2(prefix + "/developer/git-credential", { method: "POST" });
1268
+ if (credential.repository.toLowerCase() !== status.repository.toLowerCase()) throw new Error("Repository changed; reconnect before pulling.");
1269
+ git(["fetch", "--no-tags", "--", "https://github.com/" + status.repository + ".git", "refs/heads/" + status.branch + ":refs/remotes/origin/" + status.branch], root, gitEnvironment(status.repository, credential.token));
1270
+ const remoteCommit = git(["rev-parse", "FETCH_HEAD"], root);
1271
+ const counts = git(["rev-list", "--left-right", "--count", "HEAD..." + remoteCommit], root).split(/\s+/).map(Number);
1272
+ if (counts.length !== 2 || counts.some((count) => !Number.isSafeInteger(count) || count < 0)) throw new Error("Could not compare local and remote history.");
1273
+ const [ahead, behind] = counts;
1274
+ if (!behind) return { status: "up-to-date", ahead, behind, commit: git(["rev-parse", "HEAD"], root) };
1275
+ if (ahead) return { status: "blocked", reason: "Local and GitHub history have diverged. Review and reconcile them explicitly; no rebase, reset, or force-push was performed.", ahead, behind };
1276
+ if (git(["status", "--porcelain"], root)) return { status: "blocked", reason: "Local changes or untracked files are present. Commit, ignore, or otherwise resolve them explicitly before updating; nothing was stashed or overwritten.", ahead, behind };
1277
+ git(["merge", "--ff-only", "--", remoteCommit], root);
1278
+ return { status: "updated", ahead: 0, behind: 0, commit: git(["rev-parse", "HEAD"], root) };
1148
1279
  }
1149
1280
  async function readPrompt(input) {
1150
1281
  const chunks = [];
@@ -1369,15 +1500,15 @@ async function main() {
1369
1500
  break;
1370
1501
  }
1371
1502
  case "init":
1372
- initialize(option("--app") ?? args.find((value) => !value.startsWith("-")) ?? "");
1503
+ initialize(option("--app") ?? args.find((value) => !value.startsWith("-")) ?? "", process.cwd(), true, option("--agent"), option("--instruction-file"));
1373
1504
  console.log("Backify initialized. SDK, delivery instructions, and prompt history guidance are ready.");
1374
1505
  break;
1375
1506
  case "sync":
1376
- synchronize();
1507
+ synchronize(process.cwd(), option("--agent"), option("--instruction-file"));
1377
1508
  console.log("Backify instructions synchronized to version " + INSTRUCTION_VERSION);
1378
1509
  break;
1379
1510
  case "doctor": {
1380
- const result = doctor();
1511
+ const result = doctor(process.cwd(), option("--agent"), option("--instruction-file"));
1381
1512
  console.log(JSON.stringify(result, null, 2));
1382
1513
  if (!result.valid) process.exitCode = 1;
1383
1514
  break;
@@ -1415,6 +1546,11 @@ async function main() {
1415
1546
  userConfig.sessions[key] = session;
1416
1547
  saveUserConfig(configPath, userConfig);
1417
1548
  if (args.includes("--clone")) await cloneProject(client, appId);
1549
+ else if (existsSync5(join4(process.cwd(), ".git"))) {
1550
+ const update = await pullProject(client, appId);
1551
+ console.log(JSON.stringify(update, null, 2));
1552
+ if (update.status === "blocked") throw new Error(update.reason);
1553
+ }
1418
1554
  console.log(JSON.stringify(await client("/apps/" + applicationGuid(appId) + "/developer"), null, 2));
1419
1555
  break;
1420
1556
  }
@@ -1424,6 +1560,12 @@ async function main() {
1424
1560
  saveUserConfig(configPath, userConfig);
1425
1561
  console.log("Application CLI sessions revoked.");
1426
1562
  break;
1563
+ case "pull": {
1564
+ const result = await pullProject(call, option("--app") ?? projectApplication());
1565
+ console.log(JSON.stringify(result, null, 2));
1566
+ if (result.status === "blocked") process.exitCode = 1;
1567
+ break;
1568
+ }
1427
1569
  case "status": {
1428
1570
  const prefix = "/apps/" + applicationGuid(option("--app") ?? projectApplication());
1429
1571
  console.log(JSON.stringify(await call(prefix + "/developer"), null, 2));
@@ -1446,12 +1588,13 @@ async function main() {
1446
1588
  break;
1447
1589
  }
1448
1590
  case "prompt": {
1449
- if (args[0] !== "record" || !args.includes("--stdin")) throw new Error("Use backify prompt record --stdin --id <UUID> [--filename <name> ...]. Only user text and filenames are sent.");
1591
+ if (args[0] !== "record" || !args.includes("--stdin")) throw new Error("Use backify prompt record --stdin --id <UUID> [--filename <name> ...] [--model <model>] [--ai-tool <tool>] [--access-level <access>]. Only user text, filenames, and AI context metadata are sent.");
1450
1592
  const id = option("--id");
1451
1593
  if (!id || !/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(id)) throw new Error("Provide a UUID with --id; reuse it when retrying this prompt.");
1452
1594
  const filenames = attachmentNames(args.flatMap((value, index) => value === "--filename" ? [args[index + 1] ?? ""] : []));
1595
+ const metadata = promptMetadata(args);
1453
1596
  const text = await readPrompt(process.stdin);
1454
- console.log(JSON.stringify(await call("/apps/" + applicationGuid(projectApplication()) + "/prompts", { method: "POST", body: JSON.stringify({ clientPromptId: id, text, filenames }) })));
1597
+ console.log(JSON.stringify(await call("/apps/" + applicationGuid(projectApplication()) + "/prompts", { method: "POST", body: JSON.stringify({ clientPromptId: id, text, filenames, ...metadata }) })));
1455
1598
  break;
1456
1599
  }
1457
1600
  case "ship": {
@@ -1492,7 +1635,7 @@ async function main() {
1492
1635
  console.log(await call("/apps/" + applicationGuid(args[0]) + "/deployments/preview", { method: "POST", body: JSON.stringify({ sourceRevision: args[1] ?? "latest" }) }));
1493
1636
  break;
1494
1637
  default:
1495
- console.log("backify connect | init | doctor | sync | login | logout | disconnect | status | report [--check] | prompt record --stdin --id <UUID> | ship [--preview-only] | request | events environments|inspect|test | apps | deploy");
1638
+ console.log("backify connect | init | doctor | sync | login | logout | disconnect | status | pull | report [--check] | prompt record --stdin --id <UUID> | ship [--preview-only] | request | events environments|inspect|test | apps | deploy");
1496
1639
  }
1497
1640
  }
1498
1641
  main().catch((error) => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@backifydev/cli",
3
- "version": "0.3.2",
3
+ "version": "0.3.4",
4
4
  "description": "Command-line setup and diagnostics for Backify applications",
5
5
  "type": "module",
6
6
  "bin": {