diff --git a/CursorLang/Interop/PackageIdentityNative.cs b/CursorLang/Interop/PackageIdentityNative.cs
new file mode 100644
index 0000000..28a67d5
--- /dev/null
+++ b/CursorLang/Interop/PackageIdentityNative.cs
@@ -0,0 +1,38 @@
+using System.Runtime.InteropServices;
+
+namespace CursorLang.Interop;
+
+///
+/// Answers whether the application runs from an MSIX package.
+///
+///
+/// The same application runs both installed from the Store and simply unpacked
+/// into a folder. Some Windows features — startup through StartupTask, for
+/// instance — are available only to a package, and reaching for them without a
+/// check is not allowed: outside a package they throw.
+///
+internal static class PackageIdentityNative
+{
+ /// APPMODEL_ERROR_NO_PACKAGE — the process runs outside a package.
+ private const int NoPackage = 15700;
+
+ ///
+ /// The application runs from an MSIX package.
+ ///
+ ///
+ /// The value is computed once: it cannot change during the lifetime of
+ /// the process.
+ ///
+ internal static bool IsPackaged { get; } = DetectPackage();
+
+ private static bool DetectPackage()
+ {
+ // The answer comes from the return code rather than from the name itself, so
+ // no buffer is needed: with zero length a package replies complaining about space
+ uint length = 0;
+ return GetCurrentPackageFullName(ref length, IntPtr.Zero) != NoPackage;
+ }
+
+ [DllImport("kernel32.dll", CharSet = CharSet.Unicode)]
+ private static extern int GetCurrentPackageFullName(ref uint packageFullNameLength, IntPtr packageFullName);
+}
diff --git a/CursorLang/Services/IStartupService.cs b/CursorLang/Services/IStartupService.cs
new file mode 100644
index 0000000..6abbb19
--- /dev/null
+++ b/CursorLang/Services/IStartupService.cs
@@ -0,0 +1,18 @@
+using CursorLang.Models;
+
+namespace CursorLang.Services;
+
+///
+/// Starting the app together with Windows.
+///
+public interface IStartupService
+{
+ /// Finds out the current state of startup.
+ Task GetStateAsync();
+
+ ///
+ /// Asks for startup to be switched on or off and answers with the state that
+ /// came of it: the request to switch it on may well be turned down.
+ ///
+ Task SetEnabledAsync(bool enabled);
+}
diff --git a/CursorLang/Services/RegistryStartup.cs b/CursorLang/Services/RegistryStartup.cs
new file mode 100644
index 0000000..3fcf2e9
--- /dev/null
+++ b/CursorLang/Services/RegistryStartup.cs
@@ -0,0 +1,120 @@
+using System.IO;
+using System.Security;
+using CursorLang.Models;
+using Microsoft.Win32;
+
+namespace CursorLang.Services;
+
+///
+/// Startup for a build that is not a package: a value under the Run key.
+///
+///
+/// A package declares its startup task in the manifest and asks Windows to switch
+/// it on. A build unpacked into a folder has no manifest, so it registers itself
+/// the way desktop programs always have — under the Run key of the current user.
+/// Administrator rights are not needed for that: the key belongs to the user.
+///
+/// Windows keeps the user's own verdict apart from the entry itself. Turning the
+/// app off in Settings — Apps — Startup leaves the Run value where it is and marks
+/// it disabled under StartupApproved. The mark is obeyed here the same way a
+/// package obeys DisabledByUser: the app does not argue with the user.
+///
+internal sealed class RegistryStartup
+{
+ private const string RunPath = @"Software\Microsoft\Windows\CurrentVersion\Run";
+
+ private const string ApprovedPath =
+ @"Software\Microsoft\Windows\CurrentVersion\Explorer\StartupApproved\Run";
+
+ /// The name of the value — Windows shows it in the startup list.
+ private const string ValueName = "CursorLang";
+
+ private readonly RegistryKey _root;
+ private readonly string? _command;
+
+ internal RegistryStartup()
+ : this(Registry.CurrentUser, GetCommand())
+ {
+ }
+
+ /// A root of the test's own, so that the real startup list is left alone.
+ internal RegistryStartup(RegistryKey root, string? command)
+ {
+ _root = root;
+ _command = command;
+ }
+
+ internal StartupState GetState()
+ {
+ if (_command is null)
+ {
+ return StartupState.Unavailable;
+ }
+
+ try
+ {
+ using RegistryKey? run = _root.OpenSubKey(RunPath);
+
+ if (run?.GetValue(ValueName) is null)
+ {
+ return StartupState.Disabled;
+ }
+
+ return IsApprovedByUser() ? StartupState.Enabled : StartupState.DisabledByUser;
+ }
+ catch (Exception e) when (e is SecurityException or UnauthorizedAccessException or IOException)
+ {
+ return StartupState.Unavailable;
+ }
+ }
+
+ internal StartupState SetEnabled(bool enabled)
+ {
+ if (_command is null)
+ {
+ return StartupState.Unavailable;
+ }
+
+ try
+ {
+ using RegistryKey run = _root.CreateSubKey(RunPath);
+
+ if (enabled)
+ {
+ // The path is written afresh every time: the app may have been moved
+ run.SetValue(ValueName, _command, RegistryValueKind.String);
+ }
+ else
+ {
+ run.DeleteValue(ValueName, throwOnMissingValue: false);
+ }
+ }
+ catch (Exception e) when (e is SecurityException or UnauthorizedAccessException or IOException)
+ {
+ return StartupState.Unavailable;
+ }
+
+ // The answer is read back rather than assumed: an entry the user has
+ // banned stays banned no matter what was just written next to it
+ return GetState();
+ }
+
+ ///
+ /// Whether the user has left the entry alone. The verdict is a blob whose
+ /// lowest bit of the first byte stands for the ban; no value means untouched.
+ ///
+ private bool IsApprovedByUser()
+ {
+ using RegistryKey? approved = _root.OpenSubKey(ApprovedPath);
+
+ return approved?.GetValue(ValueName) is not byte[] { Length: > 0 } verdict
+ || (verdict[0] & 1) == 0;
+ }
+
+ ///
+ /// What Windows is to run. null — the path of the running program is
+ /// unknown, and there is nothing to write down.
+ ///
+ private static string? GetCommand() =>
+ Environment.ProcessPath is { Length: > 0 } path ? $"\"{path}\"" : null;
+}
diff --git a/CursorLang/Services/StartupService.cs b/CursorLang/Services/StartupService.cs
new file mode 100644
index 0000000..22d3c9b
--- /dev/null
+++ b/CursorLang/Services/StartupService.cs
@@ -0,0 +1,85 @@
+using System.Runtime.InteropServices;
+using CursorLang.Interop;
+using CursorLang.Models;
+using Windows.ApplicationModel;
+
+namespace CursorLang.Services;
+
+///
+/// Startup, arranged by whatever means the current build has.
+///
+///
+/// Startup used to be a scheduled task with the highest rights — otherwise an app
+/// that wanted administrator rights would not start from the startup folder. The
+/// app needs no such rights any more, and the two ways left are simpler.
+///
+/// A package declares the task in its manifest, and Windows lists it for the user
+/// next to the rest under Settings — Apps — Startup. Turned off there, it can no
+/// longer be turned back on by the app. Outside a package the same setting is kept
+/// in the registry: see .
+///
+public sealed class StartupService : IStartupService
+{
+ /// Matches TaskId in the package manifest.
+ private const string TaskId = "CursorLangStartup";
+
+ private readonly RegistryStartup _registry = new();
+
+ public async Task GetStateAsync()
+ {
+ if (!PackageIdentityNative.IsPackaged)
+ {
+ return _registry.GetState();
+ }
+
+ try
+ {
+ StartupTask task = await StartupTask.GetAsync(TaskId);
+ return Translate(task.State);
+ }
+ catch (Exception e) when (e is COMException or ArgumentException or InvalidOperationException)
+ {
+ // No task by that name in the manifest: that happens to a package put
+ // together by hand. The setting simply will not show
+ return StartupState.Unavailable;
+ }
+ }
+
+ public async Task SetEnabledAsync(bool enabled)
+ {
+ if (!PackageIdentityNative.IsPackaged)
+ {
+ return _registry.SetEnabled(enabled);
+ }
+
+ try
+ {
+ StartupTask task = await StartupTask.GetAsync(TaskId);
+
+ if (!enabled)
+ {
+ task.Disable();
+ return Translate(task.State);
+ }
+
+ // Windows answers with a state rather than with success: once the user
+ // has forbidden startup, the ban stays
+ return Translate(await task.RequestEnableAsync());
+ }
+ catch (Exception e) when (e is COMException or ArgumentException or InvalidOperationException)
+ {
+ return StartupState.Unavailable;
+ }
+ }
+
+ /// The state of a Windows task in the app's own terms.
+ internal static StartupState Translate(StartupTaskState state) => state switch
+ {
+ StartupTaskState.Enabled => StartupState.Enabled,
+ StartupTaskState.EnabledByPolicy => StartupState.EnabledByPolicy,
+ StartupTaskState.Disabled => StartupState.Disabled,
+ StartupTaskState.DisabledByUser => StartupState.DisabledByUser,
+ StartupTaskState.DisabledByPolicy => StartupState.DisabledByPolicy,
+ _ => StartupState.Unavailable,
+ };
+}