@makefully/dutifully 1.21.2 → 1.22.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/CHANGELOG.md CHANGED
@@ -2,6 +2,10 @@
2
2
 
3
3
  ## Unreleased
4
4
 
5
+ ## 1.22.0
6
+
7
+ - Select a subset of service listings via optional job `id` and top-level `only` (`cheer -service packfully -only heroes,ui`).
8
+
5
9
  ## 1.21.2
6
10
 
7
11
  - Yap poll uses `/status` then download; while pending, tails `/logs` for live Cheerfully worker output.
package/README.md CHANGED
@@ -42,6 +42,41 @@ cheerfully({
42
42
  });
43
43
  ```
44
44
 
45
+ ### Selecting listings (`id` + `only`)
46
+
47
+ Each service entry in `cheerfully.json` is an array of jobs (listings). Give any job an optional `id` string (unique within that service) to target it without running the whole array:
48
+
49
+ ```javascript
50
+ {
51
+ "packfully": [{
52
+ "id": "heroes",
53
+ "src": "./sprites/heroes/",
54
+ "output": "./atlases/",
55
+ "options": { "prefix": "heroes" }
56
+ }, {
57
+ "id": "ui",
58
+ "src": "./sprites/ui/",
59
+ "output": "./atlases/",
60
+ "options": { "prefix": "ui" }
61
+ }]
62
+ }
63
+ ```
64
+
65
+ Then run a subset from the CLI or from Node:
66
+
67
+ ```bash
68
+ cheer -service packfully -only heroes,ui
69
+ ```
70
+
71
+ ```javascript
72
+ cheerfully({
73
+ service: 'packfully',
74
+ only: ['heroes', 'ui'] // or only: 'heroes,ui'
75
+ });
76
+ ```
77
+
78
+ Omit `only` to run every listing for that service (unchanged). Jobs without an `id` still run in that case; they cannot be selected when `only` is set. Unknown or duplicate ids fail with a clear error. Per-job difference skipping still applies to whichever listings you select. The `id` field is for client selection; Cheerfully ignores it on the server.
79
+
45
80
  ### Say (spoken notifications)
46
81
 
47
82
  `say` generates short spoken phrases through Cheerfully's ElevenLabs service, caches the audio under `.cache/tts/`, and plays it locally via `node-web-audio-api`. Use it from npm scripts instead of wiring ElevenLabs into the consuming project:
@@ -118,6 +153,7 @@ This image-to-atlas service packs PNG/JPG/WebP sprites into texture atlases and
118
153
  ```javascript
119
154
  {
120
155
  "packfully": [{
156
+ "id": "packed",
121
157
  "format": "texturepacker",
122
158
  "src": "./example/sprites/",
123
159
  "output": "./example/atlases/",
package/cheer.js CHANGED
@@ -34,4 +34,7 @@ const
34
34
  return cmds;
35
35
  };
36
36
 
37
- cheer(getArgsAsObj());
37
+ cheer(getArgsAsObj()).catch((err) => {
38
+ console.error(err.message || err);
39
+ process.exitCode = 1;
40
+ });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@makefully/dutifully",
3
- "version": "1.21.2",
3
+ "version": "1.22.0",
4
4
  "description": "Client tools for Cheerfully service.",
5
5
  "main": "./src/index.js",
6
6
  "bin": {
@@ -0,0 +1,118 @@
1
+ /**
2
+ * Select a subset of cheerfully.json job listings by optional job `id`.
3
+ */
4
+
5
+ /**
6
+ * @param {unknown} only
7
+ * @returns {string[]|null} null means no filter; non-empty array means filter
8
+ * @throws {Error} when `only` is present but yields no ids (e.g. bare `-only`)
9
+ */
10
+ const normalizeOnly = (only) => {
11
+ if (only === undefined || only === null || only === false) {
12
+ return null;
13
+ }
14
+
15
+ if (only === true) {
16
+ throw new Error('`-only` requires one or more job ids (e.g. -only heroes,ui).');
17
+ }
18
+
19
+ let raw;
20
+
21
+ if (typeof only === 'string') {
22
+ raw = only.split(',');
23
+ } else if (Array.isArray(only)) {
24
+ raw = only;
25
+ } else {
26
+ throw new Error(`Invalid \`only\` value: expected string or string[], got ${typeof only}.`);
27
+ }
28
+
29
+ const
30
+ ids = raw
31
+ .map((id) => String(id ?? '').trim())
32
+ .filter((id) => id.length > 0);
33
+
34
+ if (!ids.length) {
35
+ throw new Error('`-only` requires one or more job ids (e.g. -only heroes,ui).');
36
+ }
37
+
38
+ return ids;
39
+ };
40
+
41
+ /**
42
+ * Build a map from job id → original array index. Hard-errors on duplicate ids.
43
+ *
44
+ * @param {object[]} configs
45
+ * @returns {Map<string, number>}
46
+ */
47
+ const indexJobsById = (configs) => {
48
+ const
49
+ byId = new Map();
50
+
51
+ for (let i = 0; i < configs.length; i++) {
52
+ const
53
+ job = configs[i],
54
+ jobId = job?.id;
55
+
56
+ if (jobId === undefined || jobId === null || jobId === '') {
57
+ continue;
58
+ }
59
+
60
+ const
61
+ key = String(jobId);
62
+
63
+ if (byId.has(key)) {
64
+ throw new Error(
65
+ `Duplicate job id "${key}" at indices ${byId.get(key)} and ${i}. Job ids must be unique within a service.`
66
+ );
67
+ }
68
+
69
+ byId.set(key, i);
70
+ }
71
+
72
+ return byId;
73
+ };
74
+
75
+ /**
76
+ * @param {object[]} configs
77
+ * @param {string[]|null} onlyIds from normalizeOnly
78
+ * @returns {{jobs: object[], indices: number[]}}
79
+ */
80
+ const filterJobsById = (configs, onlyIds) => {
81
+ if (!onlyIds) {
82
+ return {
83
+ jobs: configs,
84
+ indices: configs.map((_, i) => i)
85
+ };
86
+ }
87
+
88
+ const
89
+ byId = indexJobsById(configs),
90
+ known = [...byId.keys()].sort(),
91
+ jobs = [],
92
+ indices = [];
93
+
94
+ for (const wanted of onlyIds) {
95
+ if (!byId.has(wanted)) {
96
+ const
97
+ knownList = known.length ? known.join(', ') : '(none)';
98
+
99
+ throw new Error(
100
+ `Unknown job id "${wanted}". Known ids: ${knownList}.`
101
+ );
102
+ }
103
+
104
+ const
105
+ index = byId.get(wanted);
106
+
107
+ jobs.push(configs[index]);
108
+ indices.push(index);
109
+ }
110
+
111
+ return {jobs, indices};
112
+ };
113
+
114
+ module.exports = {
115
+ normalizeOnly,
116
+ filterJobsById,
117
+ indexJobsById
118
+ };
package/src/index.js CHANGED
@@ -17,15 +17,19 @@ const
17
17
  translate: require('./classes/Translate')
18
18
  },
19
19
  getJSON = require('./helpers/getJSON'),
20
+ {normalizeOnly, filterJobsById} = require('./helpers/filterJobs'),
20
21
  send = async function (contents) {
21
22
  const
22
23
  {id, service} = contents,
23
- configs = Array.isArray(contents[service]) ? contents[service] : [contents[service]];
24
+ configs = Array.isArray(contents[service]) ? contents[service] : [contents[service]],
25
+ onlyIds = normalizeOnly(contents.only),
26
+ {jobs, indices} = filterJobsById(configs, onlyIds),
27
+ {length} = configs;
24
28
 
25
- for (let index = 0; index < configs.length; index++) {
29
+ for (let i = 0; i < jobs.length; i++) {
26
30
  const
27
- config = configs[index],
28
- {length} = configs;
31
+ config = jobs[i],
32
+ index = indices[i];
29
33
 
30
34
  if (!config) {
31
35
  console.warn(`Empty configuration for "${service}" service.`);
@@ -58,7 +62,7 @@ const cheer = async (cmdArgs) => {
58
62
  config = await getJSON('./cheerfully.json') ?? {},
59
63
  env = await getJSON('./env-cheerfully.json') ?? {};
60
64
 
61
- send({
65
+ await send({
62
66
  id: `${package.name}-${package.version}`,
63
67
  package,
64
68
  ...config,