@tinycloud/cli 1.1.0-beta.7 → 1.1.0-beta.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.
@@ -260,7 +260,7 @@ var init_formatter = __esm({
260
260
  // src/output/errors.ts
261
261
  import { readFileSync, readdirSync } from "fs";
262
262
  import { join as join2 } from "path";
263
- import { ProfileLockTimeoutError } from "@tinycloud/operations/state";
263
+ import { ProfileDeletedError, ProfileLockTimeoutError } from "@tinycloud/operations/state";
264
264
  function setActiveProfileName(name) {
265
265
  activeProfileName = name;
266
266
  }
@@ -286,6 +286,9 @@ function wrapError(error) {
286
286
  { hint: "Wait for the other command to finish and retry. A crashed process's lock is reclaimed automatically after 30 s." }
287
287
  );
288
288
  }
289
+ if (error instanceof ProfileDeletedError) {
290
+ return new CLIError("PROFILE_NOT_FOUND", message);
291
+ }
289
292
  if (message.includes("Not signed in") || message.includes("AUTH_EXPIRED") || message.includes("Session expired")) {
290
293
  return new CLIError("AUTH_REQUIRED", message, ExitCode.AUTH_REQUIRED);
291
294
  }
@@ -644,6 +647,8 @@ import { join as join3 } from "path";
644
647
  import {
645
648
  profilePath,
646
649
  readSession,
650
+ recordProfileDeletion,
651
+ refuseWriteToDeletedProfile,
647
652
  removeSession,
648
653
  withProfileLock,
649
654
  writeSession
@@ -758,10 +763,12 @@ var init_profiles = __esm({
758
763
  * removed while holding the profile lock, so another writer's critical
759
764
  * section never sees them vanish midway: session and key first, settings
760
765
  * last, so a crash midway never leaves a session or key without its
761
- * profile. `.lock` itself is left to the lock's release; the then-empty
762
- * directory is removed afterwards unless another writer took the lock (or
763
- * wrote) meanwhile. A profile directory that is a symlink is unlinked, its
764
- * target left alone.
766
+ * profile. The deletion is then recorded, so a store write that waited for
767
+ * the lock meanwhile refuses rather than recreating a profile with only a
768
+ * session in it. `.lock` itself is left to the lock's release; the
769
+ * then-empty directory is removed afterwards unless another writer took
770
+ * the lock (or wrote) meanwhile. A profile directory that is a symlink is
771
+ * unlinked, its target left alone.
765
772
  * Throws if the name is not one path segment or names the default profile.
766
773
  */
767
774
  static async deleteProfile(name) {
@@ -782,11 +789,12 @@ var init_profiles = __esm({
782
789
  `Cannot delete the default profile "${name}". Change the default first with \`tc profile default <other>\`.`
783
790
  );
784
791
  }
785
- const isLink = await lstat(profileDir).then((stats) => stats.isSymbolicLink(), (error) => {
786
- if (error.code === "ENOENT") return false;
792
+ const kind = await lstat(profileDir).then((stats) => stats.isSymbolicLink() ? "link" : "present", (error) => {
793
+ if (error.code === "ENOENT") return "missing";
787
794
  throw error;
788
795
  });
789
- if (isLink) {
796
+ if (kind === "missing") return;
797
+ if (kind === "link") {
790
798
  await rm2(profileDir, { force: true });
791
799
  return;
792
800
  }
@@ -796,6 +804,7 @@ var init_profiles = __esm({
796
804
  if (entry !== ".lock" && entry !== "profile.json") await rm2(join3(profileDir, entry), { recursive: true, force: true });
797
805
  }
798
806
  await rm2(join3(profileDir, "profile.json"), { force: true });
807
+ await recordProfileDeletion(name);
799
808
  });
800
809
  await rmdir(profileDir).catch((error) => {
801
810
  if (error.code !== "ENOENT" && error.code !== "ENOTEMPTY" && error.code !== "EEXIST") throw error;
@@ -808,9 +817,14 @@ var init_profiles = __esm({
808
817
  static async getKey(name) {
809
818
  return readJson(join3(PROFILES_DIR, name, "key.json"));
810
819
  }
811
- /** Saves a JWK key under the profile lock (0600, in an owner-only profile directory). */
820
+ /**
821
+ * Saves a JWK key under the profile lock (0600, in an owner-only profile
822
+ * directory). Refused (PROFILE_NOT_FOUND) if the profile was deleted while
823
+ * this waited for the lock, rather than leaving a key-only profile.
824
+ */
812
825
  static async setKey(name, jwk) {
813
826
  await _ProfileManager.withLock(name, async () => {
827
+ await refuseWriteToDeletedProfile(name);
814
828
  await writeJson(join3(await _ProfileManager.ensureProfileDir(name), "key.json"), jwk);
815
829
  });
816
830
  }
@@ -1779,6 +1793,7 @@ import {
1779
1793
  profileStoreMetadataPath,
1780
1794
  readAdditionalDelegations,
1781
1795
  readAuthRequests,
1796
+ refuseWriteToDeletedProfile as refuseWriteToDeletedProfile2,
1782
1797
  updateProfileStore,
1783
1798
  withProfileLock as withProfileLock2,
1784
1799
  writeJsonAtomic
@@ -13146,6 +13161,7 @@ async function appendPermissionRequestArtifact(profile, artifact) {
13146
13161
  });
13147
13162
  }
13148
13163
  async function writeSharedRecords(profile, store, entries) {
13164
+ await refuseWriteToDeletedProfile2(profile);
13149
13165
  const path = store === "additional-delegations" ? additionalDelegationsPath(profile) : permissionRequestsPath(profile);
13150
13166
  await writeJsonAtomic(path, entries);
13151
13167
  await writeJsonAtomic(profileStoreMetadataPath(profile, store), { formatVersion: 1 });
@@ -13556,6 +13572,7 @@ async function createSDKInstance(ctx, options) {
13556
13572
  });
13557
13573
  return node;
13558
13574
  }
13575
+ var BOOTSTRAP_PROFILE_FIELDS = ["sessionDid", "spaceId"];
13559
13576
  async function bootstrapDelegatedSession(ctx, delegation) {
13560
13577
  const written = await ProfileManager.withLock(ctx.profile, async () => {
13561
13578
  const previousProfile = await ProfileManager.getProfile(ctx.profile);
@@ -13605,13 +13622,13 @@ async function bootstrapDelegatedSession(ctx, delegation) {
13605
13622
  throw error;
13606
13623
  });
13607
13624
  const session = await ProfileManager.getSession(ctx.profile);
13608
- if (JSON.stringify(profile) !== JSON.stringify(written.profile) || JSON.stringify(session) !== JSON.stringify(written.session)) {
13625
+ if (profile === null || BOOTSTRAP_PROFILE_FIELDS.some((field) => profile[field] !== written.profile[field]) || JSON.stringify(session) !== JSON.stringify(written.session)) {
13609
13626
  return `Profile "${ctx.profile}" changed while the import was pending (another login, logout or profile update), so its newer state was kept and the provisional session was not rolled back.`;
13610
13627
  }
13611
13628
  return restoreBeforeBootstrap(ctx.profile, written.previousProfile);
13612
13629
  }, { timeoutMs: PROFILE_COMMIT_LOCK_TIMEOUT_MS }).catch((error) => (
13613
13630
  // The lock or a read failed: the import's error stays the one reported.
13614
- `Rolling back the provisional session of profile "${ctx.profile}" could not run (${error instanceof Error ? error.message : String(error)}); check \`tc --profile ${ctx.profile} context\`.`
13631
+ `Rolling back the provisional session of profile "${ctx.profile}" could not run (${failureName(error)}); check \`tc --profile ${ctx.profile} context\`.`
13615
13632
  ));
13616
13633
  throw annotate(cause, note);
13617
13634
  };
@@ -13627,15 +13644,27 @@ async function restoreBeforeBootstrap(profileName, previousProfile) {
13627
13644
  const failures = [];
13628
13645
  for (const write of [
13629
13646
  () => ProfileManager.clearSession(profileName),
13630
- () => ProfileManager.setProfile(profileName, previousProfile)
13647
+ () => ProfileManager.updateProfile(profileName, (current) => {
13648
+ const restored = { ...current };
13649
+ for (const field of BOOTSTRAP_PROFILE_FIELDS) {
13650
+ if (previousProfile[field] === void 0) delete restored[field];
13651
+ else restored[field] = previousProfile[field];
13652
+ }
13653
+ return restored;
13654
+ })
13631
13655
  ]) {
13632
13656
  await write().catch((error) => {
13633
- failures.push(error instanceof Error ? error.message : String(error));
13657
+ failures.push(failureName(error));
13634
13658
  });
13635
13659
  }
13636
13660
  if (failures.length === 0) return void 0;
13637
13661
  return `Rolling back the provisional session of profile "${profileName}" failed too (${failures.join("; ")}); check \`tc --profile ${profileName} context\`.`;
13638
13662
  }
13663
+ function failureName(error) {
13664
+ if (error instanceof SyntaxError) return "a profile file is not valid JSON";
13665
+ if (typeof error === "object" && error !== null && "code" in error && typeof error.code === "string") return error.code;
13666
+ return error instanceof Error ? error.name : "unknown error";
13667
+ }
13639
13668
  function annotate(error, note) {
13640
13669
  if (note === void 0) return error;
13641
13670
  const cause = wrapError(error);