Skip to content

Commit 60e6af7

Browse files
DRanger666Loongphy
andauthored
feat: support CODEX_HOME override (#51)
* feat: support CODEX_HOME override * test: fix windows path expectation * fix: align CODEX_HOME handling with Codex * test: stop forcing CODEX_HOME in default e2e runs --------- Co-authored-by: Loongphy <Loongphy@outlook.com>
1 parent 293285f commit 60e6af7

10 files changed

Lines changed: 459 additions & 57 deletions

File tree

README.md

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,21 @@ npx @loongphy/codex-auth list
4949
> npm installs already satisfy that requirement.
5050
> Legacy standalone binary installs need Node.js 18+ on `PATH` when `codex-auth config api enable` is used.
5151
52+
## Storage Root
53+
54+
`codex-auth` uses the same Codex state root as the current process. Resolution order:
55+
56+
1. `CODEX_HOME` when set to a non-empty existing directory
57+
2. `HOME/.codex`
58+
3. `USERPROFILE/.codex` on Windows
59+
60+
When `CODEX_HOME` is set, `codex-auth` follows Codex and requires that directory to already exist.
61+
That means you can isolate auth, registry, config, and session files by running:
62+
63+
```shell
64+
CODEX_HOME=/path/to/custom-codex codex-auth list
65+
```
66+
5267
### Uninstall
5368

5469
#### npm

docs/auto-switch.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -101,7 +101,7 @@ Platform bootstrap:
101101
- macOS: `LaunchAgent` with `KeepAlive`
102102
- Windows: user scheduled task with an `ONLOGON` trigger, restart-on-failure settings, and an unlimited execution time for `codex-auth-auto.exe`, plus an immediate `schtasks /Run` during enablement
103103

104-
Service install paths still resolve from the real user home directory.
104+
Service definition files stay in the platform-standard per-user locations. The managed watcher process uses the current `codex_home` root, so when `CODEX_HOME` is set during enablement the watcher keeps reading and writing that override after it starts in the background.
105105
Foreground commands other than `help`, `version`, `status`, and `daemon` still reconcile the managed service definition after they complete.
106106
`config auto enable` also prints a short usage-mode note so the user can see whether switching is currently running with default API-backed usage data or local-only fallback semantics.
107107
When migrating from older Linux/WSL timer-based installs, enable/reconcile also removes the legacy `codex-auth-autoswitch.timer` unit file instead of leaving the old minute timer behind.

docs/implement.md

Lines changed: 11 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -10,17 +10,18 @@ This document describes how `codex-auth` stores accounts, synchronizes auth file
1010

1111
## File Layout
1212

13-
- `~/.codex/auth.json`
14-
- `~/.codex/accounts/registry.json`
15-
- `~/.codex/accounts/<account file key>.auth.json`
16-
- `~/.codex/accounts/auth.json.bak.YYYYMMDD-hhmmss[.N]`
17-
- `~/.codex/accounts/registry.json.bak.YYYYMMDD-hhmmss[.N]`
18-
- `~/.codex/sessions/...`
13+
- `<codex_home>/auth.json`
14+
- `<codex_home>/accounts/registry.json`
15+
- `<codex_home>/accounts/<account file key>.auth.json`
16+
- `<codex_home>/accounts/auth.json.bak.YYYYMMDD-hhmmss[.N]`
17+
- `<codex_home>/accounts/registry.json.bak.YYYYMMDD-hhmmss[.N]`
18+
- `<codex_home>/sessions/...`
1919

20-
`codex-auth` resolves `codex_home` from the real user home directory:
20+
`codex-auth` resolves `codex_home` in this order:
2121

22-
1. `HOME/.codex`
23-
2. `USERPROFILE/.codex` (Windows fallback)
22+
1. `CODEX_HOME` when it is set to a non-empty existing directory
23+
2. `HOME/.codex`
24+
3. `USERPROFILE/.codex` (Windows fallback)
2425

2526
## Testing Conventions (BDD Style on std.testing)
2627

@@ -216,7 +217,7 @@ The detailed runtime, thresholds, service model, and data-source priority rules
216217
This document keeps only the cross-reference points that matter to the rest of the implementation:
217218

218219
- background config still lives in `registry.json` under top-level `auto_switch` and `api` blocks
219-
- managed services still resolve install paths from the real user home directory
220+
- managed services resolve from the same `codex_home` root as the active CLI process
220221
- successful foreground `codex-auth` commands except `help`, `version`, `status`, and `daemon` still reconcile the managed service definition
221222
- Linux/WSL `config auto enable` still requires a working `systemd --user` session
222223

docs/test.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -227,4 +227,4 @@ Get-ScheduledTask -TaskName 'CodexAuthAutoSwitch' -ErrorAction SilentlyContinue
227227

228228
Expected result after disable: no task is returned.
229229

230-
For isolated HOME tests, use `daemon --once` to validate actual switching behavior. The Windows managed service artifacts are installed under the real Windows user profile, so `enable/disable/status` and `daemon --once` together provide the cleanest acceptance signal even though the managed task itself now starts the persistent watcher mode.
230+
For isolated HOME tests, use `daemon --once` to validate actual switching behavior. The Windows task definition still lives in the real Windows user profile, while the managed watcher itself uses the `codex_home` that was active during enablement. `enable/disable/status` and `daemon --once` together still provide the cleanest acceptance signal.

src/auto.zig

Lines changed: 73 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ pub const Status = struct {
3838
};
3939

4040
const service_version_env_name = "CODEX_AUTH_VERSION";
41+
const codex_home_env_name = "CODEX_HOME";
4142

4243
pub const AutoSwitchAttempt = struct {
4344
refreshed_candidates: bool,
@@ -2036,12 +2037,11 @@ pub fn deleteAbsoluteFileIfExists(path: []const u8) void {
20362037
}
20372038

20382039
fn installWindowsService(allocator: std.mem.Allocator, codex_home: []const u8, self_exe: []const u8) !void {
2039-
_ = codex_home;
20402040
const helper_path = try windowsHelperPath(allocator, self_exe);
20412041
defer allocator.free(helper_path);
20422042
try std.fs.cwd().access(helper_path, .{});
20432043

2044-
const register_script = try windowsRegisterTaskScript(allocator, helper_path);
2044+
const register_script = try windowsRegisterTaskScript(allocator, helper_path, codex_home);
20452045
defer allocator.free(register_script);
20462046
const end_script = try windowsEndTaskScript(allocator);
20472047
defer allocator.free(end_script);
@@ -2125,52 +2125,60 @@ fn queryWindowsRuntimeState(allocator: std.mem.Allocator) RuntimeState {
21252125
}
21262126

21272127
pub fn linuxUnitText(allocator: std.mem.Allocator, self_exe: []const u8, codex_home: []const u8) ![]u8 {
2128-
_ = codex_home;
21292128
const exec = try std.fmt.allocPrint(allocator, "\"{s}\" daemon --watch", .{self_exe});
21302129
defer allocator.free(exec);
21312130
const escaped_version = try escapeSystemdValue(allocator, version.app_version);
21322131
defer allocator.free(escaped_version);
2132+
const escaped_codex_home = try escapeSystemdValue(allocator, codex_home);
2133+
defer allocator.free(escaped_codex_home);
21332134
return try std.fmt.allocPrint(
21342135
allocator,
2135-
"[Unit]\nDescription=codex-auth auto-switch watcher\n\n[Service]\nType=simple\nRestart=always\nRestartSec=1\nEnvironment=\"{s}={s}\"\nExecStart={s}\n\n[Install]\nWantedBy=default.target\n",
2136+
"[Unit]\nDescription=codex-auth auto-switch watcher\n\n[Service]\nType=simple\nRestart=always\nRestartSec=1\nEnvironment=\"{s}={s}\"\nEnvironment=\"{s}={s}\"\nExecStart={s}\n\n[Install]\nWantedBy=default.target\n",
21362137
.{
21372138
service_version_env_name,
21382139
escaped_version,
2140+
codex_home_env_name,
2141+
escaped_codex_home,
21392142
exec,
21402143
},
21412144
);
21422145
}
21432146

21442147
pub fn macPlistText(allocator: std.mem.Allocator, self_exe: []const u8, codex_home: []const u8) ![]u8 {
2145-
_ = codex_home;
21462148
const exe = try escapeXml(allocator, self_exe);
21472149
defer allocator.free(exe);
21482150
const current_version = try escapeXml(allocator, version.app_version);
21492151
defer allocator.free(current_version);
2152+
const escaped_codex_home = try escapeXml(allocator, codex_home);
2153+
defer allocator.free(escaped_codex_home);
21502154
return try std.fmt.allocPrint(
21512155
allocator,
2152-
"<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/PropertyList-1.0.dtd\">\n<plist version=\"1.0\">\n<dict>\n <key>Label</key>\n <string>{s}</string>\n <key>ProgramArguments</key>\n <array>\n <string>{s}</string>\n <string>daemon</string>\n <string>--watch</string>\n </array>\n <key>EnvironmentVariables</key>\n <dict>\n <key>{s}</key>\n <string>{s}</string>\n </dict>\n <key>RunAtLoad</key>\n <true/>\n <key>KeepAlive</key>\n <true/>\n</dict>\n</plist>\n",
2153-
.{ mac_label, exe, service_version_env_name, current_version },
2156+
"<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/PropertyList-1.0.dtd\">\n<plist version=\"1.0\">\n<dict>\n <key>Label</key>\n <string>{s}</string>\n <key>ProgramArguments</key>\n <array>\n <string>{s}</string>\n <string>daemon</string>\n <string>--watch</string>\n </array>\n <key>EnvironmentVariables</key>\n <dict>\n <key>{s}</key>\n <string>{s}</string>\n <key>{s}</key>\n <string>{s}</string>\n </dict>\n <key>RunAtLoad</key>\n <true/>\n <key>KeepAlive</key>\n <true/>\n</dict>\n</plist>\n",
2157+
.{ mac_label, exe, service_version_env_name, current_version, codex_home_env_name, escaped_codex_home },
21542158
);
21552159
}
21562160

2157-
pub fn windowsTaskAction(allocator: std.mem.Allocator, helper_path: []const u8) ![]u8 {
2161+
pub fn windowsTaskAction(allocator: std.mem.Allocator, helper_path: []const u8, codex_home: []const u8) ![]u8 {
2162+
const args = try windowsTaskArguments(allocator, codex_home);
2163+
defer allocator.free(args);
21582164
return try std.fmt.allocPrint(
21592165
allocator,
2160-
"\"{s}\" --service-version {s}",
2161-
.{ helper_path, version.app_version },
2166+
"\"{s}\" {s}",
2167+
.{ helper_path, args },
21622168
);
21632169
}
21642170

2165-
pub fn windowsRegisterTaskScript(allocator: std.mem.Allocator, helper_path: []const u8) ![]u8 {
2171+
pub fn windowsRegisterTaskScript(allocator: std.mem.Allocator, helper_path: []const u8, codex_home: []const u8) ![]u8 {
21662172
const escaped_helper_path = try escapePowerShellSingleQuoted(allocator, helper_path);
21672173
defer allocator.free(escaped_helper_path);
2168-
const escaped_version = try escapePowerShellSingleQuoted(allocator, version.app_version);
2169-
defer allocator.free(escaped_version);
2174+
const args = try windowsTaskArguments(allocator, codex_home);
2175+
defer allocator.free(args);
2176+
const escaped_args = try escapePowerShellSingleQuoted(allocator, args);
2177+
defer allocator.free(escaped_args);
21702178
return try std.fmt.allocPrint(
21712179
allocator,
2172-
"$action = New-ScheduledTaskAction -Execute '{s}' -Argument '--service-version {s}'; $trigger = New-ScheduledTaskTrigger -AtLogOn; $settings = New-ScheduledTaskSettingsSet -RestartCount {s} -RestartInterval (New-TimeSpan -Minutes 1) -ExecutionTimeLimit (New-TimeSpan -Seconds 0); Register-ScheduledTask -TaskName '{s}' -Action $action -Trigger $trigger -Settings $settings -Force | Out-Null",
2173-
.{ escaped_helper_path, escaped_version, windows_task_restart_count, windows_task_name },
2180+
"$action = New-ScheduledTaskAction -Execute '{s}' -Argument '{s}'; $trigger = New-ScheduledTaskTrigger -AtLogOn; $settings = New-ScheduledTaskSettingsSet -RestartCount {s} -RestartInterval (New-TimeSpan -Minutes 1) -ExecutionTimeLimit (New-TimeSpan -Seconds 0); Register-ScheduledTask -TaskName '{s}' -Action $action -Trigger $trigger -Settings $settings -Force | Out-Null",
2181+
.{ escaped_helper_path, escaped_args, windows_task_restart_count, windows_task_name },
21742182
);
21752183
}
21762184

@@ -2309,10 +2317,9 @@ fn macPlistMatches(allocator: std.mem.Allocator, codex_home: []const u8, self_ex
23092317
}
23102318

23112319
fn windowsTaskMatches(allocator: std.mem.Allocator, codex_home: []const u8, self_exe: []const u8) !bool {
2312-
_ = codex_home;
23132320
const helper_path = try windowsHelperPath(allocator, self_exe);
23142321
defer allocator.free(helper_path);
2315-
const expected_action = try windowsExpectedTaskFingerprint(allocator, helper_path);
2322+
const expected_action = try windowsExpectedTaskFingerprint(allocator, helper_path, codex_home);
23162323
defer allocator.free(expected_action);
23172324
const expected_fingerprint = try windowsExpectedTaskDefinitionFingerprint(allocator, expected_action);
23182325
defer allocator.free(expected_fingerprint);
@@ -2335,12 +2342,10 @@ fn windowsTaskMatches(allocator: std.mem.Allocator, codex_home: []const u8, self
23352342
};
23362343
}
23372344

2338-
fn windowsExpectedTaskFingerprint(allocator: std.mem.Allocator, helper_path: []const u8) ![]u8 {
2339-
return try std.fmt.allocPrint(
2340-
allocator,
2341-
"{s} --service-version {s}",
2342-
.{ helper_path, version.app_version },
2343-
);
2345+
fn windowsExpectedTaskFingerprint(allocator: std.mem.Allocator, helper_path: []const u8, codex_home: []const u8) ![]u8 {
2346+
const args = try windowsTaskArguments(allocator, codex_home);
2347+
defer allocator.free(args);
2348+
return try std.fmt.allocPrint(allocator, "{s} {s}", .{ helper_path, args });
23442349
}
23452350

23462351
fn windowsExpectedTaskDefinitionFingerprint(allocator: std.mem.Allocator, action: []const u8) ![]u8 {
@@ -2443,6 +2448,51 @@ fn escapePowerShellSingleQuoted(allocator: std.mem.Allocator, input: []const u8)
24432448
return std.mem.replaceOwned(u8, allocator, input, "'", "''");
24442449
}
24452450

2451+
fn windowsTaskArguments(allocator: std.mem.Allocator, codex_home: []const u8) ![]u8 {
2452+
const quoted_codex_home = try quoteWindowsCommandArg(allocator, codex_home);
2453+
defer allocator.free(quoted_codex_home);
2454+
return try std.fmt.allocPrint(
2455+
allocator,
2456+
"--service-version {s} --codex-home {s}",
2457+
.{ version.app_version, quoted_codex_home },
2458+
);
2459+
}
2460+
2461+
fn quoteWindowsCommandArg(allocator: std.mem.Allocator, arg: []const u8) ![]u8 {
2462+
const needs_quotes = blk: {
2463+
if (arg.len == 0) break :blk true;
2464+
for (arg) |ch| {
2465+
if (ch <= ' ' or ch == '"') break :blk true;
2466+
}
2467+
break :blk false;
2468+
};
2469+
if (!needs_quotes) return try allocator.dupe(u8, arg);
2470+
2471+
var out = std.ArrayList(u8).empty;
2472+
defer out.deinit(allocator);
2473+
try out.append(allocator, '"');
2474+
2475+
var backslash_count: usize = 0;
2476+
for (arg) |byte| {
2477+
switch (byte) {
2478+
'\\' => backslash_count += 1,
2479+
'"' => {
2480+
try out.appendNTimes(allocator, '\\', backslash_count * 2 + 1);
2481+
try out.append(allocator, '"');
2482+
backslash_count = 0;
2483+
},
2484+
else => {
2485+
try out.appendNTimes(allocator, '\\', backslash_count);
2486+
try out.append(allocator, byte);
2487+
backslash_count = 0;
2488+
},
2489+
}
2490+
}
2491+
try out.appendNTimes(allocator, '\\', backslash_count * 2);
2492+
try out.append(allocator, '"');
2493+
return try out.toOwnedSlice(allocator);
2494+
}
2495+
24462496
test "candidate index refreshes cached ranking after a reset window expires" {
24472497
const bdd = @import("tests/bdd_helpers.zig");
24482498
const gpa = std.testing.allocator;

src/registry.zig

Lines changed: 63 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -278,10 +278,70 @@ fn getNonEmptyEnvVarOwned(allocator: std.mem.Allocator, name: []const u8) !?[]u8
278278
return val;
279279
}
280280

281+
fn resolveExistingCodexHomeOverride(allocator: std.mem.Allocator, path: []const u8) ![]u8 {
282+
const stat = std.fs.cwd().statFile(path) catch |err| switch (err) {
283+
error.IsDir => {
284+
return std.fs.realpathAlloc(allocator, path) catch |realpath_err| {
285+
logCodexHomeResolutionError("failed to canonicalize CODEX_HOME `{s}`: {s}", .{ path, @errorName(realpath_err) });
286+
return realpath_err;
287+
};
288+
},
289+
error.FileNotFound => {
290+
logCodexHomeResolutionError("CODEX_HOME points to `{s}`, but that path does not exist", .{path});
291+
return err;
292+
},
293+
else => {
294+
logCodexHomeResolutionError("failed to read CODEX_HOME `{s}`: {s}", .{ path, @errorName(err) });
295+
return err;
296+
},
297+
};
298+
if (stat.kind != .directory) {
299+
logCodexHomeResolutionError("CODEX_HOME points to `{s}`, but that path is not a directory", .{path});
300+
return error.NotDir;
301+
}
302+
return std.fs.realpathAlloc(allocator, path) catch |err| {
303+
logCodexHomeResolutionError("failed to canonicalize CODEX_HOME `{s}`: {s}", .{ path, @errorName(err) });
304+
return err;
305+
};
306+
}
307+
308+
fn logCodexHomeResolutionError(
309+
comptime fmt: []const u8,
310+
args: anytype,
311+
) void {
312+
if (builtin.is_test) return;
313+
std.log.err(fmt, args);
314+
}
315+
316+
pub fn resolveCodexHomeFromEnv(
317+
allocator: std.mem.Allocator,
318+
codex_home_override: ?[]const u8,
319+
home: ?[]const u8,
320+
user_profile: ?[]const u8,
321+
) ![]u8 {
322+
if (codex_home_override) |path| {
323+
if (path.len != 0) return try resolveExistingCodexHomeOverride(allocator, path);
324+
}
325+
if (home) |path| {
326+
if (path.len != 0) return try std.fs.path.join(allocator, &[_][]const u8{ path, ".codex" });
327+
}
328+
if (user_profile) |path| {
329+
if (path.len != 0) return try std.fs.path.join(allocator, &[_][]const u8{ path, ".codex" });
330+
}
331+
return error.EnvironmentVariableNotFound;
332+
}
333+
281334
pub fn resolveCodexHome(allocator: std.mem.Allocator) ![]u8 {
282-
const home = try resolveUserHome(allocator);
283-
defer allocator.free(home);
284-
return try std.fs.path.join(allocator, &[_][]const u8{ home, ".codex" });
335+
const codex_home_override = try getNonEmptyEnvVarOwned(allocator, "CODEX_HOME");
336+
defer if (codex_home_override) |path| allocator.free(path);
337+
338+
const home = try getNonEmptyEnvVarOwned(allocator, "HOME");
339+
defer if (home) |path| allocator.free(path);
340+
341+
const user_profile = try getNonEmptyEnvVarOwned(allocator, "USERPROFILE");
342+
defer if (user_profile) |path| allocator.free(path);
343+
344+
return try resolveCodexHomeFromEnv(allocator, codex_home_override, home, user_profile);
285345
}
286346

287347
pub fn resolveUserHome(allocator: std.mem.Allocator) ![]u8 {

src/tests/auto_test.zig

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1365,6 +1365,7 @@ test "Scenario: Given linux service unit when rendering then it keeps a persiste
13651365
try std.testing.expect(std.mem.indexOf(u8, unit, "Type=simple") != null);
13661366
try std.testing.expect(std.mem.indexOf(u8, unit, "Restart=always") != null);
13671367
try std.testing.expect(std.mem.indexOf(u8, unit, "Environment=\"CODEX_AUTH_VERSION=") != null);
1368+
try std.testing.expect(std.mem.indexOf(u8, unit, "Environment=\"CODEX_HOME=/tmp/custom-codex-home\"") != null);
13681369
try std.testing.expect(std.mem.indexOf(u8, unit, "ExecStart=\"/tmp/codex-auth\" daemon --watch") != null);
13691370
try std.testing.expect(std.mem.indexOf(u8, unit, "[Install]") != null);
13701371
try std.testing.expect(std.mem.indexOf(u8, unit, "WantedBy=default.target") != null);
@@ -1403,27 +1404,31 @@ test "Scenario: Given mac plist when rendering then it includes version metadata
14031404
defer gpa.free(plist);
14041405

14051406
try std.testing.expect(std.mem.indexOf(u8, plist, "<key>CODEX_AUTH_VERSION</key>") != null);
1407+
try std.testing.expect(std.mem.indexOf(u8, plist, "<key>CODEX_HOME</key>") != null);
1408+
try std.testing.expect(std.mem.indexOf(u8, plist, "<string>/tmp/custom-codex-home</string>") != null);
14061409
try std.testing.expect(std.mem.indexOf(u8, plist, "<string>daemon</string>") != null);
14071410
}
14081411

14091412
test "Scenario: Given windows task action when rendering then it launches the helper directly without cmd" {
14101413
const gpa = std.testing.allocator;
1411-
const action = try auto.windowsTaskAction(gpa, "C:\\Program Files\\codex-auth\\codex-auth-auto.exe");
1414+
const action = try auto.windowsTaskAction(gpa, "C:\\Program Files\\codex-auth\\codex-auth-auto.exe", "C:\\Users\\demo\\Codex Home\\");
14121415
defer gpa.free(action);
14131416

14141417
try std.testing.expect(std.mem.indexOf(u8, action, "cmd.exe /D /C") == null);
14151418
try std.testing.expect(std.mem.indexOf(u8, action, "\"C:\\Program Files\\codex-auth\\codex-auth-auto.exe\"") != null);
14161419
try std.testing.expect(std.mem.indexOf(u8, action, "--service-version ") != null);
1420+
try std.testing.expect(std.mem.indexOf(u8, action, "--codex-home \"C:\\Users\\demo\\Codex Home\\\\\"") != null);
14171421
try std.testing.expect(std.mem.indexOf(u8, action, "powershell.exe") == null);
14181422
try std.testing.expect(action.len < 262);
14191423
}
14201424

14211425
test "Scenario: Given windows task register script when rendering then it configures restart-on-failure" {
14221426
const gpa = std.testing.allocator;
1423-
const script = try auto.windowsRegisterTaskScript(gpa, "C:\\Program Files\\codex-auth\\codex-auth-auto.exe");
1427+
const script = try auto.windowsRegisterTaskScript(gpa, "C:\\Program Files\\codex-auth\\codex-auth-auto.exe", "C:\\Users\\demo\\Codex Home\\");
14241428
defer gpa.free(script);
14251429

14261430
try std.testing.expect(std.mem.indexOf(u8, script, "New-ScheduledTaskAction") != null);
1431+
try std.testing.expect(std.mem.indexOf(u8, script, "--codex-home \"C:\\Users\\demo\\Codex Home\\\\\"") != null);
14271432
try std.testing.expect(std.mem.indexOf(u8, script, "New-ScheduledTaskTrigger -AtLogOn") != null);
14281433
try std.testing.expect(std.mem.indexOf(u8, script, "New-ScheduledTaskSettingsSet -RestartCount 999 -RestartInterval (New-TimeSpan -Minutes 1)") != null);
14291434
try std.testing.expect(std.mem.indexOf(u8, script, "-ExecutionTimeLimit (New-TimeSpan -Seconds 0)") != null);

0 commit comments

Comments
 (0)