@backifydev/cli 0.2.2 → 0.3.1

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 +12 -1
  2. package/dist/index.js +408 -55
  3. package/package.json +20 -7
package/README.md CHANGED
@@ -1,12 +1,15 @@
1
1
  # @backifydev/cli
2
2
 
3
- Set up and check a Backify application repository.
3
+ Set up, connect, record requests, and ship a Backify application repository. CLI 0.3.0 adds the connected AI delivery workflow.
4
4
 
5
5
  ```sh
6
6
  npx @backifydev/cli init --app app_xxxxx
7
7
  npx @backifydev/cli doctor
8
8
  npx @backifydev/cli sync
9
9
  npx @backifydev/cli login
10
+ npx @backifydev/cli status
11
+ npx @backifydev/cli report --check
12
+ npx @backifydev/cli ship
10
13
  ```
11
14
 
12
15
  `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.
@@ -18,3 +21,11 @@ The app configuration stores `instructionVersion`, matching the version declared
18
21
  You can start from an empty folder without a Git repository. `init` creates a minimal private `package.json` before installing the SDK, without choosing a framework. Add the application's framework and development/build scripts afterward. Existing manifests are preserved; in a non-empty application or monorepo, run `init` from the appropriate JavaScript/TypeScript package directory.
19
22
 
20
23
  See the [agent setup guide](https://agent.backify.dev/).
24
+
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
+
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.
28
+
29
+ `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
+
31
+ `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
@@ -1,20 +1,73 @@
1
1
  #!/usr/bin/env node
2
2
 
3
- // src/index.ts
4
- import { readFileSync as readFileSync2, writeFileSync as writeFileSync2, existsSync as existsSync2, rmSync } from "node:fs";
5
- import { homedir } from "node:os";
6
- import { join as join2 } from "node:path";
7
- import { randomBytes } from "node:crypto";
8
- import { createServer } from "node:http";
9
- import { spawn } from "node:child_process";
3
+ // src/development.ts
4
+ import { existsSync as existsSync3, readFileSync as readFileSync3, readdirSync as readdirSync2, writeFileSync as writeFileSync3, renameSync, rmSync } from "node:fs";
5
+ import { join as join3, resolve as resolve2 } from "node:path";
6
+ import { spawnSync as spawnSync2 } from "node:child_process";
7
+ import { randomUUID } from "node:crypto";
10
8
 
11
9
  // src/setup.ts
12
- import { existsSync, readFileSync, writeFileSync, mkdirSync, readdirSync } from "node:fs";
13
- import { basename, dirname, join, resolve } from "node:path";
10
+ import { existsSync as existsSync2, readFileSync as readFileSync2, writeFileSync as writeFileSync2, mkdirSync, readdirSync } from "node:fs";
11
+ import { basename, dirname, join as join2, resolve } from "node:path";
14
12
  import { spawnSync } from "node:child_process";
15
13
 
14
+ // ../platform/dist/delivery.js
15
+ var deliveryInstructions = `# Backify application delivery and prompt history
16
+
17
+ Instruction version: 3
18
+ Versioned guide: https://agent.backify.dev/v3/delivery.md
19
+
20
+ 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.
21
+
22
+ ## Connect and initialize
23
+
24
+ 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.
25
+
26
+ 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.
27
+
28
+ 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.
29
+
30
+ Run the following to discover the connected hosting target, repository, branch, reports, releases, and deployment failures:
31
+
32
+ npx @backifydev/cli@0.3.0 status
33
+
34
+ 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.
35
+
36
+ ## Record each user request
37
+
38
+ 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:
39
+
40
+ npx @backifydev/cli@0.3.0 prompt record --stdin --id <new-UUID> [--filename <basename> ...]
41
+
42
+ 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.
43
+
44
+ ## Build for the connected hosting target
45
+
46
+ 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.
47
+
48
+ For Backify Cloud, build a static application with a build script and index.html in its output. For customer Azure, provide a working repository-root Dockerfile whose HTTP server listens on 0.0.0.0:8080. Backify handles deployment to the existing authorized target; neither application code nor the coding tool should provision cloud infrastructure.
49
+
50
+ Do not claim unsupported hosted server functions or SDK execution paths work. Report unsupported runtime requirements instead of substituting another platform. Keep secrets server-side and out of browser code, project files, and Git.
51
+
52
+ ## Verify, push, and publish
53
+
54
+ Run:
55
+
56
+ npx @backifydev/cli@0.3.0 report --check
57
+
58
+ Fix relevant failures, then commit only application changes for this request using a meaningful message. Never commit credentials or unrelated user changes. Then run:
59
+
60
+ npx @backifydev/cli@0.3.0 ship
61
+
62
+ 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.
63
+
64
+ 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.
65
+
66
+ 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.
67
+ `;
68
+
16
69
  // ../platform/dist/index.js
17
- var INSTRUCTION_VERSION = 2;
70
+ var INSTRUCTION_VERSION = 3;
18
71
  var SDK_PACKAGE = "@backifydev/sdk";
19
72
  var DOCS_URL = "https://docs.backify.dev";
20
73
  var capability = (name, imp, description, stateful = false, provisioner = name) => ({ name, import: imp, version: 1, stateful, provisioner, docs: `${DOCS_URL}/capabilities/${name}`, description, destructiveRemovalRequiresApproval: stateful });
@@ -70,6 +123,10 @@ Require explicit confirmation before destructive data operations or removing sta
70
123
  Inspect the framework, lockfile, scripts, configuration, and applicable repository instructions before editing. Preserve the existing framework and unrelated work. Implement the requested application rather than stopping at SDK setup; ask what to build if requirements are missing.
71
124
  Run relevant tests, typechecking, lint, and builds using the repository's scripts when available. Report changes, checks, and remaining blockers accurately; do not claim unverified success.
72
125
 
126
+ ## Connected application delivery
127
+
128
+ 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.
129
+
73
130
  ## Instruction maintenance
74
131
 
75
132
  The authoritative documentation is ${DOCS_URL}/. The root at https://agent.backify.dev/ always shows the latest guide; use the versioned guide above for this repository's installed instructions.
@@ -122,38 +179,64 @@ function validateConfig(value) {
122
179
  }
123
180
  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 } } } } };
124
181
 
182
+ // src/delivery.ts
183
+ import { existsSync, readFileSync, writeFileSync } from "node:fs";
184
+ import { join } from "node:path";
185
+ import { createHash } from "node:crypto";
186
+ var previousGeneratedWorkflowHashes = /* @__PURE__ */ new Set(["6eb280ae7bdb14c390011686c41400f59c023304c3015d8440b7e80aaafa1c11", "545e03a1d5b996ade179e0e60ed5c86accdadc1982c1c25efd094e34f72f47ee"]);
187
+ function validateDeliveryInstructions(root) {
188
+ const path = join(root, "BACKIFY-WORKFLOW.md");
189
+ if (existsSync(path)) {
190
+ const existing = readFileSync(path, "utf8").replace(/\r\n/g, "\n");
191
+ 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.");
192
+ }
193
+ }
194
+ function installDeliveryInstructions(root, instructionFiles2) {
195
+ validateDeliveryInstructions(root);
196
+ const path = join(root, "BACKIFY-WORKFLOW.md");
197
+ writeFileSync(path, deliveryInstructions);
198
+ for (const relative of instructionFiles2) {
199
+ const instructionPath = join(root, relative), content = readFileSync(instructionPath, "utf8");
200
+ if (content.includes("## Backify delivery")) continue;
201
+ const link = relative.startsWith(".github") ? "../BACKIFY-WORKFLOW.md" : "BACKIFY-WORKFLOW.md";
202
+ writeFileSync(instructionPath, content.trimEnd() + "\n\n## Backify delivery\n\nFor application delivery and recording each user request, follow [BACKIFY-WORKFLOW.md](" + link + ").\n");
203
+ }
204
+ }
205
+
125
206
  // src/setup.ts
126
- var instructionFiles = ["AGENTS.md", "CLAUDE.md", join(".github", "copilot-instructions.md")];
207
+ var instructionFiles = ["AGENTS.md", "CLAUDE.md", join2(".github", "copilot-instructions.md")];
127
208
  function detectPackageManager(root = process.cwd()) {
128
- for (const [file, name] of [["pnpm-lock.yaml", "pnpm"], ["yarn.lock", "yarn"], ["bun.lockb", "bun"], ["bun.lock", "bun"], ["package-lock.json", "npm"]]) if (existsSync(join(root, file))) return name;
209
+ for (const [file, name] of [["pnpm-lock.yaml", "pnpm"], ["yarn.lock", "yarn"], ["bun.lockb", "bun"], ["bun.lock", "bun"], ["package-lock.json", "npm"]]) if (existsSync2(join2(root, file))) return name;
129
210
  return "npm";
130
211
  }
131
212
  function updateInstructions(root = process.cwd()) {
132
213
  const updates = instructionFiles.map((relative) => {
133
- const path = join(root, relative);
134
- return { path, content: updateManagedInstructions(existsSync(path) ? readFileSync(path, "utf8") : "") };
214
+ const path = join2(root, relative);
215
+ return { path, content: updateManagedInstructions(existsSync2(path) ? readFileSync2(path, "utf8") : "") };
135
216
  });
136
217
  for (const { path, content } of updates) {
137
218
  mkdirSync(dirname(path), { recursive: true });
138
- writeFileSync(path, content);
219
+ writeFileSync2(path, content);
139
220
  }
140
221
  }
141
222
  function synchronize(root = process.cwd()) {
142
- const path = join(root, "backify.json");
143
- if (!existsSync(path)) throw new Error("backify.json is missing; run backify init --app app_xxxxx");
144
- const value = JSON.parse(readFileSync(path, "utf8")), issues = validateConfig(value);
223
+ const path = join2(root, "backify.json");
224
+ if (!existsSync2(path)) throw new Error("backify.json is missing; run backify init --app app_xxxxx");
225
+ const value = JSON.parse(readFileSync2(path, "utf8")), issues = validateConfig(value);
145
226
  if (issues.length) throw new Error(issues.join("; "));
146
227
  if (value.instructionVersion > INSTRUCTION_VERSION) throw new Error("This app uses newer instructions. Install a matching or newer CLI before syncing.");
228
+ validateDeliveryInstructions(root);
147
229
  updateInstructions(root);
230
+ installDeliveryInstructions(root, instructionFiles);
148
231
  value.instructionVersion = INSTRUCTION_VERSION;
149
- writeFileSync(path, JSON.stringify(value, null, 2) + "\n");
232
+ writeFileSync2(path, JSON.stringify(value, null, 2) + "\n");
150
233
  }
151
234
  function writeConfig(appId, root = process.cwd()) {
152
- writeFileSync(join(root, "backify.json"), JSON.stringify(createConfig(appId), null, 2) + "\n");
235
+ writeFileSync2(join2(root, "backify.json"), JSON.stringify(createConfig(appId), null, 2) + "\n");
153
236
  }
154
237
  function sdkInstalled(root = process.cwd()) {
155
238
  try {
156
- const pkg = JSON.parse(readFileSync(join(root, "package.json"), "utf8"));
239
+ const pkg = JSON.parse(readFileSync2(join2(root, "package.json"), "utf8"));
157
240
  return Boolean(pkg.dependencies?.[SDK_PACKAGE] || pkg.devDependencies?.[SDK_PACKAGE]);
158
241
  } catch {
159
242
  return false;
@@ -165,12 +248,12 @@ function installSdk(root = process.cwd()) {
165
248
  if (result.status !== 0) throw new Error(`Could not install ${SDK_PACKAGE} with ${manager}`);
166
249
  }
167
250
  function ensurePackageManifest(root) {
168
- const path = join(root, "package.json");
169
- if (existsSync(path)) return;
251
+ const path = join2(root, "package.json");
252
+ if (existsSync2(path)) return;
170
253
  const setupFiles = /* @__PURE__ */ new Set([".git", ".gitignore", ".github", "AGENTS.md", "CLAUDE.md", "README.md", "LICENSE", "backify.json"]);
171
254
  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/");
172
255
  const name = basename(resolve(root)).toLowerCase().replace(/[^a-z0-9-]+/g, "-").replace(/^-+|-+$/g, "") || "backify-app";
173
- writeFileSync(path, JSON.stringify({ name, version: "0.0.0", private: true, type: "module" }, null, 2) + "\n", { flag: "wx" });
256
+ writeFileSync2(path, JSON.stringify({ name, version: "0.0.0", private: true, type: "module" }, null, 2) + "\n", { flag: "wx" });
174
257
  }
175
258
  function initialize(appId, root = process.cwd(), install = true) {
176
259
  if (!/^app_[A-Za-z0-9_-]+$/.test(appId)) throw new Error("Provide an application ID using --app app_xxxxx");
@@ -178,43 +261,247 @@ function initialize(appId, root = process.cwd(), install = true) {
178
261
  if (install) installSdk(root);
179
262
  writeConfig(appId, root);
180
263
  updateInstructions(root);
264
+ installDeliveryInstructions(root, instructionFiles);
181
265
  }
182
266
  function doctor(root = process.cwd()) {
183
267
  const issues = [], warnings = [];
184
268
  if (!sdkInstalled(root)) issues.push(`${SDK_PACKAGE} is not installed`);
185
269
  let config;
186
270
  try {
187
- config = JSON.parse(readFileSync(join(root, "backify.json"), "utf8"));
271
+ config = JSON.parse(readFileSync2(join2(root, "backify.json"), "utf8"));
188
272
  issues.push(...validateConfig(config));
189
273
  } catch {
190
274
  issues.push("backify.json is missing or invalid JSON");
191
275
  }
192
276
  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.`);
193
277
  for (const relative of instructionFiles) {
194
- const path = join(root, relative);
195
- if (!existsSync(path)) {
278
+ const path = join2(root, relative);
279
+ if (!existsSync2(path)) {
196
280
  issues.push(`${relative} is missing`);
197
281
  continue;
198
282
  }
199
- issues.push(...managedInstructionIssues(readFileSync(path, "utf8")).map((issue) => `${relative} ${issue}`));
283
+ issues.push(...managedInstructionIssues(readFileSync2(path, "utf8")).map((issue) => `${relative} ${issue}`));
200
284
  }
201
285
  let version = "unknown";
202
286
  try {
203
- const pkg = JSON.parse(readFileSync(join(root, "package.json"), "utf8"));
287
+ const pkg = JSON.parse(readFileSync2(join2(root, "package.json"), "utf8"));
204
288
  version = pkg.dependencies?.[SDK_PACKAGE] ?? pkg.devDependencies?.[SDK_PACKAGE] ?? "missing";
205
289
  } catch {
206
290
  }
207
291
  return { valid: issues.length === 0, appId: config?.appId, sdkVersion: version, instructionVersion: config?.instructionVersion, currentInstructionVersion: INSTRUCTION_VERSION, issues, warnings };
208
292
  }
209
293
 
294
+ // src/development.ts
295
+ function applicationGuid(appId) {
296
+ 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();
297
+ const match = /^app_([0-9a-f]{32})$/i.exec(appId);
298
+ if (!match) throw new Error("Use the application ID supplied by Backify.");
299
+ const value = match[1];
300
+ return [value.slice(0, 8), value.slice(8, 12), value.slice(12, 16), value.slice(16, 20), value.slice(20)].join("-");
301
+ }
302
+ function projectApplication(root = process.cwd()) {
303
+ if (!existsSync3(join3(root, "backify.json"))) throw new Error("Run backify connect --app <application-id>, then backify init.");
304
+ return JSON.parse(readFileSync3(join3(root, "backify.json"), "utf8")).appId;
305
+ }
306
+ function apiAddress(value) {
307
+ const address = new URL(value);
308
+ if (address.username || address.password || address.search || address.hash || address.protocol !== "https:" && !(address.protocol === "http:" && ["localhost", "127.0.0.1", "[::1]"].includes(address.hostname))) throw new Error("Backify API must use HTTPS (HTTP is allowed only on loopback for local development).");
309
+ return address.toString().replace(/\/$/, "");
310
+ }
311
+ function apiClient(session, transport = fetch) {
312
+ const base = apiAddress(session.apiUrl);
313
+ return async (path, init) => {
314
+ if (!path.startsWith("/") || path.startsWith("//")) throw new Error("Invalid API path.");
315
+ if (session.expiresAt && Date.parse(session.expiresAt) <= Date.now()) throw new Error("CLI session expired. Copy a new setup prompt or run backify login.");
316
+ const response = await transport(base + path, { ...init, redirect: "error", signal: AbortSignal.timeout(3e4), headers: { "content-type": "application/json", "X-Organization-ID": session.organizationId, ...session.token ? { authorization: "Bearer " + session.token } : {}, ...init?.headers } });
317
+ if (!response.ok) {
318
+ let message = "Backify request failed (" + response.status + ").";
319
+ try {
320
+ const problem = await response.json();
321
+ message = [problem.code, problem.title].filter(Boolean).join(": ") || message;
322
+ } catch {
323
+ }
324
+ throw new Error(message);
325
+ }
326
+ return response.status === 204 ? void 0 : await response.json();
327
+ };
328
+ }
329
+ function saveUserConfig(path, config) {
330
+ const temporary = path + "." + randomUUID() + ".tmp";
331
+ try {
332
+ writeFileSync3(temporary, JSON.stringify(config, null, 2) + "\n", { mode: 384, flag: "wx" });
333
+ if (process.platform === "win32") {
334
+ const account = [process.env.USERDOMAIN, process.env.USERNAME].filter(Boolean).join("\\");
335
+ if (!account) throw new Error("Cannot identify the account to protect CLI credentials.");
336
+ const secured = spawnSync2("icacls", [temporary, "/inheritance:r", "/grant:r", account + ":(F)"], { windowsHide: true, encoding: "utf8" });
337
+ if (secured.status !== 0) throw new Error("Could not restrict CLI credential file permissions.");
338
+ }
339
+ renameSync(temporary, path);
340
+ } finally {
341
+ rmSync(temporary, { force: true });
342
+ }
343
+ }
344
+ function repositoryName(remote) {
345
+ const match = /^(?:https:\/\/github\.com\/|git@github\.com:|ssh:\/\/git@github\.com\/)([A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+?)(?:\.git)?\/?$/i.exec(remote);
346
+ if (!match) throw new Error("Repository must be a credential-free GitHub URL.");
347
+ return match[1].toLowerCase();
348
+ }
349
+ function git(args2, root = process.cwd(), environment = process.env) {
350
+ const result = spawnSync2("git", args2, { cwd: root, env: environment, encoding: "utf8", windowsHide: true, maxBuffer: 2 * 1024 * 1024 });
351
+ if (result.status !== 0) throw new Error("Git " + args2[0] + " failed. Check repository access, branch protection, and local changes.");
352
+ return result.stdout.trim();
353
+ }
354
+ function gitEnvironment(repository, token) {
355
+ if (!/^[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+$/.test(repository)) throw new Error("Invalid repository.");
356
+ 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" };
357
+ }
358
+ async function cloneProject(call2, appId, root = process.cwd()) {
359
+ const status = await call2("/apps/" + applicationGuid(appId) + "/developer");
360
+ if (!status.repository || !status.branch || !status.workflowReady) throw new Error("Connect the repository in Backify and wait for workflow setup.");
361
+ if (readdirSync2(root).length) throw new Error("--clone requires an empty directory. Existing files are never overwritten.");
362
+ const credential = await call2("/apps/" + applicationGuid(appId) + "/developer/git-credential", { method: "POST" });
363
+ if (credential.repository.toLowerCase() !== status.repository.toLowerCase()) throw new Error("Repository changed. Run connect again.");
364
+ git(["clone", "--single-branch", "--branch", status.branch, "--", "https://github.com/" + status.repository + ".git", "."], root, gitEnvironment(status.repository, credential.token));
365
+ }
366
+ async function readPrompt(input) {
367
+ const chunks = [];
368
+ let size = 0;
369
+ for await (const chunk of input) {
370
+ const bytes = Buffer.from(chunk);
371
+ size += bytes.length;
372
+ if (size > 128e3) throw new Error("Prompt exceeds the size limit.");
373
+ chunks.push(bytes);
374
+ }
375
+ const text = Buffer.concat(chunks).toString("utf8");
376
+ if (!text.trim() || text.length > 32e3) throw new Error("Provide between 1 and 32000 characters of user prompt text.");
377
+ if (/\bbfcli_[0-9a-f]{64}\b|--pair\s+[0-9a-f]{64}/i.test(text)) throw new Error("Do not record setup prompts or Backify credentials.");
378
+ return text;
379
+ }
380
+ function attachmentNames(values) {
381
+ if (values.length > 50) throw new Error("At most 50 attachment filenames are allowed.");
382
+ return [...new Set(values.map((value) => {
383
+ const filename = value.replace(/\\/g, "/").split("/").pop();
384
+ if (!filename || filename === "." || filename === ".." || filename.length > 255 || /[\x00-\x1f\x7f]/.test(filename)) throw new Error("Invalid attachment filename.");
385
+ return filename;
386
+ }))];
387
+ }
388
+ function localReport(runChecks, root = process.cwd()) {
389
+ const pkg = JSON.parse(readFileSync3(join3(root, "package.json"), "utf8"));
390
+ const dependencies = { ...pkg.devDependencies, ...pkg.dependencies };
391
+ const framework = ["next", "vite", "astro", "@sveltejs/kit", "nuxt", "react", "express"].find((name) => dependencies[name]) ?? "unknown";
392
+ const setup = doctor(root);
393
+ const checks = [{ name: "backify doctor", status: setup.valid ? "passed" : "failed" }];
394
+ const manager = detectPackageManager(root);
395
+ for (const name of ["typecheck", "lint", "test", "build"]) {
396
+ if (!pkg.scripts?.[name]) continue;
397
+ const result = runChecks ? spawnSync2(manager, ["run", name], { cwd: root, stdio: ["ignore", "inherit", "inherit"], env: { ...process.env, CI: "true" }, shell: process.platform === "win32", windowsHide: true, timeout: 10 * 6e4 }) : void 0;
398
+ checks.push({ name, status: result ? result.status === 0 ? "passed" : "failed" : "not-run" });
399
+ }
400
+ let branch = null, commitSha = null, dirty = true;
401
+ try {
402
+ branch = git(["branch", "--show-current"], root);
403
+ commitSha = git(["rev-parse", "HEAD"], root);
404
+ dirty = Boolean(git(["status", "--porcelain"], root));
405
+ } catch {
406
+ }
407
+ const outputDirectory = ["dist", "build", "out", "."].find((directory) => existsSync3(join3(root, directory, "index.html"))) ?? null;
408
+ return { framework, sdkVersion: dependencies["@backifydev/sdk"] ?? null, branch, commitSha, dirty, buildCommand: pkg.scripts?.build ? manager + " run build" : null, outputDirectory, hasDockerfile: existsSync3(join3(root, "Dockerfile")), checks };
409
+ }
410
+ function matchesSource(release, commit) {
411
+ return release?.commitSha === commit || release?.sourceCommitSha === commit;
412
+ }
413
+ async function waitForPreview(call2, prefix, commit, options = {}) {
414
+ const deadline = Date.now() + (options.timeoutMs ?? 20 * 6e4);
415
+ const sleep = options.sleep ?? ((delay) => new Promise((resolve3) => setTimeout(resolve3, delay)));
416
+ do {
417
+ const deployments = await call2(prefix + "/deployments");
418
+ const matching = deployments.find((release) => release.kind === "preview" && matchesSource(release, commit) && !options.previousDeploymentIds?.includes(release.id));
419
+ if (matching?.status === "failed") throw new Error("Deployment of this source failed. Run backify status and inspect its deployment steps.");
420
+ const overview = await call2(prefix + "/releases");
421
+ if (matchesSource(overview.preview, commit) && overview.preview?.status === "succeeded" && overview.preview.healthResult === "healthy") return overview;
422
+ const build = await call2(prefix + "/developer/build?commitSha=" + commit);
423
+ if ((options.previousBuildRunId === void 0 || build.runId !== options.previousBuildRunId) && ["failure", "cancelled", "timed_out", "action_required", "startup_failure"].includes(build.conclusion ?? "")) throw new Error("GitHub build " + build.conclusion + ". " + (build.url ?? "Run backify status for details."));
424
+ await sleep(options.intervalMs ?? 4e3);
425
+ } while (Date.now() < deadline);
426
+ throw new Error("Timed out waiting for this commit's healthy preview. Run backify status; no production release was requested.");
427
+ }
428
+ async function ship(call2, appId, options = {}) {
429
+ const root = options.root ?? process.cwd(), prefix = "/apps/" + applicationGuid(appId);
430
+ const status = await call2(prefix + "/developer");
431
+ if (!status.repository || !status.branch || !status.workflowReady) throw new Error("The application's repository and build workflow must be connected in Backify.");
432
+ if (!doctor(root).valid) throw new Error("Run backify doctor and fix project setup before shipping.");
433
+ if (applicationGuid(projectApplication(root)) !== applicationGuid(appId)) throw new Error("Local application ID does not match the application being shipped.");
434
+ const actualRoot = git(["rev-parse", "--show-toplevel"], root);
435
+ if (resolve2(actualRoot).toLowerCase() !== resolve2(root).toLowerCase()) throw new Error("Shipping currently requires the application at the repository root.");
436
+ const origin = git(["remote", "get-url", "origin"], root);
437
+ const pushOrigin = git(["remote", "get-url", "--push", "origin"], root);
438
+ if (repositoryName(origin) !== status.repository.toLowerCase() || repositoryName(pushOrigin) !== status.repository.toLowerCase()) throw new Error("Local repository does not match this Backify application.");
439
+ if (git(["branch", "--show-current"], root) !== status.branch) throw new Error("Use the connected branch: " + status.branch);
440
+ if (git(["status", "--porcelain"], root)) throw new Error("Commit application changes before shipping. The CLI never stages or commits unrelated files.");
441
+ const commit = git(["rev-parse", "HEAD"], root);
442
+ const report = localReport(true, root);
443
+ await call2(prefix + "/developer/report", { method: "POST", body: JSON.stringify(report) });
444
+ if (report.checks.some((check) => check.status === "failed")) throw new Error("Checks failed; nothing was pushed.");
445
+ if (status.hostingTargetType === "backify-cloud" && !report.outputDirectory) throw new Error("Backify Cloud requires a static build containing index.html.");
446
+ if (status.hostingTargetType === "azure" && !report.hasDockerfile) throw new Error("Customer Azure requires a repository-root Dockerfile.");
447
+ if (git(["status", "--porcelain"], root) || git(["rev-parse", "HEAD"], root) !== commit) throw new Error("Checks changed tracked source. Review and commit those changes before shipping.");
448
+ const previousDeployments = await call2(prefix + "/deployments");
449
+ const previousBuild = await call2(prefix + "/developer/build?commitSha=" + commit);
450
+ const credential = await call2(prefix + "/developer/git-credential", { method: "POST" });
451
+ if (credential.repository.toLowerCase() !== status.repository.toLowerCase()) throw new Error("Repository changed; reconnect before shipping.");
452
+ const environment = gitEnvironment(status.repository, credential.token);
453
+ const destination = "https://github.com/" + status.repository + ".git";
454
+ const remoteHead = git(["ls-remote", "--", destination, "refs/heads/" + status.branch], root, environment).split(/\s+/)[0];
455
+ git(["push", "--", destination, commit + ":refs/heads/" + status.branch], root, environment);
456
+ if (remoteHead === commit && !(matchesSource(status.releases.preview, commit) && status.releases.preview?.status === "succeeded")) await call2(prefix + "/deployments/preview", { method: "POST", body: JSON.stringify({ sourceRevision: "latest" }) });
457
+ const overview = await waitForPreview(call2, prefix, commit, { timeoutMs: options.timeoutMs, previousDeploymentIds: previousDeployments.map((deployment) => deployment.id), previousBuildRunId: previousBuild.runId ?? null });
458
+ if (overview.preview.commitSha !== commit) {
459
+ if (git(["status", "--porcelain"], root) || git(["rev-parse", "HEAD"], root) !== commit) throw new Error("Local work changed while the preview built. Review it before synchronizing the build commit.");
460
+ const refreshed = await call2(prefix + "/developer/git-credential", { method: "POST" });
461
+ if (refreshed.repository.toLowerCase() !== status.repository.toLowerCase()) throw new Error("Repository changed while building.");
462
+ git(["fetch", "--", destination, "refs/heads/" + status.branch], root, gitEnvironment(status.repository, refreshed.token));
463
+ git(["merge", "--ff-only", overview.preview.commitSha], root);
464
+ }
465
+ return publishPreview(call2, prefix, overview, options);
466
+ }
467
+ async function publishPreview(call2, prefix, overview, options = {}) {
468
+ if (!overview.preview || overview.preview.status !== "succeeded" || overview.preview.healthResult !== "healthy") throw new Error("Only a verified healthy preview can be published.");
469
+ const preview = overview.preview;
470
+ if (options.previewOnly) return { status: "preview-ready", url: preview.url };
471
+ if (overview.policy.requirePreviewTesting && !preview.testedAt) return { status: "testing-required", url: preview.url };
472
+ if (overview.live?.previewDeploymentId === preview.id && overview.live.status === "succeeded") return { status: "live", url: overview.live.url };
473
+ const promotion = await call2(prefix + "/deployments/promote", { method: "POST", headers: { "Idempotency-Key": "cli-ship-" + preview.id }, body: JSON.stringify({ previewDeploymentId: preview.id, tested: false }) });
474
+ if (promotion.status === "awaiting-approval" || promotion.approvalId) return { status: "awaiting-approval", url: preview.url };
475
+ if (!promotion.operationId) throw new Error("Publication returned no operation to verify. Run backify status.");
476
+ const deadline = Date.now() + (options.timeoutMs ?? 20 * 6e4);
477
+ do {
478
+ const operation = await call2("/operations/" + promotion.operationId);
479
+ if (operation.status === "failed" || operation.status === "cancelled") throw new Error(operation.errorMessage ?? "Production deployment did not complete.");
480
+ if (operation.status === "succeeded") {
481
+ const releases = await call2(prefix + "/releases");
482
+ if (releases.live?.previewDeploymentId !== preview.id || releases.live.status !== "succeeded" || releases.live.healthResult !== "healthy") throw new Error("Production did not activate this healthy release. Run backify status.");
483
+ return { status: "live", url: releases.live.url };
484
+ }
485
+ await new Promise((resolve3) => setTimeout(resolve3, 4e3));
486
+ } while (Date.now() < deadline);
487
+ throw new Error("Publication is still pending. Run backify status; deployment success has not been verified.");
488
+ }
489
+
210
490
  // src/index.ts
211
- var configPath = join2(homedir(), ".backify.json");
212
- var userConfig = existsSync2(configPath) ? JSON.parse(readFileSync2(configPath, "utf8")) : { apiUrl: process.env.BACKIFY_API_URL ?? "http://localhost:5080/api" };
491
+ import { readFileSync as readFileSync4, existsSync as existsSync4, rmSync as rmSync2 } from "node:fs";
492
+ import { homedir } from "node:os";
493
+ import { join as join4 } from "node:path";
494
+ import { randomBytes } from "node:crypto";
495
+ import { createServer } from "node:http";
496
+ import { spawn } from "node:child_process";
497
+ var configPath = join4(homedir(), ".backify.json");
498
+ var userConfig = existsSync4(configPath) ? JSON.parse(readFileSync4(configPath, "utf8")) : {};
213
499
  var [command, ...args] = process.argv.slice(2);
214
500
  async function call(path, init) {
215
- const response = await fetch(userConfig.apiUrl + path, { ...init, headers: { "content-type": "application/json", "X-Organization-ID": userConfig.organizationId ?? "", ...userConfig.token ? { authorization: `Bearer ${userConfig.token}` } : {} } });
216
- if (!response.ok) throw new Error(await response.text());
217
- return response.status === 204 ? {} : response.json();
501
+ let appId = option("--app");
502
+ if (!appId && existsSync4(join4(process.cwd(), "backify.json"))) appId = projectApplication();
503
+ const session = appId ? userConfig.sessions?.["app_" + applicationGuid(appId).replaceAll("-", "")] : void 0;
504
+ return apiClient(session ?? { token: userConfig.token ?? "", organizationId: userConfig.organizationId ?? "", apiUrl: process.env.BACKIFY_API_URL ?? userConfig.apiUrl ?? "https://api.backify.dev/api" })(path, init);
218
505
  }
219
506
  var option = (name) => {
220
507
  const index = args.indexOf(name);
@@ -246,7 +533,7 @@ async function browserLogin() {
246
533
  res.end("Backify CLI connected. You can close this tab.");
247
534
  userConfig.token = value.token;
248
535
  userConfig.organizationId = value.organizationId;
249
- writeFileSync2(configPath, JSON.stringify(userConfig, null, 2));
536
+ saveUserConfig(configPath, userConfig);
250
537
  server.close();
251
538
  resolveLogin();
252
539
  } catch {
@@ -257,7 +544,7 @@ async function browserLogin() {
257
544
  });
258
545
  let resolveLogin = () => {
259
546
  };
260
- const completed = new Promise((resolve2) => resolveLogin = resolve2);
547
+ const completed = new Promise((resolve3) => resolveLogin = resolve3);
261
548
  server.listen(0, "127.0.0.1", () => {
262
549
  const address = server.address();
263
550
  if (!address || typeof address === "string") throw new Error("Could not start local login callback");
@@ -284,20 +571,16 @@ async function browserLogin() {
284
571
  async function main() {
285
572
  switch (command) {
286
573
  case "init":
287
- initialize(option("--app") ?? args.find((x) => !x.startsWith("-")) ?? "");
288
- console.log("Backify initialized. SDK, configuration, and agent instructions are ready.");
574
+ initialize(option("--app") ?? args.find((value) => !value.startsWith("-")) ?? "");
575
+ console.log("Backify initialized. SDK, delivery instructions, and prompt history guidance are ready.");
289
576
  break;
290
- case "sync": {
577
+ case "sync":
291
578
  synchronize();
292
- console.log(`Backify instructions synchronized to version ${INSTRUCTION_VERSION}.`);
579
+ console.log("Backify instructions synchronized to version " + INSTRUCTION_VERSION);
293
580
  break;
294
- }
295
581
  case "doctor": {
296
582
  const result = doctor();
297
- console.log(`Backify doctor: ${result.valid ? "healthy" : "invalid"}`);
298
- console.log(`App: ${result.appId ?? "missing"} \xB7 SDK: ${result.sdkVersion} \xB7 Instructions: ${result.instructionVersion ?? "missing"}/${result.currentInstructionVersion}`);
299
- for (const warning of result.warnings) console.warn(`warning: ${warning}`);
300
- for (const issue of result.issues) console.error(`error: ${issue}`);
583
+ console.log(JSON.stringify(result, null, 2));
301
584
  if (!result.valid) process.exitCode = 1;
302
585
  break;
303
586
  }
@@ -305,28 +588,98 @@ async function main() {
305
588
  if (args[0]) {
306
589
  userConfig.token = args[0];
307
590
  userConfig.organizationId = args[1] ?? userConfig.organizationId;
308
- writeFileSync2(configPath, JSON.stringify(userConfig, null, 2));
309
- console.log("Logged in to Backify");
310
- } else {
591
+ saveUserConfig(configPath, userConfig);
592
+ } else await browserLogin();
593
+ console.log("Logged in to Backify");
594
+ break;
595
+ case "logout":
596
+ rmSync2(configPath, { force: true });
597
+ console.log("Local CLI credentials removed. Use disconnect before logout to revoke an application session.");
598
+ break;
599
+ case "connect": {
600
+ const appId = option("--app") ?? projectApplication(), key = "app_" + applicationGuid(appId).replaceAll("-", "");
601
+ const code = option("--pair"), organizationId = option("--organization") ?? userConfig.organizationId;
602
+ const address = apiAddress(option("--api") ?? process.env.BACKIFY_API_URL ?? userConfig.apiUrl ?? "https://api.backify.dev/api");
603
+ if (code) {
604
+ if (!organizationId) throw new Error("The setup prompt must supply the organization.");
605
+ const redeem = apiClient({ token: "", organizationId, apiUrl: address });
606
+ const paired = await redeem("/apps/" + applicationGuid(appId) + "/developer/pair", { method: "POST", body: JSON.stringify({ code }) });
607
+ userConfig.sessions ??= {};
608
+ userConfig.sessions[key] = { ...paired, apiUrl: address };
609
+ saveUserConfig(configPath, userConfig);
610
+ } else if (!userConfig.sessions?.[key] && !userConfig.token) {
311
611
  await browserLogin();
312
- console.log("Logged in to Backify");
313
612
  }
314
- ;
613
+ const session = userConfig.sessions?.[key] ?? { token: userConfig.token ?? "", organizationId: organizationId ?? userConfig.organizationId ?? "", apiUrl: address };
614
+ const client = apiClient(session);
615
+ await client("/apps/" + applicationGuid(appId) + "/developer");
616
+ userConfig.sessions ??= {};
617
+ userConfig.sessions[key] = session;
618
+ saveUserConfig(configPath, userConfig);
619
+ if (args.includes("--clone")) await cloneProject(client, appId);
620
+ console.log(JSON.stringify(await client("/apps/" + applicationGuid(appId) + "/developer"), null, 2));
315
621
  break;
316
- case "logout":
317
- rmSync(configPath, { force: true });
622
+ }
623
+ case "disconnect":
624
+ await call("/apps/" + applicationGuid(projectApplication()) + "/developer/session", { method: "DELETE" });
625
+ delete userConfig.sessions?.["app_" + applicationGuid(projectApplication()).replaceAll("-", "")];
626
+ saveUserConfig(configPath, userConfig);
627
+ console.log("Application CLI sessions revoked.");
318
628
  break;
629
+ case "status": {
630
+ const prefix = "/apps/" + applicationGuid(option("--app") ?? projectApplication());
631
+ console.log(JSON.stringify(await call(prefix + "/developer"), null, 2));
632
+ const deployments = await call(prefix + "/deployments");
633
+ const latest = deployments[0];
634
+ if (latest?.status === "failed") console.log(JSON.stringify(await call(prefix + "/deployments/" + latest.id), null, 2));
635
+ try {
636
+ const commit = git(["rev-parse", "HEAD"]);
637
+ console.log(JSON.stringify(await call(prefix + "/developer/build?commitSha=" + commit), null, 2));
638
+ } catch {
639
+ console.log("Build status is unavailable for the current local commit.");
640
+ }
641
+ break;
642
+ }
643
+ case "report": {
644
+ const report = localReport(args.includes("--check"));
645
+ await call("/apps/" + applicationGuid(projectApplication()) + "/developer/report", { method: "POST", body: JSON.stringify(report) });
646
+ console.log(JSON.stringify(report, null, 2));
647
+ if (report.checks.some((check) => check.status === "failed")) process.exitCode = 1;
648
+ break;
649
+ }
650
+ case "prompt": {
651
+ 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.");
652
+ const id = option("--id");
653
+ 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.");
654
+ const filenames = attachmentNames(args.flatMap((value, index) => value === "--filename" ? [args[index + 1] ?? ""] : []));
655
+ const text = await readPrompt(process.stdin);
656
+ console.log(JSON.stringify(await call("/apps/" + applicationGuid(projectApplication()) + "/prompts", { method: "POST", body: JSON.stringify({ clientPromptId: id, text, filenames }) })));
657
+ break;
658
+ }
659
+ case "ship": {
660
+ const timeout = Number(option("--timeout") ?? "1200");
661
+ if (!Number.isFinite(timeout) || timeout < 1 || timeout > 7200) throw new Error("--timeout must be between 1 and 7200 seconds.");
662
+ console.log(JSON.stringify(await ship(call, projectApplication(), { previewOnly: args.includes("--preview-only"), timeoutMs: timeout * 1e3 }), null, 2));
663
+ break;
664
+ }
665
+ case "request": {
666
+ const workspaceId = option("--workspace"), name = option("--name"), slug = option("--slug"), description = option("--description"), configurationFile = option("--configuration");
667
+ if (!workspaceId || !name || !slug || !description) throw new Error("Use backify request --workspace <id> --name <name> --slug <slug> --description <purpose> [--configuration <json-file>]");
668
+ const configuration = configurationFile ? JSON.parse(readFileSync4(configurationFile, "utf8")) : void 0;
669
+ console.log(await call("/apps/requests", { method: "POST", body: JSON.stringify({ workspaceId, name, slug, description, configuration }) }));
670
+ break;
671
+ }
319
672
  case "apps":
320
673
  console.table(await call("/apps"));
321
674
  break;
322
675
  case "deploy":
323
- console.log(await call(`/apps/${args[0]}/deployments/preview`, { method: "POST", body: JSON.stringify({ sourceRevision: args[1] ?? "local" }) }));
676
+ console.log(await call("/apps/" + applicationGuid(args[0]) + "/deployments/preview", { method: "POST", body: JSON.stringify({ sourceRevision: args[1] ?? "latest" }) }));
324
677
  break;
325
678
  default:
326
- console.log("backify init --app app_xxxxx | sync | doctor | login | logout | apps | deploy");
679
+ console.log("backify connect | init | doctor | sync | login | logout | disconnect | status | report [--check] | prompt record --stdin --id <UUID> | ship [--preview-only] | request | apps | deploy");
327
680
  }
328
681
  }
329
682
  main().catch((error) => {
330
- console.error(error instanceof Error ? error.message : error);
683
+ console.error(error instanceof Error ? error.message : String(error));
331
684
  process.exitCode = 1;
332
685
  });
package/package.json CHANGED
@@ -1,10 +1,15 @@
1
1
  {
2
2
  "name": "@backifydev/cli",
3
- "version": "0.2.2",
3
+ "version": "0.3.1",
4
4
  "description": "Command-line setup and diagnostics for Backify applications",
5
5
  "type": "module",
6
- "bin": { "backify": "dist/index.js" },
7
- "files": ["dist/index.js", "README.md"],
6
+ "bin": {
7
+ "backify": "dist/index.js"
8
+ },
9
+ "files": [
10
+ "dist/index.js",
11
+ "README.md"
12
+ ],
8
13
  "scripts": {
9
14
  "prebuild": "npm run build --workspace @backify/platform",
10
15
  "build": "esbuild src/index.ts --bundle --platform=node --format=esm --outfile=dist/index.js",
@@ -14,11 +19,19 @@
14
19
  "pretest": "npm run build --workspace @backify/platform",
15
20
  "test": "vitest run"
16
21
  },
17
- "publishConfig": { "access": "public" },
18
- "repository": { "type": "git", "url": "git+https://github.com/frejnorling/backify.git", "directory": "packages/cli" },
19
- "homepage": "https://agent.backify.dev/",
22
+ "publishConfig": {
23
+ "access": "public"
24
+ },
25
+ "repository": {
26
+ "type": "git",
27
+ "url": "git+https://github.com/frejnorling/backify.git",
28
+ "directory": "packages/cli"
29
+ },
30
+ "homepage": "https://agent.backify.dev/",
20
31
  "license": "MIT",
21
- "engines": { "node": ">=20" },
32
+ "engines": {
33
+ "node": ">=20"
34
+ },
22
35
  "devDependencies": {
23
36
  "@backify/platform": "*",
24
37
  "@types/node": "^24.3.1",