Skip to content
Merged
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
14 changes: 12 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@
"prettier": "^3.5.3",
"prettier-plugin-svelte": "^3.3.3",
"proper-lockfile": "^4.1.2",
"slugify": "^1.6.6",
"svelte-codemirror-editor": "^1.4.1",
"svelte-kit-cookie-session": "^4.1.1",
"thememirror": "^2.0.1"
Expand Down
5 changes: 5 additions & 0 deletions src/lib/services/api/stack-api-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,11 @@ export default class StackAPIService extends BaseAPIService {
return res.data;
}

async migrate(id: string) {
const res = await this.api.post(`/stacks/${id}/migrate`);
return res.data;
}

async list() {
const res = await this.api.get('');
return res.data;
Expand Down
195 changes: 195 additions & 0 deletions src/lib/services/docker/stack-migration-service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,195 @@
import fs from 'node:fs/promises';
import path from 'node:path';
import slugify from 'slugify';
import { ensureStacksDir, stopStack, startStack, isStackRunning } from './stack-service';

export async function migrateStacksToNameFolders() {
const stacksDir = await ensureStacksDir();
const dirs = await fs.readdir(stacksDir);

for (const dir of dirs) {
const oldDirPath = path.join(stacksDir, dir);

// Only process directories
const stat = await fs.stat(oldDirPath);
if (!stat.isDirectory()) continue;

const metaPath = path.join(oldDirPath, 'meta.json');
const newMetaPath = path.join(oldDirPath, '.stack.json');
const oldComposePath = path.join(oldDirPath, 'docker-compose.yml');

Check failure

Code scanning / ESLint

Disallow unused variables Error

'oldComposePath' is assigned a value but never used.
const newComposePath = path.join(oldDirPath, 'compose.yaml');

Check failure

Code scanning / ESLint

Disallow unused variables Error

'newComposePath' is assigned a value but never used.

// Only migrate if meta.json exists and .stack.json does not
try {
await fs.access(metaPath);
} catch {
continue; // No meta.json, skip
}
try {
await fs.access(newMetaPath);
continue; // Already migrated
} catch {}

Check failure

Code scanning / ESLint

Disallow empty block statements Error

Empty block statement.

// Check if stack is running before migration
let wasRunning = false;
try {
wasRunning = await isStackRunning(dir);
} catch {
wasRunning = false;
}

// Stop the stack before migration
try {
await stopStack(dir);
console.log(`Stopped stack "${dir}" before migration.`);
} catch (err) {
console.warn(`Failed to stop stack "${dir}" before migration:`, err);
}

// Read and parse meta.json
const metaRaw = await fs.readFile(metaPath, 'utf8');
const meta = JSON.parse(metaRaw);

// Generate new directory name
const slug = slugify(meta.name, { lower: true, strict: true, trim: true });
let newDirName = slug;
let counter = 1;
while (dirs.includes(newDirName) && newDirName !== dir) {
newDirName = `${slug}-${counter++}`;
}
const newDirPath = path.join(stacksDir, newDirName);

// Rename directory if needed
if (newDirName !== dir) {
await fs.rename(oldDirPath, newDirPath);
}

// Migrate docker-compose.yml to compose.yaml if needed
try {
await fs.access(path.join(newDirPath, 'docker-compose.yml'));
try {
await fs.access(path.join(newDirPath, 'compose.yaml'));
// compose.yaml already exists, do nothing
} catch {
await fs.rename(path.join(newDirPath, 'docker-compose.yml'), path.join(newDirPath, 'compose.yaml'));
console.log(`Migrated docker-compose.yml to compose.yaml in "${newDirName}"`);
}
} catch {
// docker-compose.yml does not exist, nothing to do
}

// Update and write .stack.json
meta.dirName = newDirName;
meta.path = newDirPath;
await fs.writeFile(path.join(newDirPath, '.stack.json'), JSON.stringify(meta, null, 2), 'utf8');

// Remove old meta.json
await fs.rm(path.join(newDirPath, 'meta.json'));

console.log(`Migrated stack "${meta.name}" to folder "${newDirName}"`);

// Start the stack after migration if it was running before
if (wasRunning) {
try {
await startStack(newDirName);
console.log(`Started stack "${newDirName}" after migration.`);
} catch (err) {
console.warn(`Failed to start stack "${newDirName}" after migration:`, err);
}
}
}
}

export async function migrateStackToNameFolder(stackId: string): Promise<void> {
const stacksDir = await ensureStacksDir();
const oldDirPath = path.join(stacksDir, stackId);

// Only process if directory exists
const stat = await fs.stat(oldDirPath);
if (!stat.isDirectory()) throw new Error(`Stack directory "${stackId}" does not exist`);

const metaPath = path.join(oldDirPath, 'meta.json');
const newMetaPath = path.join(oldDirPath, '.stack.json');
const oldComposePath = path.join(oldDirPath, 'docker-compose.yml');

Check failure

Code scanning / ESLint

Disallow unused variables Error

'oldComposePath' is assigned a value but never used.
const newComposePath = path.join(oldDirPath, 'compose.yaml');

Check failure

Code scanning / ESLint

Disallow unused variables Error

'newComposePath' is assigned a value but never used.

// Only migrate if meta.json exists and .stack.json does not
try {
await fs.access(metaPath);
} catch {
throw new Error(`No meta.json found for stack "${stackId}"`);
}
try {
await fs.access(newMetaPath);
throw new Error(`Stack "${stackId}" is already migrated`);
} catch {}

Check failure

Code scanning / ESLint

Disallow empty block statements Error

Empty block statement.

// Check if stack is running before migration
let wasRunning = false;
try {
wasRunning = await isStackRunning(stackId);
} catch {
wasRunning = false;
}

// Stop the stack before migration
try {
await stopStack(stackId);
console.log(`Stopped stack "${stackId}" before migration.`);
} catch (err) {
console.warn(`Failed to stop stack "${stackId}" before migration:`, err);
}

// Read and parse meta.json
const metaRaw = await fs.readFile(metaPath, 'utf8');
const meta = JSON.parse(metaRaw);

// Generate new directory name
const slug = slugify(meta.name, { lower: true, strict: true, trim: true });
let newDirName = slug;
let counter = 1;
const dirs = await fs.readdir(stacksDir);
while (dirs.includes(newDirName) && newDirName !== stackId) {
newDirName = `${slug}-${counter++}`;
}
const newDirPath = path.join(stacksDir, newDirName);

// Rename directory if needed
if (newDirName !== stackId) {
await fs.rename(oldDirPath, newDirPath);
}

// Migrate docker-compose.yml to compose.yaml if needed
try {
await fs.access(path.join(newDirPath, 'docker-compose.yml'));
try {
await fs.access(path.join(newDirPath, 'compose.yaml'));
// compose.yaml already exists, do nothing
} catch {
await fs.rename(path.join(newDirPath, 'docker-compose.yml'), path.join(newDirPath, 'compose.yaml'));
console.log(`Migrated docker-compose.yml to compose.yaml in "${newDirName}"`);
}
} catch {
// docker-compose.yml does not exist, nothing to do
}

// Update and write .stack.json
meta.dirName = newDirName;
meta.path = newDirPath;
await fs.writeFile(path.join(newDirPath, '.stack.json'), JSON.stringify(meta, null, 2), 'utf8');

// Remove old meta.json
await fs.rm(path.join(newDirPath, 'meta.json'));

console.log(`Migrated stack "${meta.name}" to folder "${newDirName}"`);

// Start the stack after migration if it was running before
if (wasRunning) {
try {
await startStack(newDirName);
console.log(`Started stack "${newDirName}" after migration.`);
} catch (err) {
console.warn(`Failed to start stack "${newDirName}" after migration:`, err);
}
}
}
Loading
Loading