@barnaby.build/barnaby 0.0.233 → 0.0.234

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,7 +1,7 @@
1
1
  {
2
2
  "name": "@barnaby.build/barnaby",
3
3
  "productName": "Barnaby",
4
- "version": "0.0.233",
4
+ "version": "0.0.234",
5
5
  "main": "dist-electron/main/index.js",
6
6
  "bin": {
7
7
  "barnaby": "bin/barnaby.cjs"
@@ -1,65 +1,65 @@
1
- <!DOCTYPE html>
2
- <html lang="en">
3
- <head>
4
- <meta charset="UTF-8">
5
- <meta name="viewport" content="width=device-width, initial-scale=1">
6
- <title>Barnaby Debug Output</title>
7
- <style>
8
- * { box-sizing: border-box; }
9
- html, body { margin: 0; height: 100%; font-family: 'Consolas', 'Monaco', monospace; font-size: 12px; background: #1e1e1e; color: #d4d4d4; overflow: hidden; }
10
- #toolbar { padding: 6px 8px; background: #252526; border-bottom: 1px solid #3c3c3c; display: flex; align-items: center; gap: 8px; }
11
- #toolbar button { padding: 4px 10px; font-size: 11px; cursor: pointer; background: #0e639c; color: white; border: none; border-radius: 2px; }
12
- #toolbar button:hover { background: #1177bb; }
13
- #toolbar button.secondary { background: #3c3c3c; }
14
- #toolbar button.secondary:hover { background: #505050; }
15
- #log { padding: 8px; height: calc(100% - 36px); overflow: auto; white-space: pre-wrap; word-wrap: break-word; }
16
- .error { color: #f48771; }
17
- .warn { color: #dcdcaa; }
18
- .info { color: #9cdcfe; }
19
- </style>
20
- </head>
21
- <body>
22
- <div id="toolbar">
23
- <button id="refresh">Refresh</button>
24
- <button id="clear" class="secondary">Clear view</button>
25
- <span id="status" style="font-size: 11px; color: #858585;"></span>
26
- </div>
27
- <pre id="log"></pre>
28
- <script>
29
- const logEl = document.getElementById('log');
30
- const statusEl = document.getElementById('status');
31
- function render(content) {
32
- const lines = (content || '').split('\n');
33
- logEl.innerHTML = lines.map(l => {
34
- if (l.includes('[ERROR]')) return '<span class="error">' + escapeHtml(l) + '</span>';
35
- if (l.includes('[WARN]')) return '<span class="warn">' + escapeHtml(l) + '</span>';
36
- return '<span class="info">' + escapeHtml(l) + '</span>';
37
- }).join('\n');
38
- logEl.scrollTop = logEl.scrollHeight;
39
- }
40
- function escapeHtml(s) { return s.replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;'); }
41
- async function load() {
42
- try {
43
- const r = await (window.api && window.api.getDebugLogContent ? window.api.getDebugLogContent() : Promise.resolve({ ok: false, content: '' }));
44
- render(r.ok ? r.content : '');
45
- statusEl.textContent = r.ok ? 'Live' : 'No API';
46
- } catch (e) {
47
- statusEl.textContent = 'Error: ' + e.message;
48
- }
49
- }
50
- document.getElementById('refresh').onclick = load;
51
- document.getElementById('clear').onclick = () => { logEl.innerHTML = ''; statusEl.textContent = 'Cleared'; };
52
- if (window.api && window.api.onDebugLogAppend) {
53
- window.api.onDebugLogAppend((line) => {
54
- const span = document.createElement('span');
55
- span.className = line.includes('[ERROR]') ? 'error' : line.includes('[WARN]') ? 'warn' : 'info';
56
- span.textContent = line + '\n';
57
- logEl.appendChild(span);
58
- logEl.scrollTop = logEl.scrollHeight;
59
- });
60
- }
61
- load();
62
- setInterval(load, 2000);
63
- </script>
64
- </body>
65
- </html>
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1">
6
+ <title>Barnaby Debug Output</title>
7
+ <style>
8
+ * { box-sizing: border-box; }
9
+ html, body { margin: 0; height: 100%; font-family: 'Consolas', 'Monaco', monospace; font-size: 12px; background: #1e1e1e; color: #d4d4d4; overflow: hidden; }
10
+ #toolbar { padding: 6px 8px; background: #252526; border-bottom: 1px solid #3c3c3c; display: flex; align-items: center; gap: 8px; }
11
+ #toolbar button { padding: 4px 10px; font-size: 11px; cursor: pointer; background: #0e639c; color: white; border: none; border-radius: 2px; }
12
+ #toolbar button:hover { background: #1177bb; }
13
+ #toolbar button.secondary { background: #3c3c3c; }
14
+ #toolbar button.secondary:hover { background: #505050; }
15
+ #log { padding: 8px; height: calc(100% - 36px); overflow: auto; white-space: pre-wrap; word-wrap: break-word; }
16
+ .error { color: #f48771; }
17
+ .warn { color: #dcdcaa; }
18
+ .info { color: #9cdcfe; }
19
+ </style>
20
+ </head>
21
+ <body>
22
+ <div id="toolbar">
23
+ <button id="refresh">Refresh</button>
24
+ <button id="clear" class="secondary">Clear view</button>
25
+ <span id="status" style="font-size: 11px; color: #858585;"></span>
26
+ </div>
27
+ <pre id="log"></pre>
28
+ <script>
29
+ const logEl = document.getElementById('log');
30
+ const statusEl = document.getElementById('status');
31
+ function render(content) {
32
+ const lines = (content || '').split('\n');
33
+ logEl.innerHTML = lines.map(l => {
34
+ if (l.includes('[ERROR]')) return '<span class="error">' + escapeHtml(l) + '</span>';
35
+ if (l.includes('[WARN]')) return '<span class="warn">' + escapeHtml(l) + '</span>';
36
+ return '<span class="info">' + escapeHtml(l) + '</span>';
37
+ }).join('\n');
38
+ logEl.scrollTop = logEl.scrollHeight;
39
+ }
40
+ function escapeHtml(s) { return s.replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;'); }
41
+ async function load() {
42
+ try {
43
+ const r = await (window.api && window.api.getDebugLogContent ? window.api.getDebugLogContent() : Promise.resolve({ ok: false, content: '' }));
44
+ render(r.ok ? r.content : '');
45
+ statusEl.textContent = r.ok ? 'Live' : 'No API';
46
+ } catch (e) {
47
+ statusEl.textContent = 'Error: ' + e.message;
48
+ }
49
+ }
50
+ document.getElementById('refresh').onclick = load;
51
+ document.getElementById('clear').onclick = () => { logEl.innerHTML = ''; statusEl.textContent = 'Cleared'; };
52
+ if (window.api && window.api.onDebugLogAppend) {
53
+ window.api.onDebugLogAppend((line) => {
54
+ const span = document.createElement('span');
55
+ span.className = line.includes('[ERROR]') ? 'error' : line.includes('[WARN]') ? 'warn' : 'info';
56
+ span.textContent = line + '\n';
57
+ logEl.appendChild(span);
58
+ logEl.scrollTop = logEl.scrollHeight;
59
+ });
60
+ }
61
+ load();
62
+ setInterval(load, 2000);
63
+ </script>
64
+ </body>
65
+ </html>
@@ -1,46 +1,46 @@
1
- <!doctype html>
2
- <html lang="en">
3
- <head>
4
- <meta charset="utf-8" />
5
- <meta name="viewport" content="width=device-width, initial-scale=1" />
6
- <title>Barnaby Splash</title>
7
- <style>
8
- html, body {
9
- margin: 0;
10
- width: 100%;
11
- height: 100%;
12
- background: #0b0b0b;
13
- overflow: hidden;
14
- }
15
- .root {
16
- width: 100%;
17
- height: 100%;
18
- display: flex;
19
- align-items: center;
20
- justify-content: center;
21
- }
22
- img {
23
- max-width: 90%;
24
- max-height: 90%;
25
- object-fit: contain;
26
- user-select: none;
27
- -webkit-user-drag: none;
28
- }
29
- .version {
30
- position: fixed;
31
- bottom: 8px;
32
- right: 12px;
33
- font-size: 11px;
34
- color: white;
35
- font-family: system-ui, sans-serif;
36
- opacity: 0.8;
37
- }
38
- </style>
39
- </head>
40
- <body>
41
- <div class="root">
42
- <img src="./splash.png" alt="Barnaby splash" />
43
- </div>
44
- <div id="version" class="version"></div>
45
- </body>
46
- </html>
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="utf-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
6
+ <title>Barnaby Splash</title>
7
+ <style>
8
+ html, body {
9
+ margin: 0;
10
+ width: 100%;
11
+ height: 100%;
12
+ background: #0b0b0b;
13
+ overflow: hidden;
14
+ }
15
+ .root {
16
+ width: 100%;
17
+ height: 100%;
18
+ display: flex;
19
+ align-items: center;
20
+ justify-content: center;
21
+ }
22
+ img {
23
+ max-width: 90%;
24
+ max-height: 90%;
25
+ object-fit: contain;
26
+ user-select: none;
27
+ -webkit-user-drag: none;
28
+ }
29
+ .version {
30
+ position: fixed;
31
+ bottom: 8px;
32
+ right: 12px;
33
+ font-size: 11px;
34
+ color: white;
35
+ font-family: system-ui, sans-serif;
36
+ opacity: 0.8;
37
+ }
38
+ </style>
39
+ </head>
40
+ <body>
41
+ <div class="root">
42
+ <img src="./splash.png" alt="Barnaby splash" />
43
+ </div>
44
+ <div id="version" class="version"></div>
45
+ </body>
46
+ </html>
@@ -1,35 +1,35 @@
1
- import fs from 'node:fs'
2
- import path from 'node:path'
3
-
4
- const root = process.cwd()
5
- const packageJsonPath = path.join(root, 'package.json')
6
- const packageLockPath = path.join(root, 'package-lock.json')
7
-
8
- function bumpPatch(version) {
9
- const parts = String(version).trim().split('.')
10
- if (parts.length !== 3) {
11
- throw new Error(`Expected semantic version x.y.z, got "${version}"`)
12
- }
13
- const [major, minor, patch] = parts.map((p) => Number(p))
14
- if ([major, minor, patch].some((n) => Number.isNaN(n))) {
15
- throw new Error(`Expected numeric semantic version, got "${version}"`)
16
- }
17
- return `${major}.${minor}.${patch + 1}`
18
- }
19
-
20
- const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8'))
21
- const currentVersion = packageJson.version
22
- const nextVersion = bumpPatch(currentVersion)
23
- packageJson.version = nextVersion
24
- fs.writeFileSync(packageJsonPath, `${JSON.stringify(packageJson, null, 2)}\n`, 'utf8')
25
-
26
- if (fs.existsSync(packageLockPath)) {
27
- const packageLock = JSON.parse(fs.readFileSync(packageLockPath, 'utf8'))
28
- if (typeof packageLock.version === 'string') packageLock.version = nextVersion
29
- if (packageLock.packages && packageLock.packages[''] && typeof packageLock.packages[''].version === 'string') {
30
- packageLock.packages[''].version = nextVersion
31
- }
32
- fs.writeFileSync(packageLockPath, `${JSON.stringify(packageLock, null, 2)}\n`, 'utf8')
33
- }
34
-
35
- console.log(`Version bumped: ${currentVersion} -> ${nextVersion}`)
1
+ import fs from 'node:fs'
2
+ import path from 'node:path'
3
+
4
+ const root = process.cwd()
5
+ const packageJsonPath = path.join(root, 'package.json')
6
+ const packageLockPath = path.join(root, 'package-lock.json')
7
+
8
+ function bumpPatch(version) {
9
+ const parts = String(version).trim().split('.')
10
+ if (parts.length !== 3) {
11
+ throw new Error(`Expected semantic version x.y.z, got "${version}"`)
12
+ }
13
+ const [major, minor, patch] = parts.map((p) => Number(p))
14
+ if ([major, minor, patch].some((n) => Number.isNaN(n))) {
15
+ throw new Error(`Expected numeric semantic version, got "${version}"`)
16
+ }
17
+ return `${major}.${minor}.${patch + 1}`
18
+ }
19
+
20
+ const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8'))
21
+ const currentVersion = packageJson.version
22
+ const nextVersion = bumpPatch(currentVersion)
23
+ packageJson.version = nextVersion
24
+ fs.writeFileSync(packageJsonPath, `${JSON.stringify(packageJson, null, 2)}\n`, 'utf8')
25
+
26
+ if (fs.existsSync(packageLockPath)) {
27
+ const packageLock = JSON.parse(fs.readFileSync(packageLockPath, 'utf8'))
28
+ if (typeof packageLock.version === 'string') packageLock.version = nextVersion
29
+ if (packageLock.packages && packageLock.packages[''] && typeof packageLock.packages[''].version === 'string') {
30
+ packageLock.packages[''].version = nextVersion
31
+ }
32
+ fs.writeFileSync(packageLockPath, `${JSON.stringify(packageLock, null, 2)}\n`, 'utf8')
33
+ }
34
+
35
+ console.log(`Version bumped: ${currentVersion} -> ${nextVersion}`)
@@ -1,47 +1,47 @@
1
- import fs from 'node:fs'
2
- import path from 'node:path'
3
-
4
- const root = process.cwd()
5
- const packageJsonPath = path.join(root, 'package.json')
6
- const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8'))
7
- const version = String(packageJson.version || '').trim()
8
-
9
- if (!version) {
10
- throw new Error('package.json version is missing.')
11
- }
12
-
13
- const notesFilename = `RELEASE_NOTES_${version}.md`
14
- const notesPath = path.join(root, notesFilename)
15
-
16
- if (fs.existsSync(notesPath)) {
17
- console.log(`Release notes already exist: ${notesFilename}`)
18
- process.exit(0)
19
- }
20
-
21
- const now = new Date()
22
- const month = now.toLocaleString('en-US', { month: 'long' })
23
- const year = now.getFullYear()
24
-
25
- const template = `# Barnaby ${version} - Release Notes
26
-
27
- **Released:** ${month} ${year}
28
-
29
- ## Added
30
-
31
- - TODO
32
-
33
- ## Changed
34
-
35
- - TODO
36
-
37
- ## Fixed
38
-
39
- - TODO
40
-
41
- ## Notes
42
-
43
- - Artifact: \`release/${version}/Barnaby_${version}_portable.exe\`
44
- `
45
-
46
- fs.writeFileSync(notesPath, template, 'utf8')
47
- console.log(`Created ${notesFilename}`)
1
+ import fs from 'node:fs'
2
+ import path from 'node:path'
3
+
4
+ const root = process.cwd()
5
+ const packageJsonPath = path.join(root, 'package.json')
6
+ const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8'))
7
+ const version = String(packageJson.version || '').trim()
8
+
9
+ if (!version) {
10
+ throw new Error('package.json version is missing.')
11
+ }
12
+
13
+ const notesFilename = `RELEASE_NOTES_${version}.md`
14
+ const notesPath = path.join(root, notesFilename)
15
+
16
+ if (fs.existsSync(notesPath)) {
17
+ console.log(`Release notes already exist: ${notesFilename}`)
18
+ process.exit(0)
19
+ }
20
+
21
+ const now = new Date()
22
+ const month = now.toLocaleString('en-US', { month: 'long' })
23
+ const year = now.getFullYear()
24
+
25
+ const template = `# Barnaby ${version} - Release Notes
26
+
27
+ **Released:** ${month} ${year}
28
+
29
+ ## Added
30
+
31
+ - TODO
32
+
33
+ ## Changed
34
+
35
+ - TODO
36
+
37
+ ## Fixed
38
+
39
+ - TODO
40
+
41
+ ## Notes
42
+
43
+ - Artifact: \`release/${version}/Barnaby_${version}_portable.exe\`
44
+ `
45
+
46
+ fs.writeFileSync(notesPath, template, 'utf8')
47
+ console.log(`Created ${notesFilename}`)
@@ -1,99 +1,99 @@
1
- #!/usr/bin/env node
2
- /**
3
- * Generate a license key for a customer.
4
- *
5
- * Usage:
6
- * node scripts/generate-license-key.mjs --email customer@example.com
7
- * node scripts/generate-license-key.mjs --email customer@example.com --tier pro
8
- * node scripts/generate-license-key.mjs --email customer@example.com --exp 2027-01-01
9
- *
10
- * Options:
11
- * --email Customer email (required)
12
- * --tier License tier: pro, enterprise (default: pro)
13
- * --exp Expiry date in YYYY-MM-DD format (optional, omit for perpetual)
14
- *
15
- * Reads the private key from: ~/.barnaby/license-private-key.pem
16
- * Run license-keypair-init.mjs first if you haven't generated a keypair.
17
- */
18
-
19
- import crypto from 'node:crypto'
20
- import fs from 'node:fs'
21
- import path from 'node:path'
22
- import os from 'node:os'
23
-
24
- const PRIVATE_KEY_PATH = path.join(os.homedir(), '.barnaby', 'license-private-key.pem')
25
-
26
- // Parse args
27
- const args = process.argv.slice(2)
28
- function getArg(name) {
29
- const idx = args.indexOf(`--${name}`)
30
- if (idx === -1 || idx + 1 >= args.length) return undefined
31
- return args[idx + 1]
32
- }
33
-
34
- const email = getArg('email')
35
- const tier = getArg('tier') || 'pro'
36
- const expStr = getArg('exp')
37
-
38
- if (!email) {
39
- console.error('❌ Missing --email argument')
40
- console.error('Usage: node scripts/generate-license-key.mjs --email customer@example.com')
41
- process.exit(1)
42
- }
43
-
44
- // Read private key
45
- if (!fs.existsSync(PRIVATE_KEY_PATH)) {
46
- console.error('❌ Private key not found at:', PRIVATE_KEY_PATH)
47
- console.error(' Run: node scripts/license-keypair-init.mjs')
48
- process.exit(1)
49
- }
50
-
51
- const privateKeyPem = fs.readFileSync(PRIVATE_KEY_PATH, 'utf8')
52
- const privateKey = crypto.createPrivateKey(privateKeyPem)
53
-
54
- // Build payload
55
- const payload = {
56
- email: email.toLowerCase().trim(),
57
- product: 'orchestrator',
58
- tier,
59
- iat: Date.now(),
60
- }
61
-
62
- if (expStr) {
63
- const expDate = new Date(expStr)
64
- if (isNaN(expDate.getTime())) {
65
- console.error('❌ Invalid --exp date format. Use YYYY-MM-DD.')
66
- process.exit(1)
67
- }
68
- payload.exp = expDate.getTime()
69
- }
70
-
71
- // Encode payload
72
- const payloadBuf = Buffer.from(JSON.stringify(payload), 'utf8')
73
-
74
- // Sign
75
- const signature = crypto.sign(null, payloadBuf, privateKey)
76
-
77
- // Base64url encode (no padding)
78
- function base64urlEncode(buf) {
79
- return buf.toString('base64').replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '')
80
- }
81
-
82
- const key = `ORCH-${base64urlEncode(payloadBuf)}.${base64urlEncode(signature)}`
83
-
84
- console.log()
85
- console.log('✅ License key generated for:', email)
86
- console.log()
87
- console.log('Details:')
88
- console.log(' Email:', payload.email)
89
- console.log(' Tier:', payload.tier)
90
- console.log(' Issued:', new Date(payload.iat).toISOString())
91
- console.log(' Expires:', payload.exp ? new Date(payload.exp).toISOString() : 'Never (perpetual)')
92
- console.log()
93
- console.log('License Key:')
94
- console.log('─'.repeat(60))
95
- console.log(key)
96
- console.log('─'.repeat(60))
97
- console.log()
98
- console.log('Send this key to the customer. They paste it into')
99
- console.log('Settings → Orchestrator → License Key.')
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Generate a license key for a customer.
4
+ *
5
+ * Usage:
6
+ * node scripts/generate-license-key.mjs --email customer@example.com
7
+ * node scripts/generate-license-key.mjs --email customer@example.com --tier pro
8
+ * node scripts/generate-license-key.mjs --email customer@example.com --exp 2027-01-01
9
+ *
10
+ * Options:
11
+ * --email Customer email (required)
12
+ * --tier License tier: pro, enterprise (default: pro)
13
+ * --exp Expiry date in YYYY-MM-DD format (optional, omit for perpetual)
14
+ *
15
+ * Reads the private key from: ~/.barnaby/license-private-key.pem
16
+ * Run license-keypair-init.mjs first if you haven't generated a keypair.
17
+ */
18
+
19
+ import crypto from 'node:crypto'
20
+ import fs from 'node:fs'
21
+ import path from 'node:path'
22
+ import os from 'node:os'
23
+
24
+ const PRIVATE_KEY_PATH = path.join(os.homedir(), '.barnaby', 'license-private-key.pem')
25
+
26
+ // Parse args
27
+ const args = process.argv.slice(2)
28
+ function getArg(name) {
29
+ const idx = args.indexOf(`--${name}`)
30
+ if (idx === -1 || idx + 1 >= args.length) return undefined
31
+ return args[idx + 1]
32
+ }
33
+
34
+ const email = getArg('email')
35
+ const tier = getArg('tier') || 'pro'
36
+ const expStr = getArg('exp')
37
+
38
+ if (!email) {
39
+ console.error('❌ Missing --email argument')
40
+ console.error('Usage: node scripts/generate-license-key.mjs --email customer@example.com')
41
+ process.exit(1)
42
+ }
43
+
44
+ // Read private key
45
+ if (!fs.existsSync(PRIVATE_KEY_PATH)) {
46
+ console.error('❌ Private key not found at:', PRIVATE_KEY_PATH)
47
+ console.error(' Run: node scripts/license-keypair-init.mjs')
48
+ process.exit(1)
49
+ }
50
+
51
+ const privateKeyPem = fs.readFileSync(PRIVATE_KEY_PATH, 'utf8')
52
+ const privateKey = crypto.createPrivateKey(privateKeyPem)
53
+
54
+ // Build payload
55
+ const payload = {
56
+ email: email.toLowerCase().trim(),
57
+ product: 'orchestrator',
58
+ tier,
59
+ iat: Date.now(),
60
+ }
61
+
62
+ if (expStr) {
63
+ const expDate = new Date(expStr)
64
+ if (isNaN(expDate.getTime())) {
65
+ console.error('❌ Invalid --exp date format. Use YYYY-MM-DD.')
66
+ process.exit(1)
67
+ }
68
+ payload.exp = expDate.getTime()
69
+ }
70
+
71
+ // Encode payload
72
+ const payloadBuf = Buffer.from(JSON.stringify(payload), 'utf8')
73
+
74
+ // Sign
75
+ const signature = crypto.sign(null, payloadBuf, privateKey)
76
+
77
+ // Base64url encode (no padding)
78
+ function base64urlEncode(buf) {
79
+ return buf.toString('base64').replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '')
80
+ }
81
+
82
+ const key = `ORCH-${base64urlEncode(payloadBuf)}.${base64urlEncode(signature)}`
83
+
84
+ console.log()
85
+ console.log('✅ License key generated for:', email)
86
+ console.log()
87
+ console.log('Details:')
88
+ console.log(' Email:', payload.email)
89
+ console.log(' Tier:', payload.tier)
90
+ console.log(' Issued:', new Date(payload.iat).toISOString())
91
+ console.log(' Expires:', payload.exp ? new Date(payload.exp).toISOString() : 'Never (perpetual)')
92
+ console.log()
93
+ console.log('License Key:')
94
+ console.log('─'.repeat(60))
95
+ console.log(key)
96
+ console.log('─'.repeat(60))
97
+ console.log()
98
+ console.log('Send this key to the customer. They paste it into')
99
+ console.log('Settings → Orchestrator → License Key.')