@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.
Files changed (76) hide show
  1. package/LICENSE +202 -0
  2. package/README.md +272 -0
  3. package/bin.mjs +31 -0
  4. package/dist/args.d.ts +35 -0
  5. package/dist/args.d.ts.map +1 -0
  6. package/dist/args.js +104 -0
  7. package/dist/args.js.map +1 -0
  8. package/dist/bin.d.ts +3 -0
  9. package/dist/bin.d.ts.map +1 -0
  10. package/dist/bin.js +11 -0
  11. package/dist/bin.js.map +1 -0
  12. package/dist/commands/agents.d.ts +50 -0
  13. package/dist/commands/agents.d.ts.map +1 -0
  14. package/dist/commands/agents.js +169 -0
  15. package/dist/commands/agents.js.map +1 -0
  16. package/dist/commands/artifacts.d.ts +5 -0
  17. package/dist/commands/artifacts.d.ts.map +1 -0
  18. package/dist/commands/artifacts.js +126 -0
  19. package/dist/commands/artifacts.js.map +1 -0
  20. package/dist/commands/console.d.ts +25 -0
  21. package/dist/commands/console.d.ts.map +1 -0
  22. package/dist/commands/console.js +99 -0
  23. package/dist/commands/console.js.map +1 -0
  24. package/dist/commands/db.d.ts +48 -0
  25. package/dist/commands/db.d.ts.map +1 -0
  26. package/dist/commands/db.js +585 -0
  27. package/dist/commands/db.js.map +1 -0
  28. package/dist/commands/index.d.ts +21 -0
  29. package/dist/commands/index.d.ts.map +1 -0
  30. package/dist/commands/index.js +21 -0
  31. package/dist/commands/index.js.map +1 -0
  32. package/dist/commands/inspect.d.ts +8 -0
  33. package/dist/commands/inspect.d.ts.map +1 -0
  34. package/dist/commands/inspect.js +280 -0
  35. package/dist/commands/inspect.js.map +1 -0
  36. package/dist/commands/make.d.ts +29 -0
  37. package/dist/commands/make.d.ts.map +1 -0
  38. package/dist/commands/make.js +393 -0
  39. package/dist/commands/make.js.map +1 -0
  40. package/dist/commands/mcp.d.ts +19 -0
  41. package/dist/commands/mcp.d.ts.map +1 -0
  42. package/dist/commands/mcp.js +116 -0
  43. package/dist/commands/mcp.js.map +1 -0
  44. package/dist/commands/new.d.ts +20 -0
  45. package/dist/commands/new.d.ts.map +1 -0
  46. package/dist/commands/new.js +83 -0
  47. package/dist/commands/new.js.map +1 -0
  48. package/dist/commands/run.d.ts +20 -0
  49. package/dist/commands/run.d.ts.map +1 -0
  50. package/dist/commands/run.js +355 -0
  51. package/dist/commands/run.js.map +1 -0
  52. package/dist/commands/watchdog.d.ts +2 -0
  53. package/dist/commands/watchdog.d.ts.map +1 -0
  54. package/dist/commands/watchdog.js +74 -0
  55. package/dist/commands/watchdog.js.map +1 -0
  56. package/dist/config.d.ts +89 -0
  57. package/dist/config.d.ts.map +1 -0
  58. package/dist/config.js +156 -0
  59. package/dist/config.js.map +1 -0
  60. package/dist/index.d.ts +19 -0
  61. package/dist/index.d.ts.map +1 -0
  62. package/dist/index.js +133 -0
  63. package/dist/index.js.map +1 -0
  64. package/dist/output.d.ts +96 -0
  65. package/dist/output.d.ts.map +1 -0
  66. package/dist/output.js +181 -0
  67. package/dist/output.js.map +1 -0
  68. package/dist/project.d.ts +44 -0
  69. package/dist/project.d.ts.map +1 -0
  70. package/dist/project.js +102 -0
  71. package/dist/project.js.map +1 -0
  72. package/dist/registry.d.ts +48 -0
  73. package/dist/registry.d.ts.map +1 -0
  74. package/dist/registry.js +74 -0
  75. package/dist/registry.js.map +1 -0
  76. package/package.json +49 -0
@@ -0,0 +1,393 @@
1
+ /**
2
+ * `make:*` — the six generators of SPEC.md §77.
3
+ *
4
+ * A generated file is the first Assemora code most developers will ever read, so the
5
+ * bar is not "it saves typing": it is that the file compiles, runs, and shows the
6
+ * declaration the way the framework's own examples write it. A generator that emits
7
+ * code the framework rejects is worse than no generator, and
8
+ * `make.test.ts` compiles what these templates produce against the real packages
9
+ * rather than trusting them.
10
+ *
11
+ * The CLI writes source files against APIs it deliberately does not depend on —
12
+ * `@assemora/resources`, `@assemora/pages`, `@assemora/auth` (ADR-0021). That is a
13
+ * template problem rather than an architectural one: nothing here imports those
14
+ * packages, it only writes text that names them.
15
+ */
16
+ import { mkdir, stat, writeFile } from 'node:fs/promises';
17
+ import { dirname, join, relative } from 'node:path';
18
+ import { ConflictError } from '@assemora/core';
19
+ import { bool } from '../args.js';
20
+ import { loadConfig } from '../config.js';
21
+ import { detail, fail, ok, warn } from '../output.js';
22
+ import { defineCommand, register } from '../registry.js';
23
+ /**
24
+ * The words in a name, however it was typed.
25
+ *
26
+ * `BlogPost`, `blog_post`, `blog-post` and `blogPost` are the same three syllables,
27
+ * and a person typing at a terminal should not have to remember which spelling the
28
+ * generator wanted. Everything below is derived from these words, so all four
29
+ * spellings produce a byte-identical file.
30
+ */
31
+ const wordsOf = (name) => name
32
+ // `BlogPost` → `Blog Post`, and `APIKey` → `API Key`: the second rule is what
33
+ // stops an acronym being split into one word per letter.
34
+ .replace(/([a-z0-9])([A-Z])/g, '$1 $2')
35
+ .replace(/([A-Z]+)([A-Z][a-z])/g, '$1 $2')
36
+ .split(/[^a-zA-Z0-9]+/)
37
+ .filter((word) => word !== '')
38
+ .map((word) => word.toLowerCase());
39
+ const capitalised = (word) => `${word.slice(0, 1).toUpperCase()}${word.slice(1)}`;
40
+ /** `blog_post` → `BlogPost`. */
41
+ export const pascalCase = (name) => wordsOf(name).map(capitalised).join('');
42
+ /** `blog_post` → `blogPost`. */
43
+ export const camelCase = (name) => wordsOf(name)
44
+ .map((word, index) => (index === 0 ? word : capitalised(word)))
45
+ .join('');
46
+ /** `BlogPost` → `blog-post`. The form every generated file name takes. */
47
+ export const kebabCase = (name) => wordsOf(name).join('-');
48
+ /** `BlogPost` → `blog_post`. The form every generated table name takes. */
49
+ export const snakeCase = (name) => wordsOf(name).join('_');
50
+ /** `blog_post` → `Blog post`. What a label and a description read like. */
51
+ export const sentenceCase = (name) => capitalised(wordsOf(name).join(' '));
52
+ /**
53
+ * The plural of one lower-case word.
54
+ *
55
+ * Four ordinary English rules, plus one that leaves a word already ending in `s`
56
+ * alone so that `make:policy posts` is not a policy about `postses`. A genuine
57
+ * singular ending in `s` is caught by the first rule, which is why `status` becomes
58
+ * `statuses` rather than staying put.
59
+ *
60
+ * Nothing here knows that a person is people. An irregular plural is one word to fix
61
+ * in the generated file, and a dictionary of English exceptions inside a CLI is a
62
+ * promise it cannot keep — `model('people', …)` is the honest answer.
63
+ */
64
+ export const pluralOf = (word) => {
65
+ if (/(?:ss|us|is)$/.test(word))
66
+ return `${word}es`;
67
+ if (word.endsWith('s'))
68
+ return word;
69
+ if (/(?:x|z|ch|sh)$/.test(word))
70
+ return `${word}es`;
71
+ if (/[^aeiou]y$/.test(word))
72
+ return `${word.slice(0, -1)}ies`;
73
+ return `${word}s`;
74
+ };
75
+ /** The inverse, to the same standard: it decides a file name, never a table name. */
76
+ export const singularOf = (word) => {
77
+ if (word.endsWith('ies'))
78
+ return `${word.slice(0, -3)}y`;
79
+ if (/(?:ss|us|is)$/.test(word))
80
+ return word;
81
+ if (/(?:x|z|ch|sh)es$/.test(word))
82
+ return word.slice(0, -2);
83
+ if (word.endsWith('ses'))
84
+ return word.slice(0, -2);
85
+ if (word.endsWith('s'))
86
+ return word.slice(0, -1);
87
+ return word;
88
+ };
89
+ /**
90
+ * Only the last word is inflected: `blog_post` pluralises to `blog posts`, never to
91
+ * `blogs posts`. The result is handed back space-separated, because every case
92
+ * helper re-reads it as words anyway.
93
+ */
94
+ const inflectLast = (name, inflect) => {
95
+ const words = [...wordsOf(name)];
96
+ const last = words.at(-1);
97
+ if (last !== undefined)
98
+ words[words.length - 1] = inflect(last);
99
+ return words.join(' ');
100
+ };
101
+ const pluralName = (name) => inflectLast(name, pluralOf);
102
+ const singularName = (name) => inflectLast(name, singularOf);
103
+ /**
104
+ * Import lines are assembled rather than written out.
105
+ *
106
+ * `pnpm boundaries` reads source text and cannot tell a template apart from the code
107
+ * around it, so a template spelling out an import of the resource layer reads as this
108
+ * package importing it — the one thing the CLI must never do (ADR-0021). Composing
109
+ * the line keeps that check honest without changing a character of what reaches disk.
110
+ */
111
+ const importFrom = (names, from) => `import { ${names.join(', ')} } from '${from}'`;
112
+ /**
113
+ * A relative import carries the real `.ts` extension.
114
+ *
115
+ * Node 24 runs the TypeScript file directly and does not rewrite specifiers, so
116
+ * `../models/post.js` would name a file that never exists. `apps/playground` is
117
+ * written this way for the same reason; a project's tsconfig needs
118
+ * `allowImportingTsExtensions`.
119
+ */
120
+ const fromSource = (directory, file) => `../${directory}/${file}.ts`;
121
+ const isAlreadyExists = (error) => typeof error === 'object' && error !== null && error.code === 'EEXIST';
122
+ const exists = async (path) => {
123
+ try {
124
+ await stat(path);
125
+ return true;
126
+ }
127
+ catch {
128
+ return false;
129
+ }
130
+ };
131
+ const write = async (file, shown, contents, force) => {
132
+ await mkdir(dirname(file), { recursive: true });
133
+ try {
134
+ // `wx` is what actually refuses, rather than a check followed by a write: two
135
+ // generators racing for the same path cannot then both believe they won.
136
+ await writeFile(file, contents, { encoding: 'utf8', flag: force ? 'w' : 'wx' });
137
+ }
138
+ catch (error) {
139
+ if (!isAlreadyExists(error))
140
+ throw error;
141
+ throw new ConflictError(`${shown} already exists. Pass --force to overwrite it.`);
142
+ }
143
+ };
144
+ const generate = async (spec, args, cwd) => {
145
+ const name = args.positionals[0];
146
+ // Checked before the config is loaded: a forgotten argument is a mistake in what
147
+ // was typed, and answering it with "no assemora.config.ts here" helps nobody.
148
+ if (name === undefined || wordsOf(name).length === 0) {
149
+ fail(`${spec.name} needs a name, for example: ${spec.example}`);
150
+ return 2;
151
+ }
152
+ const loaded = await loadConfig(cwd);
153
+ const generated = spec.plan(name);
154
+ const file = join(loaded.paths.source, generated.file);
155
+ const shown = relative(cwd, file) || file;
156
+ const force = bool(args, 'force');
157
+ // Asked before the write and reported after it, so that a write which failed does
158
+ // not leave behind a warning about work nobody did.
159
+ const replacing = force && (await exists(file));
160
+ await write(file, shown, generated.contents, force);
161
+ if (replacing)
162
+ warn(`${shown} existed already and was overwritten`);
163
+ // The path is the answer and goes to stdout, so `assemora make:model Post` can be
164
+ // piped into an editor; the next step is commentary and goes to stderr.
165
+ ok(shown);
166
+ detail(generated.nextStep);
167
+ return 0;
168
+ };
169
+ const generator = (spec) => defineCommand({
170
+ name: spec.name,
171
+ group: 'make',
172
+ summary: spec.summary,
173
+ usage: spec.usage,
174
+ handler: ({ args, cwd }) => generate(spec, args, cwd),
175
+ });
176
+ const modelFile = (name) => {
177
+ const model = pascalCase(singularName(name));
178
+ const table = snakeCase(pluralName(name));
179
+ return {
180
+ file: join('models', `${kebabCase(singularName(name))}.ts`),
181
+ nextStep: `Register it on a module with .models(${model}), then run \`assemora db:generate\`.`,
182
+ contents: `/**
183
+ * The ${model} model.
184
+ *
185
+ * One declaration is the record type, the database column, the runtime validation
186
+ * and every schema built from them — Studio's form, OpenAPI, the SDK and the MCP
187
+ * tool (SPEC.md §9, §17).
188
+ */
189
+ ${importFrom(['model', 'string', 'timestamp', 'uuid'], '@assemora/data')}
190
+
191
+ export const ${model} = model('${table}', {
192
+ id: uuid().primary().defaultRandom(),
193
+ title: string(),
194
+ createdAt: timestamp().created(),
195
+ updatedAt: timestamp().updated(),
196
+ })
197
+ `,
198
+ };
199
+ };
200
+ const resourceFile = (name) => {
201
+ const model = pascalCase(singularName(name));
202
+ const collection = pascalCase(pluralName(name));
203
+ const modelFileName = kebabCase(singularName(name));
204
+ return {
205
+ file: join('resources', `${kebabCase(pluralName(name))}.ts`),
206
+ nextStep: `Register it on a module with .resources(${collection}).`,
207
+ contents: `/**
208
+ * The ${collection} resource.
209
+ *
210
+ * A resource presents a model rather than inventing data, so every field here names
211
+ * a column of ${model}, and only what is declared is ever returned — a column the
212
+ * resource never mentioned cannot leak into an API response (SPEC.md §35).
213
+ */
214
+ ${importFrom(['resource', 'text'], '@assemora/resources')}
215
+
216
+ ${importFrom([model], fromSource('models', modelFileName))}
217
+
218
+ export const ${collection} = resource(
219
+ ${model},
220
+ {
221
+ title: text().required().searchable().sortable().label('Title'),
222
+ },
223
+ { label: '${sentenceCase(pluralName(name))}' },
224
+ )
225
+ `,
226
+ };
227
+ };
228
+ const blockFile = (name) => {
229
+ const block = pascalCase(name);
230
+ const type = kebabCase(name);
231
+ return {
232
+ file: join('blocks', `${type}.ts`),
233
+ nextStep: `Register it with pages({ blocks: [${block}] }).`,
234
+ contents: `/**
235
+ * The ${block} block.
236
+ *
237
+ * These fields are the Studio form, the runtime validation, the JSON Schema and what
238
+ * an agent is allowed to set — one declaration, never four (SPEC.md §55, §56).
239
+ */
240
+ ${importFrom(['block'], '@assemora/pages')}
241
+ ${importFrom(['text'], '@assemora/resources')}
242
+
243
+ export const ${block} = block(
244
+ '${type}',
245
+ {
246
+ title: text().required().label('Title'),
247
+ },
248
+ { label: '${sentenceCase(name)}' },
249
+ )
250
+ `,
251
+ };
252
+ };
253
+ const moduleFile = (name) => {
254
+ const factory = camelCase(name);
255
+ const moduleName = kebabCase(name);
256
+ return {
257
+ file: join('modules', `${moduleName}.ts`),
258
+ // `assemora()`, not `createApplication()`: the project this same CLI scaffolds
259
+ // calls the umbrella in `src/app.ts` and contains the word `createApplication`
260
+ // nowhere at all, so the other name sends a reader looking for something that is
261
+ // not there (ADR-0022).
262
+ nextStep: `Register it with assemora({ modules: [${factory}()] }) in src/app.ts.`,
263
+ contents: `/**
264
+ * The ${moduleName} module.
265
+ *
266
+ * A module is the registration unit: models, resources, blocks, routes, commands and
267
+ * queries (SPEC.md §13). \`.models()\` and \`.resources()\` appear on the builder as
268
+ * soon as the packages that own them are imported — each contributes its own method
269
+ * rather than core learning what a model is (ADR-0009).
270
+ */
271
+ ${importFrom(['module'], '@assemora/core')}
272
+
273
+ export const ${factory} = () => module('${moduleName}')
274
+ `,
275
+ };
276
+ };
277
+ /**
278
+ * `posts.publish` names a command; the file is named after it.
279
+ *
280
+ * The last segment is the verb and the one before it is what the verb acts on, so
281
+ * `posts.publish` becomes `publish-post.ts` holding `PublishPost` — the file sorts
282
+ * beside the other things done to a post, and the export reads as the act. A name
283
+ * with no dot is taken as the verb alone.
284
+ */
285
+ const commandFile = (name) => {
286
+ const segments = name
287
+ .split('.')
288
+ .map(camelCase)
289
+ .filter((segment) => segment !== '');
290
+ const verb = segments.at(-1) ?? camelCase(name);
291
+ const subject = segments.length > 1 ? (segments.at(-2) ?? '') : '';
292
+ const subjectWords = subject === '' ? '' : wordsOf(singularName(subject)).join(' ');
293
+ const definition = `${pascalCase(verb)}${subject === '' ? '' : pascalCase(singularName(subject))}`;
294
+ const fileName = subject === '' ? kebabCase(verb) : `${kebabCase(verb)}-${kebabCase(singularName(subject))}`;
295
+ const commandName = segments.join('.');
296
+ return {
297
+ file: join('commands', `${fileName}.ts`),
298
+ nextStep: `Register it on a module with .commands(${definition}).`,
299
+ contents: `/**
300
+ * The ${commandName} command.
301
+ *
302
+ * Every state change takes one path — validation, authorization, transaction,
303
+ * handler, revision, events, audit — and Studio, REST, the SDK, this CLI and an
304
+ * agent all arrive through it. The name is also the permission it requires
305
+ * (SPEC.md §14, ADR-0015).
306
+ */
307
+ ${importFrom(['command'], '@assemora/core')}
308
+ ${importFrom(['uuid'], '@assemora/schema')}
309
+
310
+ export const ${definition} = command('${commandName}', {
311
+ description: '${sentenceCase(verb)}${subjectWords === '' ? '' : ` ${subjectWords}`}',
312
+ input: { id: uuid() },
313
+ output: { id: uuid() },
314
+ handle: async ({ id }, context) => {
315
+ context.logger.info('${commandName}', { id })
316
+
317
+ return { id }
318
+ },
319
+ })
320
+ `,
321
+ };
322
+ };
323
+ const policyFile = (name) => {
324
+ const subject = snakeCase(pluralName(name));
325
+ const definition = `${pascalCase(singularName(name))}Policy`;
326
+ return {
327
+ file: join('policies', `${kebabCase(pluralName(name))}.ts`),
328
+ nextStep: `Register it with auth({ policies: [${definition}] }).`,
329
+ contents: `/**
330
+ * Who may do what to ${wordsOf(subject).join(' ')}.
331
+ *
332
+ * The same answer is given to Studio, REST, the SDK, this CLI and an agent, because
333
+ * all of them arrive through the same buses: there is no trusted caller. A rule
334
+ * about a record is asked once the record has been read (SPEC.md §51, ADR-0015).
335
+ */
336
+ ${importFrom(['policy'], '@assemora/auth')}
337
+
338
+ export const ${definition} = policy('${subject}', {
339
+ read: ({ can }) => can('${subject}.read'),
340
+ create: ({ can }) => can('${subject}.create'),
341
+ update: ({ can }) => can('${subject}.update'),
342
+ delete: ({ can }) => can('${subject}.delete'),
343
+ })
344
+ `,
345
+ };
346
+ };
347
+ /** Registered in the order SPEC.md §77 lists them, which is the order they print in. */
348
+ export const makeCommands = [
349
+ generator({
350
+ name: 'make:model',
351
+ summary: 'a model and the table it maps to',
352
+ usage: 'assemora make:model <name> [--force]',
353
+ example: 'assemora make:model Post',
354
+ plan: modelFile,
355
+ }),
356
+ generator({
357
+ name: 'make:resource',
358
+ summary: 'a resource: how a model appears as content',
359
+ usage: 'assemora make:resource <name> [--force]',
360
+ example: 'assemora make:resource Post',
361
+ plan: resourceFile,
362
+ }),
363
+ generator({
364
+ name: 'make:block',
365
+ summary: 'a block for the page builder',
366
+ usage: 'assemora make:block <name> [--force]',
367
+ example: 'assemora make:block hero',
368
+ plan: blockFile,
369
+ }),
370
+ generator({
371
+ name: 'make:module',
372
+ summary: 'a module to register declarations in',
373
+ usage: 'assemora make:module <name> [--force]',
374
+ example: 'assemora make:module blog',
375
+ plan: moduleFile,
376
+ }),
377
+ generator({
378
+ name: 'make:command',
379
+ summary: 'a command for the Command Bus',
380
+ usage: 'assemora make:command <group.action> [--force]',
381
+ example: 'assemora make:command posts.publish',
382
+ plan: commandFile,
383
+ }),
384
+ generator({
385
+ name: 'make:policy',
386
+ summary: 'a policy for one subject',
387
+ usage: 'assemora make:policy <subject> [--force]',
388
+ example: 'assemora make:policy posts',
389
+ plan: policyFile,
390
+ }),
391
+ ];
392
+ register(...makeCommands);
393
+ //# sourceMappingURL=make.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"make.js","sourceRoot":"","sources":["../../src/commands/make.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,19 @@
1
+ import { type McpEndpoint } from '../project.js';
2
+ /** The environment variable the token arrives in, because a pipe carries no headers. */
3
+ export declare const TOKEN_VARIABLE = "ASSEMORA_AGENT_TOKEN";
4
+ export type McpSession = {
5
+ /** Defaults to `process.stdin`. */
6
+ readonly input?: NodeJS.ReadableStream;
7
+ /** Defaults to `process.stdout`. */
8
+ readonly output?: NodeJS.WritableStream;
9
+ };
10
+ /**
11
+ * Serves the endpoint until the input closes, and resolves when it has.
12
+ *
13
+ * Sequential on purpose. The protocol permits a client to have several requests in
14
+ * flight, and answering them in order costs a stdio client nothing — it is one process
15
+ * talking to one process — while concurrency here would put the ordering of two writes
16
+ * to one pipe in the hands of whichever handler finished first.
17
+ */
18
+ export declare const serveMcp: (endpoint: McpEndpoint, token: string, session?: McpSession) => Promise<void>;
19
+ //# sourceMappingURL=mcp.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"mcp.d.ts","sourceRoot":"","sources":["../../src/commands/mcp.ts"],"names":[],"mappings":"AAuBA,OAAO,EAAe,KAAK,WAAW,EAAE,MAAM,eAAe,CAAA;AAG7D,wFAAwF;AACxF,eAAO,MAAM,cAAc,yBAAyB,CAAA;AAEpD,MAAM,MAAM,UAAU,GAAG;IACvB,mCAAmC;IACnC,QAAQ,CAAC,KAAK,CAAC,EAAE,MAAM,CAAC,cAAc,CAAA;IACtC,oCAAoC;IACpC,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC,cAAc,CAAA;CACxC,CAAA;AAqBD;;;;;;;GAOG;AACH,eAAO,MAAM,QAAQ,aACT,WAAW,SACd,MAAM,YACJ,UAAU,KAClB,OAAO,CAAC,IAAI,CAsCd,CAAA"}
@@ -0,0 +1,116 @@
1
+ /**
2
+ * `assemora mcp` — the project's MCP endpoint, over a pipe (SPEC.md §68, §76).
3
+ *
4
+ * The application already speaks the protocol; what it lacked was the transport every
5
+ * client actually uses. Claude Code, Claude Desktop and Cursor start a process and talk
6
+ * to it over stdin and stdout, and until this existed the only way in was a bespoke
7
+ * `POST /api/mcp` that no such client knows how to reach.
8
+ *
9
+ * It reimplements nothing. The endpoint is the one `assemora()` built — the same
10
+ * generated tools, the same buses, the same seven checks of SPEC.md §76 — handed over by
11
+ * the project's own config, which is how this package drives a feature it may not import
12
+ * (ADR-0021).
13
+ *
14
+ * The framing is newline-delimited JSON, which is what the MCP stdio transport specifies:
15
+ * one message per line, and no message may contain a newline of its own. `JSON.stringify`
16
+ * escapes them, so that holds by construction.
17
+ */
18
+ import { createInterface } from 'node:readline';
19
+ import { ConfigurationError } from '@assemora/core';
20
+ import { loadConfig } from '../config.js';
21
+ import { detail } from '../output.js';
22
+ import { loadProject } from '../project.js';
23
+ import { defineCommand, register } from '../registry.js';
24
+ /** The environment variable the token arrives in, because a pipe carries no headers. */
25
+ export const TOKEN_VARIABLE = 'ASSEMORA_AGENT_TOKEN';
26
+ /**
27
+ * A JSON-RPC error, as a message rather than as a thrown thing.
28
+ *
29
+ * A transport that dies on a malformed line takes the session with it, and a client
30
+ * that sent one bad message is owed an answer rather than a closed pipe. `-32700` is
31
+ * parse error and `-32603` is internal error, both from the JSON-RPC 2.0 specification.
32
+ */
33
+ const failure = (id, code, message) => JSON.stringify({ jsonrpc: '2.0', id: id ?? null, error: { code, message } });
34
+ /** The id of a message that may not have parsed, for the error to be addressed to. */
35
+ const idOf = (line) => {
36
+ try {
37
+ return JSON.parse(line).id ?? null;
38
+ }
39
+ catch {
40
+ return null;
41
+ }
42
+ };
43
+ /**
44
+ * Serves the endpoint until the input closes, and resolves when it has.
45
+ *
46
+ * Sequential on purpose. The protocol permits a client to have several requests in
47
+ * flight, and answering them in order costs a stdio client nothing — it is one process
48
+ * talking to one process — while concurrency here would put the ordering of two writes
49
+ * to one pipe in the hands of whichever handler finished first.
50
+ */
51
+ export const serveMcp = async (endpoint, token, session = {}) => {
52
+ const input = session.input ?? process.stdin;
53
+ const output = session.output ?? process.stdout;
54
+ const lines = createInterface({ input, crlfDelay: Number.POSITIVE_INFINITY });
55
+ for await (const line of lines) {
56
+ const trimmed = line.trim();
57
+ if (trimmed === '')
58
+ continue;
59
+ let message;
60
+ try {
61
+ message = JSON.parse(trimmed);
62
+ }
63
+ catch {
64
+ output.write(`${failure(null, -32_700, 'That line is not JSON')}\n`);
65
+ continue;
66
+ }
67
+ try {
68
+ const answered = await endpoint.handle(message, { token });
69
+ // A notification is answered by not answering, and writing an empty line for one
70
+ // is how a client comes to read a reply it never asked for.
71
+ if (answered !== undefined)
72
+ output.write(`${JSON.stringify(answered)}\n`);
73
+ }
74
+ catch (error) {
75
+ // Reported to the client rather than thrown: a refused message is one message,
76
+ // and the session is still good. What went wrong goes to stderr, which is not
77
+ // the protocol's channel and is where a client shows a server's own output.
78
+ const reason = error instanceof Error ? error.message : String(error);
79
+ detail(reason);
80
+ output.write(`${failure(idOf(trimmed), -32_603, reason)}\n`);
81
+ }
82
+ }
83
+ input.pause();
84
+ };
85
+ const mcpCommand = async ({ cwd }) => {
86
+ // Loaded before the token is looked for, and the order is the whole point: a project
87
+ // reads its own `.env` as it is imported — that is what makes one file serve `dev`,
88
+ // `db:migrate` and everything else — so a token kept there is not in the environment
89
+ // until this line has run. Asked for first, this command told a developer that
90
+ // `ASSEMORA_AGENT_TOKEN` was unset while they were looking at it in `.env`.
91
+ const loaded = await loadConfig(cwd);
92
+ const project = await loadProject(loaded);
93
+ const token = process.env[TOKEN_VARIABLE];
94
+ if (token === undefined || token.trim() === '') {
95
+ throw new ConfigurationError(`${TOKEN_VARIABLE} is not set, and an MCP session is somebody. Put it in .env, or ` +
96
+ 'run `assemora agents:create <name> --permissions … --write-mcp-json`, which ' +
97
+ 'creates the identity and writes both — an anonymous caller reaches every tool ' +
98
+ 'with no permissions at all, which is a confusing way to be refused.');
99
+ }
100
+ if (project.mcp === undefined) {
101
+ throw new ConfigurationError(`${loaded.file}: this project has no MCP endpoint. Assemble the application with ` +
102
+ 'assemora({ mcp: true }) and have the config return it whole — `app: () => ' +
103
+ 'createApp()` rather than `createApp().app`, which drops the half that speaks ' +
104
+ 'the protocol.');
105
+ }
106
+ await serveMcp(project.mcp, token.trim());
107
+ return 0;
108
+ };
109
+ register(defineCommand({
110
+ name: 'mcp',
111
+ group: 'run',
112
+ summary: 'serve this project to an agent over stdin and stdout',
113
+ usage: 'assemora mcp',
114
+ handler: mcpCommand,
115
+ }));
116
+ //# sourceMappingURL=mcp.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"mcp.js","sourceRoot":"","sources":["../../src/commands/mcp.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,20 @@
1
+ import { type ParsedArgs } from '../args.js';
2
+ /** Exactly what `scaffold()` in `create-assemora` accepts. */
3
+ export type ScaffoldOptions = {
4
+ readonly name: string;
5
+ readonly directory: string;
6
+ readonly database?: string;
7
+ readonly studio?: boolean;
8
+ readonly pages?: boolean;
9
+ readonly mcp?: boolean;
10
+ readonly template?: string;
11
+ };
12
+ /**
13
+ * argv, as the scaffolder's options.
14
+ *
15
+ * The name is not validated beyond being present. What makes a legal project name is
16
+ * `create-assemora`'s question — it is the thing that writes the directory and the
17
+ * `package.json` — and answering it twice is how the two would come to disagree.
18
+ */
19
+ export declare const scaffoldOptions: (name: string, args: ParsedArgs, cwd: string) => ScaffoldOptions;
20
+ //# sourceMappingURL=new.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"new.d.ts","sourceRoot":"","sources":["../../src/commands/new.ts"],"names":[],"mappings":"AAiBA,OAAO,EAAc,KAAK,UAAU,EAAE,MAAM,YAAY,CAAA;AAIxD,8DAA8D;AAC9D,MAAM,MAAM,eAAe,GAAG;IAC5B,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;IACrB,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAA;IAC1B,QAAQ,CAAC,QAAQ,CAAC,EAAE,MAAM,CAAA;IAC1B,QAAQ,CAAC,MAAM,CAAC,EAAE,OAAO,CAAA;IACzB,QAAQ,CAAC,KAAK,CAAC,EAAE,OAAO,CAAA;IACxB,QAAQ,CAAC,GAAG,CAAC,EAAE,OAAO,CAAA;IACtB,QAAQ,CAAC,QAAQ,CAAC,EAAE,MAAM,CAAA;CAC3B,CAAA;AAqBD;;;;;;GAMG;AACH,eAAO,MAAM,eAAe,SAAU,MAAM,QAAQ,UAAU,OAAO,MAAM,KAAG,eAc7E,CAAA"}
@@ -0,0 +1,83 @@
1
+ /**
2
+ * `assemora new <name>` — the convenience, not a second scaffolder (SPEC.md §78).
3
+ *
4
+ * `pnpm create assemora my-project` is the primary way a project starts, and this
5
+ * command runs the same `scaffold()` it does. Nothing about the layout of a
6
+ * generated project is decided here: a second implementation would drift from the
7
+ * starter the moment either changed, and the starter is the one CI typechecks
8
+ * (ADR-0021).
9
+ *
10
+ * What this file owns is the translation from argv to that function's options, and
11
+ * the defaults SPEC.md §78 fixes — Studio, Pages and MCP are all in unless the
12
+ * invocation says otherwise.
13
+ */
14
+ import { relative, resolve } from 'node:path';
15
+ import { scaffold } from 'create-assemora';
16
+ import { bool, flag } from '../args.js';
17
+ import { fail, line, ok } from '../output.js';
18
+ import { defineCommand, register } from '../registry.js';
19
+ /**
20
+ * Whether an optional part of the starter is included.
21
+ *
22
+ * SPEC.md §78 makes all three default to yes, so the question here is only whether
23
+ * the invocation turned one off. Both spellings are accepted because both get typed:
24
+ * `--no-studio` is what a person writes, and `--studio=false` is what a script does.
25
+ */
26
+ const included = (args, name) => {
27
+ if (args.flags[`no-${name}`] !== undefined)
28
+ return !bool(args, `no-${name}`);
29
+ if (args.flags[name] !== undefined)
30
+ return bool(args, name);
31
+ return true;
32
+ };
33
+ /**
34
+ * argv, as the scaffolder's options.
35
+ *
36
+ * The name is not validated beyond being present. What makes a legal project name is
37
+ * `create-assemora`'s question — it is the thing that writes the directory and the
38
+ * `package.json` — and answering it twice is how the two would come to disagree.
39
+ */
40
+ export const scaffoldOptions = (name, args, cwd) => {
41
+ const directory = flag(args, 'directory');
42
+ const database = flag(args, 'database');
43
+ const template = flag(args, 'template');
44
+ return {
45
+ name,
46
+ directory: resolve(cwd, directory ?? name),
47
+ ...(database === undefined ? {} : { database }),
48
+ ...(template === undefined ? {} : { template }),
49
+ studio: included(args, 'studio'),
50
+ pages: included(args, 'pages'),
51
+ mcp: included(args, 'mcp'),
52
+ };
53
+ };
54
+ const runScaffold = async (options) => scaffold(options);
55
+ /** An absolute path is a poor thing to type; a relative one is what `cd` wants. */
56
+ const shortest = (from, directory) => {
57
+ const step = relative(from, directory);
58
+ return step === '' || step.startsWith('..') ? directory : step;
59
+ };
60
+ const create = async ({ args, cwd }) => {
61
+ const name = args.positionals[0];
62
+ if (name === undefined || name.trim() === '') {
63
+ fail('`assemora new` needs a name for the project: `assemora new my-project`.');
64
+ return 2;
65
+ }
66
+ const options = scaffoldOptions(name, args, cwd);
67
+ const created = await runScaffold(options);
68
+ ok(`Created ${options.name}: ${created.files.length} files in ${created.directory}`);
69
+ line();
70
+ line('Next');
71
+ line(` cd ${shortest(cwd, created.directory)}`);
72
+ line(' pnpm install');
73
+ line(' assemora dev');
74
+ return 0;
75
+ };
76
+ register(defineCommand({
77
+ name: 'new',
78
+ group: 'project',
79
+ summary: 'scaffold a new project',
80
+ usage: 'assemora new <name> [--directory <path>] [--database <url>] [--template <name>] [--no-studio] [--no-pages] [--no-mcp]',
81
+ handler: create,
82
+ }));
83
+ //# sourceMappingURL=new.js.map