spindb 0.50.8 → 0.51.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/README.md +13 -0
- package/dist/cli/commands/branch.js +522 -0
- package/dist/cli/commands/branch.js.map +1 -0
- package/dist/cli/commands/info.js +15 -0
- package/dist/cli/commands/info.js.map +1 -1
- package/dist/cli/commands/menu/container-handlers.js +155 -0
- package/dist/cli/commands/menu/container-handlers.js.map +1 -1
- package/dist/cli/commands/menu/index.js +139 -3
- package/dist/cli/commands/menu/index.js.map +1 -1
- package/dist/cli/index.js +2 -0
- package/dist/cli/index.js.map +1 -1
- package/dist/cli/ui/branch-tree.js +30 -0
- package/dist/cli/ui/branch-tree.js.map +1 -0
- package/dist/config/version.js +1 -1
- package/dist/core/branch-manager.js +441 -0
- package/dist/core/branch-manager.js.map +1 -0
- package/dist/core/container-manager.js +117 -79
- package/dist/core/container-manager.js.map +1 -1
- package/dist/core/cow-copy.js +108 -0
- package/dist/core/cow-copy.js.map +1 -0
- package/dist/core/git-branch-sync.js +331 -0
- package/dist/core/git-branch-sync.js.map +1 -0
- package/dist/engines/base-engine.js +18 -0
- package/dist/engines/base-engine.js.map +1 -1
- package/dist/engines/clickhouse/index.js +8 -0
- package/dist/engines/clickhouse/index.js.map +1 -1
- package/dist/engines/ferretdb/index.js +15 -0
- package/dist/engines/ferretdb/index.js.map +1 -1
- package/dist/types/index.js.map +1 -1
- package/package.json +1 -1
|
@@ -5,6 +5,8 @@ import { stat, mkdir, rm } from 'fs/promises';
|
|
|
5
5
|
import { dirname, basename, join, resolve } from 'path';
|
|
6
6
|
import { homedir } from 'os';
|
|
7
7
|
import { containerManager, updateRenameTracking, } from '../../../core/container-manager.js';
|
|
8
|
+
import { branchManager } from '../../../core/branch-manager.js';
|
|
9
|
+
import { initRepo as gitInitRepo, findRepoRoot, } from '../../../core/git-branch-sync.js';
|
|
8
10
|
import { getMissingDependencies } from '../../../core/dependency-manager.js';
|
|
9
11
|
import { platformService } from '../../../core/platform-service.js';
|
|
10
12
|
import { portManager } from '../../../core/port-manager.js';
|
|
@@ -1000,6 +1002,26 @@ export async function showContainerSubmenu(containerName, showMainMenu) {
|
|
|
1000
1002
|
actionChoices.push(canClone
|
|
1001
1003
|
? { name: `${chalk.cyan('◇')} Clone container`, value: 'clone' }
|
|
1002
1004
|
: disabledItem('◇', 'Clone container'));
|
|
1005
|
+
// Branch container - copy-on-write fork. Available even while running: a
|
|
1006
|
+
// running source is briefly stopped, snapshotted, and restarted automatically.
|
|
1007
|
+
actionChoices.push({
|
|
1008
|
+
name: `${chalk.cyan('⎇')} Branch container`,
|
|
1009
|
+
value: 'branch',
|
|
1010
|
+
});
|
|
1011
|
+
// Reset to parent - only for containers that are themselves branches
|
|
1012
|
+
if (config.branchParent) {
|
|
1013
|
+
actionChoices.push({
|
|
1014
|
+
name: `${chalk.yellow('↺')} Reset branch to "${config.branchParent}"`,
|
|
1015
|
+
value: 'reset_branch',
|
|
1016
|
+
});
|
|
1017
|
+
}
|
|
1018
|
+
// Git branching - wire this container to the current git repo (server engines)
|
|
1019
|
+
if (!isFileBasedDB && !config.remote) {
|
|
1020
|
+
actionChoices.push({
|
|
1021
|
+
name: `${chalk.magenta('⎇')} Set up git branching here`,
|
|
1022
|
+
value: 'git_branch_init',
|
|
1023
|
+
});
|
|
1024
|
+
}
|
|
1003
1025
|
// Detach - only for file-based DBs (unregisters without deleting file)
|
|
1004
1026
|
if (isFileBasedDB) {
|
|
1005
1027
|
actionChoices.push({
|
|
@@ -1084,6 +1106,15 @@ export async function showContainerSubmenu(containerName, showMainMenu) {
|
|
|
1084
1106
|
case 'clone':
|
|
1085
1107
|
await handleCloneFromSubmenu(containerName, showMainMenu);
|
|
1086
1108
|
return;
|
|
1109
|
+
case 'branch':
|
|
1110
|
+
await handleBranchFromSubmenu(containerName, showMainMenu);
|
|
1111
|
+
return;
|
|
1112
|
+
case 'reset_branch':
|
|
1113
|
+
await handleResetBranchFromSubmenu(containerName, showMainMenu);
|
|
1114
|
+
return;
|
|
1115
|
+
case 'git_branch_init':
|
|
1116
|
+
await handleGitBranchInit(containerName, showMainMenu);
|
|
1117
|
+
return;
|
|
1087
1118
|
case 'copy':
|
|
1088
1119
|
await handleCopyConnectionString(containerName, activeDatabase);
|
|
1089
1120
|
await showContainerSubmenu(containerName, showMainMenu);
|
|
@@ -1847,6 +1878,111 @@ async function handleCloneFromSubmenu(sourceName, showMainMenu) {
|
|
|
1847
1878
|
await pressEnterToContinue();
|
|
1848
1879
|
}
|
|
1849
1880
|
}
|
|
1881
|
+
async function handleBranchFromSubmenu(sourceName, showMainMenu) {
|
|
1882
|
+
const sourceConfig = await containerManager.getConfig(sourceName);
|
|
1883
|
+
if (!sourceConfig) {
|
|
1884
|
+
console.log(uiError(`Container "${sourceName}" not found`));
|
|
1885
|
+
return;
|
|
1886
|
+
}
|
|
1887
|
+
const { branchName } = await escapeablePrompt([
|
|
1888
|
+
{
|
|
1889
|
+
type: 'input',
|
|
1890
|
+
name: 'branchName',
|
|
1891
|
+
message: 'Name for the new branch:',
|
|
1892
|
+
default: `${sourceName}-branch`,
|
|
1893
|
+
validate: (input) => {
|
|
1894
|
+
if (!input)
|
|
1895
|
+
return 'Name is required';
|
|
1896
|
+
if (!/^[a-zA-Z][a-zA-Z0-9_-]*$/.test(input)) {
|
|
1897
|
+
return 'Name must start with a letter and contain only letters, numbers, hyphens, and underscores';
|
|
1898
|
+
}
|
|
1899
|
+
return true;
|
|
1900
|
+
},
|
|
1901
|
+
},
|
|
1902
|
+
]);
|
|
1903
|
+
if (await containerManager.exists(branchName, { engine: sourceConfig.engine })) {
|
|
1904
|
+
console.log(uiError(`Container "${branchName}" already exists`));
|
|
1905
|
+
await pressEnterToContinue();
|
|
1906
|
+
return;
|
|
1907
|
+
}
|
|
1908
|
+
const spinner = createSpinner(`Branching ${sourceName} → ${branchName}...`);
|
|
1909
|
+
spinner.start();
|
|
1910
|
+
try {
|
|
1911
|
+
const result = await branchManager.createBranch({
|
|
1912
|
+
source: sourceName,
|
|
1913
|
+
name: branchName,
|
|
1914
|
+
});
|
|
1915
|
+
const methodNote = result.method === 'reflink' ? ' (copy-on-write)' : '';
|
|
1916
|
+
spinner.succeed(`Created branch "${branchName}" from "${sourceName}"${methodNote}`);
|
|
1917
|
+
if (result.warning)
|
|
1918
|
+
console.log(uiWarning(result.warning));
|
|
1919
|
+
console.log();
|
|
1920
|
+
console.log(connectionBox(branchName, result.connectionString, result.config.port));
|
|
1921
|
+
await showContainerSubmenu(branchName, showMainMenu);
|
|
1922
|
+
}
|
|
1923
|
+
catch (error) {
|
|
1924
|
+
spinner.fail(`Failed to branch "${sourceName}"`);
|
|
1925
|
+
console.log(uiError(error.message));
|
|
1926
|
+
await pressEnterToContinue();
|
|
1927
|
+
}
|
|
1928
|
+
}
|
|
1929
|
+
async function handleResetBranchFromSubmenu(branchName, showMainMenu) {
|
|
1930
|
+
const config = await containerManager.getConfig(branchName);
|
|
1931
|
+
if (!config) {
|
|
1932
|
+
console.log(uiError(`Container "${branchName}" not found`));
|
|
1933
|
+
return;
|
|
1934
|
+
}
|
|
1935
|
+
if (!config.branchParent) {
|
|
1936
|
+
console.log(uiError(`"${branchName}" is not a branch`));
|
|
1937
|
+
await pressEnterToContinue();
|
|
1938
|
+
return;
|
|
1939
|
+
}
|
|
1940
|
+
const confirmed = await promptConfirm(`Reset "${branchName}" to match "${config.branchParent}"? This discards all changes in the branch.`, false);
|
|
1941
|
+
if (!confirmed) {
|
|
1942
|
+
console.log(uiWarning('Cancelled'));
|
|
1943
|
+
await showContainerSubmenu(branchName, showMainMenu);
|
|
1944
|
+
return;
|
|
1945
|
+
}
|
|
1946
|
+
const spinner = createSpinner(`Resetting ${branchName}...`);
|
|
1947
|
+
spinner.start();
|
|
1948
|
+
try {
|
|
1949
|
+
const result = await branchManager.resetBranch(branchName);
|
|
1950
|
+
spinner.succeed(`Reset "${branchName}" to "${config.branchParent}"`);
|
|
1951
|
+
if (result.warning)
|
|
1952
|
+
console.log(uiWarning(result.warning));
|
|
1953
|
+
await showContainerSubmenu(branchName, showMainMenu);
|
|
1954
|
+
}
|
|
1955
|
+
catch (error) {
|
|
1956
|
+
spinner.fail(`Failed to reset "${branchName}"`);
|
|
1957
|
+
console.log(uiError(error.message));
|
|
1958
|
+
await pressEnterToContinue();
|
|
1959
|
+
}
|
|
1960
|
+
}
|
|
1961
|
+
async function handleGitBranchInit(containerName, showMainMenu) {
|
|
1962
|
+
const repoRoot = await findRepoRoot();
|
|
1963
|
+
if (!repoRoot) {
|
|
1964
|
+
console.log(uiWarning('Not a git repository. Launch spindb from your project directory to set up git branching.'));
|
|
1965
|
+
await pressEnterToContinue();
|
|
1966
|
+
await showContainerSubmenu(containerName, showMainMenu);
|
|
1967
|
+
return;
|
|
1968
|
+
}
|
|
1969
|
+
const spinner = createSpinner('Setting up git branching...');
|
|
1970
|
+
spinner.start();
|
|
1971
|
+
try {
|
|
1972
|
+
const { config } = await gitInitRepo({ baseContainer: containerName });
|
|
1973
|
+
spinner.succeed(`Git branching enabled (stable port ${config.stablePort})`);
|
|
1974
|
+
console.log(chalk.gray(' Switch git branches and the matching database follows automatically.'));
|
|
1975
|
+
console.log(chalk.gray(' Your DATABASE_URL never changes (port stays ') +
|
|
1976
|
+
chalk.green(String(config.stablePort)) +
|
|
1977
|
+
chalk.gray(').'));
|
|
1978
|
+
}
|
|
1979
|
+
catch (error) {
|
|
1980
|
+
spinner.fail('Failed to set up git branching');
|
|
1981
|
+
console.log(uiError(error.message));
|
|
1982
|
+
}
|
|
1983
|
+
await pressEnterToContinue();
|
|
1984
|
+
await showContainerSubmenu(containerName, showMainMenu);
|
|
1985
|
+
}
|
|
1850
1986
|
async function handleDetachContainer(containerName, showMainMenu) {
|
|
1851
1987
|
const config = await containerManager.getConfig(containerName);
|
|
1852
1988
|
if (!config) {
|
|
@@ -1898,6 +2034,25 @@ async function handleDelete(containerName) {
|
|
|
1898
2034
|
console.log(uiWarning(isRemote ? 'Unlink cancelled' : 'Deletion cancelled'));
|
|
1899
2035
|
return;
|
|
1900
2036
|
}
|
|
2037
|
+
// Branch-aware: deleting a container that has child branches would orphan
|
|
2038
|
+
// them. Offer to cascade (matching `spindb branch delete --cascade`).
|
|
2039
|
+
if (!isRemote) {
|
|
2040
|
+
const children = await branchManager.childrenOf(containerName);
|
|
2041
|
+
if (children.length > 0) {
|
|
2042
|
+
const cascade = await promptConfirm(`"${containerName}" has ${children.length} child branch(es): ${children.join(', ')}. Delete them too?`, false);
|
|
2043
|
+
if (!cascade) {
|
|
2044
|
+
console.log(uiWarning('Deletion cancelled — delete or reset the child branches first.'));
|
|
2045
|
+
return;
|
|
2046
|
+
}
|
|
2047
|
+
const cascadeSpinner = createSpinner(`Deleting ${containerName} and its branches...`);
|
|
2048
|
+
cascadeSpinner.start();
|
|
2049
|
+
const result = await branchManager.deleteBranch(containerName, {
|
|
2050
|
+
cascade: true,
|
|
2051
|
+
});
|
|
2052
|
+
cascadeSpinner.succeed(`Deleted ${result.deleted.length} container(s)`);
|
|
2053
|
+
return;
|
|
2054
|
+
}
|
|
2055
|
+
}
|
|
1901
2056
|
// Remote containers: skip process checks
|
|
1902
2057
|
if (!isRemote) {
|
|
1903
2058
|
const running = await processManager.isRunning(containerName, {
|