@textcortex/zenocode 0.1.14 → 0.1.16
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 +36 -1
- package/package.json +1 -1
- package/scripts/build-branded-opencode.mjs +292 -215
- package/scripts/build-branded-opencode.test.mjs +85 -59
- package/scripts/opencode-version.mjs +5 -3
- package/scripts/run-zenocode.mjs +600 -763
- package/scripts/run-zenocode.test.mjs +625 -430
- package/scripts/source-branding.mjs +721 -0
- package/scripts/source-branding.test.mjs +106 -0
- package/scripts/branding-patch.mjs +0 -193
- package/scripts/branding-patch.test.mjs +0 -89
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import assert from 'node:assert/strict';
|
|
2
|
+
import test from 'node:test';
|
|
3
|
+
|
|
4
|
+
import {
|
|
5
|
+
brandOpenCodeSourceFile,
|
|
6
|
+
openCodeServerUpgradeSource,
|
|
7
|
+
openCodeUpdatePromptSource,
|
|
8
|
+
sourceBrandingFiles,
|
|
9
|
+
} from './source-branding.mjs';
|
|
10
|
+
|
|
11
|
+
test('source branding replaces the terminal logo before compilation', () => {
|
|
12
|
+
const source = `export const logo = {
|
|
13
|
+
left: [" ", "█▀▀█ █▀▀█ █▀▀█ █▀▀▄", "█__█ █__█ █^^^ █__█", "▀▀▀▀ █▀▀▀ ▀▀▀▀ ▀~~▀"],
|
|
14
|
+
right: [" ▄ ", "█▀▀▀ █▀▀█ █▀▀█ █▀▀█", "█___ █__█ █__█ █^^^", "▀▀▀▀ ▀▀▀▀ ▀▀▀▀ ▀▀▀▀"],
|
|
15
|
+
}
|
|
16
|
+
`;
|
|
17
|
+
|
|
18
|
+
const branded = brandOpenCodeSourceFile('packages/tui/src/logo.ts', source);
|
|
19
|
+
|
|
20
|
+
assert.match(branded, /▀▀█ █▀▀ █▄ █ █▀█/);
|
|
21
|
+
assert.match(branded, /█▀▀ █▀█ █▀▄ █▀▀/);
|
|
22
|
+
assert.doesNotMatch(branded, /█▀▀█ █▀▀█ █▀▀█ █▀▀▄/);
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
test('source branding replaces visible terminal product names', () => {
|
|
26
|
+
const source = `
|
|
27
|
+
renderer.setTerminalTitle("OpenCode")
|
|
28
|
+
renderer.setTerminalTitle("OpenCode")
|
|
29
|
+
renderer.setTerminalTitle(\`OC | \${title}\`)
|
|
30
|
+
renderer.setTerminalTitle(\`OC | \${route.data.id}\`)
|
|
31
|
+
${openCodeUpdatePromptSource}`;
|
|
32
|
+
|
|
33
|
+
const branded = brandOpenCodeSourceFile('packages/tui/src/app.tsx', source);
|
|
34
|
+
|
|
35
|
+
assert.match(branded, /setTerminalTitle\("Zenocode"\)/);
|
|
36
|
+
assert.match(branded, /setTerminalTitle\(`ZC \| \$\{title\}`\)/);
|
|
37
|
+
assert.doesNotMatch(branded, /OpenCode/);
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
test('source branding removes the in-app update prompt', () => {
|
|
41
|
+
const branded = brandOpenCodeSourceFile(
|
|
42
|
+
'packages/tui/src/app.tsx',
|
|
43
|
+
`renderer.setTerminalTitle("OpenCode")
|
|
44
|
+
renderer.setTerminalTitle("OpenCode")
|
|
45
|
+
renderer.setTerminalTitle(\`OC | \${title}\`)
|
|
46
|
+
renderer.setTerminalTitle(\`OC | \${route.data.id}\`)
|
|
47
|
+
${openCodeUpdatePromptSource}`
|
|
48
|
+
);
|
|
49
|
+
|
|
50
|
+
assert.match(
|
|
51
|
+
branded,
|
|
52
|
+
/event\.on\("installation\.update-available", \(\) => \{\}\)/
|
|
53
|
+
);
|
|
54
|
+
assert.doesNotMatch(branded, /Update Available/);
|
|
55
|
+
assert.doesNotMatch(branded, /global\.upgrade/);
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
test('source branding refuses runtime upgrades through the server API', () => {
|
|
59
|
+
const branded = brandOpenCodeSourceFile(
|
|
60
|
+
'packages/opencode/src/server/routes/instance/httpapi/handlers/global.ts',
|
|
61
|
+
openCodeServerUpgradeSource
|
|
62
|
+
);
|
|
63
|
+
|
|
64
|
+
assert.match(branded, /Zenocode manages runtime updates/);
|
|
65
|
+
assert.match(branded, /status: 400/);
|
|
66
|
+
assert.doesNotMatch(branded, /installation\.upgrade\(/);
|
|
67
|
+
assert.doesNotMatch(branded, /installation\.latest\(/);
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
test('source branding replaces the terminal network identity', () => {
|
|
71
|
+
const source = `
|
|
72
|
+
describe: "custom domain name for mDNS service (default: opencode.local)",
|
|
73
|
+
default: "opencode.local",
|
|
74
|
+
`;
|
|
75
|
+
|
|
76
|
+
const branded = brandOpenCodeSourceFile(
|
|
77
|
+
'packages/opencode/src/cli/network.ts',
|
|
78
|
+
source
|
|
79
|
+
);
|
|
80
|
+
|
|
81
|
+
assert.match(branded, /zenocode\.local/);
|
|
82
|
+
assert.doesNotMatch(branded, /opencode\.local/);
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
test('source branding fails closed when a pinned upstream surface drifts', () => {
|
|
86
|
+
assert.throws(
|
|
87
|
+
() =>
|
|
88
|
+
brandOpenCodeSourceFile(
|
|
89
|
+
'packages/tui/src/logo.ts',
|
|
90
|
+
'export const logo = { left: [], right: [] }\n'
|
|
91
|
+
),
|
|
92
|
+
/Upstream branding surface drifted/
|
|
93
|
+
);
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
test('source branding owns every terminal-facing source file', () => {
|
|
97
|
+
assert.ok(
|
|
98
|
+
sourceBrandingFiles.includes('packages/opencode/src/cli/upgrade.ts')
|
|
99
|
+
);
|
|
100
|
+
assert.ok(sourceBrandingFiles.includes('packages/opencode/src/cli/ui.ts'));
|
|
101
|
+
assert.ok(sourceBrandingFiles.includes('packages/opencode/src/index.ts'));
|
|
102
|
+
assert.ok(sourceBrandingFiles.includes('packages/tui/src/app.tsx'));
|
|
103
|
+
assert.ok(
|
|
104
|
+
sourceBrandingFiles.includes('packages/tui/src/routes/session/sidebar.tsx')
|
|
105
|
+
);
|
|
106
|
+
});
|
|
@@ -1,193 +0,0 @@
|
|
|
1
|
-
export const zenocodeLogo = {
|
|
2
|
-
left: [
|
|
3
|
-
" ",
|
|
4
|
-
" \\u2580\\u2580\\u2588 \\u2588\\u2580\\u2580 \\u2588\\u2584 \\u2588 \\u2588\\u2580\\u2588",
|
|
5
|
-
" \\u2588\\u2584\\u2584 \\u2588\\u2588\\u2584 \\u2588 \\u2580\\u2588 \\u2588\\u2584\\u2588",
|
|
6
|
-
" ",
|
|
7
|
-
],
|
|
8
|
-
right: [
|
|
9
|
-
" ",
|
|
10
|
-
" \\u2588\\u2580\\u2580 \\u2588\\u2580\\u2588 \\u2588\\u2580\\u2584 \\u2588\\u2580\\u2580",
|
|
11
|
-
" \\u2588\\u2584\\u2584 \\u2588\\u2584\\u2588 \\u2588\\u2584\\u2580 \\u2588\\u2588\\u2584",
|
|
12
|
-
" ",
|
|
13
|
-
],
|
|
14
|
-
};
|
|
15
|
-
|
|
16
|
-
const opencodeLogoSnippet = `var logo = {
|
|
17
|
-
left: [" ", "\\u2588\\u2580\\u2580\\u2588 \\u2588\\u2580\\u2580\\u2588 \\u2588\\u2580\\u2580\\u2588 \\u2588\\u2580\\u2580\\u2584", "\\u2588__\\u2588 \\u2588__\\u2588 \\u2588^^^ \\u2588__\\u2588", "\\u2580\\u2580\\u2580\\u2580 \\u2588\\u2580\\u2580\\u2580 \\u2580\\u2580\\u2580\\u2580 \\u2580~~\\u2580"],
|
|
18
|
-
right: [" \\u2584 ", "\\u2588\\u2580\\u2580\\u2580 \\u2588\\u2580\\u2580\\u2588 \\u2588\\u2580\\u2580\\u2588 \\u2588\\u2580\\u2580\\u2588", "\\u2588___ \\u2588__\\u2588 \\u2588__\\u2588 \\u2588^^^", "\\u2580\\u2580\\u2580\\u2580 \\u2580\\u2580\\u2580\\u2580 \\u2580\\u2580\\u2580\\u2580 \\u2580\\u2580\\u2580\\u2580"]
|
|
19
|
-
};
|
|
20
|
-
var marks = "_^~";`;
|
|
21
|
-
|
|
22
|
-
const openCodeVersionFooterSnippetPattern =
|
|
23
|
-
/insertNode\(([$\w]+), createTextNode\(`Open`\)\);\s*insertNode\(([$\w]+), ([$\w]+)\);\s*insertNode\(\3, createTextNode\(`Code`\)\);\s*((?:insert\d*))\(([$\w]+), \(\) => Installation\.VERSION\);/g;
|
|
24
|
-
|
|
25
|
-
export function padBinaryReplacement(target, replacement) {
|
|
26
|
-
if (replacement.length > target.length) {
|
|
27
|
-
return null;
|
|
28
|
-
}
|
|
29
|
-
const paddingLength = target.length - replacement.length;
|
|
30
|
-
if (paddingLength === 0) {
|
|
31
|
-
return replacement;
|
|
32
|
-
}
|
|
33
|
-
return `${replacement}${" ".repeat(paddingLength)}`;
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
function _buildLogoSourceArray(lines) {
|
|
37
|
-
return `[${lines.map((line) => `"${line.replaceAll('"', '\\"')}"`).join(", ")}]`;
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
const doubleEscapedZenocodeTextLogoSnippetCore = `var logo = {
|
|
41
|
-
left: ${JSON.stringify(zenocodeLogo.left)},
|
|
42
|
-
right: ${JSON.stringify(zenocodeLogo.right)}
|
|
43
|
-
};
|
|
44
|
-
var marks = "_^~";`;
|
|
45
|
-
|
|
46
|
-
const zenocodeTextLogoSnippetCore = `var logo = {
|
|
47
|
-
left: ${_buildLogoSourceArray(zenocodeLogo.left)},
|
|
48
|
-
right: ${_buildLogoSourceArray(zenocodeLogo.right)}
|
|
49
|
-
};
|
|
50
|
-
var marks = "_^~";`;
|
|
51
|
-
|
|
52
|
-
const legacyZenocodeTextLogoSnippetCores = [
|
|
53
|
-
doubleEscapedZenocodeTextLogoSnippetCore,
|
|
54
|
-
`var logo = {
|
|
55
|
-
left: [" ", " Zenocode ", " Zenocode ", " "],
|
|
56
|
-
right: [" ", " ", " ", " "]
|
|
57
|
-
};
|
|
58
|
-
var marks = "_^~";`,
|
|
59
|
-
`var logo = {
|
|
60
|
-
left: [" ZZZZZ EEEEE N N OOO CCCC OOO DDDD EEEEE", " ZZ EE NN N O O C O O D D EE ", " ZZ EEEE N N N O O C O O D D EEEE ", " ZZZZZ EEEEE N N OOO CCCC OOO DDDD EEEEE"],
|
|
61
|
-
right: ["", "", "", ""]
|
|
62
|
-
};
|
|
63
|
-
var marks = "_^~";`,
|
|
64
|
-
];
|
|
65
|
-
|
|
66
|
-
export function buildPaddedLogoSnippet(coreSnippet) {
|
|
67
|
-
return padBinaryReplacement(opencodeLogoSnippet, coreSnippet);
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
function _buildZenocodeLogoSnippet() {
|
|
71
|
-
return buildPaddedLogoSnippet(zenocodeTextLogoSnippetCore);
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
export function patchLogoSnippetText(text) {
|
|
75
|
-
const replacement = _buildZenocodeLogoSnippet();
|
|
76
|
-
if (!replacement || text.includes(replacement)) {
|
|
77
|
-
return { patched: false, text };
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
const patchTargets = [
|
|
81
|
-
opencodeLogoSnippet,
|
|
82
|
-
...legacyZenocodeTextLogoSnippetCores.map((coreSnippet) =>
|
|
83
|
-
buildPaddedLogoSnippet(coreSnippet),
|
|
84
|
-
),
|
|
85
|
-
].filter(Boolean);
|
|
86
|
-
let logoOffset = -1;
|
|
87
|
-
let matchedTargetLength = replacement.length;
|
|
88
|
-
for (const target of patchTargets) {
|
|
89
|
-
logoOffset = text.indexOf(target);
|
|
90
|
-
if (logoOffset !== -1) {
|
|
91
|
-
matchedTargetLength = target.length;
|
|
92
|
-
break;
|
|
93
|
-
}
|
|
94
|
-
}
|
|
95
|
-
if (logoOffset === -1) {
|
|
96
|
-
return { patched: false, text };
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
const nextText = `${text.slice(0, logoOffset)}${replacement}${text.slice(logoOffset + matchedTargetLength)}`;
|
|
100
|
-
return { patched: true, text: nextText };
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
function _buildZenocodeVersionFooterSnippet(
|
|
104
|
-
openTextRef,
|
|
105
|
-
codeContainerRef,
|
|
106
|
-
codeTextRef,
|
|
107
|
-
versionInsertRef,
|
|
108
|
-
versionRef,
|
|
109
|
-
) {
|
|
110
|
-
return [
|
|
111
|
-
`insertNode(${openTextRef}, createTextNode(\`Zeno\`));`,
|
|
112
|
-
`insertNode(${codeContainerRef}, ${codeTextRef});`,
|
|
113
|
-
`insertNode(${codeTextRef}, createTextNode(\`code\`));`,
|
|
114
|
-
`${versionInsertRef}(${versionRef}, () => "");`,
|
|
115
|
-
].join(" ");
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
export function patchOpenCodeVersionFooterText(text) {
|
|
119
|
-
openCodeVersionFooterSnippetPattern.lastIndex = 0;
|
|
120
|
-
let patched = false;
|
|
121
|
-
const next = text.replace(
|
|
122
|
-
openCodeVersionFooterSnippetPattern,
|
|
123
|
-
(match, openTextRef, codeContainerRef, codeTextRef, versionInsertRef, versionRef) => {
|
|
124
|
-
const replacement = _buildZenocodeVersionFooterSnippet(
|
|
125
|
-
openTextRef,
|
|
126
|
-
codeContainerRef,
|
|
127
|
-
codeTextRef,
|
|
128
|
-
versionInsertRef,
|
|
129
|
-
versionRef,
|
|
130
|
-
);
|
|
131
|
-
const padded = replacement.length <= match.length ? replacement.padEnd(match.length, " ") : null;
|
|
132
|
-
if (!padded || padded === match) {
|
|
133
|
-
return match;
|
|
134
|
-
}
|
|
135
|
-
patched = true;
|
|
136
|
-
return padded;
|
|
137
|
-
},
|
|
138
|
-
);
|
|
139
|
-
return { patched, text: next };
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
export function patchOpenCodeDisplayNameText(text) {
|
|
143
|
-
const replacements = [
|
|
144
|
-
['TD("Uninstall OpenCode")', 'TD("Uninstall Zenocode")'],
|
|
145
|
-
['"name":"OpenCode"', '"name":"Zenocode"'],
|
|
146
|
-
['"short_name":"OpenCode"', '"short_name":"Zenocode"'],
|
|
147
|
-
];
|
|
148
|
-
|
|
149
|
-
let patched = false;
|
|
150
|
-
let nextText = text;
|
|
151
|
-
for (const [target, replacement] of replacements) {
|
|
152
|
-
if (!nextText.includes(target)) {
|
|
153
|
-
continue;
|
|
154
|
-
}
|
|
155
|
-
nextText = nextText.replaceAll(target, replacement);
|
|
156
|
-
patched = true;
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
return { patched, text: nextText };
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
export function patchZenocodeBinaryText(text) {
|
|
163
|
-
let patched = false;
|
|
164
|
-
let nextText = text;
|
|
165
|
-
const patches = {
|
|
166
|
-
logo: false,
|
|
167
|
-
footer: false,
|
|
168
|
-
displayName: false,
|
|
169
|
-
};
|
|
170
|
-
|
|
171
|
-
const logoPatch = patchLogoSnippetText(nextText);
|
|
172
|
-
if (logoPatch.patched) {
|
|
173
|
-
nextText = logoPatch.text;
|
|
174
|
-
patched = true;
|
|
175
|
-
patches.logo = true;
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
const footerPatch = patchOpenCodeVersionFooterText(nextText);
|
|
179
|
-
if (footerPatch.patched) {
|
|
180
|
-
nextText = footerPatch.text;
|
|
181
|
-
patched = true;
|
|
182
|
-
patches.footer = true;
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
const displayNamePatch = patchOpenCodeDisplayNameText(nextText);
|
|
186
|
-
if (displayNamePatch.patched) {
|
|
187
|
-
nextText = displayNamePatch.text;
|
|
188
|
-
patched = true;
|
|
189
|
-
patches.displayName = true;
|
|
190
|
-
}
|
|
191
|
-
|
|
192
|
-
return { patched, patches, text: nextText };
|
|
193
|
-
}
|
|
@@ -1,89 +0,0 @@
|
|
|
1
|
-
import assert from "node:assert/strict";
|
|
2
|
-
import test from "node:test";
|
|
3
|
-
import {
|
|
4
|
-
padBinaryReplacement,
|
|
5
|
-
patchOpenCodeDisplayNameText,
|
|
6
|
-
patchOpenCodeVersionFooterText,
|
|
7
|
-
patchZenocodeBinaryText,
|
|
8
|
-
} from "./branding-patch.mjs";
|
|
9
|
-
|
|
10
|
-
test("padBinaryReplacement keeps binary length stable", () => {
|
|
11
|
-
const padded = padBinaryReplacement("abcdef", "abc");
|
|
12
|
-
assert.equal(padded, "abc ");
|
|
13
|
-
assert.equal(padded.length, 6);
|
|
14
|
-
});
|
|
15
|
-
|
|
16
|
-
test("patchOpenCodeVersionFooterText rewrites OpenCode + version footer", () => {
|
|
17
|
-
const input = [
|
|
18
|
-
"insertNode(_el$74, createTextNode(`Open`));",
|
|
19
|
-
" insertNode(_el$76, _el$77);",
|
|
20
|
-
" insertNode(_el$77, createTextNode(`Code`));",
|
|
21
|
-
" insert(_el$80, () => Installation.VERSION);",
|
|
22
|
-
].join("\n");
|
|
23
|
-
|
|
24
|
-
const result = patchOpenCodeVersionFooterText(input);
|
|
25
|
-
assert.equal(result.patched, true);
|
|
26
|
-
assert.equal(result.text.length, input.length);
|
|
27
|
-
assert.match(result.text, /createTextNode\(`Zeno`\)/);
|
|
28
|
-
assert.match(result.text, /createTextNode\(`code`\)/);
|
|
29
|
-
assert.match(result.text, /insert\(_el\$80, \(\) => ""\);/);
|
|
30
|
-
assert.doesNotMatch(result.text, /Installation\.VERSION/);
|
|
31
|
-
});
|
|
32
|
-
|
|
33
|
-
test("patchOpenCodeVersionFooterText rewrites the compiled 1.3.2 footer variant", () => {
|
|
34
|
-
const input = [
|
|
35
|
-
"insertNode(_el$74, createTextNode(`Open`));",
|
|
36
|
-
" insertNode(_el$76, _el$77);",
|
|
37
|
-
" insertNode(_el$77, createTextNode(`Code`));",
|
|
38
|
-
" insert3(_el$80, () => Installation.VERSION);",
|
|
39
|
-
].join("\n");
|
|
40
|
-
|
|
41
|
-
const result = patchOpenCodeVersionFooterText(input);
|
|
42
|
-
assert.equal(result.patched, true);
|
|
43
|
-
assert.equal(result.text.length, input.length);
|
|
44
|
-
assert.match(result.text, /createTextNode\(`Zeno`\)/);
|
|
45
|
-
assert.match(result.text, /createTextNode\(`code`\)/);
|
|
46
|
-
assert.match(result.text, /insert3\(_el\$80, \(\) => ""\);/);
|
|
47
|
-
assert.doesNotMatch(result.text, /Installation\.VERSION/);
|
|
48
|
-
});
|
|
49
|
-
|
|
50
|
-
test("patchOpenCodeVersionFooterText is a no-op when already branded", () => {
|
|
51
|
-
const branded = [
|
|
52
|
-
"insertNode(_el$74, createTextNode(`Zeno`));",
|
|
53
|
-
"insertNode(_el$76, _el$77);",
|
|
54
|
-
"insertNode(_el$77, createTextNode(`code`));",
|
|
55
|
-
"insert(_el$80, () => \"\");",
|
|
56
|
-
].join(" ");
|
|
57
|
-
|
|
58
|
-
const result = patchOpenCodeVersionFooterText(branded);
|
|
59
|
-
assert.equal(result.patched, false);
|
|
60
|
-
assert.equal(result.text, branded);
|
|
61
|
-
});
|
|
62
|
-
|
|
63
|
-
test("patchOpenCodeDisplayNameText rewrites current compiled display names", () => {
|
|
64
|
-
const text = [
|
|
65
|
-
'TD("Uninstall OpenCode");"name":"OpenCode","short_name":"OpenCode"',
|
|
66
|
-
'"Powered by OpenCode"',
|
|
67
|
-
].join(";");
|
|
68
|
-
|
|
69
|
-
const result = patchOpenCodeDisplayNameText(text);
|
|
70
|
-
|
|
71
|
-
assert.equal(result.patched, true);
|
|
72
|
-
assert.equal(result.text.length, text.length);
|
|
73
|
-
assert.equal(
|
|
74
|
-
result.text,
|
|
75
|
-
'TD("Uninstall Zenocode");"name":"Zenocode","short_name":"Zenocode";"Powered by OpenCode"',
|
|
76
|
-
);
|
|
77
|
-
});
|
|
78
|
-
|
|
79
|
-
test("patchZenocodeBinaryText reports display-name-only branding separately", () => {
|
|
80
|
-
const result = patchZenocodeBinaryText('"name":"OpenCode"');
|
|
81
|
-
|
|
82
|
-
assert.equal(result.patched, true);
|
|
83
|
-
assert.deepEqual(result.patches, {
|
|
84
|
-
logo: false,
|
|
85
|
-
footer: false,
|
|
86
|
-
displayName: true,
|
|
87
|
-
});
|
|
88
|
-
assert.equal(result.text, '"name":"Zenocode"');
|
|
89
|
-
});
|