Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 23 additions & 0 deletions packages/plugin-codestorage/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
{
"name": "@valet/plugin-codestorage",
"version": "0.0.1",
"private": true,
"type": "module",
"main": "./dist/index.js",
"types": "./dist/index.d.ts",
"exports": {
"./repo": "./src/repo.ts"
},
"scripts": {
"build": "tsc",
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing tsconfig.json in this package. "build": "tsc" and "typecheck": "tsc --noEmit" will fail or pick up the root config unexpectedly. Every other code plugin has one — needs a tsconfig.json extending the root config, plus references in root tsconfig.json and packages/worker/tsconfig.json.

"typecheck": "tsc --noEmit",
"test": "vitest run"
},
"dependencies": {
"@valet/sdk": "workspace:*"
},
"devDependencies": {
"typescript": "^5.3.3",
"vitest": "^4.0.18"
}
}
4 changes: 4 additions & 0 deletions packages/plugin-codestorage/plugin.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
name: codestorage
version: 0.0.1
description: code.storage repo provider for git clone/push session runtime
icon: "🧱"
196 changes: 196 additions & 0 deletions packages/plugin-codestorage/src/repo.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,196 @@
import type {
RepoProvider,
RepoCredential,
RepoList,
RepoValidation,
} from '@valet/sdk/repos';

const CODESTORAGE_URL_RE = /(?:https?:\/\/)?(?:[^@]+@)?([a-z0-9._-]+\.code\.storage)\/(.+?)(?:\.git)?$/i;

type ParsedRepoUrl = {
host: string;
path: string;
cloneUrl: string;
};

function parseCodeStorageRepoUrl(repoUrl: string): ParsedRepoUrl | null {
const normalized = repoUrl.trim();
const match = normalized.match(CODESTORAGE_URL_RE);
if (!match) return null;

const host = match[1];
const path = match[2].replace(/^\/+/, '');
return {
host,
path,
cloneUrl: `https://${host}/${path}.git`,
};
}

function withCodeStorageUsername(url: string): string {
// code.storage docs use username "t" with bearer/password token auth.
// Keep this in the URL to ensure git credential lookups include a username.
return url.replace(/^https:\/\//i, 'https://t@');
}

function parseApiBase(credential: RepoCredential): string {
const explicit = credential.metadata?.apiBase || credential.metadata?.api_base;
if (explicit) return explicit.replace(/\/$/, '');

const issuer = credential.metadata?.issuer;
if (issuer) return `https://api.${issuer}.code.storage/api/v1`;

return '';
}

function asToken(credential: RepoCredential): string {
const token = credential.accessToken || credential.metadata?.token || credential.metadata?.access_token;
if (!token) throw new Error('code.storage repo provider requires an access token');
return token;
}

async function codestorageFetch(apiBase: string, path: string, token: string): Promise<Response> {
return fetch(`${apiBase}${path}`, {
headers: {
Authorization: `Bearer ${token}`,
Accept: 'application/json',
'User-Agent': 'Valet',
},
});
}

export const codestorageRepoProvider: RepoProvider = {
id: 'codestorage',
displayName: 'code.storage',
icon: 'package',
supportsOrgLevel: true,
supportsPersonalLevel: true,
urlPatterns: [/\.code\.storage\//i],

async listRepos(credential: RepoCredential, opts?): Promise<RepoList> {
const apiBase = parseApiBase(credential);
const token = asToken(credential);

// Initial backend-first support: if API base isn't configured yet,
// skip list UX gracefully while clone/push remains fully supported.
if (!apiBase) return { repos: [], hasMore: false };

const page = opts?.page || 1;
const limit = 30;
const q = opts?.search?.trim();
const query = new URLSearchParams({ limit: String(limit), page: String(page) });
if (q) query.set('q', q);

const res = await codestorageFetch(apiBase, `/repos?${query.toString()}`, token);
if (!res.ok) {
throw new Error(`code.storage list repos failed: ${res.status}`);
}

const data = (await res.json()) as { items?: any[]; repos?: any[]; nextCursor?: string; hasMore?: boolean };
const repos = (data.items || data.repos || []).map((r: any) => {
const fullName = r.full_name || r.fullName || r.name || '';
const parsed = parseCodeStorageRepoUrl(r.clone_url || r.cloneUrl || r.url || '');
return {
id: typeof r.id === 'number' ? r.id : undefined,
name: r.name || fullName.split('/').pop() || fullName,
fullName,
url: r.url || (parsed ? parsed.cloneUrl.replace(/\.git$/, '') : ''),
cloneUrl: r.clone_url || r.cloneUrl || parsed?.cloneUrl || '',
defaultBranch: r.default_branch || r.defaultBranch || 'main',
private: r.private ?? true,
description: r.description ?? null,
updatedAt: r.updated_at || r.updatedAt,
language: r.language ?? null,
};
}).filter((r: any) => r.fullName && r.cloneUrl);

return {
repos,
hasMore: Boolean(data.hasMore || data.nextCursor || repos.length === limit),
};
},

async validateRepo(credential: RepoCredential, repoUrl: string): Promise<RepoValidation> {
const parsed = parseCodeStorageRepoUrl(repoUrl);
if (!parsed) {
return { accessible: false, error: 'Invalid code.storage repository URL' };
}

const apiBase = parseApiBase(credential);
const token = asToken(credential);

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In bootstrap mode (no apiBase), this returns accessible: true based purely on URL syntax + token existence. A typo'd URL or revoked token will pass validation and only fail at clone time. Worth adding a code comment explaining why this is acceptable so future readers don't "fix" it.

if (!apiBase) {
// Bootstrap mode: no API base configured, so we can't verify the repo
// server-side. We accept the URL if it parses and a token exists. A typo'd
// URL or revoked token will surface as a clone-time error, which is acceptable
// for this initial backend-first rollout — full validation requires apiBase
// (derived from credential metadata) to be configured.
return {
accessible: true,
permissions: { push: true, pull: true, admin: false },
fullName: parsed.path,
defaultBranch: 'main',
private: true,
cloneUrl: parsed.cloneUrl,
};
}

const encodedPath = encodeURIComponent(parsed.path);
const res = await codestorageFetch(apiBase, `/repos/${encodedPath}`, token);
if (!res.ok) {
return { accessible: false, error: `Repository not accessible: ${res.status}` };
}

const data = (await res.json()) as {
full_name?: string;
default_branch?: string;
private?: boolean;
clone_url?: string;
permissions?: { push?: boolean; pull?: boolean; admin?: boolean };
};

return {
accessible: true,
permissions: {
push: data.permissions?.push ?? true,
pull: data.permissions?.pull ?? true,
admin: data.permissions?.admin ?? false,
},
fullName: data.full_name || parsed.path,
defaultBranch: data.default_branch || 'main',
private: data.private ?? true,
cloneUrl: data.clone_url || parsed.cloneUrl,
};
},

async assembleSessionEnv(_credential: RepoCredential, opts) {
// Note: the minted token is not embedded here. The Runner's git-setup.ts
// configures a global git credential.helper that calls back to the Runner
// gateway (/git/credentials), which invokes mintToken() on-demand. The t@
// username in the clone URL ensures git triggers a credential lookup.
const parsed = parseCodeStorageRepoUrl(opts.repoUrl);
if (!parsed) {
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

assembleSessionEnv returns a t@ clone URL but doesn't set up a credential helper or GIT_ASKPASS to feed the minted token to git. The GitHub provider does the same, so I assume there's a session-layer mechanism that handles this — but a comment pointing to where/how that happens would help future readers.

throw new Error('Invalid code.storage repository URL');
}

return {
envVars: {
REPO_URL: withCodeStorageUsername(parsed.cloneUrl),
...(opts.branch ? { REPO_BRANCH: opts.branch } : {}),
...(opts.ref ? { REPO_REF: opts.ref } : {}),
},
gitConfig: {
'user.name': opts.gitUser.name,
'user.email': opts.gitUser.email,
},
};
},

async mintToken(credential: RepoCredential) {
const token = asToken(credential);
return {
accessToken: token,
expiresAt: credential.expiresAt,
};
},
};
10 changes: 10 additions & 0 deletions packages/plugin-codestorage/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{
"extends": "../../tsconfig.json",
"compilerOptions": {
"outDir": "./dist",
"rootDir": "./src",
"types": []
},
"include": ["src/**/*"],
"references": [{ "path": "../sdk" }, { "path": "../shared" }]
}
1 change: 1 addition & 0 deletions packages/worker/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
"dependencies": {
"@valet/sdk": "workspace:*",
"@valet/plugin-cloudflare": "workspace:*",
"@valet/plugin-codestorage": "workspace:*",
"@valet/plugin-figma": "workspace:*",
"@valet/plugin-deepwiki": "workspace:*",
"@valet/plugin-email-auth": "workspace:*",
Expand Down
8 changes: 8 additions & 0 deletions packages/worker/src/plugins/content-registry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -250,6 +250,14 @@ timeout 10 agent-browser --headed wait --text "Welcome" # Wait for specific
capabilities: ["actions"],
artifacts: [],
},
{
name: "codestorage",
version: "0.0.1",
description: "code.storage repo provider for git clone/push session runtime",
icon: "🧱",
capabilities: [],
artifacts: [],
},
{
name: "deepwiki",
version: "0.0.1",
Expand Down
5 changes: 3 additions & 2 deletions packages/worker/src/repos/packages.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
// AUTO-GENERATED by scripts/generate-plugin-registry.ts — do not edit

import type { RepoProvider } from '@valet/sdk/repos';
import { githubRepoProvider as rp0 } from '@valet/plugin-github/repo';
import { codestorageRepoProvider as rp0 } from '@valet/plugin-codestorage/repo';
import { githubRepoProvider as rp1 } from '@valet/plugin-github/repo';

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This file is marked AUTO-GENERATED by scripts/generate-plugin-registry.ts — do not edit. Should be regenerated via make generate-registries rather than hand-edited. The change is correct, but editing auto-generated files directly is fragile and sets a bad precedent.

export const installedRepoProviders: RepoProvider[] = [rp0];
export const installedRepoProviders: RepoProvider[] = [rp0, rp1];
1 change: 1 addition & 0 deletions packages/worker/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
{ "path": "../shared" },
{ "path": "../sdk" },
{ "path": "../plugin-cloudflare" },
{ "path": "../plugin-codestorage" },
{ "path": "../plugin-figma" },
{ "path": "../plugin-deepwiki" },
{ "path": "../plugin-email-auth" },
Expand Down
Loading
Loading