@superblocksteam/sdk 2.0.93-next.7 → 2.0.94-next.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (88) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/dist/application-build.d.mts.map +1 -1
  3. package/dist/application-build.mjs +2 -0
  4. package/dist/application-build.mjs.map +1 -1
  5. package/dist/cli-replacement/assets/git-workflows/github-superblocks-sync-workflow.d.ts +5 -0
  6. package/dist/cli-replacement/assets/git-workflows/github-superblocks-sync-workflow.d.ts.map +1 -0
  7. package/dist/cli-replacement/assets/git-workflows/github-superblocks-sync-workflow.js +61 -0
  8. package/dist/cli-replacement/assets/git-workflows/github-superblocks-sync-workflow.js.map +1 -0
  9. package/dist/cli-replacement/assets/git-workflows/github-superblocks-sync-workflow.test.d.ts +2 -0
  10. package/dist/cli-replacement/assets/git-workflows/github-superblocks-sync-workflow.test.d.ts.map +1 -0
  11. package/dist/cli-replacement/assets/git-workflows/github-superblocks-sync-workflow.test.js +32 -0
  12. package/dist/cli-replacement/assets/git-workflows/github-superblocks-sync-workflow.test.js.map +1 -0
  13. package/dist/cli-replacement/dev.d.mts.map +1 -1
  14. package/dist/cli-replacement/dev.mjs +350 -16
  15. package/dist/cli-replacement/dev.mjs.map +1 -1
  16. package/dist/client-sync.test.d.mts +2 -0
  17. package/dist/client-sync.test.d.mts.map +1 -0
  18. package/dist/client-sync.test.mjs +57 -0
  19. package/dist/client-sync.test.mjs.map +1 -0
  20. package/dist/client.d.ts +22 -0
  21. package/dist/client.d.ts.map +1 -1
  22. package/dist/client.js +50 -0
  23. package/dist/client.js.map +1 -1
  24. package/dist/collect-sdk-apis.d.mts +18 -0
  25. package/dist/collect-sdk-apis.d.mts.map +1 -0
  26. package/dist/collect-sdk-apis.mjs +26 -0
  27. package/dist/collect-sdk-apis.mjs.map +1 -0
  28. package/dist/collect-sdk-apis.test.d.mts +2 -0
  29. package/dist/collect-sdk-apis.test.d.mts.map +1 -0
  30. package/dist/collect-sdk-apis.test.mjs +72 -0
  31. package/dist/collect-sdk-apis.test.mjs.map +1 -0
  32. package/dist/dbfs/client.d.ts +1 -0
  33. package/dist/dbfs/client.d.ts.map +1 -1
  34. package/dist/dbfs/client.js +12 -0
  35. package/dist/dbfs/client.js.map +1 -1
  36. package/dist/dev-utils/dev-server.d.mts +4 -1
  37. package/dist/dev-utils/dev-server.d.mts.map +1 -1
  38. package/dist/dev-utils/dev-server.mjs +14 -6
  39. package/dist/dev-utils/dev-server.mjs.map +1 -1
  40. package/dist/dev-utils/vite-plugin-build-manifest-stub.mjs +1 -1
  41. package/dist/dev-utils/vite-plugin-build-manifest-stub.mjs.map +1 -1
  42. package/dist/parse-sdk-registry.d.mts +20 -0
  43. package/dist/parse-sdk-registry.d.mts.map +1 -0
  44. package/dist/parse-sdk-registry.mjs +161 -0
  45. package/dist/parse-sdk-registry.mjs.map +1 -0
  46. package/dist/parse-sdk-registry.test.d.mts +2 -0
  47. package/dist/parse-sdk-registry.test.d.mts.map +1 -0
  48. package/dist/parse-sdk-registry.test.mjs +83 -0
  49. package/dist/parse-sdk-registry.test.mjs.map +1 -0
  50. package/dist/sdk.d.ts +10 -1
  51. package/dist/sdk.d.ts.map +1 -1
  52. package/dist/sdk.js +14 -2
  53. package/dist/sdk.js.map +1 -1
  54. package/dist/vite-plugin-generate-api-build-manifest.d.mts.map +1 -1
  55. package/dist/vite-plugin-generate-api-build-manifest.mjs +13 -1
  56. package/dist/vite-plugin-generate-api-build-manifest.mjs.map +1 -1
  57. package/dist/vite-plugin-sdk-api-entry-point.d.mts +10 -0
  58. package/dist/vite-plugin-sdk-api-entry-point.d.mts.map +1 -0
  59. package/dist/vite-plugin-sdk-api-entry-point.mjs +63 -0
  60. package/dist/vite-plugin-sdk-api-entry-point.mjs.map +1 -0
  61. package/dist/vite-plugin-sdk-api-entry-point.test.d.mts +2 -0
  62. package/dist/vite-plugin-sdk-api-entry-point.test.d.mts.map +1 -0
  63. package/dist/vite-plugin-sdk-api-entry-point.test.mjs +166 -0
  64. package/dist/vite-plugin-sdk-api-entry-point.test.mjs.map +1 -0
  65. package/eslint.config.js +1 -1
  66. package/lint-staged.config.mjs +2 -1
  67. package/package.json +19 -21
  68. package/src/application-build.mts +2 -0
  69. package/src/cli-replacement/assets/git-workflows/github-superblocks-sync-workflow.test.ts +60 -0
  70. package/src/cli-replacement/assets/git-workflows/github-superblocks-sync-workflow.ts +70 -0
  71. package/src/cli-replacement/dev.mts +528 -18
  72. package/src/client-sync.test.mts +66 -0
  73. package/src/client.ts +102 -0
  74. package/src/collect-sdk-apis.mts +43 -0
  75. package/src/collect-sdk-apis.test.mts +91 -0
  76. package/src/dbfs/client.ts +16 -0
  77. package/src/dev-utils/dev-server.mts +25 -4
  78. package/src/dev-utils/vite-plugin-build-manifest-stub.mts +1 -1
  79. package/src/parse-sdk-registry.mts +227 -0
  80. package/src/parse-sdk-registry.test.mts +133 -0
  81. package/src/sdk.ts +52 -0
  82. package/src/vite-plugin-generate-api-build-manifest.mts +17 -1
  83. package/src/vite-plugin-sdk-api-entry-point.mts +76 -0
  84. package/src/vite-plugin-sdk-api-entry-point.test.mts +266 -0
  85. package/tsconfig.json +2 -2
  86. package/tsconfig.tsbuildinfo +1 -1
  87. package/turbo.json +3 -8
  88. package/.prettierrc +0 -18
@@ -1,10 +1,14 @@
1
1
  import * as child_process from "node:child_process";
2
+ import * as nodeFs from "node:fs/promises";
2
3
  import path from "node:path";
3
4
  import * as readline from "node:readline";
4
5
  import { promisify } from "node:util";
5
6
  import { SpanStatusCode } from "@opentelemetry/api";
6
-
7
- import { ConflictError, NotFoundError } from "@superblocksteam/shared";
7
+ import {
8
+ ConflictError,
9
+ NotFoundError,
10
+ SUPERBLOCKS_LIVE_GIT_BRANCH,
11
+ } from "@superblocksteam/shared";
8
12
  import { maskUnixSignals } from "@superblocksteam/util";
9
13
  import {
10
14
  AiService,
@@ -12,6 +16,7 @@ import {
12
16
  SnapshotManager,
13
17
  isSdkApiTemplate,
14
18
  } from "@superblocksteam/vite-plugin-file-sync/ai-service";
19
+ import { createGitService } from "@superblocksteam/vite-plugin-file-sync/git-service";
15
20
  import {
16
21
  LockService,
17
22
  LockType,
@@ -24,9 +29,11 @@ import fs from "fs-extra";
24
29
  import { resolveCommand } from "package-manager-detector";
25
30
  import { detect } from "package-manager-detector/detect";
26
31
 
27
- import { AUTO_UPGRADE_EXIT_CODE, createDevServer } from "../index.js";
32
+ import { createDevServer } from "../dev-utils/dev-server.mjs";
33
+ import { AUTO_UPGRADE_EXIT_CODE } from "../index.js";
28
34
  import { getTracer } from "../telemetry/index.js";
29
35
  import { getErrorMeta, getLogger, type Logger } from "../telemetry/logging.js";
36
+ import { buildGithubSuperblocksSyncWorkflow } from "./assets/git-workflows/github-superblocks-sync-workflow.js";
30
37
  import { checkVersionsAndWritePackageJson } from "./automatic-upgrades.js";
31
38
  import { getCurrentCliVersion } from "./version-detection.js";
32
39
  import type {
@@ -41,9 +48,9 @@ import type {
41
48
  TokenConfig,
42
49
  } from "../types/index.js";
43
50
  import type { DraftInterface } from "@superblocksteam/vite-plugin-file-sync/draft-interface";
51
+ import type { GitService } from "@superblocksteam/vite-plugin-file-sync/git-service";
44
52
 
45
53
  const exec = promisify(child_process.exec);
46
-
47
54
  const passErrorToVSCode = (message: string | undefined, logger: Logger) => {
48
55
  if (message && process.env.SUPERBLOCKS_VSCODE === "true") {
49
56
  // Prefixing with `clierr:` will make the VS code extension capture this message and show it to the user.
@@ -287,7 +294,10 @@ export async function dev(options: {
287
294
  let lockService: LockService | undefined;
288
295
  let syncService: SyncService | undefined;
289
296
  let aiService: AiService | undefined;
297
+ let gitService: GitService | undefined;
290
298
  let snapshotManager: SnapshotManager | undefined;
299
+ let gitUserName: string | undefined;
300
+ let gitUserEmail: string | undefined;
291
301
  const tracer = getTracer();
292
302
  const logger = getLogger(options.logger);
293
303
  const skipAutoUpgrade = autoUpgradeMode === DevServerAutoUpgradeMode.SKIP;
@@ -318,6 +328,7 @@ export async function dev(options: {
318
328
  const port = devServerPort ?? 5173;
319
329
 
320
330
  const fsOperationQueue = new OperationQueue();
331
+ let activeDbfsBranchName = applicationConfig?.branchName ?? "main";
321
332
  if (applicationConfig && !skipSync) {
322
333
  const rpcClient = new AutoConnectingRpcClient(
323
334
  tokenConfig.superblocksBaseUrl,
@@ -346,11 +357,19 @@ export async function dev(options: {
346
357
  lockType === LockType.CSB
347
358
  ? featureFlags.devServerCloudInactivityTimeoutMinutes() * 60 * 1000
348
359
  : featureFlags.devServerLocalInactivityTimeoutMinutes() * 60 * 1000;
360
+ activeDbfsBranchName = await resolveDbfsBranchName(
361
+ sdk,
362
+ applicationConfig,
363
+ logger,
364
+ );
365
+ logger.info(
366
+ `Using DBFS branch '${activeDbfsBranchName}' for live edit sync`,
367
+ );
349
368
 
350
369
  lockService = new LockService({
351
370
  superblocksBaseUrl: tokenConfig.superblocksBaseUrl,
352
371
  applicationId: applicationConfig.id,
353
- branchName: applicationConfig.branchName,
372
+ branchName: activeDbfsBranchName,
354
373
  lockType: lockType,
355
374
  tracer,
356
375
  logger,
@@ -361,7 +380,7 @@ export async function dev(options: {
361
380
  syncService = new SyncService({
362
381
  appRootDirPath: cwd,
363
382
  applicationId: applicationConfig.id,
364
- branchName: applicationConfig.branchName,
383
+ branchName: activeDbfsBranchName,
365
384
  fsOperationQueue,
366
385
  lockService: lockService,
367
386
  tracer,
@@ -427,7 +446,12 @@ export async function dev(options: {
427
446
  try {
428
447
  const results = await Promise.all([
429
448
  sdk.hashLocalDirectory(cwd),
430
- getDraftOrLiveEditHash(sdk, applicationConfig, logger),
449
+ getDraftOrLiveEditHash(
450
+ sdk,
451
+ applicationConfig.id,
452
+ activeDbfsBranchName,
453
+ logger,
454
+ ),
431
455
  sdk.fetchCurrentUser(),
432
456
  ]);
433
457
  return results;
@@ -436,6 +460,9 @@ export async function dev(options: {
436
460
  }
437
461
  });
438
462
 
463
+ gitUserName = currentUser.user.name;
464
+ gitUserEmail = currentUser.user.email;
465
+
439
466
  snapshotManager = new SnapshotManager(currentUser.user.id);
440
467
  const didSnapshotRestore =
441
468
  await snapshotManager.executePendingRestore(cwd);
@@ -457,6 +484,13 @@ export async function dev(options: {
457
484
  | Record<string, unknown>
458
485
  | undefined),
459
486
  "clark.sdk-api.enabled": sdkApiEnabled,
487
+ "superblocks.native-git.enabled":
488
+ sdkApiEnabled &&
489
+ !!(
490
+ currentUser.flagBootstrap as
491
+ | Record<string, unknown>
492
+ | undefined
493
+ )?.["superblocks.native-git.enabled"],
460
494
  };
461
495
 
462
496
  aiService = new AiService({
@@ -490,11 +524,11 @@ export async function dev(options: {
490
524
 
491
525
  if (isSynced) {
492
526
  logger.info(
493
- `Local files are in sync with the server on branch '${applicationConfig.branchName}', local hash: ${localContents.hash}, server hash: ${serverHash}`,
527
+ `Local files are in sync with the server on branch '${activeDbfsBranchName}', local hash: ${localContents.hash}, server hash: ${serverHash}`,
494
528
  );
495
529
  } else {
496
530
  logger.info(
497
- `Local files are out of sync with the server on branch '${applicationConfig.branchName}', local hash: ${localContents.hash}, server hash: ${serverHash}`,
531
+ `Local files are out of sync with the server on branch '${activeDbfsBranchName}', local hash: ${localContents.hash}, server hash: ${serverHash}`,
498
532
  );
499
533
  }
500
534
 
@@ -516,7 +550,7 @@ export async function dev(options: {
516
550
  if (downloadFirst && !isSynced) {
517
551
  await tracer.startActiveSpan("downloadFirst", async (span) => {
518
552
  logger.info(
519
- `Starting download of branch '${applicationConfig.branchName}'`,
553
+ `Starting download of branch '${activeDbfsBranchName}'`,
520
554
  );
521
555
 
522
556
  await syncService!.downloadDirectory();
@@ -524,6 +558,76 @@ export async function dev(options: {
524
558
  });
525
559
  }
526
560
 
561
+ // Git sync step — runs after DBFS download, before version check.
562
+ // Best-effort: never blocks startup on failure.
563
+ await tracer.startActiveSpan("gitSync", async (gitSpan) => {
564
+ try {
565
+ const bootstrappedGitService = await bootstrapGitService({
566
+ sdk,
567
+ applicationId: applicationConfig.id,
568
+ cwd,
569
+ logger,
570
+ userName: gitUserName,
571
+ userEmail: gitUserEmail,
572
+ });
573
+
574
+ if (!bootstrappedGitService) {
575
+ logger.info("No git remote configured, skipping git sync");
576
+ gitSpan.end();
577
+ return;
578
+ }
579
+
580
+ gitService = bootstrappedGitService;
581
+
582
+ // At this point the local repo is usable for status(),
583
+ // so gitService stays set even if the remote sync below
584
+ // fails. The footer will show uncommitted changes and
585
+ // the user can fix their PAT if push/pull don't work.
586
+ // Also ensure lock/sync branch context is reconciled now,
587
+ // so startup does not remain pinned to main if git config
588
+ // became visible after initial branch resolution.
589
+ activeDbfsBranchName = await ensureRuntimeDbfsBranchConsistency(
590
+ {
591
+ sdk,
592
+ applicationConfig,
593
+ logger,
594
+ lockService,
595
+ syncService,
596
+ currentBranchName: activeDbfsBranchName,
597
+ },
598
+ );
599
+
600
+ // 3. Fetch remote — failures are non-fatal.
601
+ // We don't auto-merge here; the user pulls from the editor
602
+ // when ready, and Clark handles any conflicts.
603
+ try {
604
+ await fetchAndEnsureLiveBranch(
605
+ gitService,
606
+ "Git remote sync failed (local repo still usable)",
607
+ );
608
+ } catch (syncError) {
609
+ logger.warn(
610
+ `Git remote sync failed (local repo still usable): ${syncError instanceof Error ? syncError.message : String(syncError)}`,
611
+ );
612
+ }
613
+ } catch (gitError) {
614
+ // Init/configure/ensureGitRepo failed — repo is not usable
615
+ gitService = undefined;
616
+ logger.warn(
617
+ `Git setup failed, continuing without git: ${gitError instanceof Error ? gitError.message : String(gitError)}`,
618
+ );
619
+ gitSpan.setStatus({
620
+ code: SpanStatusCode.ERROR,
621
+ message:
622
+ gitError instanceof Error
623
+ ? gitError.message
624
+ : String(gitError),
625
+ });
626
+ } finally {
627
+ gitSpan.end();
628
+ }
629
+ });
630
+
527
631
  let hasCliUpdated = false;
528
632
  let upgradePromises: Promise<void>[] = [];
529
633
  const forceUpgrade =
@@ -594,7 +698,7 @@ export async function dev(options: {
594
698
 
595
699
  if (hasPackageChanged || uploadFirst) {
596
700
  logger.info(
597
- `Uploading local files to branch '${applicationConfig.branchName}' on server before starting`,
701
+ `Uploading local files to branch '${activeDbfsBranchName}' on server before starting`,
598
702
  );
599
703
  await tracer.startActiveSpan(
600
704
  "uploadFirstOrPackageChanged",
@@ -635,10 +739,78 @@ export async function dev(options: {
635
739
  logger.info("Skipping directory sync");
636
740
  }
637
741
 
742
+ const activateRuntimeGitService = async (): Promise<
743
+ GitService | undefined
744
+ > => {
745
+ if (gitService) {
746
+ const hasGit = await nodeFs.access(path.join(cwd, ".git")).then(
747
+ () => true,
748
+ () => false,
749
+ );
750
+ if (!hasGit) {
751
+ logger.warn(
752
+ "[git] activateRuntimeGitService: .git directory disappeared, clearing cached git service",
753
+ );
754
+ gitService = undefined;
755
+ return undefined;
756
+ }
757
+
758
+ activeDbfsBranchName = await ensureRuntimeDbfsBranchConsistency({
759
+ sdk,
760
+ applicationConfig,
761
+ logger,
762
+ lockService,
763
+ syncService,
764
+ currentBranchName: activeDbfsBranchName,
765
+ });
766
+ return gitService;
767
+ }
768
+
769
+ try {
770
+ const runtimeGitService = await bootstrapGitService({
771
+ sdk,
772
+ applicationId: applicationConfig.id,
773
+ cwd,
774
+ logger,
775
+ userName: gitUserName,
776
+ userEmail: gitUserEmail,
777
+ });
778
+ if (!runtimeGitService) {
779
+ return undefined;
780
+ }
781
+
782
+ try {
783
+ await fetchAndEnsureLiveBranch(
784
+ runtimeGitService,
785
+ "Git runtime bootstrap fetch failed",
786
+ );
787
+ } catch {
788
+ // fetchAndEnsureLiveBranch already logs the failure
789
+ }
790
+
791
+ activeDbfsBranchName = await ensureRuntimeDbfsBranchConsistency({
792
+ sdk,
793
+ applicationConfig,
794
+ logger,
795
+ lockService,
796
+ syncService,
797
+ currentBranchName: activeDbfsBranchName,
798
+ });
799
+
800
+ gitService = runtimeGitService;
801
+ return gitService;
802
+ } catch (error) {
803
+ logger.warn(
804
+ `Git runtime bootstrap failed: ${error instanceof Error ? error.message : String(error)}`,
805
+ );
806
+ return undefined;
807
+ }
808
+ };
809
+
638
810
  const httpServer = await tracer.startActiveSpan(
639
811
  "createDevServer",
640
812
  async (span) => {
641
- const result = await createDevServer({
813
+ const createDevServerOptions = {
642
814
  root: options.cwd,
643
815
  mode: "development",
644
816
  port: port,
@@ -646,10 +818,12 @@ export async function dev(options: {
646
818
  syncService: syncService,
647
819
  lockService: lockService,
648
820
  aiService: aiService,
821
+ activateGitService: activateRuntimeGitService,
649
822
  snapshotManager: snapshotManager,
650
823
  logger: options.logger,
651
824
  sdk,
652
- });
825
+ } as unknown as Parameters<typeof createDevServer>[0];
826
+ const result = await createDevServer(createDevServerOptions);
653
827
  span.end();
654
828
  return result;
655
829
  },
@@ -686,22 +860,253 @@ export async function dev(options: {
686
860
  }
687
861
  });
688
862
  }
863
+ // ---------------------------------------------------------------------------
864
+ // Git sync helpers
865
+ // ---------------------------------------------------------------------------
866
+
867
+ async function bootstrapGitService({
868
+ sdk,
869
+ applicationId,
870
+ cwd,
871
+ logger,
872
+ userName,
873
+ userEmail,
874
+ }: {
875
+ sdk: SuperblocksSdk;
876
+ applicationId: string;
877
+ cwd: string;
878
+ logger: Logger;
879
+ userName?: string;
880
+ userEmail?: string;
881
+ }): Promise<GitService | undefined> {
882
+ interface GitConfigSdk {
883
+ getApplicationGitConfig(applicationId: string): Promise<{
884
+ gitRemoteUrl?: string;
885
+ hasCredential?: boolean;
886
+ } | null>;
887
+ getGitFreshToken(
888
+ host: string,
889
+ applicationId: string,
890
+ ): Promise<{ username: string; token: string }>;
891
+ }
892
+ const gitConfigSdk = sdk as unknown as GitConfigSdk;
893
+ const gitConfig = await gitConfigSdk.getApplicationGitConfig(applicationId);
894
+ if (!gitConfig?.gitRemoteUrl) {
895
+ return undefined;
896
+ }
897
+
898
+ const service = createGitService(cwd);
899
+ let credentials: { username: string; token: string } | undefined;
900
+
901
+ if (gitConfig.hasCredential) {
902
+ try {
903
+ const host = new URL(gitConfig.gitRemoteUrl).hostname;
904
+ const freshToken = await gitConfigSdk.getGitFreshToken(
905
+ host,
906
+ applicationId,
907
+ );
908
+ if (freshToken) {
909
+ credentials = freshToken;
910
+ }
911
+ } catch {
912
+ logger.warn("Could not fetch git credentials, continuing without auth");
913
+ }
914
+ }
915
+
916
+ await service.configure({ credentials, userName, userEmail });
917
+ await ensureGitRepo(service, gitConfig.gitRemoteUrl);
918
+
919
+ return service;
920
+ }
921
+
922
+ async function fetchAndEnsureLiveBranch(
923
+ git: GitService,
924
+ errorPrefix: string,
925
+ ): Promise<void> {
926
+ let fetchError: unknown;
927
+ try {
928
+ await git.fetch();
929
+ } catch (error) {
930
+ fetchError = error;
931
+ getLogger().warn(
932
+ `${errorPrefix}: ${error instanceof Error ? error.message : String(error)}`,
933
+ );
934
+ }
935
+ // Always ensure the local superblocks/live branch exists,
936
+ // even if fetch failed (remote may not be reachable yet).
937
+ await ensureLiveBranchCheckedOutAfterFetch(git);
938
+ if (fetchError) {
939
+ throw fetchError;
940
+ }
941
+ }
942
+
943
+ async function ensureGitRepo(
944
+ git: GitService,
945
+ remoteUrl: string,
946
+ ): Promise<void> {
947
+ const hasGit = await nodeFs.access(path.join(git.workDir, ".git")).then(
948
+ () => true,
949
+ () => false,
950
+ );
951
+
952
+ if (!hasGit) {
953
+ await git.init();
954
+ await git.addRemote("origin", remoteUrl);
955
+
956
+ const remoteRefs = await git
957
+ .raw([
958
+ "ls-remote",
959
+ "--heads",
960
+ "origin",
961
+ `refs/heads/${SUPERBLOCKS_LIVE_GIT_BRANCH}`,
962
+ ])
963
+ .catch((err) => {
964
+ getLogger().warn(
965
+ `[git] ensureGitRepo: ls-remote failed (remote may be unreachable): ${err instanceof Error ? err.message : String(err)}`,
966
+ );
967
+ return "";
968
+ });
969
+
970
+ if (!remoteRefs.includes(`refs/heads/${SUPERBLOCKS_LIVE_GIT_BRANCH}`)) {
971
+ // No superblocks/live on remote — keep existing bootstrap flow.
972
+ await seedGithubWorkflowIfNeeded(git.workDir, remoteUrl);
973
+ await git.raw(["commit", "--allow-empty", "-m", "Initial commit"]);
974
+ const currentBranch = (
975
+ await git.raw(["branch", "--show-current"])
976
+ ).trim();
977
+
978
+ await git.push("origin", currentBranch);
979
+
980
+ // Additional alignment step: base live branch off remote default branch tip.
981
+ const defaultBranch = await git.getDefaultBranch();
982
+ await git.raw(["fetch", "origin", defaultBranch]).catch(() => "");
983
+ const preferredStartPoint = `origin/${defaultBranch}`;
984
+ const fallbackStartPoint = `origin/${currentBranch}`;
985
+ const liveStartPoint = (await canResolveRef(git, preferredStartPoint))
986
+ ? preferredStartPoint
987
+ : fallbackStartPoint;
988
+
989
+ await git.checkoutOrCreate(SUPERBLOCKS_LIVE_GIT_BRANCH, liveStartPoint);
990
+ } else {
991
+ // Remote already has superblocks/live — fetch and set up local branch.
992
+ await git.raw(["fetch", "origin"]);
993
+ await git.raw([
994
+ "symbolic-ref",
995
+ "HEAD",
996
+ `refs/heads/${SUPERBLOCKS_LIVE_GIT_BRANCH}`,
997
+ ]);
998
+ await git.raw(["reset", `origin/${SUPERBLOCKS_LIVE_GIT_BRANCH}`]);
999
+ }
1000
+ } else {
1001
+ // If remote URL changed, update it
1002
+ try {
1003
+ const remotes = await git.getRemotes(true);
1004
+ const origin = remotes.find((r) => r.name === "origin");
1005
+ if (origin && origin.refs.fetch !== remoteUrl) {
1006
+ getLogger().warn(
1007
+ `[git] ensureGitRepo: remote URL changed from "${origin.refs.fetch}" to "${remoteUrl}", updating`,
1008
+ );
1009
+ await git.remote(["set-url", "origin", remoteUrl]);
1010
+ } else if (!origin) {
1011
+ getLogger().warn("[git] ensureGitRepo: no origin remote found, adding");
1012
+ await git.addRemote("origin", remoteUrl);
1013
+ }
1014
+ } catch (remoteErr) {
1015
+ getLogger().warn(
1016
+ `[git] ensureGitRepo: failed to inspect/update remotes, attempting fallback: ${remoteErr instanceof Error ? remoteErr.message : String(remoteErr)}`,
1017
+ );
1018
+ try {
1019
+ await git.addRemote("origin", remoteUrl);
1020
+ } catch {
1021
+ await git.remote(["set-url", "origin", remoteUrl]);
1022
+ }
1023
+ }
1024
+ }
1025
+
1026
+ // Ensure GitHub workflow exists for native git flows even when reusing an
1027
+ // existing local repo (hasGit=true), so users don't miss .github bootstrap.
1028
+ await seedGithubWorkflowIfNeeded(git.workDir, remoteUrl);
1029
+ }
1030
+
1031
+ function isGithubRemote(remoteUrl: string): boolean {
1032
+ return remoteUrl.includes("github.com");
1033
+ }
1034
+
1035
+ async function seedGithubWorkflowIfNeeded(
1036
+ workDir: string,
1037
+ remoteUrl: string,
1038
+ ): Promise<void> {
1039
+ if (!isGithubRemote(remoteUrl)) {
1040
+ return;
1041
+ }
1042
+
1043
+ const workflowDir = path.join(workDir, ".github", "workflows");
1044
+ const workflowFile = path.join(workflowDir, "superblocks-sync.yml");
1045
+ const alreadyExists = await nodeFs
1046
+ .access(workflowFile)
1047
+ .then(() => true)
1048
+ .catch(() => false);
1049
+ if (alreadyExists) {
1050
+ return;
1051
+ }
1052
+
1053
+ await nodeFs.mkdir(workflowDir, { recursive: true });
1054
+ await nodeFs.writeFile(
1055
+ workflowFile,
1056
+ buildGithubSuperblocksSyncWorkflow(),
1057
+ "utf-8",
1058
+ );
1059
+ }
1060
+
1061
+ async function canResolveRef(git: GitService, ref: string): Promise<boolean> {
1062
+ try {
1063
+ await git.revparse(ref);
1064
+ return true;
1065
+ } catch {
1066
+ return false;
1067
+ }
1068
+ }
1069
+
1070
+ async function ensureLiveBranchCheckedOutAfterFetch(
1071
+ git: GitService,
1072
+ ): Promise<void> {
1073
+ // If remote live branch exists, initialize/switch local live branch from it.
1074
+ // This handles fresh repos where HEAD is unborn before first fetch.
1075
+ if (await canResolveRef(git, `origin/${SUPERBLOCKS_LIVE_GIT_BRANCH}`)) {
1076
+ await git.checkoutOrCreate(
1077
+ SUPERBLOCKS_LIVE_GIT_BRANCH,
1078
+ `origin/${SUPERBLOCKS_LIVE_GIT_BRANCH}`,
1079
+ );
1080
+ return;
1081
+ }
1082
+
1083
+ // No remote live branch yet — keep working on local superblocks/live.
1084
+ const branch = await git
1085
+ .raw(["branch", "--show-current"])
1086
+ .then((b) => b.trim())
1087
+ .catch(() => "");
1088
+ if (branch !== SUPERBLOCKS_LIVE_GIT_BRANCH) {
1089
+ await git.checkoutOrCreate(SUPERBLOCKS_LIVE_GIT_BRANCH);
1090
+ }
1091
+ }
1092
+
689
1093
  async function getDraftOrLiveEditHash(
690
1094
  sdk: SuperblocksSdk,
691
- applicationConfig: ApplicationConfig,
1095
+ applicationId: string,
1096
+ branchName: string,
692
1097
  logger: Logger,
693
1098
  ): Promise<string> {
694
1099
  try {
695
1100
  const draftHash = await sdk.dbfsGetApplicationDirectoryHash({
696
- applicationId: applicationConfig.id,
697
- branch: applicationConfig.branchName,
1101
+ applicationId,
1102
+ branch: branchName,
698
1103
  });
699
1104
  return draftHash;
700
1105
  } catch (e: unknown) {
701
1106
  if (e instanceof NotFoundError) {
702
1107
  const liveEditHash = await sdk.dbfsGetApplicationDirectoryHash({
703
- applicationId: applicationConfig.id,
704
- branch: applicationConfig.branchName,
1108
+ applicationId,
1109
+ branch: branchName,
705
1110
  });
706
1111
  logger.warn(
707
1112
  "Draft state not found, using live edit hash: " + liveEditHash,
@@ -712,3 +1117,108 @@ async function getDraftOrLiveEditHash(
712
1117
  }
713
1118
  }
714
1119
  }
1120
+
1121
+ async function ensureRuntimeDbfsBranchConsistency({
1122
+ sdk,
1123
+ applicationConfig,
1124
+ logger,
1125
+ lockService,
1126
+ syncService,
1127
+ currentBranchName,
1128
+ }: {
1129
+ sdk: SuperblocksSdk;
1130
+ applicationConfig: ApplicationConfig;
1131
+ logger: Logger;
1132
+ lockService?: LockService;
1133
+ syncService?: SyncService;
1134
+ currentBranchName: string;
1135
+ }): Promise<string> {
1136
+ interface BranchSwitchingLockService {
1137
+ isLocked: boolean;
1138
+ switchBranch(
1139
+ nextBranchName: string,
1140
+ options?: { reacquireLock?: boolean },
1141
+ ): Promise<void>;
1142
+ }
1143
+ interface BranchSwitchingSyncService {
1144
+ setBranchName(nextBranchName: string): void;
1145
+ }
1146
+
1147
+ const targetBranchName = await resolveDbfsBranchName(
1148
+ sdk,
1149
+ applicationConfig,
1150
+ logger,
1151
+ );
1152
+
1153
+ if (targetBranchName === currentBranchName) {
1154
+ return currentBranchName;
1155
+ }
1156
+
1157
+ if (!lockService || !syncService) {
1158
+ logger.warn(
1159
+ `Target DBFS branch changed to '${targetBranchName}', but lock/sync services are unavailable; keeping '${currentBranchName}'`,
1160
+ );
1161
+ return currentBranchName;
1162
+ }
1163
+
1164
+ const lockSvc = lockService as unknown as BranchSwitchingLockService;
1165
+ const syncSvc = syncService as unknown as BranchSwitchingSyncService;
1166
+ const wasLocked = lockSvc.isLocked;
1167
+ logger.info(
1168
+ `Switching runtime DBFS branch context from '${currentBranchName}' to '${targetBranchName}'`,
1169
+ );
1170
+
1171
+ try {
1172
+ syncSvc.setBranchName(targetBranchName);
1173
+ await lockSvc.switchBranch(targetBranchName, { reacquireLock: wasLocked });
1174
+ // Align local workspace with the newly selected DBFS branch.
1175
+ await syncService.downloadDirectory();
1176
+ return targetBranchName;
1177
+ } catch (error) {
1178
+ logger.warn(
1179
+ `Failed to switch runtime DBFS branch to '${targetBranchName}', keeping '${currentBranchName}': ${error instanceof Error ? error.message : String(error)}`,
1180
+ );
1181
+ syncSvc.setBranchName(currentBranchName);
1182
+ try {
1183
+ await lockSvc.switchBranch(currentBranchName, {
1184
+ reacquireLock: lockSvc.isLocked || wasLocked,
1185
+ });
1186
+ } catch (rollbackError) {
1187
+ logger.warn(
1188
+ `Failed to rollback lock service branch context to '${currentBranchName}': ${rollbackError instanceof Error ? rollbackError.message : String(rollbackError)}`,
1189
+ );
1190
+ }
1191
+ return currentBranchName;
1192
+ }
1193
+ }
1194
+
1195
+ async function resolveDbfsBranchName(
1196
+ sdk: SuperblocksSdk,
1197
+ applicationConfig: ApplicationConfig,
1198
+ logger: Logger,
1199
+ ): Promise<string> {
1200
+ interface GitAwareSdk {
1201
+ getApplicationGitConfig(
1202
+ applicationId: string,
1203
+ ): Promise<{ gitRemoteUrl?: string } | null>;
1204
+ }
1205
+
1206
+ const fallbackBranchName = applicationConfig.branchName;
1207
+ const gitAwareSdk = sdk as unknown as GitAwareSdk;
1208
+
1209
+ try {
1210
+ const gitConfig = await gitAwareSdk.getApplicationGitConfig(
1211
+ applicationConfig.id,
1212
+ );
1213
+ // Keep DBFS branch selection independent of server-side branch records.
1214
+ // If Git is connected, always use the live workspace branch.
1215
+ return gitConfig?.gitRemoteUrl
1216
+ ? SUPERBLOCKS_LIVE_GIT_BRANCH
1217
+ : fallbackBranchName;
1218
+ } catch (error) {
1219
+ logger.warn(
1220
+ `Could not resolve DBFS branch selection; falling back to '${fallbackBranchName}': ${error instanceof Error ? error.message : String(error)}`,
1221
+ );
1222
+ return fallbackBranchName;
1223
+ }
1224
+ }