@deckasoft/waify 0.6.0 → 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 +92 -15
- 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
|
|
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"]
|
|
1138
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));
|