@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 CHANGED
@@ -60,7 +60,9 @@ All config files live in `~/.config/waify/`:
60
60
 
61
61
  ## Scheduling
62
62
 
63
- Use `waify schedule add` to create cron jobs. The cron format is **6 fields** (sec min hour dom month dow). Changes require restarting the scheduler:
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
- defaultConfig = () => ConfigSchema.parse({});
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
- const raw = readFileSync(path, "utf-8");
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
- hostEnvFile: process.env["WAIFY_HOST_ENV_FILE"] ?? envPath()
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
- `volume = ${runtime.hostDataDir}:/data`,
195
- `volume = ${runtime.hostEnvFile}:/app/.env:ro`
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 readFileSync5 } from "fs";
338
- import { dirname as dirname5 } from "path";
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(dirname5(path), { recursive: true });
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 = readFileSync5(path, "utf-8").split("\n").filter((l) => l.length > 0);
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));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@deckasoft/waify",
3
- "version": "0.6.0",
3
+ "version": "0.6.1",
4
4
  "description": "AI-powered daily message sender for WhatsApp, powered by OpenWA",
5
5
  "keywords": [
6
6
  "whatsapp",