@backifydev/cli 0.3.2 → 0.3.3

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 +6 -3
  2. package/dist/index.js +170 -51
  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,10 +23,12 @@ 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.
26
27
 
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.
28
29
 
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.
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
 
31
34
  `disconnect` revokes that user's application CLI sessions. `logout` removes local credentials only. Prompt histories and local reports remain subject to application permissions. Deploy the matching API and publish CLI 0.3.0 before enabling this flow in production.
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,10 +813,20 @@ 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> ...]
809
817
 
810
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.
811
819
 
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.
829
+
812
830
  ## Build for the connected hosting target
813
831
 
814
832
  Inspect the framework, lockfile, scripts, configuration, and applicable repository instructions before editing. Preserve the existing framework. Use @backifydev/sdk for platform capabilities, verify API signatures against its installed types and https://docs.backify.dev/capabilities/, and do not directly provision external infrastructure.
@@ -821,11 +839,11 @@ 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
 
@@ -835,7 +853,7 @@ These instructions are an explicit agent workflow, not a universal chat intercep
835
853
  `;
836
854
 
837
855
  // ../platform/dist/index.js
838
- var INSTRUCTION_VERSION = 3;
856
+ var INSTRUCTION_VERSION = 4;
839
857
  var SDK_PACKAGE = "@backifydev/sdk";
840
858
  var DOCS_URL = "https://docs.backify.dev";
841
859
  var MAIL_LIMITS = Object.freeze({
@@ -908,7 +926,7 @@ Run relevant tests, typechecking, lint, and builds using the repository's script
908
926
 
909
927
  ## Connected application delivery
910
928
 
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.
929
+ 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
930
 
913
931
  ## Instruction maintenance
914
932
 
@@ -954,69 +972,105 @@ function validateConfig(value) {
954
972
  errors.push("appId must use the app_<id> format");
955
973
  if (!Number.isInteger(x.instructionVersion) || Number(x.instructionVersion) < 1)
956
974
  errors.push("instructionVersion must be a positive integer");
975
+ if (x.agent !== void 0 && !["codex", "claude", "copilot", "gemini", "custom"].includes(x.agent))
976
+ errors.push("agent must be codex, claude, copilot, gemini, or custom");
977
+ if (x.instructionFile !== void 0 && (typeof x.instructionFile !== "string" || !x.instructionFile.trim()))
978
+ errors.push("instructionFile must be a nonempty relative path");
957
979
  if (x.instructions?.preferBackifyCapabilities !== true)
958
980
  errors.push("instructions.preferBackifyCapabilities must be true");
959
981
  if (x.instructions?.allowDirectCloudInfrastructure !== false)
960
982
  errors.push("instructions.allowDirectCloudInfrastructure must be false");
961
983
  return errors;
962
984
  }
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 } } } } };
985
+ 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
986
 
965
987
  // src/delivery.ts
966
988
  import { existsSync as existsSync2, readFileSync, writeFileSync as writeFileSync2 } from "node:fs";
967
- import { join } from "node:path";
989
+ import { dirname as dirname2, join, relative as relativePath } from "node:path";
968
990
  import { createHash } from "node:crypto";
969
- var previousGeneratedWorkflowHashes = /* @__PURE__ */ new Set(["6eb280ae7bdb14c390011686c41400f59c023304c3015d8440b7e80aaafa1c11", "545e03a1d5b996ade179e0e60ed5c86accdadc1982c1c25efd094e34f72f47ee"]);
991
+ var previousGeneratedWorkflowHashes = /* @__PURE__ */ new Set(["578dfb4c771c623fd04454e8a604efd701d5563d11e11191324862369bd24718", "6eb280ae7bdb14c390011686c41400f59c023304c3015d8440b7e80aaafa1c11", "545e03a1d5b996ade179e0e60ed5c86accdadc1982c1c25efd094e34f72f47ee"]);
970
992
  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.");
993
+ for (const filename of ["BACKIFY.md", "BACKIFY-WORKFLOW.md"]) {
994
+ const path = join(root, filename);
995
+ if (existsSync2(path)) {
996
+ const existing = readFileSync(path, "utf8").replace(/\r\n/g, "\n");
997
+ 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.");
998
+ }
975
999
  }
976
1000
  }
977
1001
  function installDeliveryInstructions(root, instructionFiles2) {
978
1002
  validateDeliveryInstructions(root);
979
- const path = join(root, "BACKIFY-WORKFLOW.md");
980
- writeFileSync2(path, deliveryInstructions);
1003
+ writeFileSync2(join(root, "BACKIFY.md"), deliveryInstructions);
981
1004
  for (const relative of instructionFiles2) {
982
1005
  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");
1006
+ const link = relativePath(dirname2(instructionPath), join(root, "BACKIFY.md")).replaceAll("\\", "/");
1007
+ const legacyLink = relativePath(dirname2(instructionPath), join(root, "BACKIFY-WORKFLOW.md")).replaceAll("\\", "/");
1008
+ const legacy = "For application delivery and recording each user request, follow [BACKIFY-WORKFLOW.md](" + legacyLink + ").";
1009
+ const guidance = "For application delivery and recording each user request, follow [BACKIFY.md](" + link + ").";
1010
+ if (content.includes(legacy)) writeFileSync2(instructionPath, content.replace(legacy, guidance));
1011
+ else if (!content.includes("## Backify delivery")) writeFileSync2(instructionPath, content.trimEnd() + "\n\n## Backify delivery\n\n" + guidance + "\n");
986
1012
  }
987
1013
  }
988
1014
 
989
1015
  // src/setup.ts
990
- var instructionFiles = ["AGENTS.md", "CLAUDE.md", join2(".github", "copilot-instructions.md")];
1016
+ var instructionFiles = ["AGENTS.md"];
1017
+ var agentInstructionFiles = { codex: "AGENTS.md", claude: "CLAUDE.md", copilot: join2(".github", "copilot-instructions.md"), gemini: "GEMINI.md" };
1018
+ function instructionSelection(root, agent, instructionFile) {
1019
+ const configPath2 = join2(root, "backify.json"), configured = existsSync3(configPath2) ? JSON.parse(readFileSync2(configPath2, "utf8")) : {};
1020
+ const custom = instructionFile ?? (agent === void 0 ? configured.instructionFile : void 0);
1021
+ const selected = instructionFile !== void 0 ? "custom" : agent ?? configured.agent ?? "codex";
1022
+ if (typeof selected !== "string" || selected !== "custom" && !Object.hasOwn(agentInstructionFiles, selected)) throw new Error("Select the current tool with --agent or --instruction-file <relative-path>.");
1023
+ const filename = custom ?? agentInstructionFiles[selected];
1024
+ 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.");
1025
+ const normalized = filename.replaceAll("\\", "/"), parts = normalized.split("/");
1026
+ 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.");
1027
+ for (let index = 1; index <= parts.length; index++) {
1028
+ const entry = join2(root, ...parts.slice(0, index));
1029
+ try {
1030
+ const metadata = lstatSync(entry);
1031
+ 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.");
1032
+ } catch (reason) {
1033
+ if (reason.code !== "ENOENT") throw reason;
1034
+ }
1035
+ }
1036
+ return { agent: selected, instructionFile: normalized, files: [normalized] };
1037
+ }
1038
+ function selectedInstructionFiles(root = process.cwd(), agent, instructionFile) {
1039
+ return instructionSelection(root, agent, instructionFile).files;
1040
+ }
1041
+ function isToolWorkspaceEntry(entry, root = process.cwd()) {
1042
+ if (entry.name.toLowerCase() === ".git" || entry.name.toLowerCase() === ".env" || entry.name.toLowerCase().startsWith(".env.")) return false;
1043
+ 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));
1044
+ }
991
1045
  function detectPackageManager(root = process.cwd()) {
992
1046
  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
1047
  return "npm";
994
1048
  }
995
- function updateInstructions(root = process.cwd()) {
996
- const updates = instructionFiles.map((relative) => {
1049
+ function updateInstructions(root = process.cwd(), agent, instructionFile) {
1050
+ const updates = selectedInstructionFiles(root, agent, instructionFile).map((relative) => {
997
1051
  const path = join2(root, relative);
998
1052
  return { path, content: updateManagedInstructions(existsSync3(path) ? readFileSync2(path, "utf8") : "") };
999
1053
  });
1000
1054
  for (const { path, content } of updates) {
1001
- mkdirSync2(dirname2(path), { recursive: true });
1055
+ mkdirSync2(dirname3(path), { recursive: true });
1002
1056
  writeFileSync3(path, content);
1003
1057
  }
1004
1058
  }
1005
- function synchronize(root = process.cwd()) {
1059
+ function synchronize(root = process.cwd(), agent, instructionFile) {
1006
1060
  const path = join2(root, "backify.json");
1007
1061
  if (!existsSync3(path)) throw new Error("backify.json is missing; run backify init --app app_xxxxx");
1008
1062
  const value = JSON.parse(readFileSync2(path, "utf8")), issues = validateConfig(value);
1009
1063
  if (issues.length) throw new Error(issues.join("; "));
1010
1064
  if (value.instructionVersion > INSTRUCTION_VERSION) throw new Error("This app uses newer instructions. Install a matching or newer CLI before syncing.");
1065
+ const selected = instructionSelection(root, agent, instructionFile);
1011
1066
  validateDeliveryInstructions(root);
1012
- updateInstructions(root);
1013
- installDeliveryInstructions(root, instructionFiles);
1067
+ updateInstructions(root, selected.agent, selected.instructionFile);
1068
+ installDeliveryInstructions(root, selected.files);
1069
+ value.agent = selected.agent;
1070
+ value.instructionFile = selected.instructionFile;
1014
1071
  value.instructionVersion = INSTRUCTION_VERSION;
1015
1072
  writeFileSync3(path, JSON.stringify(value, null, 2) + "\n");
1016
1073
  }
1017
- function writeConfig(appId, root = process.cwd()) {
1018
- writeFileSync3(join2(root, "backify.json"), JSON.stringify(createConfig(appId), null, 2) + "\n");
1019
- }
1020
1074
  function sdkInstalled(root = process.cwd()) {
1021
1075
  try {
1022
1076
  const pkg = JSON.parse(readFileSync2(join2(root, "package.json"), "utf8"));
@@ -1033,20 +1087,24 @@ function installSdk(root = process.cwd()) {
1033
1087
  function ensurePackageManifest(root) {
1034
1088
  const path = join2(root, "package.json");
1035
1089
  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/");
1090
+ const setupFiles = /* @__PURE__ */ new Set([".git", ".gitignore", ".github", "AGENTS.md", "CLAUDE.md", "GEMINI.md", "BACKIFY.md", "BACKIFY-WORKFLOW.md", "README.md", "LICENSE", "backify.json"]);
1091
+ 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
1092
  const name = basename(resolve2(root)).toLowerCase().replace(/[^a-z0-9-]+/g, "-").replace(/^-+|-+$/g, "") || "backify-app";
1039
1093
  writeFileSync3(path, JSON.stringify({ name, version: "0.0.0", private: true, type: "module" }, null, 2) + "\n", { flag: "wx" });
1040
1094
  }
1041
- function initialize(appId, root = process.cwd(), install = true) {
1095
+ function initialize(appId, root = process.cwd(), install = true, agent, instructionFile) {
1042
1096
  if (!/^app_[A-Za-z0-9_-]+$/.test(appId)) throw new Error("Provide an application ID using --app app_xxxxx");
1097
+ const selected = instructionSelection(root, agent, instructionFile), files = selected.files;
1098
+ validateDeliveryInstructions(root);
1099
+ for (const relative of files) if (existsSync3(join2(root, relative))) updateManagedInstructions(readFileSync2(join2(root, relative), "utf8"));
1043
1100
  ensurePackageManifest(root);
1044
1101
  if (install) installSdk(root);
1045
- writeConfig(appId, root);
1046
- updateInstructions(root);
1047
- installDeliveryInstructions(root, instructionFiles);
1102
+ const configPath2 = join2(root, "backify.json"), existing = existsSync3(configPath2) ? JSON.parse(readFileSync2(configPath2, "utf8")) : {};
1103
+ writeFileSync3(configPath2, JSON.stringify({ ...existing, ...createConfig(appId), agent: selected.agent, instructionFile: selected.instructionFile }, null, 2) + "\n");
1104
+ updateInstructions(root, selected.agent, selected.instructionFile);
1105
+ installDeliveryInstructions(root, files);
1048
1106
  }
1049
- function doctor(root = process.cwd()) {
1107
+ function doctor(root = process.cwd(), agent, instructionFile) {
1050
1108
  const issues = [], warnings = [];
1051
1109
  if (!sdkInstalled(root)) issues.push(`${SDK_PACKAGE} is not installed`);
1052
1110
  let config;
@@ -1057,7 +1115,13 @@ function doctor(root = process.cwd()) {
1057
1115
  issues.push("backify.json is missing or invalid JSON");
1058
1116
  }
1059
1117
  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) {
1118
+ let files = instructionFiles;
1119
+ try {
1120
+ files = selectedInstructionFiles(root, agent, instructionFile);
1121
+ } catch (reason) {
1122
+ issues.push(reason instanceof Error ? reason.message : String(reason));
1123
+ }
1124
+ for (const relative of files) {
1061
1125
  const path = join2(root, relative);
1062
1126
  if (!existsSync3(path)) {
1063
1127
  issues.push(`${relative} is missing`);
@@ -1138,13 +1202,57 @@ function gitEnvironment(repository, token) {
1138
1202
  if (!/^[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+$/.test(repository)) throw new Error("Invalid repository.");
1139
1203
  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
1204
  }
1205
+ function validateCloneMerge(source, target) {
1206
+ for (const entry of readdirSync2(source, { withFileTypes: true })) {
1207
+ const destination = join3(target, entry.name);
1208
+ let existing;
1209
+ try {
1210
+ existing = lstatSync2(destination);
1211
+ } catch (reason) {
1212
+ if (reason.code === "ENOENT") continue;
1213
+ throw reason;
1214
+ }
1215
+ if (entry.isDirectory() && existing.isDirectory() && !existing.isSymbolicLink()) validateCloneMerge(join3(source, entry.name), destination);
1216
+ else throw new Error("Clone conflicts with existing workspace entry: " + destination + ". Existing files are never overwritten; reconcile the conflict or use a new folder.");
1217
+ }
1218
+ }
1141
1219
  async function cloneProject(call2, appId, root = process.cwd()) {
1142
1220
  const status = await call2("/apps/" + applicationGuid(appId) + "/developer");
1143
1221
  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.");
1222
+ 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
1223
  const credential = await call2("/apps/" + applicationGuid(appId) + "/developer/git-credential", { method: "POST" });
1146
1224
  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));
1225
+ const staging = mkdtempSync(join3(dirname4(resolve3(root)), ".backify-clone-"));
1226
+ try {
1227
+ git(["clone", "--single-branch", "--branch", status.branch, "--", "https://github.com/" + status.repository + ".git", "."], staging, gitEnvironment(status.repository, credential.token));
1228
+ validateCloneMerge(staging, root);
1229
+ for (const entry of readdirSync2(staging)) cpSync(join3(staging, entry), join3(root, entry), { recursive: true, force: false, errorOnExist: true, dereference: false });
1230
+ } finally {
1231
+ 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.");
1232
+ rmSync(staging, { recursive: true, force: true });
1233
+ }
1234
+ }
1235
+ async function pullProject(call2, appId, root = process.cwd()) {
1236
+ if (existsSync4(join3(root, "backify.json")) && applicationGuid(projectApplication(root)) !== applicationGuid(appId)) throw new Error("Local application ID does not match the connected application.");
1237
+ const prefix = "/apps/" + applicationGuid(appId);
1238
+ const status = await call2(prefix + "/developer");
1239
+ if (!status.repository || !status.branch) throw new Error("Connect the repository and branch in Backify before pulling.");
1240
+ if (resolve3(git(["rev-parse", "--show-toplevel"], root)) !== resolve3(root)) throw new Error("Run pull from the connected repository root.");
1241
+ if (repositoryName(git(["remote", "get-url", "origin"], root)) !== status.repository.toLowerCase()) throw new Error("Local origin does not match the repository connected in Backify.");
1242
+ 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.");
1243
+ git(["check-ref-format", "--branch", status.branch], root);
1244
+ const credential = await call2(prefix + "/developer/git-credential", { method: "POST" });
1245
+ if (credential.repository.toLowerCase() !== status.repository.toLowerCase()) throw new Error("Repository changed; reconnect before pulling.");
1246
+ 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));
1247
+ const remoteCommit = git(["rev-parse", "FETCH_HEAD"], root);
1248
+ const counts = git(["rev-list", "--left-right", "--count", "HEAD..." + remoteCommit], root).split(/\s+/).map(Number);
1249
+ if (counts.length !== 2 || counts.some((count) => !Number.isSafeInteger(count) || count < 0)) throw new Error("Could not compare local and remote history.");
1250
+ const [ahead, behind] = counts;
1251
+ if (!behind) return { status: "up-to-date", ahead, behind, commit: git(["rev-parse", "HEAD"], root) };
1252
+ 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 };
1253
+ 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 };
1254
+ git(["merge", "--ff-only", "--", remoteCommit], root);
1255
+ return { status: "updated", ahead: 0, behind: 0, commit: git(["rev-parse", "HEAD"], root) };
1148
1256
  }
1149
1257
  async function readPrompt(input) {
1150
1258
  const chunks = [];
@@ -1369,15 +1477,15 @@ async function main() {
1369
1477
  break;
1370
1478
  }
1371
1479
  case "init":
1372
- initialize(option("--app") ?? args.find((value) => !value.startsWith("-")) ?? "");
1480
+ initialize(option("--app") ?? args.find((value) => !value.startsWith("-")) ?? "", process.cwd(), true, option("--agent"), option("--instruction-file"));
1373
1481
  console.log("Backify initialized. SDK, delivery instructions, and prompt history guidance are ready.");
1374
1482
  break;
1375
1483
  case "sync":
1376
- synchronize();
1484
+ synchronize(process.cwd(), option("--agent"), option("--instruction-file"));
1377
1485
  console.log("Backify instructions synchronized to version " + INSTRUCTION_VERSION);
1378
1486
  break;
1379
1487
  case "doctor": {
1380
- const result = doctor();
1488
+ const result = doctor(process.cwd(), option("--agent"), option("--instruction-file"));
1381
1489
  console.log(JSON.stringify(result, null, 2));
1382
1490
  if (!result.valid) process.exitCode = 1;
1383
1491
  break;
@@ -1415,6 +1523,11 @@ async function main() {
1415
1523
  userConfig.sessions[key] = session;
1416
1524
  saveUserConfig(configPath, userConfig);
1417
1525
  if (args.includes("--clone")) await cloneProject(client, appId);
1526
+ else if (existsSync5(join4(process.cwd(), ".git"))) {
1527
+ const update = await pullProject(client, appId);
1528
+ console.log(JSON.stringify(update, null, 2));
1529
+ if (update.status === "blocked") throw new Error(update.reason);
1530
+ }
1418
1531
  console.log(JSON.stringify(await client("/apps/" + applicationGuid(appId) + "/developer"), null, 2));
1419
1532
  break;
1420
1533
  }
@@ -1424,6 +1537,12 @@ async function main() {
1424
1537
  saveUserConfig(configPath, userConfig);
1425
1538
  console.log("Application CLI sessions revoked.");
1426
1539
  break;
1540
+ case "pull": {
1541
+ const result = await pullProject(call, option("--app") ?? projectApplication());
1542
+ console.log(JSON.stringify(result, null, 2));
1543
+ if (result.status === "blocked") process.exitCode = 1;
1544
+ break;
1545
+ }
1427
1546
  case "status": {
1428
1547
  const prefix = "/apps/" + applicationGuid(option("--app") ?? projectApplication());
1429
1548
  console.log(JSON.stringify(await call(prefix + "/developer"), null, 2));
@@ -1492,7 +1611,7 @@ async function main() {
1492
1611
  console.log(await call("/apps/" + applicationGuid(args[0]) + "/deployments/preview", { method: "POST", body: JSON.stringify({ sourceRevision: args[1] ?? "latest" }) }));
1493
1612
  break;
1494
1613
  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");
1614
+ 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
1615
  }
1497
1616
  }
1498
1617
  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.3",
4
4
  "description": "Command-line setup and diagnostics for Backify applications",
5
5
  "type": "module",
6
6
  "bin": {