@backifydev/cli 0.3.7 → 0.3.8
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 +2 -0
- package/dist/index.js +89 -18
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -276,3 +276,5 @@ Copy the application AI prompt in Backify to receive the application ID, organiz
|
|
|
276
276
|
`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.
|
|
277
277
|
|
|
278
278
|
`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.
|
|
279
|
+
|
|
280
|
+
Connections now report the device hostname, operating system, architecture, and installed CLI version. Browser-based `connect` exchanges a general sign-in token for an application-scoped device session. Application **People & access → CLI access** shows these sessions and lets access managers revoke individual devices; source managers can revoke their own. Existing connections may need reconnecting to record device details. See [CLI access and country lookup configuration](../../docs/cli-access.md) for privacy, permissions and deployment requirements.
|
package/dist/index.js
CHANGED
|
@@ -17660,6 +17660,71 @@ async function packageFunctions(entry, output) {
|
|
|
17660
17660
|
return outputPath;
|
|
17661
17661
|
}
|
|
17662
17662
|
|
|
17663
|
+
// src/cli-session.ts
|
|
17664
|
+
import { hostname, platform, release, arch } from "node:os";
|
|
17665
|
+
|
|
17666
|
+
// package.json
|
|
17667
|
+
var package_default = {
|
|
17668
|
+
name: "@backifydev/cli",
|
|
17669
|
+
version: "0.3.8",
|
|
17670
|
+
description: "Command-line setup and diagnostics for Backify applications",
|
|
17671
|
+
type: "module",
|
|
17672
|
+
bin: {
|
|
17673
|
+
backify: "dist/index.js"
|
|
17674
|
+
},
|
|
17675
|
+
files: [
|
|
17676
|
+
"dist/index.js",
|
|
17677
|
+
"README.md"
|
|
17678
|
+
],
|
|
17679
|
+
scripts: {
|
|
17680
|
+
prebuild: "npm run build -w @backify/platform && npm run build -w @backifydev/sdk && npm run build -w @backify/analyzer",
|
|
17681
|
+
build: `esbuild src/index.ts --bundle --platform=node --format=esm --external:esbuild --external:typescript --outfile=dist/index.js --banner:js="import { createRequire as __backifyCreateRequire } from 'node:module'; var require = __backifyCreateRequire(import.meta.url);"`,
|
|
17682
|
+
prepack: "npm run build",
|
|
17683
|
+
pretypecheck: "npm run build -w @backify/platform && npm run build -w @backifydev/sdk && npm run build -w @backify/analyzer",
|
|
17684
|
+
typecheck: "tsc --noEmit",
|
|
17685
|
+
pretest: "npm run build -w @backify/platform && npm run build -w @backifydev/sdk && npm run build -w @backify/analyzer",
|
|
17686
|
+
test: "vitest run"
|
|
17687
|
+
},
|
|
17688
|
+
publishConfig: {
|
|
17689
|
+
access: "public"
|
|
17690
|
+
},
|
|
17691
|
+
repository: {
|
|
17692
|
+
type: "git",
|
|
17693
|
+
url: "git+https://github.com/frejnorling/backify.git",
|
|
17694
|
+
directory: "packages/cli"
|
|
17695
|
+
},
|
|
17696
|
+
homepage: "https://agent.backify.dev/",
|
|
17697
|
+
license: "MIT",
|
|
17698
|
+
engines: {
|
|
17699
|
+
node: ">=20"
|
|
17700
|
+
},
|
|
17701
|
+
dependencies: {
|
|
17702
|
+
"@azure/functions": "^4.7.2",
|
|
17703
|
+
"@backifydev/sdk": "^0.1.1",
|
|
17704
|
+
esbuild: "^0.25.10",
|
|
17705
|
+
fflate: "^0.8.2",
|
|
17706
|
+
typescript: "^5.9.2"
|
|
17707
|
+
},
|
|
17708
|
+
devDependencies: {
|
|
17709
|
+
"@backify/analyzer": "*",
|
|
17710
|
+
"@backify/platform": "*",
|
|
17711
|
+
"@hono/node-server": "2.1.3",
|
|
17712
|
+
"@types/node": "^24.3.1",
|
|
17713
|
+
hono: "4.13.12",
|
|
17714
|
+
vitest: "^4.1.11"
|
|
17715
|
+
}
|
|
17716
|
+
};
|
|
17717
|
+
|
|
17718
|
+
// src/cli-session.ts
|
|
17719
|
+
function cliDevice() {
|
|
17720
|
+
return { deviceName: hostname(), operatingSystem: platform() + " " + release(), architecture: arch(), cliVersion: package_default.version };
|
|
17721
|
+
}
|
|
17722
|
+
var cliUserAgent = "BackifyCLI/" + package_default.version;
|
|
17723
|
+
async function registerCliSession(client, applicationId, apiUrl) {
|
|
17724
|
+
const session = await client("/apps/" + applicationId + "/developer/session", { method: "POST", body: JSON.stringify(cliDevice()) });
|
|
17725
|
+
return { ...session, apiUrl };
|
|
17726
|
+
}
|
|
17727
|
+
|
|
17663
17728
|
// src/local-runtime.ts
|
|
17664
17729
|
import { createHash, randomUUID } from "node:crypto";
|
|
17665
17730
|
import { chmodSync, existsSync as existsSync2, lstatSync, mkdirSync as mkdirSync2, readFileSync, readdirSync, writeFileSync as writeFileSync2 } from "node:fs";
|
|
@@ -17669,13 +17734,13 @@ import { spawnSync } from "node:child_process";
|
|
|
17669
17734
|
|
|
17670
17735
|
// src/local-state.ts
|
|
17671
17736
|
import { renameSync } from "node:fs";
|
|
17672
|
-
function replaceLocalFile(source, destination, rename,
|
|
17737
|
+
function replaceLocalFile(source, destination, rename, platform2, wait) {
|
|
17673
17738
|
for (let attempt = 0; ; attempt++) {
|
|
17674
17739
|
try {
|
|
17675
17740
|
rename(source, destination);
|
|
17676
17741
|
return;
|
|
17677
17742
|
} catch (error) {
|
|
17678
|
-
if (
|
|
17743
|
+
if (platform2 !== "win32" || attempt >= 4 || !(error instanceof Error) || !("code" in error) || !["EPERM", "EACCES", "EBUSY"].includes(String(error.code))) throw error;
|
|
17679
17744
|
wait(10 * 2 ** attempt);
|
|
17680
17745
|
}
|
|
17681
17746
|
}
|
|
@@ -17688,8 +17753,8 @@ function renameLocalFile(source, destination) {
|
|
|
17688
17753
|
var maxArchiveBytes = 256 * 1024 * 1024;
|
|
17689
17754
|
var maxExpandedBytes = 512 * 1024 * 1024;
|
|
17690
17755
|
var hash = (bytes) => createHash("sha256").update(bytes).digest("hex");
|
|
17691
|
-
function runtimeIdentifier(
|
|
17692
|
-
const system = { win32: "win", darwin: "osx", linux: "linux" }[
|
|
17756
|
+
function runtimeIdentifier(platform2 = process.platform, architecture = process.arch) {
|
|
17757
|
+
const system = { win32: "win", darwin: "osx", linux: "linux" }[platform2];
|
|
17693
17758
|
if (!system || !["x64", "arm64"].includes(architecture)) throw new Error("Local runtime supports Windows, macOS and Linux on x64/arm64");
|
|
17694
17759
|
return system + "-" + architecture;
|
|
17695
17760
|
}
|
|
@@ -17847,7 +17912,7 @@ function parseApplicationManifest(source) {
|
|
|
17847
17912
|
if (name2 === "database" && settings.migrations !== void 0 && !portablePath(settings.migrations))
|
|
17848
17913
|
throw new Error("Migrations must be repository-relative");
|
|
17849
17914
|
if (name2 === "storage" && settings.buckets !== void 0) {
|
|
17850
|
-
if (!Array.isArray(settings.buckets) || settings.buckets.length > 50 || settings.buckets.some((bucket) => !object(bucket) || typeof bucket.name !== "string" || !/^[A-Za-z0-9_-]{1,100}$/.test(bucket.name) || bucket.access !== void 0 && bucket.access !== "private" || Object.keys(bucket).some((key) => !["name", "access"].includes(key))) || new Set(settings.buckets.map((bucket) => bucket.name)).size !== settings.buckets.length)
|
|
17915
|
+
if (!Array.isArray(settings.buckets) || settings.buckets.length > 50 || settings.buckets.some((bucket) => !object(bucket) || typeof bucket.name !== "string" || !/^[A-Za-z0-9_-]{1,100}$/.test(bucket.name) || bucket.access !== void 0 && bucket.access !== "private" || bucket.isolation !== void 0 && !["private", "user", "tenant", "authenticated", "public", "selected"].includes(String(bucket.isolation)) || bucket.allowSignedUrls !== void 0 && typeof bucket.allowSignedUrls !== "boolean" || Object.keys(bucket).some((key) => !["name", "access", "isolation", "allowSignedUrls"].includes(key))) || new Set(settings.buckets.map((bucket) => bucket.name)).size !== settings.buckets.length)
|
|
17851
17916
|
throw new Error("Storage requires at most 50 distinct private logical buckets");
|
|
17852
17917
|
}
|
|
17853
17918
|
capabilities[name2] = structuredClone(settings);
|
|
@@ -19825,7 +19890,7 @@ async function startLocalServices(root, manifest, options = {}) {
|
|
|
19825
19890
|
if (databasePort === hcrPort) throw new Error("HCR and PostgreSQL require separate ports");
|
|
19826
19891
|
const cluster = join4(directory2, "postgres");
|
|
19827
19892
|
assertLocalStatePath(root, cluster);
|
|
19828
|
-
const
|
|
19893
|
+
const release2 = acquireLocalLease(root, ".backify/dev/services/lifecycle.lock");
|
|
19829
19894
|
let postgresStarted = false;
|
|
19830
19895
|
let hcr;
|
|
19831
19896
|
let timer;
|
|
@@ -19862,7 +19927,7 @@ async function startLocalServices(root, manifest, options = {}) {
|
|
|
19862
19927
|
if (postgresStarted) notify("postgres", "stopped");
|
|
19863
19928
|
if (existsSync5(tokenPath)) unlinkSync3(tokenPath);
|
|
19864
19929
|
} finally {
|
|
19865
|
-
|
|
19930
|
+
release2();
|
|
19866
19931
|
}
|
|
19867
19932
|
})();
|
|
19868
19933
|
const applied = [];
|
|
@@ -21242,7 +21307,7 @@ function initializeLocalSecrets(root, application, keyFile) {
|
|
|
21242
21307
|
if (dirname8(current) === current) break;
|
|
21243
21308
|
}
|
|
21244
21309
|
ensureIgnored(root);
|
|
21245
|
-
const
|
|
21310
|
+
const release2 = acquireLocalLease(root, ".backify/dev/secrets.lock");
|
|
21246
21311
|
const key = randomBytes4(32);
|
|
21247
21312
|
try {
|
|
21248
21313
|
if (existsSync7(filename2)) throw failure();
|
|
@@ -21251,7 +21316,7 @@ function initializeLocalSecrets(root, application, keyFile) {
|
|
|
21251
21316
|
writePrivate2(filename2, encrypt(root, application, key, {}));
|
|
21252
21317
|
} finally {
|
|
21253
21318
|
key.fill(0);
|
|
21254
|
-
|
|
21319
|
+
release2();
|
|
21255
21320
|
}
|
|
21256
21321
|
return { schemaVersion: 1, status: "initialized", keyFile: external, names: [] };
|
|
21257
21322
|
}
|
|
@@ -21259,7 +21324,7 @@ function setLocalSecret(root, application, keyFile, name2, value) {
|
|
|
21259
21324
|
if (!secretName.test(name2) || Buffer.byteLength(value) < 1 || Buffer.byteLength(value) > 65536) throw new Error("Local secrets require an uppercase logical name and 1-65536 UTF-8 bytes from stdin");
|
|
21260
21325
|
const filename2 = vaultPath(root);
|
|
21261
21326
|
assertLocalStatePath(root, filename2);
|
|
21262
|
-
const
|
|
21327
|
+
const release2 = acquireLocalLease(root, ".backify/dev/secrets.lock");
|
|
21263
21328
|
let key;
|
|
21264
21329
|
try {
|
|
21265
21330
|
const secrets = readLocalSecrets(root, application, keyFile);
|
|
@@ -21271,7 +21336,7 @@ function setLocalSecret(root, application, keyFile, name2, value) {
|
|
|
21271
21336
|
renameLocalFile(temporary, filename2);
|
|
21272
21337
|
} finally {
|
|
21273
21338
|
key?.fill(0);
|
|
21274
|
-
|
|
21339
|
+
release2();
|
|
21275
21340
|
}
|
|
21276
21341
|
return { schemaVersion: 1, status: "stored", name: name2 };
|
|
21277
21342
|
}
|
|
@@ -22666,7 +22731,7 @@ function apiClient(session, transport = fetch) {
|
|
|
22666
22731
|
return async (path, init) => {
|
|
22667
22732
|
if (!path.startsWith("/") || path.startsWith("//")) throw new Error("Invalid API path.");
|
|
22668
22733
|
if (session.expiresAt && Date.parse(session.expiresAt) <= Date.now()) throw new Error("CLI session expired. Copy a new setup prompt or run backify login.");
|
|
22669
|
-
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 } });
|
|
22734
|
+
const response = await transport(base + path, { ...init, redirect: "error", signal: AbortSignal.timeout(3e4), headers: { "content-type": "application/json", "user-agent": cliUserAgent, "X-Organization-ID": session.organizationId, ...session.token ? { authorization: "Bearer " + session.token } : {}, ...init?.headers } });
|
|
22670
22735
|
if (!response.ok) {
|
|
22671
22736
|
let message = "Backify request failed (" + response.status + ").";
|
|
22672
22737
|
try {
|
|
@@ -22804,8 +22869,8 @@ function localReport(runChecks, root = process.cwd()) {
|
|
|
22804
22869
|
const outputDirectory = ["dist", "build", "out", "."].find((directory2) => existsSync14(join16(root, directory2, "index.html"))) ?? null;
|
|
22805
22870
|
return { framework, sdkVersion: dependencies["@backifydev/sdk"] ?? null, branch, commitSha, dirty, buildCommand: pkg.scripts?.build ? manager + " run build" : null, outputDirectory, hasDockerfile: existsSync14(join16(root, "Dockerfile")), checks };
|
|
22806
22871
|
}
|
|
22807
|
-
function matchesSource(
|
|
22808
|
-
return
|
|
22872
|
+
function matchesSource(release2, commit) {
|
|
22873
|
+
return release2?.commitSha === commit || release2?.sourceCommitSha === commit;
|
|
22809
22874
|
}
|
|
22810
22875
|
function pushSource(appId, commit, branch, destination, root, environment) {
|
|
22811
22876
|
const remoteHead = git2(["ls-remote", "--", destination, "refs/heads/" + branch], root, environment).split(/\s+/)[0];
|
|
@@ -22827,7 +22892,7 @@ async function waitForPreview(call2, prefix, commit, options = {}) {
|
|
|
22827
22892
|
const sleep = options.sleep ?? ((delay) => new Promise((resolve24) => setTimeout(resolve24, delay)));
|
|
22828
22893
|
do {
|
|
22829
22894
|
const deployments = await call2(prefix + "/deployments");
|
|
22830
|
-
const matching = deployments.find((
|
|
22895
|
+
const matching = deployments.find((release2) => release2.kind === "preview" && matchesSource(release2, commit) && !options.previousDeploymentIds?.includes(release2.id));
|
|
22831
22896
|
if (matching?.status === "failed") throw new Error("Deployment of this source failed. Run backify status and inspect its deployment steps.");
|
|
22832
22897
|
const overview = await call2(prefix + "/releases");
|
|
22833
22898
|
if (matchesSource(overview.preview, commit) && overview.preview?.status === "succeeded" && overview.preview.healthResult === "healthy") return overview;
|
|
@@ -22864,7 +22929,7 @@ async function ship(call2, appId, options = {}) {
|
|
|
22864
22929
|
const environment = gitEnvironment(status.repository, credential.token);
|
|
22865
22930
|
const destination = "https://github.com/" + status.repository + ".git";
|
|
22866
22931
|
const alreadyPushed = pushSource(appId, commit, status.branch, destination, root, environment);
|
|
22867
|
-
const currentPreview = previousDeployments.find((
|
|
22932
|
+
const currentPreview = previousDeployments.find((release2) => release2.kind === "preview" && matchesSource(release2, commit));
|
|
22868
22933
|
if (alreadyPushed && (!currentPreview || currentPreview.status === "failed") && !(matchesSource(status.releases.preview, commit) && status.releases.preview?.status === "succeeded")) await call2(prefix + "/deployments/preview", { method: "POST", body: JSON.stringify({ sourceRevision: "latest" }) });
|
|
22869
22934
|
const overview = await waitForPreview(call2, prefix, commit, { timeoutMs: options.timeoutMs, previousDeploymentIds: previousDeployments.filter((deployment) => deployment.status === "failed").map((deployment) => deployment.id), previousBuildRunId: previousBuild.runId ?? null });
|
|
22870
22935
|
if (overview.preview.commitSha !== commit) {
|
|
@@ -23144,14 +23209,20 @@ async function main() {
|
|
|
23144
23209
|
if (code) {
|
|
23145
23210
|
if (!organizationId) throw new Error("The setup prompt must supply the organization.");
|
|
23146
23211
|
const redeem = apiClient({ token: "", organizationId, apiUrl: address });
|
|
23147
|
-
const paired = await redeem("/apps/" + applicationGuid(appId) + "/developer/pair", { method: "POST", body: JSON.stringify({ code }) });
|
|
23212
|
+
const paired = await redeem("/apps/" + applicationGuid(appId) + "/developer/pair", { method: "POST", body: JSON.stringify({ code, device: cliDevice() }) });
|
|
23148
23213
|
userConfig.sessions ??= {};
|
|
23149
23214
|
userConfig.sessions[key] = { ...paired, apiUrl: address };
|
|
23150
23215
|
saveUserConfig(configPath, userConfig);
|
|
23151
23216
|
} else if (!userConfig.sessions?.[key] && !userConfig.token) {
|
|
23152
23217
|
await browserLogin();
|
|
23153
23218
|
}
|
|
23154
|
-
|
|
23219
|
+
let session = userConfig.sessions?.[key] ?? { token: userConfig.token ?? "", organizationId: organizationId ?? userConfig.organizationId ?? "", apiUrl: address };
|
|
23220
|
+
if (!session.token.startsWith("bfcli_")) {
|
|
23221
|
+
session = await registerCliSession(apiClient(session), applicationGuid(appId), session.apiUrl);
|
|
23222
|
+
userConfig.sessions ??= {};
|
|
23223
|
+
userConfig.sessions[key] = session;
|
|
23224
|
+
saveUserConfig(configPath, userConfig);
|
|
23225
|
+
}
|
|
23155
23226
|
const client = apiClient(session);
|
|
23156
23227
|
await client("/apps/" + applicationGuid(appId) + "/developer");
|
|
23157
23228
|
userConfig.sessions ??= {};
|