akm-cli 0.9.17-alpha.6 → 0.9.17-alpha.7
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/CHANGELOG.md +146 -0
- package/STABILITY.md +2 -2
- package/dist/akm +7 -7
- package/dist/assets/prompts/retrieval-relevance-judge.md +6 -0
- package/dist/commands/improve/consolidate.js +11 -0
- package/dist/commands/improve/improve-cli.js +27 -7
- package/dist/commands/improve/ledger.js +7 -3
- package/dist/commands/improve/preparation.js +40 -10
- package/dist/commands/improve/reflect.js +46 -22
- package/dist/commands/improve/retrieval-gate.js +127 -0
- package/dist/commands/improve/retrieval-scope.js +77 -0
- package/dist/commands/read/curate.js +1 -17
- package/dist/commands/tasks/tasks-cli.js +10 -12
- package/dist/commands/tasks/tasks.js +57 -56
- package/dist/commands/tasks/validate.js +27 -46
- package/dist/core/adapter/adapters/akm-task-adapter.js +29 -8
- package/dist/core/improve-result.js +4 -1
- package/dist/core/non-task-input.js +20 -0
- package/dist/core/paths.js +0 -4
- package/dist/indexer/indexer.js +1 -3
- package/dist/indexer/usage/usage-events.js +34 -0
- package/dist/scripts/akm-migrate-node.js +5822 -5833
- package/dist/scripts/akm-migrate.js +6301 -6312
- package/dist/storage/repositories/proposals-repository.js +4 -0
- package/dist/tasks/backends/cron.js +80 -43
- package/dist/tasks/backends/launchd.js +28 -15
- package/dist/tasks/backends/schtasks.js +25 -10
- package/dist/tasks/run/load-task.js +1 -1
- package/dist/tasks/scheduler-binding.js +4 -2
- package/dist/tasks/scheduler-invocation.js +127 -235
- package/dist/tasks/scheduler-sync.js +13 -8
- package/dist/tasks/source/parse-task-source.js +22 -126
- package/dist/tasks/source/task-to-v3.js +1 -55
- package/dist/tasks/source/task-to-v4.js +1 -13
- package/docs/migration/v0.9.1-to-v0.9.2.md +7 -3
- package/docs/reference/cli.md +4 -3
- package/docs/reference/tasks.md +58 -40
- package/package.json +1 -1
|
@@ -1,13 +1,39 @@
|
|
|
1
1
|
// This Source Code Form is subject to the terms of the Mozilla Public
|
|
2
2
|
// License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
3
3
|
// file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
|
4
|
-
|
|
4
|
+
/**
|
|
5
|
+
* How a native scheduler row invokes akm.
|
|
6
|
+
*
|
|
7
|
+
* A row is `<launcher…> task run <id> --bundle <bundle> --scheduled
|
|
8
|
+
* [--<input> <value>…]`, or `<launcher…> workflow run <qualified-ref>`, and it
|
|
9
|
+
* carries its own context: the environment the scheduled process needs,
|
|
10
|
+
* set inline by the row itself (a cron `VAR=value` prefix, a launchd
|
|
11
|
+
* `EnvironmentVariables` entry, a PowerShell `$env:` assignment).
|
|
12
|
+
*
|
|
13
|
+
* - `AKM_BUNDLE_DIR`: the working stash of the process that ran
|
|
14
|
+
* `akm task sync`, on every row. The scheduled run uses the same working
|
|
15
|
+
* stash, `--bundle <name>` finds a stash no config names, and sync
|
|
16
|
+
* attributes the row to the installation that wrote it (#846).
|
|
17
|
+
* - `AKM_CONFIG_DIR`, `AKM_DATA_DIR`, `AKM_CACHE_DIR` and `AKM_STATE_DIR`
|
|
18
|
+
* only when the process that ran `akm task sync` set them explicitly. A
|
|
19
|
+
* default resolves at fire time exactly as it does for an interactive
|
|
20
|
+
* command.
|
|
21
|
+
*
|
|
22
|
+
* That is exactly what the `--scheduler-context` descriptor held.
|
|
23
|
+
*
|
|
24
|
+
* `PATH` is the scheduler's own: the crontab's `# akm:env` block, the plist's
|
|
25
|
+
* `EnvironmentVariables`.
|
|
26
|
+
*
|
|
27
|
+
* Rows written by 0.9.0 through 0.9.17-alpha.6 name a
|
|
28
|
+
* `--scheduler-context <descriptor>` file instead. The CLI still accepts that
|
|
29
|
+
* argument and applies the file's environment, so such a row keeps firing
|
|
30
|
+
* until the next `akm task sync` rewrites it.
|
|
31
|
+
*/
|
|
5
32
|
import fs from "node:fs";
|
|
6
33
|
import path from "node:path";
|
|
7
34
|
import { bundleRefToString, parseBundleRef } from "../core/asset/asset-ref.js";
|
|
8
35
|
import { resolveStashDir } from "../core/common.js";
|
|
9
36
|
import { ConfigError } from "../core/errors.js";
|
|
10
|
-
import { getTaskContextDir } from "../core/paths.js";
|
|
11
37
|
import { INPUT_NAME_PATTERN } from "../execution/input-contract.js";
|
|
12
38
|
import { normaliseTaskConceptId } from "./task-id.js";
|
|
13
39
|
export const SCHEDULED_TASK_CONTEXT_KEYS = [
|
|
@@ -18,20 +44,18 @@ export const SCHEDULED_TASK_CONTEXT_KEYS = [
|
|
|
18
44
|
"AKM_STATE_DIR",
|
|
19
45
|
];
|
|
20
46
|
/**
|
|
21
|
-
* Directories a
|
|
22
|
-
*
|
|
23
|
-
* defaults: a
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
* see. At fire time an absent key resolves exactly as it does for an
|
|
28
|
-
* interactive command on the same host.
|
|
47
|
+
* Directories a row carries ONLY when the process that ran `task sync` had
|
|
48
|
+
* them set explicitly in its environment. They are never resolved from
|
|
49
|
+
* defaults: a sync run from inside an app whose environment pointed `$STATE`
|
|
50
|
+
* somewhere private (an OpenCode desktop session, 2026-08 → 2026-09) froze
|
|
51
|
+
* that directory into eight cron rows, and the scheduled improve runs then
|
|
52
|
+
* held their locks in a `locks/` directory no interactive command could see.
|
|
29
53
|
*/
|
|
30
54
|
const EXPLICIT_CONTEXT_KEYS = ["AKM_CONFIG_DIR", "AKM_DATA_DIR", "AKM_CACHE_DIR", "AKM_STATE_DIR"];
|
|
31
55
|
/**
|
|
32
|
-
* The AKM directory context
|
|
33
|
-
*
|
|
34
|
-
*
|
|
56
|
+
* The AKM directory context of the current process. Child environments that
|
|
57
|
+
* are built from an allowlist must forward this closed set explicitly so
|
|
58
|
+
* nested AKM commands stay in the same installation.
|
|
35
59
|
*/
|
|
36
60
|
export function scheduledTaskContextEnv(env = process.env) {
|
|
37
61
|
const out = {};
|
|
@@ -42,141 +66,112 @@ export function scheduledTaskContextEnv(env = process.env) {
|
|
|
42
66
|
}
|
|
43
67
|
return out;
|
|
44
68
|
}
|
|
45
|
-
export const SCHEDULER_CONTEXT_ARG = "--scheduler-context";
|
|
46
69
|
/**
|
|
47
|
-
* The
|
|
48
|
-
*
|
|
49
|
-
* explicitly.
|
|
50
|
-
*
|
|
70
|
+
* The environment every row a sync writes sets inline: `AKM_BUNDLE_DIR`,
|
|
71
|
+
* the syncing process's working stash, plus whichever `AKM_*_DIR` overrides
|
|
72
|
+
* it set explicitly. With no working stash to resolve (no default bundle, no
|
|
73
|
+
* `AKM_BUNDLE_DIR`), rows name their bundle by `--bundle` alone.
|
|
51
74
|
*/
|
|
52
|
-
export function
|
|
53
|
-
const
|
|
75
|
+
export function scheduledRowEnvironment(env = process.env) {
|
|
76
|
+
const out = {};
|
|
77
|
+
try {
|
|
78
|
+
out.AKM_BUNDLE_DIR = path.resolve(resolveStashDir(env));
|
|
79
|
+
}
|
|
80
|
+
catch {
|
|
81
|
+
// No working stash: there is no primary bundle to attribute rows to either.
|
|
82
|
+
}
|
|
54
83
|
for (const key of EXPLICIT_CONTEXT_KEYS) {
|
|
55
84
|
const value = env[key]?.trim();
|
|
56
85
|
if (value)
|
|
57
|
-
|
|
86
|
+
out[key] = path.resolve(value);
|
|
58
87
|
}
|
|
59
|
-
return
|
|
60
|
-
}
|
|
61
|
-
/** Build an installed argv from one already-validated public scheduler tail. */
|
|
62
|
-
export function buildScheduledBindingInvocation(akmArgv, contextPath, invocation) {
|
|
63
|
-
const parsed = parsePublicSchedulerInvocation(invocation);
|
|
64
|
-
if (!parsed)
|
|
65
|
-
throw invalidSchedulerInvocation();
|
|
66
|
-
return {
|
|
67
|
-
argv: [...akmArgv, SCHEDULER_CONTEXT_ARG, assertAbsolutePath(contextPath), ...parsed.invocation],
|
|
68
|
-
};
|
|
69
|
-
}
|
|
70
|
-
export function schedulerContextDescriptor(context = resolveScheduledTaskContext()) {
|
|
71
|
-
return { version: 1, environment: canonicalContext(context) };
|
|
72
|
-
}
|
|
73
|
-
export function schedulerContextPath(descriptor) {
|
|
74
|
-
const bytes = serializeDescriptor(descriptor);
|
|
75
|
-
const digest = createHash("sha256").update(bytes).digest("hex");
|
|
76
|
-
// The descriptor lives under the data directory it names when it names
|
|
77
|
-
// one, otherwise under this process's own.
|
|
78
|
-
return path.join(getTaskContextDir({ ...process.env, ...descriptor.environment }), `${digest}.json`);
|
|
88
|
+
return out;
|
|
79
89
|
}
|
|
80
|
-
/**
|
|
81
|
-
export function
|
|
82
|
-
const
|
|
83
|
-
const
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
}
|
|
88
|
-
if (lstatIfExists(file)) {
|
|
89
|
-
if (serializeDescriptor(validateSchedulerContextDescriptor(file)) !== bytes)
|
|
90
|
-
throw invalidSchedulerContext();
|
|
91
|
-
return file;
|
|
92
|
-
}
|
|
93
|
-
const temp = path.join(path.dirname(file), `.${path.basename(file)}.${process.pid}.${Date.now()}.tmp`);
|
|
94
|
-
try {
|
|
95
|
-
fs.writeFileSync(temp, bytes, { encoding: "utf8", mode: 0o600, flag: "wx" });
|
|
96
|
-
try {
|
|
97
|
-
fs.renameSync(temp, file);
|
|
98
|
-
}
|
|
99
|
-
catch (error) {
|
|
100
|
-
if (!lstatIfExists(file) || serializeDescriptor(validateSchedulerContextDescriptor(file)) !== bytes)
|
|
101
|
-
throw error;
|
|
102
|
-
}
|
|
103
|
-
restrictDescriptor(file);
|
|
104
|
-
validateSchedulerContextDescriptor(file);
|
|
105
|
-
}
|
|
106
|
-
finally {
|
|
107
|
-
fs.rmSync(temp, { force: true });
|
|
90
|
+
/** A row environment's entries, in {@link SCHEDULED_TASK_CONTEXT_KEYS} order. */
|
|
91
|
+
export function scheduledRowEnvironmentEntries(environment) {
|
|
92
|
+
const entries = [];
|
|
93
|
+
for (const key of SCHEDULED_TASK_CONTEXT_KEYS) {
|
|
94
|
+
const value = environment?.[key];
|
|
95
|
+
if (value)
|
|
96
|
+
entries.push([key, value]);
|
|
108
97
|
}
|
|
109
|
-
return
|
|
98
|
+
return entries;
|
|
110
99
|
}
|
|
111
|
-
|
|
112
|
-
|
|
100
|
+
/** Keep the {@link SCHEDULED_TASK_CONTEXT_KEYS} a parsed row set; `undefined` when there are none. */
|
|
101
|
+
export function scheduledRowEnvironmentFrom(variables) {
|
|
102
|
+
const out = {};
|
|
113
103
|
for (const key of SCHEDULED_TASK_CONTEXT_KEYS) {
|
|
114
|
-
const value =
|
|
115
|
-
if (value
|
|
116
|
-
|
|
104
|
+
const value = variables[key];
|
|
105
|
+
if (value)
|
|
106
|
+
out[key] = value;
|
|
117
107
|
}
|
|
118
|
-
|
|
119
|
-
env.PATH = descriptor.environment.PATH;
|
|
108
|
+
return Object.keys(out).length > 0 ? out : undefined;
|
|
120
109
|
}
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
110
|
+
const SCHEDULER_CONTEXT_ARG = "--scheduler-context";
|
|
111
|
+
/** The installed argv: the launcher, then one already-validated public scheduler tail. */
|
|
112
|
+
export function buildScheduledInvocation(akmArgv, invocation) {
|
|
113
|
+
const parsed = parsePublicSchedulerInvocation(invocation);
|
|
114
|
+
if (!parsed)
|
|
115
|
+
throw invalidSchedulerInvocation();
|
|
116
|
+
return [...akmArgv, ...parsed.invocation];
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* Parse an installed row's argv (after any inline environment): the current
|
|
120
|
+
* shape, the `--scheduler-context <descriptor>` shape 0.9.0 – 0.9.17-alpha.6
|
|
121
|
+
* wrote, or the bare shape before that. A row that carries
|
|
122
|
+
* `--scheduler-context` but does not parse around it is never reread as
|
|
123
|
+
* another shape.
|
|
124
|
+
*/
|
|
125
|
+
export function parseScheduledInvocationArgv(argv) {
|
|
126
|
+
const contextIndex = argv.indexOf(SCHEDULER_CONTEXT_ARG);
|
|
127
|
+
if (contextIndex !== -1) {
|
|
128
|
+
if (contextIndex < 1 || argv.indexOf(SCHEDULER_CONTEXT_ARG, contextIndex + 1) !== -1)
|
|
129
|
+
return undefined;
|
|
130
|
+
const contextPath = argv[contextIndex + 1];
|
|
131
|
+
if (!contextPath)
|
|
132
|
+
return undefined;
|
|
133
|
+
const publicInvocation = parsePublicSchedulerInvocation(argv.slice(contextIndex + 2));
|
|
134
|
+
if (!publicInvocation)
|
|
135
|
+
return undefined;
|
|
136
|
+
return { binding: argv.slice(0, contextIndex), contextPath, ...publicInvocation };
|
|
129
137
|
}
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
let descriptorBytes;
|
|
135
|
-
let fd;
|
|
136
|
-
try {
|
|
137
|
-
const noFollow = process.platform === "win32" ? 0 : fs.constants.O_NOFOLLOW;
|
|
138
|
-
fd = fs.openSync(absolute, fs.constants.O_RDONLY | noFollow);
|
|
139
|
-
const stat = fs.fstatSync(fd);
|
|
140
|
-
if (!stat.isFile())
|
|
141
|
-
throw schedulerContextFileError(absolute, "path is not a regular file");
|
|
142
|
-
if (process.platform !== "win32") {
|
|
143
|
-
if (typeof process.getuid !== "function") {
|
|
144
|
-
throw schedulerContextFileError(absolute, "current uid is unavailable for ownership verification");
|
|
145
|
-
}
|
|
146
|
-
const uid = process.getuid();
|
|
147
|
-
if (stat.uid !== uid)
|
|
148
|
-
throw schedulerContextFileError(absolute, `file owner ${stat.uid} does not match uid ${uid}`);
|
|
149
|
-
if ((stat.mode & 0o077) !== 0) {
|
|
150
|
-
throw schedulerContextFileError(absolute, "group or other permissions must be disabled");
|
|
151
|
-
}
|
|
138
|
+
for (let index = 1; index < argv.length - 1; index += 1) {
|
|
139
|
+
if ((argv[index] === "task" || argv[index] === "workflow") && argv[index + 1] === "run") {
|
|
140
|
+
const publicInvocation = parsePublicSchedulerInvocation(argv.slice(index));
|
|
141
|
+
return publicInvocation ? { binding: argv.slice(0, index), ...publicInvocation } : undefined;
|
|
152
142
|
}
|
|
153
|
-
descriptorBytes = fs.readFileSync(fd);
|
|
154
|
-
}
|
|
155
|
-
catch (error) {
|
|
156
|
-
if (error instanceof ConfigError)
|
|
157
|
-
throw error;
|
|
158
|
-
throw schedulerContextFileError(absolute, error instanceof Error ? error.message : String(error));
|
|
159
|
-
}
|
|
160
|
-
finally {
|
|
161
|
-
if (fd !== undefined)
|
|
162
|
-
fs.closeSync(fd);
|
|
163
|
-
}
|
|
164
|
-
const filename = path.basename(absolute);
|
|
165
|
-
const match = /^([a-f0-9]{64})\.json$/.exec(filename);
|
|
166
|
-
const digest = createHash("sha256").update(descriptorBytes).digest("hex");
|
|
167
|
-
if (!match || match[1] !== digest) {
|
|
168
|
-
throw schedulerContextFileError(absolute, "content SHA-256 does not match the descriptor filename");
|
|
169
143
|
}
|
|
144
|
+
return undefined;
|
|
145
|
+
}
|
|
146
|
+
/**
|
|
147
|
+
* The environment a `--scheduler-context` descriptor names: its `AKM_*_DIR`
|
|
148
|
+
* keys and, in one written before 0.9.17, `PATH`. It is read as plain JSON —
|
|
149
|
+
* the file is this user's own, next to the data it names.
|
|
150
|
+
*/
|
|
151
|
+
export function readLegacySchedulerContext(file) {
|
|
170
152
|
let parsed;
|
|
171
153
|
try {
|
|
172
|
-
parsed = JSON.parse(
|
|
154
|
+
parsed = JSON.parse(fs.readFileSync(file, "utf8"));
|
|
173
155
|
}
|
|
174
156
|
catch (error) {
|
|
175
|
-
throw
|
|
157
|
+
throw new ConfigError(`Cannot read scheduler context "${file}": ${error instanceof Error ? error.message : String(error)}. ` +
|
|
158
|
+
"Run `akm task sync` to rewrite this scheduled row.", "INVALID_CONFIG_FILE");
|
|
159
|
+
}
|
|
160
|
+
const environment = parsed !== null && typeof parsed === "object" ? parsed.environment : undefined;
|
|
161
|
+
const out = {};
|
|
162
|
+
if (environment === null || typeof environment !== "object")
|
|
163
|
+
return out;
|
|
164
|
+
for (const key of [...SCHEDULED_TASK_CONTEXT_KEYS, "PATH"]) {
|
|
165
|
+
const value = environment[key];
|
|
166
|
+
if (typeof value === "string" && value.length > 0)
|
|
167
|
+
out[key] = value;
|
|
176
168
|
}
|
|
177
|
-
return
|
|
169
|
+
return out;
|
|
178
170
|
}
|
|
179
|
-
/**
|
|
171
|
+
/**
|
|
172
|
+
* Apply and remove a row's `--scheduler-context <descriptor>` before citty
|
|
173
|
+
* parses argv. Only a row written by 0.9.0 – 0.9.17-alpha.6 passes it.
|
|
174
|
+
*/
|
|
180
175
|
export function consumeSchedulerContextArg(argv, env = process.env) {
|
|
181
176
|
const separator = argv.indexOf("--");
|
|
182
177
|
const index = argv.slice(0, separator === -1 ? argv.length : separator).indexOf(SCHEDULER_CONTEXT_ARG);
|
|
@@ -184,37 +179,16 @@ export function consumeSchedulerContextArg(argv, env = process.env) {
|
|
|
184
179
|
return argv;
|
|
185
180
|
const file = argv[index + 1];
|
|
186
181
|
if (!file) {
|
|
187
|
-
throw new ConfigError(`${SCHEDULER_CONTEXT_ARG} requires
|
|
182
|
+
throw new ConfigError(`${SCHEDULER_CONTEXT_ARG} requires a descriptor path.`, "INVALID_CONFIG_FILE");
|
|
188
183
|
}
|
|
189
|
-
|
|
184
|
+
Object.assign(env, readLegacySchedulerContext(file));
|
|
190
185
|
return [...argv.slice(0, index), ...argv.slice(index + 2)];
|
|
191
186
|
}
|
|
192
|
-
/** Parse either the public scheduled task or qualified workflow invocation. */
|
|
193
|
-
export function parseScheduledBindingArgv(argv) {
|
|
194
|
-
const contextIndex = argv.indexOf(SCHEDULER_CONTEXT_ARG);
|
|
195
|
-
if (contextIndex < 1 || argv.indexOf(SCHEDULER_CONTEXT_ARG, contextIndex + 1) !== -1)
|
|
196
|
-
return undefined;
|
|
197
|
-
const contextPath = argv[contextIndex + 1];
|
|
198
|
-
if (!contextPath)
|
|
199
|
-
return undefined;
|
|
200
|
-
const publicInvocation = parsePublicSchedulerInvocation(argv.slice(contextIndex + 2));
|
|
201
|
-
if (!publicInvocation)
|
|
202
|
-
return undefined;
|
|
203
|
-
return {
|
|
204
|
-
binding: [...argv.slice(0, contextIndex)],
|
|
205
|
-
contextPath: assertAbsolutePath(contextPath),
|
|
206
|
-
invocation: publicInvocation.invocation,
|
|
207
|
-
...(publicInvocation.target !== undefined ? { target: publicInvocation.target } : {}),
|
|
208
|
-
};
|
|
209
|
-
}
|
|
210
187
|
/**
|
|
211
|
-
* Parse just the public `task run …` / `workflow run …` tail,
|
|
212
|
-
*
|
|
213
|
-
* pre-`--scheduler-context` invocation still sitting inside akm's own
|
|
214
|
-
* ownership-marked block (see `extractCronInvocation` in
|
|
215
|
-
* `src/tasks/backends/cron.ts`) without re-implementing this grammar.
|
|
188
|
+
* Parse just the public `task run …` / `workflow run …` tail, the part of a
|
|
189
|
+
* row that names what it runs.
|
|
216
190
|
*/
|
|
217
|
-
|
|
191
|
+
function parsePublicSchedulerInvocation(invocation) {
|
|
218
192
|
if (invocation[0] === "task" && invocation[1] === "run" && invocation[2]) {
|
|
219
193
|
try {
|
|
220
194
|
if (normaliseTaskConceptId(invocation[2]) !== invocation[2])
|
|
@@ -308,88 +282,6 @@ function isValidSchedulerInputFlagTail(tail) {
|
|
|
308
282
|
}
|
|
309
283
|
return true;
|
|
310
284
|
}
|
|
311
|
-
function canonicalContext(input) {
|
|
312
|
-
if (Object.keys(input).some((key) => !SCHEDULED_TASK_CONTEXT_KEYS.includes(key))) {
|
|
313
|
-
throw invalidSchedulerContext();
|
|
314
|
-
}
|
|
315
|
-
const context = {};
|
|
316
|
-
for (const key of SCHEDULED_TASK_CONTEXT_KEYS) {
|
|
317
|
-
const value = input[key];
|
|
318
|
-
if (value === undefined) {
|
|
319
|
-
if (key === "AKM_BUNDLE_DIR")
|
|
320
|
-
throw invalidSchedulerContext();
|
|
321
|
-
continue;
|
|
322
|
-
}
|
|
323
|
-
if (typeof value !== "string" ||
|
|
324
|
-
value.trim().length === 0 ||
|
|
325
|
-
containsControlCharacter(value) ||
|
|
326
|
-
(!path.posix.isAbsolute(value) && !path.win32.isAbsolute(value))) {
|
|
327
|
-
throw invalidSchedulerContext();
|
|
328
|
-
}
|
|
329
|
-
context[key] = value;
|
|
330
|
-
}
|
|
331
|
-
return context;
|
|
332
|
-
}
|
|
333
|
-
function canonicalDescriptor(input) {
|
|
334
|
-
if (typeof input !== "object" || input === null || Array.isArray(input))
|
|
335
|
-
throw invalidSchedulerContext();
|
|
336
|
-
const record = input;
|
|
337
|
-
if (Object.keys(record).length !== 2 || record.version !== 1)
|
|
338
|
-
throw invalidSchedulerContext();
|
|
339
|
-
const rawEnvironment = record.environment;
|
|
340
|
-
if (typeof rawEnvironment !== "object" || rawEnvironment === null || Array.isArray(rawEnvironment)) {
|
|
341
|
-
throw invalidSchedulerContext();
|
|
342
|
-
}
|
|
343
|
-
const { PATH, ...rawContext } = rawEnvironment;
|
|
344
|
-
const context = canonicalContext(rawContext);
|
|
345
|
-
return {
|
|
346
|
-
version: 1,
|
|
347
|
-
environment: PATH === undefined ? context : { ...context, PATH: validatePathValue(PATH) },
|
|
348
|
-
};
|
|
349
|
-
}
|
|
350
|
-
function serializeDescriptor(descriptor) {
|
|
351
|
-
return `${JSON.stringify(canonicalDescriptor(descriptor))}\n`;
|
|
352
|
-
}
|
|
353
|
-
function validatePathValue(value) {
|
|
354
|
-
if (typeof value !== "string" || containsControlCharacter(value))
|
|
355
|
-
throw invalidSchedulerContext();
|
|
356
|
-
return value;
|
|
357
|
-
}
|
|
358
|
-
function assertAbsolutePath(value) {
|
|
359
|
-
if (containsControlCharacter(value) || (!path.posix.isAbsolute(value) && !path.win32.isAbsolute(value))) {
|
|
360
|
-
throw invalidSchedulerContext();
|
|
361
|
-
}
|
|
362
|
-
return value;
|
|
363
|
-
}
|
|
364
|
-
function restrictDescriptor(file) {
|
|
365
|
-
if (process.platform !== "win32")
|
|
366
|
-
fs.chmodSync(file, 0o600);
|
|
367
|
-
}
|
|
368
|
-
function lstatIfExists(file) {
|
|
369
|
-
try {
|
|
370
|
-
return fs.lstatSync(file);
|
|
371
|
-
}
|
|
372
|
-
catch (error) {
|
|
373
|
-
if (error.code === "ENOENT")
|
|
374
|
-
return undefined;
|
|
375
|
-
throw error;
|
|
376
|
-
}
|
|
377
|
-
}
|
|
378
|
-
function schedulerContextFileError(file, reason) {
|
|
379
|
-
return new ConfigError(`Invalid scheduler context descriptor "${file}": ${reason}.`, "INVALID_CONFIG_FILE");
|
|
380
|
-
}
|
|
381
|
-
function containsControlCharacter(value) {
|
|
382
|
-
for (const char of value) {
|
|
383
|
-
const code = char.codePointAt(0) ?? 0;
|
|
384
|
-
if (code < 0x20 || (code >= 0x7f && code <= 0x9f))
|
|
385
|
-
return true;
|
|
386
|
-
}
|
|
387
|
-
return false;
|
|
388
|
-
}
|
|
389
|
-
function invalidSchedulerContext() {
|
|
390
|
-
return new ConfigError("Invalid scheduler context; expected AKM_BUNDLE_DIR as an absolute path, " +
|
|
391
|
-
`optionally with ${EXPLICIT_CONTEXT_KEYS.join(", ")} as absolute paths.`, "INVALID_CONFIG_FILE");
|
|
392
|
-
}
|
|
393
285
|
function invalidSchedulerInvocation() {
|
|
394
286
|
return new ConfigError("Invalid scheduler invocation; expected public " +
|
|
395
287
|
"`task run <id> [--bundle <bundle>] --scheduled [--<input> <value>…]` or `workflow run <qualified-ref>` argv.", "INVALID_CONFIG_FILE");
|
|
@@ -263,9 +263,10 @@ export function scheduledInvocationBundle(invocation) {
|
|
|
263
263
|
/**
|
|
264
264
|
* The scope that owns an installed row, or none. A row written before
|
|
265
265
|
* `--bundle` existed names no bundle and belongs to the primary scope
|
|
266
|
-
* unless
|
|
267
|
-
* to the scope of that name — for the primary scope only
|
|
268
|
-
*
|
|
266
|
+
* unless the bundle path it carries says otherwise; a row that names a
|
|
267
|
+
* bundle belongs to the scope of that name — for the primary scope only
|
|
268
|
+
* when its `AKM_BUNDLE_DIR` (inline, or in an older row's descriptor) also
|
|
269
|
+
* proves the path.
|
|
269
270
|
*/
|
|
270
271
|
export function installedRowScope(row, scopes) {
|
|
271
272
|
const bundle = row.target ?? scheduledInvocationBundle(row.invocation);
|
|
@@ -393,12 +394,13 @@ export function planSchedulerSync(input) {
|
|
|
393
394
|
failures: Object.freeze(failures),
|
|
394
395
|
});
|
|
395
396
|
}
|
|
396
|
-
/** An installed row keeps its own launcher unless `rebind`;
|
|
397
|
+
/** An installed row keeps its own launcher unless `rebind`; its environment always follows the current policy. */
|
|
397
398
|
function installOptionsFor(input, current) {
|
|
398
399
|
if (current && !input.rebind) {
|
|
400
|
+
const environment = input.installOptions?.environment ?? current.environment;
|
|
399
401
|
return Object.freeze({
|
|
400
402
|
binding: Object.freeze([...current.binding]),
|
|
401
|
-
|
|
403
|
+
...(environment !== undefined ? { environment } : {}),
|
|
402
404
|
});
|
|
403
405
|
}
|
|
404
406
|
return input.installOptions ? Object.freeze({ ...input.installOptions }) : undefined;
|
|
@@ -417,9 +419,12 @@ function installedRowRef(row, scope) {
|
|
|
417
419
|
/** Where an installed row was scheduled from, for messages. */
|
|
418
420
|
export function installedRowOwner(row) {
|
|
419
421
|
const bundle = row.target ?? scheduledInvocationBundle(row.invocation);
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
422
|
+
const installation = row.ownerBundlePath
|
|
423
|
+
? `the installation whose working stash is ${JSON.stringify(row.ownerBundlePath)}`
|
|
424
|
+
: undefined;
|
|
425
|
+
if (bundle)
|
|
426
|
+
return installation ? `bundle ${JSON.stringify(bundle)} of ${installation}` : `bundle ${JSON.stringify(bundle)}`;
|
|
427
|
+
return installation ?? "an installation this sync cannot attribute";
|
|
423
428
|
}
|
|
424
429
|
function foreignRowMessage(nativeId, row) {
|
|
425
430
|
return `Scheduler id ${JSON.stringify(nativeId)} is already scheduled from ${installedRowOwner(row)}; desired source ids must not collide across bundles. Leaving it out of this sync.`;
|