Added Web WebGateway
Added connector so gateway works. scripts are much more polished and functional.
This commit is contained in:
parent
a72ddf6aec
commit
069b38071c
3
.gitignore
vendored
3
.gitignore
vendored
@ -24,6 +24,9 @@ project.fragment.lock.json
|
||||
.nuget/
|
||||
.dotnet_home/
|
||||
.journal-sidecar/
|
||||
.tmp
|
||||
.npm
|
||||
output/
|
||||
|
||||
# Publish output
|
||||
publish/
|
||||
|
||||
@ -20,10 +20,17 @@ type RuntimeConfig = {
|
||||
vaultDirectory: string;
|
||||
};
|
||||
|
||||
async function getRuntimeConfig(): Promise<RuntimeConfig> {
|
||||
const data = await sendCommand<RuntimeConfigRaw>({
|
||||
type PersistOptions = {
|
||||
keepalive?: boolean;
|
||||
};
|
||||
|
||||
async function getRuntimeConfig(options: PersistOptions = {}): Promise<RuntimeConfig> {
|
||||
const data = await sendCommand<RuntimeConfigRaw>(
|
||||
{
|
||||
action: "config.get"
|
||||
});
|
||||
},
|
||||
options
|
||||
);
|
||||
|
||||
return {
|
||||
dataDirectory: pickCase(data, "dataDirectory", "DataDirectory", ""),
|
||||
@ -55,22 +62,28 @@ export async function unlockVaultWorkspace(password: string): Promise<void> {
|
||||
});
|
||||
}
|
||||
|
||||
export async function persistAndClearVault(password: string): Promise<void> {
|
||||
const config = await getRuntimeConfig();
|
||||
export async function persistAndClearVault(password: string, options: PersistOptions = {}): Promise<void> {
|
||||
const config = await getRuntimeConfig(options);
|
||||
|
||||
await sendCommand<boolean>({
|
||||
await sendCommand<boolean>(
|
||||
{
|
||||
action: "vault.rebuild_all",
|
||||
payload: {
|
||||
password,
|
||||
vaultDirectory: config.vaultDirectory,
|
||||
dataDirectory: config.dataDirectory
|
||||
}
|
||||
});
|
||||
},
|
||||
options
|
||||
);
|
||||
|
||||
await sendCommand<boolean>({
|
||||
await sendCommand<boolean>(
|
||||
{
|
||||
action: "vault.clear_data_directory",
|
||||
payload: {
|
||||
dataDirectory: config.dataDirectory
|
||||
}
|
||||
});
|
||||
},
|
||||
options
|
||||
);
|
||||
}
|
||||
|
||||
@ -1,16 +1,23 @@
|
||||
import { invoke } from "@tauri-apps/api/core";
|
||||
import { invoke } from "$lib/runtime/invoke";
|
||||
import type { BackendCommand, BackendResponse } from "./types";
|
||||
|
||||
function newCorrelationId(): string {
|
||||
return `ui-${Date.now()}-${Math.random().toString(16).slice(2)}`;
|
||||
}
|
||||
|
||||
export async function sendCommand<T>(command: BackendCommand): Promise<T> {
|
||||
type SendCommandOptions = {
|
||||
keepalive?: boolean;
|
||||
};
|
||||
|
||||
export async function sendCommand<T>(command: BackendCommand, options: SendCommandOptions = {}): Promise<T> {
|
||||
const envelope: BackendCommand = {
|
||||
...command,
|
||||
correlationId: command.correlationId ?? newCorrelationId()
|
||||
};
|
||||
const response = await invoke<BackendResponse<T>>("sidecar_command", { command: envelope });
|
||||
const response = await invoke<BackendResponse<T>>("sidecar_command", {
|
||||
command: envelope,
|
||||
keepalive: options.keepalive === true
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(response.error || "Backend command failed");
|
||||
|
||||
143
Journal.App/src/lib/runtime/invoke.ts
Normal file
143
Journal.App/src/lib/runtime/invoke.ts
Normal file
@ -0,0 +1,143 @@
|
||||
import type { BackendCommand } from "$lib/backend/types";
|
||||
|
||||
type InvokeArgs = Record<string, unknown> | undefined;
|
||||
|
||||
type WindowWithTauri = Window & {
|
||||
__TAURI_INTERNALS__?: unknown;
|
||||
};
|
||||
|
||||
type UiSettingsPayload = {
|
||||
tags?: string[];
|
||||
fragmentTypes?: string[];
|
||||
};
|
||||
|
||||
type FetchJsonOptions = {
|
||||
keepalive?: boolean;
|
||||
};
|
||||
|
||||
const UI_SETTINGS_KEY = "journal.ui.settings";
|
||||
|
||||
function normalizedApiBase(): string {
|
||||
const configured = import.meta.env.VITE_JOURNAL_API_BASE?.trim();
|
||||
if (!configured) {
|
||||
return "/api";
|
||||
}
|
||||
|
||||
return configured.endsWith("/") ? configured.slice(0, -1) : configured;
|
||||
}
|
||||
|
||||
export function isTauriRuntime(): boolean {
|
||||
if (typeof window === "undefined") {
|
||||
return false;
|
||||
}
|
||||
|
||||
return Object.prototype.hasOwnProperty.call(window as WindowWithTauri, "__TAURI_INTERNALS__");
|
||||
}
|
||||
|
||||
function readUiSettingsFromLocalStorage(): UiSettingsPayload {
|
||||
if (typeof window === "undefined") {
|
||||
return {};
|
||||
}
|
||||
|
||||
const raw = window.localStorage.getItem(UI_SETTINGS_KEY);
|
||||
if (!raw) {
|
||||
return {};
|
||||
}
|
||||
|
||||
try {
|
||||
const parsed = JSON.parse(raw) as UiSettingsPayload;
|
||||
return {
|
||||
tags: Array.isArray(parsed.tags) ? parsed.tags : undefined,
|
||||
fragmentTypes: Array.isArray(parsed.fragmentTypes) ? parsed.fragmentTypes : undefined
|
||||
};
|
||||
} catch {
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
function writeUiSettingsToLocalStorage(payload: UiSettingsPayload): void {
|
||||
if (typeof window === "undefined") {
|
||||
return;
|
||||
}
|
||||
|
||||
const safePayload: UiSettingsPayload = {
|
||||
tags: Array.isArray(payload.tags) ? payload.tags : undefined,
|
||||
fragmentTypes: Array.isArray(payload.fragmentTypes) ? payload.fragmentTypes : undefined
|
||||
};
|
||||
|
||||
window.localStorage.setItem(UI_SETTINGS_KEY, JSON.stringify(safePayload));
|
||||
}
|
||||
|
||||
async function fetchJson<T>(path: string, init: RequestInit = {}, options: FetchJsonOptions = {}): Promise<T> {
|
||||
const response = await fetch(`${normalizedApiBase()}${path}`, {
|
||||
...init,
|
||||
keepalive: options.keepalive === true,
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
...(init.headers ?? {})
|
||||
}
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const text = await response.text();
|
||||
throw new Error(text || `Request failed (${response.status})`);
|
||||
}
|
||||
|
||||
if (response.status === 204) {
|
||||
return undefined as T;
|
||||
}
|
||||
|
||||
return (await response.json()) as T;
|
||||
}
|
||||
|
||||
export async function invoke<T>(command: string, args?: InvokeArgs): Promise<T> {
|
||||
if (isTauriRuntime()) {
|
||||
const tauriCore = await import("@tauri-apps/api/core");
|
||||
return tauriCore.invoke<T>(command, args);
|
||||
}
|
||||
|
||||
switch (command) {
|
||||
case "sidecar_command": {
|
||||
const envelope = args?.command;
|
||||
if (!envelope || typeof envelope !== "object") {
|
||||
throw new Error("Missing command payload.");
|
||||
}
|
||||
|
||||
const keepalive = args?.keepalive === true;
|
||||
|
||||
return fetchJson<T>(
|
||||
"/command",
|
||||
{
|
||||
method: "POST",
|
||||
body: JSON.stringify(envelope as BackendCommand)
|
||||
},
|
||||
{ keepalive }
|
||||
);
|
||||
}
|
||||
case "get_sidecar_root":
|
||||
return fetchJson<T>("/sidecar/root");
|
||||
case "set_sidecar_root": {
|
||||
const path = typeof args?.path === "string" ? args.path : "";
|
||||
return fetchJson<T>("/sidecar/root", {
|
||||
method: "POST",
|
||||
body: JSON.stringify({ path })
|
||||
});
|
||||
}
|
||||
case "get_ui_settings":
|
||||
return readUiSettingsFromLocalStorage() as T;
|
||||
case "set_ui_settings": {
|
||||
const tags = Array.isArray(args?.tags) ? (args?.tags as string[]) : undefined;
|
||||
const fragmentTypes =
|
||||
Array.isArray(args?.fragmentTypes) ? (args?.fragmentTypes as string[]) :
|
||||
Array.isArray(args?.fragment_types) ? (args?.fragment_types as string[]) :
|
||||
undefined;
|
||||
|
||||
writeUiSettingsToLocalStorage({ tags, fragmentTypes });
|
||||
return undefined as T;
|
||||
}
|
||||
case "shutdown":
|
||||
return undefined as T;
|
||||
default:
|
||||
throw new Error(`Unsupported command in web runtime: ${command}`);
|
||||
}
|
||||
}
|
||||
@ -1,6 +1,6 @@
|
||||
import { writable } from "svelte/store";
|
||||
import { get } from "svelte/store";
|
||||
import { invoke } from "@tauri-apps/api/core";
|
||||
import { invoke } from "$lib/runtime/invoke";
|
||||
|
||||
const defaultTags = ["Personal", "Work", "Ideas", "Journal"];
|
||||
const defaultFragmentTypes = ["Quote", "Snippet", "Reference"];
|
||||
@ -131,3 +131,4 @@ export function removeFragmentType(index: number): boolean {
|
||||
queuePersist();
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
@ -1,40 +1,71 @@
|
||||
<script lang="ts">
|
||||
import { getCurrentWindow } from "@tauri-apps/api/window";
|
||||
import { invoke } from "@tauri-apps/api/core";
|
||||
import { onMount } from "svelte";
|
||||
import { getSessionPassword, clearVaultSession, flushBeforeClose } from "$lib/stores/session";
|
||||
import { persistAndClearVault } from "$lib/backend/auth";
|
||||
import { hydrateUiSettings } from "$lib/stores/settings";
|
||||
import { invoke, isTauriRuntime } from "$lib/runtime/invoke";
|
||||
|
||||
let closeInProgress = false;
|
||||
|
||||
onMount(() => {
|
||||
void hydrateUiSettings();
|
||||
|
||||
const appWindow = getCurrentWindow();
|
||||
const unlistenPromise = appWindow.onCloseRequested(async (event) => {
|
||||
async function persistSession(keepalive = false): Promise<void> {
|
||||
if (closeInProgress) return;
|
||||
event.preventDefault();
|
||||
closeInProgress = true;
|
||||
|
||||
try { await flushBeforeClose(); } catch {}
|
||||
try {
|
||||
await flushBeforeClose();
|
||||
} catch {
|
||||
// best effort
|
||||
}
|
||||
|
||||
const password = getSessionPassword();
|
||||
if (password) {
|
||||
if (!password) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await persistAndClearVault(password);
|
||||
await persistAndClearVault(password, { keepalive });
|
||||
clearVaultSession();
|
||||
} catch (error) {
|
||||
console.error("Vault persistence on exit failed:", error);
|
||||
}
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
void hydrateUiSettings();
|
||||
|
||||
if (isTauriRuntime()) {
|
||||
const unlistenPromise = (async () => {
|
||||
const tauriWindow = await import("@tauri-apps/api/window");
|
||||
const appWindow = tauriWindow.getCurrentWindow();
|
||||
return appWindow.onCloseRequested(async (event) => {
|
||||
if (closeInProgress) return;
|
||||
event.preventDefault();
|
||||
|
||||
await persistSession();
|
||||
await invoke("shutdown");
|
||||
});
|
||||
})();
|
||||
|
||||
return () => {
|
||||
void unlistenPromise.then((unlisten) => unlisten());
|
||||
};
|
||||
}
|
||||
|
||||
const handlePageHide = () => {
|
||||
void persistSession(true);
|
||||
};
|
||||
|
||||
const handleBeforeUnload = () => {
|
||||
void persistSession(true);
|
||||
};
|
||||
|
||||
window.addEventListener("pagehide", handlePageHide);
|
||||
window.addEventListener("beforeunload", handleBeforeUnload);
|
||||
|
||||
return () => {
|
||||
window.removeEventListener("pagehide", handlePageHide);
|
||||
window.removeEventListener("beforeunload", handleBeforeUnload);
|
||||
};
|
||||
});
|
||||
</script>
|
||||
|
||||
|
||||
@ -12,7 +12,7 @@
|
||||
updateFragmentType,
|
||||
updateSettingsTag
|
||||
} from "$lib/stores/settings";
|
||||
import { invoke } from "@tauri-apps/api/core";
|
||||
import { invoke } from "$lib/runtime/invoke";
|
||||
import { onMount } from "svelte";
|
||||
|
||||
const activeSection = "settings";
|
||||
@ -483,3 +483,4 @@
|
||||
font-size: 0.82rem;
|
||||
}
|
||||
</style>
|
||||
|
||||
|
||||
@ -4,7 +4,7 @@ namespace Journal.Core.Services.Config;
|
||||
|
||||
public sealed class JournalConfigService : IJournalConfigService
|
||||
{
|
||||
public JournalConfig Current { get; } = BuildConfig();
|
||||
public JournalConfig Current => BuildConfig();
|
||||
|
||||
private static JournalConfig BuildConfig()
|
||||
{
|
||||
@ -105,3 +105,4 @@ public sealed class JournalConfigService : IJournalConfigService
|
||||
return int.TryParse(value, out var parsed) ? parsed : null;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
13
Journal.WebGateway/Journal.WebGateway.csproj
Normal file
13
Journal.WebGateway/Journal.WebGateway.csproj
Normal file
@ -0,0 +1,13 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\Journal.Core\Journal.Core.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
276
Journal.WebGateway/Program.cs
Normal file
276
Journal.WebGateway/Program.cs
Normal file
@ -0,0 +1,276 @@
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
using Journal.Core;
|
||||
using Microsoft.Extensions.FileProviders;
|
||||
|
||||
var gatewayJsonOptions = new JsonSerializerOptions
|
||||
{
|
||||
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
|
||||
PropertyNameCaseInsensitive = true,
|
||||
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull
|
||||
};
|
||||
|
||||
var repoRoot = ResolveRepoRoot();
|
||||
Environment.SetEnvironmentVariable("JOURNAL_PROJECT_ROOT", repoRoot);
|
||||
var webDistPath = ResolveWebDist(repoRoot);
|
||||
|
||||
var builder = WebApplication.CreateBuilder(args);
|
||||
|
||||
builder.Services.AddFragmentServices();
|
||||
builder.Services.AddSingleton<Entry>();
|
||||
builder.Services.AddSingleton(new SidecarRootState(repoRoot));
|
||||
builder.Services.AddSingleton(new WebUiState(webDistPath));
|
||||
|
||||
builder.Services.AddCors(options =>
|
||||
{
|
||||
options.AddPolicy("GatewayCors", policy =>
|
||||
{
|
||||
policy.AllowAnyOrigin()
|
||||
.AllowAnyHeader()
|
||||
.AllowAnyMethod();
|
||||
});
|
||||
});
|
||||
|
||||
builder.Services.ConfigureHttpJsonOptions(options =>
|
||||
{
|
||||
options.SerializerOptions.PropertyNamingPolicy = JsonNamingPolicy.CamelCase;
|
||||
options.SerializerOptions.PropertyNameCaseInsensitive = true;
|
||||
});
|
||||
|
||||
var app = builder.Build();
|
||||
|
||||
app.UseCors("GatewayCors");
|
||||
|
||||
app.MapGet("/api/health", () => Results.Ok(new
|
||||
{
|
||||
ok = true,
|
||||
service = "Journal.WebGateway"
|
||||
}));
|
||||
|
||||
app.MapGet("/api/web/status", (WebUiState webUiState) => Results.Ok(new
|
||||
{
|
||||
distPath = webUiState.DistPath,
|
||||
exists = webUiState.Exists
|
||||
}));
|
||||
|
||||
app.MapPost("/api/command", async (CommandEnvelope? command, Entry entry) =>
|
||||
{
|
||||
if (command is null || string.IsNullOrWhiteSpace(command.Action))
|
||||
{
|
||||
return Results.Content(ErrorResponse("Missing action"), "application/json");
|
||||
}
|
||||
|
||||
var inputJson = JsonSerializer.Serialize(command, gatewayJsonOptions);
|
||||
var responseJson = await entry.HandleCommandAsync(inputJson);
|
||||
return Results.Content(responseJson, "application/json");
|
||||
});
|
||||
|
||||
app.MapGet("/api/sidecar/root", (SidecarRootState rootState) =>
|
||||
{
|
||||
var snapshot = rootState.Get();
|
||||
return Results.Ok(new
|
||||
{
|
||||
root = snapshot.Root,
|
||||
isCustom = snapshot.IsCustom
|
||||
});
|
||||
});
|
||||
|
||||
app.MapPost("/api/sidecar/root", (SetSidecarRootRequest? request, SidecarRootState rootState) =>
|
||||
{
|
||||
var path = request?.Path ?? "";
|
||||
if (!string.IsNullOrWhiteSpace(path) && !Directory.Exists(path))
|
||||
{
|
||||
return Results.BadRequest($"Directory '{path}' does not exist.");
|
||||
}
|
||||
|
||||
rootState.Set(path);
|
||||
var snapshot = rootState.Get();
|
||||
Environment.SetEnvironmentVariable("JOURNAL_PROJECT_ROOT", snapshot.Root);
|
||||
return Results.Ok(new
|
||||
{
|
||||
root = snapshot.Root,
|
||||
isCustom = snapshot.IsCustom
|
||||
});
|
||||
});
|
||||
|
||||
if (Directory.Exists(webDistPath) && File.Exists(Path.Combine(webDistPath, "index.html")))
|
||||
{
|
||||
var fileProvider = new PhysicalFileProvider(webDistPath);
|
||||
var indexPath = Path.Combine(webDistPath, "index.html");
|
||||
|
||||
app.UseDefaultFiles(new DefaultFilesOptions
|
||||
{
|
||||
FileProvider = fileProvider,
|
||||
RequestPath = ""
|
||||
});
|
||||
|
||||
app.UseStaticFiles(new StaticFileOptions
|
||||
{
|
||||
FileProvider = fileProvider,
|
||||
RequestPath = ""
|
||||
});
|
||||
|
||||
app.MapGet("/", async context =>
|
||||
{
|
||||
context.Response.ContentType = "text/html; charset=utf-8";
|
||||
await context.Response.SendFileAsync(indexPath);
|
||||
});
|
||||
|
||||
app.MapFallback(async context =>
|
||||
{
|
||||
if (context.Request.Path.StartsWithSegments("/api"))
|
||||
{
|
||||
context.Response.StatusCode = StatusCodes.Status404NotFound;
|
||||
return;
|
||||
}
|
||||
|
||||
context.Response.ContentType = "text/html; charset=utf-8";
|
||||
await context.Response.SendFileAsync(indexPath);
|
||||
});
|
||||
}
|
||||
else
|
||||
{
|
||||
app.MapGet("/", () => Results.Ok(new
|
||||
{
|
||||
name = "Journal.WebGateway",
|
||||
status = "ok",
|
||||
uiAvailable = false,
|
||||
message = "No built web UI found. Build Journal.App with ./scripts/publish-app.ps1 -Target web.",
|
||||
expectedDist = webDistPath
|
||||
}));
|
||||
}
|
||||
|
||||
app.Run();
|
||||
|
||||
string ResolveRepoRoot()
|
||||
{
|
||||
var fromEnv = Environment.GetEnvironmentVariable("JOURNAL_PROJECT_ROOT");
|
||||
if (!string.IsNullOrWhiteSpace(fromEnv) && Directory.Exists(fromEnv))
|
||||
{
|
||||
return Path.GetFullPath(fromEnv);
|
||||
}
|
||||
|
||||
foreach (var start in new[] { Directory.GetCurrentDirectory(), AppContext.BaseDirectory })
|
||||
{
|
||||
var resolved = FindRepoRoot(start);
|
||||
if (resolved is not null)
|
||||
{
|
||||
return resolved;
|
||||
}
|
||||
}
|
||||
|
||||
return Path.GetFullPath(Directory.GetCurrentDirectory());
|
||||
}
|
||||
|
||||
string? FindRepoRoot(string start)
|
||||
{
|
||||
var cursor = Path.GetFullPath(start);
|
||||
|
||||
while (!string.IsNullOrWhiteSpace(cursor))
|
||||
{
|
||||
if (File.Exists(Path.Combine(cursor, "Journal.slnx")) ||
|
||||
Directory.Exists(Path.Combine(cursor, "Journal.Sidecar")) ||
|
||||
Directory.Exists(Path.Combine(cursor, "Journal.Core")))
|
||||
{
|
||||
return cursor;
|
||||
}
|
||||
|
||||
var parent = Directory.GetParent(cursor);
|
||||
if (parent is null || string.Equals(parent.FullName, cursor, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
cursor = parent.FullName;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
string ResolveWebDist(string repoRootPath)
|
||||
{
|
||||
var fromEnv = Environment.GetEnvironmentVariable("JOURNAL_WEB_DIST");
|
||||
if (!string.IsNullOrWhiteSpace(fromEnv))
|
||||
{
|
||||
return Path.GetFullPath(fromEnv);
|
||||
}
|
||||
|
||||
var packagedWwwRoot = Path.Combine(AppContext.BaseDirectory, "wwwroot");
|
||||
if (Directory.Exists(packagedWwwRoot))
|
||||
{
|
||||
return packagedWwwRoot;
|
||||
}
|
||||
|
||||
return Path.Combine(repoRootPath, "Journal.App", "build");
|
||||
}
|
||||
|
||||
string ErrorResponse(string message)
|
||||
=> JsonSerializer.Serialize(new { ok = false, error = message }, gatewayJsonOptions);
|
||||
|
||||
sealed class WebUiState
|
||||
{
|
||||
public WebUiState(string distPath)
|
||||
{
|
||||
DistPath = distPath;
|
||||
}
|
||||
|
||||
public string DistPath { get; }
|
||||
|
||||
public bool Exists => Directory.Exists(DistPath) && File.Exists(Path.Combine(DistPath, "index.html"));
|
||||
}
|
||||
|
||||
sealed class SidecarRootState
|
||||
{
|
||||
private readonly object _sync = new();
|
||||
private readonly string _autoRoot;
|
||||
private string _currentRoot;
|
||||
private bool _isCustom;
|
||||
|
||||
public SidecarRootState(string autoRoot)
|
||||
{
|
||||
_autoRoot = autoRoot;
|
||||
_currentRoot = autoRoot;
|
||||
_isCustom = false;
|
||||
}
|
||||
|
||||
public (string Root, bool IsCustom) Get()
|
||||
{
|
||||
lock (_sync)
|
||||
{
|
||||
return (_currentRoot, _isCustom);
|
||||
}
|
||||
}
|
||||
|
||||
public void Set(string? path)
|
||||
{
|
||||
lock (_sync)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(path))
|
||||
{
|
||||
_currentRoot = _autoRoot;
|
||||
_isCustom = false;
|
||||
return;
|
||||
}
|
||||
|
||||
_currentRoot = Path.GetFullPath(path.Trim());
|
||||
_isCustom = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
sealed class SetSidecarRootRequest
|
||||
{
|
||||
public string? Path { get; set; }
|
||||
}
|
||||
|
||||
sealed class CommandEnvelope
|
||||
{
|
||||
public string Action { get; set; } = "";
|
||||
public string? CorrelationId { get; set; }
|
||||
public string? Id { get; set; }
|
||||
public string? Type { get; set; }
|
||||
public string? Tag { get; set; }
|
||||
public JsonElement? Payload { get; set; }
|
||||
}
|
||||
|
||||
|
||||
24
scripts/dev-shell.ps1
Normal file
24
scripts/dev-shell.ps1
Normal file
@ -0,0 +1,24 @@
|
||||
# Run this in PowerShell before development commands:
|
||||
# . ./scripts/dev-shell.ps1
|
||||
|
||||
$commonScript = Join-Path $PSScriptRoot "script-common.ps1"
|
||||
if (-not (Test-Path $commonScript)) {
|
||||
throw "Missing helper script: $commonScript"
|
||||
}
|
||||
. $commonScript
|
||||
|
||||
$repoRoot = Resolve-JournalRepoRoot -StartPath $PSScriptRoot
|
||||
|
||||
# Clear dead proxy overrides and offline-only pip mode in current shell.
|
||||
Clear-JournalProxyEnv
|
||||
|
||||
# Keep .NET artifacts local to repo to avoid restricted user-profile paths.
|
||||
Initialize-JournalDotnetEnv -RepoRoot $repoRoot
|
||||
|
||||
# Keep pip artifacts local to repo.
|
||||
Initialize-JournalPipEnv -RepoRoot $repoRoot
|
||||
|
||||
# Keep Hugging Face cache local and silence symlink-only warning on Windows.
|
||||
Initialize-JournalHuggingFaceEnv -RepoRoot $repoRoot
|
||||
|
||||
Write-Host "Development shell initialized for repo-local dotnet/pip paths at: $repoRoot"
|
||||
46
scripts/migration-gate.ps1
Normal file
46
scripts/migration-gate.ps1
Normal file
@ -0,0 +1,46 @@
|
||||
param(
|
||||
[switch]$SkipSmoke,
|
||||
[switch]$SkipApi
|
||||
)
|
||||
|
||||
$ErrorActionPreference = "Stop"
|
||||
$repoRoot = Split-Path -Parent $PSScriptRoot
|
||||
$parityReport = Join-Path $repoRoot "logs\parity_harness_results.json"
|
||||
|
||||
Write-Host "migration-gate: repo root = $repoRoot"
|
||||
|
||||
Push-Location $repoRoot
|
||||
try {
|
||||
Write-Host "migration-gate: building sidecar binary..."
|
||||
& "$repoRoot\scripts\dotnet-min.ps1" build Journal.Sidecar/Journal.Sidecar.csproj
|
||||
if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }
|
||||
|
||||
if (-not $SkipSmoke) {
|
||||
Write-Host "migration-gate: running csharp smoke tests..."
|
||||
& "$repoRoot\scripts\dotnet-min.ps1" run --project Journal.SmokeTests/Journal.SmokeTests.csproj
|
||||
if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }
|
||||
}
|
||||
else {
|
||||
Write-Host "migration-gate: skipping smoke tests (--SkipSmoke)."
|
||||
}
|
||||
|
||||
Write-Host "migration-gate: running parity harness + fixture matrix..."
|
||||
$env:PARITY_HARNESS_REPORT = $parityReport
|
||||
& python -m unittest discover -s tests -p "test_parity_harness.py" -v
|
||||
if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }
|
||||
|
||||
if (-not $SkipApi) {
|
||||
Write-Host "migration-gate: running API contract tests..."
|
||||
& python -m unittest discover -s tests -p "test_api_contract.py" -v
|
||||
if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }
|
||||
}
|
||||
else {
|
||||
Write-Host "migration-gate: skipping API contract tests (--SkipApi)."
|
||||
}
|
||||
|
||||
Write-Host "migration-gate: PASS"
|
||||
Write-Host "migration-gate: parity report => $parityReport"
|
||||
}
|
||||
finally {
|
||||
Pop-Location
|
||||
}
|
||||
56
scripts/pip-min.ps1
Normal file
56
scripts/pip-min.ps1
Normal file
@ -0,0 +1,56 @@
|
||||
param(
|
||||
[Parameter(ValueFromRemainingArguments = $true)]
|
||||
[string[]]$PipArgs
|
||||
)
|
||||
|
||||
$commonScript = Join-Path $PSScriptRoot "script-common.ps1"
|
||||
if (-not (Test-Path $commonScript)) {
|
||||
throw "Missing helper script: $commonScript"
|
||||
}
|
||||
. $commonScript
|
||||
|
||||
$repoRoot = Resolve-JournalRepoRoot -StartPath $PSScriptRoot
|
||||
|
||||
Initialize-JournalPipEnv -RepoRoot $repoRoot
|
||||
Clear-JournalProxyEnv
|
||||
|
||||
if (-not $PipArgs -or $PipArgs.Count -eq 0) {
|
||||
Write-Host "Usage: ./scripts/pip-min.ps1 <pip args>"
|
||||
Write-Host "Example: ./scripts/pip-min.ps1 install --index-url https://pypi.org/simple faster-whisper"
|
||||
exit 2
|
||||
}
|
||||
|
||||
# Default install target to a repo-local directory so installs do not require
|
||||
# user/site-packages write access on constrained hosts.
|
||||
$effectiveArgs = @($PipArgs)
|
||||
$firstArg = $effectiveArgs[0].ToLowerInvariant()
|
||||
if ($firstArg -eq "install") {
|
||||
# On Windows, map PyAudio to pyaudiowpatch (wheel available for newer CPython),
|
||||
# avoiding source builds that require PortAudio headers/toolchain wiring.
|
||||
for ($i = 0; $i -lt $effectiveArgs.Count; $i++) {
|
||||
$arg = $effectiveArgs[$i]
|
||||
if ($arg -match '^(?i)pyaudio($|[<>=!~].*)') {
|
||||
$suffix = $arg.Substring(7)
|
||||
$effectiveArgs[$i] = "pyaudiowpatch$suffix"
|
||||
Write-Host "pip-min: mapped '$arg' -> '$($effectiveArgs[$i])' on Windows."
|
||||
}
|
||||
}
|
||||
|
||||
$hasTarget = $effectiveArgs -contains "--target" -or $effectiveArgs -contains "-t" -or $effectiveArgs -contains "--prefix"
|
||||
if (-not $hasTarget) {
|
||||
$effectiveArgs = $effectiveArgs | Where-Object { $_ -ne "--user" }
|
||||
$localTarget = Join-Path $repoRoot ".pydeps\py314"
|
||||
New-Item -ItemType Directory -Force -Path $localTarget | Out-Null
|
||||
$effectiveArgs += @("--target", $localTarget)
|
||||
Write-Host "pip-min: using local target $localTarget"
|
||||
}
|
||||
}
|
||||
|
||||
$pipWrapper = Join-Path $PSScriptRoot "pip_safe.py"
|
||||
if (Test-Path $pipWrapper) {
|
||||
& python $pipWrapper @effectiveArgs
|
||||
}
|
||||
else {
|
||||
& python -m pip @effectiveArgs
|
||||
}
|
||||
exit $LASTEXITCODE
|
||||
46
scripts/pip_safe.py
Normal file
46
scripts/pip_safe.py
Normal file
@ -0,0 +1,46 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
import tempfile
|
||||
from typing import Callable
|
||||
|
||||
|
||||
def _mkdtemp_compat(
|
||||
suffix: str | None = None,
|
||||
prefix: str | None = None,
|
||||
dir: str | None = None,
|
||||
) -> str:
|
||||
# Python 3.14 on some Windows hosts creates mkdtemp dirs that are
|
||||
# immediately non-writable by the same process when mode=0o700 is used.
|
||||
# pip relies heavily on tempfile; force 0o777 for compatibility.
|
||||
if dir is None:
|
||||
dir = tempfile.gettempdir()
|
||||
if prefix is None:
|
||||
prefix = tempfile.template
|
||||
if suffix is None:
|
||||
suffix = ""
|
||||
|
||||
names = tempfile._get_candidate_names()
|
||||
for _ in range(tempfile.TMP_MAX):
|
||||
name = next(names)
|
||||
path = os.path.join(dir, f"{prefix}{name}{suffix}")
|
||||
try:
|
||||
os.mkdir(path, 0o777)
|
||||
return path
|
||||
except FileExistsError:
|
||||
continue
|
||||
|
||||
raise FileExistsError("No usable temporary directory name found.")
|
||||
|
||||
|
||||
def main(argv: list[str]) -> int:
|
||||
tempfile.mkdtemp = _mkdtemp_compat # type: ignore[assignment]
|
||||
|
||||
from pip._internal.cli.main import main as pip_main
|
||||
|
||||
return int(pip_main(argv))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main(__import__("sys").argv[1:]))
|
||||
|
||||
157
scripts/publish-app.ps1
Normal file
157
scripts/publish-app.ps1
Normal file
@ -0,0 +1,157 @@
|
||||
param(
|
||||
[ValidateSet("web", "tauri")]
|
||||
[string]$Target = "web",
|
||||
[ValidateSet("Release", "Debug")]
|
||||
[string]$Configuration = "Release",
|
||||
[ValidateSet("none", "nsis", "msi")]
|
||||
[string]$TauriBundles = "none",
|
||||
[switch]$InstallDeps,
|
||||
[switch]$SkipInstall,
|
||||
[switch]$DryRun
|
||||
)
|
||||
|
||||
$commonScript = Join-Path $PSScriptRoot "script-common.ps1"
|
||||
if (-not (Test-Path $commonScript)) {
|
||||
throw "Missing helper script: $commonScript"
|
||||
}
|
||||
. $commonScript
|
||||
|
||||
$repoRoot = Resolve-JournalRepoRoot -StartPath $PSScriptRoot
|
||||
$appRoot = Resolve-JournalAppRoot -RepoRoot $repoRoot
|
||||
|
||||
Clear-JournalProxyEnv
|
||||
|
||||
# Keep npm cache and temp local to the repo.
|
||||
$npmCacheDir = Join-Path $repoRoot ".npm\cache"
|
||||
$npmTempDir = Join-Path $repoRoot ".tmp\npm-temp"
|
||||
New-Item -ItemType Directory -Force -Path $npmCacheDir, $npmTempDir | Out-Null
|
||||
$env:npm_config_cache = $npmCacheDir
|
||||
$env:npm_config_update_notifier = "false"
|
||||
$env:npm_config_fund = "false"
|
||||
$env:npm_config_audit = "false"
|
||||
$env:npm_config_offline = "false"
|
||||
$env:npm_config_prefer_offline = "false"
|
||||
$env:npm_config_prefer_online = "true"
|
||||
$env:TEMP = $npmTempDir
|
||||
$env:TMP = $npmTempDir
|
||||
|
||||
if (-not (Get-Command npm -ErrorAction SilentlyContinue)) {
|
||||
throw "npm is required but was not found in PATH."
|
||||
}
|
||||
|
||||
$nodeModulesPath = Join-Path $appRoot "node_modules"
|
||||
$packageLockPath = Join-Path $appRoot "package-lock.json"
|
||||
$shouldInstall = $InstallDeps -or (-not (Test-Path $nodeModulesPath))
|
||||
if ($SkipInstall) {
|
||||
$shouldInstall = $false
|
||||
}
|
||||
|
||||
Write-Host "Building Journal.App target '$Target' ($Configuration)..." -ForegroundColor Cyan
|
||||
Write-Host "Using app root: $appRoot" -ForegroundColor DarkGray
|
||||
|
||||
Push-Location $appRoot
|
||||
try {
|
||||
if ($shouldInstall) {
|
||||
$installArgs = if (Test-Path $packageLockPath) {
|
||||
@("ci", "--no-audit", "--fund=false")
|
||||
}
|
||||
else {
|
||||
@("install", "--no-audit", "--fund=false")
|
||||
}
|
||||
|
||||
Write-Host "> npm $($installArgs -join ' ')" -ForegroundColor DarkGray
|
||||
if (-not $DryRun) {
|
||||
& npm @installArgs
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
throw "Dependency install failed with exit code $LASTEXITCODE."
|
||||
}
|
||||
}
|
||||
}
|
||||
else {
|
||||
Write-Host "Skipping dependency install (node_modules already present)." -ForegroundColor DarkGray
|
||||
}
|
||||
|
||||
if ($Target -eq "web") {
|
||||
$buildArgs = @("run", "build")
|
||||
Write-Host "> npm $($buildArgs -join ' ')" -ForegroundColor DarkGray
|
||||
if (-not $DryRun) {
|
||||
& npm @buildArgs
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
throw "Frontend build failed with exit code $LASTEXITCODE."
|
||||
}
|
||||
}
|
||||
|
||||
$outputPath = Join-Path $appRoot "build"
|
||||
if ($DryRun) {
|
||||
Write-Host "`nDry run complete (no commands executed)." -ForegroundColor Yellow
|
||||
Write-Host "Expected output: $outputPath" -ForegroundColor Gray
|
||||
}
|
||||
else {
|
||||
Write-Host "`nFrontend build successful." -ForegroundColor Green
|
||||
Write-Host "Output: $outputPath" -ForegroundColor Gray
|
||||
}
|
||||
}
|
||||
else {
|
||||
$tauriArgs = @("run", "tauri", "build")
|
||||
$tauriCliArgs = @()
|
||||
if ($TauriBundles -eq "none") {
|
||||
$tauriCliArgs += "--no-bundle"
|
||||
}
|
||||
else {
|
||||
$tauriCliArgs += @("--bundles", $TauriBundles)
|
||||
}
|
||||
if ($Configuration -eq "Debug") {
|
||||
$tauriCliArgs += "--debug"
|
||||
}
|
||||
if ($tauriCliArgs.Count -gt 0) {
|
||||
$tauriArgs += "--"
|
||||
$tauriArgs += $tauriCliArgs
|
||||
}
|
||||
|
||||
Write-Host "> npm $($tauriArgs -join ' ')" -ForegroundColor DarkGray
|
||||
if (-not $DryRun) {
|
||||
& npm @tauriArgs
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
throw "Tauri build failed with exit code $LASTEXITCODE."
|
||||
}
|
||||
}
|
||||
|
||||
$targetConfigDir = if ($Configuration -eq "Debug") { "debug" } else { "release" }
|
||||
$tauriTargetPath = Join-Path $appRoot "src-tauri\target"
|
||||
$rawExePath = Join-Path $tauriTargetPath "$targetConfigDir\journalapp.exe"
|
||||
if ($DryRun) {
|
||||
Write-Host "`nDry run complete (no commands executed)." -ForegroundColor Yellow
|
||||
if ($TauriBundles -eq "none") {
|
||||
Write-Host "Expected executable: $rawExePath" -ForegroundColor Gray
|
||||
}
|
||||
else {
|
||||
Write-Host "Expected output root: $tauriTargetPath" -ForegroundColor Gray
|
||||
}
|
||||
}
|
||||
else {
|
||||
Write-Host "`nTauri build successful." -ForegroundColor Green
|
||||
if ($TauriBundles -eq "none") {
|
||||
if (Test-Path $rawExePath) {
|
||||
Write-Host "Executable location: $rawExePath" -ForegroundColor Gray
|
||||
}
|
||||
else {
|
||||
$exeCandidates = Get-ChildItem -Path (Join-Path $tauriTargetPath $targetConfigDir) -File -Filter *.exe -ErrorAction SilentlyContinue |
|
||||
Sort-Object LastWriteTime -Descending
|
||||
if ($exeCandidates -and $exeCandidates.Count -gt 0) {
|
||||
Write-Host "Executable location: $($exeCandidates[0].FullName)" -ForegroundColor Gray
|
||||
}
|
||||
else {
|
||||
Write-Host "Output root: $tauriTargetPath" -ForegroundColor Gray
|
||||
}
|
||||
}
|
||||
}
|
||||
else {
|
||||
Write-Host "Output root: $tauriTargetPath" -ForegroundColor Gray
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
finally {
|
||||
Pop-Location
|
||||
}
|
||||
|
||||
54
scripts/publish-sidecar.ps1
Normal file
54
scripts/publish-sidecar.ps1
Normal file
@ -0,0 +1,54 @@
|
||||
param(
|
||||
[string]$Configuration = "Release",
|
||||
[string]$Runtime = "win-x64"
|
||||
)
|
||||
|
||||
$commonScript = Join-Path $PSScriptRoot "script-common.ps1"
|
||||
if (-not (Test-Path $commonScript)) {
|
||||
throw "Missing helper script: $commonScript"
|
||||
}
|
||||
. $commonScript
|
||||
|
||||
$repoRoot = Resolve-JournalRepoRoot -StartPath $PSScriptRoot
|
||||
$csproj = Resolve-JournalSidecarProjectPath -RepoRoot $repoRoot
|
||||
$outputDir = Join-Path $repoRoot "output"
|
||||
|
||||
# Setup local dotnet environment (matches dotnet-min.ps1 logic)
|
||||
Clear-JournalProxyEnv
|
||||
Initialize-JournalDotnetEnv -RepoRoot $repoRoot
|
||||
|
||||
Write-Host "Publishing Journal.Sidecar ($Configuration, $Runtime)..." -ForegroundColor Cyan
|
||||
Write-Host "Using project: $csproj" -ForegroundColor DarkGray
|
||||
|
||||
$publishArgs = @(
|
||||
"publish", $csproj,
|
||||
"-c", $Configuration,
|
||||
"-r", $Runtime,
|
||||
"--self-contained",
|
||||
"-p:PublishSingleFile=true",
|
||||
"-p:IncludeNativeLibrariesForSelfExtract=true",
|
||||
"-p:RestoreIgnoreFailedSources=true",
|
||||
"-p:NuGetAudit=false",
|
||||
"-o", $outputDir
|
||||
)
|
||||
|
||||
& dotnet @publishArgs
|
||||
|
||||
if ($LASTEXITCODE -eq 0) {
|
||||
$binaryName = [System.IO.Path]::GetFileNameWithoutExtension($csproj)
|
||||
$isWindowsRuntime = $Runtime -like "win-*"
|
||||
$binaryFile = if ($isWindowsRuntime) { "$binaryName.exe" } else { $binaryName }
|
||||
$binaryPath = Join-Path $outputDir $binaryFile
|
||||
|
||||
Write-Host "`nPublish successful!" -ForegroundColor Green
|
||||
if (Test-Path $binaryPath) {
|
||||
Write-Host "Executable location: $binaryPath" -ForegroundColor Gray
|
||||
}
|
||||
else {
|
||||
Write-Host "Output directory: $outputDir" -ForegroundColor Gray
|
||||
}
|
||||
}
|
||||
else {
|
||||
Write-Host "`nPublish failed with exit code $LASTEXITCODE" -ForegroundColor Red
|
||||
exit $LASTEXITCODE
|
||||
}
|
||||
55
scripts/publish-webgateway.ps1
Normal file
55
scripts/publish-webgateway.ps1
Normal file
@ -0,0 +1,55 @@
|
||||
param(
|
||||
[ValidateSet("Release", "Debug")]
|
||||
[string]$Configuration = "Release",
|
||||
[string]$Runtime = "win-x64",
|
||||
[switch]$SelfContained,
|
||||
[switch]$SkipWebAssets
|
||||
)
|
||||
|
||||
$commonScript = Join-Path $PSScriptRoot "script-common.ps1"
|
||||
if (-not (Test-Path $commonScript)) {
|
||||
throw "Missing helper script: $commonScript"
|
||||
}
|
||||
. $commonScript
|
||||
|
||||
$repoRoot = Resolve-JournalRepoRoot -StartPath $PSScriptRoot
|
||||
$gatewayProject = Resolve-JournalWebGatewayProjectPath -RepoRoot $repoRoot
|
||||
$outputDir = Join-Path $repoRoot "output\webgateway"
|
||||
$webBuildDir = Join-Path $repoRoot "Journal.App\build"
|
||||
$webOutputDir = Join-Path $outputDir "wwwroot"
|
||||
|
||||
Clear-JournalProxyEnv
|
||||
Initialize-JournalDotnetEnv -RepoRoot $repoRoot
|
||||
|
||||
Write-Host "Publishing Journal.WebGateway ($Configuration, $Runtime)..." -ForegroundColor Cyan
|
||||
Write-Host "Project: $gatewayProject" -ForegroundColor DarkGray
|
||||
|
||||
$publishArgs = @(
|
||||
"publish", $gatewayProject,
|
||||
"-c", $Configuration,
|
||||
"-r", $Runtime,
|
||||
"--self-contained", ($SelfContained.IsPresent.ToString().ToLowerInvariant()),
|
||||
"-p:RestoreIgnoreFailedSources=true",
|
||||
"-p:NuGetAudit=false",
|
||||
"-o", $outputDir
|
||||
)
|
||||
|
||||
& dotnet @publishArgs
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
Write-Host "`nPublish failed with exit code $LASTEXITCODE" -ForegroundColor Red
|
||||
exit $LASTEXITCODE
|
||||
}
|
||||
|
||||
if (-not $SkipWebAssets) {
|
||||
if (Test-Path $webBuildDir) {
|
||||
New-Item -ItemType Directory -Force -Path $webOutputDir | Out-Null
|
||||
Copy-Item -Path (Join-Path $webBuildDir "*") -Destination $webOutputDir -Recurse -Force
|
||||
Write-Host "Copied web assets to: $webOutputDir" -ForegroundColor Gray
|
||||
}
|
||||
else {
|
||||
Write-Warning "Journal.App build output not found at $webBuildDir. Run ./scripts/publish-app.ps1 -Target web first."
|
||||
}
|
||||
}
|
||||
|
||||
Write-Host "`nPublish successful." -ForegroundColor Green
|
||||
Write-Host "Output directory: $outputDir" -ForegroundColor Gray
|
||||
48
scripts/run-webgateway.ps1
Normal file
48
scripts/run-webgateway.ps1
Normal file
@ -0,0 +1,48 @@
|
||||
param(
|
||||
[ValidateSet("Release", "Debug")]
|
||||
[string]$Configuration = "Release",
|
||||
[string]$Urls = "http://0.0.0.0:5180",
|
||||
[string]$ProjectRoot
|
||||
)
|
||||
|
||||
$commonScript = Join-Path $PSScriptRoot "script-common.ps1"
|
||||
if (-not (Test-Path $commonScript)) {
|
||||
throw "Missing helper script: $commonScript"
|
||||
}
|
||||
. $commonScript
|
||||
|
||||
$repoRoot = Resolve-JournalRepoRoot -StartPath $PSScriptRoot
|
||||
$gatewayProject = Resolve-JournalWebGatewayProjectPath -RepoRoot $repoRoot
|
||||
|
||||
$effectiveProjectRoot = if ([string]::IsNullOrWhiteSpace($ProjectRoot)) {
|
||||
$repoRoot
|
||||
}
|
||||
else {
|
||||
[System.IO.Path]::GetFullPath($ProjectRoot)
|
||||
}
|
||||
|
||||
if (-not (Test-Path $effectiveProjectRoot)) {
|
||||
throw "ProjectRoot does not exist: $effectiveProjectRoot"
|
||||
}
|
||||
|
||||
Clear-JournalProxyEnv
|
||||
Initialize-JournalDotnetEnv -RepoRoot $repoRoot
|
||||
$env:JOURNAL_PROJECT_ROOT = $effectiveProjectRoot
|
||||
|
||||
Write-Host "Running Journal.WebGateway ($Configuration)..." -ForegroundColor Cyan
|
||||
Write-Host "Project: $gatewayProject" -ForegroundColor DarkGray
|
||||
Write-Host "URLs: $Urls" -ForegroundColor DarkGray
|
||||
Write-Host "JOURNAL_PROJECT_ROOT: $effectiveProjectRoot" -ForegroundColor DarkGray
|
||||
|
||||
$runArgs = @(
|
||||
"run",
|
||||
"--project", $gatewayProject,
|
||||
"-c", $Configuration,
|
||||
"--no-launch-profile",
|
||||
"--urls", $Urls,
|
||||
"-p:RestoreIgnoreFailedSources=true",
|
||||
"-p:NuGetAudit=false"
|
||||
)
|
||||
|
||||
& dotnet @runArgs
|
||||
exit $LASTEXITCODE
|
||||
240
scripts/script-common.ps1
Normal file
240
scripts/script-common.ps1
Normal file
@ -0,0 +1,240 @@
|
||||
function Clear-JournalProxyEnv {
|
||||
# Clear proxy/no-index env vars that commonly break package restore.
|
||||
Remove-Item Env:HTTP_PROXY -ErrorAction SilentlyContinue
|
||||
Remove-Item Env:HTTPS_PROXY -ErrorAction SilentlyContinue
|
||||
Remove-Item Env:ALL_PROXY -ErrorAction SilentlyContinue
|
||||
Remove-Item Env:http_proxy -ErrorAction SilentlyContinue
|
||||
Remove-Item Env:https_proxy -ErrorAction SilentlyContinue
|
||||
Remove-Item Env:all_proxy -ErrorAction SilentlyContinue
|
||||
Remove-Item Env:GIT_HTTP_PROXY -ErrorAction SilentlyContinue
|
||||
Remove-Item Env:GIT_HTTPS_PROXY -ErrorAction SilentlyContinue
|
||||
Remove-Item Env:PIP_NO_INDEX -ErrorAction SilentlyContinue
|
||||
}
|
||||
|
||||
function Test-JournalRepoRootCandidate {
|
||||
param(
|
||||
[string]$Path
|
||||
)
|
||||
|
||||
if ([string]::IsNullOrWhiteSpace($Path)) {
|
||||
return $false
|
||||
}
|
||||
|
||||
$markers = @(
|
||||
"Journal.slnx",
|
||||
"scripts\dev-shell.ps1",
|
||||
"Journal.Sidecar\Journal.Sidecar.csproj"
|
||||
)
|
||||
|
||||
foreach ($marker in $markers) {
|
||||
if (Test-Path (Join-Path $Path $marker)) {
|
||||
return $true
|
||||
}
|
||||
}
|
||||
|
||||
return $false
|
||||
}
|
||||
|
||||
function Resolve-JournalRepoRoot {
|
||||
param(
|
||||
[string]$StartPath
|
||||
)
|
||||
|
||||
$candidateStarts = @()
|
||||
if (-not [string]::IsNullOrWhiteSpace($StartPath)) {
|
||||
$candidateStarts += $StartPath
|
||||
}
|
||||
|
||||
$currentPath = (Get-Location).Path
|
||||
if (-not [string]::IsNullOrWhiteSpace($currentPath) -and ($candidateStarts -notcontains $currentPath)) {
|
||||
$candidateStarts += $currentPath
|
||||
}
|
||||
|
||||
$override = $env:JOURNAL_REPO_ROOT
|
||||
if (-not [string]::IsNullOrWhiteSpace($override)) {
|
||||
$overridePath = [System.IO.Path]::GetFullPath($override)
|
||||
if (Test-JournalRepoRootCandidate -Path $overridePath) {
|
||||
return $overridePath
|
||||
}
|
||||
Write-Warning "JOURNAL_REPO_ROOT is set but does not look like this repo: $overridePath"
|
||||
}
|
||||
|
||||
if (Get-Command git -ErrorAction SilentlyContinue) {
|
||||
foreach ($start in $candidateStarts) {
|
||||
try {
|
||||
$gitRoot = & git -C $start rev-parse --show-toplevel 2>$null
|
||||
if ($? -and -not [string]::IsNullOrWhiteSpace($gitRoot)) {
|
||||
$gitRootPath = [System.IO.Path]::GetFullPath($gitRoot.Trim())
|
||||
if (Test-JournalRepoRootCandidate -Path $gitRootPath) {
|
||||
return $gitRootPath
|
||||
}
|
||||
}
|
||||
}
|
||||
catch {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($start in $candidateStarts) {
|
||||
$cursor = [System.IO.Path]::GetFullPath($start)
|
||||
while (-not [string]::IsNullOrWhiteSpace($cursor)) {
|
||||
if (Test-JournalRepoRootCandidate -Path $cursor) {
|
||||
return $cursor
|
||||
}
|
||||
|
||||
$parent = [System.IO.Directory]::GetParent($cursor)
|
||||
if ($null -eq $parent) {
|
||||
break
|
||||
}
|
||||
|
||||
if ($parent.FullName -eq $cursor) {
|
||||
break
|
||||
}
|
||||
|
||||
$cursor = $parent.FullName
|
||||
}
|
||||
}
|
||||
|
||||
throw "Could not locate repository root. Set JOURNAL_REPO_ROOT to the repo path."
|
||||
}
|
||||
|
||||
function Initialize-JournalDotnetEnv {
|
||||
param(
|
||||
[Parameter(Mandatory = $true)]
|
||||
[string]$RepoRoot
|
||||
)
|
||||
|
||||
$dotnetCliHome = Join-Path $RepoRoot ".dotnet_home"
|
||||
$nugetPackages = Join-Path $RepoRoot ".nuget\packages"
|
||||
$nugetHttpCachePath = Join-Path $RepoRoot ".nuget\http-cache"
|
||||
|
||||
$env:DOTNET_CLI_HOME = $dotnetCliHome
|
||||
$env:NUGET_PACKAGES = $nugetPackages
|
||||
$env:NUGET_HTTP_CACHE_PATH = $nugetHttpCachePath
|
||||
$env:DOTNET_SKIP_FIRST_TIME_EXPERIENCE = "1"
|
||||
$env:DOTNET_ADD_GLOBAL_TOOLS_TO_PATH = "0"
|
||||
$env:DOTNET_GENERATE_ASPNET_CERTIFICATE = "0"
|
||||
$env:DOTNET_CLI_TELEMETRY_OPTOUT = "1"
|
||||
$env:NUGET_CERT_REVOCATION_MODE = "offline"
|
||||
|
||||
New-Item -ItemType Directory -Force -Path $dotnetCliHome, $nugetPackages, $nugetHttpCachePath | Out-Null
|
||||
}
|
||||
|
||||
function Initialize-JournalPipEnv {
|
||||
param(
|
||||
[Parameter(Mandatory = $true)]
|
||||
[string]$RepoRoot
|
||||
)
|
||||
|
||||
$pipCacheDir = Join-Path $RepoRoot ".pip\cache"
|
||||
$pipTempDir = Join-Path $RepoRoot ".tmp\pip-temp"
|
||||
|
||||
$env:PIP_CACHE_DIR = $pipCacheDir
|
||||
$env:TEMP = $pipTempDir
|
||||
$env:TMP = $pipTempDir
|
||||
$env:PIP_DISABLE_PIP_VERSION_CHECK = "1"
|
||||
$env:PIP_DEFAULT_TIMEOUT = "30"
|
||||
$env:PIP_RETRIES = "2"
|
||||
|
||||
New-Item -ItemType Directory -Force -Path $pipCacheDir, $pipTempDir | Out-Null
|
||||
}
|
||||
|
||||
function Initialize-JournalHuggingFaceEnv {
|
||||
param(
|
||||
[Parameter(Mandatory = $true)]
|
||||
[string]$RepoRoot
|
||||
)
|
||||
|
||||
$hfHome = Join-Path $RepoRoot ".cache\huggingface"
|
||||
$hfHubCache = Join-Path $hfHome "hub"
|
||||
|
||||
$env:HF_HOME = $hfHome
|
||||
$env:HUGGINGFACE_HUB_CACHE = $hfHubCache
|
||||
$env:HF_HUB_DISABLE_SYMLINKS_WARNING = "1"
|
||||
|
||||
New-Item -ItemType Directory -Force -Path $hfHubCache | Out-Null
|
||||
}
|
||||
|
||||
function Resolve-JournalSidecarProjectPath {
|
||||
param(
|
||||
[Parameter(Mandatory = $true)]
|
||||
[string]$RepoRoot
|
||||
)
|
||||
|
||||
$defaultPath = Join-Path $RepoRoot "Journal.Sidecar\Journal.Sidecar.csproj"
|
||||
if (Test-Path $defaultPath) {
|
||||
return (Resolve-Path $defaultPath).Path
|
||||
}
|
||||
|
||||
$exactMatches = @(Get-ChildItem -Path $RepoRoot -Recurse -File -Filter "Journal.Sidecar.csproj" -ErrorAction SilentlyContinue)
|
||||
if ($exactMatches.Count -eq 1) {
|
||||
return $exactMatches[0].FullName
|
||||
}
|
||||
if ($exactMatches.Count -gt 1) {
|
||||
$matchList = ($exactMatches | ForEach-Object { $_.FullName }) -join "; "
|
||||
throw "Found multiple Journal.Sidecar.csproj files: $matchList"
|
||||
}
|
||||
|
||||
$fallbackMatches = @(Get-ChildItem -Path $RepoRoot -Recurse -File -Filter "*.csproj" -ErrorAction SilentlyContinue | Where-Object {
|
||||
$_.BaseName -match "(?i)sidecar" -or $_.DirectoryName -match "(?i)sidecar"
|
||||
})
|
||||
if ($fallbackMatches.Count -eq 1) {
|
||||
return $fallbackMatches[0].FullName
|
||||
}
|
||||
if ($fallbackMatches.Count -gt 1) {
|
||||
$matchList = ($fallbackMatches | ForEach-Object { $_.FullName }) -join "; "
|
||||
throw "Found multiple sidecar-like csproj files: $matchList"
|
||||
}
|
||||
|
||||
throw "Could not locate a sidecar project file under: $RepoRoot"
|
||||
}
|
||||
|
||||
function Resolve-JournalAppRoot {
|
||||
param(
|
||||
[Parameter(Mandatory = $true)]
|
||||
[string]$RepoRoot
|
||||
)
|
||||
|
||||
$defaultPath = Join-Path $RepoRoot "Journal.App"
|
||||
$defaultPackageJson = Join-Path $defaultPath "package.json"
|
||||
$defaultTauriConfig = Join-Path $defaultPath "src-tauri\tauri.conf.json"
|
||||
if ((Test-Path $defaultPackageJson) -and (Test-Path $defaultTauriConfig)) {
|
||||
return (Resolve-Path $defaultPath).Path
|
||||
}
|
||||
|
||||
$packageJsonMatches = @(Get-ChildItem -Path $RepoRoot -Recurse -File -Filter "package.json" -ErrorAction SilentlyContinue | Where-Object {
|
||||
Test-Path (Join-Path $_.DirectoryName "src-tauri\tauri.conf.json")
|
||||
})
|
||||
if ($packageJsonMatches.Count -eq 1) {
|
||||
return $packageJsonMatches[0].DirectoryName
|
||||
}
|
||||
if ($packageJsonMatches.Count -gt 1) {
|
||||
$matchList = ($packageJsonMatches | ForEach-Object { $_.DirectoryName }) -join "; "
|
||||
throw "Found multiple Tauri app roots under repo: $matchList"
|
||||
}
|
||||
|
||||
throw "Could not locate Journal.App root under: $RepoRoot"
|
||||
}
|
||||
|
||||
function Resolve-JournalWebGatewayProjectPath {
|
||||
param(
|
||||
[Parameter(Mandatory = $true)]
|
||||
[string]$RepoRoot
|
||||
)
|
||||
|
||||
$defaultPath = Join-Path $RepoRoot "Journal.WebGateway\Journal.WebGateway.csproj"
|
||||
if (Test-Path $defaultPath) {
|
||||
return (Resolve-Path $defaultPath).Path
|
||||
}
|
||||
|
||||
$exactMatches = @(Get-ChildItem -Path $RepoRoot -Recurse -File -Filter "Journal.WebGateway.csproj" -ErrorAction SilentlyContinue)
|
||||
if ($exactMatches.Count -eq 1) {
|
||||
return $exactMatches[0].FullName
|
||||
}
|
||||
if ($exactMatches.Count -gt 1) {
|
||||
$matchList = ($exactMatches | ForEach-Object { $_.FullName }) -join "; "
|
||||
throw "Found multiple Journal.WebGateway.csproj files: $matchList"
|
||||
}
|
||||
|
||||
throw "Could not locate Journal.WebGateway.csproj under: $RepoRoot"
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user