@nmakarov/cli-toolkit 0.50.0 → 0.51.0

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nmakarov/cli-toolkit",
3
- "version": "0.50.0",
3
+ "version": "0.51.0",
4
4
  "description": "A comprehensive toolkit for building CLI applications",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -78,7 +78,8 @@
78
78
  "bin": {
79
79
  "cli-runner": "./dist/cli-runner.js",
80
80
  "cli-deploy": "./scripts/deploy/cli.js",
81
- "cli-aws-discover": "./scripts/aws/discover.js"
81
+ "cli-aws-discover": "./scripts/aws/discover.js",
82
+ "cli-send-task": "./scripts/send-task.js"
82
83
  },
83
84
  "scripts": {
84
85
  "ssm:list": "node scripts/ssm/ssm-admin.js list",
@@ -158,6 +159,7 @@
158
159
  "scripts/ssm/",
159
160
  "scripts/deploy/",
160
161
  "scripts/aws/",
162
+ "scripts/send-task.js",
161
163
  "README.md",
162
164
  "LICENSE"
163
165
  ],
@@ -0,0 +1,173 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * cli-send-task — enqueue a task row on the queue (e.g. `tasks`).
4
+ *
5
+ * Thin CLI: `--name` → `TasksRegistry#resolveTaskParams` → `enqueueTask`.
6
+ * Tasks with `defaultWaitForResult` (or `--wait`) wait for the history row.
7
+ *
8
+ * Published as the `cli-send-task` bin. From an app that depends on
9
+ * `@nmakarov/cli-toolkit`:
10
+ *
11
+ * npx cli-send-task --dbName=everystate --name=ping --wait
12
+ * npx cli-send-task --dbName=everystate --name=setRuntimeParam \
13
+ * --serviceGroup=photos --paramKey=maxParallel --paramValue=16 --wait
14
+ *
15
+ * From this repo (after `npm run build`):
16
+ *
17
+ * npm run tasks:send -- --dbName=local --name=ping --wait
18
+ */
19
+
20
+ import { init } from "../dist/init.js";
21
+ import { Db } from "../dist/db.js";
22
+ import {
23
+ enqueueTask,
24
+ ensureTaskTables,
25
+ waitForTaskResult,
26
+ TasksRegistry,
27
+ } from "../dist/tasks.js";
28
+
29
+ const scriptDefs = {
30
+ /** Override: wait for completion regardless of TaskClass.defaultWaitForResult. */
31
+ wait: "boolean default false",
32
+ /** Override: skip waiting even when TaskClass.defaultWaitForResult is true. */
33
+ noWait: "boolean default false",
34
+ timeoutMs: "number default 120000",
35
+ pollMs: "number default 100",
36
+ };
37
+
38
+ /**
39
+ * Prefer example/dummy registry when present (local checkout); otherwise core tasks only
40
+ * (published npm package — `scripts/customTasks` is not shipped).
41
+ */
42
+ async function loadRegistry() {
43
+ try {
44
+ const mod = await import("./customTasks/registry.js");
45
+ return mod.createExampleTasksRegistry();
46
+ } catch {
47
+ return TasksRegistry.withCoreTasks();
48
+ }
49
+ }
50
+
51
+ function parseTime(v) {
52
+ if (v == null) return null;
53
+ const t = new Date(v).getTime();
54
+ return Number.isNaN(t) ? null : t;
55
+ }
56
+
57
+ function durationMs(row) {
58
+ const s = parseTime(row.started_at);
59
+ const e = parseTime(row.completed_at);
60
+ if (s == null || e == null) return null;
61
+ return Math.max(0, e - s);
62
+ }
63
+
64
+ function formatResultReport(row) {
65
+ const dur = durationMs(row);
66
+ const executor = {
67
+ service_group: row.service_group ?? null,
68
+ service_name: row.service_name ?? null,
69
+ instance_number: row.instance_number ?? null,
70
+ server_name: row.server_name ?? null,
71
+ };
72
+ const report = {
73
+ taskId: row.id,
74
+ name: row.name,
75
+ status: row.status,
76
+ success: row.success,
77
+ results: row.results,
78
+ executedBy: executor,
79
+ timing: {
80
+ created_at: row.created_at,
81
+ started_at: row.started_at,
82
+ completed_at: row.completed_at,
83
+ duration_ms: dur,
84
+ },
85
+ };
86
+ return JSON.stringify(report, null, 2);
87
+ }
88
+
89
+ const flow = async (context) => {
90
+ const logger = context.logger;
91
+ const { wait, noWait, timeoutMs, pollMs } = context.params.getAll(scriptDefs);
92
+
93
+ const db = await Db.init(context);
94
+ context.db = db;
95
+
96
+ const registry = await loadRegistry();
97
+ const nameHint = context.params.get("name", "string");
98
+ const TaskClass = registry.requireClass(
99
+ typeof nameHint === "string" ? nameHint.trim() : nameHint
100
+ );
101
+
102
+ const wantWait = noWait === true
103
+ ? false
104
+ : (wait === true || TaskClass.defaultWaitForResult === true);
105
+
106
+ // Tasks that know how to expand targeting (e.g. setRuntimeParam broadcast).
107
+ if (typeof TaskClass.enqueue === "function") {
108
+ await ensureTaskTables(context, {
109
+ queueName: context.params.get("queueName", "string") || "tasks",
110
+ recreate: false,
111
+ });
112
+ const { ids, targets } = await TaskClass.enqueue(context, {});
113
+ logger.info?.(
114
+ `[send-task] enqueued ${ids.length} task(s) name=${nameHint} targets=${targets.join(",") || "-"}`
115
+ );
116
+ if (!wantWait) {
117
+ logger.info?.(ids.join(","));
118
+ return;
119
+ }
120
+ let anyFail = false;
121
+ for (const id of ids) {
122
+ const done = await waitForTaskResult(context, id, {
123
+ queueName: context.params.get("queueName", "string") || "tasks",
124
+ timeoutMs: Number(timeoutMs) || 120_000,
125
+ pollMs: Number(pollMs) || 100,
126
+ });
127
+ if (!done) {
128
+ logger.error?.(`send-task: timeout waiting for task ${id}`);
129
+ process.exitCode = 1;
130
+ return;
131
+ }
132
+ logger.info?.(String(id));
133
+ logger.info?.(formatResultReport(done));
134
+ if (done.success === false) anyFail = true;
135
+ }
136
+ if (anyFail) process.exitCode = 1;
137
+ return;
138
+ }
139
+
140
+ const payload = await registry.resolveTaskParams(context);
141
+
142
+ await ensureTaskTables(context, { queueName: payload.queueName, recreate: false });
143
+ const id = await enqueueTask(context, payload);
144
+
145
+ logger.info?.(
146
+ `[send-task] enqueued id=${id} name=${payload.name} queue=${payload.queueName} group=${payload.serviceGroup ?? "-"} serviceName=${payload.serviceName ?? "-"} instance=${payload.instanceNumber ?? "-"} server=${payload.serverName ?? "-"}`
147
+ );
148
+
149
+ if (!wantWait) {
150
+ logger.info?.(String(id));
151
+ return;
152
+ }
153
+
154
+ const done = await waitForTaskResult(context, id, {
155
+ queueName: payload.queueName,
156
+ timeoutMs: Number(timeoutMs) || 120_000,
157
+ pollMs: Number(pollMs) || 100,
158
+ });
159
+
160
+ if (!done) {
161
+ logger.error?.(`send-task: timeout waiting for task ${id} in ${payload.queueName}_history (${timeoutMs}ms)`);
162
+ process.exitCode = 1;
163
+ return;
164
+ }
165
+
166
+ logger.info?.(String(id));
167
+ logger.info?.(formatResultReport(done));
168
+ if (done.success === false) {
169
+ process.exitCode = 1;
170
+ }
171
+ };
172
+
173
+ void init(flow);