@kargojs/cli 0.1.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.
Files changed (47) hide show
  1. package/bin/kargo.js +7 -0
  2. package/dist/commands/create.d.ts +6 -0
  3. package/dist/commands/create.d.ts.map +1 -0
  4. package/dist/commands/create.js +48 -0
  5. package/dist/commands/make-panel.d.ts +7 -0
  6. package/dist/commands/make-panel.d.ts.map +1 -0
  7. package/dist/commands/make-panel.js +39 -0
  8. package/dist/commands/make-resource.d.ts +14 -0
  9. package/dist/commands/make-resource.d.ts.map +1 -0
  10. package/dist/commands/make-resource.js +101 -0
  11. package/dist/index.d.ts +2 -0
  12. package/dist/index.d.ts.map +1 -0
  13. package/dist/index.js +43 -0
  14. package/dist/templates/database.d.ts +13 -0
  15. package/dist/templates/database.d.ts.map +1 -0
  16. package/dist/templates/database.js +43 -0
  17. package/dist/templates/form.d.ts +3 -0
  18. package/dist/templates/form.d.ts.map +1 -0
  19. package/dist/templates/form.js +11 -0
  20. package/dist/templates/project.d.ts +7 -0
  21. package/dist/templates/project.d.ts.map +1 -0
  22. package/dist/templates/project.js +459 -0
  23. package/dist/templates/resource.d.ts +3 -0
  24. package/dist/templates/resource.d.ts.map +1 -0
  25. package/dist/templates/resource.js +15 -0
  26. package/dist/templates/service-in-memory.d.ts +3 -0
  27. package/dist/templates/service-in-memory.d.ts.map +1 -0
  28. package/dist/templates/service-in-memory.js +47 -0
  29. package/dist/templates/service.d.ts +3 -0
  30. package/dist/templates/service.d.ts.map +1 -0
  31. package/dist/templates/service.js +21 -0
  32. package/dist/templates/table.d.ts +3 -0
  33. package/dist/templates/table.d.ts.map +1 -0
  34. package/dist/templates/table.js +31 -0
  35. package/dist/utils/fs.d.ts +12 -0
  36. package/dist/utils/fs.d.ts.map +1 -0
  37. package/dist/utils/fs.js +26 -0
  38. package/dist/utils/log.d.ts +11 -0
  39. package/dist/utils/log.d.ts.map +1 -0
  40. package/dist/utils/log.js +16 -0
  41. package/dist/utils/register-resource.d.ts +22 -0
  42. package/dist/utils/register-resource.d.ts.map +1 -0
  43. package/dist/utils/register-resource.js +71 -0
  44. package/dist/utils/strings.d.ts +31 -0
  45. package/dist/utils/strings.d.ts.map +1 -0
  46. package/dist/utils/strings.js +63 -0
  47. package/package.json +31 -0
package/bin/kargo.js ADDED
@@ -0,0 +1,7 @@
1
+ #!/usr/bin/env node
2
+ // Thin shebang wrapper — main logic lives in dist/ (compiled TypeScript).
3
+ // This file exists so `tsc` doesn't need to produce a shebang line.
4
+ import('../dist/index.js').then((m) => m.main()).catch((err) => {
5
+ console.error(err);
6
+ process.exit(1);
7
+ });
@@ -0,0 +1,6 @@
1
+ import type { DatabaseProvider } from '../templates/database.js';
2
+ export declare function create(name: string, opts: {
3
+ force?: boolean;
4
+ database?: DatabaseProvider;
5
+ }): Promise<void>;
6
+ //# sourceMappingURL=create.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"create.d.ts","sourceRoot":"","sources":["../../src/commands/create.ts"],"names":[],"mappings":"AAMA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,0BAA0B,CAAC;AAEjE,wBAAsB,MAAM,CAC1B,IAAI,EAAE,MAAM,EACZ,IAAI,EAAE;IAAE,KAAK,CAAC,EAAE,OAAO,CAAC;IAAC,QAAQ,CAAC,EAAE,gBAAgB,CAAA;CAAE,iBAoDvD"}
@@ -0,0 +1,48 @@
1
+ import { access } from 'node:fs/promises';
2
+ import { resolve } from 'node:path';
3
+ import pc from 'picocolors';
4
+ import { log, banner } from '../utils/log.js';
5
+ import { writeFiles } from '../utils/fs.js';
6
+ import { projectFiles } from '../templates/project.js';
7
+ export async function create(name, opts) {
8
+ banner('create', '0.1.0');
9
+ const target = resolve(name);
10
+ const database = opts.database ?? 'sqlite';
11
+ if (!['sqlite', 'mysql', 'postgres'].includes(database)) {
12
+ log.error(`Unknown database: ${pc.cyan(database)}. Use sqlite, mysql, or postgres.`);
13
+ process.exit(1);
14
+ }
15
+ if (!opts.force) {
16
+ try {
17
+ await access(target);
18
+ log.error(`Directory already exists: ${pc.cyan(name)}`);
19
+ log.info(`Use ${pc.bold('--force')} to overwrite existing files.`);
20
+ process.exit(1);
21
+ }
22
+ catch {
23
+ // directory doesn't exist — good
24
+ }
25
+ }
26
+ log.step(`Scaffolding project: ${pc.bold(pc.cyan(name))} ${pc.gray(`(${database})`)}`);
27
+ log.blank();
28
+ const files = projectFiles(name, database);
29
+ await writeFiles(files, { force: opts.force });
30
+ log.blank();
31
+ log.success(`Project ${pc.bold(name)} created!`);
32
+ log.blank();
33
+ console.log(` ${pc.bold('Next steps:')}`);
34
+ console.log();
35
+ console.log(` ${pc.gray('1.')} ${pc.cyan(`cd ${name}`)}`);
36
+ console.log(` ${pc.gray('2.')} ${pc.cyan('npm install')}`);
37
+ if (database !== 'sqlite') {
38
+ console.log(` ${pc.gray(' ')} ${pc.yellow(`Edit apps/api/.env with your ${database} connection details first.`)}`);
39
+ }
40
+ console.log(` ${pc.gray('3.')} Create the database: ${pc.cyan('cd apps/api && npm run db:migrate')}`);
41
+ console.log(` ${pc.gray('4.')} Seed example data: ${pc.cyan('npm run db:seed')}`);
42
+ console.log(` ${pc.gray('5.')} Start the API: ${pc.cyan('npm run dev')}`);
43
+ console.log(` ${pc.gray('6.')} Start the web: ${pc.cyan('cd apps/web && npm run dev')}`);
44
+ console.log(` ${pc.gray('7.')} Open: ${pc.cyan('http://localhost:3000/admin')}`);
45
+ console.log();
46
+ console.log(` ${pc.gray('Tip:')} Add more resources with ${pc.cyan('kargo make:resource <Name>')} inside ${pc.cyan('apps/api')}.`);
47
+ console.log();
48
+ }
@@ -0,0 +1,7 @@
1
+ interface MakePanelOpts {
2
+ outDir?: string;
3
+ force?: boolean;
4
+ }
5
+ export declare function makePanel(id: string, opts?: MakePanelOpts): Promise<void>;
6
+ export {};
7
+ //# sourceMappingURL=make-panel.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"make-panel.d.ts","sourceRoot":"","sources":["../../src/commands/make-panel.ts"],"names":[],"mappings":"AAKA,UAAU,aAAa;IACrB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,KAAK,CAAC,EAAE,OAAO,CAAC;CACjB;AAmBD,wBAAsB,SAAS,CAAC,EAAE,EAAE,MAAM,EAAE,IAAI,GAAE,aAAkB,iBAsBnE"}
@@ -0,0 +1,39 @@
1
+ import { resolve, join } from 'node:path';
2
+ import pc from 'picocolors';
3
+ import { log } from '../utils/log.js';
4
+ import { writeGeneratedFile } from '../utils/fs.js';
5
+ function panelTemplate(id, path) {
6
+ return `import { Panel, registerPanel } from '@kargojs/core';
7
+
8
+ export const ${id.replace(/-/g, '_')}Panel = registerPanel(
9
+ Panel.make({
10
+ id: '${id}',
11
+ path: '${path}',
12
+ globalSearch: true,
13
+ databaseNotifications: false,
14
+ })
15
+ .colors({
16
+ primary: '#2563eb',
17
+ }),
18
+ );
19
+ `;
20
+ }
21
+ export async function makePanel(id, opts = {}) {
22
+ const cwd = process.cwd();
23
+ const outDir = opts.outDir ?? join(cwd, 'src');
24
+ const filePath = resolve(outDir, `${id}.panel.ts`);
25
+ const adminPath = id === 'admin' ? '/admin' : `/${id}`;
26
+ log.blank();
27
+ log.step(`Generating panel: ${pc.bold(pc.cyan(id))}`);
28
+ log.blank();
29
+ await writeGeneratedFile(filePath, panelTemplate(id, adminPath), { force: opts.force });
30
+ log.blank();
31
+ log.success(`Panel ${pc.bold(id)} generated!`);
32
+ log.blank();
33
+ console.log(` ${pc.bold('Import it in your app.module.ts:')}`);
34
+ console.log();
35
+ console.log(pc.gray(` import { ${id.replace(/-/g, '_')}Panel } from './${id}.panel.js';`));
36
+ console.log();
37
+ console.log(pc.gray(` KargoModule.forRoot({ panel: ${id.replace(/-/g, '_')}Panel, resources: [] })`));
38
+ console.log();
39
+ }
@@ -0,0 +1,14 @@
1
+ interface MakeResourceOpts {
2
+ /** Directory to write resource folders into (default: src/resources) */
3
+ resourceDir?: string;
4
+ /** Directory to write service files into (default: src/services) */
5
+ serviceDir?: string;
6
+ /** Skip generating the companion service file */
7
+ noService?: boolean;
8
+ /** Generate an in-memory array-backed service stub instead of a Prisma-backed one */
9
+ inMemory?: boolean;
10
+ force?: boolean;
11
+ }
12
+ export declare function makeResource(rawName: string, opts?: MakeResourceOpts): Promise<void>;
13
+ export {};
14
+ //# sourceMappingURL=make-resource.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"make-resource.d.ts","sourceRoot":"","sources":["../../src/commands/make-resource.ts"],"names":[],"mappings":"AAaA,UAAU,gBAAgB;IACxB,wEAAwE;IACxE,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,oEAAoE;IACpE,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,iDAAiD;IACjD,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,qFAAqF;IACrF,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,KAAK,CAAC,EAAE,OAAO,CAAC;CACjB;AAcD,wBAAsB,YAAY,CAAC,OAAO,EAAE,MAAM,EAAE,IAAI,GAAE,gBAAqB,iBAuF9E"}
@@ -0,0 +1,101 @@
1
+ import { access, appendFile } from 'node:fs/promises';
2
+ import { resolve, join } from 'node:path';
3
+ import pc from 'picocolors';
4
+ import { log } from '../utils/log.js';
5
+ import { writeGeneratedFile } from '../utils/fs.js';
6
+ import { resourceNames } from '../utils/strings.js';
7
+ import { resourceTemplate } from '../templates/resource.js';
8
+ import { formTemplate } from '../templates/form.js';
9
+ import { tableTemplate } from '../templates/table.js';
10
+ import { serviceTemplate } from '../templates/service.js';
11
+ import { serviceInMemoryTemplate } from '../templates/service-in-memory.js';
12
+ import { registerResource } from '../utils/register-resource.js';
13
+ async function appendPrismaModel(cwd, pascalName) {
14
+ const schemaPath = resolve(cwd, 'prisma', 'schema.prisma');
15
+ try {
16
+ await access(schemaPath);
17
+ }
18
+ catch {
19
+ return false;
20
+ }
21
+ const block = `\nmodel ${pascalName} {\n id Int @id @default(autoincrement())\n name String\n createdAt DateTime @default(now())\n}\n`;
22
+ await appendFile(schemaPath, block, 'utf8');
23
+ return true;
24
+ }
25
+ export async function makeResource(rawName, opts = {}) {
26
+ const n = resourceNames(rawName);
27
+ const cwd = process.cwd();
28
+ const resourceDir = opts.resourceDir ?? join(cwd, 'src', 'resources');
29
+ const serviceDir = opts.serviceDir ?? join(cwd, 'src', 'services');
30
+ const resourceFolder = join(resourceDir, n.kebab);
31
+ const resourcePath = resolve(resourceFolder, `${n.kebab}.resource.ts`);
32
+ const formPath = resolve(resourceFolder, `${n.kebab}.form.ts`);
33
+ const tablePath = resolve(resourceFolder, `${n.kebab}.table.ts`);
34
+ const servicePath = resolve(serviceDir, `${n.kebab}.service.ts`);
35
+ log.blank();
36
+ log.step(`Generating ${pc.bold(n.pascal)}Resource`);
37
+ log.blank();
38
+ await writeGeneratedFile(resourcePath, resourceTemplate(n), { force: opts.force });
39
+ await writeGeneratedFile(formPath, formTemplate(n), { force: opts.force });
40
+ await writeGeneratedFile(tablePath, tableTemplate(n), { force: opts.force });
41
+ if (!opts.noService) {
42
+ const content = opts.inMemory ? serviceInMemoryTemplate(n) : serviceTemplate(n);
43
+ await writeGeneratedFile(servicePath, content, { force: opts.force });
44
+ }
45
+ let modelAppended = false;
46
+ if (!opts.noService && !opts.inMemory) {
47
+ modelAppended = await appendPrismaModel(cwd, n.pascal);
48
+ if (modelAppended)
49
+ log.file(`prisma/schema.prisma (appended ${n.pascal} model)`);
50
+ }
51
+ log.blank();
52
+ log.success(`${pc.bold(n.pascal)}Resource generated!`);
53
+ log.blank();
54
+ let registered = false;
55
+ let registerReason;
56
+ if (!opts.noService) {
57
+ const appModulePath = resolve(cwd, 'src', 'app.module.ts');
58
+ try {
59
+ await access(appModulePath);
60
+ const result = registerResource({
61
+ appModulePath,
62
+ pascalName: n.pascal,
63
+ resourcePath,
64
+ servicePath,
65
+ });
66
+ registered = result.success;
67
+ registerReason = result.reason;
68
+ }
69
+ catch {
70
+ registerReason = 'src/app.module.ts not found';
71
+ }
72
+ }
73
+ if (registered) {
74
+ log.success(`Registered ${pc.bold(n.pascal)}Resource in ${pc.cyan('src/app.module.ts')}`);
75
+ }
76
+ else {
77
+ if (registerReason)
78
+ log.warn(`Could not auto-register: ${registerReason}`);
79
+ console.log(` ${pc.bold('Register it in your app.module.ts:')}`);
80
+ console.log();
81
+ console.log(pc.gray(` import { ${n.pascal}Resource } from './resources/${n.kebab}/${n.kebab}.resource.js';`));
82
+ if (!opts.noService) {
83
+ console.log(pc.gray(` import { ${n.pascal}Service } from './services/${n.kebab}.service.js';`));
84
+ }
85
+ console.log();
86
+ console.log(pc.gray(` KargoModule.forRoot({`));
87
+ console.log(pc.gray(` panel: adminPanel,`));
88
+ console.log(pc.gray(` resources: [`));
89
+ console.log(pc.gray(` // ... existing resources`));
90
+ if (!opts.noService) {
91
+ console.log(pc.cyan(` { resource: ${n.pascal}Resource, service: ${n.pascal}Service },`));
92
+ }
93
+ console.log(pc.gray(` ],`));
94
+ console.log(pc.gray(` })`));
95
+ }
96
+ if (modelAppended) {
97
+ console.log();
98
+ console.log(` ${pc.gray('Tip:')} Run ${pc.cyan('npm run db:migrate')} to apply the new ${n.pascal} model.`);
99
+ }
100
+ console.log();
101
+ }
@@ -0,0 +1,2 @@
1
+ export declare function main(): Promise<void>;
2
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAOA,wBAAsB,IAAI,kBA2DzB"}
package/dist/index.js ADDED
@@ -0,0 +1,43 @@
1
+ import { Command } from 'commander';
2
+ import { create } from './commands/create.js';
3
+ import { makeResource } from './commands/make-resource.js';
4
+ import { makePanel } from './commands/make-panel.js';
5
+ const VERSION = '0.1.0';
6
+ export async function main() {
7
+ const program = new Command();
8
+ program
9
+ .name('kargo')
10
+ .description('Kargo admin framework CLI')
11
+ .version(VERSION, '-v, --version');
12
+ // ── kargo create <name> ──────────────────────────────────────────────
13
+ program
14
+ .command('create <name>')
15
+ .description('Scaffold a new Kargo project (NestJS API + Next.js web)')
16
+ .option('-f, --force', 'Overwrite existing files', false)
17
+ .option('--database <provider>', 'sqlite, mysql, or postgres', 'sqlite')
18
+ .action((name, opts) => create(name, opts));
19
+ // ── kargo make:resource <Name> ───────────────────────────────────────
20
+ program
21
+ .command('make:resource <name>')
22
+ .description('Generate a Resource class and its companion Service')
23
+ .option('--resource-dir <dir>', 'Directory for resource files', 'src/resources')
24
+ .option('--service-dir <dir>', 'Directory for service files', 'src/services')
25
+ .option('--no-service', 'Skip generating the service file')
26
+ .option('--in-memory', 'Generate an in-memory array-backed service stub instead of Prisma-backed', false)
27
+ .option('-f, --force', 'Overwrite existing files', false)
28
+ .action((name, opts) => makeResource(name, {
29
+ resourceDir: opts.resourceDir,
30
+ serviceDir: opts.serviceDir,
31
+ noService: !opts.service,
32
+ inMemory: opts.inMemory,
33
+ force: opts.force,
34
+ }));
35
+ // ── kargo make:panel [id] ────────────────────────────────────────────
36
+ program
37
+ .command('make:panel [id]')
38
+ .description('Generate a Panel configuration file')
39
+ .option('--out-dir <dir>', 'Directory to write the panel file', 'src')
40
+ .option('-f, --force', 'Overwrite existing files', false)
41
+ .action((id, opts) => makePanel(id ?? 'admin', { outDir: opts.outDir, force: opts.force }));
42
+ await program.parseAsync(process.argv);
43
+ }
@@ -0,0 +1,13 @@
1
+ export type DatabaseProvider = 'sqlite' | 'mysql' | 'postgres';
2
+ interface DbConfig {
3
+ /** schema.prisma datasource provider value */
4
+ prismaProvider: 'sqlite' | 'mysql' | 'postgresql';
5
+ adapterPackage: string;
6
+ adapterClass: string;
7
+ envContent: string;
8
+ /** Code that constructs the adapter instance, assigned to `const adapter = ...` */
9
+ adapterConstructor: string;
10
+ }
11
+ export declare function getDbConfig(database: DatabaseProvider): DbConfig;
12
+ export {};
13
+ //# sourceMappingURL=database.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"database.d.ts","sourceRoot":"","sources":["../../src/templates/database.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,gBAAgB,GAAG,QAAQ,GAAG,OAAO,GAAG,UAAU,CAAC;AAE/D,UAAU,QAAQ;IAChB,8CAA8C;IAC9C,cAAc,EAAE,QAAQ,GAAG,OAAO,GAAG,YAAY,CAAC;IAClD,cAAc,EAAE,MAAM,CAAC;IACvB,YAAY,EAAE,MAAM,CAAC;IACrB,UAAU,EAAE,MAAM,CAAC;IACnB,mFAAmF;IACnF,kBAAkB,EAAE,MAAM,CAAC;CAC5B;AAED,wBAAgB,WAAW,CAAC,QAAQ,EAAE,gBAAgB,GAAG,QAAQ,CA4ChE"}
@@ -0,0 +1,43 @@
1
+ export function getDbConfig(database) {
2
+ switch (database) {
3
+ case 'postgres':
4
+ return {
5
+ prismaProvider: 'postgresql',
6
+ adapterPackage: '@prisma/adapter-pg',
7
+ adapterClass: 'PrismaPg',
8
+ envContent: 'DATABASE_URL="postgresql://user:password@localhost:5432/mydb"\n',
9
+ adapterConstructor: "new PrismaPg({ connectionString: process.env['DATABASE_URL']! })",
10
+ };
11
+ case 'mysql':
12
+ return {
13
+ prismaProvider: 'mysql',
14
+ adapterPackage: '@prisma/adapter-mariadb',
15
+ adapterClass: 'PrismaMariaDb',
16
+ envContent: [
17
+ '# PrismaMariaDb takes discrete connection fields, not a single URL',
18
+ 'MYSQL_HOST=localhost',
19
+ 'MYSQL_PORT=3306',
20
+ 'MYSQL_USER=root',
21
+ 'MYSQL_PASSWORD=',
22
+ 'MYSQL_DATABASE=mydb',
23
+ '',
24
+ ].join('\n'),
25
+ adapterConstructor: `new PrismaMariaDb({
26
+ host: process.env['MYSQL_HOST'] ?? 'localhost',
27
+ port: Number(process.env['MYSQL_PORT']) || 3306,
28
+ user: process.env['MYSQL_USER'],
29
+ password: process.env['MYSQL_PASSWORD'],
30
+ database: process.env['MYSQL_DATABASE'],
31
+ })`,
32
+ };
33
+ case 'sqlite':
34
+ default:
35
+ return {
36
+ prismaProvider: 'sqlite',
37
+ adapterPackage: '@prisma/adapter-better-sqlite3',
38
+ adapterClass: 'PrismaBetterSqlite3',
39
+ envContent: 'DATABASE_URL="file:./dev.db"\n',
40
+ adapterConstructor: "new PrismaBetterSqlite3({ url: process.env['DATABASE_URL'] ?? 'file:./dev.db' })",
41
+ };
42
+ }
43
+ }
@@ -0,0 +1,3 @@
1
+ import type { ResourceNames } from '../utils/strings.js';
2
+ export declare function formTemplate(n: ResourceNames): string;
3
+ //# sourceMappingURL=form.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"form.d.ts","sourceRoot":"","sources":["../../src/templates/form.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC;AAEzD,wBAAgB,YAAY,CAAC,CAAC,EAAE,aAAa,GAAG,MAAM,CAUrD"}
@@ -0,0 +1,11 @@
1
+ export function formTemplate(n) {
2
+ return `import { FormSchema } from '@kargojs/core';
3
+ import { TextInput } from '@kargojs/forms';
4
+
5
+ export function ${n.camel}Form(): FormSchema {
6
+ return FormSchema.make([
7
+ TextInput.make('name').label('Name').required(),
8
+ ]);
9
+ }
10
+ `;
11
+ }
@@ -0,0 +1,7 @@
1
+ import { type DatabaseProvider } from './database.js';
2
+ /** All files generated by `kargo create <name>`. Returns an array of {path, content}. */
3
+ export declare function projectFiles(name: string, database?: DatabaseProvider): Array<{
4
+ path: string;
5
+ content: string;
6
+ }>;
7
+ //# sourceMappingURL=project.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"project.d.ts","sourceRoot":"","sources":["../../src/templates/project.ts"],"names":[],"mappings":"AAAA,OAAO,EAAe,KAAK,gBAAgB,EAAE,MAAM,eAAe,CAAC;AAEnE,yFAAyF;AACzF,wBAAgB,YAAY,CAC1B,IAAI,EAAE,MAAM,EACZ,QAAQ,GAAE,gBAA2B,GACpC,KAAK,CAAC;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAE,CAAC,CAge1C"}