ework-aio 0.1.17 → 0.2.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.
@@ -0,0 +1,208 @@
1
+ // `ework-aio config <subcommand>` — read/change runtime .env keys.
2
+ //
3
+ // Subcommands:
4
+ // list Show all settable keys + current values
5
+ // get <KEY> Print one key's value
6
+ // set <KEY> <VALUE> Write to .env, then restart affected service
7
+ // (use --no-restart to skip)
8
+ // restart <web|daemon|both> Restart one or both services
9
+ //
10
+ // Allow-list: only keys in SETTABLE_KEYS (src/types.ts) are exposed.
11
+ // Secrets, DB paths, and the web<->daemon contract keys are deliberately
12
+ // excluded — those need `rm .env && ework-aio install` to regenerate.
13
+ //
14
+ // Cross-link propagation: when WORK_PORT changes, the daemon .env's
15
+ // GITEA_URL is rewritten; when DAEMON_PORT changes, the web .env's
16
+ // WORK_DAEMON_WEBHOOK_URL is rewritten. Both services then restart.
17
+
18
+ import fs from "node:fs";
19
+ import path from "node:path";
20
+ import { Logger, InstallError } from "../log.ts";
21
+ import { resolvePaths } from "../paths.ts";
22
+ import { parseEnvFile, patchEnvKey } from "../env.ts";
23
+ import { SECRET_ENV_VARS } from "../config.ts";
24
+ import {
25
+ SETTABLE_KEYS,
26
+ findSettableKey,
27
+ serviceForKey,
28
+ type GlobalOptions,
29
+ type ServiceTarget,
30
+ } from "../types.ts";
31
+ import { runRestart } from "./lifecycle.ts";
32
+
33
+ function envFileForService(
34
+ svc: "web" | "daemon",
35
+ paths: ReturnType<typeof resolvePaths>,
36
+ ): string {
37
+ return svc === "web" ? paths.webEnvFile : paths.daemonEnvFile;
38
+ }
39
+
40
+ async function readEnvKey(envFile: string, key: string): Promise<string | null> {
41
+ try {
42
+ const content = await Bun.file(envFile).text();
43
+ const v = parseEnvFile(content).entries.get(key);
44
+ return v !== undefined ? v : null;
45
+ } catch (err) {
46
+ if ((err as NodeJS.ErrnoException).code === "ENOENT") return null;
47
+ throw err;
48
+ }
49
+ }
50
+
51
+ export async function configList(opts: GlobalOptions, logger: Logger): Promise<void> {
52
+ const paths = resolvePaths({ dataDir: opts.dataDir, scope: opts.scope, useSystemd: opts.useSystemd });
53
+ logger.hr();
54
+ logger.log("Settable config keys");
55
+ logger.hr();
56
+ logger.log(` ${"KEY".padEnd(28)} ${"SERVICE".padEnd(8)} VALUE`);
57
+ for (const entry of SETTABLE_KEYS) {
58
+ const envFile = envFileForService(entry.service, paths);
59
+ let val = await readEnvKey(envFile, entry.key);
60
+ if (val === null) val = "(unset)";
61
+ logger.log(` ${entry.key.padEnd(28)} ${entry.service.padEnd(8)} ${val}`);
62
+ }
63
+ logger.hr();
64
+ logger.log(`Use ${logger.bold("config set <KEY> <VALUE>")} to change a key.`);
65
+ }
66
+
67
+ export async function configGet(
68
+ opts: GlobalOptions,
69
+ logger: Logger,
70
+ key: string,
71
+ ): Promise<void> {
72
+ const entry = findSettableKey(key);
73
+ if (!entry) {
74
+ throw new InstallError(
75
+ `Key '${key}' is not settable. Run 'ework-aio config list' for the allow-list.`,
76
+ );
77
+ }
78
+ if (SECRET_ENV_VARS.has(key)) {
79
+ throw new InstallError(
80
+ `Key '${key}' is a secret — use 'rm .env && ework-aio install' to regenerate.`,
81
+ );
82
+ }
83
+ const paths = resolvePaths({ dataDir: opts.dataDir, scope: opts.scope, useSystemd: opts.useSystemd });
84
+ const envFile = envFileForService(entry.service, paths);
85
+ const val = await readEnvKey(envFile, key);
86
+ if (val === null) {
87
+ logger.warn(`${key} is not currently set in ${envFile}`);
88
+ return;
89
+ }
90
+ process.stdout.write(`${val}\n`);
91
+ }
92
+
93
+ export async function configSet(
94
+ opts: GlobalOptions,
95
+ logger: Logger,
96
+ key: string,
97
+ value: string,
98
+ ): Promise<void> {
99
+ const entry = findSettableKey(key);
100
+ if (!entry) {
101
+ throw new InstallError(
102
+ `Key '${key}' is not settable. Run 'ework-aio config list' for the allow-list.`,
103
+ );
104
+ }
105
+ if (SECRET_ENV_VARS.has(key)) {
106
+ throw new InstallError(
107
+ `Key '${key}' is a secret — use 'rm .env && ework-aio install' to regenerate.`,
108
+ );
109
+ }
110
+ // .env is a line-oriented format; a value containing a newline would be
111
+ // parsed as two entries by parseEnvFile. Reject it outright so a stray
112
+ // shell expansion (e.g. $'alice\nWORK_TOKEN=evil') can't inject keys.
113
+ if (/[\r\n]/.test(value)) {
114
+ throw new InstallError(
115
+ `Value for ${key} contains a newline — multi-line values are not allowed in .env (got ${value.length} chars)`,
116
+ );
117
+ }
118
+
119
+ const paths = resolvePaths({ dataDir: opts.dataDir, scope: opts.scope, useSystemd: opts.useSystemd });
120
+ const envFile = envFileForService(entry.service, paths);
121
+
122
+ logger.log(`setting ${key}=${value} in ${envFile}`);
123
+ await patchEnvKey(envFile, key, value);
124
+ logger.ok(`${key} updated`);
125
+
126
+ if (entry.propagate) {
127
+ const targetFile = envFileForService(entry.propagate.targetService, paths);
128
+ const newVal = entry.propagate.template(value);
129
+ logger.log(`propagating to ${entry.propagate.targetService} (${entry.propagate.targetKey})`);
130
+ await patchEnvKey(targetFile, entry.propagate.targetKey, newVal);
131
+ logger.ok(`${entry.propagate.targetService} .env updated`);
132
+ }
133
+
134
+ if (opts.noRestart) {
135
+ const target = serviceForKey(key) ?? "both";
136
+ logger.warn(`--no-restart: changes saved but service not reloaded. Run 'ework-aio config restart ${target}' to apply.`);
137
+ return;
138
+ }
139
+
140
+ const target: ServiceTarget = serviceForKey(key) ?? "both";
141
+ logger.log(`restarting ${target}...`);
142
+ await runRestart(opts, logger, target);
143
+ }
144
+
145
+ export async function configRestart(
146
+ opts: GlobalOptions,
147
+ logger: Logger,
148
+ target: ServiceTarget,
149
+ ): Promise<void> {
150
+ await runRestart(opts, logger, target);
151
+ }
152
+
153
+ export interface ConfigArgs {
154
+ subcommand: "list" | "get" | "set" | "restart" | "help";
155
+ key?: string;
156
+ value?: string;
157
+ target?: ServiceTarget;
158
+ }
159
+
160
+ export async function runConfig(
161
+ opts: GlobalOptions,
162
+ logger: Logger,
163
+ args: ConfigArgs,
164
+ ): Promise<void> {
165
+ switch (args.subcommand) {
166
+ case "list":
167
+ await configList(opts, logger);
168
+ return;
169
+ case "get":
170
+ if (!args.key) throw new InstallError("Usage: ework-aio config get <KEY>");
171
+ await configGet(opts, logger, args.key);
172
+ return;
173
+ case "set":
174
+ if (!args.key || args.value === undefined) {
175
+ throw new InstallError("Usage: ework-aio config set <KEY> <VALUE>");
176
+ }
177
+ await configSet(opts, logger, args.key, args.value);
178
+ return;
179
+ case "restart":
180
+ await configRestart(opts, logger, args.target ?? "both");
181
+ return;
182
+ case "help":
183
+ printConfigHelp(logger);
184
+ return;
185
+ }
186
+ }
187
+
188
+ export function printConfigHelp(logger: Logger): void {
189
+ logger.log(`ework-aio config <subcommand>`);
190
+ logger.log(``);
191
+ logger.log(`Subcommands:`);
192
+ logger.log(` list List all settable keys + current values`);
193
+ logger.log(` get <KEY> Print current value of one key`);
194
+ logger.log(` set <KEY> <VALUE> Set a key in .env, then restart the affected`);
195
+ logger.log(` service (unless --no-restart is given)`);
196
+ logger.log(` restart <web|daemon|both> Restart one or both services`);
197
+ logger.log(``);
198
+ logger.log(`Examples:`);
199
+ logger.log(` ework-aio config list`);
200
+ logger.log(` ework-aio config get WORK_PORT`);
201
+ logger.log(` ework-aio config set WORK_PORT 8080`);
202
+ logger.log(` ework-aio config set WORK_TRANSLATE_URL http://127.0.0.1:8000/v1 --no-restart`);
203
+ logger.log(` ework-aio config restart both`);
204
+ logger.log(``);
205
+ logger.log(`Note: changing WORK_PORT or DAEMON_PORT also rewrites the cross-link the other`);
206
+ logger.log(`service uses, and restarts both. Secrets and DB paths are not settable here —`);
207
+ logger.log(`rerun \`ework-aio install\` (with \`rm .env\` first if you need new tokens).`);
208
+ }
@@ -0,0 +1,27 @@
1
+ // `ework-aio env` — print resolved paths (no secrets). Useful for users
2
+ // to find where data, .env files, and the bot token live without grepping.
3
+
4
+ import { Logger } from "../log.ts";
5
+ import { resolvePaths } from "../paths.ts";
6
+ import type { GlobalOptions } from "../types.ts";
7
+
8
+ export async function runEnv(opts: GlobalOptions, logger: Logger): Promise<void> {
9
+ const paths = resolvePaths({
10
+ dataDir: opts.dataDir,
11
+ scope: opts.scope,
12
+ useSystemd: opts.useSystemd,
13
+ });
14
+
15
+ logger.hr();
16
+ logger.log("ework-aio paths");
17
+ logger.hr();
18
+ logger.log(` data dir : ${paths.dataDir}`);
19
+ logger.log(` web env : ${paths.webEnvFile}`);
20
+ logger.log(` daemon env : ${paths.daemonEnvFile}`);
21
+ logger.log(` bot token : ${paths.botTokenFile}`);
22
+ logger.log(` opencode cfg : ${paths.opencodeConfigFile}`);
23
+ logger.log(` scope : ${opts.scope}`);
24
+ logger.log(` mode : ${opts.useSystemd ? "systemd" : "PID-file"}`);
25
+ logger.log(` web unit : ${paths.webUnitFile ?? "(not used)"}`);
26
+ logger.log(` daemon unit : ${paths.daemonUnitFile ?? "(not used)"}`);
27
+ }