@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.
- package/README.md +12 -1
- package/dist/index.js +408 -55
- package/package.json +20 -7
package/README.md
CHANGED
|
@@ -1,12 +1,15 @@
|
|
|
1
1
|
# @backifydev/cli
|
|
2
2
|
|
|
3
|
-
Set up and
|
|
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/
|
|
4
|
-
import { readFileSync as
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
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 =
|
|
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",
|
|
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 (
|
|
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 =
|
|
134
|
-
return { path, content: updateManagedInstructions(
|
|
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
|
-
|
|
219
|
+
writeFileSync2(path, content);
|
|
139
220
|
}
|
|
140
221
|
}
|
|
141
222
|
function synchronize(root = process.cwd()) {
|
|
142
|
-
const path =
|
|
143
|
-
if (!
|
|
144
|
-
const value = JSON.parse(
|
|
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
|
-
|
|
232
|
+
writeFileSync2(path, JSON.stringify(value, null, 2) + "\n");
|
|
150
233
|
}
|
|
151
234
|
function writeConfig(appId, root = process.cwd()) {
|
|
152
|
-
|
|
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(
|
|
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 =
|
|
169
|
-
if (
|
|
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
|
-
|
|
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(
|
|
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 =
|
|
195
|
-
if (!
|
|
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(
|
|
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(
|
|
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
|
-
|
|
212
|
-
|
|
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
|
-
|
|
216
|
-
if (!
|
|
217
|
-
|
|
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
|
-
|
|
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((
|
|
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((
|
|
288
|
-
console.log("Backify initialized. SDK,
|
|
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(
|
|
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(
|
|
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
|
-
|
|
309
|
-
|
|
310
|
-
|
|
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
|
-
|
|
317
|
-
|
|
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(
|
|
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
|
|
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.
|
|
3
|
+
"version": "0.3.1",
|
|
4
4
|
"description": "Command-line setup and diagnostics for Backify applications",
|
|
5
5
|
"type": "module",
|
|
6
|
-
"bin": {
|
|
7
|
-
|
|
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": {
|
|
18
|
-
|
|
19
|
-
|
|
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": {
|
|
32
|
+
"engines": {
|
|
33
|
+
"node": ">=20"
|
|
34
|
+
},
|
|
22
35
|
"devDependencies": {
|
|
23
36
|
"@backify/platform": "*",
|
|
24
37
|
"@types/node": "^24.3.1",
|