@chriscode/hush 2.0.0 → 2.2.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/dist/cli.js +81 -14
- package/dist/commands/run.d.ts +3 -0
- package/dist/commands/run.d.ts.map +1 -0
- package/dist/commands/run.js +77 -0
- package/dist/commands/set.d.ts +3 -0
- package/dist/commands/set.d.ts.map +1 -0
- package/dist/commands/set.js +87 -0
- package/dist/commands/skill.d.ts +3 -0
- package/dist/commands/skill.d.ts.map +1 -0
- package/dist/commands/skill.js +1077 -0
- package/dist/core/sops.d.ts +5 -0
- package/dist/core/sops.d.ts.map +1 -1
- package/dist/core/sops.js +49 -1
- package/dist/formats/index.d.ts +2 -1
- package/dist/formats/index.d.ts.map +1 -1
- package/dist/formats/index.js +4 -1
- package/dist/formats/yaml.d.ts +13 -0
- package/dist/formats/yaml.d.ts.map +1 -0
- package/dist/formats/yaml.js +50 -0
- package/dist/types.d.ts +19 -2
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +5 -0
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -3,6 +3,8 @@ import pc from 'picocolors';
|
|
|
3
3
|
import { decryptCommand } from './commands/decrypt.js';
|
|
4
4
|
import { encryptCommand } from './commands/encrypt.js';
|
|
5
5
|
import { editCommand } from './commands/edit.js';
|
|
6
|
+
import { setCommand } from './commands/set.js';
|
|
7
|
+
import { runCommand } from './commands/run.js';
|
|
6
8
|
import { statusCommand } from './commands/status.js';
|
|
7
9
|
import { pushCommand } from './commands/push.js';
|
|
8
10
|
import { initCommand } from './commands/init.js';
|
|
@@ -10,7 +12,8 @@ import { listCommand } from './commands/list.js';
|
|
|
10
12
|
import { inspectCommand } from './commands/inspect.js';
|
|
11
13
|
import { hasCommand } from './commands/has.js';
|
|
12
14
|
import { checkCommand } from './commands/check.js';
|
|
13
|
-
|
|
15
|
+
import { skillCommand } from './commands/skill.js';
|
|
16
|
+
const VERSION = '2.1.0';
|
|
14
17
|
function printHelp() {
|
|
15
18
|
console.log(`
|
|
16
19
|
${pc.bold('hush')} - SOPS-based secrets management for monorepos
|
|
@@ -21,43 +24,54 @@ ${pc.bold('Usage:')}
|
|
|
21
24
|
${pc.bold('Commands:')}
|
|
22
25
|
init Initialize hush.yaml config
|
|
23
26
|
encrypt Encrypt source .env files
|
|
24
|
-
|
|
25
|
-
|
|
27
|
+
run -- <cmd> Run command with secrets in memory (AI-safe)
|
|
28
|
+
set <KEY> Set a single secret interactively (AI-safe)
|
|
29
|
+
edit [file] Edit all secrets in $EDITOR
|
|
26
30
|
list List all variables (shows values)
|
|
27
31
|
inspect List all variables (masked values, AI-safe)
|
|
28
32
|
has <key> Check if a secret exists (exit 0 if set, 1 if not)
|
|
29
33
|
check Verify secrets are encrypted (for pre-commit hooks)
|
|
30
34
|
push Push secrets to Cloudflare Workers
|
|
31
35
|
status Show configuration and status
|
|
36
|
+
skill Install Claude Code / OpenCode skill
|
|
37
|
+
|
|
38
|
+
${pc.bold('Deprecated Commands:')}
|
|
39
|
+
decrypt Write secrets to disk (unsafe - use 'run' instead)
|
|
32
40
|
|
|
33
41
|
${pc.bold('Options:')}
|
|
34
42
|
-e, --env <env> Environment: development or production (default: development)
|
|
35
43
|
-r, --root <dir> Root directory (default: current directory)
|
|
44
|
+
-t, --target <t> Target name from hush.yaml (run only)
|
|
36
45
|
-q, --quiet Suppress output (has/check commands)
|
|
37
46
|
--dry-run Preview changes without applying (push only)
|
|
38
47
|
--warn Warn but exit 0 on drift (check only)
|
|
39
48
|
--json Output machine-readable JSON (check only)
|
|
40
49
|
--only-changed Only check git-modified files (check only)
|
|
41
50
|
--require-source Fail if source file is missing (check only)
|
|
51
|
+
--global Install skill to ~/.claude/skills/ (skill only)
|
|
52
|
+
--local Install skill to ./.claude/skills/ (skill/set only)
|
|
42
53
|
-h, --help Show this help message
|
|
43
54
|
-v, --version Show version number
|
|
44
55
|
|
|
45
56
|
${pc.bold('Examples:')}
|
|
46
57
|
hush init Initialize hush.yaml config
|
|
47
58
|
hush encrypt Encrypt .env files
|
|
48
|
-
hush
|
|
49
|
-
hush
|
|
50
|
-
hush
|
|
51
|
-
hush
|
|
52
|
-
hush
|
|
59
|
+
hush run -- npm start Run with secrets in memory (AI-safe!)
|
|
60
|
+
hush run -e prod -- npm build Run with production secrets
|
|
61
|
+
hush run -t api -- wrangler dev Run filtered for 'api' target
|
|
62
|
+
hush set DATABASE_URL Set a secret interactively (AI-safe)
|
|
63
|
+
hush set API_KEY -e prod Set a production secret
|
|
64
|
+
hush set API_KEY --local Set a personal local override
|
|
65
|
+
hush edit Edit all shared secrets in $EDITOR
|
|
66
|
+
hush edit development Edit development secrets in $EDITOR
|
|
67
|
+
hush edit local Edit personal local overrides
|
|
53
68
|
hush inspect List all variables (masked, AI-safe)
|
|
54
69
|
hush has DATABASE_URL Check if DATABASE_URL is set
|
|
55
70
|
hush has API_KEY -q && echo "API_KEY is configured"
|
|
56
71
|
hush check Verify secrets are encrypted
|
|
57
|
-
hush check --warn Check but don't fail on drift
|
|
58
|
-
hush check --json Output JSON for CI
|
|
59
72
|
hush push --dry-run Preview push to Cloudflare
|
|
60
73
|
hush status Show current status
|
|
74
|
+
hush skill Install Claude skill (interactive)
|
|
61
75
|
`);
|
|
62
76
|
}
|
|
63
77
|
function parseEnvironment(value) {
|
|
@@ -68,7 +82,7 @@ function parseEnvironment(value) {
|
|
|
68
82
|
return null;
|
|
69
83
|
}
|
|
70
84
|
function parseFileKey(value) {
|
|
71
|
-
if (value === 'shared' || value === 'development' || value === 'production')
|
|
85
|
+
if (value === 'shared' || value === 'development' || value === 'production' || value === 'local')
|
|
72
86
|
return value;
|
|
73
87
|
if (value === 'dev')
|
|
74
88
|
return 'development';
|
|
@@ -79,6 +93,7 @@ function parseFileKey(value) {
|
|
|
79
93
|
function parseArgs(args) {
|
|
80
94
|
let command = '';
|
|
81
95
|
let env = 'development';
|
|
96
|
+
let envExplicit = false;
|
|
82
97
|
let root = process.cwd();
|
|
83
98
|
let dryRun = false;
|
|
84
99
|
let quiet = false;
|
|
@@ -86,8 +101,12 @@ function parseArgs(args) {
|
|
|
86
101
|
let json = false;
|
|
87
102
|
let onlyChanged = false;
|
|
88
103
|
let requireSource = false;
|
|
104
|
+
let global = false;
|
|
105
|
+
let local = false;
|
|
89
106
|
let file;
|
|
90
107
|
let key;
|
|
108
|
+
let target;
|
|
109
|
+
let cmdArgs = [];
|
|
91
110
|
for (let i = 0; i < args.length; i++) {
|
|
92
111
|
const arg = args[i];
|
|
93
112
|
if (arg === '-h' || arg === '--help') {
|
|
@@ -103,6 +122,7 @@ function parseArgs(args) {
|
|
|
103
122
|
const parsed = parseEnvironment(nextArg);
|
|
104
123
|
if (parsed) {
|
|
105
124
|
env = parsed;
|
|
125
|
+
envExplicit = true;
|
|
106
126
|
}
|
|
107
127
|
else {
|
|
108
128
|
console.error(pc.red(`Invalid environment: ${nextArg}`));
|
|
@@ -139,6 +159,22 @@ function parseArgs(args) {
|
|
|
139
159
|
requireSource = true;
|
|
140
160
|
continue;
|
|
141
161
|
}
|
|
162
|
+
if (arg === '--global') {
|
|
163
|
+
global = true;
|
|
164
|
+
continue;
|
|
165
|
+
}
|
|
166
|
+
if (arg === '--local') {
|
|
167
|
+
local = true;
|
|
168
|
+
continue;
|
|
169
|
+
}
|
|
170
|
+
if (arg === '-t' || arg === '--target') {
|
|
171
|
+
target = args[++i];
|
|
172
|
+
continue;
|
|
173
|
+
}
|
|
174
|
+
if (arg === '--') {
|
|
175
|
+
cmdArgs = args.slice(i + 1);
|
|
176
|
+
break;
|
|
177
|
+
}
|
|
142
178
|
if (!command && !arg.startsWith('-')) {
|
|
143
179
|
command = arg;
|
|
144
180
|
continue;
|
|
@@ -150,17 +186,21 @@ function parseArgs(args) {
|
|
|
150
186
|
}
|
|
151
187
|
else {
|
|
152
188
|
console.error(pc.red(`Invalid file: ${arg}`));
|
|
153
|
-
console.error(pc.dim('Use: shared, development, or
|
|
189
|
+
console.error(pc.dim('Use: shared, development, production, or local'));
|
|
154
190
|
process.exit(1);
|
|
155
191
|
}
|
|
156
192
|
continue;
|
|
157
193
|
}
|
|
194
|
+
if (command === 'set' && !arg.startsWith('-') && !key) {
|
|
195
|
+
key = arg;
|
|
196
|
+
continue;
|
|
197
|
+
}
|
|
158
198
|
if (command === 'has' && !arg.startsWith('-') && !key) {
|
|
159
199
|
key = arg;
|
|
160
200
|
continue;
|
|
161
201
|
}
|
|
162
202
|
}
|
|
163
|
-
return { command, env, root, dryRun, quiet, warn, json, onlyChanged, requireSource, file, key };
|
|
203
|
+
return { command, env, envExplicit, root, dryRun, quiet, warn, json, onlyChanged, requireSource, global, local, file, key, target, cmdArgs };
|
|
164
204
|
}
|
|
165
205
|
async function main() {
|
|
166
206
|
const args = process.argv.slice(2);
|
|
@@ -168,7 +208,7 @@ async function main() {
|
|
|
168
208
|
printHelp();
|
|
169
209
|
process.exit(0);
|
|
170
210
|
}
|
|
171
|
-
const { command, env, root, dryRun, quiet, warn, json, onlyChanged, requireSource, file, key } = parseArgs(args);
|
|
211
|
+
const { command, env, envExplicit, root, dryRun, quiet, warn, json, onlyChanged, requireSource, global, local, file, key, target, cmdArgs } = parseArgs(args);
|
|
172
212
|
try {
|
|
173
213
|
switch (command) {
|
|
174
214
|
case 'init':
|
|
@@ -178,8 +218,32 @@ async function main() {
|
|
|
178
218
|
await encryptCommand({ root });
|
|
179
219
|
break;
|
|
180
220
|
case 'decrypt':
|
|
221
|
+
console.warn(pc.yellow('⚠️ Warning: "hush decrypt" is deprecated and writes unencrypted secrets to disk.'));
|
|
222
|
+
console.warn(pc.yellow(' Use "hush run -- <command>" instead for better security.'));
|
|
223
|
+
console.warn(pc.dim(' To suppress this warning, use "hush unsafe:decrypt"'));
|
|
224
|
+
console.warn('');
|
|
225
|
+
await decryptCommand({ root, env });
|
|
226
|
+
break;
|
|
227
|
+
case 'unsafe:decrypt':
|
|
228
|
+
console.warn(pc.red('⚠️ UNSAFE MODE: Writing unencrypted secrets to disk.'));
|
|
229
|
+
console.warn(pc.red(' These files will be readable by AI assistants and other tools.'));
|
|
230
|
+
console.warn('');
|
|
181
231
|
await decryptCommand({ root, env });
|
|
182
232
|
break;
|
|
233
|
+
case 'run':
|
|
234
|
+
await runCommand({ root, env, target, command: cmdArgs });
|
|
235
|
+
break;
|
|
236
|
+
case 'set': {
|
|
237
|
+
let setFile = 'shared';
|
|
238
|
+
if (local) {
|
|
239
|
+
setFile = 'local';
|
|
240
|
+
}
|
|
241
|
+
else if (envExplicit) {
|
|
242
|
+
setFile = env;
|
|
243
|
+
}
|
|
244
|
+
await setCommand({ root, file: setFile, key });
|
|
245
|
+
break;
|
|
246
|
+
}
|
|
183
247
|
case 'edit':
|
|
184
248
|
await editCommand({ root, file });
|
|
185
249
|
break;
|
|
@@ -205,6 +269,9 @@ async function main() {
|
|
|
205
269
|
case 'status':
|
|
206
270
|
await statusCommand({ root });
|
|
207
271
|
break;
|
|
272
|
+
case 'skill':
|
|
273
|
+
await skillCommand({ root, global, local });
|
|
274
|
+
break;
|
|
208
275
|
default:
|
|
209
276
|
if (command) {
|
|
210
277
|
console.error(pc.red(`Unknown command: ${command}`));
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"run.d.ts","sourceRoot":"","sources":["../../src/commands/run.ts"],"names":[],"mappings":"AAUA,OAAO,KAAK,EAAE,UAAU,EAAmC,MAAM,aAAa,CAAC;AAoC/E,wBAAsB,UAAU,CAAC,OAAO,EAAE,UAAU,GAAG,OAAO,CAAC,IAAI,CAAC,CAkDnE"}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { spawnSync } from 'node:child_process';
|
|
2
|
+
import { existsSync } from 'node:fs';
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
import pc from 'picocolors';
|
|
5
|
+
import { loadConfig } from '../config/loader.js';
|
|
6
|
+
import { filterVarsForTarget } from '../core/filter.js';
|
|
7
|
+
import { interpolateVars, getUnresolvedVars } from '../core/interpolate.js';
|
|
8
|
+
import { mergeVars } from '../core/merge.js';
|
|
9
|
+
import { parseEnvContent } from '../core/parse.js';
|
|
10
|
+
import { decrypt as sopsDecrypt } from '../core/sops.js';
|
|
11
|
+
function getEncryptedPath(sourcePath) {
|
|
12
|
+
return sourcePath + '.encrypted';
|
|
13
|
+
}
|
|
14
|
+
function getDecryptedSecrets(root, env, config) {
|
|
15
|
+
const sharedEncrypted = join(root, getEncryptedPath(config.sources.shared));
|
|
16
|
+
const envEncrypted = join(root, getEncryptedPath(config.sources[env]));
|
|
17
|
+
const localEncrypted = join(root, getEncryptedPath(config.sources.local));
|
|
18
|
+
const varSources = [];
|
|
19
|
+
if (existsSync(sharedEncrypted)) {
|
|
20
|
+
const content = sopsDecrypt(sharedEncrypted);
|
|
21
|
+
varSources.push(parseEnvContent(content));
|
|
22
|
+
}
|
|
23
|
+
if (existsSync(envEncrypted)) {
|
|
24
|
+
const content = sopsDecrypt(envEncrypted);
|
|
25
|
+
varSources.push(parseEnvContent(content));
|
|
26
|
+
}
|
|
27
|
+
if (existsSync(localEncrypted)) {
|
|
28
|
+
const content = sopsDecrypt(localEncrypted);
|
|
29
|
+
varSources.push(parseEnvContent(content));
|
|
30
|
+
}
|
|
31
|
+
if (varSources.length === 0) {
|
|
32
|
+
throw new Error(`No encrypted files found. Expected: ${sharedEncrypted}`);
|
|
33
|
+
}
|
|
34
|
+
const merged = mergeVars(...varSources);
|
|
35
|
+
return interpolateVars(merged);
|
|
36
|
+
}
|
|
37
|
+
export async function runCommand(options) {
|
|
38
|
+
const { root, env, target, command } = options;
|
|
39
|
+
if (!command || command.length === 0) {
|
|
40
|
+
console.error(pc.red('Usage: hush run -- <command>'));
|
|
41
|
+
console.error(pc.dim('Example: hush run -- npm start'));
|
|
42
|
+
console.error(pc.dim(' hush run -e production -- npm run build'));
|
|
43
|
+
console.error(pc.dim(' hush run --target api -- wrangler dev'));
|
|
44
|
+
process.exit(1);
|
|
45
|
+
}
|
|
46
|
+
const config = loadConfig(root);
|
|
47
|
+
let vars = getDecryptedSecrets(root, env, config);
|
|
48
|
+
if (target) {
|
|
49
|
+
const targetConfig = config.targets.find(t => t.name === target);
|
|
50
|
+
if (!targetConfig) {
|
|
51
|
+
console.error(pc.red(`Target "${target}" not found in hush.yaml`));
|
|
52
|
+
console.error(pc.dim(`Available targets: ${config.targets.map(t => t.name).join(', ')}`));
|
|
53
|
+
process.exit(1);
|
|
54
|
+
}
|
|
55
|
+
vars = filterVarsForTarget(vars, targetConfig);
|
|
56
|
+
}
|
|
57
|
+
const unresolved = getUnresolvedVars(vars);
|
|
58
|
+
if (unresolved.length > 0) {
|
|
59
|
+
console.warn(pc.yellow(`Warning: ${unresolved.length} vars have unresolved references`));
|
|
60
|
+
}
|
|
61
|
+
const childEnv = {
|
|
62
|
+
...process.env,
|
|
63
|
+
...Object.fromEntries(vars.map(v => [v.key, v.value])),
|
|
64
|
+
};
|
|
65
|
+
const [cmd, ...args] = command;
|
|
66
|
+
const result = spawnSync(cmd, args, {
|
|
67
|
+
stdio: 'inherit',
|
|
68
|
+
env: childEnv,
|
|
69
|
+
shell: true,
|
|
70
|
+
cwd: root,
|
|
71
|
+
});
|
|
72
|
+
if (result.error) {
|
|
73
|
+
console.error(pc.red(`Failed to execute: ${result.error.message}`));
|
|
74
|
+
process.exit(1);
|
|
75
|
+
}
|
|
76
|
+
process.exit(result.status ?? 1);
|
|
77
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"set.d.ts","sourceRoot":"","sources":["../../src/commands/set.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AAuD9C,wBAAsB,UAAU,CAAC,OAAO,EAAE,UAAU,GAAG,OAAO,CAAC,IAAI,CAAC,CA0CnE"}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import { existsSync } from 'node:fs';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
import pc from 'picocolors';
|
|
4
|
+
import { loadConfig } from '../config/loader.js';
|
|
5
|
+
import { setKey } from '../core/sops.js';
|
|
6
|
+
function promptForValue(key) {
|
|
7
|
+
return new Promise((resolve, reject) => {
|
|
8
|
+
if (!process.stdin.isTTY) {
|
|
9
|
+
reject(new Error('Interactive input requires a terminal (TTY)'));
|
|
10
|
+
return;
|
|
11
|
+
}
|
|
12
|
+
process.stdout.write(`Enter value for ${pc.cyan(key)}: `);
|
|
13
|
+
const stdin = process.stdin;
|
|
14
|
+
stdin.setRawMode(true);
|
|
15
|
+
stdin.resume();
|
|
16
|
+
stdin.setEncoding('utf8');
|
|
17
|
+
let value = '';
|
|
18
|
+
const onData = (char) => {
|
|
19
|
+
switch (char) {
|
|
20
|
+
case '\n':
|
|
21
|
+
case '\r':
|
|
22
|
+
case '\u0004': // Ctrl+D
|
|
23
|
+
stdin.setRawMode(false);
|
|
24
|
+
stdin.pause();
|
|
25
|
+
stdin.removeListener('data', onData);
|
|
26
|
+
process.stdout.write('\n');
|
|
27
|
+
resolve(value);
|
|
28
|
+
break;
|
|
29
|
+
case '\u0003': // Ctrl+C
|
|
30
|
+
stdin.setRawMode(false);
|
|
31
|
+
stdin.pause();
|
|
32
|
+
stdin.removeListener('data', onData);
|
|
33
|
+
process.stdout.write('\n');
|
|
34
|
+
reject(new Error('Cancelled'));
|
|
35
|
+
break;
|
|
36
|
+
case '\u007F': // Backspace
|
|
37
|
+
case '\b':
|
|
38
|
+
if (value.length > 0) {
|
|
39
|
+
value = value.slice(0, -1);
|
|
40
|
+
process.stdout.write('\b \b');
|
|
41
|
+
}
|
|
42
|
+
break;
|
|
43
|
+
default:
|
|
44
|
+
value += char;
|
|
45
|
+
process.stdout.write('\u2022'); // Bullet character for hidden input
|
|
46
|
+
}
|
|
47
|
+
};
|
|
48
|
+
stdin.on('data', onData);
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
export async function setCommand(options) {
|
|
52
|
+
const { root, file, key } = options;
|
|
53
|
+
const config = loadConfig(root);
|
|
54
|
+
const fileKey = file ?? 'shared';
|
|
55
|
+
const sourcePath = config.sources[fileKey];
|
|
56
|
+
const encryptedPath = join(root, sourcePath + '.encrypted');
|
|
57
|
+
if (!key) {
|
|
58
|
+
console.error(pc.red('Usage: hush set <KEY> [-e environment]'));
|
|
59
|
+
console.error(pc.dim('Example: hush set DATABASE_URL'));
|
|
60
|
+
console.error(pc.dim(' hush set API_KEY -e production'));
|
|
61
|
+
console.error(pc.dim('\nTo edit all secrets in an editor, use: hush edit'));
|
|
62
|
+
process.exit(1);
|
|
63
|
+
}
|
|
64
|
+
if (!existsSync(encryptedPath) && !existsSync(join(root, '.sops.yaml'))) {
|
|
65
|
+
console.error(pc.red('Hush is not initialized in this directory'));
|
|
66
|
+
console.error(pc.dim('Run "hush init" first, then "hush encrypt"'));
|
|
67
|
+
process.exit(1);
|
|
68
|
+
}
|
|
69
|
+
try {
|
|
70
|
+
const value = await promptForValue(key);
|
|
71
|
+
if (!value) {
|
|
72
|
+
console.error(pc.yellow('No value entered, aborting'));
|
|
73
|
+
process.exit(1);
|
|
74
|
+
}
|
|
75
|
+
setKey(encryptedPath, key, value);
|
|
76
|
+
const envLabel = fileKey === 'shared' ? '' : ` in ${fileKey}`;
|
|
77
|
+
console.log(pc.green(`\n${key} set${envLabel} (${value.length} chars, encrypted)`));
|
|
78
|
+
}
|
|
79
|
+
catch (error) {
|
|
80
|
+
const err = error;
|
|
81
|
+
if (err.message === 'Cancelled') {
|
|
82
|
+
console.log(pc.yellow('Cancelled'));
|
|
83
|
+
process.exit(1);
|
|
84
|
+
}
|
|
85
|
+
throw err;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"skill.d.ts","sourceRoot":"","sources":["../../src/commands/skill.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAwhChD,wBAAsB,YAAY,CAAC,OAAO,EAAE,YAAY,GAAG,OAAO,CAAC,IAAI,CAAC,CA0CvE"}
|