@nettee/beacon 0.1.3 → 0.1.4
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 +31 -5
- package/dist/cli-app.js +56 -23
- package/dist/cli.js +2 -0
- package/dist/schedule-trigger.js +81 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -31,10 +31,11 @@ Upgrade to the current stable release with the same `latest` command. Run
|
|
|
31
31
|
service. Beacon does not manage launchd installation, restart, or rollback in
|
|
32
32
|
this release.
|
|
33
33
|
|
|
34
|
-
|
|
34
|
+
Operator commands find the standard Beacon home automatically; a service
|
|
35
|
+
deployment names its configuration explicitly:
|
|
35
36
|
|
|
36
37
|
```sh
|
|
37
|
-
beacon doctor
|
|
38
|
+
beacon doctor
|
|
38
39
|
beacon serve --config /Users/USERNAME/.beacon/config.yaml
|
|
39
40
|
```
|
|
40
41
|
|
|
@@ -83,18 +84,37 @@ Each Schedule uses a five-field cron expression and an IANA timezone. Its `deliv
|
|
|
83
84
|
|
|
84
85
|
```text
|
|
85
86
|
beacon serve --config <absolute-path>
|
|
86
|
-
beacon doctor --config <absolute-path>
|
|
87
|
-
beacon trigger --config <absolute-path> --profile <id> --input -
|
|
87
|
+
beacon doctor [--config <absolute-path>]
|
|
88
|
+
beacon trigger [--config <absolute-path>] --profile <id> --input -
|
|
89
|
+
beacon schedule trigger [--config <absolute-path>] --profile <id> --schedule <id>
|
|
88
90
|
beacon version
|
|
89
91
|
```
|
|
90
92
|
|
|
93
|
+
Operator commands use `~/.beacon/config.yaml` when `--config` is omitted.
|
|
94
|
+
An explicit override must still be an absolute path. `serve` keeps requiring an
|
|
95
|
+
explicit absolute config path so service definitions identify their deployment
|
|
96
|
+
configuration unambiguously.
|
|
97
|
+
|
|
91
98
|
An operator can run one Profile without Feishu delivery by piping input to `trigger`; the Final Outcome is printed to stdout:
|
|
92
99
|
|
|
93
100
|
```sh
|
|
94
101
|
printf '%s\n' 'Summarize the workspace status.' | \
|
|
95
|
-
beacon trigger --
|
|
102
|
+
beacon trigger --profile example --input -
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
An operator can also immediately run a configured Schedule through the real
|
|
106
|
+
Schedule input and Feishu chat Delivery path:
|
|
107
|
+
|
|
108
|
+
```sh
|
|
109
|
+
beacon schedule trigger --profile example --schedule daily-report
|
|
96
110
|
```
|
|
97
111
|
|
|
112
|
+
This creates a distinct durable Run on every invocation and prints its
|
|
113
|
+
`run_id`. It uses the Schedule's configured `input` and `delivery.chat_id`, but
|
|
114
|
+
does not read, initialize, or advance the Schedule's cron cursor. Unknown
|
|
115
|
+
Profiles or Schedules and failed Runs or Deliveries exit non-zero; failure
|
|
116
|
+
messages include the `run_id` whenever a Run record was created.
|
|
117
|
+
|
|
98
118
|
## State and failure behavior
|
|
99
119
|
|
|
100
120
|
Per-Profile state lives below `profiles/<profile-id>/state/`. Trigger claims, normalized inputs, Run state, Final Outcomes, Delivery state, and Schedule cursors use durable JSON snapshots. Records do not expire, and Beacon has no automatic cleanup task. Manually deleting state also deletes its deduplication memory.
|
|
@@ -143,6 +163,12 @@ launchctl print gui/$(id -u)/io.nettee.beacon
|
|
|
143
163
|
|
|
144
164
|
The example uses a restrictive umask and asks launchd to restart only after abnormal exit. `SIGTERM` initiates an orderly shutdown: timers and Gateways stop, accepted intake work drains, and the local Outcome server closes.
|
|
145
165
|
|
|
166
|
+
The repository also contains Chinese operator runbooks based on the verified
|
|
167
|
+
`macmini.liuyi` deployment:
|
|
168
|
+
|
|
169
|
+
- [Deploy or update Beacon on macmini](docs/macmini-deploy-and-update.md)
|
|
170
|
+
- [Add a Beacon Profile on macmini](docs/macmini-add-profile.md)
|
|
171
|
+
|
|
146
172
|
## Development checks
|
|
147
173
|
|
|
148
174
|
Development requires pnpm 10.33.2. Install dependencies before running checks
|
package/dist/cli-app.js
CHANGED
|
@@ -1,18 +1,40 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { homedir } from "node:os";
|
|
2
|
+
import { isAbsolute, join } from "node:path";
|
|
3
|
+
export const defaultConfigPath = join(homedir(), ".beacon", "config.yaml");
|
|
2
4
|
function usage() {
|
|
3
5
|
throw new Error(`Usage:
|
|
4
6
|
beacon serve --config <absolute-path>
|
|
5
|
-
beacon doctor --config <absolute-path>
|
|
6
|
-
beacon trigger --config <absolute-path> --profile <id> --input -
|
|
7
|
+
beacon doctor [--config <absolute-path>]
|
|
8
|
+
beacon trigger [--config <absolute-path>] --profile <id> --input -
|
|
9
|
+
beacon schedule trigger [--config <absolute-path>] --profile <id> --schedule <id>
|
|
7
10
|
beacon version`);
|
|
8
11
|
}
|
|
9
|
-
function
|
|
12
|
+
function requiredConfig(args) {
|
|
10
13
|
if (args.length !== 2 || args[0] !== "--config" || !args[1])
|
|
11
14
|
usage();
|
|
12
15
|
if (!isAbsolute(args[1]))
|
|
13
16
|
throw new Error("Config path must be absolute");
|
|
14
17
|
return args[1];
|
|
15
18
|
}
|
|
19
|
+
function options(args) {
|
|
20
|
+
if (args.length % 2 !== 0)
|
|
21
|
+
usage();
|
|
22
|
+
const values = new Map();
|
|
23
|
+
for (let index = 0; index < args.length; index += 2) {
|
|
24
|
+
const key = args[index];
|
|
25
|
+
const value = args[index + 1];
|
|
26
|
+
if (!key?.startsWith("--") || !value || values.has(key))
|
|
27
|
+
usage();
|
|
28
|
+
values.set(key, value);
|
|
29
|
+
}
|
|
30
|
+
return values;
|
|
31
|
+
}
|
|
32
|
+
function optionalConfig(values) {
|
|
33
|
+
const config = values.get("--config") ?? defaultConfigPath;
|
|
34
|
+
if (!isAbsolute(config))
|
|
35
|
+
throw new Error("Config path must be absolute");
|
|
36
|
+
return config;
|
|
37
|
+
}
|
|
16
38
|
export function parseCli(args) {
|
|
17
39
|
const [command, ...rest] = args;
|
|
18
40
|
if (command === "version") {
|
|
@@ -20,30 +42,38 @@ export function parseCli(args) {
|
|
|
20
42
|
usage();
|
|
21
43
|
return { command: "version" };
|
|
22
44
|
}
|
|
23
|
-
if (command === "serve"
|
|
24
|
-
return { command, config:
|
|
45
|
+
if (command === "serve") {
|
|
46
|
+
return { command, config: requiredConfig(rest) };
|
|
25
47
|
}
|
|
26
|
-
if (command === "
|
|
27
|
-
|
|
48
|
+
if (command === "doctor") {
|
|
49
|
+
const values = options(rest);
|
|
50
|
+
if ([...values.keys()].some((key) => key !== "--config"))
|
|
28
51
|
usage();
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
if (!key?.startsWith("--") || !value || values.has(key))
|
|
34
|
-
usage();
|
|
35
|
-
values.set(key, value);
|
|
36
|
-
}
|
|
37
|
-
const config = values.get("--config");
|
|
52
|
+
return { command, config: optionalConfig(values) };
|
|
53
|
+
}
|
|
54
|
+
if (command === "trigger") {
|
|
55
|
+
const values = options(rest);
|
|
38
56
|
const profile = values.get("--profile");
|
|
39
|
-
if (!
|
|
40
|
-
!profile ||
|
|
57
|
+
if (!profile ||
|
|
41
58
|
values.get("--input") !== "-" ||
|
|
42
|
-
values.
|
|
59
|
+
[...values.keys()].some((key) => !["--config", "--profile", "--input"].includes(key)))
|
|
60
|
+
usage();
|
|
61
|
+
return { command: "trigger", config: optionalConfig(values), profile };
|
|
62
|
+
}
|
|
63
|
+
if (command === "schedule" && rest[0] === "trigger") {
|
|
64
|
+
const values = options(rest.slice(1));
|
|
65
|
+
const profile = values.get("--profile");
|
|
66
|
+
const schedule = values.get("--schedule");
|
|
67
|
+
if (!profile ||
|
|
68
|
+
!schedule ||
|
|
69
|
+
[...values.keys()].some((key) => !["--config", "--profile", "--schedule"].includes(key)))
|
|
43
70
|
usage();
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
71
|
+
return {
|
|
72
|
+
command: "schedule-trigger",
|
|
73
|
+
config: optionalConfig(values),
|
|
74
|
+
profile,
|
|
75
|
+
schedule,
|
|
76
|
+
};
|
|
47
77
|
}
|
|
48
78
|
if (command === "outcome" && rest.length === 1 && rest[0] === "submit") {
|
|
49
79
|
return { command: "outcome-submit" };
|
|
@@ -65,6 +95,9 @@ export async function runCli(command, dependencies) {
|
|
|
65
95
|
if (command.command === "trigger") {
|
|
66
96
|
return dependencies.trigger(command.config, command.profile, await readStdin());
|
|
67
97
|
}
|
|
98
|
+
if (command.command === "schedule-trigger") {
|
|
99
|
+
return dependencies.triggerSchedule(command.config, command.profile, command.schedule);
|
|
100
|
+
}
|
|
68
101
|
if (command.command === "outcome-submit")
|
|
69
102
|
return dependencies.submitOutcome();
|
|
70
103
|
console.log(dependencies.version);
|
package/dist/cli.js
CHANGED
|
@@ -3,6 +3,7 @@ import { parseCli, runCli } from "./cli-app.js";
|
|
|
3
3
|
import { runDoctor } from "./doctor.js";
|
|
4
4
|
import { runManualTrigger } from "./manual-trigger.js";
|
|
5
5
|
import { submitOutcomeFromCli } from "./outcome/submit.js";
|
|
6
|
+
import { runScheduleTrigger } from "./schedule-trigger.js";
|
|
6
7
|
import { runBeacon } from "./service.js";
|
|
7
8
|
import { packageVersion } from "./version.js";
|
|
8
9
|
async function main() {
|
|
@@ -10,6 +11,7 @@ async function main() {
|
|
|
10
11
|
serve: runBeacon,
|
|
11
12
|
doctor: runDoctor,
|
|
12
13
|
trigger: runManualTrigger,
|
|
14
|
+
triggerSchedule: runScheduleTrigger,
|
|
13
15
|
submitOutcome: submitOutcomeFromCli,
|
|
14
16
|
version: packageVersion,
|
|
15
17
|
});
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import { loadGlobalConfig } from "./config/global.js";
|
|
3
|
+
import { loadProfileRegistry } from "./config/registry.js";
|
|
4
|
+
import { loadFeishuCredentials } from "./config/secrets.js";
|
|
5
|
+
import { createFeishuGateway } from "./feishu/gateway.js";
|
|
6
|
+
import { startOutcomeServer } from "./outcome/server.js";
|
|
7
|
+
import { createPiRunOrchestrator } from "./run/create-pi-orchestrator.js";
|
|
8
|
+
import { RunQueue } from "./run/queue.js";
|
|
9
|
+
import { TriggerStore } from "./state/trigger-store.js";
|
|
10
|
+
function findSchedule(profile, scheduleId) {
|
|
11
|
+
const schedule = profile.schedules.find((candidate) => candidate.id === scheduleId);
|
|
12
|
+
if (!schedule) {
|
|
13
|
+
throw new Error(`Unknown Schedule in Profile ${profile.id}: ${scheduleId}`);
|
|
14
|
+
}
|
|
15
|
+
return schedule;
|
|
16
|
+
}
|
|
17
|
+
export async function triggerScheduleOnce(options) {
|
|
18
|
+
const schedule = findSchedule(options.profile, options.scheduleId);
|
|
19
|
+
const scheduledFor = (options.now ?? (() => new Date()))().toISOString();
|
|
20
|
+
const invocationId = (options.id ?? randomUUID)();
|
|
21
|
+
const claim = await options.store.claim({
|
|
22
|
+
sourceKey: ["manual-schedule", schedule.id, invocationId],
|
|
23
|
+
target: { kind: "chat", chatId: schedule.delivery.chatId },
|
|
24
|
+
ingress: {
|
|
25
|
+
invocation: "manual",
|
|
26
|
+
scheduleId: schedule.id,
|
|
27
|
+
scheduledFor,
|
|
28
|
+
text: schedule.input,
|
|
29
|
+
},
|
|
30
|
+
});
|
|
31
|
+
if (!claim.created) {
|
|
32
|
+
throw new Error(`Duplicate manual Schedule invocation: ${invocationId}`);
|
|
33
|
+
}
|
|
34
|
+
await options.process(claim.record.triggerKey, async () => ({
|
|
35
|
+
kind: "schedule",
|
|
36
|
+
scheduleId: schedule.id,
|
|
37
|
+
scheduledFor,
|
|
38
|
+
text: schedule.input,
|
|
39
|
+
}));
|
|
40
|
+
const record = (await options.store.list()).find((candidate) => candidate.triggerKey === claim.record.triggerKey);
|
|
41
|
+
if (!record?.run) {
|
|
42
|
+
throw new Error(`Schedule Trigger produced no Run record: trigger_id=${claim.record.triggerId}`);
|
|
43
|
+
}
|
|
44
|
+
if (record.run.state !== "succeeded" ||
|
|
45
|
+
record.delivery?.state !== "delivered") {
|
|
46
|
+
const failure = record.run.failure?.code ?? record.delivery?.failure?.code ?? "unknown";
|
|
47
|
+
throw new Error(`Schedule Trigger failed (run_id=${record.run.runId}): ${failure}`);
|
|
48
|
+
}
|
|
49
|
+
return record;
|
|
50
|
+
}
|
|
51
|
+
export async function runScheduleTrigger(configPath, profileId, scheduleId) {
|
|
52
|
+
const global = await loadGlobalConfig(configPath);
|
|
53
|
+
const profiles = await loadProfileRegistry(global.profilesDirectory);
|
|
54
|
+
const profile = profiles.find((candidate) => candidate.id === profileId);
|
|
55
|
+
if (!profile)
|
|
56
|
+
throw new Error(`Unknown Profile: ${profileId}`);
|
|
57
|
+
findSchedule(profile, scheduleId);
|
|
58
|
+
const credentials = await loadFeishuCredentials(profile.id, global.secretsPath);
|
|
59
|
+
const outcomes = await startOutcomeServer();
|
|
60
|
+
try {
|
|
61
|
+
const store = new TriggerStore(profile.directory, profile.id);
|
|
62
|
+
const orchestrator = createPiRunOrchestrator({
|
|
63
|
+
config: global,
|
|
64
|
+
profile,
|
|
65
|
+
store,
|
|
66
|
+
queue: new RunQueue(global.runs.maxConcurrent, global.runs.maxQueued),
|
|
67
|
+
outcomes,
|
|
68
|
+
delivery: createFeishuGateway(credentials),
|
|
69
|
+
});
|
|
70
|
+
const record = await triggerScheduleOnce({
|
|
71
|
+
profile,
|
|
72
|
+
scheduleId,
|
|
73
|
+
store,
|
|
74
|
+
process: (triggerKey, normalize) => orchestrator.process(triggerKey, normalize),
|
|
75
|
+
});
|
|
76
|
+
console.log(`[beacon] Schedule Trigger succeeded run_id=${record.run.runId}`);
|
|
77
|
+
}
|
|
78
|
+
finally {
|
|
79
|
+
await outcomes.close();
|
|
80
|
+
}
|
|
81
|
+
}
|