@backifydev/cli 0.2.1 → 0.3.0

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 +16 -3
  2. package/dist/index.js +398 -70
  3. package/package.json +3 -3
package/README.md CHANGED
@@ -1,18 +1,31 @@
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.
13
16
 
14
17
  `init` installs `@backifydev/sdk`, writes `backify.json`, and maintains the Backify blocks in `AGENTS.md`, `CLAUDE.md`, and `.github/copilot-instructions.md`. It preserves other instructions in those files.
15
18
 
19
+ 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
+
16
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.
17
-
18
- See the [agent setup guide](https://docs.backify.dev/setup).
22
+
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,18 @@
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
 
16
14
  // ../platform/dist/index.js
17
- var INSTRUCTION_VERSION = 1;
15
+ var INSTRUCTION_VERSION = 2;
18
16
  var SDK_PACKAGE = "@backifydev/sdk";
19
17
  var DOCS_URL = "https://docs.backify.dev";
20
18
  var capability = (name, imp, description, stateful = false, provisioner = name) => ({ name, import: imp, version: 1, stateful, provisioner, docs: `${DOCS_URL}/capabilities/${name}`, description, destructiveRemovalRequiresApproval: stateful });
@@ -41,25 +39,62 @@ function managedInstructions() {
41
39
 
42
40
  # Backify Platform Instructions
43
41
 
44
- This application runs on Backify.
42
+ Instruction version: ${INSTRUCTION_VERSION}
43
+ Versioned guide: https://agent.backify.dev/v${INSTRUCTION_VERSION}
44
+
45
+ ## Scope and platform policy
46
+
47
+ This application runs on Backify. These rules govern application code targeting Backify, not the implementation of the Backify platform itself.
48
+ Use \`@backifydev/sdk\` for platform capabilities. Do not directly integrate or provision cloud infrastructure, hosted authentication, databases, storage, queues, or schedulers when an equivalent Backify capability exists. If a capability is missing, explain the gap and ask before adding an external platform dependency.
49
+
50
+ ## SDK capability map
51
+
52
+ - Authentication and authorization: \`auth\`.
53
+ - Persistent data and files: \`db\` and \`storage\`.
54
+ - Server functions: \`defineFunction\`; background work and schedules: \`jobs\` and \`jobs.schedule\`.
55
+ - Events, outbound HTTP, and AI: \`events\`, \`http.fetch\`, and \`ai\`.
56
+ - Server-side secrets, configuration, audit, and logs: \`secrets\`, \`config\`, \`audit\`, and \`logs\`.
57
+
58
+ Verify API signatures against the installed SDK types and the relevant guide at ${DOCS_URL}/capabilities/. Never invent SDK methods. Use \`@backifydev/sdk/local\` for tests and local development; \`@backifydev/sdk/runtime\` is for runtime hosts, not application code.
45
59
 
46
- Use \`@backifydev/sdk\` for application platform capabilities. Prefer Backify for authentication, database, storage, server functions, background and scheduled jobs, events, secrets, AI, external HTTP access, audit logging, application logging, and configuration.
60
+ ## Security and data safety
47
61
 
48
- Do not directly provision or integrate Azure, AWS, GCP, Supabase, Firebase, Auth0, database hosting, object storage, queues, schedulers, or infrastructure-as-code when an equivalent Backify capability exists.
62
+ Keep secrets server-side. Never commit or log credentials, expose secrets to browser code, or place cloud-management credentials in applications. Enforce authorization in trusted server code and scope data access to the authenticated user or organization as required.
63
+ Scope cookies to the exact application hostname by omitting the \`Domain\` attribute. Never use a parent-domain cookie unless explicitly required.
64
+ Require explicit confirmation before destructive data operations or removing stateful resources. Removing a capability declaration does not authorize deleting its data.
49
65
 
50
- The authoritative documentation is ${DOCS_URL}. If unsure, check it before adding an external platform dependency. Preserve all repository instructions outside this managed block.
66
+ ## Implementation workflow
67
+
68
+ 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.
69
+ 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.
70
+
71
+ ## Instruction maintenance
72
+
73
+ 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.
74
+ Keep \`backify.json\` \`instructionVersion\` aligned with this block. Run \`npx @backifydev/cli@latest doctor\` to check consistency and \`npx @backifydev/cli@latest sync\` to adopt a newer instruction version. Never silently upgrade instructions during an unrelated feature change.
75
+ Preserve all content outside this managed block and all more-specific repository instructions.
51
76
 
52
77
  ${MANAGED_END}`;
53
78
  }
79
+ function managedInstructionIssues(content) {
80
+ const normalized = content.replace(/\r\n/g, "\n"), starts = normalized.split(MANAGED_START).length - 1, ends = normalized.split(MANAGED_END).length - 1;
81
+ if (starts === 0 && ends === 0)
82
+ return ["does not contain Backify managed instructions"];
83
+ if (starts !== 1 || ends !== 1)
84
+ return ["contains duplicate or incomplete Backify managed markers"];
85
+ const start = normalized.indexOf(MANAGED_START), end = normalized.indexOf(MANAGED_END);
86
+ if (end < start)
87
+ return ["contains reversed Backify managed markers"];
88
+ return normalized.slice(start, end + MANAGED_END.length) === managedInstructions() ? [] : ["contains stale or inconsistent Backify managed instructions; run backify sync"];
89
+ }
54
90
  function updateManagedInstructions(content) {
91
+ const issues = managedInstructionIssues(content);
92
+ if (issues.some((issue) => issue.includes("markers")))
93
+ throw new Error(issues.join("; "));
55
94
  const block = managedInstructions(), start = content.indexOf(MANAGED_START), end = content.indexOf(MANAGED_END);
56
- if (start >= 0 && end >= start)
95
+ if (start >= 0)
57
96
  return content.slice(0, start) + block + content.slice(end + MANAGED_END.length);
58
- return content.trim() ? `${content.trimEnd()}
59
-
60
- ${block}
61
- ` : `${block}
62
- `;
97
+ return content.trim() ? content.trimEnd() + "\n\n" + block + "\n" : block + "\n";
63
98
  }
64
99
  function createConfig(appId) {
65
100
  return { $schema: `${DOCS_URL}/schema/backify.json`, platform: "backify", sdk: SDK_PACKAGE, appId, instructionVersion: INSTRUCTION_VERSION, docs: DOCS_URL, instructions: { preferBackifyCapabilities: true, allowDirectCloudInfrastructure: false } };
@@ -85,25 +120,54 @@ function validateConfig(value) {
85
120
  }
86
121
  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 } } } } };
87
122
 
123
+ // src/delivery.ts
124
+ import { existsSync, readFileSync, writeFileSync } from "node:fs";
125
+ import { join } from "node:path";
126
+ var deliveryInstructions = "# Backify delivery and prompt history\n\nFor EVERY subsequent user request to build or modify this application, record only the user's own request text before changing code through standard input to:\n\n npx @backifydev/cli@0.3.0 prompt record --stdin --id <new-UUID> [--filename <basename> ...]\n\nRecord requests separately and in order. Reuse the UUID on retries. Never include setup instructions, pairing commands, assistant responses, tool output, attachment contents, extracted text, images, full paths, or credentials. Filename arguments are metadata only; never read attachment bodies to submit them. Retry and report recording failures instead of silently dropping prompts.\n\nUse npx @backifydev/cli@0.3.0 status to inspect hosting and repository readiness. Run npx @backifydev/cli@0.3.0 report --check, fix failures, and commit only application changes relevant to this request. Then run npx @backifydev/cli@0.3.0 ship. The CLI verifies the connected repository and branch, runs checks, pushes committed source with short-lived repository-scoped credentials, waits for its healthy preview, and requests policy-governed publication. It never stages or commits files, force-pushes, changes hosting, or fabricates preview testing.\n\nWhen testing or approval is required, return the preview URL and remaining action. Return a production URL only after healthy activation is verified. Respect the coding tool's permissions and preserve unrelated work. Use --preview-only only when the user requests a preview. Report unsupported hosted runtime requirements instead of substituting another platform.\n\nCredentials belong outside the project. Never store pairing codes or session tokens here or in Git. Expired access requires a new setup prompt or browser login. These instructions are an explicit agent workflow, not a universal chat interceptor.\n";
127
+ function installDeliveryInstructions(root, instructionFiles2) {
128
+ const path = join(root, "BACKIFY-WORKFLOW.md");
129
+ if (existsSync(path) && readFileSync(path, "utf8") !== deliveryInstructions) throw new Error("BACKIFY-WORKFLOW.md contains different instructions. Review it before initializing; it will not be overwritten.");
130
+ writeFileSync(path, deliveryInstructions);
131
+ for (const relative of instructionFiles2) {
132
+ const instructionPath = join(root, relative), content = readFileSync(instructionPath, "utf8");
133
+ if (content.includes("## Backify delivery")) continue;
134
+ const link = relative.startsWith(".github") ? "../BACKIFY-WORKFLOW.md" : "BACKIFY-WORKFLOW.md";
135
+ writeFileSync(instructionPath, content.trimEnd() + "\n\n## Backify delivery\n\nFor application delivery and recording each user request, follow [BACKIFY-WORKFLOW.md](" + link + ").\n");
136
+ }
137
+ }
138
+
88
139
  // src/setup.ts
89
- var instructionFiles = ["AGENTS.md", "CLAUDE.md", join(".github", "copilot-instructions.md")];
140
+ var instructionFiles = ["AGENTS.md", "CLAUDE.md", join2(".github", "copilot-instructions.md")];
90
141
  function detectPackageManager(root = process.cwd()) {
91
- 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;
142
+ 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;
92
143
  return "npm";
93
144
  }
94
145
  function updateInstructions(root = process.cwd()) {
95
- for (const relative of instructionFiles) {
96
- const path = join(root, relative);
146
+ const updates = instructionFiles.map((relative) => {
147
+ const path = join2(root, relative);
148
+ return { path, content: updateManagedInstructions(existsSync2(path) ? readFileSync2(path, "utf8") : "") };
149
+ });
150
+ for (const { path, content } of updates) {
97
151
  mkdirSync(dirname(path), { recursive: true });
98
- writeFileSync(path, updateManagedInstructions(existsSync(path) ? readFileSync(path, "utf8") : ""));
152
+ writeFileSync2(path, content);
99
153
  }
100
154
  }
155
+ function synchronize(root = process.cwd()) {
156
+ const path = join2(root, "backify.json");
157
+ if (!existsSync2(path)) throw new Error("backify.json is missing; run backify init --app app_xxxxx");
158
+ const value = JSON.parse(readFileSync2(path, "utf8")), issues = validateConfig(value);
159
+ if (issues.length) throw new Error(issues.join("; "));
160
+ if (value.instructionVersion > INSTRUCTION_VERSION) throw new Error("This app uses newer instructions. Install a matching or newer CLI before syncing.");
161
+ updateInstructions(root);
162
+ value.instructionVersion = INSTRUCTION_VERSION;
163
+ writeFileSync2(path, JSON.stringify(value, null, 2) + "\n");
164
+ }
101
165
  function writeConfig(appId, root = process.cwd()) {
102
- writeFileSync(join(root, "backify.json"), JSON.stringify(createConfig(appId), null, 2) + "\n");
166
+ writeFileSync2(join2(root, "backify.json"), JSON.stringify(createConfig(appId), null, 2) + "\n");
103
167
  }
104
168
  function sdkInstalled(root = process.cwd()) {
105
169
  try {
106
- const pkg = JSON.parse(readFileSync(join(root, "package.json"), "utf8"));
170
+ const pkg = JSON.parse(readFileSync2(join2(root, "package.json"), "utf8"));
107
171
  return Boolean(pkg.dependencies?.[SDK_PACKAGE] || pkg.devDependencies?.[SDK_PACKAGE]);
108
172
  } catch {
109
173
  return false;
@@ -115,12 +179,12 @@ function installSdk(root = process.cwd()) {
115
179
  if (result.status !== 0) throw new Error(`Could not install ${SDK_PACKAGE} with ${manager}`);
116
180
  }
117
181
  function ensurePackageManifest(root) {
118
- const path = join(root, "package.json");
119
- if (existsSync(path)) return;
182
+ const path = join2(root, "package.json");
183
+ if (existsSync2(path)) return;
120
184
  const setupFiles = /* @__PURE__ */ new Set([".git", ".gitignore", ".github", "AGENTS.md", "CLAUDE.md", "README.md", "LICENSE", "backify.json"]);
121
- 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://docs.backify.dev/setup");
185
+ 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/");
122
186
  const name = basename(resolve(root)).toLowerCase().replace(/[^a-z0-9-]+/g, "-").replace(/^-+|-+$/g, "") || "backify-app";
123
- writeFileSync(path, JSON.stringify({ name, version: "0.0.0", private: true, type: "module" }, null, 2) + "\n", { flag: "wx" });
187
+ writeFileSync2(path, JSON.stringify({ name, version: "0.0.0", private: true, type: "module" }, null, 2) + "\n", { flag: "wx" });
124
188
  }
125
189
  function initialize(appId, root = process.cwd(), install = true) {
126
190
  if (!/^app_[A-Za-z0-9_-]+$/.test(appId)) throw new Error("Provide an application ID using --app app_xxxxx");
@@ -128,44 +192,247 @@ function initialize(appId, root = process.cwd(), install = true) {
128
192
  if (install) installSdk(root);
129
193
  writeConfig(appId, root);
130
194
  updateInstructions(root);
195
+ installDeliveryInstructions(root, instructionFiles);
131
196
  }
132
197
  function doctor(root = process.cwd()) {
133
198
  const issues = [], warnings = [];
134
199
  if (!sdkInstalled(root)) issues.push(`${SDK_PACKAGE} is not installed`);
135
200
  let config;
136
201
  try {
137
- config = JSON.parse(readFileSync(join(root, "backify.json"), "utf8"));
202
+ config = JSON.parse(readFileSync2(join2(root, "backify.json"), "utf8"));
138
203
  issues.push(...validateConfig(config));
139
204
  } catch {
140
205
  issues.push("backify.json is missing or invalid JSON");
141
206
  }
142
- if (config?.instructionVersion < INSTRUCTION_VERSION) warnings.push(`Repository instruction version ${config.instructionVersion}; current version ${INSTRUCTION_VERSION}. Run backify sync.`);
207
+ 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.`);
143
208
  for (const relative of instructionFiles) {
144
- const path = join(root, relative);
145
- if (!existsSync(path)) {
209
+ const path = join2(root, relative);
210
+ if (!existsSync2(path)) {
146
211
  issues.push(`${relative} is missing`);
147
212
  continue;
148
213
  }
149
- const text = readFileSync(path, "utf8");
150
- if (!text.includes(MANAGED_START) || !text.includes(MANAGED_END)) issues.push(`${relative} does not contain Backify managed instructions`);
214
+ issues.push(...managedInstructionIssues(readFileSync2(path, "utf8")).map((issue) => `${relative} ${issue}`));
151
215
  }
152
216
  let version = "unknown";
153
217
  try {
154
- const pkg = JSON.parse(readFileSync(join(root, "package.json"), "utf8"));
218
+ const pkg = JSON.parse(readFileSync2(join2(root, "package.json"), "utf8"));
155
219
  version = pkg.dependencies?.[SDK_PACKAGE] ?? pkg.devDependencies?.[SDK_PACKAGE] ?? "missing";
156
220
  } catch {
157
221
  }
158
222
  return { valid: issues.length === 0, appId: config?.appId, sdkVersion: version, instructionVersion: config?.instructionVersion, currentInstructionVersion: INSTRUCTION_VERSION, issues, warnings };
159
223
  }
160
224
 
225
+ // src/development.ts
226
+ function applicationGuid(appId) {
227
+ 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();
228
+ const match = /^app_([0-9a-f]{32})$/i.exec(appId);
229
+ if (!match) throw new Error("Use the application ID supplied by Backify.");
230
+ const value = match[1];
231
+ return [value.slice(0, 8), value.slice(8, 12), value.slice(12, 16), value.slice(16, 20), value.slice(20)].join("-");
232
+ }
233
+ function projectApplication(root = process.cwd()) {
234
+ if (!existsSync3(join3(root, "backify.json"))) throw new Error("Run backify connect --app <application-id>, then backify init.");
235
+ return JSON.parse(readFileSync3(join3(root, "backify.json"), "utf8")).appId;
236
+ }
237
+ function apiAddress(value) {
238
+ const address = new URL(value);
239
+ 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).");
240
+ return address.toString().replace(/\/$/, "");
241
+ }
242
+ function apiClient(session, transport = fetch) {
243
+ const base = apiAddress(session.apiUrl);
244
+ return async (path, init) => {
245
+ if (!path.startsWith("/") || path.startsWith("//")) throw new Error("Invalid API path.");
246
+ if (session.expiresAt && Date.parse(session.expiresAt) <= Date.now()) throw new Error("CLI session expired. Copy a new setup prompt or run backify login.");
247
+ 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 } });
248
+ if (!response.ok) {
249
+ let message = "Backify request failed (" + response.status + ").";
250
+ try {
251
+ const problem = await response.json();
252
+ message = [problem.code, problem.title].filter(Boolean).join(": ") || message;
253
+ } catch {
254
+ }
255
+ throw new Error(message);
256
+ }
257
+ return response.status === 204 ? void 0 : await response.json();
258
+ };
259
+ }
260
+ function saveUserConfig(path, config) {
261
+ const temporary = path + "." + randomUUID() + ".tmp";
262
+ try {
263
+ writeFileSync3(temporary, JSON.stringify(config, null, 2) + "\n", { mode: 384, flag: "wx" });
264
+ if (process.platform === "win32") {
265
+ const account = [process.env.USERDOMAIN, process.env.USERNAME].filter(Boolean).join("\\");
266
+ if (!account) throw new Error("Cannot identify the account to protect CLI credentials.");
267
+ const secured = spawnSync2("icacls", [temporary, "/inheritance:r", "/grant:r", account + ":(F)"], { windowsHide: true, encoding: "utf8" });
268
+ if (secured.status !== 0) throw new Error("Could not restrict CLI credential file permissions.");
269
+ }
270
+ renameSync(temporary, path);
271
+ } finally {
272
+ rmSync(temporary, { force: true });
273
+ }
274
+ }
275
+ function repositoryName(remote) {
276
+ const match = /^(?:https:\/\/github\.com\/|git@github\.com:|ssh:\/\/git@github\.com\/)([A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+?)(?:\.git)?\/?$/i.exec(remote);
277
+ if (!match) throw new Error("Repository must be a credential-free GitHub URL.");
278
+ return match[1].toLowerCase();
279
+ }
280
+ function git(args2, root = process.cwd(), environment = process.env) {
281
+ const result = spawnSync2("git", args2, { cwd: root, env: environment, encoding: "utf8", windowsHide: true, maxBuffer: 2 * 1024 * 1024 });
282
+ if (result.status !== 0) throw new Error("Git " + args2[0] + " failed. Check repository access, branch protection, and local changes.");
283
+ return result.stdout.trim();
284
+ }
285
+ function gitEnvironment(repository, token) {
286
+ if (!/^[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+$/.test(repository)) throw new Error("Invalid repository.");
287
+ 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" };
288
+ }
289
+ async function cloneProject(call2, appId, root = process.cwd()) {
290
+ const status = await call2("/apps/" + applicationGuid(appId) + "/developer");
291
+ if (!status.repository || !status.branch || !status.workflowReady) throw new Error("Connect the repository in Backify and wait for workflow setup.");
292
+ if (readdirSync2(root).length) throw new Error("--clone requires an empty directory. Existing files are never overwritten.");
293
+ const credential = await call2("/apps/" + applicationGuid(appId) + "/developer/git-credential", { method: "POST" });
294
+ if (credential.repository.toLowerCase() !== status.repository.toLowerCase()) throw new Error("Repository changed. Run connect again.");
295
+ git(["clone", "--single-branch", "--branch", status.branch, "--", "https://github.com/" + status.repository + ".git", "."], root, gitEnvironment(status.repository, credential.token));
296
+ }
297
+ async function readPrompt(input) {
298
+ const chunks = [];
299
+ let size = 0;
300
+ for await (const chunk of input) {
301
+ const bytes = Buffer.from(chunk);
302
+ size += bytes.length;
303
+ if (size > 128e3) throw new Error("Prompt exceeds the size limit.");
304
+ chunks.push(bytes);
305
+ }
306
+ const text = Buffer.concat(chunks).toString("utf8");
307
+ if (!text.trim() || text.length > 32e3) throw new Error("Provide between 1 and 32000 characters of user prompt text.");
308
+ 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.");
309
+ return text;
310
+ }
311
+ function attachmentNames(values) {
312
+ if (values.length > 50) throw new Error("At most 50 attachment filenames are allowed.");
313
+ return [...new Set(values.map((value) => {
314
+ const filename = value.replace(/\\/g, "/").split("/").pop();
315
+ if (!filename || filename === "." || filename === ".." || filename.length > 255 || /[\x00-\x1f\x7f]/.test(filename)) throw new Error("Invalid attachment filename.");
316
+ return filename;
317
+ }))];
318
+ }
319
+ function localReport(runChecks, root = process.cwd()) {
320
+ const pkg = JSON.parse(readFileSync3(join3(root, "package.json"), "utf8"));
321
+ const dependencies = { ...pkg.devDependencies, ...pkg.dependencies };
322
+ const framework = ["next", "vite", "astro", "@sveltejs/kit", "nuxt", "react", "express"].find((name) => dependencies[name]) ?? "unknown";
323
+ const setup = doctor(root);
324
+ const checks = [{ name: "backify doctor", status: setup.valid ? "passed" : "failed" }];
325
+ const manager = detectPackageManager(root);
326
+ for (const name of ["typecheck", "lint", "test", "build"]) {
327
+ if (!pkg.scripts?.[name]) continue;
328
+ 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;
329
+ checks.push({ name, status: result ? result.status === 0 ? "passed" : "failed" : "not-run" });
330
+ }
331
+ let branch = null, commitSha = null, dirty = true;
332
+ try {
333
+ branch = git(["branch", "--show-current"], root);
334
+ commitSha = git(["rev-parse", "HEAD"], root);
335
+ dirty = Boolean(git(["status", "--porcelain"], root));
336
+ } catch {
337
+ }
338
+ const outputDirectory = ["dist", "build", "out", "."].find((directory) => existsSync3(join3(root, directory, "index.html"))) ?? null;
339
+ 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 };
340
+ }
341
+ function matchesSource(release, commit) {
342
+ return release?.commitSha === commit || release?.sourceCommitSha === commit;
343
+ }
344
+ async function waitForPreview(call2, prefix, commit, options = {}) {
345
+ const deadline = Date.now() + (options.timeoutMs ?? 20 * 6e4);
346
+ const sleep = options.sleep ?? ((delay) => new Promise((resolve3) => setTimeout(resolve3, delay)));
347
+ do {
348
+ const deployments = await call2(prefix + "/deployments");
349
+ const matching = deployments.find((release) => release.kind === "preview" && matchesSource(release, commit) && !options.previousDeploymentIds?.includes(release.id));
350
+ if (matching?.status === "failed") throw new Error("Deployment of this source failed. Run backify status and inspect its deployment steps.");
351
+ const overview = await call2(prefix + "/releases");
352
+ if (matchesSource(overview.preview, commit) && overview.preview?.status === "succeeded" && overview.preview.healthResult === "healthy") return overview;
353
+ const build = await call2(prefix + "/developer/build?commitSha=" + commit);
354
+ 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."));
355
+ await sleep(options.intervalMs ?? 4e3);
356
+ } while (Date.now() < deadline);
357
+ throw new Error("Timed out waiting for this commit's healthy preview. Run backify status; no production release was requested.");
358
+ }
359
+ async function ship(call2, appId, options = {}) {
360
+ const root = options.root ?? process.cwd(), prefix = "/apps/" + applicationGuid(appId);
361
+ const status = await call2(prefix + "/developer");
362
+ if (!status.repository || !status.branch || !status.workflowReady) throw new Error("The application's repository and build workflow must be connected in Backify.");
363
+ if (!doctor(root).valid) throw new Error("Run backify doctor and fix project setup before shipping.");
364
+ if (applicationGuid(projectApplication(root)) !== applicationGuid(appId)) throw new Error("Local application ID does not match the application being shipped.");
365
+ const actualRoot = git(["rev-parse", "--show-toplevel"], root);
366
+ if (resolve2(actualRoot).toLowerCase() !== resolve2(root).toLowerCase()) throw new Error("Shipping currently requires the application at the repository root.");
367
+ const origin = git(["remote", "get-url", "origin"], root);
368
+ const pushOrigin = git(["remote", "get-url", "--push", "origin"], root);
369
+ if (repositoryName(origin) !== status.repository.toLowerCase() || repositoryName(pushOrigin) !== status.repository.toLowerCase()) throw new Error("Local repository does not match this Backify application.");
370
+ if (git(["branch", "--show-current"], root) !== status.branch) throw new Error("Use the connected branch: " + status.branch);
371
+ if (git(["status", "--porcelain"], root)) throw new Error("Commit application changes before shipping. The CLI never stages or commits unrelated files.");
372
+ const commit = git(["rev-parse", "HEAD"], root);
373
+ const report = localReport(true, root);
374
+ await call2(prefix + "/developer/report", { method: "POST", body: JSON.stringify(report) });
375
+ if (report.checks.some((check) => check.status === "failed")) throw new Error("Checks failed; nothing was pushed.");
376
+ if (status.hostingTargetType === "backify-cloud" && !report.outputDirectory) throw new Error("Backify Cloud requires a static build containing index.html.");
377
+ if (status.hostingTargetType === "azure" && !report.hasDockerfile) throw new Error("Customer Azure requires a repository-root Dockerfile.");
378
+ 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.");
379
+ const previousDeployments = await call2(prefix + "/deployments");
380
+ const previousBuild = await call2(prefix + "/developer/build?commitSha=" + commit);
381
+ const credential = await call2(prefix + "/developer/git-credential", { method: "POST" });
382
+ if (credential.repository.toLowerCase() !== status.repository.toLowerCase()) throw new Error("Repository changed; reconnect before shipping.");
383
+ const environment = gitEnvironment(status.repository, credential.token);
384
+ const destination = "https://github.com/" + status.repository + ".git";
385
+ const remoteHead = git(["ls-remote", "--", destination, "refs/heads/" + status.branch], root, environment).split(/\s+/)[0];
386
+ git(["push", "--", destination, commit + ":refs/heads/" + status.branch], root, environment);
387
+ 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" }) });
388
+ const overview = await waitForPreview(call2, prefix, commit, { timeoutMs: options.timeoutMs, previousDeploymentIds: previousDeployments.map((deployment) => deployment.id), previousBuildRunId: previousBuild.runId ?? null });
389
+ if (overview.preview.commitSha !== commit) {
390
+ 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.");
391
+ const refreshed = await call2(prefix + "/developer/git-credential", { method: "POST" });
392
+ if (refreshed.repository.toLowerCase() !== status.repository.toLowerCase()) throw new Error("Repository changed while building.");
393
+ git(["fetch", "--", destination, "refs/heads/" + status.branch], root, gitEnvironment(status.repository, refreshed.token));
394
+ git(["merge", "--ff-only", overview.preview.commitSha], root);
395
+ }
396
+ return publishPreview(call2, prefix, overview, options);
397
+ }
398
+ async function publishPreview(call2, prefix, overview, options = {}) {
399
+ if (!overview.preview || overview.preview.status !== "succeeded" || overview.preview.healthResult !== "healthy") throw new Error("Only a verified healthy preview can be published.");
400
+ const preview = overview.preview;
401
+ if (options.previewOnly) return { status: "preview-ready", url: preview.url };
402
+ if (overview.policy.requirePreviewTesting && !preview.testedAt) return { status: "testing-required", url: preview.url };
403
+ if (overview.live?.previewDeploymentId === preview.id && overview.live.status === "succeeded") return { status: "live", url: overview.live.url };
404
+ const promotion = await call2(prefix + "/deployments/promote", { method: "POST", headers: { "Idempotency-Key": "cli-ship-" + preview.id }, body: JSON.stringify({ previewDeploymentId: preview.id, tested: false }) });
405
+ if (promotion.status === "awaiting-approval" || promotion.approvalId) return { status: "awaiting-approval", url: preview.url };
406
+ if (!promotion.operationId) throw new Error("Publication returned no operation to verify. Run backify status.");
407
+ const deadline = Date.now() + (options.timeoutMs ?? 20 * 6e4);
408
+ do {
409
+ const operation = await call2("/operations/" + promotion.operationId);
410
+ if (operation.status === "failed" || operation.status === "cancelled") throw new Error(operation.errorMessage ?? "Production deployment did not complete.");
411
+ if (operation.status === "succeeded") {
412
+ const releases = await call2(prefix + "/releases");
413
+ 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.");
414
+ return { status: "live", url: releases.live.url };
415
+ }
416
+ await new Promise((resolve3) => setTimeout(resolve3, 4e3));
417
+ } while (Date.now() < deadline);
418
+ throw new Error("Publication is still pending. Run backify status; deployment success has not been verified.");
419
+ }
420
+
161
421
  // src/index.ts
162
- var configPath = join2(homedir(), ".backify.json");
163
- var userConfig = existsSync2(configPath) ? JSON.parse(readFileSync2(configPath, "utf8")) : { apiUrl: process.env.BACKIFY_API_URL ?? "http://localhost:5080/api" };
422
+ import { readFileSync as readFileSync4, existsSync as existsSync4, rmSync as rmSync2 } from "node:fs";
423
+ import { homedir } from "node:os";
424
+ import { join as join4 } from "node:path";
425
+ import { randomBytes } from "node:crypto";
426
+ import { createServer } from "node:http";
427
+ import { spawn } from "node:child_process";
428
+ var configPath = join4(homedir(), ".backify.json");
429
+ var userConfig = existsSync4(configPath) ? JSON.parse(readFileSync4(configPath, "utf8")) : {};
164
430
  var [command, ...args] = process.argv.slice(2);
165
431
  async function call(path, init) {
166
- const response = await fetch(userConfig.apiUrl + path, { ...init, headers: { "content-type": "application/json", "X-Organization-ID": userConfig.organizationId ?? "", ...userConfig.token ? { authorization: `Bearer ${userConfig.token}` } : {} } });
167
- if (!response.ok) throw new Error(await response.text());
168
- return response.status === 204 ? {} : response.json();
432
+ let appId = option("--app");
433
+ if (!appId && existsSync4(join4(process.cwd(), "backify.json"))) appId = projectApplication();
434
+ const session = appId ? userConfig.sessions?.["app_" + applicationGuid(appId).replaceAll("-", "")] : void 0;
435
+ return apiClient(session ?? { token: userConfig.token ?? "", organizationId: userConfig.organizationId ?? "", apiUrl: process.env.BACKIFY_API_URL ?? userConfig.apiUrl ?? "https://api.backify.dev/api" })(path, init);
169
436
  }
170
437
  var option = (name) => {
171
438
  const index = args.indexOf(name);
@@ -197,7 +464,7 @@ async function browserLogin() {
197
464
  res.end("Backify CLI connected. You can close this tab.");
198
465
  userConfig.token = value.token;
199
466
  userConfig.organizationId = value.organizationId;
200
- writeFileSync2(configPath, JSON.stringify(userConfig, null, 2));
467
+ saveUserConfig(configPath, userConfig);
201
468
  server.close();
202
469
  resolveLogin();
203
470
  } catch {
@@ -208,7 +475,7 @@ async function browserLogin() {
208
475
  });
209
476
  let resolveLogin = () => {
210
477
  };
211
- const completed = new Promise((resolve2) => resolveLogin = resolve2);
478
+ const completed = new Promise((resolve3) => resolveLogin = resolve3);
212
479
  server.listen(0, "127.0.0.1", () => {
213
480
  const address = server.address();
214
481
  if (!address || typeof address === "string") throw new Error("Could not start local login callback");
@@ -235,25 +502,16 @@ async function browserLogin() {
235
502
  async function main() {
236
503
  switch (command) {
237
504
  case "init":
238
- initialize(option("--app") ?? args.find((x) => !x.startsWith("-")) ?? "");
239
- console.log("Backify initialized. SDK, configuration, and agent instructions are ready.");
505
+ initialize(option("--app") ?? args.find((value) => !value.startsWith("-")) ?? "");
506
+ console.log("Backify initialized. SDK, delivery instructions, and prompt history guidance are ready.");
240
507
  break;
241
- case "sync": {
242
- const path = join2(process.cwd(), "backify.json");
243
- if (!existsSync2(path)) throw new Error("backify.json is missing; run backify init --app app_xxxxx");
244
- const value = JSON.parse(readFileSync2(path, "utf8"));
245
- value.instructionVersion = INSTRUCTION_VERSION;
246
- writeFileSync2(path, JSON.stringify(value, null, 2) + "\n");
247
- updateInstructions();
248
- console.log(`Backify instructions synchronized to version ${INSTRUCTION_VERSION}.`);
508
+ case "sync":
509
+ synchronize();
510
+ console.log("Backify instructions synchronized to version " + INSTRUCTION_VERSION);
249
511
  break;
250
- }
251
512
  case "doctor": {
252
513
  const result = doctor();
253
- console.log(`Backify doctor: ${result.valid ? "healthy" : "invalid"}`);
254
- console.log(`App: ${result.appId ?? "missing"} \xB7 SDK: ${result.sdkVersion} \xB7 Instructions: ${result.instructionVersion ?? "missing"}/${result.currentInstructionVersion}`);
255
- for (const warning of result.warnings) console.warn(`warning: ${warning}`);
256
- for (const issue of result.issues) console.error(`error: ${issue}`);
514
+ console.log(JSON.stringify(result, null, 2));
257
515
  if (!result.valid) process.exitCode = 1;
258
516
  break;
259
517
  }
@@ -261,28 +519,98 @@ async function main() {
261
519
  if (args[0]) {
262
520
  userConfig.token = args[0];
263
521
  userConfig.organizationId = args[1] ?? userConfig.organizationId;
264
- writeFileSync2(configPath, JSON.stringify(userConfig, null, 2));
265
- console.log("Logged in to Backify");
266
- } else {
522
+ saveUserConfig(configPath, userConfig);
523
+ } else await browserLogin();
524
+ console.log("Logged in to Backify");
525
+ break;
526
+ case "logout":
527
+ rmSync2(configPath, { force: true });
528
+ console.log("Local CLI credentials removed. Use disconnect before logout to revoke an application session.");
529
+ break;
530
+ case "connect": {
531
+ const appId = option("--app") ?? projectApplication(), key = "app_" + applicationGuid(appId).replaceAll("-", "");
532
+ const code = option("--pair"), organizationId = option("--organization") ?? userConfig.organizationId;
533
+ const address = apiAddress(option("--api") ?? process.env.BACKIFY_API_URL ?? userConfig.apiUrl ?? "https://api.backify.dev/api");
534
+ if (code) {
535
+ if (!organizationId) throw new Error("The setup prompt must supply the organization.");
536
+ const redeem = apiClient({ token: "", organizationId, apiUrl: address });
537
+ const paired = await redeem("/apps/" + applicationGuid(appId) + "/developer/pair", { method: "POST", body: JSON.stringify({ code }) });
538
+ userConfig.sessions ??= {};
539
+ userConfig.sessions[key] = { ...paired, apiUrl: address };
540
+ saveUserConfig(configPath, userConfig);
541
+ } else if (!userConfig.sessions?.[key] && !userConfig.token) {
267
542
  await browserLogin();
268
- console.log("Logged in to Backify");
269
543
  }
270
- ;
544
+ const session = userConfig.sessions?.[key] ?? { token: userConfig.token ?? "", organizationId: organizationId ?? userConfig.organizationId ?? "", apiUrl: address };
545
+ const client = apiClient(session);
546
+ await client("/apps/" + applicationGuid(appId) + "/developer");
547
+ userConfig.sessions ??= {};
548
+ userConfig.sessions[key] = session;
549
+ saveUserConfig(configPath, userConfig);
550
+ if (args.includes("--clone")) await cloneProject(client, appId);
551
+ console.log(JSON.stringify(await client("/apps/" + applicationGuid(appId) + "/developer"), null, 2));
271
552
  break;
272
- case "logout":
273
- rmSync(configPath, { force: true });
553
+ }
554
+ case "disconnect":
555
+ await call("/apps/" + applicationGuid(projectApplication()) + "/developer/session", { method: "DELETE" });
556
+ delete userConfig.sessions?.["app_" + applicationGuid(projectApplication()).replaceAll("-", "")];
557
+ saveUserConfig(configPath, userConfig);
558
+ console.log("Application CLI sessions revoked.");
559
+ break;
560
+ case "status": {
561
+ const prefix = "/apps/" + applicationGuid(option("--app") ?? projectApplication());
562
+ console.log(JSON.stringify(await call(prefix + "/developer"), null, 2));
563
+ const deployments = await call(prefix + "/deployments");
564
+ const latest = deployments[0];
565
+ if (latest?.status === "failed") console.log(JSON.stringify(await call(prefix + "/deployments/" + latest.id), null, 2));
566
+ try {
567
+ const commit = git(["rev-parse", "HEAD"]);
568
+ console.log(JSON.stringify(await call(prefix + "/developer/build?commitSha=" + commit), null, 2));
569
+ } catch {
570
+ console.log("Build status is unavailable for the current local commit.");
571
+ }
274
572
  break;
573
+ }
574
+ case "report": {
575
+ const report = localReport(args.includes("--check"));
576
+ await call("/apps/" + applicationGuid(projectApplication()) + "/developer/report", { method: "POST", body: JSON.stringify(report) });
577
+ console.log(JSON.stringify(report, null, 2));
578
+ if (report.checks.some((check) => check.status === "failed")) process.exitCode = 1;
579
+ break;
580
+ }
581
+ case "prompt": {
582
+ 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.");
583
+ const id = option("--id");
584
+ 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.");
585
+ const filenames = attachmentNames(args.flatMap((value, index) => value === "--filename" ? [args[index + 1] ?? ""] : []));
586
+ const text = await readPrompt(process.stdin);
587
+ console.log(JSON.stringify(await call("/apps/" + applicationGuid(projectApplication()) + "/prompts", { method: "POST", body: JSON.stringify({ clientPromptId: id, text, filenames }) })));
588
+ break;
589
+ }
590
+ case "ship": {
591
+ const timeout = Number(option("--timeout") ?? "1200");
592
+ if (!Number.isFinite(timeout) || timeout < 1 || timeout > 7200) throw new Error("--timeout must be between 1 and 7200 seconds.");
593
+ console.log(JSON.stringify(await ship(call, projectApplication(), { previewOnly: args.includes("--preview-only"), timeoutMs: timeout * 1e3 }), null, 2));
594
+ break;
595
+ }
596
+ case "request": {
597
+ const workspaceId = option("--workspace"), name = option("--name"), slug = option("--slug"), description = option("--description"), configurationFile = option("--configuration");
598
+ if (!workspaceId || !name || !slug || !description) throw new Error("Use backify request --workspace <id> --name <name> --slug <slug> --description <purpose> [--configuration <json-file>]");
599
+ const configuration = configurationFile ? JSON.parse(readFileSync4(configurationFile, "utf8")) : void 0;
600
+ console.log(await call("/apps/requests", { method: "POST", body: JSON.stringify({ workspaceId, name, slug, description, configuration }) }));
601
+ break;
602
+ }
275
603
  case "apps":
276
604
  console.table(await call("/apps"));
277
605
  break;
278
606
  case "deploy":
279
- console.log(await call(`/apps/${args[0]}/deployments/preview`, { method: "POST", body: JSON.stringify({ sourceRevision: args[1] ?? "local" }) }));
607
+ console.log(await call("/apps/" + applicationGuid(args[0]) + "/deployments/preview", { method: "POST", body: JSON.stringify({ sourceRevision: args[1] ?? "latest" }) }));
280
608
  break;
281
609
  default:
282
- console.log("backify init --app app_xxxxx | sync | doctor | login | logout | apps | deploy");
610
+ console.log("backify connect | init | doctor | sync | login | logout | disconnect | status | report [--check] | prompt record --stdin --id <UUID> | ship [--preview-only] | request | apps | deploy");
283
611
  }
284
612
  }
285
613
  main().catch((error) => {
286
- console.error(error instanceof Error ? error.message : error);
614
+ console.error(error instanceof Error ? error.message : String(error));
287
615
  process.exitCode = 1;
288
616
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@backifydev/cli",
3
- "version": "0.2.1",
3
+ "version": "0.3.0",
4
4
  "description": "Command-line setup and diagnostics for Backify applications",
5
5
  "type": "module",
6
6
  "bin": { "backify": "dist/index.js" },
@@ -15,8 +15,8 @@
15
15
  "test": "vitest run"
16
16
  },
17
17
  "publishConfig": { "access": "public" },
18
- "repository": { "type": "git", "url": "git+https://github.com/frejnorling/backify.git", "directory": "packages/cli" },
19
- "homepage": "https://docs.backify.dev/setup",
18
+ "repository": { "type": "git", "url": "git+https://github.com/frejnorling/backify.git", "directory": "packages/cli" },
19
+ "homepage": "https://agent.backify.dev/",
20
20
  "license": "MIT",
21
21
  "engines": { "node": ">=20" },
22
22
  "devDependencies": {