@assemora/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.
- package/LICENSE +202 -0
- package/README.md +272 -0
- package/bin.mjs +31 -0
- package/dist/args.d.ts +35 -0
- package/dist/args.d.ts.map +1 -0
- package/dist/args.js +104 -0
- package/dist/args.js.map +1 -0
- package/dist/bin.d.ts +3 -0
- package/dist/bin.d.ts.map +1 -0
- package/dist/bin.js +11 -0
- package/dist/bin.js.map +1 -0
- package/dist/commands/agents.d.ts +50 -0
- package/dist/commands/agents.d.ts.map +1 -0
- package/dist/commands/agents.js +169 -0
- package/dist/commands/agents.js.map +1 -0
- package/dist/commands/artifacts.d.ts +5 -0
- package/dist/commands/artifacts.d.ts.map +1 -0
- package/dist/commands/artifacts.js +126 -0
- package/dist/commands/artifacts.js.map +1 -0
- package/dist/commands/console.d.ts +25 -0
- package/dist/commands/console.d.ts.map +1 -0
- package/dist/commands/console.js +99 -0
- package/dist/commands/console.js.map +1 -0
- package/dist/commands/db.d.ts +48 -0
- package/dist/commands/db.d.ts.map +1 -0
- package/dist/commands/db.js +585 -0
- package/dist/commands/db.js.map +1 -0
- package/dist/commands/index.d.ts +21 -0
- package/dist/commands/index.d.ts.map +1 -0
- package/dist/commands/index.js +21 -0
- package/dist/commands/index.js.map +1 -0
- package/dist/commands/inspect.d.ts +8 -0
- package/dist/commands/inspect.d.ts.map +1 -0
- package/dist/commands/inspect.js +280 -0
- package/dist/commands/inspect.js.map +1 -0
- package/dist/commands/make.d.ts +29 -0
- package/dist/commands/make.d.ts.map +1 -0
- package/dist/commands/make.js +393 -0
- package/dist/commands/make.js.map +1 -0
- package/dist/commands/mcp.d.ts +19 -0
- package/dist/commands/mcp.d.ts.map +1 -0
- package/dist/commands/mcp.js +116 -0
- package/dist/commands/mcp.js.map +1 -0
- package/dist/commands/new.d.ts +20 -0
- package/dist/commands/new.d.ts.map +1 -0
- package/dist/commands/new.js +83 -0
- package/dist/commands/new.js.map +1 -0
- package/dist/commands/run.d.ts +20 -0
- package/dist/commands/run.d.ts.map +1 -0
- package/dist/commands/run.js +355 -0
- package/dist/commands/run.js.map +1 -0
- package/dist/commands/watchdog.d.ts +2 -0
- package/dist/commands/watchdog.d.ts.map +1 -0
- package/dist/commands/watchdog.js +74 -0
- package/dist/commands/watchdog.js.map +1 -0
- package/dist/config.d.ts +89 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +156 -0
- package/dist/config.js.map +1 -0
- package/dist/index.d.ts +19 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +133 -0
- package/dist/index.js.map +1 -0
- package/dist/output.d.ts +96 -0
- package/dist/output.d.ts.map +1 -0
- package/dist/output.js +181 -0
- package/dist/output.js.map +1 -0
- package/dist/project.d.ts +44 -0
- package/dist/project.d.ts.map +1 -0
- package/dist/project.js +102 -0
- package/dist/project.js.map +1 -0
- package/dist/registry.d.ts +48 -0
- package/dist/registry.d.ts.map +1 -0
- package/dist/registry.js +74 -0
- package/dist/registry.js.map +1 -0
- package/package.json +49 -0
package/dist/bin.js
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* The `assemora` executable.
|
|
4
|
+
*
|
|
5
|
+
* It does the one thing `run()` deliberately does not: end the process. Setting
|
|
6
|
+
* `process.exitCode` rather than calling `process.exit()` lets Node flush what was
|
|
7
|
+
* written first — `process.exit()` truncates a piped listing halfway through a line.
|
|
8
|
+
*/
|
|
9
|
+
import { run } from './index.js';
|
|
10
|
+
process.exitCode = await run(process.argv.slice(2));
|
|
11
|
+
//# sourceMappingURL=bin.js.map
|
package/dist/bin.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"bin.js","sourceRoot":"","sources":["../src/bin.ts"],"names":[],"mappings":";AACA;;;;;;GAMG;AACH,OAAO,EAAE,GAAG,EAAE,MAAM,YAAY,CAAA;AAEhC,OAAO,CAAC,QAAQ,GAAG,MAAM,GAAG,CAAC,OAAO,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAA"}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { type ParsedArgs } from '../args.js';
|
|
2
|
+
/** Where `--write-mcp-json` writes when it is given no path. */
|
|
3
|
+
export declare const MCP_CONFIG_FILE = ".mcp.json";
|
|
4
|
+
/** Where a project keeps its secrets, and where the token goes. */
|
|
5
|
+
export declare const ENV_FILE = ".env";
|
|
6
|
+
/**
|
|
7
|
+
* The permissions, from a comma-separated list.
|
|
8
|
+
*
|
|
9
|
+
* One flag rather than a repeated one, because the parser keeps the last value of a
|
|
10
|
+
* repeated flag and `--permissions a --permissions b` would silently mean `b` — a
|
|
11
|
+
* narrower agent than the person asked for, which is the direction that fails quietly.
|
|
12
|
+
*
|
|
13
|
+
* Empty entries are dropped rather than passed on: `--permissions "pages.read,"` is a
|
|
14
|
+
* trailing comma, not a request for a permission with no name.
|
|
15
|
+
*/
|
|
16
|
+
export declare const permissionsOf: (args: ParsedArgs) => readonly string[];
|
|
17
|
+
/**
|
|
18
|
+
* The client configuration, as a client reads it — and with no credential in it.
|
|
19
|
+
*
|
|
20
|
+
* `pnpm assemora mcp` rather than a path: the executable is a dependency of the project
|
|
21
|
+
* rather than a global one, and the package manager that put it there is the one thing
|
|
22
|
+
* this file cannot know from the outside. `cwd` is written absolute, because a client
|
|
23
|
+
* starts the process from wherever it happens to be.
|
|
24
|
+
*
|
|
25
|
+
* No `env`, deliberately. The token would make this a secret file, and it is the file
|
|
26
|
+
* most likely to be committed: it is the project's client configuration, it is the same
|
|
27
|
+
* for everybody working on it, and it is the sort of thing somebody adds to a
|
|
28
|
+
* repository without thinking. So the token goes where this project already keeps its
|
|
29
|
+
* secrets — `.env`, which is gitignored and which the project reads as it is imported —
|
|
30
|
+
* and the process the client starts inherits it.
|
|
31
|
+
*/
|
|
32
|
+
export declare const mcpConfig: (project: string, name: string) => {
|
|
33
|
+
mcpServers: {
|
|
34
|
+
[x: string]: {
|
|
35
|
+
command: string;
|
|
36
|
+
args: string[];
|
|
37
|
+
cwd: string;
|
|
38
|
+
};
|
|
39
|
+
};
|
|
40
|
+
};
|
|
41
|
+
/**
|
|
42
|
+
* Writes `name=value` into a project's `.env`, replacing whatever that name said.
|
|
43
|
+
*
|
|
44
|
+
* Replacing rather than appending: a second agent for the same project is the ordinary
|
|
45
|
+
* case, and a file that grew a line each time would end up a column of dead
|
|
46
|
+
* credentials with no way to tell which one is live. Every other line is carried
|
|
47
|
+
* through untouched, so a `.env` written by hand keeps its comments and its order.
|
|
48
|
+
*/
|
|
49
|
+
export declare const remember: (file: string, name: string, value: string) => Promise<void>;
|
|
50
|
+
//# sourceMappingURL=agents.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"agents.d.ts","sourceRoot":"","sources":["../../src/commands/agents.ts"],"names":[],"mappings":"AAoBA,OAAO,EAAc,KAAK,UAAU,EAAE,MAAM,YAAY,CAAA;AASxD,gEAAgE;AAChE,eAAO,MAAM,eAAe,cAAc,CAAA;AAE1C,mEAAmE;AACnE,eAAO,MAAM,QAAQ,SAAS,CAAA;AAE9B;;;;;;;;;GASG;AACH,eAAO,MAAM,aAAa,SAAU,UAAU,KAAG,SAAS,MAAM,EAI5B,CAAA;AAEpC;;;;;;;;;;;;;;GAcG;AACH,eAAO,MAAM,SAAS,YAAa,MAAM,QAAQ,MAAM;;;;;;;;CAQrD,CAAA;AAEF;;;;;;;GAOG;AACH,eAAO,MAAM,QAAQ,SAAgB,MAAM,QAAQ,MAAM,SAAS,MAAM,KAAG,OAAO,CAAC,IAAI,CActF,CAAA"}
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `assemora agents:create` — an agent identity and the token that is it (SPEC.md §72).
|
|
3
|
+
*
|
|
4
|
+
* Until this existed there was no documented way to get one. The command was on the
|
|
5
|
+
* bus and reachable through the generic `POST /api/commands/auth.agents.create`, which
|
|
6
|
+
* needs an administrator session first and is written down nowhere; Studio's Agents tab
|
|
7
|
+
* lists and enables; `assemora agents` lists. So the answer to "how do I connect an
|
|
8
|
+
* agent" was to read the source, and everybody who found `/api/mcp` instead met a 401
|
|
9
|
+
* with no route past it.
|
|
10
|
+
*
|
|
11
|
+
* It reimplements nothing: the identity is created by `auth.agents.create` on the
|
|
12
|
+
* Command Bus, so it is validated, authorized, audited and refused exactly as it is
|
|
13
|
+
* from anywhere else (ADR-0021). What this adds is the part a terminal is good at —
|
|
14
|
+
* showing the token once, and writing the file a client reads.
|
|
15
|
+
*/
|
|
16
|
+
import { chmod, readFile, writeFile } from 'node:fs/promises';
|
|
17
|
+
import { relative, resolve } from 'node:path';
|
|
18
|
+
import { bool, flag } from '../args.js';
|
|
19
|
+
import { loadConfig } from '../config.js';
|
|
20
|
+
import { detail, fail, line, ok, warn } from '../output.js';
|
|
21
|
+
import { loadApplication } from '../project.js';
|
|
22
|
+
import { defineCommand, register } from '../registry.js';
|
|
23
|
+
import { TOKEN_VARIABLE } from './mcp.js';
|
|
24
|
+
const CREATE = 'auth.agents.create';
|
|
25
|
+
/** Where `--write-mcp-json` writes when it is given no path. */
|
|
26
|
+
export const MCP_CONFIG_FILE = '.mcp.json';
|
|
27
|
+
/** Where a project keeps its secrets, and where the token goes. */
|
|
28
|
+
export const ENV_FILE = '.env';
|
|
29
|
+
/**
|
|
30
|
+
* The permissions, from a comma-separated list.
|
|
31
|
+
*
|
|
32
|
+
* One flag rather than a repeated one, because the parser keeps the last value of a
|
|
33
|
+
* repeated flag and `--permissions a --permissions b` would silently mean `b` — a
|
|
34
|
+
* narrower agent than the person asked for, which is the direction that fails quietly.
|
|
35
|
+
*
|
|
36
|
+
* Empty entries are dropped rather than passed on: `--permissions "pages.read,"` is a
|
|
37
|
+
* trailing comma, not a request for a permission with no name.
|
|
38
|
+
*/
|
|
39
|
+
export const permissionsOf = (args) => (flag(args, 'permissions') ?? '')
|
|
40
|
+
.split(',')
|
|
41
|
+
.map((entry) => entry.trim())
|
|
42
|
+
.filter((entry) => entry !== '');
|
|
43
|
+
/**
|
|
44
|
+
* The client configuration, as a client reads it — and with no credential in it.
|
|
45
|
+
*
|
|
46
|
+
* `pnpm assemora mcp` rather than a path: the executable is a dependency of the project
|
|
47
|
+
* rather than a global one, and the package manager that put it there is the one thing
|
|
48
|
+
* this file cannot know from the outside. `cwd` is written absolute, because a client
|
|
49
|
+
* starts the process from wherever it happens to be.
|
|
50
|
+
*
|
|
51
|
+
* No `env`, deliberately. The token would make this a secret file, and it is the file
|
|
52
|
+
* most likely to be committed: it is the project's client configuration, it is the same
|
|
53
|
+
* for everybody working on it, and it is the sort of thing somebody adds to a
|
|
54
|
+
* repository without thinking. So the token goes where this project already keeps its
|
|
55
|
+
* secrets — `.env`, which is gitignored and which the project reads as it is imported —
|
|
56
|
+
* and the process the client starts inherits it.
|
|
57
|
+
*/
|
|
58
|
+
export const mcpConfig = (project, name) => ({
|
|
59
|
+
mcpServers: {
|
|
60
|
+
[name]: {
|
|
61
|
+
command: 'pnpm',
|
|
62
|
+
args: ['assemora', 'mcp'],
|
|
63
|
+
cwd: project,
|
|
64
|
+
},
|
|
65
|
+
},
|
|
66
|
+
});
|
|
67
|
+
/**
|
|
68
|
+
* Writes `name=value` into a project's `.env`, replacing whatever that name said.
|
|
69
|
+
*
|
|
70
|
+
* Replacing rather than appending: a second agent for the same project is the ordinary
|
|
71
|
+
* case, and a file that grew a line each time would end up a column of dead
|
|
72
|
+
* credentials with no way to tell which one is live. Every other line is carried
|
|
73
|
+
* through untouched, so a `.env` written by hand keeps its comments and its order.
|
|
74
|
+
*/
|
|
75
|
+
export const remember = async (file, name, value) => {
|
|
76
|
+
const existing = await readFile(file, 'utf8').catch(() => '');
|
|
77
|
+
const kept = existing
|
|
78
|
+
.split('\n')
|
|
79
|
+
.filter((line) => !line.startsWith(`${name}=`))
|
|
80
|
+
.join('\n')
|
|
81
|
+
.trimEnd();
|
|
82
|
+
await writeFile(file, `${kept === '' ? '' : `${kept}\n`}${name}=${value}\n`, { mode: 0o600 });
|
|
83
|
+
// `writeFile`'s mode applies only to a file it creates, so an existing `.env` keeps
|
|
84
|
+
// whatever permissions it had. Narrow it either way: it holds a credential now.
|
|
85
|
+
await chmod(file, 0o600);
|
|
86
|
+
};
|
|
87
|
+
/** `Content agent` becomes `content-agent`, which is what a client shows in a list. */
|
|
88
|
+
const slug = (name) => name
|
|
89
|
+
.toLowerCase()
|
|
90
|
+
.replace(/[^a-z0-9]+/g, '-')
|
|
91
|
+
.replace(/^-+|-+$/g, '') || 'assemora';
|
|
92
|
+
const refusalHint = (actorId) => actorId === undefined
|
|
93
|
+
? 'Creating an agent is authorized like every other command, and no actor was named. ' +
|
|
94
|
+
'Pass --actor <user id> — a user who holds auth.agents.create.'
|
|
95
|
+
: `${actorId} may not create an agent, or may not grant one of those permissions: an ` +
|
|
96
|
+
'actor cannot hand an agent a permission it does not hold itself (SPEC.md §72).';
|
|
97
|
+
const isRefusal = (error) => {
|
|
98
|
+
const status = error.status;
|
|
99
|
+
return typeof status === 'number' && status >= 400 && status < 500;
|
|
100
|
+
};
|
|
101
|
+
const agentsCreate = async ({ args, cwd }) => {
|
|
102
|
+
const name = args.positionals[0];
|
|
103
|
+
if (name === undefined || name.trim() === '') {
|
|
104
|
+
fail('An agent needs a name. It is what a person reads in the audit log.');
|
|
105
|
+
detail('assemora agents:create "Content agent" --permissions pages.read,blocks.update');
|
|
106
|
+
return 2;
|
|
107
|
+
}
|
|
108
|
+
const permissions = permissionsOf(args);
|
|
109
|
+
if (permissions.length === 0) {
|
|
110
|
+
fail('An agent with no permissions can reach every tool and do none of them.');
|
|
111
|
+
detail('Pass --permissions, comma-separated: --permissions pages.read,blocks.update');
|
|
112
|
+
return 2;
|
|
113
|
+
}
|
|
114
|
+
const app = await loadApplication(await loadConfig(cwd));
|
|
115
|
+
if (!app.commands.has(CREATE)) {
|
|
116
|
+
fail(`This application registers no "${CREATE}", so it has no agent identities to create. ` +
|
|
117
|
+
'They come with @assemora/auth (SPEC.md §72).');
|
|
118
|
+
return 1;
|
|
119
|
+
}
|
|
120
|
+
const named = flag(args, 'actor');
|
|
121
|
+
const actor = named === undefined ? undefined : { type: 'user', id: named };
|
|
122
|
+
const context = { source: 'cli', ...(actor === undefined ? {} : { actor }) };
|
|
123
|
+
const description = flag(args, 'description');
|
|
124
|
+
let created;
|
|
125
|
+
try {
|
|
126
|
+
created = (await app.run(context, () => app.commands.execute(CREATE, {
|
|
127
|
+
name: name.trim(),
|
|
128
|
+
permissions,
|
|
129
|
+
...(description === undefined ? {} : { description }),
|
|
130
|
+
})));
|
|
131
|
+
}
|
|
132
|
+
catch (error) {
|
|
133
|
+
if (!isRefusal(error))
|
|
134
|
+
throw error;
|
|
135
|
+
fail(error instanceof Error ? error.message : String(error));
|
|
136
|
+
detail(refusalHint(named));
|
|
137
|
+
return 1;
|
|
138
|
+
}
|
|
139
|
+
ok(`Created ${name.trim()} — ${permissions.length} permission${permissions.length === 1 ? '' : 's'}.`);
|
|
140
|
+
line();
|
|
141
|
+
line(created.token);
|
|
142
|
+
line();
|
|
143
|
+
// Said plainly, because it is true and because the next thing somebody does is close
|
|
144
|
+
// the terminal. The row stores a SHA-256 digest of it and nothing else, so there is
|
|
145
|
+
// no second chance and no support call that can recover it.
|
|
146
|
+
warn('That token is shown once. It is stored hashed, so nothing can print it again.');
|
|
147
|
+
if (!bool(args, 'write-mcp-json')) {
|
|
148
|
+
detail(`Put it in .env as ${TOKEN_VARIABLE}, or pass --write-mcp-json next time.`);
|
|
149
|
+
return 0;
|
|
150
|
+
}
|
|
151
|
+
const target = resolve(cwd, flag(args, 'write-mcp-json') ?? MCP_CONFIG_FILE);
|
|
152
|
+
// Two files, and the split is the point: the credential goes where this project
|
|
153
|
+
// already keeps its secrets, and the client configuration — which is the same for
|
|
154
|
+
// everybody working here and is the sort of file that gets committed — holds none.
|
|
155
|
+
await remember(resolve(cwd, ENV_FILE), TOKEN_VARIABLE, created.token);
|
|
156
|
+
await writeFile(target, `${JSON.stringify(mcpConfig(cwd, slug(name)), null, 2)}\n`);
|
|
157
|
+
line();
|
|
158
|
+
ok(`Wrote ${ENV_FILE} and ${relative(cwd, target) || target}`);
|
|
159
|
+
detail(`${ENV_FILE} holds the token and is the one to keep out of git.`);
|
|
160
|
+
return 0;
|
|
161
|
+
};
|
|
162
|
+
register(defineCommand({
|
|
163
|
+
name: 'agents:create',
|
|
164
|
+
group: 'identity',
|
|
165
|
+
summary: 'create an agent identity and print its token once',
|
|
166
|
+
usage: 'assemora agents:create <name> --permissions <a,b> [--description <text>] [--actor <id>] [--write-mcp-json [path]]',
|
|
167
|
+
handler: agentsCreate,
|
|
168
|
+
}));
|
|
169
|
+
//# sourceMappingURL=agents.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"agents.js","sourceRoot":"","sources":["../../src/commands/agents.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export declare const OpenApi: import("../registry.js").CliCommand;
|
|
2
|
+
export declare const Sdk: import("../registry.js").CliCommand;
|
|
3
|
+
/** In the order SPEC.md §77 lists them, which is the order the help prints them in. */
|
|
4
|
+
export declare const artifactCommands: readonly [import("../registry.js").CliCommand, import("../registry.js").CliCommand];
|
|
5
|
+
//# sourceMappingURL=artifacts.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"artifacts.d.ts","sourceRoot":"","sources":["../../src/commands/artifacts.ts"],"names":[],"mappings":"AA2GA,eAAO,MAAM,OAAO,qCAoBlB,CAAA;AAEF,eAAO,MAAM,GAAG,qCA0Bd,CAAA;AAEF,uFAAuF;AACvF,eAAO,MAAM,gBAAgB,qFAA0B,CAAA"}
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `assemora api:openapi` and `assemora sdk:generate` (SPEC.md §44, §48, §77).
|
|
3
|
+
*
|
|
4
|
+
* Both have the same shape: boot the project's application, hand its Schema Registry
|
|
5
|
+
* to a generator, and put the answer somewhere. Neither writes a line of the artifact
|
|
6
|
+
* itself — everything emitted is generated from what the application declared, which
|
|
7
|
+
* is what keeps a document current by construction rather than by discipline
|
|
8
|
+
* (SPEC.md §3.7). An application declaring nothing is not a special case; it produces
|
|
9
|
+
* a document and a client that describe nothing, which is the truth about it.
|
|
10
|
+
*
|
|
11
|
+
* The two generators are imported inside their handlers. `@assemora/openapi` also
|
|
12
|
+
* ships the routes that publish the document, so importing it reaches `@assemora/http`
|
|
13
|
+
* and the server library under that — a cost `assemora --help` should not pay.
|
|
14
|
+
*/
|
|
15
|
+
import { mkdir, readFile, writeFile } from 'node:fs/promises';
|
|
16
|
+
import { dirname, isAbsolute, join, relative } from 'node:path';
|
|
17
|
+
import { bool, flag } from '../args.js';
|
|
18
|
+
import { loadConfig } from '../config.js';
|
|
19
|
+
import { line, ok } from '../output.js';
|
|
20
|
+
import { loadApplication } from '../project.js';
|
|
21
|
+
import { defineCommand, register } from '../registry.js';
|
|
22
|
+
/** The path as the reader would retype it: from where they are, unless that is worse. */
|
|
23
|
+
const shortest = (path, cwd) => {
|
|
24
|
+
const near = relative(cwd, path);
|
|
25
|
+
return near === '' || near.startsWith('..') || isAbsolute(near) ? path : near;
|
|
26
|
+
};
|
|
27
|
+
/**
|
|
28
|
+
* Where a generated artifact goes.
|
|
29
|
+
*
|
|
30
|
+
* `--out` beats the config, the config beats the default, and `--stdout` beats all
|
|
31
|
+
* three by naming no file at all. Nothing else is written when it is piped: the
|
|
32
|
+
* "wrote it here" line answers "where did this go", and when the answer is "into your
|
|
33
|
+
* pipe" there is nothing left to say.
|
|
34
|
+
*/
|
|
35
|
+
const emit = async (input) => {
|
|
36
|
+
// Exactly one newline at the end, whatever the generator left: `generateSdk` ends
|
|
37
|
+
// with its own and `JSON.stringify` ends with none.
|
|
38
|
+
const body = input.contents.replace(/\n+$/, '');
|
|
39
|
+
if (bool(input.args, 'stdout')) {
|
|
40
|
+
line(body);
|
|
41
|
+
return 0;
|
|
42
|
+
}
|
|
43
|
+
const path = input.loaded.resolve(flag(input.args, 'out') ?? input.declared ?? input.fallback);
|
|
44
|
+
// A project that declares `src/generated/sdk.ts` should not have to create the
|
|
45
|
+
// directory before the command that fills it will run.
|
|
46
|
+
await mkdir(dirname(path), { recursive: true });
|
|
47
|
+
const bytes = `${body}\n`;
|
|
48
|
+
await writeFile(path, bytes, 'utf8');
|
|
49
|
+
ok(`Wrote ${shortest(path, input.cwd)} (${Buffer.byteLength(bytes)} bytes)`);
|
|
50
|
+
return 0;
|
|
51
|
+
};
|
|
52
|
+
const DEFAULT_OPENAPI_OUT = 'openapi.json';
|
|
53
|
+
const UNNAMED_PROJECT = { title: 'Assemora application', version: '0.0.0' };
|
|
54
|
+
const stringAt = (value, key) => {
|
|
55
|
+
const found = typeof value === 'object' && value !== null
|
|
56
|
+
? value[key]
|
|
57
|
+
: undefined;
|
|
58
|
+
return typeof found === 'string' && found !== '' ? found : undefined;
|
|
59
|
+
};
|
|
60
|
+
/**
|
|
61
|
+
* What the document says it describes.
|
|
62
|
+
*
|
|
63
|
+
* The config decides; where it says nothing the project's own `package.json` does. A
|
|
64
|
+
* document titled after the project is right far more often than one titled after the
|
|
65
|
+
* framework, and nobody should have to write down what the package manifest already
|
|
66
|
+
* knows.
|
|
67
|
+
*/
|
|
68
|
+
const infoFor = async (loaded) => {
|
|
69
|
+
const declared = loaded.config.openapi?.info;
|
|
70
|
+
if (declared !== undefined)
|
|
71
|
+
return declared;
|
|
72
|
+
const manifest = await readFile(loaded.resolve('package.json'), 'utf8')
|
|
73
|
+
.then((contents) => JSON.parse(contents))
|
|
74
|
+
.catch(() => undefined);
|
|
75
|
+
return {
|
|
76
|
+
title: stringAt(manifest, 'name') ?? UNNAMED_PROJECT.title,
|
|
77
|
+
version: stringAt(manifest, 'version') ?? UNNAMED_PROJECT.version,
|
|
78
|
+
};
|
|
79
|
+
};
|
|
80
|
+
export const OpenApi = defineCommand({
|
|
81
|
+
name: 'api:openapi',
|
|
82
|
+
group: 'artifacts',
|
|
83
|
+
summary: 'write the OpenAPI 3.1 document this application describes itself with',
|
|
84
|
+
usage: 'assemora api:openapi [--out <file>] [--stdout]',
|
|
85
|
+
handler: async ({ args, cwd }) => {
|
|
86
|
+
const loaded = await loadConfig(cwd);
|
|
87
|
+
const app = await loadApplication(loaded);
|
|
88
|
+
const { buildOpenApiDocument } = await import('@assemora/openapi');
|
|
89
|
+
return emit({
|
|
90
|
+
args,
|
|
91
|
+
cwd,
|
|
92
|
+
loaded,
|
|
93
|
+
declared: loaded.config.openapi?.out,
|
|
94
|
+
fallback: DEFAULT_OPENAPI_OUT,
|
|
95
|
+
contents: JSON.stringify(buildOpenApiDocument(app.registry, await infoFor(loaded)), null, 2),
|
|
96
|
+
});
|
|
97
|
+
},
|
|
98
|
+
});
|
|
99
|
+
export const Sdk = defineCommand({
|
|
100
|
+
name: 'sdk:generate',
|
|
101
|
+
group: 'artifacts',
|
|
102
|
+
summary: 'write the typed client for this application',
|
|
103
|
+
usage: 'assemora sdk:generate [--out <file>] [--stdout]',
|
|
104
|
+
handler: async ({ args, cwd }) => {
|
|
105
|
+
const loaded = await loadConfig(cwd);
|
|
106
|
+
const app = await loadApplication(loaded);
|
|
107
|
+
const { generateSdk } = await import('@assemora/sdk');
|
|
108
|
+
const declared = loaded.config.sdk;
|
|
109
|
+
return emit({
|
|
110
|
+
args,
|
|
111
|
+
cwd,
|
|
112
|
+
loaded,
|
|
113
|
+
declared: declared?.out,
|
|
114
|
+
// Beside the project's own source rather than at its root: the client is
|
|
115
|
+
// imported by the application's code, and `paths.source` is where that lives.
|
|
116
|
+
fallback: join(loaded.paths.source, 'generated', 'sdk.ts'),
|
|
117
|
+
contents: generateSdk(app.registry.describe(), {
|
|
118
|
+
...(declared?.clientModule === undefined ? {} : { clientModule: declared.clientModule }),
|
|
119
|
+
}),
|
|
120
|
+
});
|
|
121
|
+
},
|
|
122
|
+
});
|
|
123
|
+
/** In the order SPEC.md §77 lists them, which is the order the help prints them in. */
|
|
124
|
+
export const artifactCommands = [OpenApi, Sdk];
|
|
125
|
+
register(...artifactCommands);
|
|
126
|
+
//# sourceMappingURL=artifacts.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"artifacts.js","sourceRoot":"","sources":["../../src/commands/artifacts.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type { Application } from '@assemora/core';
|
|
2
|
+
/**
|
|
3
|
+
* What the console needs of an application, which is less than all of it.
|
|
4
|
+
*
|
|
5
|
+
* Naming the four members here is what lets a test hand over something small, and it
|
|
6
|
+
* says plainly that the console reaches for nothing else.
|
|
7
|
+
*/
|
|
8
|
+
export type ConsoleApplication = Pick<Application, 'commands' | 'queries' | 'registry' | 'run' | 'shutdown'>;
|
|
9
|
+
export type ConsoleSession = {
|
|
10
|
+
/** Defaults to `process.stdin`. */
|
|
11
|
+
readonly input?: NodeJS.ReadableStream;
|
|
12
|
+
/** Defaults to `process.stdout`. */
|
|
13
|
+
readonly output?: NodeJS.WritableStream;
|
|
14
|
+
/** Defaults to whether stdin is a terminal. */
|
|
15
|
+
readonly terminal?: boolean;
|
|
16
|
+
};
|
|
17
|
+
/**
|
|
18
|
+
* Opens the REPL and resolves once it has closed and the application has stopped.
|
|
19
|
+
*
|
|
20
|
+
* The application is a parameter rather than something this function loads, so the
|
|
21
|
+
* whole of the console's behaviour — what is in scope, what `.exit` does, whether it
|
|
22
|
+
* returns at all — is drivable from a test with two streams.
|
|
23
|
+
*/
|
|
24
|
+
export declare const openConsole: (app: ConsoleApplication, session?: ConsoleSession) => Promise<void>;
|
|
25
|
+
//# sourceMappingURL=console.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"console.d.ts","sourceRoot":"","sources":["../../src/commands/console.ts"],"names":[],"mappings":"AAgBA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,gBAAgB,CAAA;AAOjD;;;;;GAKG;AACH,MAAM,MAAM,kBAAkB,GAAG,IAAI,CACnC,WAAW,EACX,UAAU,GAAG,SAAS,GAAG,UAAU,GAAG,KAAK,GAAG,UAAU,CACzD,CAAA;AAED,MAAM,MAAM,cAAc,GAAG;IAC3B,mCAAmC;IACnC,QAAQ,CAAC,KAAK,CAAC,EAAE,MAAM,CAAC,cAAc,CAAA;IACtC,oCAAoC;IACpC,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC,cAAc,CAAA;IACvC,+CAA+C;IAC/C,QAAQ,CAAC,QAAQ,CAAC,EAAE,OAAO,CAAA;CAC5B,CAAA;AAED;;;;;;GAMG;AACH,eAAO,MAAM,WAAW,QACjB,kBAAkB,YACd,cAAc,KACtB,OAAO,CAAC,IAAI,CAqEd,CAAA"}
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `assemora console` — a REPL holding the project's application (SPEC.md §77).
|
|
3
|
+
*
|
|
4
|
+
* The console is not a back door. What it puts in scope is the Command Bus, the
|
|
5
|
+
* Query Bus and the Schema Registry — the same three doors Studio, REST and MCP go
|
|
6
|
+
* through — so a mutation typed here passes validation, authorization, revisions and
|
|
7
|
+
* audit exactly as one typed anywhere else does (ADR-0021). There is no database
|
|
8
|
+
* handle in scope, and this package could not obtain one.
|
|
9
|
+
*
|
|
10
|
+
* The one thing a console must get right is leaving. `.exit` closes the application
|
|
11
|
+
* so the pool it opened goes with it, and the command does not answer until the REPL
|
|
12
|
+
* has closed and released the input it was reading — a REPL that prints its goodbye
|
|
13
|
+
* and then holds the terminal is the failure this file is written against.
|
|
14
|
+
*/
|
|
15
|
+
import { start } from 'node:repl';
|
|
16
|
+
import { loadConfig } from '../config.js';
|
|
17
|
+
import { line } from '../output.js';
|
|
18
|
+
import { loadApplication } from '../project.js';
|
|
19
|
+
import { defineCommand, register } from '../registry.js';
|
|
20
|
+
/**
|
|
21
|
+
* Opens the REPL and resolves once it has closed and the application has stopped.
|
|
22
|
+
*
|
|
23
|
+
* The application is a parameter rather than something this function loads, so the
|
|
24
|
+
* whole of the console's behaviour — what is in scope, what `.exit` does, whether it
|
|
25
|
+
* returns at all — is drivable from a test with two streams.
|
|
26
|
+
*/
|
|
27
|
+
export const openConsole = async (app, session = {}) => {
|
|
28
|
+
const input = session.input ?? process.stdin;
|
|
29
|
+
const output = session.output ?? process.stdout;
|
|
30
|
+
const terminal = session.terminal ?? process.stdin.isTTY === true;
|
|
31
|
+
/**
|
|
32
|
+
* Runs an operation as a user, inside a context the application can see.
|
|
33
|
+
*
|
|
34
|
+
* A read typed here is authorized exactly as a read from Studio is, so
|
|
35
|
+
* `queries.execute('auth.agents.list', {})` answers nothing until it is told who
|
|
36
|
+
* is asking. `as` is how the console says who that is (SPEC.md §12).
|
|
37
|
+
*/
|
|
38
|
+
const as = (actorId, operation) => app.run({ source: 'cli', actor: { type: 'user', id: actorId } }, async () => operation());
|
|
39
|
+
const repl = start({
|
|
40
|
+
input,
|
|
41
|
+
output,
|
|
42
|
+
terminal,
|
|
43
|
+
useColors: terminal,
|
|
44
|
+
prompt: 'assemora > ',
|
|
45
|
+
// A context of its own rather than the CLI's globals: `commands` and `queries`
|
|
46
|
+
// are the names an operator reaches for first, and neither should be able to
|
|
47
|
+
// collide with something this process happens to have defined.
|
|
48
|
+
useGlobal: false,
|
|
49
|
+
ignoreUndefined: true,
|
|
50
|
+
});
|
|
51
|
+
const scope = {
|
|
52
|
+
app,
|
|
53
|
+
commands: app.commands,
|
|
54
|
+
queries: app.queries,
|
|
55
|
+
registry: app.registry,
|
|
56
|
+
as,
|
|
57
|
+
};
|
|
58
|
+
// Defined rather than assigned, so `app = null` typed by accident is an error
|
|
59
|
+
// instead of the rest of the session being useless. `.clear` builds a fresh
|
|
60
|
+
// context, which is why they are put back on every reset.
|
|
61
|
+
const define = () => {
|
|
62
|
+
for (const [name, value] of Object.entries(scope)) {
|
|
63
|
+
Object.defineProperty(repl.context, name, { value, enumerable: true, configurable: true });
|
|
64
|
+
}
|
|
65
|
+
};
|
|
66
|
+
define();
|
|
67
|
+
repl.on('reset', define);
|
|
68
|
+
line('Assemora console. The application is booted; everything below is in scope.');
|
|
69
|
+
line();
|
|
70
|
+
line(' app the application itself');
|
|
71
|
+
line(" commands await commands.execute('entries.create', { resource: 'posts', data: {} })");
|
|
72
|
+
line(" queries await queries.execute('pages.list', {})");
|
|
73
|
+
line(' registry registry.describe()');
|
|
74
|
+
line(" as await as('user-id', () => queries.execute('auth.agents.list', {}))");
|
|
75
|
+
line();
|
|
76
|
+
line('.exit closes the application and leaves.');
|
|
77
|
+
await new Promise((resolve) => {
|
|
78
|
+
repl.once('exit', () => {
|
|
79
|
+
resolve();
|
|
80
|
+
});
|
|
81
|
+
});
|
|
82
|
+
// The REPL stops reading when it closes, but an input it was handed rather than
|
|
83
|
+
// one it created can still hold the event loop open, and stdin always does.
|
|
84
|
+
input.pause();
|
|
85
|
+
await app.shutdown();
|
|
86
|
+
};
|
|
87
|
+
const consoleCommand = async ({ cwd }) => {
|
|
88
|
+
const loaded = await loadConfig(cwd);
|
|
89
|
+
await openConsole(await loadApplication(loaded));
|
|
90
|
+
return 0;
|
|
91
|
+
};
|
|
92
|
+
register(defineCommand({
|
|
93
|
+
name: 'console',
|
|
94
|
+
group: 'console',
|
|
95
|
+
summary: 'open a REPL holding the booted application',
|
|
96
|
+
usage: 'assemora console',
|
|
97
|
+
handler: consoleCommand,
|
|
98
|
+
}));
|
|
99
|
+
//# sourceMappingURL=console.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"console.js","sourceRoot":"","sources":["../../src/commands/console.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AACH,OAAO,EAAE,KAAK,EAAE,MAAM,WAAW,CAAA;AAIjC,OAAO,EAAE,UAAU,EAAE,MAAM,cAAc,CAAA;AACzC,OAAO,EAAE,IAAI,EAAE,MAAM,cAAc,CAAA;AACnC,OAAO,EAAE,eAAe,EAAE,MAAM,eAAe,CAAA;AAC/C,OAAO,EAAuB,aAAa,EAAE,QAAQ,EAAE,MAAM,gBAAgB,CAAA;AAsB7E;;;;;;GAMG;AACH,MAAM,CAAC,MAAM,WAAW,GAAG,KAAK,EAC9B,GAAuB,EACvB,OAAO,GAAmB,EAAE,EACb,EAAE;IACjB,MAAM,KAAK,GAAG,OAAO,CAAC,KAAK,IAAI,OAAO,CAAC,KAAK,CAAA;IAC5C,MAAM,MAAM,GAAG,OAAO,CAAC,MAAM,IAAI,OAAO,CAAC,MAAM,CAAA;IAC/C,MAAM,QAAQ,GAAG,OAAO,CAAC,QAAQ,IAAI,OAAO,CAAC,KAAK,CAAC,KAAK,KAAK,IAAI,CAAA;IAEjE;;;;;;OAMG;IACH,MAAM,EAAE,GAAG,CAAI,OAAe,EAAE,SAA+B,EAAc,EAAE,CAC7E,GAAG,CAAC,GAAG,CAAC,EAAE,MAAM,EAAE,KAAK,EAAE,KAAK,EAAE,EAAE,IAAI,EAAE,MAAM,EAAE,EAAE,EAAE,OAAO,EAAE,EAAE,EAAE,KAAK,IAAI,EAAE,CAAC,SAAS,EAAE,CAAC,CAAA;IAE3F,MAAM,IAAI,GAAG,KAAK,CAAC;QACjB,KAAK;QACL,MAAM;QACN,QAAQ;QACR,SAAS,EAAE,QAAQ;QACnB,MAAM,EAAE,aAAa;QACrB,+EAA+E;QAC/E,6EAA6E;QAC7E,+DAA+D;QAC/D,SAAS,EAAE,KAAK;QAChB,eAAe,EAAE,IAAI;KACtB,CAAC,CAAA;IAEF,MAAM,KAAK,GAAG;QACZ,GAAG;QACH,QAAQ,EAAE,GAAG,CAAC,QAAQ;QACtB,OAAO,EAAE,GAAG,CAAC,OAAO;QACpB,QAAQ,EAAE,GAAG,CAAC,QAAQ;QACtB,EAAE;KACH,CAAA;IAED,8EAA8E;IAC9E,4EAA4E;IAC5E,0DAA0D;IAC1D,MAAM,MAAM,GAAG,GAAS,EAAE;QACxB,KAAK,MAAM,CAAC,IAAI,EAAE,KAAK,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,KAAK,CAAC,EAAE,CAAC;YAClD,MAAM,CAAC,cAAc,CAAC,IAAI,CAAC,OAAO,EAAE,IAAI,EAAE,EAAE,KAAK,EAAE,UAAU,EAAE,IAAI,EAAE,YAAY,EAAE,IAAI,EAAE,CAAC,CAAA;QAC5F,CAAC;IACH,CAAC,CAAA;IAED,MAAM,EAAE,CAAA;IACR,IAAI,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,CAAC,CAAA;IAExB,IAAI,CAAC,4EAA4E,CAAC,CAAA;IAClF,IAAI,EAAE,CAAA;IACN,IAAI,CAAC,qCAAqC,CAAC,CAAA;IAC3C,IAAI,CAAC,wFAAwF,CAAC,CAAA;IAC9F,IAAI,CAAC,sDAAsD,CAAC,CAAA;IAC5D,IAAI,CAAC,kCAAkC,CAAC,CAAA;IACxC,IAAI,CAAC,iFAAiF,CAAC,CAAA;IACvF,IAAI,EAAE,CAAA;IACN,IAAI,CAAC,0CAA0C,CAAC,CAAA;IAEhD,MAAM,IAAI,OAAO,CAAO,CAAC,OAAO,EAAE,EAAE;QAClC,IAAI,CAAC,IAAI,CAAC,MAAM,EAAE,GAAG,EAAE;YACrB,OAAO,EAAE,CAAA;QACX,CAAC,CAAC,CAAA;IACJ,CAAC,CAAC,CAAA;IAEF,gFAAgF;IAChF,4EAA4E;IAC5E,KAAK,CAAC,KAAK,EAAE,CAAA;IAEb,MAAM,GAAG,CAAC,QAAQ,EAAE,CAAA;AACtB,CAAC,CAAA;AAED,MAAM,cAAc,GAAmB,KAAK,EAAE,EAAE,GAAG,EAAE,EAAE,EAAE;IACvD,MAAM,MAAM,GAAG,MAAM,UAAU,CAAC,GAAG,CAAC,CAAA;IAEpC,MAAM,WAAW,CAAC,MAAM,eAAe,CAAC,MAAM,CAAC,CAAC,CAAA;IAEhD,OAAO,CAAC,CAAA;AACV,CAAC,CAAA;AAED,QAAQ,CACN,aAAa,CAAC;IACZ,IAAI,EAAE,SAAS;IACf,KAAK,EAAE,SAAS;IAChB,OAAO,EAAE,4CAA4C;IACrD,KAAK,EAAE,kBAAkB;IACzB,OAAO,EAAE,cAAc;CACxB,CAAC,CACH,CAAA"}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Everything a migration is, in either direction.
|
|
3
|
+
*
|
|
4
|
+
* The generator produces one and the file writer consumes one; the parser turns a
|
|
5
|
+
* file back into one. Having a single type for all three is what makes the round trip
|
|
6
|
+
* something a test can state in one line.
|
|
7
|
+
*/
|
|
8
|
+
export type MigrationContent = {
|
|
9
|
+
readonly up: readonly string[];
|
|
10
|
+
readonly down: readonly string[];
|
|
11
|
+
/** One sentence per `up` statement that changes or destroys stored data. */
|
|
12
|
+
readonly destructive: readonly string[];
|
|
13
|
+
};
|
|
14
|
+
/**
|
|
15
|
+
* Reads a migration file back into the statements it holds.
|
|
16
|
+
*
|
|
17
|
+
* A file with no `-- +migration` marker at all is read as an `up` migration, which is
|
|
18
|
+
* what somebody who dropped a plain `.sql` file into the directory meant. `file` is
|
|
19
|
+
* only ever used to name the file in a complaint.
|
|
20
|
+
*/
|
|
21
|
+
export declare const parseMigration: (text: string, file: string) => MigrationContent;
|
|
22
|
+
/** The text of a migration file, from what the generator produced. */
|
|
23
|
+
export declare const migrationFileText: (name: string, generated: MigrationContent) => string;
|
|
24
|
+
export type MigrationFile = {
|
|
25
|
+
/** `0002_add-sku` — what the runner records as applied, and what sorts. */
|
|
26
|
+
readonly name: string;
|
|
27
|
+
readonly path: string;
|
|
28
|
+
readonly number: number;
|
|
29
|
+
readonly up: readonly string[];
|
|
30
|
+
readonly down: readonly string[];
|
|
31
|
+
readonly destructive: readonly string[];
|
|
32
|
+
};
|
|
33
|
+
/** Every migration in the directory, in the order they must run. */
|
|
34
|
+
export declare const readMigrations: (directory: string) => Promise<readonly MigrationFile[]>;
|
|
35
|
+
/** One past the highest number already there, always four digits. */
|
|
36
|
+
export declare const nextMigrationNumber: (filenames: readonly string[]) => string;
|
|
37
|
+
/**
|
|
38
|
+
* `Add products!` becomes `add-products`, which is how SPEC.md §34 writes one.
|
|
39
|
+
*
|
|
40
|
+
* Empty is a valid answer, and the caller is what refuses it: a name made entirely of
|
|
41
|
+
* punctuation is a mistake worth naming, not one to paper over with a default.
|
|
42
|
+
*/
|
|
43
|
+
export declare const migrationSlug: (name: string) => string;
|
|
44
|
+
export declare const dbGenerate: import("../registry.js").CliCommand;
|
|
45
|
+
export declare const dbMigrate: import("../registry.js").CliCommand;
|
|
46
|
+
export declare const dbRollback: import("../registry.js").CliCommand;
|
|
47
|
+
export declare const dbStatus: import("../registry.js").CliCommand;
|
|
48
|
+
//# sourceMappingURL=db.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"db.d.ts","sourceRoot":"","sources":["../../src/commands/db.ts"],"names":[],"mappings":"AAgCA;;;;;;GAMG;AACH,MAAM,MAAM,gBAAgB,GAAG;IAC7B,QAAQ,CAAC,EAAE,EAAE,SAAS,MAAM,EAAE,CAAA;IAC9B,QAAQ,CAAC,IAAI,EAAE,SAAS,MAAM,EAAE,CAAA;IAChC,4EAA4E;IAC5E,QAAQ,CAAC,WAAW,EAAE,SAAS,MAAM,EAAE,CAAA;CACxC,CAAA;AAyID;;;;;;GAMG;AACH,eAAO,MAAM,cAAc,SAAU,MAAM,QAAQ,MAAM,KAAG,gBA6F3D,CAAA;AAKD,sEAAsE;AACtE,eAAO,MAAM,iBAAiB,SAAU,MAAM,aAAa,gBAAgB,KAAG,MAY9D,CAAA;AAUhB,MAAM,MAAM,aAAa,GAAG;IAC1B,2EAA2E;IAC3E,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;IACrB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;IACrB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAA;IACvB,QAAQ,CAAC,EAAE,EAAE,SAAS,MAAM,EAAE,CAAA;IAC9B,QAAQ,CAAC,IAAI,EAAE,SAAS,MAAM,EAAE,CAAA;IAChC,QAAQ,CAAC,WAAW,EAAE,SAAS,MAAM,EAAE,CAAA;CACxC,CAAA;AAiCD,oEAAoE;AACpE,eAAO,MAAM,cAAc,cAAqB,MAAM,KAAG,OAAO,CAAC,SAAS,aAAa,EAAE,CA6BxF,CAAA;AAeD,qEAAqE;AACrE,eAAO,MAAM,mBAAmB,cAAe,SAAS,MAAM,EAAE,KAAG,MAQlE,CAAA;AAED;;;;;GAKG;AACH,eAAO,MAAM,aAAa,SAAU,MAAM,KAAG,MAIjB,CAAA;AAsK5B,eAAO,MAAM,UAAU,qCA4ErB,CAAA;AA8BF,eAAO,MAAM,SAAS,qCAsDpB,CAAA;AAEF,eAAO,MAAM,UAAU,qCA4CrB,CAAA;AAEF,eAAO,MAAM,QAAQ,qCAwCnB,CAAA"}
|