mirror of
https://github.com/tiennm99/ccs.git
synced 2026-08-06 04:22:40 +00:00
feat(auth): add --bare flag and MCP server sync for profiles
- Add --bare flag to `ccs auth create` to skip shared symlinks (commands/, skills/, agents/, settings.json). Bare profiles are clean instances without ClaudeKit or other global extensions. `ccs sync` respects bare flag and skips re-linking. - Add MCP server sync from global ~/.claude.json to instances. Claude Code stores MCP config in ~/.claude.json (not settings.json), which was invisible to CCS profiles using CLAUDE_CONFIG_DIR isolation. Selective copy of mcpServers key only (not OAuth sessions/caches). `ccs sync` refreshes MCP servers across all non-bare instances. Closes #691 Closes #692
This commit is contained in:
@@ -91,6 +91,9 @@ class AuthCommands {
|
|||||||
` ${color('ccs auth create backup --context-group sprint-a --deeper-continuity', 'command')}`
|
` ${color('ccs auth create backup --context-group sprint-a --deeper-continuity', 'command')}`
|
||||||
);
|
);
|
||||||
console.log('');
|
console.log('');
|
||||||
|
console.log(` ${dim('# Create clean profile without shared commands/skills/agents')}`);
|
||||||
|
console.log(` ${color('ccs auth create sandbox --bare', 'command')}`);
|
||||||
|
console.log('');
|
||||||
console.log(` ${dim('# Set work as default')}`);
|
console.log(` ${dim('# Set work as default')}`);
|
||||||
console.log(` ${color('ccs auth default work', 'command')}`);
|
console.log(` ${color('ccs auth default work', 'command')}`);
|
||||||
console.log('');
|
console.log('');
|
||||||
@@ -116,6 +119,9 @@ class AuthCommands {
|
|||||||
console.log(
|
console.log(
|
||||||
` ${color('--deeper-continuity', 'command')} Advanced shared mode: sync additional continuity artifacts`
|
` ${color('--deeper-continuity', 'command')} Advanced shared mode: sync additional continuity artifacts`
|
||||||
);
|
);
|
||||||
|
console.log(
|
||||||
|
` ${color('--bare', 'command')} Create clean profile without shared symlinks (no CK/commands/skills)`
|
||||||
|
);
|
||||||
console.log(
|
console.log(
|
||||||
` ${color('--yes, -y', 'command')} Skip confirmation prompts (remove)`
|
` ${color('--yes, -y', 'command')} Skip confirmation prompts (remove)`
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ function sanitizeProfileNameForInstance(name: string): string {
|
|||||||
*/
|
*/
|
||||||
export async function handleCreate(ctx: CommandContext, args: string[]): Promise<void> {
|
export async function handleCreate(ctx: CommandContext, args: string[]): Promise<void> {
|
||||||
await initUI();
|
await initUI();
|
||||||
const { profileName, force, shareContext, contextGroup, deeperContinuity, unknownFlags } =
|
const { profileName, force, shareContext, contextGroup, deeperContinuity, bare, unknownFlags } =
|
||||||
parseArgs(args);
|
parseArgs(args);
|
||||||
|
|
||||||
if (unknownFlags && unknownFlags.length > 0) {
|
if (unknownFlags && unknownFlags.length > 0) {
|
||||||
@@ -39,7 +39,7 @@ export async function handleCreate(ctx: CommandContext, args: string[]): Promise
|
|||||||
console.log(fail(`Unknown option(s): ${unknownList}`));
|
console.log(fail(`Unknown option(s): ${unknownList}`));
|
||||||
console.log('');
|
console.log('');
|
||||||
console.log(
|
console.log(
|
||||||
`Usage: ${color('ccs auth create <profile> [--force] [--share-context] [--context-group <name>] [--deeper-continuity]', 'command')}`
|
`Usage: ${color('ccs auth create <profile> [--force] [--bare] [--share-context] [--context-group <name>] [--deeper-continuity]', 'command')}`
|
||||||
);
|
);
|
||||||
console.log(`Help: ${color('ccs auth --help', 'command')}`);
|
console.log(`Help: ${color('ccs auth --help', 'command')}`);
|
||||||
console.log('');
|
console.log('');
|
||||||
@@ -50,7 +50,7 @@ export async function handleCreate(ctx: CommandContext, args: string[]): Promise
|
|||||||
console.log(fail('Profile name is required'));
|
console.log(fail('Profile name is required'));
|
||||||
console.log('');
|
console.log('');
|
||||||
console.log(
|
console.log(
|
||||||
`Usage: ${color('ccs auth create <profile> [--force] [--share-context] [--context-group <name>] [--deeper-continuity]', 'command')}`
|
`Usage: ${color('ccs auth create <profile> [--force] [--bare] [--share-context] [--context-group <name>] [--deeper-continuity]', 'command')}`
|
||||||
);
|
);
|
||||||
console.log('');
|
console.log('');
|
||||||
console.log('Example:');
|
console.log('Example:');
|
||||||
@@ -179,7 +179,9 @@ export async function handleCreate(ctx: CommandContext, args: string[]): Promise
|
|||||||
try {
|
try {
|
||||||
// Create instance directory
|
// Create instance directory
|
||||||
console.log(info(`Creating profile: ${profileName}`));
|
console.log(info(`Creating profile: ${profileName}`));
|
||||||
const instancePath = await ctx.instanceMgr.ensureInstance(profileName, contextPolicy);
|
const instancePath = await ctx.instanceMgr.ensureInstance(profileName, contextPolicy, {
|
||||||
|
bare: !!bare,
|
||||||
|
});
|
||||||
|
|
||||||
// Create/update profile entry based on config mode
|
// Create/update profile entry based on config mode
|
||||||
if (useUnifiedConfig) {
|
if (useUnifiedConfig) {
|
||||||
@@ -188,10 +190,14 @@ export async function handleCreate(ctx: CommandContext, args: string[]): Promise
|
|||||||
ctx.registry.updateAccountUnified(profileName, {
|
ctx.registry.updateAccountUnified(profileName, {
|
||||||
context_mode: contextMetadata.context_mode,
|
context_mode: contextMetadata.context_mode,
|
||||||
context_group: contextMetadata.context_group,
|
context_group: contextMetadata.context_group,
|
||||||
|
...(bare ? { bare: true } : {}),
|
||||||
});
|
});
|
||||||
ctx.registry.touchAccountUnified(profileName);
|
ctx.registry.touchAccountUnified(profileName);
|
||||||
} else {
|
} else {
|
||||||
ctx.registry.createAccountUnified(profileName, contextMetadata);
|
ctx.registry.createAccountUnified(profileName, {
|
||||||
|
...contextMetadata,
|
||||||
|
...(bare ? { bare: true } : {}),
|
||||||
|
});
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// Use legacy profiles.json
|
// Use legacy profiles.json
|
||||||
@@ -200,12 +206,14 @@ export async function handleCreate(ctx: CommandContext, args: string[]): Promise
|
|||||||
type: 'account',
|
type: 'account',
|
||||||
context_mode: contextMetadata.context_mode,
|
context_mode: contextMetadata.context_mode,
|
||||||
context_group: contextMetadata.context_group,
|
context_group: contextMetadata.context_group,
|
||||||
|
...(bare ? { bare: true } : {}),
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
ctx.registry.createProfile(profileName, {
|
ctx.registry.createProfile(profileName, {
|
||||||
type: 'account',
|
type: 'account',
|
||||||
context_mode: contextMetadata.context_mode,
|
context_mode: contextMetadata.context_mode,
|
||||||
context_group: contextMetadata.context_group,
|
context_group: contextMetadata.context_group,
|
||||||
|
...(bare ? { bare: true } : {}),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -262,7 +270,8 @@ export async function handleCreate(ctx: CommandContext, args: string[]): Promise
|
|||||||
`Profile: ${profileName}\n` +
|
`Profile: ${profileName}\n` +
|
||||||
`Instance: ${instancePath}\n` +
|
`Instance: ${instancePath}\n` +
|
||||||
`Type: account\n` +
|
`Type: account\n` +
|
||||||
`Context: ${formatAccountContextPolicy(contextPolicy)}`,
|
`Context: ${formatAccountContextPolicy(contextPolicy)}` +
|
||||||
|
(bare ? '\nMode: bare (no shared symlinks)' : ''),
|
||||||
'Profile Created'
|
'Profile Created'
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -63,6 +63,7 @@ export async function handleShow(ctx: CommandContext, args: string[]): Promise<v
|
|||||||
continuity_mode: contextPolicy.mode === 'shared' ? contextPolicy.continuityMode : null,
|
continuity_mode: contextPolicy.mode === 'shared' ? contextPolicy.continuityMode : null,
|
||||||
instance_path: instancePath,
|
instance_path: instancePath,
|
||||||
session_count: sessionCount,
|
session_count: sessionCount,
|
||||||
|
...(profile.bare ? { bare: true } : {}),
|
||||||
};
|
};
|
||||||
console.log(JSON.stringify(output, null, 2));
|
console.log(JSON.stringify(output, null, 2));
|
||||||
return;
|
return;
|
||||||
@@ -80,6 +81,7 @@ export async function handleShow(ctx: CommandContext, args: string[]): Promise<v
|
|||||||
['Created', new Date(profile.created).toLocaleString()],
|
['Created', new Date(profile.created).toLocaleString()],
|
||||||
['Last Used', profile.last_used ? new Date(profile.last_used).toLocaleString() : 'Never'],
|
['Last Used', profile.last_used ? new Date(profile.last_used).toLocaleString() : 'Never'],
|
||||||
['Context', formatAccountContextPolicy(contextPolicy)],
|
['Context', formatAccountContextPolicy(contextPolicy)],
|
||||||
|
...(profile.bare ? [['Bare', 'yes (no shared symlinks)']] : []),
|
||||||
['Sessions', `${sessionCount}`],
|
['Sessions', `${sessionCount}`],
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ export interface AuthCommandArgs {
|
|||||||
shareContext?: boolean;
|
shareContext?: boolean;
|
||||||
contextGroup?: string;
|
contextGroup?: string;
|
||||||
deeperContinuity?: boolean;
|
deeperContinuity?: boolean;
|
||||||
|
bare?: boolean;
|
||||||
unknownFlags?: string[];
|
unknownFlags?: string[];
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -39,6 +40,7 @@ export interface ProfileOutput {
|
|||||||
continuity_mode?: 'standard' | 'deeper' | null;
|
continuity_mode?: 'standard' | 'deeper' | null;
|
||||||
instance_path?: string;
|
instance_path?: string;
|
||||||
session_count?: number;
|
session_count?: number;
|
||||||
|
bare?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -73,6 +75,7 @@ export function parseArgs(args: string[]): AuthCommandArgs {
|
|||||||
'-y',
|
'-y',
|
||||||
'--share-context',
|
'--share-context',
|
||||||
'--deeper-continuity',
|
'--deeper-continuity',
|
||||||
|
'--bare',
|
||||||
]);
|
]);
|
||||||
const knownValueFlags = new Set(['--context-group']);
|
const knownValueFlags = new Set(['--context-group']);
|
||||||
|
|
||||||
@@ -125,6 +128,7 @@ export function parseArgs(args: string[]): AuthCommandArgs {
|
|||||||
yes: args.includes('--yes') || args.includes('-y'),
|
yes: args.includes('--yes') || args.includes('-y'),
|
||||||
shareContext: args.includes('--share-context'),
|
shareContext: args.includes('--share-context'),
|
||||||
deeperContinuity: args.includes('--deeper-continuity'),
|
deeperContinuity: args.includes('--deeper-continuity'),
|
||||||
|
bare: args.includes('--bare'),
|
||||||
contextGroup,
|
contextGroup,
|
||||||
unknownFlags: [...unknownFlags],
|
unknownFlags: [...unknownFlags],
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -156,7 +156,7 @@ Run ${color('ccs config', 'command')} for web dashboard`.trim();
|
|||||||
['ccs auth --help', 'Show account management commands'],
|
['ccs auth --help', 'Show account management commands'],
|
||||||
[
|
[
|
||||||
'ccs auth create <name>',
|
'ccs auth create <name>',
|
||||||
'Create account profile (supports shared groups + --deeper-continuity)',
|
'Create account profile (supports --bare, shared groups, --deeper-continuity)',
|
||||||
],
|
],
|
||||||
['ccs config', 'Dashboard: Accounts table can edit context mode/group/continuity depth'],
|
['ccs config', 'Dashboard: Accounts table can edit context mode/group/continuity depth'],
|
||||||
[
|
[
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
* Handle sync command for CCS.
|
* Handle sync command for CCS.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { initUI, header, ok } from '../utils/ui';
|
import { initUI, header, ok, info } from '../utils/ui';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Handle sync command
|
* Handle sync command
|
||||||
@@ -41,6 +41,31 @@ export async function handleSyncCommand(): Promise<void> {
|
|||||||
sharedManager.ensureSharedDirectories();
|
sharedManager.ensureSharedDirectories();
|
||||||
console.log(ok('Shared symlinks verified'));
|
console.log(ok('Shared symlinks verified'));
|
||||||
|
|
||||||
|
// Sync MCP servers from global ~/.claude.json to all non-bare instances
|
||||||
|
const { InstanceManager } = await import('../management/instance-manager');
|
||||||
|
const instanceMgr = new InstanceManager();
|
||||||
|
const ProfileRegistry = (await import('../auth/profile-registry')).default;
|
||||||
|
const registry = new ProfileRegistry();
|
||||||
|
const allProfiles = registry.getAllProfilesMerged();
|
||||||
|
let mcpSynced = 0;
|
||||||
|
|
||||||
|
for (const [name, profile] of Object.entries(allProfiles)) {
|
||||||
|
if (profile.bare) {
|
||||||
|
continue; // Skip bare profiles
|
||||||
|
}
|
||||||
|
const instancePath = instanceMgr.getInstancePath(name);
|
||||||
|
if (instancePath) {
|
||||||
|
instanceMgr.syncMcpServers(instancePath);
|
||||||
|
mcpSynced++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (mcpSynced > 0) {
|
||||||
|
console.log(ok(`MCP servers synced to ${mcpSynced} instance(s)`));
|
||||||
|
} else {
|
||||||
|
console.log(info('No instances to sync MCP servers'));
|
||||||
|
}
|
||||||
|
|
||||||
console.log('');
|
console.log('');
|
||||||
console.log(ok('Sync complete!'));
|
console.log(ok('Sync complete!'));
|
||||||
console.log('');
|
console.log('');
|
||||||
|
|||||||
@@ -46,6 +46,8 @@ export interface AccountConfig {
|
|||||||
context_group?: string;
|
context_group?: string;
|
||||||
/** Shared continuity depth when context_mode='shared' */
|
/** Shared continuity depth when context_mode='shared' */
|
||||||
continuity_mode?: 'standard' | 'deeper';
|
continuity_mode?: 'standard' | 'deeper';
|
||||||
|
/** Bare profile: no shared symlinks (commands, skills, agents, settings.json) */
|
||||||
|
bare?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -11,7 +11,13 @@ import * as path from 'path';
|
|||||||
import SharedManager from './shared-manager';
|
import SharedManager from './shared-manager';
|
||||||
import ProfileContextSyncLock from './profile-context-sync-lock';
|
import ProfileContextSyncLock from './profile-context-sync-lock';
|
||||||
import { AccountContextPolicy, DEFAULT_ACCOUNT_CONTEXT_MODE } from '../auth/account-context';
|
import { AccountContextPolicy, DEFAULT_ACCOUNT_CONTEXT_MODE } from '../auth/account-context';
|
||||||
import { getCcsDir } from '../utils/config-manager';
|
import { getCcsDir, getCcsHome } from '../utils/config-manager';
|
||||||
|
|
||||||
|
/** Options for instance creation */
|
||||||
|
interface InstanceOptions {
|
||||||
|
/** Skip shared symlinks (commands, skills, agents, settings.json) */
|
||||||
|
bare?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Instance Manager Class
|
* Instance Manager Class
|
||||||
@@ -32,7 +38,8 @@ class InstanceManager {
|
|||||||
*/
|
*/
|
||||||
async ensureInstance(
|
async ensureInstance(
|
||||||
profileName: string,
|
profileName: string,
|
||||||
contextPolicy: AccountContextPolicy = { mode: DEFAULT_ACCOUNT_CONTEXT_MODE }
|
contextPolicy: AccountContextPolicy = { mode: DEFAULT_ACCOUNT_CONTEXT_MODE },
|
||||||
|
options: InstanceOptions = {}
|
||||||
): Promise<string> {
|
): Promise<string> {
|
||||||
const instancePath = this.getInstancePath(profileName);
|
const instancePath = this.getInstancePath(profileName);
|
||||||
|
|
||||||
@@ -40,7 +47,7 @@ class InstanceManager {
|
|||||||
await this.contextSyncLock.withLock(profileName, async () => {
|
await this.contextSyncLock.withLock(profileName, async () => {
|
||||||
// Lazy initialization
|
// Lazy initialization
|
||||||
if (!fs.existsSync(instancePath)) {
|
if (!fs.existsSync(instancePath)) {
|
||||||
this.initializeInstance(profileName, instancePath);
|
this.initializeInstance(profileName, instancePath, options);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Validate structure (auto-fix missing dirs)
|
// Validate structure (auto-fix missing dirs)
|
||||||
@@ -51,6 +58,11 @@ class InstanceManager {
|
|||||||
await this.sharedManager.syncAdvancedContinuityArtifacts(instancePath, contextPolicy);
|
await this.sharedManager.syncAdvancedContinuityArtifacts(instancePath, contextPolicy);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Sync MCP servers from global ~/.claude.json (unless bare)
|
||||||
|
if (!options.bare) {
|
||||||
|
this.syncMcpServers(instancePath);
|
||||||
|
}
|
||||||
|
|
||||||
return instancePath;
|
return instancePath;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -65,7 +77,11 @@ class InstanceManager {
|
|||||||
/**
|
/**
|
||||||
* Initialize new instance directory
|
* Initialize new instance directory
|
||||||
*/
|
*/
|
||||||
private initializeInstance(profileName: string, instancePath: string): void {
|
private initializeInstance(
|
||||||
|
profileName: string,
|
||||||
|
instancePath: string,
|
||||||
|
options: InstanceOptions = {}
|
||||||
|
): void {
|
||||||
try {
|
try {
|
||||||
// Create base directory
|
// Create base directory
|
||||||
fs.mkdirSync(instancePath, { recursive: true, mode: 0o700 });
|
fs.mkdirSync(instancePath, { recursive: true, mode: 0o700 });
|
||||||
@@ -88,8 +104,10 @@ class InstanceManager {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Symlink shared directories (Phase 1: commands, skills)
|
// Bare profiles skip shared symlinks (commands, skills, agents, settings.json)
|
||||||
this.sharedManager.linkSharedDirectories(instancePath);
|
if (!options.bare) {
|
||||||
|
this.sharedManager.linkSharedDirectories(instancePath);
|
||||||
|
}
|
||||||
|
|
||||||
// Copy global configs if exist (settings.json only)
|
// Copy global configs if exist (settings.json only)
|
||||||
this.copyGlobalConfigs(instancePath);
|
this.copyGlobalConfigs(instancePath);
|
||||||
@@ -167,33 +185,49 @@ class InstanceManager {
|
|||||||
*/
|
*/
|
||||||
private copyGlobalConfigs(_instancePath: string): void {
|
private copyGlobalConfigs(_instancePath: string): void {
|
||||||
// No longer needed - settings.json now symlinked via SharedManager
|
// No longer needed - settings.json now symlinked via SharedManager
|
||||||
// Keeping method for backward compatibility (empty implementation)
|
|
||||||
// Can be removed in future major version
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Copy directory recursively - Currently unused
|
* Sync MCP servers from global ~/.claude.json to instance .claude.json.
|
||||||
|
* Selectively copies only mcpServers key (not OAuth sessions or caches).
|
||||||
*/
|
*/
|
||||||
/*
|
syncMcpServers(instancePath: string): void {
|
||||||
private copyDirectory(src: string, dest: string): void {
|
const homeDir = getCcsHome();
|
||||||
if (!fs.existsSync(dest)) {
|
const globalClaudeJson = path.join(homeDir, '.claude.json');
|
||||||
fs.mkdirSync(dest, { recursive: true, mode: 0o700 });
|
|
||||||
|
if (!fs.existsSync(globalClaudeJson)) {
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const entries = fs.readdirSync(src, { withFileTypes: true });
|
try {
|
||||||
|
const globalContent = JSON.parse(fs.readFileSync(globalClaudeJson, 'utf8'));
|
||||||
|
const mcpServers = globalContent.mcpServers;
|
||||||
|
|
||||||
for (const entry of entries) {
|
if (!mcpServers || Object.keys(mcpServers).length === 0) {
|
||||||
const srcPath = path.join(src, entry.name);
|
return;
|
||||||
const destPath = path.join(dest, entry.name);
|
|
||||||
|
|
||||||
if (entry.isDirectory()) {
|
|
||||||
this.copyDirectory(srcPath, destPath);
|
|
||||||
} else {
|
|
||||||
fs.copyFileSync(srcPath, destPath);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const instanceClaudeJson = path.join(instancePath, '.claude.json');
|
||||||
|
let instanceContent: Record<string, unknown> = {};
|
||||||
|
|
||||||
|
if (fs.existsSync(instanceClaudeJson)) {
|
||||||
|
try {
|
||||||
|
instanceContent = JSON.parse(fs.readFileSync(instanceClaudeJson, 'utf8'));
|
||||||
|
} catch {
|
||||||
|
// Corrupted file, start fresh
|
||||||
|
instanceContent = {};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Merge: global MCP servers as base, instance-specific overrides on top
|
||||||
|
const existingMcp = (instanceContent.mcpServers as Record<string, unknown> | undefined) || {};
|
||||||
|
instanceContent.mcpServers = { ...mcpServers, ...existingMcp };
|
||||||
|
|
||||||
|
fs.writeFileSync(instanceClaudeJson, JSON.stringify(instanceContent, null, 2), 'utf8');
|
||||||
|
} catch {
|
||||||
|
// Best-effort: don't fail instance creation if MCP sync fails
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
*/
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sanitize profile name for filesystem
|
* Sanitize profile name for filesystem
|
||||||
|
|||||||
@@ -100,6 +100,8 @@ export interface ProfileMetadata {
|
|||||||
context_group?: string;
|
context_group?: string;
|
||||||
/** Shared continuity depth when context_mode='shared' */
|
/** Shared continuity depth when context_mode='shared' */
|
||||||
continuity_mode?: 'standard' | 'deeper';
|
continuity_mode?: 'standard' | 'deeper';
|
||||||
|
/** Bare profile: no shared symlinks (commands, skills, agents, settings.json) */
|
||||||
|
bare?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ProfilesRegistry {
|
export interface ProfilesRegistry {
|
||||||
|
|||||||
Reference in New Issue
Block a user