@oore/client 0.11.0 → 0.13.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/README.md +37 -0
- package/dist/cli/config.js +57 -0
- package/dist/cli/main.js +17 -0
- package/dist/cli/qr.js +327 -0
- package/dist/cli/run.js +438 -0
- package/dist/generated/types.gen.d.ts +34 -0
- package/dist/generated/valibot/v-create-notification-channel-body.d.ts +4 -0
- package/dist/generated/valibot/v-create-notification-channel-request.d.ts +4 -0
- package/dist/generated/valibot/v-create-notification-channel-request.js +2 -0
- package/dist/generated/valibot/v-create-notification-channel-response.d.ts +4 -0
- package/dist/generated/valibot/v-get-notification-channel-response.d.ts +4 -0
- package/dist/generated/valibot/v-get-pipeline-google-play-publish-response.d.ts +8 -0
- package/dist/generated/valibot/v-google-play-credentials-test-response.d.ts +1 -0
- package/dist/generated/valibot/v-google-play-credentials-test-response.js +2 -1
- package/dist/generated/valibot/v-list-notification-channels-response.d.ts +4 -0
- package/dist/generated/valibot/v-list-notification-channels-response2.d.ts +4 -0
- package/dist/generated/valibot/v-notification-channel-response.d.ts +4 -0
- package/dist/generated/valibot/v-notification-channel.d.ts +4 -0
- package/dist/generated/valibot/v-notification-channel.js +2 -0
- package/dist/generated/valibot/v-pipeline-google-play-publish-response.d.ts +8 -0
- package/dist/generated/valibot/v-pipeline-google-play-publish-response.js +2 -0
- package/dist/generated/valibot/v-slack-mention.d.ts +5 -0
- package/dist/generated/valibot/v-slack-mention.js +6 -0
- package/dist/generated/valibot/v-test-pipeline-google-play-credentials-response.d.ts +1 -0
- package/dist/generated/valibot/v-update-notification-channel-body.d.ts +4 -0
- package/dist/generated/valibot/v-update-notification-channel-request.d.ts +4 -0
- package/dist/generated/valibot/v-update-notification-channel-request.js +2 -0
- package/dist/generated/valibot/v-update-notification-channel-response.d.ts +4 -0
- package/dist/generated/valibot/v-update-pipeline-google-play-publish-response.d.ts +8 -0
- package/dist/msw-scenarios.d.ts +12 -1
- package/dist/msw-scenarios.js +345 -1
- package/dist/valibot.d.ts +1 -0
- package/dist/valibot.js +1 -0
- package/openapi/openapi.json +58 -0
- package/package.json +5 -2
package/README.md
CHANGED
|
@@ -99,6 +99,21 @@ const server = setupServer(...createOoreMockHandlers())
|
|
|
99
99
|
|
|
100
100
|
The default scenario owns a fresh copy of its state each time. Creating a project changes later project-list responses. The data and timestamps are fixed, not random.
|
|
101
101
|
|
|
102
|
+
It also has three releases: `rollingReleaseScenario` (Google Play at 20%), `haltedReleaseScenario` (halted at 5% after a crash spike) and `completeReleaseScenario` (App Store, fully rolled out). The release list, detail and activity handlers answer from that state, and raise, pause, resume, halt, complete and approve move it the way the daemon does: `allowed_actions` follows the state, and an action the state doesn't allow gets a 409. Pass your own `releases` to start from other states:
|
|
103
|
+
|
|
104
|
+
```ts
|
|
105
|
+
import {
|
|
106
|
+
createOoreMockHandlers,
|
|
107
|
+
defaultOoreScenario,
|
|
108
|
+
haltedReleaseScenario,
|
|
109
|
+
} from '@oore/client/msw/scenarios'
|
|
110
|
+
|
|
111
|
+
const handlers = createOoreMockHandlers({
|
|
112
|
+
...defaultOoreScenario,
|
|
113
|
+
releases: [haltedReleaseScenario],
|
|
114
|
+
})
|
|
115
|
+
```
|
|
116
|
+
|
|
102
117
|
## Valibot
|
|
103
118
|
|
|
104
119
|
Install `valibot` when using this entrypoint.
|
|
@@ -176,6 +191,28 @@ const link = await generateDownloadLink({ client, path: { artifact_id } })
|
|
|
176
191
|
window.location.assign(link.download_url)
|
|
177
192
|
```
|
|
178
193
|
|
|
194
|
+
## Developer CLI
|
|
195
|
+
|
|
196
|
+
The package installs an `oore-dev` command for triggering and following builds from a terminal. It uses only Node's standard library and the operations above, and no package entrypoint imports it.
|
|
197
|
+
|
|
198
|
+
```sh
|
|
199
|
+
npm install --global @oore/client # or: npx -p @oore/client oore-dev --help
|
|
200
|
+
|
|
201
|
+
# Create an API token in the web app (Settings → API tokens), then:
|
|
202
|
+
echo "$OORE_TOKEN" | oore-dev login --url https://ci.example.com
|
|
203
|
+
|
|
204
|
+
oore-dev run Release --branch feature/login --platform android -f
|
|
205
|
+
oore-dev logs -f <build-id>
|
|
206
|
+
oore-dev install <build-id> # prints the install link and a QR code
|
|
207
|
+
oore-dev status
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
`run` takes a pipeline ID or name; when a name exists in more than one project, add `--project`. `run -f` and `logs -f` print the stored logs, stream new lines until the build finishes, and exit with status 1 unless it succeeded. They switch to polling if the live stream drops.
|
|
211
|
+
|
|
212
|
+
`login` checks the token with `GET /v1/users/me` and saves it to `$XDG_CONFIG_HOME/oore-dev/config.json` (default `~/.config/oore-dev/config.json`, or `$OORE_DEV_CONFIG`), readable only by you. Save more than one daemon with `--name` and choose one with `--instance`; `OORE_URL` and `OORE_TOKEN` override the file, for example in CI. A session token works too, but API tokens don't expire when you sign out of the web app.
|
|
213
|
+
|
|
214
|
+
The command is named `oore-dev` so it doesn't clash with `oore`, the operator CLI that installs and runs the Oore daemon.
|
|
215
|
+
|
|
179
216
|
## Entrypoints
|
|
180
217
|
|
|
181
218
|
| Import | Contents |
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { chmod, mkdir, readFile, writeFile } from 'node:fs/promises';
|
|
2
|
+
import { homedir } from 'node:os';
|
|
3
|
+
import { dirname, join } from 'node:path';
|
|
4
|
+
/**
|
|
5
|
+
* `$OORE_DEV_CONFIG`, else `$XDG_CONFIG_HOME/oore-dev/config.json`, else
|
|
6
|
+
* `~/.config/oore-dev/config.json`. Kept apart from the operator CLI's files.
|
|
7
|
+
*/
|
|
8
|
+
export const configPath = (env) => env.OORE_DEV_CONFIG ||
|
|
9
|
+
join(env.XDG_CONFIG_HOME || join(env.HOME || homedir(), '.config'), 'oore-dev', 'config.json');
|
|
10
|
+
export async function readConfig(path) {
|
|
11
|
+
let text;
|
|
12
|
+
try {
|
|
13
|
+
text = await readFile(path, 'utf8');
|
|
14
|
+
}
|
|
15
|
+
catch (error) {
|
|
16
|
+
if (error.code === 'ENOENT') {
|
|
17
|
+
return { instances: {} };
|
|
18
|
+
}
|
|
19
|
+
throw error;
|
|
20
|
+
}
|
|
21
|
+
const parsed = JSON.parse(text);
|
|
22
|
+
return { ...parsed, instances: parsed.instances ?? {} };
|
|
23
|
+
}
|
|
24
|
+
/** Writes the config readable by the current user only: it holds tokens. */
|
|
25
|
+
export async function writeConfig(path, config) {
|
|
26
|
+
await mkdir(dirname(path), { mode: 0o700, recursive: true });
|
|
27
|
+
await writeFile(path, `${JSON.stringify(config, null, 2)}\n`, { mode: 0o600 });
|
|
28
|
+
await chmod(path, 0o600);
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Picks the instance: `OORE_URL`/`OORE_TOKEN` (or `--url`/`--token`) override
|
|
32
|
+
* the file, `--instance` picks a named entry, else the default entry, else
|
|
33
|
+
* the only entry.
|
|
34
|
+
*/
|
|
35
|
+
export function resolveInstance(config, options, env) {
|
|
36
|
+
const names = Object.keys(config.instances);
|
|
37
|
+
const name = options.instance ??
|
|
38
|
+
env.OORE_INSTANCE ??
|
|
39
|
+
config.default ??
|
|
40
|
+
(names.length === 1 ? names[0] : undefined);
|
|
41
|
+
const saved = name ? config.instances[name] : undefined;
|
|
42
|
+
if (name && !saved && (options.instance || env.OORE_INSTANCE)) {
|
|
43
|
+
throw new Error(`No instance named "${name}". Known: ${names.join(', ') || 'none'}`);
|
|
44
|
+
}
|
|
45
|
+
const url = options.url ?? env.OORE_URL ?? saved?.url;
|
|
46
|
+
const token = options.token ?? env.OORE_TOKEN ?? saved?.token;
|
|
47
|
+
if (!url || !token) {
|
|
48
|
+
throw new Error('No Oore instance configured. Run `oore-dev login --url <daemon url>` ' +
|
|
49
|
+
'with an API token, or set OORE_URL and OORE_TOKEN.');
|
|
50
|
+
}
|
|
51
|
+
return {
|
|
52
|
+
name: name ?? 'env',
|
|
53
|
+
project: env.OORE_PROJECT ?? saved?.project,
|
|
54
|
+
token,
|
|
55
|
+
url,
|
|
56
|
+
};
|
|
57
|
+
}
|
package/dist/cli/main.js
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { runCli } from './run.js';
|
|
3
|
+
const readStdin = async () => {
|
|
4
|
+
if (process.stdin.isTTY)
|
|
5
|
+
return '';
|
|
6
|
+
let text = '';
|
|
7
|
+
for await (const chunk of process.stdin)
|
|
8
|
+
text += String(chunk);
|
|
9
|
+
return text;
|
|
10
|
+
};
|
|
11
|
+
process.exitCode = await runCli(process.argv.slice(2), {
|
|
12
|
+
env: process.env,
|
|
13
|
+
isTTY: process.stdout.isTTY === true,
|
|
14
|
+
readStdin,
|
|
15
|
+
stderr: (text) => process.stderr.write(text),
|
|
16
|
+
stdout: (text) => process.stdout.write(text),
|
|
17
|
+
});
|
package/dist/cli/qr.js
ADDED
|
@@ -0,0 +1,327 @@
|
|
|
1
|
+
// A small QR Code encoder (byte mode, error correction level M) for printing
|
|
2
|
+
// install links in a terminal. It follows ISO/IEC 18004 and the structure of
|
|
3
|
+
// Project Nayuki's reference encoder, without a runtime dependency.
|
|
4
|
+
/** Error correction codewords per block, by version (index 0 unused). */
|
|
5
|
+
const ECC_PER_BLOCK = [
|
|
6
|
+
0, 10, 16, 26, 18, 24, 16, 18, 22, 22, 26, 30, 22, 22, 24, 24, 28, 28, 26, 26,
|
|
7
|
+
26, 26, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28,
|
|
8
|
+
28, 28,
|
|
9
|
+
];
|
|
10
|
+
/** Error correction blocks, by version (index 0 unused). */
|
|
11
|
+
const ECC_BLOCKS = [
|
|
12
|
+
0, 1, 1, 1, 2, 2, 4, 4, 4, 5, 5, 5, 8, 9, 9, 10, 10, 11, 13, 14, 16, 17, 17,
|
|
13
|
+
18, 20, 21, 23, 25, 26, 28, 29, 31, 33, 35, 37, 38, 40, 43, 45, 47, 49,
|
|
14
|
+
];
|
|
15
|
+
/** Format bits for level M. */
|
|
16
|
+
const LEVEL_M_FORMAT_BITS = 0;
|
|
17
|
+
const bit = (value, index) => ((value >>> index) & 1) !== 0;
|
|
18
|
+
const rawDataModules = (version) => {
|
|
19
|
+
let result = (16 * version + 128) * version + 64;
|
|
20
|
+
if (version >= 2) {
|
|
21
|
+
const align = Math.floor(version / 7) + 2;
|
|
22
|
+
result -= (25 * align - 10) * align - 55;
|
|
23
|
+
if (version >= 7)
|
|
24
|
+
result -= 36;
|
|
25
|
+
}
|
|
26
|
+
return result;
|
|
27
|
+
};
|
|
28
|
+
const dataCodewords = (version) => Math.floor(rawDataModules(version) / 8) -
|
|
29
|
+
ECC_PER_BLOCK[version] * ECC_BLOCKS[version];
|
|
30
|
+
const gfMultiply = (x, y) => {
|
|
31
|
+
let z = 0;
|
|
32
|
+
for (let i = 7; i >= 0; i--) {
|
|
33
|
+
z = (z << 1) ^ ((z >>> 7) * 0x11d);
|
|
34
|
+
z ^= ((y >>> i) & 1) * x;
|
|
35
|
+
}
|
|
36
|
+
return z & 0xff;
|
|
37
|
+
};
|
|
38
|
+
const rsDivisor = (degree) => {
|
|
39
|
+
const result = new Array(degree).fill(0);
|
|
40
|
+
result[degree - 1] = 1;
|
|
41
|
+
let root = 1;
|
|
42
|
+
for (let i = 0; i < degree; i++) {
|
|
43
|
+
for (let j = 0; j < degree; j++) {
|
|
44
|
+
result[j] = gfMultiply(result[j], root);
|
|
45
|
+
if (j + 1 < degree)
|
|
46
|
+
result[j] ^= result[j + 1];
|
|
47
|
+
}
|
|
48
|
+
root = gfMultiply(root, 0x02);
|
|
49
|
+
}
|
|
50
|
+
return result;
|
|
51
|
+
};
|
|
52
|
+
const rsRemainder = (data, divisor) => {
|
|
53
|
+
const result = new Array(divisor.length).fill(0);
|
|
54
|
+
for (const byte of data) {
|
|
55
|
+
const factor = byte ^ result.shift();
|
|
56
|
+
result.push(0);
|
|
57
|
+
divisor.forEach((coefficient, i) => {
|
|
58
|
+
result[i] ^= gfMultiply(coefficient, factor);
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
return result;
|
|
62
|
+
};
|
|
63
|
+
const alignmentPositions = (version) => {
|
|
64
|
+
if (version === 1)
|
|
65
|
+
return [];
|
|
66
|
+
const count = Math.floor(version / 7) + 2;
|
|
67
|
+
const step = Math.floor((version * 8 + count * 3 + 5) / (count * 4 - 4)) * 2;
|
|
68
|
+
const result = [6];
|
|
69
|
+
for (let pos = version * 4 + 10; result.length < count; pos -= step) {
|
|
70
|
+
result.splice(1, 0, pos);
|
|
71
|
+
}
|
|
72
|
+
return result;
|
|
73
|
+
};
|
|
74
|
+
const MASKS = [
|
|
75
|
+
(x, y) => (x + y) % 2 === 0,
|
|
76
|
+
(_x, y) => y % 2 === 0,
|
|
77
|
+
(x) => x % 3 === 0,
|
|
78
|
+
(x, y) => (x + y) % 3 === 0,
|
|
79
|
+
(x, y) => (Math.floor(x / 3) + Math.floor(y / 2)) % 2 === 0,
|
|
80
|
+
(x, y) => ((x * y) % 2) + ((x * y) % 3) === 0,
|
|
81
|
+
(x, y) => (((x * y) % 2) + ((x * y) % 3)) % 2 === 0,
|
|
82
|
+
(x, y) => (((x + y) % 2) + ((x * y) % 3)) % 2 === 0,
|
|
83
|
+
];
|
|
84
|
+
/** Encodes text as a QR Code at error correction level M. */
|
|
85
|
+
export function encodeQr(text) {
|
|
86
|
+
const bytes = new TextEncoder().encode(text);
|
|
87
|
+
let version = 1;
|
|
88
|
+
for (;; version++) {
|
|
89
|
+
if (version > 40)
|
|
90
|
+
throw new Error('Text is too long for a QR Code');
|
|
91
|
+
const countBits = version <= 9 ? 8 : 16;
|
|
92
|
+
if (bytes.length < 2 ** countBits &&
|
|
93
|
+
4 + countBits + bytes.length * 8 <= dataCodewords(version) * 8)
|
|
94
|
+
break;
|
|
95
|
+
}
|
|
96
|
+
// Data bits: byte mode, length, payload, terminator and padding.
|
|
97
|
+
const bits = [];
|
|
98
|
+
const append = (value, length) => {
|
|
99
|
+
for (let i = length - 1; i >= 0; i--)
|
|
100
|
+
bits.push((value >>> i) & 1);
|
|
101
|
+
};
|
|
102
|
+
append(0b0100, 4);
|
|
103
|
+
append(bytes.length, version <= 9 ? 8 : 16);
|
|
104
|
+
for (const byte of bytes)
|
|
105
|
+
append(byte, 8);
|
|
106
|
+
const capacity = dataCodewords(version) * 8;
|
|
107
|
+
append(0, Math.min(4, capacity - bits.length));
|
|
108
|
+
append(0, (8 - (bits.length % 8)) % 8);
|
|
109
|
+
for (let pad = 0xec; bits.length < capacity; pad ^= 0xec ^ 0x11) {
|
|
110
|
+
append(pad, 8);
|
|
111
|
+
}
|
|
112
|
+
const data = [];
|
|
113
|
+
for (let i = 0; i < bits.length; i += 8) {
|
|
114
|
+
data.push(bits.slice(i, i + 8).reduce((acc, b) => (acc << 1) | b, 0));
|
|
115
|
+
}
|
|
116
|
+
// Split into blocks, add error correction, interleave.
|
|
117
|
+
const blockCount = ECC_BLOCKS[version];
|
|
118
|
+
const eccLength = ECC_PER_BLOCK[version];
|
|
119
|
+
const rawCodewords = Math.floor(rawDataModules(version) / 8);
|
|
120
|
+
const shortBlocks = blockCount - (rawCodewords % blockCount);
|
|
121
|
+
const shortBlockLength = Math.floor(rawCodewords / blockCount);
|
|
122
|
+
const divisor = rsDivisor(eccLength);
|
|
123
|
+
const blocks = [];
|
|
124
|
+
for (let i = 0, k = 0; i < blockCount; i++) {
|
|
125
|
+
const length = shortBlockLength - eccLength + (i < shortBlocks ? 0 : 1);
|
|
126
|
+
const block = data.slice(k, k + length);
|
|
127
|
+
k += length;
|
|
128
|
+
const ecc = rsRemainder(block, divisor);
|
|
129
|
+
if (i < shortBlocks)
|
|
130
|
+
block.push(0);
|
|
131
|
+
blocks.push([...block, ...ecc]);
|
|
132
|
+
}
|
|
133
|
+
const codewords = [];
|
|
134
|
+
for (let i = 0; i < blocks[0].length; i++) {
|
|
135
|
+
blocks.forEach((block, j) => {
|
|
136
|
+
if (i !== shortBlockLength - eccLength || j >= shortBlocks) {
|
|
137
|
+
codewords.push(block[i]);
|
|
138
|
+
}
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
const size = version * 4 + 17;
|
|
142
|
+
const modules = Array.from({ length: size }, () => new Array(size).fill(false));
|
|
143
|
+
const reserved = Array.from({ length: size }, () => new Array(size).fill(false));
|
|
144
|
+
const setFunction = (x, y, dark) => {
|
|
145
|
+
modules[y][x] = dark;
|
|
146
|
+
reserved[y][x] = true;
|
|
147
|
+
};
|
|
148
|
+
for (let i = 0; i < size; i++) {
|
|
149
|
+
setFunction(6, i, i % 2 === 0);
|
|
150
|
+
setFunction(i, 6, i % 2 === 0);
|
|
151
|
+
}
|
|
152
|
+
for (const [cx, cy] of [
|
|
153
|
+
[3, 3],
|
|
154
|
+
[size - 4, 3],
|
|
155
|
+
[3, size - 4],
|
|
156
|
+
]) {
|
|
157
|
+
for (let dy = -4; dy <= 4; dy++) {
|
|
158
|
+
for (let dx = -4; dx <= 4; dx++) {
|
|
159
|
+
const x = cx + dx;
|
|
160
|
+
const y = cy + dy;
|
|
161
|
+
const distance = Math.max(Math.abs(dx), Math.abs(dy));
|
|
162
|
+
if (x >= 0 && x < size && y >= 0 && y < size) {
|
|
163
|
+
setFunction(x, y, distance !== 2 && distance !== 4);
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
const align = alignmentPositions(version);
|
|
169
|
+
align.forEach((ax, i) => {
|
|
170
|
+
align.forEach((ay, j) => {
|
|
171
|
+
const last = align.length - 1;
|
|
172
|
+
if ((i === 0 && j === 0) || (i === 0 && j === last) || (i === last && j === 0)) {
|
|
173
|
+
return;
|
|
174
|
+
}
|
|
175
|
+
for (let dy = -2; dy <= 2; dy++) {
|
|
176
|
+
for (let dx = -2; dx <= 2; dx++) {
|
|
177
|
+
setFunction(ax + dx, ay + dy, Math.max(Math.abs(dx), Math.abs(dy)) !== 1);
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
});
|
|
181
|
+
});
|
|
182
|
+
const drawFormat = (mask) => {
|
|
183
|
+
const value = (LEVEL_M_FORMAT_BITS << 3) | mask;
|
|
184
|
+
let remainder = value;
|
|
185
|
+
for (let i = 0; i < 10; i++) {
|
|
186
|
+
remainder = (remainder << 1) ^ ((remainder >>> 9) * 0x537);
|
|
187
|
+
}
|
|
188
|
+
const format = ((value << 10) | remainder) ^ 0x5412;
|
|
189
|
+
for (let i = 0; i <= 5; i++)
|
|
190
|
+
setFunction(8, i, bit(format, i));
|
|
191
|
+
setFunction(8, 7, bit(format, 6));
|
|
192
|
+
setFunction(8, 8, bit(format, 7));
|
|
193
|
+
setFunction(7, 8, bit(format, 8));
|
|
194
|
+
for (let i = 9; i < 15; i++)
|
|
195
|
+
setFunction(14 - i, 8, bit(format, i));
|
|
196
|
+
for (let i = 0; i < 8; i++)
|
|
197
|
+
setFunction(size - 1 - i, 8, bit(format, i));
|
|
198
|
+
for (let i = 8; i < 15; i++)
|
|
199
|
+
setFunction(8, size - 15 + i, bit(format, i));
|
|
200
|
+
setFunction(8, size - 8, true);
|
|
201
|
+
};
|
|
202
|
+
drawFormat(0);
|
|
203
|
+
if (version >= 7) {
|
|
204
|
+
let remainder = version;
|
|
205
|
+
for (let i = 0; i < 12; i++) {
|
|
206
|
+
remainder = (remainder << 1) ^ ((remainder >>> 11) * 0x1f25);
|
|
207
|
+
}
|
|
208
|
+
const info = (version << 12) | remainder;
|
|
209
|
+
for (let i = 0; i < 18; i++) {
|
|
210
|
+
const a = size - 11 + (i % 3);
|
|
211
|
+
const b = Math.floor(i / 3);
|
|
212
|
+
setFunction(a, b, bit(info, i));
|
|
213
|
+
setFunction(b, a, bit(info, i));
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
// Zigzag placement of the codewords.
|
|
217
|
+
let index = 0;
|
|
218
|
+
for (let right = size - 1; right >= 1; right -= 2) {
|
|
219
|
+
if (right === 6)
|
|
220
|
+
right = 5;
|
|
221
|
+
for (let vertical = 0; vertical < size; vertical++) {
|
|
222
|
+
for (let j = 0; j < 2; j++) {
|
|
223
|
+
const x = right - j;
|
|
224
|
+
const upward = ((right + 1) & 2) === 0;
|
|
225
|
+
const y = upward ? size - 1 - vertical : vertical;
|
|
226
|
+
if (!reserved[y][x] && index < codewords.length * 8) {
|
|
227
|
+
modules[y][x] = bit(codewords[index >>> 3], 7 - (index & 7));
|
|
228
|
+
index++;
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
const applyMask = (mask) => {
|
|
234
|
+
const invert = MASKS[mask];
|
|
235
|
+
for (let y = 0; y < size; y++) {
|
|
236
|
+
for (let x = 0; x < size; x++) {
|
|
237
|
+
if (!reserved[y][x] && invert(x, y))
|
|
238
|
+
modules[y][x] = !modules[y][x];
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
};
|
|
242
|
+
let bestMask = 0;
|
|
243
|
+
let bestPenalty = Infinity;
|
|
244
|
+
for (let mask = 0; mask < 8; mask++) {
|
|
245
|
+
applyMask(mask);
|
|
246
|
+
drawFormat(mask);
|
|
247
|
+
const score = penalty(modules);
|
|
248
|
+
if (score < bestPenalty) {
|
|
249
|
+
bestMask = mask;
|
|
250
|
+
bestPenalty = score;
|
|
251
|
+
}
|
|
252
|
+
applyMask(mask);
|
|
253
|
+
}
|
|
254
|
+
applyMask(bestMask);
|
|
255
|
+
drawFormat(bestMask);
|
|
256
|
+
return modules;
|
|
257
|
+
}
|
|
258
|
+
const FINDER_LIKE = [
|
|
259
|
+
[true, false, true, true, true, false, true, false, false, false, false],
|
|
260
|
+
[false, false, false, false, true, false, true, true, true, false, true],
|
|
261
|
+
];
|
|
262
|
+
/** The four penalty rules of ISO/IEC 18004 section 8.8.2. */
|
|
263
|
+
function penalty(modules) {
|
|
264
|
+
const size = modules.length;
|
|
265
|
+
const at = (x, y, vertical) => vertical ? modules[x][y] : modules[y][x];
|
|
266
|
+
let score = 0;
|
|
267
|
+
let dark = 0;
|
|
268
|
+
for (const vertical of [false, true]) {
|
|
269
|
+
for (let y = 0; y < size; y++) {
|
|
270
|
+
let run = 1;
|
|
271
|
+
for (let x = 1; x <= size; x++) {
|
|
272
|
+
if (x < size && at(x, y, vertical) === at(x - 1, y, vertical)) {
|
|
273
|
+
run++;
|
|
274
|
+
continue;
|
|
275
|
+
}
|
|
276
|
+
if (run >= 5)
|
|
277
|
+
score += run - 2;
|
|
278
|
+
run = 1;
|
|
279
|
+
}
|
|
280
|
+
for (let x = 0; x + 11 <= size; x++) {
|
|
281
|
+
for (const pattern of FINDER_LIKE) {
|
|
282
|
+
if (pattern.every((value, i) => at(x + i, y, vertical) === value)) {
|
|
283
|
+
score += 40;
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
for (let y = 0; y < size; y++) {
|
|
290
|
+
for (let x = 0; x < size; x++) {
|
|
291
|
+
const value = modules[y][x];
|
|
292
|
+
if (value)
|
|
293
|
+
dark++;
|
|
294
|
+
if (x + 1 < size &&
|
|
295
|
+
y + 1 < size &&
|
|
296
|
+
value === modules[y][x + 1] &&
|
|
297
|
+
value === modules[y + 1][x] &&
|
|
298
|
+
value === modules[y + 1][x + 1]) {
|
|
299
|
+
score += 3;
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
const total = size * size;
|
|
304
|
+
score += (Math.ceil(Math.abs(dark * 20 - total * 10) / total) - 1) * 10;
|
|
305
|
+
return score;
|
|
306
|
+
}
|
|
307
|
+
/**
|
|
308
|
+
* Renders a QR Code with half-block characters, two module rows per line,
|
|
309
|
+
* inside a quiet zone. With `color`, it forces black on white so the code
|
|
310
|
+
* scans on dark terminal themes too.
|
|
311
|
+
*/
|
|
312
|
+
export function renderQr(matrix, options = {}) {
|
|
313
|
+
const margin = options.margin ?? 4;
|
|
314
|
+
const size = matrix.length;
|
|
315
|
+
const dark = (x, y) => x >= 0 && y >= 0 && x < size && y < size && matrix[y][x];
|
|
316
|
+
const lines = [];
|
|
317
|
+
for (let y = -margin; y < size + margin; y += 2) {
|
|
318
|
+
let line = '';
|
|
319
|
+
for (let x = -margin; x < size + margin; x++) {
|
|
320
|
+
const top = dark(x, y);
|
|
321
|
+
const bottom = dark(x, y + 1);
|
|
322
|
+
line += top ? (bottom ? '█' : '▀') : bottom ? '▄' : ' ';
|
|
323
|
+
}
|
|
324
|
+
lines.push(options.color ? `\x1b[30;107m${line}\x1b[0m` : line);
|
|
325
|
+
}
|
|
326
|
+
return lines.join('\n');
|
|
327
|
+
}
|