@aiworkbench/vibe-publish 0.0.1
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/package.json +32 -0
- package/src/__tests__/config.test.ts +87 -0
- package/src/__tests__/github-adapter.test.ts +57 -0
- package/src/config.ts +72 -0
- package/src/git-platform/azure-devops.ts +39 -0
- package/src/git-platform/github.ts +129 -0
- package/src/git-platform/gitlab.ts +38 -0
- package/src/git-platform/index.ts +37 -0
- package/src/git-platform/interface.ts +23 -0
- package/src/index.ts +56 -0
- package/src/integrity.ts +10 -0
- package/src/publish.ts +108 -0
- package/src/registry.ts +83 -0
- package/src/storage/azure-blob.ts +72 -0
- package/src/storage/gcs.ts +37 -0
- package/src/storage/index.ts +40 -0
- package/src/storage/interface.ts +17 -0
- package/src/storage/s3.ts +35 -0
package/package.json
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@aiworkbench/vibe-publish",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"publishConfig": { "access": "public" },
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"vibe-publish": "./dist/index.js"
|
|
8
|
+
},
|
|
9
|
+
"main": "dist/index.js",
|
|
10
|
+
"exports": {
|
|
11
|
+
".": {
|
|
12
|
+
"bun": "./src/index.ts",
|
|
13
|
+
"import": "./dist/index.js"
|
|
14
|
+
}
|
|
15
|
+
},
|
|
16
|
+
"scripts": {
|
|
17
|
+
"build": "bun build ./src/index.ts --outfile ./dist/index.js --target=node --external @azure/storage-blob",
|
|
18
|
+
"dev": "bun --watch src/index.ts",
|
|
19
|
+
"test": "bun test",
|
|
20
|
+
"type-check": "tsc --noEmit",
|
|
21
|
+
"clean": "rm -rf dist"
|
|
22
|
+
},
|
|
23
|
+
"dependencies": {
|
|
24
|
+
"@aiworkbench/vibe-types": "workspace:*",
|
|
25
|
+
"@azure/storage-blob": "^12.25.0"
|
|
26
|
+
},
|
|
27
|
+
"devDependencies": {
|
|
28
|
+
"@types/node": "^22.0.0",
|
|
29
|
+
"typescript": "^5.7.0"
|
|
30
|
+
},
|
|
31
|
+
"files": ["dist", "src"]
|
|
32
|
+
}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import { describe, test, expect, beforeEach, afterEach } from "bun:test";
|
|
2
|
+
import { loadConfig } from "../config.js";
|
|
3
|
+
|
|
4
|
+
describe("loadConfig — GitHub URL resolution", () => {
|
|
5
|
+
const envBackup: Record<string, string | undefined> = {};
|
|
6
|
+
const envKeys = [
|
|
7
|
+
"VIBE_GITHUB_API_URL",
|
|
8
|
+
"VIBE_GITHUB_SERVER_URL",
|
|
9
|
+
"GITHUB_API_URL",
|
|
10
|
+
"GITHUB_SERVER_URL",
|
|
11
|
+
];
|
|
12
|
+
|
|
13
|
+
beforeEach(() => {
|
|
14
|
+
for (const key of envKeys) {
|
|
15
|
+
envBackup[key] = process.env[key];
|
|
16
|
+
delete process.env[key];
|
|
17
|
+
}
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
afterEach(() => {
|
|
21
|
+
for (const key of envKeys) {
|
|
22
|
+
if (envBackup[key] === undefined) {
|
|
23
|
+
delete process.env[key];
|
|
24
|
+
} else {
|
|
25
|
+
process.env[key] = envBackup[key];
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
test("defaults to public GitHub URLs when no env vars set", () => {
|
|
31
|
+
const config = loadConfig();
|
|
32
|
+
expect(config.githubApiUrl).toBe("https://api.github.com");
|
|
33
|
+
expect(config.githubServerUrl).toBe("https://github.com");
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
test("VIBE_GITHUB_* env vars override defaults", () => {
|
|
37
|
+
process.env.VIBE_GITHUB_API_URL = "https://ghe.corp.com/api/v3";
|
|
38
|
+
process.env.VIBE_GITHUB_SERVER_URL = "https://ghe.corp.com";
|
|
39
|
+
|
|
40
|
+
const config = loadConfig();
|
|
41
|
+
expect(config.githubApiUrl).toBe("https://ghe.corp.com/api/v3");
|
|
42
|
+
expect(config.githubServerUrl).toBe("https://ghe.corp.com");
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
test("GITHUB_* env vars (CI-provided) override defaults", () => {
|
|
46
|
+
process.env.GITHUB_API_URL = "https://ghe.corp.com/api/v3";
|
|
47
|
+
process.env.GITHUB_SERVER_URL = "https://ghe.corp.com";
|
|
48
|
+
|
|
49
|
+
const config = loadConfig();
|
|
50
|
+
expect(config.githubApiUrl).toBe("https://ghe.corp.com/api/v3");
|
|
51
|
+
expect(config.githubServerUrl).toBe("https://ghe.corp.com");
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
test("VIBE_GITHUB_* takes precedence over GITHUB_*", () => {
|
|
55
|
+
process.env.VIBE_GITHUB_API_URL = "https://vibe-override/api/v3";
|
|
56
|
+
process.env.GITHUB_API_URL = "https://ci-provided/api/v3";
|
|
57
|
+
|
|
58
|
+
const config = loadConfig();
|
|
59
|
+
expect(config.githubApiUrl).toBe("https://vibe-override/api/v3");
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
test("explicit overrides take precedence over all env vars", () => {
|
|
63
|
+
process.env.VIBE_GITHUB_API_URL = "https://env-value/api/v3";
|
|
64
|
+
|
|
65
|
+
const config = loadConfig({
|
|
66
|
+
githubApiUrl: "https://explicit-override/api/v3",
|
|
67
|
+
});
|
|
68
|
+
expect(config.githubApiUrl).toBe("https://explicit-override/api/v3");
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
test("empty string env vars are treated as unset", () => {
|
|
72
|
+
process.env.VIBE_GITHUB_API_URL = "";
|
|
73
|
+
process.env.VIBE_GITHUB_SERVER_URL = "";
|
|
74
|
+
|
|
75
|
+
const config = loadConfig();
|
|
76
|
+
expect(config.githubApiUrl).toBe("https://api.github.com");
|
|
77
|
+
expect(config.githubServerUrl).toBe("https://github.com");
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
test("empty VIBE_GITHUB_* falls through to GITHUB_*", () => {
|
|
81
|
+
process.env.VIBE_GITHUB_API_URL = "";
|
|
82
|
+
process.env.GITHUB_API_URL = "https://ghe.corp.com/api/v3";
|
|
83
|
+
|
|
84
|
+
const config = loadConfig();
|
|
85
|
+
expect(config.githubApiUrl).toBe("https://ghe.corp.com/api/v3");
|
|
86
|
+
});
|
|
87
|
+
});
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { describe, test, expect } from "bun:test";
|
|
2
|
+
import { GitHubAdapter, type GitHubConfig } from "../git-platform/github.js";
|
|
3
|
+
|
|
4
|
+
function createAdapter(overrides: Partial<GitHubConfig> = {}): GitHubAdapter {
|
|
5
|
+
return new GitHubAdapter({
|
|
6
|
+
registryRepo: "my-org/vibe-registry",
|
|
7
|
+
token: "ghp_test123",
|
|
8
|
+
apiUrl: "https://api.github.com",
|
|
9
|
+
serverUrl: "https://github.com",
|
|
10
|
+
...overrides,
|
|
11
|
+
});
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
describe("GitHubAdapter — public GitHub", () => {
|
|
15
|
+
const adapter = createAdapter();
|
|
16
|
+
|
|
17
|
+
test("cloneUrl uses github.com", () => {
|
|
18
|
+
// Access private method via prototype for testing
|
|
19
|
+
const url = (adapter as any).cloneUrl();
|
|
20
|
+
expect(url).toBe(
|
|
21
|
+
"https://x-access-token:ghp_test123@github.com/my-org/vibe-registry.git",
|
|
22
|
+
);
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
test("hostname is derived from serverUrl", () => {
|
|
26
|
+
expect((adapter as any).hostname).toBe("github.com");
|
|
27
|
+
});
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
describe("GitHubAdapter — GitHub Enterprise Server", () => {
|
|
31
|
+
const adapter = createAdapter({
|
|
32
|
+
apiUrl: "https://ghe.corp.com/api/v3",
|
|
33
|
+
serverUrl: "https://ghe.corp.com",
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
test("cloneUrl uses GHE hostname", () => {
|
|
37
|
+
const url = (adapter as any).cloneUrl();
|
|
38
|
+
expect(url).toBe(
|
|
39
|
+
"https://x-access-token:ghp_test123@ghe.corp.com/my-org/vibe-registry.git",
|
|
40
|
+
);
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
test("hostname is derived from GHE serverUrl", () => {
|
|
44
|
+
expect((adapter as any).hostname).toBe("ghe.corp.com");
|
|
45
|
+
});
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
describe("GitHubAdapter — GHE with custom port", () => {
|
|
49
|
+
const adapter = createAdapter({
|
|
50
|
+
apiUrl: "https://ghe.corp.com:8443/api/v3",
|
|
51
|
+
serverUrl: "https://ghe.corp.com:8443",
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
test("hostname does not include port", () => {
|
|
55
|
+
expect((adapter as any).hostname).toBe("ghe.corp.com");
|
|
56
|
+
});
|
|
57
|
+
});
|
package/src/config.ts
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
export type Environment = "dev" | "staging" | "prod";
|
|
2
|
+
export type StorageProviderType = "azure-blob" | "s3" | "gcs";
|
|
3
|
+
export type GitPlatformType = "github" | "azure-devops" | "gitlab";
|
|
4
|
+
|
|
5
|
+
export interface PublishConfig {
|
|
6
|
+
environment: Environment;
|
|
7
|
+
directPush: boolean;
|
|
8
|
+
|
|
9
|
+
// Storage
|
|
10
|
+
storageProvider: StorageProviderType;
|
|
11
|
+
azureAccountName?: string;
|
|
12
|
+
azureAccountKey?: string;
|
|
13
|
+
azureContainerName?: string;
|
|
14
|
+
|
|
15
|
+
// Git platform
|
|
16
|
+
gitPlatform: GitPlatformType;
|
|
17
|
+
registryRepo?: string;
|
|
18
|
+
registryToken?: string;
|
|
19
|
+
|
|
20
|
+
// GitHub host URLs (for GitHub Enterprise Server support)
|
|
21
|
+
githubApiUrl: string;
|
|
22
|
+
githubServerUrl: string;
|
|
23
|
+
|
|
24
|
+
// Publisher identity
|
|
25
|
+
publishedBy: string;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function loadConfig(overrides: Partial<PublishConfig> = {}): PublishConfig {
|
|
29
|
+
const env = (overrides.environment ??
|
|
30
|
+
process.env.VIBE_ENVIRONMENT ??
|
|
31
|
+
"dev") as Environment;
|
|
32
|
+
|
|
33
|
+
return {
|
|
34
|
+
environment: env,
|
|
35
|
+
directPush:
|
|
36
|
+
overrides.directPush ?? process.env.VIBE_DIRECT_PUSH === "true",
|
|
37
|
+
|
|
38
|
+
storageProvider: (overrides.storageProvider ??
|
|
39
|
+
process.env.VIBE_STORAGE_PROVIDER ??
|
|
40
|
+
"azure-blob") as StorageProviderType,
|
|
41
|
+
azureAccountName:
|
|
42
|
+
overrides.azureAccountName ?? process.env.VIBE_STORAGE_ACCOUNT_NAME,
|
|
43
|
+
azureAccountKey:
|
|
44
|
+
overrides.azureAccountKey ?? process.env.VIBE_STORAGE_ACCOUNT_KEY,
|
|
45
|
+
azureContainerName:
|
|
46
|
+
overrides.azureContainerName ?? process.env.VIBE_STORAGE_CONTAINER_NAME,
|
|
47
|
+
|
|
48
|
+
gitPlatform: (overrides.gitPlatform ??
|
|
49
|
+
process.env.VIBE_GIT_PLATFORM ??
|
|
50
|
+
"github") as GitPlatformType,
|
|
51
|
+
registryRepo: overrides.registryRepo ?? process.env.VIBE_REGISTRY_REPO,
|
|
52
|
+
registryToken: overrides.registryToken ?? process.env.VIBE_REGISTRY_TOKEN,
|
|
53
|
+
|
|
54
|
+
githubApiUrl:
|
|
55
|
+
overrides.githubApiUrl ??
|
|
56
|
+
(process.env.VIBE_GITHUB_API_URL || undefined) ??
|
|
57
|
+
(process.env.GITHUB_API_URL || undefined) ??
|
|
58
|
+
"https://api.github.com",
|
|
59
|
+
|
|
60
|
+
githubServerUrl:
|
|
61
|
+
overrides.githubServerUrl ??
|
|
62
|
+
(process.env.VIBE_GITHUB_SERVER_URL || undefined) ??
|
|
63
|
+
(process.env.GITHUB_SERVER_URL || undefined) ??
|
|
64
|
+
"https://github.com",
|
|
65
|
+
|
|
66
|
+
publishedBy:
|
|
67
|
+
overrides.publishedBy ??
|
|
68
|
+
process.env.GITHUB_ACTOR ??
|
|
69
|
+
process.env.USER ??
|
|
70
|
+
"unknown",
|
|
71
|
+
};
|
|
72
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import type { GitPlatformAdapter } from "./interface.js";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Azure DevOps Git adapter — stub.
|
|
5
|
+
*
|
|
6
|
+
* To implement: use the Azure DevOps REST API or `az repos` CLI
|
|
7
|
+
* for clone, push, and PR creation.
|
|
8
|
+
*/
|
|
9
|
+
export class AzureDevOpsAdapter implements GitPlatformAdapter {
|
|
10
|
+
constructor(_config: { organization: string; project: string; repo: string; token: string }) {
|
|
11
|
+
throw new Error(
|
|
12
|
+
"AzureDevOpsAdapter is not yet implemented. See azure-devops.ts for notes.",
|
|
13
|
+
);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
cloneRegistry(_targetDir: string): Promise<string> {
|
|
17
|
+
throw new Error("Not implemented");
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
createPullRequest(_opts: {
|
|
21
|
+
repoDir: string;
|
|
22
|
+
branchName: string;
|
|
23
|
+
title: string;
|
|
24
|
+
body: string;
|
|
25
|
+
baseBranch: string;
|
|
26
|
+
filesToAdd: string[];
|
|
27
|
+
}): Promise<{ url: string; number: number }> {
|
|
28
|
+
throw new Error("Not implemented");
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
pushDirectly(_opts: {
|
|
32
|
+
repoDir: string;
|
|
33
|
+
branchName: string;
|
|
34
|
+
commitMessage: string;
|
|
35
|
+
filesToAdd: string[];
|
|
36
|
+
}): Promise<void> {
|
|
37
|
+
throw new Error("Not implemented");
|
|
38
|
+
}
|
|
39
|
+
}
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
import { execFileSync } from "node:child_process";
|
|
2
|
+
import type { GitPlatformAdapter } from "./interface.js";
|
|
3
|
+
|
|
4
|
+
export interface GitHubConfig {
|
|
5
|
+
registryRepo: string; // "org/vibe-registry"
|
|
6
|
+
token: string;
|
|
7
|
+
apiUrl: string; // e.g. "https://api.github.com" or "https://ghe.corp.com/api/v3"
|
|
8
|
+
serverUrl: string; // e.g. "https://github.com" or "https://ghe.corp.com"
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export class GitHubAdapter implements GitPlatformAdapter {
|
|
12
|
+
private repo: string;
|
|
13
|
+
private token: string;
|
|
14
|
+
private apiUrl: string;
|
|
15
|
+
private serverUrl: string;
|
|
16
|
+
private hostname: string;
|
|
17
|
+
|
|
18
|
+
constructor(config: GitHubConfig) {
|
|
19
|
+
this.repo = config.registryRepo;
|
|
20
|
+
this.token = config.token;
|
|
21
|
+
this.apiUrl = config.apiUrl;
|
|
22
|
+
this.serverUrl = config.serverUrl;
|
|
23
|
+
this.hostname = new URL(config.serverUrl).hostname;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
private cloneUrl(): string {
|
|
27
|
+
return `https://x-access-token:${this.token}@${this.hostname}/${this.repo}.git`;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
private redact(text: string): string {
|
|
31
|
+
return this.token ? text.replaceAll(this.token, "***") : text;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
private git(args: string[], cwd: string): string {
|
|
35
|
+
try {
|
|
36
|
+
return execFileSync("git", args, {
|
|
37
|
+
cwd,
|
|
38
|
+
encoding: "utf-8",
|
|
39
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
40
|
+
}).trim();
|
|
41
|
+
} catch (err) {
|
|
42
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
43
|
+
throw new Error(this.redact(msg));
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
async cloneRegistry(targetDir: string): Promise<string> {
|
|
48
|
+
try {
|
|
49
|
+
execFileSync(
|
|
50
|
+
"git",
|
|
51
|
+
["clone", "--depth", "1", this.cloneUrl(), targetDir],
|
|
52
|
+
{ stdio: ["pipe", "pipe", "pipe"] },
|
|
53
|
+
);
|
|
54
|
+
} catch (err) {
|
|
55
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
56
|
+
throw new Error(this.redact(msg));
|
|
57
|
+
}
|
|
58
|
+
this.git(["config", "user.name", "vibe-publish[bot]"], targetDir);
|
|
59
|
+
this.git(
|
|
60
|
+
["config", "user.email", `vibe-publish[bot]@users.noreply.${this.hostname}`],
|
|
61
|
+
targetDir,
|
|
62
|
+
);
|
|
63
|
+
return targetDir;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
async createPullRequest(opts: {
|
|
67
|
+
repoDir: string;
|
|
68
|
+
branchName: string;
|
|
69
|
+
title: string;
|
|
70
|
+
body: string;
|
|
71
|
+
baseBranch: string;
|
|
72
|
+
filesToAdd: string[];
|
|
73
|
+
}): Promise<{ url: string; number: number }> {
|
|
74
|
+
const { repoDir, branchName, title, body, baseBranch, filesToAdd } = opts;
|
|
75
|
+
|
|
76
|
+
this.git(["checkout", "-b", branchName], repoDir);
|
|
77
|
+
|
|
78
|
+
for (const file of filesToAdd) {
|
|
79
|
+
this.git(["add", file], repoDir);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
this.git(["commit", "-m", title], repoDir);
|
|
83
|
+
this.git(["push", "origin", branchName], repoDir);
|
|
84
|
+
|
|
85
|
+
// Create PR via GitHub API
|
|
86
|
+
const apiUrl = `${this.apiUrl}/repos/${this.repo}/pulls`;
|
|
87
|
+
const response = await fetch(apiUrl, {
|
|
88
|
+
method: "POST",
|
|
89
|
+
headers: {
|
|
90
|
+
Authorization: `Bearer ${this.token}`,
|
|
91
|
+
Accept: "application/vnd.github+json",
|
|
92
|
+
"Content-Type": "application/json",
|
|
93
|
+
},
|
|
94
|
+
body: JSON.stringify({
|
|
95
|
+
title,
|
|
96
|
+
body,
|
|
97
|
+
head: branchName,
|
|
98
|
+
base: baseBranch,
|
|
99
|
+
}),
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
if (!response.ok) {
|
|
103
|
+
const text = await response.text();
|
|
104
|
+
throw new Error(`Failed to create PR: ${response.status} ${text}`);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const pr = (await response.json()) as {
|
|
108
|
+
html_url: string;
|
|
109
|
+
number: number;
|
|
110
|
+
};
|
|
111
|
+
return { url: pr.html_url, number: pr.number };
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
async pushDirectly(opts: {
|
|
115
|
+
repoDir: string;
|
|
116
|
+
branchName: string;
|
|
117
|
+
commitMessage: string;
|
|
118
|
+
filesToAdd: string[];
|
|
119
|
+
}): Promise<void> {
|
|
120
|
+
const { repoDir, branchName, commitMessage, filesToAdd } = opts;
|
|
121
|
+
|
|
122
|
+
for (const file of filesToAdd) {
|
|
123
|
+
this.git(["add", file], repoDir);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
this.git(["commit", "-m", commitMessage], repoDir);
|
|
127
|
+
this.git(["push", "origin", branchName], repoDir);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import type { GitPlatformAdapter } from "./interface.js";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* GitLab adapter — stub.
|
|
5
|
+
*
|
|
6
|
+
* To implement: use the GitLab REST API for clone, push, and MR creation.
|
|
7
|
+
*/
|
|
8
|
+
export class GitLabAdapter implements GitPlatformAdapter {
|
|
9
|
+
constructor(_config: { host: string; project: string; token: string }) {
|
|
10
|
+
throw new Error(
|
|
11
|
+
"GitLabAdapter is not yet implemented. See gitlab.ts for notes.",
|
|
12
|
+
);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
cloneRegistry(_targetDir: string): Promise<string> {
|
|
16
|
+
throw new Error("Not implemented");
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
createPullRequest(_opts: {
|
|
20
|
+
repoDir: string;
|
|
21
|
+
branchName: string;
|
|
22
|
+
title: string;
|
|
23
|
+
body: string;
|
|
24
|
+
baseBranch: string;
|
|
25
|
+
filesToAdd: string[];
|
|
26
|
+
}): Promise<{ url: string; number: number }> {
|
|
27
|
+
throw new Error("Not implemented");
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
pushDirectly(_opts: {
|
|
31
|
+
repoDir: string;
|
|
32
|
+
branchName: string;
|
|
33
|
+
commitMessage: string;
|
|
34
|
+
filesToAdd: string[];
|
|
35
|
+
}): Promise<void> {
|
|
36
|
+
throw new Error("Not implemented");
|
|
37
|
+
}
|
|
38
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import type { GitPlatformAdapter } from "./interface.js";
|
|
2
|
+
import { GitHubAdapter } from "./github.js";
|
|
3
|
+
import type { PublishConfig } from "../config.js";
|
|
4
|
+
|
|
5
|
+
export type { GitPlatformAdapter } from "./interface.js";
|
|
6
|
+
|
|
7
|
+
export function resolveGitPlatform(config: PublishConfig): GitPlatformAdapter {
|
|
8
|
+
switch (config.gitPlatform) {
|
|
9
|
+
case "github":
|
|
10
|
+
if (!config.registryRepo || !config.registryToken) {
|
|
11
|
+
throw new Error(
|
|
12
|
+
"GitHub requires VIBE_REGISTRY_REPO and VIBE_REGISTRY_TOKEN",
|
|
13
|
+
);
|
|
14
|
+
}
|
|
15
|
+
return new GitHubAdapter({
|
|
16
|
+
registryRepo: config.registryRepo,
|
|
17
|
+
token: config.registryToken,
|
|
18
|
+
apiUrl: config.githubApiUrl,
|
|
19
|
+
serverUrl: config.githubServerUrl,
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
case "azure-devops":
|
|
23
|
+
throw new Error(
|
|
24
|
+
'Azure DevOps adapter is not yet implemented. Set VIBE_GIT_PLATFORM="github" or implement AzureDevOpsAdapter.',
|
|
25
|
+
);
|
|
26
|
+
|
|
27
|
+
case "gitlab":
|
|
28
|
+
throw new Error(
|
|
29
|
+
'GitLab adapter is not yet implemented. Set VIBE_GIT_PLATFORM="github" or implement GitLabAdapter.',
|
|
30
|
+
);
|
|
31
|
+
|
|
32
|
+
default:
|
|
33
|
+
throw new Error(
|
|
34
|
+
`Unknown git platform: "${config.gitPlatform}". Supported: github, azure-devops, gitlab`,
|
|
35
|
+
);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/** Adapter for interacting with a Git hosting platform. */
|
|
2
|
+
export interface GitPlatformAdapter {
|
|
3
|
+
/** Clone the registry repo into a local directory. Returns the path. */
|
|
4
|
+
cloneRegistry(targetDir: string): Promise<string>;
|
|
5
|
+
|
|
6
|
+
/** Create a pull request with the given changes. */
|
|
7
|
+
createPullRequest(opts: {
|
|
8
|
+
repoDir: string;
|
|
9
|
+
branchName: string;
|
|
10
|
+
title: string;
|
|
11
|
+
body: string;
|
|
12
|
+
baseBranch: string;
|
|
13
|
+
filesToAdd: string[];
|
|
14
|
+
}): Promise<{ url: string; number: number }>;
|
|
15
|
+
|
|
16
|
+
/** Push changes directly (no PR). Used for dev environment. */
|
|
17
|
+
pushDirectly(opts: {
|
|
18
|
+
repoDir: string;
|
|
19
|
+
branchName: string;
|
|
20
|
+
commitMessage: string;
|
|
21
|
+
filesToAdd: string[];
|
|
22
|
+
}): Promise<void>;
|
|
23
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* @aiworkbench/vibe-publish
|
|
4
|
+
*
|
|
5
|
+
* CLI to publish a mini-app bundle to blob storage and update the registry.
|
|
6
|
+
*
|
|
7
|
+
* Usage:
|
|
8
|
+
* vibe-publish --environment dev [--direct-push]
|
|
9
|
+
*
|
|
10
|
+
* Configuration is loaded from VIBE_* environment variables.
|
|
11
|
+
* See config.ts for the full list.
|
|
12
|
+
*/
|
|
13
|
+
import { parseArgs } from "node:util";
|
|
14
|
+
import { loadConfig, type Environment } from "./config.js";
|
|
15
|
+
import { publish } from "./publish.js";
|
|
16
|
+
|
|
17
|
+
async function main() {
|
|
18
|
+
const { values } = parseArgs({
|
|
19
|
+
options: {
|
|
20
|
+
environment: { type: "string", short: "e" },
|
|
21
|
+
"direct-push": { type: "boolean", default: false },
|
|
22
|
+
"app-dir": { type: "string", default: "." },
|
|
23
|
+
help: { type: "boolean", short: "h" },
|
|
24
|
+
},
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
if (values.help) {
|
|
28
|
+
console.log(`
|
|
29
|
+
Usage: vibe-publish [options]
|
|
30
|
+
|
|
31
|
+
Options:
|
|
32
|
+
-e, --environment <env> Target environment: dev, staging, prod (default: dev)
|
|
33
|
+
--direct-push Push directly to main instead of opening a PR
|
|
34
|
+
--app-dir <path> Path to the mini-app directory (default: .)
|
|
35
|
+
-h, --help Show this help
|
|
36
|
+
`.trim());
|
|
37
|
+
process.exit(0);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const config = loadConfig({
|
|
41
|
+
environment: values.environment as Environment | undefined,
|
|
42
|
+
directPush: values["direct-push"] || undefined,
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
const appDir = values["app-dir"] ?? process.cwd();
|
|
46
|
+
|
|
47
|
+
try {
|
|
48
|
+
const result = await publish(appDir, config);
|
|
49
|
+
console.log(`\nDone. ${result.entry.id} v${result.entry.version} published.`);
|
|
50
|
+
} catch (err) {
|
|
51
|
+
console.error(`\nPublish failed: ${(err as Error).message}`);
|
|
52
|
+
process.exit(1);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
main();
|
package/src/integrity.ts
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Compute an SRI integrity hash (sha384) for a buffer.
|
|
5
|
+
* Returns a string like "sha384-oqVuAfXRKap7fdgcCY5uykM6+R9GqQ8K…"
|
|
6
|
+
*/
|
|
7
|
+
export function computeIntegrity(content: Buffer): string {
|
|
8
|
+
const hash = createHash("sha384").update(content).digest("base64");
|
|
9
|
+
return `sha384-${hash}`;
|
|
10
|
+
}
|
package/src/publish.ts
ADDED
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import { readFileSync } from "node:fs";
|
|
2
|
+
import { resolve } from "node:path";
|
|
3
|
+
import type { VibeManifest, RegistryEntry } from "@aiworkbench/vibe-types";
|
|
4
|
+
import type { PublishConfig } from "./config.js";
|
|
5
|
+
import { computeIntegrity } from "./integrity.js";
|
|
6
|
+
import { resolveStorageProvider } from "./storage/index.js";
|
|
7
|
+
import { resolveGitPlatform } from "./git-platform/index.js";
|
|
8
|
+
import { updateRegistry } from "./registry.js";
|
|
9
|
+
|
|
10
|
+
export interface PublishResult {
|
|
11
|
+
entry: RegistryEntry;
|
|
12
|
+
registryMethod: "push" | "pr";
|
|
13
|
+
prUrl?: string;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Main publish orchestrator.
|
|
18
|
+
*
|
|
19
|
+
* 1. Read manifest.json + dist/index.js from the mini-app directory
|
|
20
|
+
* 2. Compute SRI integrity hash
|
|
21
|
+
* 3. Check immutability — reject if version already in storage
|
|
22
|
+
* 4. Upload bundle to blob storage
|
|
23
|
+
* 5. Build RegistryEntry
|
|
24
|
+
* 6. Update registry (push or PR)
|
|
25
|
+
*/
|
|
26
|
+
export async function publish(
|
|
27
|
+
appDir: string,
|
|
28
|
+
config: PublishConfig,
|
|
29
|
+
): Promise<PublishResult> {
|
|
30
|
+
// 1. Read manifest and bundle
|
|
31
|
+
const manifestPath = resolve(appDir, "manifest.json");
|
|
32
|
+
const bundlePath = resolve(appDir, "dist/index.js");
|
|
33
|
+
|
|
34
|
+
let manifest: VibeManifest;
|
|
35
|
+
try {
|
|
36
|
+
manifest = JSON.parse(readFileSync(manifestPath, "utf-8")) as VibeManifest;
|
|
37
|
+
} catch {
|
|
38
|
+
throw new Error(`Cannot read manifest.json at ${manifestPath}`);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
let bundleContent: Buffer;
|
|
42
|
+
try {
|
|
43
|
+
bundleContent = readFileSync(bundlePath) as unknown as Buffer;
|
|
44
|
+
} catch {
|
|
45
|
+
throw new Error(
|
|
46
|
+
`Cannot read dist/index.js at ${bundlePath}. Did you run "bun run build"?`,
|
|
47
|
+
);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const { id, version } = manifest;
|
|
51
|
+
console.log(`Publishing ${id} v${version} → ${config.environment}`);
|
|
52
|
+
|
|
53
|
+
// 2. Compute integrity
|
|
54
|
+
const integrity = computeIntegrity(bundleContent);
|
|
55
|
+
|
|
56
|
+
// 3. Resolve storage + check immutability
|
|
57
|
+
const storage = resolveStorageProvider(config);
|
|
58
|
+
const alreadyExists = await storage.exists(id, version, "index.js");
|
|
59
|
+
if (alreadyExists) {
|
|
60
|
+
throw new Error(
|
|
61
|
+
`Immutability violation: ${id}/${version}/index.js already exists in storage. ` +
|
|
62
|
+
`Bump the version in manifest.json to publish a new release.`,
|
|
63
|
+
);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// 4. Upload bundle
|
|
67
|
+
console.log(`Uploading ${id}/${version}/index.js…`);
|
|
68
|
+
const { url: bundleUrl, sizeBytes } = await storage.upload(
|
|
69
|
+
id,
|
|
70
|
+
version,
|
|
71
|
+
"index.js",
|
|
72
|
+
bundleContent,
|
|
73
|
+
"application/javascript",
|
|
74
|
+
);
|
|
75
|
+
console.log(`Uploaded: ${bundleUrl} (${sizeBytes} bytes)`);
|
|
76
|
+
|
|
77
|
+
// 5. Build registry entry
|
|
78
|
+
const entry: RegistryEntry = {
|
|
79
|
+
...manifest,
|
|
80
|
+
bundleUrl,
|
|
81
|
+
integrity,
|
|
82
|
+
bundleSizeBytes: sizeBytes,
|
|
83
|
+
publishedAt: new Date().toISOString(),
|
|
84
|
+
publishedBy: config.publishedBy,
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
// 6. Update registry
|
|
88
|
+
console.log(`Updating registry (${config.environment})…`);
|
|
89
|
+
const git = resolveGitPlatform(config);
|
|
90
|
+
const result = await updateRegistry({
|
|
91
|
+
entry,
|
|
92
|
+
environment: config.environment,
|
|
93
|
+
directPush: config.directPush,
|
|
94
|
+
git,
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
if (result.method === "pr") {
|
|
98
|
+
console.log(`PR created: ${result.url}`);
|
|
99
|
+
} else {
|
|
100
|
+
console.log(`Pushed directly to main`);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
return {
|
|
104
|
+
entry,
|
|
105
|
+
registryMethod: result.method,
|
|
106
|
+
prUrl: result.url,
|
|
107
|
+
};
|
|
108
|
+
}
|
package/src/registry.ts
ADDED
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { readFileSync, writeFileSync, mkdirSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { tmpdir } from "node:os";
|
|
4
|
+
import { randomBytes } from "node:crypto";
|
|
5
|
+
import type { RegistryEntry } from "@aiworkbench/vibe-types";
|
|
6
|
+
import type { GitPlatformAdapter } from "./git-platform/interface.js";
|
|
7
|
+
import type { Environment } from "./config.js";
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Upsert a registry entry into the target environment's apps.json.
|
|
11
|
+
*
|
|
12
|
+
* - If `directPush` is true, pushes directly to main (used for dev).
|
|
13
|
+
* - Otherwise, creates a PR (used for staging/prod).
|
|
14
|
+
*/
|
|
15
|
+
export async function updateRegistry(opts: {
|
|
16
|
+
entry: RegistryEntry;
|
|
17
|
+
environment: Environment;
|
|
18
|
+
directPush: boolean;
|
|
19
|
+
git: GitPlatformAdapter;
|
|
20
|
+
}): Promise<{ method: "push" | "pr"; url?: string }> {
|
|
21
|
+
const { entry, environment, directPush, git } = opts;
|
|
22
|
+
|
|
23
|
+
// Clone registry to temp dir
|
|
24
|
+
const tmpBase = join(tmpdir(), `vibe-registry-${randomBytes(4).toString("hex")}`);
|
|
25
|
+
mkdirSync(tmpBase, { recursive: true });
|
|
26
|
+
const repoDir = await git.cloneRegistry(tmpBase);
|
|
27
|
+
|
|
28
|
+
// Read existing apps.json
|
|
29
|
+
const appsFile = join(repoDir, environment, "apps.json");
|
|
30
|
+
let apps: RegistryEntry[];
|
|
31
|
+
try {
|
|
32
|
+
apps = JSON.parse(readFileSync(appsFile, "utf-8")) as RegistryEntry[];
|
|
33
|
+
} catch {
|
|
34
|
+
apps = [];
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// Upsert: replace same app id, or append
|
|
38
|
+
const existingIdx = apps.findIndex((a) => a.id === entry.id);
|
|
39
|
+
if (existingIdx !== -1) {
|
|
40
|
+
apps[existingIdx] = entry;
|
|
41
|
+
} else {
|
|
42
|
+
apps.push(entry);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// Sort by id for deterministic output
|
|
46
|
+
apps.sort((a, b) => a.id.localeCompare(b.id));
|
|
47
|
+
|
|
48
|
+
// Write back
|
|
49
|
+
writeFileSync(appsFile, JSON.stringify(apps, null, 2) + "\n");
|
|
50
|
+
|
|
51
|
+
const relPath = `${environment}/apps.json`;
|
|
52
|
+
|
|
53
|
+
if (directPush) {
|
|
54
|
+
await git.pushDirectly({
|
|
55
|
+
repoDir,
|
|
56
|
+
branchName: "main",
|
|
57
|
+
commitMessage: `publish: ${entry.id} v${entry.version} → ${environment}`,
|
|
58
|
+
filesToAdd: [relPath],
|
|
59
|
+
});
|
|
60
|
+
return { method: "push" };
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const branchName = `publish/${environment}/${entry.id}-${entry.version}`;
|
|
64
|
+
const pr = await git.createPullRequest({
|
|
65
|
+
repoDir,
|
|
66
|
+
branchName,
|
|
67
|
+
title: `publish: ${entry.id} v${entry.version} → ${environment}`,
|
|
68
|
+
body: [
|
|
69
|
+
`## Publish ${entry.id} v${entry.version}`,
|
|
70
|
+
"",
|
|
71
|
+
`| Field | Value |`,
|
|
72
|
+
`|---|---|`,
|
|
73
|
+
`| **Environment** | ${environment} |`,
|
|
74
|
+
`| **Bundle URL** | ${entry.bundleUrl} |`,
|
|
75
|
+
`| **Integrity** | \`${entry.integrity.slice(0, 20)}…\` |`,
|
|
76
|
+
`| **Published by** | ${entry.publishedBy} |`,
|
|
77
|
+
].join("\n"),
|
|
78
|
+
baseBranch: "main",
|
|
79
|
+
filesToAdd: [relPath],
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
return { method: "pr", url: pr.url };
|
|
83
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import {
|
|
2
|
+
BlobServiceClient,
|
|
3
|
+
StorageSharedKeyCredential,
|
|
4
|
+
} from "@azure/storage-blob";
|
|
5
|
+
import type { StorageProvider } from "./interface.js";
|
|
6
|
+
|
|
7
|
+
export interface AzureBlobConfig {
|
|
8
|
+
accountName: string;
|
|
9
|
+
accountKey: string;
|
|
10
|
+
containerName: string;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export class AzureBlobProvider implements StorageProvider {
|
|
14
|
+
private client: BlobServiceClient;
|
|
15
|
+
private containerName: string;
|
|
16
|
+
private accountName: string;
|
|
17
|
+
|
|
18
|
+
constructor(config: AzureBlobConfig) {
|
|
19
|
+
this.accountName = config.accountName;
|
|
20
|
+
this.containerName = config.containerName;
|
|
21
|
+
|
|
22
|
+
const credential = new StorageSharedKeyCredential(
|
|
23
|
+
config.accountName,
|
|
24
|
+
config.accountKey,
|
|
25
|
+
);
|
|
26
|
+
this.client = new BlobServiceClient(
|
|
27
|
+
`https://${config.accountName}.blob.core.windows.net`,
|
|
28
|
+
credential,
|
|
29
|
+
);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
getUrl(appId: string, version: string, filename: string): string {
|
|
33
|
+
return `https://${this.accountName}.blob.core.windows.net/${this.containerName}/${appId}/${version}/${filename}`;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
async exists(
|
|
37
|
+
appId: string,
|
|
38
|
+
version: string,
|
|
39
|
+
filename: string,
|
|
40
|
+
): Promise<boolean> {
|
|
41
|
+
const container = this.client.getContainerClient(this.containerName);
|
|
42
|
+
const blob = container.getBlockBlobClient(
|
|
43
|
+
`${appId}/${version}/${filename}`,
|
|
44
|
+
);
|
|
45
|
+
return blob.exists();
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
async upload(
|
|
49
|
+
appId: string,
|
|
50
|
+
version: string,
|
|
51
|
+
filename: string,
|
|
52
|
+
content: Buffer,
|
|
53
|
+
contentType: string,
|
|
54
|
+
): Promise<{ url: string; sizeBytes: number }> {
|
|
55
|
+
const container = this.client.getContainerClient(this.containerName);
|
|
56
|
+
const blob = container.getBlockBlobClient(
|
|
57
|
+
`${appId}/${version}/${filename}`,
|
|
58
|
+
);
|
|
59
|
+
|
|
60
|
+
await blob.upload(content, content.length, {
|
|
61
|
+
blobHTTPHeaders: {
|
|
62
|
+
blobContentType: contentType,
|
|
63
|
+
blobCacheControl: "public, max-age=31536000, immutable",
|
|
64
|
+
},
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
return {
|
|
68
|
+
url: this.getUrl(appId, version, filename),
|
|
69
|
+
sizeBytes: content.length,
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import type { StorageProvider } from "./interface.js";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Google Cloud Storage provider — stub.
|
|
5
|
+
*
|
|
6
|
+
* To implement: install `@google-cloud/storage` and fill in
|
|
7
|
+
* upload / exists / getUrl using the GCS SDK.
|
|
8
|
+
*/
|
|
9
|
+
export class GCSProvider implements StorageProvider {
|
|
10
|
+
constructor(_config: { bucket: string; projectId: string }) {
|
|
11
|
+
throw new Error(
|
|
12
|
+
"GCSProvider is not yet implemented. See gcs.ts for notes.",
|
|
13
|
+
);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
getUrl(_appId: string, _version: string, _filename: string): string {
|
|
17
|
+
throw new Error("Not implemented");
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
exists(
|
|
21
|
+
_appId: string,
|
|
22
|
+
_version: string,
|
|
23
|
+
_filename: string,
|
|
24
|
+
): Promise<boolean> {
|
|
25
|
+
throw new Error("Not implemented");
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
upload(
|
|
29
|
+
_appId: string,
|
|
30
|
+
_version: string,
|
|
31
|
+
_filename: string,
|
|
32
|
+
_content: Buffer,
|
|
33
|
+
_contentType: string,
|
|
34
|
+
): Promise<{ url: string; sizeBytes: number }> {
|
|
35
|
+
throw new Error("Not implemented");
|
|
36
|
+
}
|
|
37
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import type { StorageProvider } from "./interface.js";
|
|
2
|
+
import { AzureBlobProvider } from "./azure-blob.js";
|
|
3
|
+
import type { PublishConfig } from "../config.js";
|
|
4
|
+
|
|
5
|
+
export type { StorageProvider } from "./interface.js";
|
|
6
|
+
|
|
7
|
+
export function resolveStorageProvider(config: PublishConfig): StorageProvider {
|
|
8
|
+
switch (config.storageProvider) {
|
|
9
|
+
case "azure-blob":
|
|
10
|
+
if (
|
|
11
|
+
!config.azureAccountName ||
|
|
12
|
+
!config.azureAccountKey ||
|
|
13
|
+
!config.azureContainerName
|
|
14
|
+
) {
|
|
15
|
+
throw new Error(
|
|
16
|
+
"Azure Blob requires VIBE_STORAGE_ACCOUNT_NAME, VIBE_STORAGE_ACCOUNT_KEY, and VIBE_STORAGE_CONTAINER_NAME",
|
|
17
|
+
);
|
|
18
|
+
}
|
|
19
|
+
return new AzureBlobProvider({
|
|
20
|
+
accountName: config.azureAccountName,
|
|
21
|
+
accountKey: config.azureAccountKey,
|
|
22
|
+
containerName: config.azureContainerName,
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
case "s3":
|
|
26
|
+
throw new Error(
|
|
27
|
+
'S3 provider is not yet implemented. Set VIBE_STORAGE_PROVIDER="azure-blob" or implement S3Provider.',
|
|
28
|
+
);
|
|
29
|
+
|
|
30
|
+
case "gcs":
|
|
31
|
+
throw new Error(
|
|
32
|
+
'GCS provider is not yet implemented. Set VIBE_STORAGE_PROVIDER="azure-blob" or implement GCSProvider.',
|
|
33
|
+
);
|
|
34
|
+
|
|
35
|
+
default:
|
|
36
|
+
throw new Error(
|
|
37
|
+
`Unknown storage provider: "${config.storageProvider}". Supported: azure-blob, s3, gcs`,
|
|
38
|
+
);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/** Storage provider for uploading mini-app bundles. */
|
|
2
|
+
export interface StorageProvider {
|
|
3
|
+
/** Upload a file to storage. Returns the public URL and size. */
|
|
4
|
+
upload(
|
|
5
|
+
appId: string,
|
|
6
|
+
version: string,
|
|
7
|
+
filename: string,
|
|
8
|
+
content: Buffer,
|
|
9
|
+
contentType: string,
|
|
10
|
+
): Promise<{ url: string; sizeBytes: number }>;
|
|
11
|
+
|
|
12
|
+
/** Check whether a file already exists in storage. */
|
|
13
|
+
exists(appId: string, version: string, filename: string): Promise<boolean>;
|
|
14
|
+
|
|
15
|
+
/** Get the public URL for a file (does not check existence). */
|
|
16
|
+
getUrl(appId: string, version: string, filename: string): string;
|
|
17
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import type { StorageProvider } from "./interface.js";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* AWS S3 storage provider — stub.
|
|
5
|
+
*
|
|
6
|
+
* To implement: install `@aws-sdk/client-s3` and fill in
|
|
7
|
+
* upload / exists / getUrl using the S3 SDK.
|
|
8
|
+
*/
|
|
9
|
+
export class S3Provider implements StorageProvider {
|
|
10
|
+
constructor(_config: { bucket: string; region: string }) {
|
|
11
|
+
throw new Error("S3Provider is not yet implemented. See s3.ts for notes.");
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
getUrl(_appId: string, _version: string, _filename: string): string {
|
|
15
|
+
throw new Error("Not implemented");
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
exists(
|
|
19
|
+
_appId: string,
|
|
20
|
+
_version: string,
|
|
21
|
+
_filename: string,
|
|
22
|
+
): Promise<boolean> {
|
|
23
|
+
throw new Error("Not implemented");
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
upload(
|
|
27
|
+
_appId: string,
|
|
28
|
+
_version: string,
|
|
29
|
+
_filename: string,
|
|
30
|
+
_content: Buffer,
|
|
31
|
+
_contentType: string,
|
|
32
|
+
): Promise<{ url: string; sizeBytes: number }> {
|
|
33
|
+
throw new Error("Not implemented");
|
|
34
|
+
}
|
|
35
|
+
}
|