@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.
- package/.turbo/turbo-build.log +1 -1
- package/dist/application-build.d.mts.map +1 -1
- package/dist/application-build.mjs +2 -0
- package/dist/application-build.mjs.map +1 -1
- package/dist/cli-replacement/assets/git-workflows/github-superblocks-sync-workflow.d.ts +5 -0
- package/dist/cli-replacement/assets/git-workflows/github-superblocks-sync-workflow.d.ts.map +1 -0
- package/dist/cli-replacement/assets/git-workflows/github-superblocks-sync-workflow.js +61 -0
- package/dist/cli-replacement/assets/git-workflows/github-superblocks-sync-workflow.js.map +1 -0
- package/dist/cli-replacement/assets/git-workflows/github-superblocks-sync-workflow.test.d.ts +2 -0
- package/dist/cli-replacement/assets/git-workflows/github-superblocks-sync-workflow.test.d.ts.map +1 -0
- package/dist/cli-replacement/assets/git-workflows/github-superblocks-sync-workflow.test.js +32 -0
- package/dist/cli-replacement/assets/git-workflows/github-superblocks-sync-workflow.test.js.map +1 -0
- package/dist/cli-replacement/dev.d.mts.map +1 -1
- package/dist/cli-replacement/dev.mjs +350 -16
- package/dist/cli-replacement/dev.mjs.map +1 -1
- package/dist/client-sync.test.d.mts +2 -0
- package/dist/client-sync.test.d.mts.map +1 -0
- package/dist/client-sync.test.mjs +57 -0
- package/dist/client-sync.test.mjs.map +1 -0
- package/dist/client.d.ts +22 -0
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +50 -0
- package/dist/client.js.map +1 -1
- package/dist/collect-sdk-apis.d.mts +18 -0
- package/dist/collect-sdk-apis.d.mts.map +1 -0
- package/dist/collect-sdk-apis.mjs +26 -0
- package/dist/collect-sdk-apis.mjs.map +1 -0
- package/dist/collect-sdk-apis.test.d.mts +2 -0
- package/dist/collect-sdk-apis.test.d.mts.map +1 -0
- package/dist/collect-sdk-apis.test.mjs +72 -0
- package/dist/collect-sdk-apis.test.mjs.map +1 -0
- package/dist/dbfs/client.d.ts +1 -0
- package/dist/dbfs/client.d.ts.map +1 -1
- package/dist/dbfs/client.js +12 -0
- package/dist/dbfs/client.js.map +1 -1
- package/dist/dev-utils/dev-server.d.mts +4 -1
- package/dist/dev-utils/dev-server.d.mts.map +1 -1
- package/dist/dev-utils/dev-server.mjs +14 -6
- package/dist/dev-utils/dev-server.mjs.map +1 -1
- package/dist/dev-utils/vite-plugin-build-manifest-stub.mjs +1 -1
- package/dist/dev-utils/vite-plugin-build-manifest-stub.mjs.map +1 -1
- package/dist/parse-sdk-registry.d.mts +20 -0
- package/dist/parse-sdk-registry.d.mts.map +1 -0
- package/dist/parse-sdk-registry.mjs +161 -0
- package/dist/parse-sdk-registry.mjs.map +1 -0
- package/dist/parse-sdk-registry.test.d.mts +2 -0
- package/dist/parse-sdk-registry.test.d.mts.map +1 -0
- package/dist/parse-sdk-registry.test.mjs +83 -0
- package/dist/parse-sdk-registry.test.mjs.map +1 -0
- package/dist/sdk.d.ts +10 -1
- package/dist/sdk.d.ts.map +1 -1
- package/dist/sdk.js +14 -2
- package/dist/sdk.js.map +1 -1
- package/dist/vite-plugin-generate-api-build-manifest.d.mts.map +1 -1
- package/dist/vite-plugin-generate-api-build-manifest.mjs +13 -1
- package/dist/vite-plugin-generate-api-build-manifest.mjs.map +1 -1
- package/dist/vite-plugin-sdk-api-entry-point.d.mts +10 -0
- package/dist/vite-plugin-sdk-api-entry-point.d.mts.map +1 -0
- package/dist/vite-plugin-sdk-api-entry-point.mjs +63 -0
- package/dist/vite-plugin-sdk-api-entry-point.mjs.map +1 -0
- package/dist/vite-plugin-sdk-api-entry-point.test.d.mts +2 -0
- package/dist/vite-plugin-sdk-api-entry-point.test.d.mts.map +1 -0
- package/dist/vite-plugin-sdk-api-entry-point.test.mjs +166 -0
- package/dist/vite-plugin-sdk-api-entry-point.test.mjs.map +1 -0
- package/eslint.config.js +1 -1
- package/lint-staged.config.mjs +2 -1
- package/package.json +19 -21
- package/src/application-build.mts +2 -0
- package/src/cli-replacement/assets/git-workflows/github-superblocks-sync-workflow.test.ts +60 -0
- package/src/cli-replacement/assets/git-workflows/github-superblocks-sync-workflow.ts +70 -0
- package/src/cli-replacement/dev.mts +528 -18
- package/src/client-sync.test.mts +66 -0
- package/src/client.ts +102 -0
- package/src/collect-sdk-apis.mts +43 -0
- package/src/collect-sdk-apis.test.mts +91 -0
- package/src/dbfs/client.ts +16 -0
- package/src/dev-utils/dev-server.mts +25 -4
- package/src/dev-utils/vite-plugin-build-manifest-stub.mts +1 -1
- package/src/parse-sdk-registry.mts +227 -0
- package/src/parse-sdk-registry.test.mts +133 -0
- package/src/sdk.ts +52 -0
- package/src/vite-plugin-generate-api-build-manifest.mts +17 -1
- package/src/vite-plugin-sdk-api-entry-point.mts +76 -0
- package/src/vite-plugin-sdk-api-entry-point.test.mts +266 -0
- package/tsconfig.json +2 -2
- package/tsconfig.tsbuildinfo +1 -1
- package/turbo.json +3 -8
- 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
|
-
|
|
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 {
|
|
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:
|
|
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:
|
|
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(
|
|
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 '${
|
|
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 '${
|
|
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 '${
|
|
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 '${
|
|
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
|
|
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
|
-
|
|
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
|
|
697
|
-
branch:
|
|
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
|
|
704
|
-
branch:
|
|
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
|
+
}
|