@deckasoft/waify 0.5.1 → 0.6.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 +3 -1
- package/dist/cli/index.js +111 -19
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -60,7 +60,9 @@ All config files live in `~/.config/waify/`:
|
|
|
60
60
|
|
|
61
61
|
## Scheduling
|
|
62
62
|
|
|
63
|
-
|
|
63
|
+
Scheduling runs on an [Ofelia](https://github.com/mcuadros/ofelia) `scheduler` container that fires a transient sender container per job tick. `waify setup` generates the `Dockerfile`, builds the `openwa-scripts-sender:latest` image locally, and starts the scheduler — so scheduling works out of the box after setup.
|
|
64
|
+
|
|
65
|
+
Use `waify schedule add` to create cron jobs. The cron format is **6 fields** (sec min hour dom month dow). Changes restart the scheduler automatically; to restart manually:
|
|
64
66
|
|
|
65
67
|
```bash
|
|
66
68
|
waify schedule add morning "0 0 9 * * *"
|
package/dist/cli/index.js
CHANGED
|
@@ -12,7 +12,7 @@ var __export = (target, all) => {
|
|
|
12
12
|
// src/core/paths.ts
|
|
13
13
|
import { join } from "path";
|
|
14
14
|
import { homedir } from "os";
|
|
15
|
-
var dataDir, configPath, promptPath, scheduleJsonPath, schedulePath, logPath, envPath, composePath, qrImagePath;
|
|
15
|
+
var dataDir, configPath, promptPath, scheduleJsonPath, schedulePath, logPath, envPath, composePath, dockerfilePath, qrImagePath;
|
|
16
16
|
var init_paths = __esm({
|
|
17
17
|
"src/core/paths.ts"() {
|
|
18
18
|
"use strict";
|
|
@@ -24,6 +24,7 @@ var init_paths = __esm({
|
|
|
24
24
|
logPath = () => join(dataDir(), "messages.log");
|
|
25
25
|
envPath = () => process.env["WAIFY_ENV_PATH"] ?? join(dataDir(), ".env");
|
|
26
26
|
composePath = () => join(dataDir(), "docker-compose.yml");
|
|
27
|
+
dockerfilePath = () => join(dataDir(), "Dockerfile");
|
|
27
28
|
qrImagePath = () => join(dataDir(), "qr.png");
|
|
28
29
|
}
|
|
29
30
|
});
|
|
@@ -32,7 +33,7 @@ var init_paths = __esm({
|
|
|
32
33
|
import { existsSync, readFileSync, writeFileSync, mkdirSync } from "fs";
|
|
33
34
|
import { dirname } from "path";
|
|
34
35
|
import { z } from "zod";
|
|
35
|
-
var RecipientSchema, ConfigSchema, defaultConfig, loadConfig, saveConfig, assertConfigReady;
|
|
36
|
+
var RecipientSchema, ConfigSchema, parseConfig, defaultConfig, loadConfig, saveConfig, assertConfigReady;
|
|
36
37
|
var init_config = __esm({
|
|
37
38
|
"src/core/config.ts"() {
|
|
38
39
|
"use strict";
|
|
@@ -47,12 +48,16 @@ var init_config = __esm({
|
|
|
47
48
|
openwaApiKey: z.string().nullable().default(null),
|
|
48
49
|
recipients: z.array(RecipientSchema).max(1).default([])
|
|
49
50
|
});
|
|
50
|
-
|
|
51
|
+
parseConfig = (raw) => {
|
|
52
|
+
const source = typeof raw === "object" && raw !== null ? raw : {};
|
|
53
|
+
const override = process.env["OPENWA_BASE_URL"];
|
|
54
|
+
return ConfigSchema.parse(override ? { ...source, openwaBaseUrl: override } : source);
|
|
55
|
+
};
|
|
56
|
+
defaultConfig = () => parseConfig({});
|
|
51
57
|
loadConfig = () => {
|
|
52
58
|
const path = configPath();
|
|
53
59
|
if (!existsSync(path)) return defaultConfig();
|
|
54
|
-
|
|
55
|
-
return ConfigSchema.parse(JSON.parse(raw));
|
|
60
|
+
return parseConfig(JSON.parse(readFileSync(path, "utf-8")));
|
|
56
61
|
};
|
|
57
62
|
saveConfig = (config) => {
|
|
58
63
|
const path = configPath();
|
|
@@ -121,7 +126,7 @@ var init_prompt = __esm({
|
|
|
121
126
|
import { existsSync as existsSync3, mkdirSync as mkdirSync3, readFileSync as readFileSync3, writeFileSync as writeFileSync3 } from "fs";
|
|
122
127
|
import { dirname as dirname3 } from "path";
|
|
123
128
|
import { z as z3 } from "zod";
|
|
124
|
-
var CRON_RANGES, STEP_RE, RANGE_RE, isValidCronField, isValidCron, ScheduledJobSchema, ScheduleSchema, defaultSchedule, scheduleJsonPath2, loadSchedule, saveSchedule, ofeliaRuntime, renderJob, renderOfeliaIni, regenerateOfeliaIni, addJob, removeJob;
|
|
129
|
+
var CRON_RANGES, STEP_RE, RANGE_RE, isValidCronField, isValidCron, ScheduledJobSchema, ScheduleSchema, defaultSchedule, scheduleJsonPath2, loadSchedule, saveSchedule, ofeliaRuntime, renderEnv, renderJob, renderOfeliaIni, regenerateOfeliaIni, addJob, removeJob;
|
|
125
130
|
var init_schedule = __esm({
|
|
126
131
|
"src/core/schedule.ts"() {
|
|
127
132
|
"use strict";
|
|
@@ -183,16 +188,24 @@ var init_schedule = __esm({
|
|
|
183
188
|
image: process.env["WAIFY_SENDER_IMAGE"] ?? "openwa-scripts-sender:latest",
|
|
184
189
|
network: process.env["WAIFY_NETWORK"] ?? "waify-network",
|
|
185
190
|
hostDataDir: process.env["WAIFY_HOST_DATA_DIR"] ?? dataDir(),
|
|
186
|
-
|
|
191
|
+
apiBaseUrl: process.env["WAIFY_API_INTERNAL_URL"] ?? "http://openwa-api:2785"
|
|
187
192
|
});
|
|
193
|
+
renderEnv = (key, value) => `environment = ${key}\\=${value}`;
|
|
188
194
|
renderJob = (job, runtime) => [
|
|
189
195
|
`[job-run "${job.name}"]`,
|
|
190
196
|
`schedule = ${job.schedule}`,
|
|
191
197
|
`image = ${runtime.image}`,
|
|
192
198
|
`network = ${runtime.network}`,
|
|
199
|
+
// The sender image is built locally, so Ofelia must not try to pull it
|
|
200
|
+
// (default Pull=true → 404 pull access denied). See mcuadros/ofelia#55.
|
|
201
|
+
`pull = false`,
|
|
193
202
|
`command = ${job.command}`,
|
|
194
|
-
|
|
195
|
-
|
|
203
|
+
// WAIFY_DATA_DIR points config/.env resolution at the mounted /data dir;
|
|
204
|
+
// OPENWA_BASE_URL reaches the API by service name over waify-network
|
|
205
|
+
// (the mounted config.json says localhost, which is wrong inside a container).
|
|
206
|
+
renderEnv("WAIFY_DATA_DIR", "/data"),
|
|
207
|
+
renderEnv("OPENWA_BASE_URL", runtime.apiBaseUrl),
|
|
208
|
+
`volume = ${runtime.hostDataDir}:/data`
|
|
196
209
|
].join("\n");
|
|
197
210
|
renderOfeliaIni = (schedule, runtime = ofeliaRuntime()) => {
|
|
198
211
|
const header = [
|
|
@@ -334,8 +347,8 @@ var init_sender = __esm({
|
|
|
334
347
|
});
|
|
335
348
|
|
|
336
349
|
// src/core/logger.ts
|
|
337
|
-
import { appendFileSync, existsSync as existsSync7, mkdirSync as mkdirSync6, readFileSync as
|
|
338
|
-
import { dirname as
|
|
350
|
+
import { appendFileSync, existsSync as existsSync7, mkdirSync as mkdirSync6, readFileSync as readFileSync6 } from "fs";
|
|
351
|
+
import { dirname as dirname6 } from "path";
|
|
339
352
|
var log, LINE_RE, parseLine, readHistory;
|
|
340
353
|
var init_logger = __esm({
|
|
341
354
|
"src/core/logger.ts"() {
|
|
@@ -343,7 +356,7 @@ var init_logger = __esm({
|
|
|
343
356
|
init_paths();
|
|
344
357
|
log = (status, detail) => {
|
|
345
358
|
const path = logPath();
|
|
346
|
-
mkdirSync6(
|
|
359
|
+
mkdirSync6(dirname6(path), { recursive: true });
|
|
347
360
|
const timestamp = (/* @__PURE__ */ new Date()).toISOString();
|
|
348
361
|
const line = `[${timestamp}] ${status.toUpperCase()} | ${detail}
|
|
349
362
|
`;
|
|
@@ -360,7 +373,7 @@ var init_logger = __esm({
|
|
|
360
373
|
readHistory = (limit) => {
|
|
361
374
|
const path = logPath();
|
|
362
375
|
if (!existsSync7(path)) return [];
|
|
363
|
-
const lines =
|
|
376
|
+
const lines = readFileSync6(path, "utf-8").split("\n").filter((l) => l.length > 0);
|
|
364
377
|
const entries = lines.map(parseLine).filter((e) => e !== null);
|
|
365
378
|
return limit ? entries.slice(-limit) : entries;
|
|
366
379
|
};
|
|
@@ -985,9 +998,10 @@ var registerInit = (program2) => {
|
|
|
985
998
|
|
|
986
999
|
// src/cli/commands/setup.ts
|
|
987
1000
|
import { spawnSync } from "child_process";
|
|
988
|
-
import { existsSync as existsSync6, mkdirSync as mkdirSync5, writeFileSync as writeFileSync6 } from "fs";
|
|
1001
|
+
import { existsSync as existsSync6, mkdirSync as mkdirSync5, readFileSync as readFileSync5, writeFileSync as writeFileSync6 } from "fs";
|
|
989
1002
|
import { homedir as homedir2 } from "os";
|
|
990
|
-
import { join as join2 } from "path";
|
|
1003
|
+
import { dirname as dirname5, join as join2 } from "path";
|
|
1004
|
+
import { fileURLToPath } from "url";
|
|
991
1005
|
import { createInterface } from "readline";
|
|
992
1006
|
import qrcode from "qrcode-terminal";
|
|
993
1007
|
|
|
@@ -1131,17 +1145,80 @@ var composeTemplate = () => `services:
|
|
|
1131
1145
|
- REDIS_BUILTIN=false
|
|
1132
1146
|
volumes:
|
|
1133
1147
|
- openwa-data:/app/data
|
|
1148
|
+
networks:
|
|
1149
|
+
- waify-network
|
|
1150
|
+
restart: unless-stopped
|
|
1151
|
+
|
|
1152
|
+
scheduler:
|
|
1153
|
+
image: mcuadros/ofelia:latest
|
|
1154
|
+
depends_on:
|
|
1155
|
+
- openwa-api
|
|
1156
|
+
command: daemon --config=/etc/ofelia/config.ini
|
|
1157
|
+
volumes:
|
|
1158
|
+
- /var/run/docker.sock:/var/run/docker.sock
|
|
1159
|
+
- ${schedulePath()}:/etc/ofelia/config.ini:ro
|
|
1160
|
+
networks:
|
|
1161
|
+
- waify-network
|
|
1134
1162
|
restart: unless-stopped
|
|
1135
1163
|
|
|
1136
1164
|
volumes:
|
|
1137
1165
|
openwa-data:
|
|
1166
|
+
|
|
1167
|
+
networks:
|
|
1168
|
+
waify-network:
|
|
1169
|
+
name: waify-network
|
|
1138
1170
|
`;
|
|
1171
|
+
var waifyVersion = () => {
|
|
1172
|
+
const dir = dirname5(fileURLToPath(import.meta.url));
|
|
1173
|
+
const candidates = [
|
|
1174
|
+
join2(dir, "..", "..", "package.json"),
|
|
1175
|
+
join2(dir, "..", "..", "..", "package.json")
|
|
1176
|
+
];
|
|
1177
|
+
const found = candidates.find((p) => existsSync6(p));
|
|
1178
|
+
if (!found) return "latest";
|
|
1179
|
+
try {
|
|
1180
|
+
return z5.object({ version: z5.string() }).parse(JSON.parse(readFileSync5(found, "utf-8"))).version;
|
|
1181
|
+
} catch {
|
|
1182
|
+
return "latest";
|
|
1183
|
+
}
|
|
1184
|
+
};
|
|
1185
|
+
var dockerfileTemplate = (version) => `FROM node:22-alpine
|
|
1186
|
+
RUN npm install -g @deckasoft/waify@${version}
|
|
1187
|
+
ENTRYPOINT ["waify"]
|
|
1188
|
+
`;
|
|
1189
|
+
var buildSenderImage = () => {
|
|
1190
|
+
console.warn("Writing Dockerfile and building sender image...");
|
|
1191
|
+
writeFileSync6(dockerfilePath(), dockerfileTemplate(waifyVersion()), "utf-8");
|
|
1192
|
+
const result = spawnSync(
|
|
1193
|
+
"docker",
|
|
1194
|
+
["build", "-t", "openwa-scripts-sender:latest", "-f", dockerfilePath(), dataDir()],
|
|
1195
|
+
{ stdio: "inherit" }
|
|
1196
|
+
);
|
|
1197
|
+
if (result.status !== 0) {
|
|
1198
|
+
console.warn(
|
|
1199
|
+
"warning: sender image build failed \u2014 scheduled sends will not run until it is built."
|
|
1200
|
+
);
|
|
1201
|
+
}
|
|
1202
|
+
};
|
|
1203
|
+
var startScheduler = () => {
|
|
1204
|
+
console.warn("Starting scheduler...");
|
|
1205
|
+
const result = spawnSync("docker", ["compose", "-f", composePath(), "up", "-d"], {
|
|
1206
|
+
stdio: "inherit"
|
|
1207
|
+
});
|
|
1208
|
+
if (result.status !== 0) {
|
|
1209
|
+
console.warn(
|
|
1210
|
+
`warning: failed to start scheduler \u2014 run manually: docker compose -f ${composePath()} up -d`
|
|
1211
|
+
);
|
|
1212
|
+
}
|
|
1213
|
+
};
|
|
1139
1214
|
var finalizeSetup = (sessionId, jobs) => {
|
|
1140
1215
|
saveConfig({ ...loadConfig(), openwaSessionId: sessionId });
|
|
1141
1216
|
if (!existsSync6(promptPath())) {
|
|
1142
1217
|
savePrompt(defaultPrompt);
|
|
1143
1218
|
}
|
|
1144
1219
|
saveSchedule({ jobs });
|
|
1220
|
+
buildSenderImage();
|
|
1221
|
+
startScheduler();
|
|
1145
1222
|
console.warn("\n\u2713 All done! Run `waify send` to send your first message.");
|
|
1146
1223
|
};
|
|
1147
1224
|
var promptLine = (rl, question) => new Promise((resolve) => rl.question(question, resolve));
|
|
@@ -1676,6 +1753,19 @@ var registerPrompt = (program2) => {
|
|
|
1676
1753
|
|
|
1677
1754
|
// src/cli/commands/schedule.ts
|
|
1678
1755
|
init_schedule();
|
|
1756
|
+
init_paths();
|
|
1757
|
+
import { spawnSync as spawnSync3 } from "child_process";
|
|
1758
|
+
var restartScheduler = () => {
|
|
1759
|
+
console.warn("restarting scheduler\u2026");
|
|
1760
|
+
const result = spawnSync3("docker", ["compose", "-f", composePath(), "restart", "scheduler"], {
|
|
1761
|
+
stdio: "inherit"
|
|
1762
|
+
});
|
|
1763
|
+
if (result.status !== 0) {
|
|
1764
|
+
console.warn(
|
|
1765
|
+
`warning: scheduler restart failed \u2014 run manually: docker compose -f ${composePath()} restart scheduler`
|
|
1766
|
+
);
|
|
1767
|
+
}
|
|
1768
|
+
};
|
|
1679
1769
|
var registerSchedule = (program2) => {
|
|
1680
1770
|
const schedule = program2.command("schedule").description("Manage Ofelia scheduled jobs");
|
|
1681
1771
|
schedule.command("list").description("List all scheduled jobs").action(() => {
|
|
@@ -1689,14 +1779,16 @@ var registerSchedule = (program2) => {
|
|
|
1689
1779
|
`);
|
|
1690
1780
|
});
|
|
1691
1781
|
});
|
|
1692
|
-
schedule.command("add <name> <cron>").description('Add a new job. <cron> uses 6-field syntax (sec min hour dom month dow), e.g. "0 0 9 * * *"').option("-c, --command <cmd>", "command for the sender container to run", "send").action((name, cron, { command }) => {
|
|
1782
|
+
schedule.command("add <name> <cron>").description('Add a new job. <cron> uses 6-field syntax (sec min hour dom month dow), e.g. "0 0 9 * * *"').option("-c, --command <cmd>", "command for the sender container to run", "send").option("--no-restart", "skip automatic scheduler restart").action((name, cron, { command, restart }) => {
|
|
1693
1783
|
const job = ScheduledJobSchema.parse({ name, schedule: cron, command });
|
|
1694
1784
|
addJob(job);
|
|
1695
|
-
console.warn(`added job "${name}"
|
|
1785
|
+
console.warn(`added job "${name}"`);
|
|
1786
|
+
if (restart) restartScheduler();
|
|
1696
1787
|
});
|
|
1697
|
-
schedule.command("remove <name>").description("Remove a job by name").action((name) => {
|
|
1788
|
+
schedule.command("remove <name>").description("Remove a job by name").option("--no-restart", "skip automatic scheduler restart").action((name, { restart }) => {
|
|
1698
1789
|
removeJob(name);
|
|
1699
|
-
console.warn(`removed job "${name}"
|
|
1790
|
+
console.warn(`removed job "${name}"`);
|
|
1791
|
+
if (restart) restartScheduler();
|
|
1700
1792
|
});
|
|
1701
1793
|
};
|
|
1702
1794
|
|