@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.
Files changed (3) hide show
  1. package/README.md +2 -0
  2. package/dist/index.js +89 -18
  3. 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, platform, wait) {
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 (platform !== "win32" || attempt >= 4 || !(error instanceof Error) || !("code" in error) || !["EPERM", "EACCES", "EBUSY"].includes(String(error.code))) throw error;
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(platform = process.platform, architecture = process.arch) {
17692
- const system = { win32: "win", darwin: "osx", linux: "linux" }[platform];
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 release = acquireLocalLease(root, ".backify/dev/services/lifecycle.lock");
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
- release();
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 release = acquireLocalLease(root, ".backify/dev/secrets.lock");
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
- release();
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 release = acquireLocalLease(root, ".backify/dev/secrets.lock");
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
- release();
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(release, commit) {
22808
- return release?.commitSha === commit || release?.sourceCommitSha === commit;
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((release) => release.kind === "preview" && matchesSource(release, commit) && !options.previousDeploymentIds?.includes(release.id));
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((release) => release.kind === "preview" && matchesSource(release, commit));
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
- const session = userConfig.sessions?.[key] ?? { token: userConfig.token ?? "", organizationId: organizationId ?? userConfig.organizationId ?? "", apiUrl: address };
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 ??= {};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@backifydev/cli",
3
- "version": "0.3.7",
3
+ "version": "0.3.8",
4
4
  "description": "Command-line setup and diagnostics for Backify applications",
5
5
  "type": "module",
6
6
  "bin": {