create-stitchkit 0.6.5 → 0.6.7

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/CHANGELOG.md CHANGED
@@ -12,6 +12,47 @@ step is overwritten by the next release.
12
12
 
13
13
  ## [Unreleased]
14
14
 
15
+ ## [0.6.7] — 2026-09-24
16
+
17
+ ### Fixed
18
+
19
+ - **A bot from `--template telegram-bot` starts with an operators' chat.** With
20
+ `OPERATOR_CHAT_ID` set, the optional `operator-channel` was one of the bot's
21
+ dependencies, and the kernel refuses a required resource that depends on an
22
+ optional one: the bot started without the chat and not with it. The channel
23
+ now stands beside the bot and still drains on the way down; the template's
24
+ test starts the graph with the chat and with local Bot API files. A bot
25
+ generated from 0.6.5 or 0.6.6 moves the `operator-channel` resource out of the
26
+ `dependsOn` it passes to `grammyBotResources` and into the application's
27
+ `resources`.
28
+
29
+ ### Changed
30
+
31
+ - Generated projects track Stitchkit `^0.96.1`, whose operator channel masks
32
+ a bot token before a message leaves.
33
+
34
+ ## [0.6.6] — 2026-09-24
35
+
36
+ ### Fixed
37
+
38
+ - **A project from `--template agent` or `--template telegram-bot` installs.**
39
+ The scaffolder rewrote their Stitchkit dependency to `catalog:` and wrote a
40
+ root `catalog`, which Bun reads only in a workspace root, so `bun install`
41
+ refused the generated project ("stitchkit@catalog: is not in the catalog").
42
+ A project without `workspaces` now gets the range itself —
43
+ `"stitchkit": "^0.96.0"` — and no catalog; the application template, a
44
+ workspace, is unchanged. A project already generated from either template
45
+ replaces each `"catalog:"` in its dependencies with the range from its
46
+ `catalog` field and deletes that field.
47
+ - **A project from `--template agent` passes its own tests.** It mounts its
48
+ agent with `stitchkit/tools`, whose optional peer
49
+ `@modelcontextprotocol/server` the template did not declare; inside this
50
+ repository the workspace supplied it. The template now depends on
51
+ `@modelcontextprotocol/server` `^2.0.0`.
52
+ - A new lane generates both single-package templates the way a user does,
53
+ installs them from the registry, and runs their check and tests — neither
54
+ defect above could be seen by a lane that installs the template directory.
55
+
15
56
  ## [0.6.5] — 2026-09-24
16
57
 
17
58
  ### Added
package/dist/cli.js CHANGED
@@ -299,6 +299,7 @@ var TEMPLATE_RENAMES = new Map([
299
299
  var RootManifestSchema = z2.looseObject({
300
300
  name: z2.string().min(1),
301
301
  catalog: z2.record(z2.string(), z2.string()).optional(),
302
+ workspaces: z2.unknown().optional(),
302
303
  dependencies: z2.record(z2.string(), z2.string()).optional(),
303
304
  devDependencies: z2.record(z2.string(), z2.string()).optional()
304
305
  });
@@ -435,21 +436,33 @@ async function scaffoldProject(templateDirectory, destination, options = {}) {
435
436
  const manifestPath = join(resolvedDestination, "package.json");
436
437
  const manifest = RootManifestSchema.parse(JSON.parse(await readFile(manifestPath, "utf8")));
437
438
  const catalog = options.stitchkitCatalogTarget ? { ...manifest.catalog ?? {}, stitchkit: options.stitchkitCatalogTarget } : manifest.catalog;
439
+ const isWorkspace = manifest.workspaces !== undefined;
440
+ const localReference = (name) => {
441
+ if (isWorkspace)
442
+ return "catalog:";
443
+ const range = catalog?.[name];
444
+ if (!range)
445
+ throw new Error(`Template catalog has no range for ${name}`);
446
+ return range;
447
+ };
438
448
  const replaceLocalPackages = (dependencies) => {
439
449
  if (!dependencies)
440
450
  return dependencies;
441
451
  return {
442
452
  ...dependencies,
443
- ...dependencies.stitchkit?.startsWith("file:") && { stitchkit: "catalog:" },
453
+ ...dependencies.stitchkit?.startsWith("file:") && {
454
+ stitchkit: localReference("stitchkit")
455
+ },
444
456
  ...dependencies["stitchkit-tui"]?.startsWith("file:") && {
445
- "stitchkit-tui": "catalog:"
457
+ "stitchkit-tui": localReference("stitchkit-tui")
446
458
  }
447
459
  };
448
460
  };
461
+ const { catalog: _templateCatalog, ...withoutCatalog } = manifest;
449
462
  await writeFile(manifestPath, `${JSON.stringify({
450
- ...manifest,
463
+ ...isWorkspace ? manifest : withoutCatalog,
451
464
  name: identity.slug,
452
- ...catalog && { catalog },
465
+ ...isWorkspace && catalog && { catalog },
453
466
  ...manifest.dependencies && {
454
467
  dependencies: replaceLocalPackages(manifest.dependencies)
455
468
  },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-stitchkit",
3
- "version": "0.6.5",
3
+ "version": "0.6.7",
4
4
  "description": "Create a production-shaped Stitchkit application",
5
5
  "license": "MIT",
6
6
  "author": "Max Listov <maxlistov@gmail.com>",
@@ -84,7 +84,7 @@
84
84
  "@types/react": "^19.3.0",
85
85
  "ai": "^7.0.111",
86
86
  "react": "^19.3.0",
87
- "stitchkit": "0.96.0",
87
+ "stitchkit": "0.96.1",
88
88
  "typescript": "^7.0.2"
89
89
  },
90
90
  "engines": {
package/template/bun.lock CHANGED
@@ -145,7 +145,7 @@
145
145
  "zod": "4.6.5",
146
146
  },
147
147
  "catalog": {
148
- "stitchkit": "^0.96.0",
148
+ "stitchkit": "^0.96.1",
149
149
  },
150
150
  "packages": {
151
151
  "@ai-sdk/gateway": ["@ai-sdk/gateway@4.0.89", "", { "dependencies": { "@ai-sdk/provider": "4.0.17", "@ai-sdk/provider-utils": "5.0.45", "@vercel/oidc": "3.2.0" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-n0Q88UUASYQBGK3Ogs9pCe8h3ZXeTHFSuGOLBJAyH73MVGNKUv9w8EJX2f340WUF3eDWWOV305gO7YBpSCPblA=="],
@@ -1128,7 +1128,7 @@
1128
1128
 
1129
1129
  "std-env": ["std-env@3.10.0", "", {}, "sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg=="],
1130
1130
 
1131
- "stitchkit": ["stitchkit@0.96.0", "", { "dependencies": { "ky": "^2.1.0" }, "peerDependencies": { "@modelcontextprotocol/ext-apps": "^1.7.2 || ^2.0.0", "@modelcontextprotocol/server": "^2.0.0", "@openrouter/ai-sdk-provider": "^3.1.0", "@opentelemetry/api": "^1.9.0", "@socket.io/bun-engine": "^0.1.2", "@socket.io/component-emitter": "^3.1.2", "@tanstack/react-query": ">=5", "@types/bun": "^1.4.2", "ai": "^7.0.107", "google-auth-library": "^11.1.0", "grammy": "^1.46.0", "maxmind": "^5.0.7", "react": ">=18", "react-query-kit": "^3.3.3", "socket.io": "^4.8.3", "socket.io-client": "^4.8.3", "srvx": "^0.12.5 || ^1.0.5", "zod": "^4.6.5" }, "optionalPeers": ["@modelcontextprotocol/ext-apps", "@modelcontextprotocol/server", "@openrouter/ai-sdk-provider", "@opentelemetry/api", "@socket.io/bun-engine", "@socket.io/component-emitter", "@tanstack/react-query", "@types/bun", "ai", "google-auth-library", "grammy", "maxmind", "react", "react-query-kit", "socket.io", "socket.io-client", "srvx"], "bin": { "stitchkit": "dist/entrypoints/bin/upgrade-cli.js" } }, "sha512-5cgzNatZtqE0HCXozqGXdcAeudG+QLxKXKZSHFasYu7O6JvU0rViDVO76hWFSwOiczaSoqsqwk/GM01ArMMUmw=="],
1131
+ "stitchkit": ["stitchkit@0.96.1", "", { "dependencies": { "ky": "^2.1.0" }, "peerDependencies": { "@modelcontextprotocol/ext-apps": "^1.7.2 || ^2.0.0", "@modelcontextprotocol/server": "^2.0.0", "@openrouter/ai-sdk-provider": "^3.1.0", "@opentelemetry/api": "^1.9.0", "@socket.io/bun-engine": "^0.1.2", "@socket.io/component-emitter": "^3.1.2", "@tanstack/react-query": ">=5", "@types/bun": "^1.4.2", "ai": "^7.0.107", "google-auth-library": "^11.1.0", "grammy": "^1.46.0", "maxmind": "^5.0.7", "react": ">=18", "react-query-kit": "^3.3.3", "socket.io": "^4.8.3", "socket.io-client": "^4.8.3", "srvx": "^0.12.5 || ^1.0.5", "zod": "^4.6.5" }, "optionalPeers": ["@modelcontextprotocol/ext-apps", "@modelcontextprotocol/server", "@openrouter/ai-sdk-provider", "@opentelemetry/api", "@socket.io/bun-engine", "@socket.io/component-emitter", "@tanstack/react-query", "@types/bun", "ai", "google-auth-library", "grammy", "maxmind", "react", "react-query-kit", "socket.io", "socket.io-client", "srvx"], "bin": { "stitchkit": "dist/entrypoints/bin/upgrade-cli.js" } }, "sha512-Jdd3OAXBVvfNR0v7Hgv46THObNQMl+4a3LaIdr4127eHSjZiW2n8Y/ujmFlhD1V57TFiDv4FkUT9FrTdjkqszQ=="],
1132
1132
 
1133
1133
  "stringify-entities": ["stringify-entities@4.0.4", "", { "dependencies": { "character-entities-html4": "^2.0.0", "character-entities-legacy": "^3.0.0" } }, "sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg=="],
1134
1134
 
@@ -7,7 +7,7 @@
7
7
  "packages/*"
8
8
  ],
9
9
  "catalog": {
10
- "stitchkit": "^0.96.0"
10
+ "stitchkit": "^0.96.1"
11
11
  },
12
12
  "overrides": {
13
13
  "@playwright/test": "1.63.0",
@@ -16,6 +16,7 @@
16
16
  "build": "bun build src/index.ts --outdir dist --target bun --packages external"
17
17
  },
18
18
  "dependencies": {
19
+ "@modelcontextprotocol/server": "^2.0.0",
19
20
  "@openrouter/ai-sdk-provider": "^3.1.0",
20
21
  "ai": "^7.0.111",
21
22
  "stitchkit": "file:../../../core",
@@ -16,9 +16,10 @@ bun run dev
16
16
 
17
17
  - `src/handlers.ts` — the product: commands, messages, what goes to the operators' chat.
18
18
  - `src/application.ts` — the resource graph: `database` → (`telegram-files`) →
19
- (`operator-channel`) → `telegram-configuration` → `telegram-polling`. Queues, an
20
- HTTP server for payment webhooks, schedules and metrics go between the database
21
- and the bot.
19
+ `telegram-configuration` → `telegram-polling`, with the optional
20
+ `operator-channel` beside the bot rather than under it. Required resources the
21
+ handlers need — queues, an HTTP server for payment webhooks — join the bot's
22
+ dependencies; optional ones — the operators' chat, metrics — stand beside it.
22
23
  - `src/index.ts` — the entry point: environment, journal, signals, and the one
23
24
  policy for a poller that ended on its own (shut down, exit 1, let the supervisor
24
25
  restart).
@@ -27,11 +27,14 @@ export interface BotApplicationConfig {
27
27
  /**
28
28
  * The graph, in start order:
29
29
  *
30
- * database → [telegram-files] → [operator-channel] → telegram-configuration → telegram-polling
30
+ * database → [telegram-files] → telegram-configuration → telegram-polling
31
+ * [operator-channel] beside the bot, not under it
31
32
  *
32
- * Product resources — queues, an HTTP server for payment webhooks, schedules,
33
- * metrics — go between the database and the bot, and join the bot's
34
- * `dependsOn` when handlers need them before the first update.
33
+ * The bot depends only on what it cannot answer without — required resources.
34
+ * An optional one (the operators' chat, metrics) stands beside the bot: a
35
+ * required resource may not depend on an optional one, and the bot must keep
36
+ * answering when the chat is unreachable. Queues and an HTTP server for
37
+ * payment webhooks are required and join the bot's `dependsOn`.
35
38
  */
36
39
  export function createBotApplication(config: BotApplicationConfig): ApplicationHandle {
37
40
  const database = createDatabase(config.databasePath);
@@ -59,17 +62,17 @@ export function createBotApplication(config: BotApplicationConfig): ApplicationH
59
62
  );
60
63
  }
61
64
  const operators = config.operators;
62
- if (operators) {
63
- botDependencies.push(
64
- defineManagedResource({
65
- id: 'operator-channel',
66
- required: false,
67
- start() {},
68
- drain: (context) => operators.drain(context.signal),
69
- close: () => operators.close(),
70
- }),
71
- );
72
- }
65
+ const beside: ManagedResource[] = operators
66
+ ? [
67
+ defineManagedResource({
68
+ id: 'operator-channel',
69
+ required: false,
70
+ start() {},
71
+ drain: (context) => operators.drain(context.signal),
72
+ close: () => operators.close(),
73
+ }),
74
+ ]
75
+ : [];
73
76
 
74
77
  const telegram = grammyBotResources({
75
78
  bot: config.bot,
@@ -84,7 +87,7 @@ export function createBotApplication(config: BotApplicationConfig): ApplicationH
84
87
 
85
88
  return createApplication({
86
89
  id: 'telegram-bot',
87
- resources: [...botDependencies, ...telegram.resources],
90
+ resources: [...botDependencies, ...beside, ...telegram.resources],
88
91
  shutdown: config.shutdown ?? { gracePeriodMs: 30_000, forceTimeoutMs: 5_000 },
89
92
  onResourceFailure: ({ resourceId, phase, error }) =>
90
93
  config.log.error('Resource failed', { resourceId, phase, error }),
@@ -1,5 +1,9 @@
1
1
  import { afterEach, describe, expect, test } from 'bun:test';
2
+ import { mkdir, mkdtemp, rm } from 'node:fs/promises';
3
+ import { tmpdir } from 'node:os';
4
+ import { join } from 'node:path';
2
5
  import { Bot } from 'grammy';
6
+ import { createTelegramLocalFiles, createTelegramOperatorChannel } from 'stitchkit/telegram';
3
7
  import { z } from 'zod';
4
8
  import { createBotApplication } from '../src/application';
5
9
  import { createLog } from '../src/log';
@@ -73,8 +77,12 @@ function fakeTelegram() {
73
77
  }
74
78
 
75
79
  const servers: { stop(): void }[] = [];
76
- afterEach(() => {
80
+ const directories: string[] = [];
81
+ afterEach(async () => {
77
82
  for (const server of servers.splice(0)) server.stop();
83
+ await Promise.all(
84
+ directories.splice(0).map((path) => rm(path, { recursive: true, force: true })),
85
+ );
78
86
  });
79
87
 
80
88
  async function until(check: () => boolean): Promise<void> {
@@ -117,4 +125,47 @@ describe('the bot application', () => {
117
125
  expect(Math.max(...telegram.offsets)).toBe(6);
118
126
  expect(ended).toEqual([]);
119
127
  });
128
+
129
+ test('starts with an operators chat and local Bot API files, and the channel drains on the way down', async () => {
130
+ const telegram = fakeTelegram();
131
+ servers.push(telegram);
132
+ const root = await mkdtemp(join(tmpdir(), 'template-bot-files-'));
133
+ directories.push(root);
134
+ await mkdir(join(root, '1:template'));
135
+ const bot = new Bot('1:template', { client: { apiRoot: telegram.origin } });
136
+ const posted: string[] = [];
137
+ const operators = createTelegramOperatorChannel<'users' | 'errors'>({
138
+ chatId: -100,
139
+ minIntervalMs: 1,
140
+ send: async (message) => void posted.push(message.text),
141
+ });
142
+ const app = createBotApplication({
143
+ bot,
144
+ databasePath: ':memory:',
145
+ log: createLog({ LOG_LEVEL: 'info' }),
146
+ operators,
147
+ files: createTelegramLocalFiles({ root, token: '1:template' }),
148
+ onPollingEnded: () => undefined,
149
+ shutdown: { gracePeriodMs: 2_000, forceTimeoutMs: 200 },
150
+ });
151
+
152
+ const snapshot = await app.start();
153
+ expect(snapshot.ready).toBe(true);
154
+ expect(snapshot.resources.map((resource) => resource.id)).toEqual([
155
+ 'database',
156
+ 'operator-channel',
157
+ 'telegram-files',
158
+ 'telegram-configuration',
159
+ 'telegram-polling',
160
+ ]);
161
+ // The bot waits for what it cannot answer without, never for the chat.
162
+ expect(
163
+ snapshot.resources.find((resource) => resource.id === 'telegram-configuration')
164
+ ?.dependsOn,
165
+ ).toEqual(['database', 'telegram-files']);
166
+ telegram.push(8);
167
+ await until(() => posted.length > 0);
168
+ expect(posted).toEqual(['New user 7']);
169
+ expect((await app.shutdown()).outcome).toBe('clean');
170
+ });
120
171
  });