@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.
- package/README.md +6 -3
- package/dist/index.js +170 -51
- 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
|
|
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
|
|
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
|
|
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:
|
|
786
|
-
Versioned guide: https://agent.backify.dev/
|
|
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
|
-
|
|
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
|
-
|
|
794
|
+
npx @backifydev/cli@latest connect --app <app-id> --organization <organization-id> --api https://api.backify.dev/api --pair <pairing-code>
|
|
795
795
|
|
|
796
|
-
|
|
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@
|
|
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@
|
|
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@
|
|
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@
|
|
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 =
|
|
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
|
|
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
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
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
|
-
|
|
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
|
-
|
|
984
|
-
const
|
|
985
|
-
|
|
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"
|
|
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 =
|
|
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(
|
|
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,
|
|
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((
|
|
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
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
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
|
-
|
|
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).
|
|
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
|
-
|
|
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) => {
|