1.0.8
This commit is contained in:
1 parent
027aee434c
commit
62636426bf
15 files changed
+317
-65
No files matched your search
@@ -420,38 +420,61 @@
|
||||
// Effect to update variable markers
|
||||
const updateMarkersEffect = StateEffect.define<VariableMarker[]>();
|
||||
|
||||
// State field to store current markers (used for recalculation on doc change)
|
||||
const currentMarkersField = StateField.define<VariableMarker[]>({
|
||||
create() {
|
||||
return [];
|
||||
},
|
||||
update(markers, tr) {
|
||||
for (const effect of tr.effects) {
|
||||
if (effect.is(updateMarkersEffect)) {
|
||||
return effect.value;
|
||||
}
|
||||
}
|
||||
return markers;
|
||||
}
|
||||
});
|
||||
|
||||
// State field to track variable markers (gutter)
|
||||
// IMPORTANT: Only updates via effects, not closure reference (fixes stale closure bug)
|
||||
// Recalculates on doc change to avoid position mapping issues
|
||||
const variableMarkersField = StateField.define<RangeSet<GutterMarker>>({
|
||||
create() {
|
||||
// Start empty - markers will be pushed via effect
|
||||
return RangeSet.empty;
|
||||
},
|
||||
update(markers, tr) {
|
||||
// Check for marker updates first
|
||||
for (const effect of tr.effects) {
|
||||
if (effect.is(updateMarkersEffect)) {
|
||||
return createVariableDecorations(tr.state.doc, effect.value);
|
||||
}
|
||||
}
|
||||
// Don't recalculate on docChanged - wait for explicit effect from parent
|
||||
// Recalculate on doc change using stored markers
|
||||
if (tr.docChanged) {
|
||||
const currentMarkers = tr.state.field(currentMarkersField);
|
||||
return createVariableDecorations(tr.state.doc, currentMarkers);
|
||||
}
|
||||
return markers;
|
||||
}
|
||||
});
|
||||
|
||||
// State field to track value decorations (inline widgets)
|
||||
// IMPORTANT: Only updates via effects, not closure reference (fixes stale closure bug)
|
||||
// Recalculates on doc change to avoid widget duplication issues
|
||||
const valueDecorationsField = StateField.define<DecorationSet>({
|
||||
create() {
|
||||
// Start empty - decorations will be pushed via effect
|
||||
return Decoration.none;
|
||||
},
|
||||
update(decorations, tr) {
|
||||
// Check for marker updates first
|
||||
for (const effect of tr.effects) {
|
||||
if (effect.is(updateMarkersEffect)) {
|
||||
return createValueDecorations(tr.state.doc, effect.value);
|
||||
}
|
||||
}
|
||||
// Don't recalculate on docChanged - wait for explicit effect from parent
|
||||
// Recalculate on doc change using stored markers
|
||||
if (tr.docChanged) {
|
||||
const currentMarkers = tr.state.field(currentMarkersField);
|
||||
return createValueDecorations(tr.state.doc, currentMarkers);
|
||||
}
|
||||
return decorations;
|
||||
},
|
||||
provide: f => EditorView.decorations.from(f)
|
||||
@@ -647,7 +670,7 @@
|
||||
}
|
||||
|
||||
// Always add variable markers gutter and value decorations (can be updated dynamically)
|
||||
extensions.push(variableMarkersField, variableGutter, valueDecorationsField);
|
||||
extensions.push(currentMarkersField, variableMarkersField, variableGutter, valueDecorationsField);
|
||||
|
||||
const state = EditorState.create({
|
||||
doc: value,
|
||||
@@ -666,14 +689,11 @@
|
||||
// Skip onchange during programmatic value sync (only fire for user edits)
|
||||
const lastChangingTr = trs.findLast(tr => tr.docChanged);
|
||||
if (lastChangingTr && onchangeRef && !isSyncingExternalValue) {
|
||||
// Defer callback to next microtask to avoid blocking input handling
|
||||
// This allows key repeat to work properly
|
||||
// Call synchronously to ensure parent state updates before any
|
||||
// reactive $effect runs - this prevents race conditions on iPad Safari
|
||||
// where paste content was being overwritten by stale external value
|
||||
const newContent = lastChangingTr.newDoc.toString();
|
||||
queueMicrotask(() => {
|
||||
if (onchangeRef) {
|
||||
onchangeRef(newContent);
|
||||
}
|
||||
});
|
||||
onchangeRef(newContent);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -104,7 +104,7 @@
|
||||
<div class="space-y-3">
|
||||
<!-- Variables List -->
|
||||
<div class="space-y-3">
|
||||
{#each variables as variable, index (`${index}-${variable.key}`)}
|
||||
{#each variables as variable, index (index)}
|
||||
{@const source = getSource(variable.key)}
|
||||
{@const isVarRequired = isRequired(variable.key)}
|
||||
{@const isVarOptional = isOptional(variable.key)}
|
||||
|
||||
@@ -1,4 +1,18 @@
|
||||
[
|
||||
{
|
||||
"version": "1.0.8",
|
||||
"date": "2026-01-13",
|
||||
"changes": [
|
||||
{ "type": "fix", "text": "Fix imported stack working directory for relative volume paths" },
|
||||
{ "type": "fix", "text": "Fix environment refresh after auth login" },
|
||||
{ "type": "fix", "text": "Fix single container update clearing up all update badges" },
|
||||
{ "type": "fix", "text": "Fix code editor paste issue on Safari on iPad" },
|
||||
{ "type": "fix", "text": "Fix registry login failing due to Bun stdin API incompatibility" },
|
||||
{ "type": "fix", "text": "Fix env var editor focus issues" },
|
||||
{ "type": "fix", "text": "Fix git stack naming issues: validation, rename sync, and delete cleanup" }
|
||||
],
|
||||
"imageTag": "fnsys/dockhand:v1.0.8"
|
||||
},
|
||||
{
|
||||
"version": "1.0.7",
|
||||
"date": "2026-01-06",
|
||||
|
||||
@@ -2643,6 +2643,25 @@ export async function deleteStackSource(stackName: string, environmentId?: numbe
|
||||
return true;
|
||||
}
|
||||
|
||||
export async function updateStackSourceName(
|
||||
oldStackName: string,
|
||||
newStackName: string,
|
||||
environmentId?: number | null
|
||||
): Promise<boolean> {
|
||||
await db.update(stackSources)
|
||||
.set({
|
||||
stackName: newStackName,
|
||||
updatedAt: new Date().toISOString()
|
||||
})
|
||||
.where(and(
|
||||
eq(stackSources.stackName, oldStackName),
|
||||
environmentId !== undefined && environmentId !== null
|
||||
? eq(stackSources.environmentId, environmentId)
|
||||
: isNull(stackSources.environmentId)
|
||||
));
|
||||
return true;
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// VULNERABILITY SCAN RESULTS
|
||||
// =============================================================================
|
||||
|
||||
+96
-23
@@ -688,10 +688,9 @@ async function loginToRegistries(dockerHost?: string, logPrefix = '[Stack]'): Pr
|
||||
}
|
||||
);
|
||||
|
||||
// Write password to stdin
|
||||
const writer = proc.stdin.getWriter();
|
||||
await writer.write(new TextEncoder().encode(reg.password));
|
||||
await writer.close();
|
||||
// Write password to stdin (Bun's FileSink API)
|
||||
proc.stdin.write(reg.password);
|
||||
proc.stdin.end();
|
||||
|
||||
const exitCode = await proc.exited;
|
||||
|
||||
@@ -717,6 +716,10 @@ interface ComposeCommandOptions {
|
||||
forceRecreate?: boolean;
|
||||
removeVolumes?: boolean;
|
||||
stackFiles?: Record<string, string>; // All files to send to Hawser
|
||||
/** Working directory for compose execution (for imported stacks) */
|
||||
workingDir?: string;
|
||||
/** Full path to the compose file (for imported stacks, to avoid writing to internal dir) */
|
||||
composePath?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -724,6 +727,8 @@ interface ComposeCommandOptions {
|
||||
*
|
||||
* @param envVars - Non-secret environment variables (from .env file, passed for backward compat)
|
||||
* @param secretVars - Secret environment variables (injected via shell env, NEVER written to disk)
|
||||
* @param workingDir - Optional working directory for compose execution (for imported stacks)
|
||||
* @param customComposePath - Optional path to existing compose file (for imported stacks, skips writing)
|
||||
*/
|
||||
async function executeLocalCompose(
|
||||
operation: 'up' | 'down' | 'stop' | 'start' | 'restart' | 'pull',
|
||||
@@ -734,17 +739,33 @@ async function executeLocalCompose(
|
||||
secretVars?: Record<string, string>,
|
||||
forceRecreate?: boolean,
|
||||
removeVolumes?: boolean,
|
||||
envId?: number | null
|
||||
envId?: number | null,
|
||||
workingDir?: string,
|
||||
customComposePath?: string
|
||||
): Promise<StackOperationResult> {
|
||||
const logPrefix = `[Stack:${stackName}]`;
|
||||
// For operations that write (up), use getStackDir; for others, try to find existing first
|
||||
const stackDir = operation === 'up'
|
||||
? await getStackDir(stackName, envId)
|
||||
: (await findStackDir(stackName, envId) || await getStackDir(stackName, envId));
|
||||
mkdirSync(stackDir, { recursive: true });
|
||||
|
||||
const composeFile = join(stackDir, 'docker-compose.yml');
|
||||
await Bun.write(composeFile, composeContent);
|
||||
// Determine working directory and compose file path
|
||||
// For imported stacks (custom paths), use the provided workingDir and composePath
|
||||
// For internal stacks, use the default data directory
|
||||
let stackDir: string;
|
||||
let composeFile: string;
|
||||
|
||||
if (customComposePath && workingDir) {
|
||||
// Imported stack: use original location, don't copy files
|
||||
stackDir = workingDir;
|
||||
composeFile = customComposePath;
|
||||
// Don't write to the compose file - it already exists at the custom location
|
||||
// The user manages this file externally
|
||||
} else {
|
||||
// Internal stack: use default data directory
|
||||
stackDir = operation === 'up'
|
||||
? await getStackDir(stackName, envId)
|
||||
: (await findStackDir(stackName, envId) || await getStackDir(stackName, envId));
|
||||
mkdirSync(stackDir, { recursive: true });
|
||||
composeFile = join(stackDir, 'docker-compose.yml');
|
||||
await Bun.write(composeFile, composeContent);
|
||||
}
|
||||
|
||||
// Build spawn environment:
|
||||
// 1. Start with process.env
|
||||
@@ -1042,7 +1063,7 @@ async function executeComposeCommand(
|
||||
envVars?: Record<string, string>,
|
||||
secretVars?: Record<string, string>
|
||||
): Promise<StackOperationResult> {
|
||||
const { stackName, envId, forceRecreate, removeVolumes, stackFiles } = options;
|
||||
const { stackName, envId, forceRecreate, removeVolumes, stackFiles, workingDir, composePath } = options;
|
||||
|
||||
// Get environment configuration
|
||||
const env = envId ? await getEnvironment(envId) : null;
|
||||
@@ -1058,7 +1079,9 @@ async function executeComposeCommand(
|
||||
secretVars,
|
||||
forceRecreate,
|
||||
removeVolumes,
|
||||
envId
|
||||
envId,
|
||||
workingDir,
|
||||
composePath
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1089,7 +1112,9 @@ async function executeComposeCommand(
|
||||
secretVars,
|
||||
forceRecreate,
|
||||
removeVolumes,
|
||||
envId
|
||||
envId,
|
||||
workingDir,
|
||||
composePath
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1104,7 +1129,9 @@ async function executeComposeCommand(
|
||||
secretVars,
|
||||
forceRecreate,
|
||||
removeVolumes,
|
||||
envId
|
||||
envId,
|
||||
workingDir,
|
||||
composePath
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1313,6 +1340,10 @@ export interface RequireComposeResult {
|
||||
secretVars?: Record<string, string>;
|
||||
needsFileLocation?: boolean;
|
||||
error?: string;
|
||||
/** Directory containing the compose file (for working directory) */
|
||||
stackDir?: string;
|
||||
/** Full path to the compose file (for imported stacks) */
|
||||
composePath?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1400,7 +1431,14 @@ async function requireComposeFile(
|
||||
// This ensures external edits to .env are respected during deployment
|
||||
const envVars = { ...dbNonSecretVars, ...fileEnvVars };
|
||||
|
||||
return { success: true, content: composeResult.content!, envVars, secretVars };
|
||||
return {
|
||||
success: true,
|
||||
content: composeResult.content!,
|
||||
envVars,
|
||||
secretVars,
|
||||
stackDir: composeResult.stackDir,
|
||||
composePath: composeResult.composePath
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1418,7 +1456,13 @@ export async function startStack(
|
||||
return withContainerFallback(stackName, envId, 'start');
|
||||
}
|
||||
|
||||
return executeComposeCommand('up', { stackName, envId }, result.content!, result.envVars, result.secretVars);
|
||||
return executeComposeCommand(
|
||||
'up',
|
||||
{ stackName, envId, workingDir: result.stackDir, composePath: result.composePath },
|
||||
result.content!,
|
||||
result.envVars,
|
||||
result.secretVars
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1436,7 +1480,13 @@ export async function stopStack(
|
||||
return withContainerFallback(stackName, envId, 'stop');
|
||||
}
|
||||
|
||||
return executeComposeCommand('stop', { stackName, envId }, result.content!, result.envVars, result.secretVars);
|
||||
return executeComposeCommand(
|
||||
'stop',
|
||||
{ stackName, envId, workingDir: result.stackDir, composePath: result.composePath },
|
||||
result.content!,
|
||||
result.envVars,
|
||||
result.secretVars
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1454,7 +1504,13 @@ export async function restartStack(
|
||||
return withContainerFallback(stackName, envId, 'restart');
|
||||
}
|
||||
|
||||
return executeComposeCommand('restart', { stackName, envId }, result.content!, result.envVars, result.secretVars);
|
||||
return executeComposeCommand(
|
||||
'restart',
|
||||
{ stackName, envId, workingDir: result.stackDir, composePath: result.composePath },
|
||||
result.content!,
|
||||
result.envVars,
|
||||
result.secretVars
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1473,7 +1529,13 @@ export async function downStack(
|
||||
return withContainerFallback(stackName, envId, 'stop');
|
||||
}
|
||||
|
||||
return executeComposeCommand('down', { stackName, envId, removeVolumes }, result.content!, result.envVars, result.secretVars);
|
||||
return executeComposeCommand(
|
||||
'down',
|
||||
{ stackName, envId, removeVolumes, workingDir: result.stackDir, composePath: result.composePath },
|
||||
result.content!,
|
||||
result.envVars,
|
||||
result.secretVars
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1495,7 +1557,12 @@ export async function removeStack(
|
||||
const secretVars = await getSecretEnvVarsAsRecord(stackName, envId);
|
||||
const downResult = await executeComposeCommand(
|
||||
'down',
|
||||
{ stackName, envId },
|
||||
{
|
||||
stackName,
|
||||
envId,
|
||||
workingDir: composeResult.stackDir,
|
||||
composePath: composeResult.composePath
|
||||
},
|
||||
composeResult.content!,
|
||||
envVars,
|
||||
secretVars
|
||||
@@ -1710,7 +1777,13 @@ export async function pullStackImages(
|
||||
};
|
||||
}
|
||||
|
||||
return executeComposeCommand('pull', { stackName, envId }, result.content!, result.envVars, result.secretVars);
|
||||
return executeComposeCommand(
|
||||
'pull',
|
||||
{ stackName, envId, workingDir: result.stackDir, composePath: result.composePath },
|
||||
result.content!,
|
||||
result.envVars,
|
||||
result.secretVars
|
||||
);
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
|
||||
Reference in new issue
Block a user