@matthesketh/fleet 1.8.1 → 1.11.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +186 -16
- package/dist/bin/fleet-agent.d.ts +2 -0
- package/dist/bin/fleet-agent.js +7 -0
- package/dist/cli.d.ts +5 -0
- package/dist/cli.js +73 -31
- package/dist/commands/add.d.ts +2 -1
- package/dist/commands/add.js +66 -59
- package/dist/commands/audit.d.ts +1 -0
- package/dist/commands/audit.js +144 -0
- package/dist/commands/backup.d.ts +1 -0
- package/dist/commands/backup.js +510 -0
- package/dist/commands/boot-start.d.ts +3 -1
- package/dist/commands/boot-start.js +39 -47
- package/dist/commands/completions.d.ts +6 -0
- package/dist/commands/completions.js +83 -0
- package/dist/commands/config.d.ts +16 -0
- package/dist/commands/config.js +96 -0
- package/dist/commands/deploy.js +3 -2
- package/dist/commands/deps.js +5 -1
- package/dist/commands/doctor.d.ts +32 -0
- package/dist/commands/doctor.js +186 -0
- package/dist/commands/egress.d.ts +1 -1
- package/dist/commands/egress.js +13 -10
- package/dist/commands/freeze.d.ts +8 -4
- package/dist/commands/freeze.js +77 -59
- package/dist/commands/git.js +2 -2
- package/dist/commands/health.d.ts +2 -1
- package/dist/commands/health.js +38 -56
- package/dist/commands/init.d.ts +2 -1
- package/dist/commands/init.js +83 -73
- package/dist/commands/install-mcp.d.ts +3 -1
- package/dist/commands/install-mcp.js +53 -34
- package/dist/commands/list.d.ts +2 -1
- package/dist/commands/list.js +22 -19
- package/dist/commands/logs.js +1 -1
- package/dist/commands/patch-systemd.d.ts +7 -1
- package/dist/commands/patch-systemd.js +71 -31
- package/dist/commands/remove.d.ts +3 -1
- package/dist/commands/remove.js +37 -26
- package/dist/commands/restart.d.ts +4 -1
- package/dist/commands/restart.js +17 -20
- package/dist/commands/rollback.d.ts +4 -1
- package/dist/commands/rollback.js +33 -42
- package/dist/commands/secrets.js +157 -9
- package/dist/commands/start.d.ts +4 -1
- package/dist/commands/start.js +17 -20
- package/dist/commands/status.d.ts +1 -1
- package/dist/commands/status.js +21 -26
- package/dist/commands/stop.d.ts +4 -1
- package/dist/commands/stop.js +17 -20
- package/dist/commands/testflight.d.ts +1 -0
- package/dist/commands/testflight.js +193 -0
- package/dist/commands/update.d.ts +16 -0
- package/dist/commands/update.js +95 -0
- package/dist/core/audit/cache.d.ts +4 -0
- package/dist/core/audit/cache.js +37 -0
- package/dist/core/audit/config.d.ts +5 -0
- package/dist/core/audit/config.js +35 -0
- package/dist/core/audit/greenlight.d.ts +11 -0
- package/dist/core/audit/greenlight.js +81 -0
- package/dist/core/audit/reporters/cli.d.ts +3 -0
- package/dist/core/audit/reporters/cli.js +68 -0
- package/dist/core/audit/suppress.d.ts +6 -0
- package/dist/core/audit/suppress.js +37 -0
- package/dist/core/audit/target.d.ts +5 -0
- package/dist/core/audit/target.js +26 -0
- package/dist/core/audit/types.d.ts +54 -0
- package/dist/core/audit/types.js +5 -0
- package/dist/core/backup/browser-api.d.ts +66 -0
- package/dist/core/backup/browser-api.js +197 -0
- package/dist/core/backup/browser-server.d.ts +11 -0
- package/dist/core/backup/browser-server.js +241 -0
- package/dist/core/backup/browser-ui.d.ts +5 -0
- package/dist/core/backup/browser-ui.js +268 -0
- package/dist/core/backup/cloudflare.d.ts +7 -0
- package/dist/core/backup/cloudflare.js +82 -0
- package/dist/core/backup/config.d.ts +9 -0
- package/dist/core/backup/config.js +80 -0
- package/dist/core/backup/detect.d.ts +11 -0
- package/dist/core/backup/detect.js +71 -0
- package/dist/core/backup/dump.d.ts +11 -0
- package/dist/core/backup/dump.js +82 -0
- package/dist/core/backup/index.d.ts +9 -0
- package/dist/core/backup/index.js +9 -0
- package/dist/core/backup/repo.d.ts +71 -0
- package/dist/core/backup/repo.js +256 -0
- package/dist/core/backup/schedule.d.ts +17 -0
- package/dist/core/backup/schedule.js +90 -0
- package/dist/core/backup/sensitive.d.ts +5 -0
- package/dist/core/backup/sensitive.js +37 -0
- package/dist/core/backup/status.d.ts +3 -0
- package/dist/core/backup/status.js +29 -0
- package/dist/core/backup/statuspage.d.ts +23 -0
- package/dist/core/backup/statuspage.js +145 -0
- package/dist/core/backup/system.d.ts +24 -0
- package/dist/core/backup/system.js +209 -0
- package/dist/core/backup/totp.d.ts +16 -0
- package/dist/core/backup/totp.js +116 -0
- package/dist/core/backup/types.d.ts +70 -0
- package/dist/core/backup/types.js +7 -0
- package/dist/core/backup/unlock.d.ts +19 -0
- package/dist/core/backup/unlock.js +69 -0
- package/dist/core/boot-refresh.d.ts +1 -1
- package/dist/core/boot-refresh.js +10 -9
- package/dist/core/deps/actors/pr-creator.d.ts +5 -3
- package/dist/core/deps/actors/pr-creator.js +71 -18
- package/dist/core/deps/collectors/fetch-with-timeout.d.ts +7 -0
- package/dist/core/deps/collectors/fetch-with-timeout.js +16 -0
- package/dist/core/deps/collectors/npm.js +3 -1
- package/dist/core/deps/collectors/vulnerability.d.ts +8 -0
- package/dist/core/deps/collectors/vulnerability.js +31 -2
- package/dist/core/deps/config.js +6 -0
- package/dist/core/deps/scanner.js +1 -1
- package/dist/core/deps/types.d.ts +8 -0
- package/dist/core/env.d.ts +3 -0
- package/dist/core/env.js +11 -0
- package/dist/core/exec.d.ts +1 -0
- package/dist/core/exec.js +4 -0
- package/dist/core/file-lock.d.ts +18 -0
- package/dist/core/file-lock.js +44 -0
- package/dist/core/git-onboard.js +10 -13
- package/dist/core/github.d.ts +3 -1
- package/dist/core/github.js +10 -7
- package/dist/core/logs-policy.d.ts +5 -0
- package/dist/core/logs-policy.js +20 -1
- package/dist/core/operator.d.ts +21 -0
- package/dist/core/operator.js +54 -0
- package/dist/core/registry.d.ts +18 -0
- package/dist/core/registry.js +26 -0
- package/dist/core/routines/schema.d.ts +11 -11
- package/dist/core/routines/schema.js +14 -3
- package/dist/core/routines/store.d.ts +8 -8
- package/dist/core/secrets-ops.d.ts +31 -6
- package/dist/core/secrets-ops.js +208 -102
- package/dist/core/secrets-providers.js +2 -2
- package/dist/core/secrets-rotation.d.ts +1 -1
- package/dist/core/secrets-rotation.js +58 -52
- package/dist/core/secrets-v2-cleanup.d.ts +19 -0
- package/dist/core/secrets-v2-cleanup.js +94 -0
- package/dist/core/secrets-v2-creds.d.ts +9 -0
- package/dist/core/secrets-v2-creds.js +44 -0
- package/dist/core/secrets-v2-install.d.ts +13 -0
- package/dist/core/secrets-v2-install.js +76 -0
- package/dist/core/secrets-v2-keypair.d.ts +10 -0
- package/dist/core/secrets-v2-keypair.js +31 -0
- package/dist/core/secrets-v2-migrate.d.ts +29 -0
- package/dist/core/secrets-v2-migrate.js +395 -0
- package/dist/core/secrets-v2-ops.d.ts +36 -0
- package/dist/core/secrets-v2-ops.js +184 -0
- package/dist/core/secrets-v2-protocol.d.ts +19 -0
- package/dist/core/secrets-v2-protocol.js +60 -0
- package/dist/core/secrets-v2-snapshot.d.ts +36 -0
- package/dist/core/secrets-v2-snapshot.js +115 -0
- package/dist/core/secrets-v2.d.ts +21 -0
- package/dist/core/secrets-v2.js +249 -0
- package/dist/core/secrets.d.ts +39 -4
- package/dist/core/secrets.js +91 -11
- package/dist/core/self-update.d.ts +32 -11
- package/dist/core/self-update.js +52 -14
- package/dist/core/testflight/asc.d.ts +12 -0
- package/dist/core/testflight/asc.js +101 -0
- package/dist/core/testflight/credentials.d.ts +3 -0
- package/dist/core/testflight/credentials.js +35 -0
- package/dist/core/testflight/resolve.d.ts +6 -0
- package/dist/core/testflight/resolve.js +44 -0
- package/dist/core/testflight/types.d.ts +13 -0
- package/dist/core/testflight/types.js +3 -0
- package/dist/core/testflight/workflow.d.ts +17 -0
- package/dist/core/testflight/workflow.js +65 -0
- package/dist/core/validate.d.ts +1 -0
- package/dist/core/validate.js +8 -0
- package/dist/index.js +0 -0
- package/dist/mcp/audit-tools.d.ts +2 -0
- package/dist/mcp/audit-tools.js +94 -0
- package/dist/mcp/git-tools.js +1 -1
- package/dist/mcp/registry-bridge.d.ts +10 -0
- package/dist/mcp/registry-bridge.js +65 -0
- package/dist/mcp/secrets-tools.js +2 -2
- package/dist/mcp/server.js +16 -82
- package/dist/mcp/testflight-tools.d.ts +2 -0
- package/dist/mcp/testflight-tools.js +52 -0
- package/dist/registry/context.d.ts +7 -0
- package/dist/registry/context.js +37 -0
- package/dist/registry/index.d.ts +5 -0
- package/dist/registry/index.js +44 -0
- package/dist/registry/parse-args.d.ts +13 -0
- package/dist/registry/parse-args.js +74 -0
- package/dist/registry/registry.d.ts +24 -0
- package/dist/registry/registry.js +26 -0
- package/dist/registry/render.d.ts +3 -0
- package/dist/registry/render.js +29 -0
- package/dist/registry/types.d.ts +50 -0
- package/dist/registry/types.js +1 -0
- package/dist/templates/agent-unit.d.ts +5 -0
- package/dist/templates/agent-unit.js +40 -0
- package/dist/templates/app-unit-edit.d.ts +2 -0
- package/dist/templates/app-unit-edit.js +46 -0
- package/dist/templates/compose-edit.d.ts +2 -0
- package/dist/templates/compose-edit.js +156 -0
- package/dist/templates/nginx.js +11 -0
- package/dist/templates/systemd.js +6 -0
- package/dist/tui/components/ArgForm.d.ts +7 -0
- package/dist/tui/components/ArgForm.js +64 -0
- package/dist/tui/components/ArgForm.test.d.ts +1 -0
- package/dist/tui/components/ArgForm.test.js +19 -0
- package/dist/tui/components/KeyHint.js +5 -0
- package/dist/tui/hooks/use-secrets.d.ts +8 -8
- package/dist/tui/hooks/use-secrets.js +7 -7
- package/dist/tui/router.d.ts +1 -0
- package/dist/tui/router.js +26 -9
- package/dist/tui/router.test.d.ts +1 -0
- package/dist/tui/router.test.js +13 -0
- package/dist/tui/routines/components/SignalsGrid.test.js +2 -2
- package/dist/tui/routines/tabs/ScaffoldTab.js +1 -1
- package/dist/tui/tests/redaction-rerender.test.d.ts +1 -0
- package/dist/tui/tests/redaction-rerender.test.js +53 -0
- package/dist/tui/tests/scroll-flicker-proof.test.d.ts +1 -0
- package/dist/tui/tests/scroll-flicker-proof.test.js +145 -0
- package/dist/tui/types.d.ts +1 -1
- package/dist/tui/views/CommandPalette.d.ts +5 -0
- package/dist/tui/views/CommandPalette.js +90 -0
- package/dist/tui/views/CommandPalette.test.d.ts +1 -0
- package/dist/tui/views/CommandPalette.test.js +117 -0
- package/dist/tui/views/Dashboard.js +9 -6
- package/dist/tui/views/HealthView.js +9 -4
- package/dist/tui/views/SecretEdit.js +15 -16
- package/dist/tui/views/SecretEdit.test.d.ts +1 -0
- package/dist/tui/views/SecretEdit.test.js +82 -0
- package/dist/tui/views/SecretsView.js +26 -16
- package/package.json +8 -5
package/dist/core/env.js
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { FleetError } from './errors.js';
|
|
2
|
+
/** returns a required env var, or throws a clear error. use for secrets,
|
|
3
|
+
* keys and credential paths where a silent default would be dangerous. */
|
|
4
|
+
export function requireEnv(name) {
|
|
5
|
+
const value = process.env[name];
|
|
6
|
+
if (value === undefined || value === '') {
|
|
7
|
+
throw new FleetError(`required environment variable ${name} is not set — ` +
|
|
8
|
+
`it has no safe default and must be provided explicitly`);
|
|
9
|
+
}
|
|
10
|
+
return value;
|
|
11
|
+
}
|
package/dist/core/exec.d.ts
CHANGED
package/dist/core/exec.js
CHANGED
|
@@ -7,6 +7,10 @@ export function execSafe(cmd, args, opts = {}) {
|
|
|
7
7
|
encoding: 'utf-8',
|
|
8
8
|
stdio: 'pipe',
|
|
9
9
|
input: opts.input,
|
|
10
|
+
// node's default is 1mb. restic --json on a long-running snapshot emits
|
|
11
|
+
// hundreds of progress lines that easily blow past that; bump to 256mb
|
|
12
|
+
// so a multi-hour run can't hit ENOBUFS just from status chatter.
|
|
13
|
+
maxBuffer: opts.maxBuffer ?? 256 * 1024 * 1024,
|
|
10
14
|
});
|
|
11
15
|
if (result.error) {
|
|
12
16
|
return {
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Inter-process lock around a state-file path. Uses proper-lockfile (the same
|
|
3
|
+
* dependency the claude-cli runner uses for its mutex), which creates a
|
|
4
|
+
* <path>.lock directory atomically via mkdir(2).
|
|
5
|
+
*
|
|
6
|
+
* The wrapped path itself does not need to exist yet — `realpath: false`
|
|
7
|
+
* tells proper-lockfile to skip the realpath check, so we can lock around a
|
|
8
|
+
* registry/manifest file that hasn't been written for the first time. The
|
|
9
|
+
* parent directory of <path> must exist (we ensureDir below) so the .lock
|
|
10
|
+
* mkdir can succeed.
|
|
11
|
+
*
|
|
12
|
+
* Important: this lock is NOT reentrant. Callers should wrap the outermost
|
|
13
|
+
* read-modify-write boundary (e.g. a CLI command, an MCP tool handler, a
|
|
14
|
+
* cron entry) and let inner helpers do plain unlocked reads/writes; the lock
|
|
15
|
+
* bounds the whole RMW. Locking inside helpers that the outer caller already
|
|
16
|
+
* locked will deadlock.
|
|
17
|
+
*/
|
|
18
|
+
export declare function withFileLock<T>(path: string, fn: () => Promise<T> | T): Promise<T>;
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { existsSync, mkdirSync } from 'node:fs';
|
|
2
|
+
import { dirname } from 'node:path';
|
|
3
|
+
import lockfile from 'proper-lockfile';
|
|
4
|
+
/**
|
|
5
|
+
* Inter-process lock around a state-file path. Uses proper-lockfile (the same
|
|
6
|
+
* dependency the claude-cli runner uses for its mutex), which creates a
|
|
7
|
+
* <path>.lock directory atomically via mkdir(2).
|
|
8
|
+
*
|
|
9
|
+
* The wrapped path itself does not need to exist yet — `realpath: false`
|
|
10
|
+
* tells proper-lockfile to skip the realpath check, so we can lock around a
|
|
11
|
+
* registry/manifest file that hasn't been written for the first time. The
|
|
12
|
+
* parent directory of <path> must exist (we ensureDir below) so the .lock
|
|
13
|
+
* mkdir can succeed.
|
|
14
|
+
*
|
|
15
|
+
* Important: this lock is NOT reentrant. Callers should wrap the outermost
|
|
16
|
+
* read-modify-write boundary (e.g. a CLI command, an MCP tool handler, a
|
|
17
|
+
* cron entry) and let inner helpers do plain unlocked reads/writes; the lock
|
|
18
|
+
* bounds the whole RMW. Locking inside helpers that the outer caller already
|
|
19
|
+
* locked will deadlock.
|
|
20
|
+
*/
|
|
21
|
+
export async function withFileLock(path, fn) {
|
|
22
|
+
const dir = dirname(path);
|
|
23
|
+
if (!existsSync(dir))
|
|
24
|
+
mkdirSync(dir, { recursive: true });
|
|
25
|
+
const release = await lockfile.lock(path, {
|
|
26
|
+
// Inter-process contention is normally microseconds (one process writes,
|
|
27
|
+
// releases). Retry up to ~5s of backoff so a slow disk / paused process
|
|
28
|
+
// doesn't immediately error out the second caller.
|
|
29
|
+
retries: { retries: 5, factor: 1.5, minTimeout: 50, maxTimeout: 500 },
|
|
30
|
+
// If a process crashes mid-lock, the .lock dir's mtime stops being
|
|
31
|
+
// refreshed. Anyone waiting longer than `stale` ms treats the lock as
|
|
32
|
+
// abandoned and steals it. 30s is generous for the kinds of operations
|
|
33
|
+
// that touch the registry/manifest (a write is < 100ms typically).
|
|
34
|
+
stale: 30_000,
|
|
35
|
+
// Allow locking paths that don't exist on disk yet (first-write case).
|
|
36
|
+
realpath: false,
|
|
37
|
+
});
|
|
38
|
+
try {
|
|
39
|
+
return await fn();
|
|
40
|
+
}
|
|
41
|
+
finally {
|
|
42
|
+
await release();
|
|
43
|
+
}
|
|
44
|
+
}
|
package/dist/core/git-onboard.js
CHANGED
|
@@ -4,23 +4,20 @@ import { load, findApp, save } from './registry.js';
|
|
|
4
4
|
export function detectScenario(status) {
|
|
5
5
|
if (!status.initialised)
|
|
6
6
|
return 'fresh';
|
|
7
|
-
if (status.remoteUrl && status.remoteUrl.includes('heskethwebdesign/'))
|
|
8
|
-
return 'resume';
|
|
9
|
-
if (status.remoteUrl && status.remoteUrl.includes('wrxck/'))
|
|
10
|
-
return 'migrate';
|
|
11
7
|
if (!status.remoteUrl)
|
|
12
8
|
return 'no-remote';
|
|
13
|
-
|
|
9
|
+
// a remote already on the configured org is a resume; any other org is a migrate.
|
|
10
|
+
return status.remoteUrl.includes(`${github.githubOrg()}/`) ? 'resume' : 'migrate';
|
|
14
11
|
}
|
|
15
12
|
export function describeOnboardPlan(scenario, repoName, _status) {
|
|
16
|
-
const repoUrl = `git@github.com
|
|
13
|
+
const repoUrl = `git@github.com:${github.githubOrg()}/${repoName}.git`;
|
|
17
14
|
const steps = [];
|
|
18
15
|
switch (scenario) {
|
|
19
16
|
case 'fresh':
|
|
20
17
|
steps.push('generate .gitignore');
|
|
21
18
|
steps.push('git init -b main');
|
|
22
19
|
steps.push('git add . && git commit -m "initial commit"');
|
|
23
|
-
steps.push(`create private repo
|
|
20
|
+
steps.push(`create private repo ${github.githubOrg()}/${repoName}`);
|
|
24
21
|
steps.push(`add remote origin ${repoUrl}`);
|
|
25
22
|
steps.push('push main');
|
|
26
23
|
steps.push('create and push develop branch');
|
|
@@ -29,7 +26,7 @@ export function describeOnboardPlan(scenario, repoName, _status) {
|
|
|
29
26
|
break;
|
|
30
27
|
case 'migrate':
|
|
31
28
|
steps.push('ensure .gitignore exists');
|
|
32
|
-
steps.push(`create private repo
|
|
29
|
+
steps.push(`create private repo ${github.githubOrg()}/${repoName}`);
|
|
33
30
|
steps.push(`git remote set-url origin ${repoUrl}`);
|
|
34
31
|
steps.push('git push --all origin');
|
|
35
32
|
steps.push('ensure develop branch exists');
|
|
@@ -39,7 +36,7 @@ export function describeOnboardPlan(scenario, repoName, _status) {
|
|
|
39
36
|
case 'no-remote':
|
|
40
37
|
steps.push('ensure .gitignore exists');
|
|
41
38
|
steps.push('commit any outstanding changes');
|
|
42
|
-
steps.push(`create private repo
|
|
39
|
+
steps.push(`create private repo ${github.githubOrg()}/${repoName}`);
|
|
43
40
|
steps.push(`add remote origin ${repoUrl}`);
|
|
44
41
|
steps.push('git push --all origin');
|
|
45
42
|
steps.push('ensure develop branch exists');
|
|
@@ -79,7 +76,7 @@ export function executeOnboard(scenario, cwd, repoName, appName, status) {
|
|
|
79
76
|
gitCommit(cwd, 'Initial commit');
|
|
80
77
|
steps.push('created initial commit');
|
|
81
78
|
github.createRepo(repoName);
|
|
82
|
-
steps.push(`created private repo
|
|
79
|
+
steps.push(`created private repo ${github.githubOrg()}/${repoName}`);
|
|
83
80
|
gitAddRemote(cwd, 'origin', repoUrl);
|
|
84
81
|
gitPush(cwd, 'main', true);
|
|
85
82
|
steps.push('pushed main to origin');
|
|
@@ -90,7 +87,7 @@ export function executeOnboard(scenario, cwd, repoName, appName, status) {
|
|
|
90
87
|
}
|
|
91
88
|
case 'migrate': {
|
|
92
89
|
github.createRepo(repoName);
|
|
93
|
-
steps.push(`created private repo
|
|
90
|
+
steps.push(`created private repo ${github.githubOrg()}/${repoName}`);
|
|
94
91
|
gitSetRemoteUrl(cwd, repoUrl);
|
|
95
92
|
steps.push(`updated remote to ${repoUrl}`);
|
|
96
93
|
gitPushAll(cwd);
|
|
@@ -110,7 +107,7 @@ export function executeOnboard(scenario, cwd, repoName, appName, status) {
|
|
|
110
107
|
steps.push('created initial commit');
|
|
111
108
|
}
|
|
112
109
|
github.createRepo(repoName);
|
|
113
|
-
steps.push(`created private repo
|
|
110
|
+
steps.push(`created private repo ${github.githubOrg()}/${repoName}`);
|
|
114
111
|
gitAddRemote(cwd, 'origin', repoUrl);
|
|
115
112
|
gitPushAll(cwd);
|
|
116
113
|
steps.push('added remote and pushed all branches');
|
|
@@ -139,7 +136,7 @@ export function executeOnboard(scenario, cwd, repoName, appName, status) {
|
|
|
139
136
|
const reg = load();
|
|
140
137
|
const app = findApp(reg, appName);
|
|
141
138
|
if (app) {
|
|
142
|
-
app.gitRepo =
|
|
139
|
+
app.gitRepo = `${github.githubOrg()}/${repoName}`;
|
|
143
140
|
app.gitRemoteUrl = repoUrl;
|
|
144
141
|
app.gitOnboardedAt = new Date().toISOString();
|
|
145
142
|
save(reg);
|
package/dist/core/github.d.ts
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
|
-
|
|
1
|
+
/** the GitHub org this fleet instance publishes to — from operator config,
|
|
2
|
+
* with no default: guessing another operator's org is never correct. */
|
|
3
|
+
export declare function githubOrg(): string;
|
|
2
4
|
export interface PullRequest {
|
|
3
5
|
number: number;
|
|
4
6
|
title: string;
|
package/dist/core/github.js
CHANGED
|
@@ -3,8 +3,11 @@ import { tmpdir } from 'node:os';
|
|
|
3
3
|
import { join } from 'node:path';
|
|
4
4
|
import { execSafe } from './exec.js';
|
|
5
5
|
import { GitError } from './errors.js';
|
|
6
|
+
import { loadOperator } from './operator.js';
|
|
6
7
|
import { assertAppName } from './validate.js';
|
|
7
|
-
|
|
8
|
+
/** the GitHub org this fleet instance publishes to — from operator config,
|
|
9
|
+
* with no default: guessing another operator's org is never correct. */
|
|
10
|
+
export function githubOrg() { return loadOperator().githubOrg; }
|
|
8
11
|
export function isGhAuthenticated() {
|
|
9
12
|
return execSafe('gh', ['auth', 'status'], { timeout: 10_000 }).ok;
|
|
10
13
|
}
|
|
@@ -15,25 +18,25 @@ export function requireGhAuth() {
|
|
|
15
18
|
}
|
|
16
19
|
export function repoExists(name) {
|
|
17
20
|
assertAppName(name);
|
|
18
|
-
return execSafe('gh', ['repo', 'view', `${
|
|
21
|
+
return execSafe('gh', ['repo', 'view', `${githubOrg()}/${name}`, '--json', 'name'], { timeout: 15_000 }).ok;
|
|
19
22
|
}
|
|
20
23
|
export function createRepo(name) {
|
|
21
24
|
requireGhAuth();
|
|
22
25
|
assertAppName(name);
|
|
23
26
|
if (repoExists(name))
|
|
24
27
|
return;
|
|
25
|
-
const r = execSafe('gh', ['repo', 'create', `${
|
|
28
|
+
const r = execSafe('gh', ['repo', 'create', `${githubOrg()}/${name}`, '--private'], { timeout: 30_000 });
|
|
26
29
|
if (!r.ok)
|
|
27
30
|
throw new GitError(`failed to create repo: ${r.stderr}`);
|
|
28
31
|
}
|
|
29
32
|
export function getRepoUrl(name) {
|
|
30
|
-
return `git@github.com:${
|
|
33
|
+
return `git@github.com:${githubOrg()}/${name}.git`;
|
|
31
34
|
}
|
|
32
35
|
export function createPullRequest(repo, opts) {
|
|
33
36
|
requireGhAuth();
|
|
34
37
|
const r = execSafe('gh', [
|
|
35
38
|
'pr', 'create',
|
|
36
|
-
'--repo', `${
|
|
39
|
+
'--repo', `${githubOrg()}/${repo}`,
|
|
37
40
|
'--title', opts.title,
|
|
38
41
|
'--body', opts.body ?? '',
|
|
39
42
|
'--head', opts.head,
|
|
@@ -63,7 +66,7 @@ export function listPullRequests(repo, state = 'open') {
|
|
|
63
66
|
requireGhAuth();
|
|
64
67
|
const r = execSafe('gh', [
|
|
65
68
|
'pr', 'list',
|
|
66
|
-
'--repo', `${
|
|
69
|
+
'--repo', `${githubOrg()}/${repo}`,
|
|
67
70
|
'--state', state,
|
|
68
71
|
'--json', 'number,title,url,headRefName,baseRefName,state',
|
|
69
72
|
], { timeout: 15_000 });
|
|
@@ -97,7 +100,7 @@ export function protectBranch(repo, branch) {
|
|
|
97
100
|
try {
|
|
98
101
|
const r = execSafe('gh', [
|
|
99
102
|
'api', '-X', 'PUT',
|
|
100
|
-
`repos/${
|
|
103
|
+
`repos/${githubOrg()}/${repo}/branches/${branch}/protection`,
|
|
101
104
|
'--input', tmpFile,
|
|
102
105
|
], { timeout: 15_000 });
|
|
103
106
|
return r.ok;
|
|
@@ -18,6 +18,11 @@ export declare function effectivePolicy(app: AppEntry): LogPolicy;
|
|
|
18
18
|
*/
|
|
19
19
|
export declare function buildComposeOverride(app: AppEntry, policy: LogPolicy): string;
|
|
20
20
|
export declare function overridePath(app: AppEntry): string;
|
|
21
|
+
/** ensure the app repo's .gitignore covers .fleet/ so the auto-generated
|
|
22
|
+
* override file doesn't get committed accidentally. no-op when there's no
|
|
23
|
+
* .gitignore (operator may be using a different vcs or none at all) and
|
|
24
|
+
* idempotent — never appends a duplicate entry. */
|
|
25
|
+
export declare function ensureFleetGitignored(composePath: string): void;
|
|
21
26
|
export declare function writeComposeOverride(app: AppEntry, policy: LogPolicy): string;
|
|
22
27
|
export interface LogStatus {
|
|
23
28
|
app: string;
|
package/dist/core/logs-policy.js
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* compose override file (.fleet/logging.override.yml) per app, plus journald
|
|
4
4
|
* vacuum policy. Conservative defaults applied when unset.
|
|
5
5
|
*/
|
|
6
|
-
import { existsSync, mkdirSync, writeFileSync, statSync } from 'node:fs';
|
|
6
|
+
import { existsSync, mkdirSync, writeFileSync, readFileSync, statSync } from 'node:fs';
|
|
7
7
|
import { join, dirname } from 'node:path';
|
|
8
8
|
import { execSafe } from './exec.js';
|
|
9
9
|
export const DEFAULT_POLICY = {
|
|
@@ -50,12 +50,31 @@ export function buildComposeOverride(app, policy) {
|
|
|
50
50
|
export function overridePath(app) {
|
|
51
51
|
return join(app.composePath, '.fleet', 'logging.override.yml');
|
|
52
52
|
}
|
|
53
|
+
/** ensure the app repo's .gitignore covers .fleet/ so the auto-generated
|
|
54
|
+
* override file doesn't get committed accidentally. no-op when there's no
|
|
55
|
+
* .gitignore (operator may be using a different vcs or none at all) and
|
|
56
|
+
* idempotent — never appends a duplicate entry. */
|
|
57
|
+
export function ensureFleetGitignored(composePath) {
|
|
58
|
+
const giPath = join(composePath, '.gitignore');
|
|
59
|
+
if (!existsSync(giPath))
|
|
60
|
+
return;
|
|
61
|
+
const current = readFileSync(giPath, 'utf-8');
|
|
62
|
+
// accept any of: .fleet, .fleet/, /.fleet, /.fleet/ — operators write all
|
|
63
|
+
// four forms.
|
|
64
|
+
if (/^\s*\/?\.fleet\/?\s*$/m.test(current))
|
|
65
|
+
return;
|
|
66
|
+
const append = current.endsWith('\n') ? '' : '\n';
|
|
67
|
+
writeFileSync(giPath, `${current}${append}\n# auto-added by fleet logs setup\n.fleet/\n`);
|
|
68
|
+
}
|
|
53
69
|
export function writeComposeOverride(app, policy) {
|
|
54
70
|
const path = overridePath(app);
|
|
55
71
|
const dir = dirname(path);
|
|
56
72
|
if (!existsSync(dir))
|
|
57
73
|
mkdirSync(dir, { recursive: true });
|
|
58
74
|
writeFileSync(path, buildComposeOverride(app, policy));
|
|
75
|
+
// the override lands inside the operator's app repo. add .fleet/ to the
|
|
76
|
+
// app's .gitignore (if there is one) so it doesn't get committed.
|
|
77
|
+
ensureFleetGitignored(app.composePath);
|
|
59
78
|
return path;
|
|
60
79
|
}
|
|
61
80
|
/** Best-effort docker-side log status. Uses `docker inspect` for driver + size. */
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
export interface OperatorConfig {
|
|
2
|
+
username: string;
|
|
3
|
+
homeDir: string;
|
|
4
|
+
domain: string;
|
|
5
|
+
githubOrg: string;
|
|
6
|
+
}
|
|
7
|
+
export type OperatorField = keyof OperatorConfig;
|
|
8
|
+
export declare const OPERATOR_FIELDS: readonly OperatorField[];
|
|
9
|
+
/** test-only: clears the memoised config. */
|
|
10
|
+
export declare function _resetOperatorCache(): void;
|
|
11
|
+
/** path the operator config is read from / written to. exported so the
|
|
12
|
+
* fleet config command can print where it lives. */
|
|
13
|
+
export declare function operatorPath(): string;
|
|
14
|
+
/** loads operator identity from data/operator.json (gitignored, instance-local).
|
|
15
|
+
* throws if the file is missing or incomplete — there is no safe default,
|
|
16
|
+
* and guessing another operator's identity is never correct. */
|
|
17
|
+
export declare function loadOperator(): OperatorConfig;
|
|
18
|
+
/** persist the operator config to disk and clear the memoised copy so the
|
|
19
|
+
* next loadOperator() picks up the new values. atomic write via .tmp +
|
|
20
|
+
* rename so a crash mid-write never leaves a partial file behind. */
|
|
21
|
+
export declare function saveOperator(cfg: OperatorConfig): void;
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { existsSync, mkdirSync, readFileSync, renameSync, writeFileSync } from 'node:fs';
|
|
2
|
+
import { dirname, join } from 'node:path';
|
|
3
|
+
import { fileURLToPath } from 'node:url';
|
|
4
|
+
import { FleetError } from './errors.js';
|
|
5
|
+
const here = dirname(fileURLToPath(import.meta.url));
|
|
6
|
+
export const OPERATOR_FIELDS = ['username', 'homeDir', 'domain', 'githubOrg'];
|
|
7
|
+
const FIELDS = OPERATOR_FIELDS;
|
|
8
|
+
let cache = null;
|
|
9
|
+
/** test-only: clears the memoised config. */
|
|
10
|
+
export function _resetOperatorCache() { cache = null; }
|
|
11
|
+
/** path the operator config is read from / written to. exported so the
|
|
12
|
+
* fleet config command can print where it lives. */
|
|
13
|
+
export function operatorPath() {
|
|
14
|
+
return process.env.FLEET_OPERATOR_PATH ?? join(here, '..', '..', 'data', 'operator.json');
|
|
15
|
+
}
|
|
16
|
+
/** loads operator identity from data/operator.json (gitignored, instance-local).
|
|
17
|
+
* throws if the file is missing or incomplete — there is no safe default,
|
|
18
|
+
* and guessing another operator's identity is never correct. */
|
|
19
|
+
export function loadOperator() {
|
|
20
|
+
if (cache)
|
|
21
|
+
return cache;
|
|
22
|
+
const path = operatorPath();
|
|
23
|
+
if (!existsSync(path)) {
|
|
24
|
+
throw new FleetError(`operator config not found at ${path} — ` +
|
|
25
|
+
`copy data/operator.example.json to data/operator.json and fill it in`);
|
|
26
|
+
}
|
|
27
|
+
const raw = JSON.parse(readFileSync(path, 'utf-8'));
|
|
28
|
+
for (const field of FIELDS) {
|
|
29
|
+
if (!raw[field])
|
|
30
|
+
throw new FleetError(`operator config ${path} is missing field: ${field}`);
|
|
31
|
+
}
|
|
32
|
+
cache = raw;
|
|
33
|
+
return cache;
|
|
34
|
+
}
|
|
35
|
+
/** persist the operator config to disk and clear the memoised copy so the
|
|
36
|
+
* next loadOperator() picks up the new values. atomic write via .tmp +
|
|
37
|
+
* rename so a crash mid-write never leaves a partial file behind. */
|
|
38
|
+
export function saveOperator(cfg) {
|
|
39
|
+
for (const field of FIELDS) {
|
|
40
|
+
if (typeof cfg[field] !== 'string' || cfg[field].length === 0) {
|
|
41
|
+
throw new FleetError(`operator config: ${field} must be a non-empty string`);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
const path = operatorPath();
|
|
45
|
+
const dir = dirname(path);
|
|
46
|
+
if (!existsSync(dir))
|
|
47
|
+
mkdirSync(dir, { recursive: true });
|
|
48
|
+
const tmp = path + '.tmp';
|
|
49
|
+
writeFileSync(tmp, JSON.stringify(cfg, null, 2) + '\n', { mode: 0o600 });
|
|
50
|
+
// rename is atomic on the same filesystem; covers the crash-mid-write race
|
|
51
|
+
// a plain writeFileSync exposes.
|
|
52
|
+
renameSync(tmp, path);
|
|
53
|
+
cache = null;
|
|
54
|
+
}
|
package/dist/core/registry.d.ts
CHANGED
|
@@ -61,3 +61,21 @@ export declare function findApp(reg: Registry, name: string): AppEntry | undefin
|
|
|
61
61
|
export declare function addApp(reg: Registry, app: AppEntry): Registry;
|
|
62
62
|
export declare function removeApp(reg: Registry, name: string): Registry;
|
|
63
63
|
export declare function registryPath(): string;
|
|
64
|
+
/**
|
|
65
|
+
* Run a read-modify-write transaction against the registry under an
|
|
66
|
+
* inter-process lock. The lock is held for the full load → mutate → save
|
|
67
|
+
* cycle, so concurrent CLI / cron / systemd / bot invocations don't lose
|
|
68
|
+
* each other's updates.
|
|
69
|
+
*
|
|
70
|
+
* The mutator may return a different Registry object (e.g. one returned by
|
|
71
|
+
* `addApp` / `removeApp`, which mutate in place but also return the registry
|
|
72
|
+
* for chaining) or simply mutate the input and return it. The returned value
|
|
73
|
+
* is what gets persisted.
|
|
74
|
+
*
|
|
75
|
+
* Returns void: callers needing the post-save state should re-load. Keeping
|
|
76
|
+
* this side-effecting matches how `load()` + `save()` are used today.
|
|
77
|
+
*
|
|
78
|
+
* Important: do not call this from inside another `withRegistry` block on the
|
|
79
|
+
* same process — proper-lockfile is not reentrant and will deadlock.
|
|
80
|
+
*/
|
|
81
|
+
export declare function withRegistry(fn: (reg: Registry) => Registry | Promise<Registry>): Promise<void>;
|
package/dist/core/registry.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { readFileSync, existsSync, mkdirSync, copyFileSync, renameSync, openSync, writeSync, fsyncSync, closeSync } from 'node:fs';
|
|
2
2
|
import { dirname, join } from 'node:path';
|
|
3
3
|
import { fileURLToPath } from 'node:url';
|
|
4
|
+
import { withFileLock } from './file-lock.js';
|
|
4
5
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
5
6
|
function resolveRegistryPath() {
|
|
6
7
|
return process.env.FLEET_REGISTRY_PATH
|
|
@@ -92,3 +93,28 @@ export function removeApp(reg, name) {
|
|
|
92
93
|
export function registryPath() {
|
|
93
94
|
return resolveRegistryPath();
|
|
94
95
|
}
|
|
96
|
+
/**
|
|
97
|
+
* Run a read-modify-write transaction against the registry under an
|
|
98
|
+
* inter-process lock. The lock is held for the full load → mutate → save
|
|
99
|
+
* cycle, so concurrent CLI / cron / systemd / bot invocations don't lose
|
|
100
|
+
* each other's updates.
|
|
101
|
+
*
|
|
102
|
+
* The mutator may return a different Registry object (e.g. one returned by
|
|
103
|
+
* `addApp` / `removeApp`, which mutate in place but also return the registry
|
|
104
|
+
* for chaining) or simply mutate the input and return it. The returned value
|
|
105
|
+
* is what gets persisted.
|
|
106
|
+
*
|
|
107
|
+
* Returns void: callers needing the post-save state should re-load. Keeping
|
|
108
|
+
* this side-effecting matches how `load()` + `save()` are used today.
|
|
109
|
+
*
|
|
110
|
+
* Important: do not call this from inside another `withRegistry` block on the
|
|
111
|
+
* same process — proper-lockfile is not reentrant and will deadlock.
|
|
112
|
+
*/
|
|
113
|
+
export async function withRegistry(fn) {
|
|
114
|
+
const path = resolveRegistryPath();
|
|
115
|
+
await withFileLock(path, async () => {
|
|
116
|
+
const reg = load();
|
|
117
|
+
const next = await fn(reg);
|
|
118
|
+
save(next);
|
|
119
|
+
});
|
|
120
|
+
}
|
|
@@ -204,15 +204,14 @@ export declare const RoutineSchema: z.ZodObject<{
|
|
|
204
204
|
createdAt: z.ZodOptional<z.ZodString>;
|
|
205
205
|
updatedAt: z.ZodOptional<z.ZodString>;
|
|
206
206
|
}, "strip", z.ZodTypeAny, {
|
|
207
|
-
enabled: boolean;
|
|
208
207
|
name: string;
|
|
208
|
+
enabled: boolean;
|
|
209
209
|
id: string;
|
|
210
210
|
notify: {
|
|
211
211
|
config: Record<string, unknown>;
|
|
212
212
|
kind: "email" | "stdout" | "webhook" | "slack";
|
|
213
213
|
on: "always" | "failure" | "success";
|
|
214
214
|
}[];
|
|
215
|
-
description: string;
|
|
216
215
|
schedule: {
|
|
217
216
|
kind: "manual";
|
|
218
217
|
} | {
|
|
@@ -221,6 +220,8 @@ export declare const RoutineSchema: z.ZodObject<{
|
|
|
221
220
|
randomizedDelaySec: number;
|
|
222
221
|
persistent: boolean;
|
|
223
222
|
};
|
|
223
|
+
tags: string[];
|
|
224
|
+
description: string;
|
|
224
225
|
targets: string[];
|
|
225
226
|
perTarget: boolean;
|
|
226
227
|
task: {
|
|
@@ -244,7 +245,6 @@ export declare const RoutineSchema: z.ZodObject<{
|
|
|
244
245
|
tool: string;
|
|
245
246
|
args: Record<string, unknown>;
|
|
246
247
|
};
|
|
247
|
-
tags: string[];
|
|
248
248
|
updatedAt?: string | undefined;
|
|
249
249
|
createdAt?: string | undefined;
|
|
250
250
|
}, {
|
|
@@ -286,10 +286,10 @@ export declare const RoutineSchema: z.ZodObject<{
|
|
|
286
286
|
config?: Record<string, unknown> | undefined;
|
|
287
287
|
on?: "always" | "failure" | "success" | undefined;
|
|
288
288
|
}[] | undefined;
|
|
289
|
+
tags?: string[] | undefined;
|
|
289
290
|
description?: string | undefined;
|
|
290
291
|
targets?: string[] | undefined;
|
|
291
292
|
perTarget?: boolean | undefined;
|
|
292
|
-
tags?: string[] | undefined;
|
|
293
293
|
createdAt?: string | undefined;
|
|
294
294
|
}>;
|
|
295
295
|
export type Routine = z.infer<typeof RoutineSchema>;
|
|
@@ -303,13 +303,13 @@ export declare const RunEventSchema: z.ZodDiscriminatedUnion<"kind", [z.ZodObjec
|
|
|
303
303
|
}, "strip", z.ZodTypeAny, {
|
|
304
304
|
at: string;
|
|
305
305
|
kind: "start";
|
|
306
|
-
routineId: string;
|
|
307
306
|
target: string | null;
|
|
307
|
+
routineId: string;
|
|
308
308
|
}, {
|
|
309
309
|
at: string;
|
|
310
310
|
kind: "start";
|
|
311
|
-
routineId: string;
|
|
312
311
|
target: string | null;
|
|
312
|
+
routineId: string;
|
|
313
313
|
}>, z.ZodObject<{
|
|
314
314
|
kind: z.ZodLiteral<"stdout">;
|
|
315
315
|
chunk: z.ZodString;
|
|
@@ -370,14 +370,14 @@ export declare const RunEventSchema: z.ZodDiscriminatedUnion<"kind", [z.ZodObjec
|
|
|
370
370
|
error: z.ZodOptional<z.ZodString>;
|
|
371
371
|
}, "strip", z.ZodTypeAny, {
|
|
372
372
|
at: string;
|
|
373
|
-
status: "
|
|
373
|
+
status: "aborted" | "timeout" | "ok" | "queued" | "running" | "failed";
|
|
374
374
|
kind: "end";
|
|
375
375
|
exitCode: number;
|
|
376
376
|
durationMs: number;
|
|
377
377
|
error?: string | undefined;
|
|
378
378
|
}, {
|
|
379
379
|
at: string;
|
|
380
|
-
status: "
|
|
380
|
+
status: "aborted" | "timeout" | "ok" | "queued" | "running" | "failed";
|
|
381
381
|
kind: "end";
|
|
382
382
|
exitCode: number;
|
|
383
383
|
durationMs: number;
|
|
@@ -397,16 +397,16 @@ export declare const SignalSchema: z.ZodObject<{
|
|
|
397
397
|
collectedAt: z.ZodString;
|
|
398
398
|
ttlMs: z.ZodNumber;
|
|
399
399
|
}, "strip", z.ZodTypeAny, {
|
|
400
|
-
detail: string;
|
|
401
|
-
kind: "git-clean" | "git-ahead" | "git-behind" | "open-prs" | "pr-age-max" | "deps-outdated" | "deps-vulns" | "build-ok" | "tests-ok" | "env-schema-ok" | "container-up" | "ci-status" | "cache-age";
|
|
402
400
|
value: string | number | boolean | null;
|
|
401
|
+
kind: "git-clean" | "git-ahead" | "git-behind" | "open-prs" | "pr-age-max" | "deps-outdated" | "deps-vulns" | "build-ok" | "tests-ok" | "env-schema-ok" | "container-up" | "ci-status" | "cache-age";
|
|
402
|
+
detail: string;
|
|
403
403
|
repo: string;
|
|
404
404
|
state: "warn" | "error" | "unknown" | "ok";
|
|
405
405
|
collectedAt: string;
|
|
406
406
|
ttlMs: number;
|
|
407
407
|
}, {
|
|
408
|
-
kind: "git-clean" | "git-ahead" | "git-behind" | "open-prs" | "pr-age-max" | "deps-outdated" | "deps-vulns" | "build-ok" | "tests-ok" | "env-schema-ok" | "container-up" | "ci-status" | "cache-age";
|
|
409
408
|
value: string | number | boolean | null;
|
|
409
|
+
kind: "git-clean" | "git-ahead" | "git-behind" | "open-prs" | "pr-age-max" | "deps-outdated" | "deps-vulns" | "build-ok" | "tests-ok" | "env-schema-ok" | "container-up" | "ci-status" | "cache-age";
|
|
410
410
|
repo: string;
|
|
411
411
|
state: "warn" | "error" | "unknown" | "ok";
|
|
412
412
|
collectedAt: string;
|
|
@@ -1,6 +1,17 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
2
|
const ROUTINE_ID_REGEX = /^[a-z][a-z0-9-]{0,62}$/;
|
|
3
3
|
const NO_SHELL_META = /^[^`$;&|><\n\r\\"]*$/;
|
|
4
|
+
// Printable ASCII only — no newlines, no control chars. Routine names and
|
|
5
|
+
// descriptions are interpolated into systemd unit files; a newline would let
|
|
6
|
+
// a hostile name inject directives like [Service]/User=root/ExecStart=…
|
|
7
|
+
// that the systemd loader then runs as root after daemon-reload. Keep this
|
|
8
|
+
// strict — widen explicitly if a future feature truly needs more.
|
|
9
|
+
const ROUTINE_TEXT_REGEX = /^[\x20-\x7E]+$/;
|
|
10
|
+
// systemd's documented OnCalendar grammar: weekdays, dates, times — all
|
|
11
|
+
// printable ASCII without quotes, semicolons, control chars or newlines.
|
|
12
|
+
// Same injection concern as above. Widen if "~" (last weekday) etc. is
|
|
13
|
+
// actually needed.
|
|
14
|
+
const ON_CALENDAR_REGEX = /^[A-Za-z0-9*\-/:., \t]+$/;
|
|
4
15
|
const DEFAULT_WALLCLOCK_MS = 15 * 60 * 1000;
|
|
5
16
|
const DEFAULT_TOKEN_CAP = 100_000;
|
|
6
17
|
const DEFAULT_MAX_USD = 5;
|
|
@@ -38,15 +49,15 @@ export const RoutineScheduleSchema = z.union([
|
|
|
38
49
|
z.object({ kind: z.literal('manual') }),
|
|
39
50
|
z.object({
|
|
40
51
|
kind: z.literal('calendar'),
|
|
41
|
-
onCalendar: z.string().min(1).max(200),
|
|
52
|
+
onCalendar: z.string().min(1).max(200).regex(ON_CALENDAR_REGEX, 'systemd OnCalendar tokens only (letters, digits, *-/:., space, tab)'),
|
|
42
53
|
randomizedDelaySec: z.number().int().nonnegative().max(3600).default(0),
|
|
43
54
|
persistent: z.boolean().default(true),
|
|
44
55
|
}),
|
|
45
56
|
]);
|
|
46
57
|
export const RoutineSchema = z.object({
|
|
47
58
|
id: z.string().regex(ROUTINE_ID_REGEX, 'lowercase alphanumeric and dashes only'),
|
|
48
|
-
name: z.string().min(1).max(100),
|
|
49
|
-
description: z.string().max(2000).default(''),
|
|
59
|
+
name: z.string().min(1).max(100).regex(ROUTINE_TEXT_REGEX, 'printable ASCII only, no newlines or control chars'),
|
|
60
|
+
description: z.string().max(2000).regex(/^[\x20-\x7E]*$/, 'printable ASCII only, no newlines or control chars').default(''),
|
|
50
61
|
schedule: RoutineScheduleSchema,
|
|
51
62
|
enabled: z.boolean().default(true),
|
|
52
63
|
targets: z.array(z.string().min(1)).default([]),
|