@stacksjs/defaults 0.75.336 → 0.75.339

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.
@@ -577,6 +577,8 @@ buddy upgrade # upgrade Stacks framework (alias: update)
577
577
  --canary # upgrade to canary build
578
578
  --stable # switch to stable release
579
579
  -f/--force # force re-download
580
+ --dry-run # preview; marks any (downgrade) a real run refuses
581
+ --allow-downgrade # accept a release older than the app's own pins
580
582
  buddy upgrade:all # upgrade framework + dependencies + Bun + binary
581
583
  buddy upgrade:dependencies # upgrade dependencies (alias: upgrade:deps)
582
584
  buddy upgrade:bun # upgrade Bun runtime
@@ -513,15 +513,20 @@ Receipts acknowledge only messages through the id actually displayed.
513
513
  `send(id, body, retryKey, attachments?)` records files the application has
514
514
  already stored. The trait keeps a reference, never bytes: `file` is the
515
515
  application's own name for the upload (a disk path, a bucket key, an id), and
516
- `type`, `size`, `width` and `height` are optional. Resolving and authorizing
517
- the file stays with the application, so a disk, a bucket and a CDN look the
518
- same from here.
516
+ `type`, `name`, `size`, `width` and `height` are optional. `name` is what the
517
+ sender called the file (`Week 42 plan.pdf`): `file` is an unguessable stored
518
+ name, so for a document `name` is the only thing that tells the recipient what
519
+ they were sent. Resolving and authorizing the file stays with the application,
520
+ so a disk, a bucket and a CDN look the same from here.
519
521
 
520
522
  ```ts
521
523
  import type { DirectMessageAttachment } from '@stacksjs/chat'
522
524
 
523
525
  const photo: DirectMessageAttachment = { file: upload.key, type: 'image/jpeg', size: upload.size, width: 1200, height: 900 }
524
526
  await messenger.send(thread.id, '', retryKey, [photo])
527
+
528
+ const plan: DirectMessageAttachment = { file: upload.key, type: 'application/pdf', name: upload.originalName, size: upload.size }
529
+ await messenger.send(thread.id, '', retryKey, [plan])
525
530
  ```
526
531
 
527
532
  - A photo with no caption is a message: `body` may be empty when there is at
@@ -531,8 +536,10 @@ await messenger.send(thread.id, '', retryKey, [photo])
531
536
  - A retry key covers the attachments too. The same key with a different file
532
537
  is a different message and returns 409, rather than silently keeping the
533
538
  first photo.
534
- - At most 10 attachments, 4000 characters once serialised. Keys the trait does
535
- not know are dropped, so the column cannot become a second message body.
539
+ - At most 10 attachments, 4000 characters once serialised. `type` and `name`
540
+ are 1 to 255 characters. A key the trait does not know is refused with a 422
541
+ that names it, rather than dropped, so the column cannot become a second
542
+ message body and nothing a caller sent goes missing silently.
536
543
  - The column is `chat_messages.attachments`, added by `buddy migrate` to a
537
544
  database created before it. A database that has not migrated yet still sends
538
545
  text.
@@ -194,6 +194,20 @@ autoLoadEnv() // .env, .env.local, .env.<effectiveEnv>, .env.<effectiveEnv>.loc
194
194
  envPlugin()
195
195
  ```
196
196
 
197
+ To keep a key out of a child process, withhold it rather than deleting it.
198
+ `delete process.env.X` before a spawn does nothing useful: Bun loads `.env`
199
+ natively in the child and `loadEnv` fills anything absent, so the child reads
200
+ the value straight back from disk (#3028). Pass `withholdEnv(keys)` into the
201
+ child's `env` (it sets `STACKS_ENV_WITHHOLD`). `loadEnv` then skips those keys
202
+ in every file and removes any copy Bun loaded natively; a child that runs no
203
+ Stacks preload calls `applyWithheldEnv()` before reading config.
204
+
205
+ ```typescript
206
+ import { withholdEnv } from '@stacksjs/env'
207
+
208
+ await runAction(Action.BuildIos, { env: withholdEnv(['MOBILE_URL']) })
209
+ ```
210
+
197
211
  ## .env Parser
198
212
 
199
213
  ```typescript
@@ -206,6 +206,12 @@ While an app is in TestFlight before its first public release, keep the iOS
206
206
  marketing version at `1.0.0` and run `buddy release:ios --bump build`. Each
207
207
  upload needs a new build number, not a patch to the marketing version.
208
208
 
209
+ A release never reads dev-shell overrides. `release:ios` withholds
210
+ `MOBILE_URL`, `IOS_APP_VERSION` and `IOS_BUILD_NUMBER` from its build child
211
+ (`withholdEnv` from `@stacksjs/env`, since deleting them from `process.env` is
212
+ undone by the child's own `.env` load), and refuses a generated
213
+ `devServerURL` on a local address before committing or tagging (#3028).
214
+
209
215
  Each `release:ios` also creates the tag's GitHub release (the TestFlight build
210
216
  number and the changes since the previous tag), using `GITHUB_TOKEN`,
211
217
  `GH_TOKEN`, the GitHub CLI's token or the git credential. When that fails the
@@ -11,8 +11,8 @@ Browser runtime bindings are documented separately in `stacks-auto-imports`.
11
11
  | Package | Task guidance | Source entry | Retained tests |
12
12
  |---|---|---|---|
13
13
  | `@stacksjs/action-runner` | [`stacks-actions`](../stacks-actions/SKILL.md) | [entrypoint](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/action-runner/src/index.ts) | 1 |
14
- | `@stacksjs/actions` | [`stacks-actions`](../stacks-actions/SKILL.md) | [entrypoint](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/actions/src/index.ts) | 89 |
15
- | `@stacksjs/ai` | [`stacks-ai`](../stacks-ai/SKILL.md) | [entrypoint](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/ai/src/index.ts) | 17 |
14
+ | `@stacksjs/actions` | [`stacks-actions`](../stacks-actions/SKILL.md) | [entrypoint](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/actions/src/index.ts) | 91 |
15
+ | `@stacksjs/ai` | [`stacks-ai`](../stacks-ai/SKILL.md) | [entrypoint](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/ai/src/index.ts) | 18 |
16
16
  | `@stacksjs/alias` | [`stacks-alias`](../stacks-alias/SKILL.md) | [entrypoint](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/alias/src/index.ts) | 1 |
17
17
  | `@stacksjs/analytics` | [`stacks-analytics`](../stacks-analytics/SKILL.md) | [entrypoint](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/analytics/src/index.ts) | 6 |
18
18
  | `@stacksjs/api` | [`stacks-api`](../stacks-api/SKILL.md) | [entrypoint](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/api/src/index.ts) | 9 |
@@ -21,7 +21,7 @@ Browser runtime bindings are documented separately in `stacks-auto-imports`.
21
21
  | `@stacksjs/auth` | [`stacks-auth`](../stacks-auth/SKILL.md) | [entrypoint](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/auth/src/index.ts) | 120 |
22
22
  | `@stacksjs/browser` | [`stacks-browser`](../stacks-browser/SKILL.md) | [entrypoint](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/browser/src/index.ts) | 8 |
23
23
  | `@stacksjs/browser-extension` | [`stacks-browser-extension`](../stacks-browser-extension/SKILL.md) | [entrypoint](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/browser-extension/src/index.ts) | 7 |
24
- | `@stacksjs/buddy` | [`stacks-buddy`](../stacks-buddy/SKILL.md) | [entrypoint](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/buddy/src/index.ts) | 169 |
24
+ | `@stacksjs/buddy` | [`stacks-buddy`](../stacks-buddy/SKILL.md) | [entrypoint](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/buddy/src/index.ts) | 170 |
25
25
  | `@stacksjs/build` | [`stacks-build`](../stacks-build/SKILL.md) | [entrypoint](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/build/src/index.ts) | 4 |
26
26
  | `@stacksjs/cache` | [`stacks-cache`](../stacks-cache/SKILL.md) | [entrypoint](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/cache/src/index.ts) | 14 |
27
27
  | `@stacksjs/calendar-api` | [`stacks-calendar`](../stacks-calendar/SKILL.md) | [entrypoint](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/calendar-api/src/index.ts) | 2 |
@@ -45,7 +45,7 @@ Browser runtime bindings are documented separately in `stacks-auto-imports`.
45
45
  | `@stacksjs/docs` | [`stacks-docs`](../stacks-docs/SKILL.md) | [entrypoint](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/docs/src/index.ts) | 2 |
46
46
  | `@stacksjs/email` | [`stacks-email`](../stacks-email/SKILL.md) | [entrypoint](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/email/src/index.ts) | 23 |
47
47
  | `@stacksjs/enums` | [`stacks-enums`](../stacks-enums/SKILL.md) | [entrypoint](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/enums/src/index.ts) | 3 |
48
- | `@stacksjs/env` | [`stacks-env`](../stacks-env/SKILL.md) | [entrypoint](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/env/src/index.ts) | 30 |
48
+ | `@stacksjs/env` | [`stacks-env`](../stacks-env/SKILL.md) | [entrypoint](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/env/src/index.ts) | 31 |
49
49
  | `@stacksjs/error-handling` | [`stacks-error-handling`](../stacks-error-handling/SKILL.md) | [entrypoint](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/error-handling/src/index.ts) | 10 |
50
50
  | `@stacksjs/events` | [`stacks-events`](../stacks-events/SKILL.md) | [entrypoint](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/events/src/index.ts) | 12 |
51
51
  | `@stacksjs/faker` | [`stacks-faker`](../stacks-faker/SKILL.md) | [entrypoint](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/faker/src/index.ts) | 1 |
@@ -68,14 +68,14 @@ Browser runtime bindings are documented separately in `stacks-auto-imports`.
68
68
  | `@stacksjs/orm` | [`stacks-orm`](../stacks-orm/SKILL.md) | [entrypoint](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/orm/src/index.ts) | 73 |
69
69
  | `@stacksjs/pagination` | [`stacks-pagination`](../stacks-pagination/SKILL.md) | [entrypoint](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/pagination/src/index.ts) | 0 |
70
70
  | `@stacksjs/path` | [`stacks-path`](../stacks-path/SKILL.md) | [entrypoint](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/path/src/index.ts) | 13 |
71
- | `@stacksjs/payments` | [`stacks-payments`](../stacks-payments/SKILL.md) | [entrypoint](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/payments/src/index.ts) | 29 |
71
+ | `@stacksjs/payments` | [`stacks-payments`](../stacks-payments/SKILL.md) | [entrypoint](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/payments/src/index.ts) | 30 |
72
72
  | `@stacksjs/push` | [`stacks-push`](../stacks-push/SKILL.md) | [entrypoint](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/push/src/index.ts) | 6 |
73
73
  | `@stacksjs/query-builder` | [`stacks-query-builder`](../stacks-query-builder/SKILL.md) | [entrypoint](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/query-builder/src/index.ts) | 6 |
74
74
  | `@stacksjs/queue` | [`stacks-queue`](../stacks-queue/SKILL.md) | [entrypoint](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/queue/src/index.ts) | 49 |
75
75
  | `@stacksjs/realtime` | [`stacks-realtime`](../stacks-realtime/SKILL.md) | [entrypoint](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/realtime/src/index.ts) | 12 |
76
76
  | `@stacksjs/registry` | [`stacks-registry`](../stacks-registry/SKILL.md) | [entrypoint](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/registry/src/index.ts) | 1 |
77
77
  | `@stacksjs/repl` | [`stacks-repl`](../stacks-repl/SKILL.md) | [entrypoint](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/repl/src/index.ts) | 2 |
78
- | `@stacksjs/router` | [`stacks-router`](../stacks-router/SKILL.md) | [entrypoint](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/router/src/index.ts) | 96 |
78
+ | `@stacksjs/router` | [`stacks-router`](../stacks-router/SKILL.md) | [entrypoint](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/router/src/index.ts) | 97 |
79
79
  | `@stacksjs/scheduler` | [`stacks-scheduler`](../stacks-scheduler/SKILL.md) | [entrypoint](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/scheduler/src/index.ts) | 14 |
80
80
  | `@stacksjs/search-engine` | [`stacks-search-engine`](../stacks-search-engine/SKILL.md) | [entrypoint](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/search-engine/src/index.ts) | 11 |
81
81
  | `@stacksjs/security` | [`stacks-security`](../stacks-security/SKILL.md) | [entrypoint](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/security/src/index.ts) | 11 |
@@ -119,7 +119,7 @@ Declared runtime exports: `BlogAdminError`, `DEFAULT_IOS_DEPENDENCIES`, `DEFAULT
119
119
 
120
120
  Local source modules: [src/action.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/actions/src/action.ts), [src/add.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/actions/src/add.ts), [src/blog-admin.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/actions/src/blog-admin.ts), [src/build/ios-toolchain.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/actions/src/build/ios-toolchain.ts), [src/commit.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/actions/src/commit.ts), [src/dev/index.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/actions/src/dev/index.ts), [src/discover-packages.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/actions/src/discover-packages.ts), [src/generate/index.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/actions/src/generate/index.ts), [src/github-release.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/actions/src/github-release.ts), [src/helpers/index.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/actions/src/helpers/index.ts), [src/library/index.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/actions/src/library/index.ts), [src/lint/lint.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/actions/src/lint/lint.ts), [src/lint/stx-gate.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/actions/src/lint/stx-gate.ts), [src/make-command.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/actions/src/make-command.ts), [src/make-job.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/actions/src/make-job.ts), [src/make-policy.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/actions/src/make-policy.ts), [src/make-resource.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/actions/src/make-resource.ts), [src/make.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/actions/src/make.ts), [src/publication-preflight.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/actions/src/publication-preflight.ts), [src/release-evidence.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/actions/src/release-evidence.ts), [src/scaffold-crud.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/actions/src/scaffold-crud.ts), [src/setup/index.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/actions/src/setup/index.ts), [src/stacks.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/actions/src/stacks.ts), [src/stx-source.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/actions/src/stx-source.ts), [src/upgrade/package-project.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/actions/src/upgrade/package-project.ts), [src/upgrade/route-actions.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/actions/src/upgrade/route-actions.ts).
121
121
 
122
- Evidence directory: [89 retained tests](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/actions/tests). These files are evidence pointers, not a claim that all tests were run for this catalog.
122
+ Evidence directory: [91 retained tests](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/actions/tests). These files are evidence pointers, not a claim that all tests were run for this catalog.
123
123
 
124
124
  ### @stacksjs/ai
125
125
 
@@ -127,9 +127,9 @@ Guidance: [`stacks-ai`](../stacks-ai/SKILL.md). Source: [storage/framework/core/
127
127
 
128
128
  Declared runtime exports: `assistantTurn`, `buildMessageWithImages`, `clearUsageReporters`, `estimateMessageTokens`, `estimateTokens`, `fetchWithRetry`, `listUsageReporters`, `normalizeMessagesForProvider`, `onUsage`, `recordUsage`, `sanitizePrompt`, `toolResultsTurn`.
129
129
 
130
- Local source modules: [src/agents/index.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/ai/src/agents/index.ts), [src/buddy.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/ai/src/buddy.ts), [src/client.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/ai/src/client.ts), [src/context.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/ai/src/context.ts), [src/drivers/index.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/ai/src/drivers/index.ts), [src/image.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/ai/src/image.ts), [src/mcp-server.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/ai/src/mcp-server.ts), [src/mcp.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/ai/src/mcp.ts), [src/models.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/ai/src/models.ts), [src/personalization.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/ai/src/personalization.ts), [src/search.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/ai/src/search.ts), [src/speech.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/ai/src/speech.ts), [src/text.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/ai/src/text.ts), [src/types.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/ai/src/types.ts), [src/utils/client-bedrock-runtime.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/ai/src/utils/client-bedrock-runtime.ts), [src/utils/client-bedrock.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/ai/src/utils/client-bedrock.ts), [src/utils/model-access.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/ai/src/utils/model-access.ts), [src/utils/retry.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/ai/src/utils/retry.ts), [src/utils/tokens.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/ai/src/utils/tokens.ts), [src/utils/tools.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/ai/src/utils/tools.ts), [src/utils/usage.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/ai/src/utils/usage.ts), [src/utils/vision.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/ai/src/utils/vision.ts).
130
+ Local source modules: [src/agents/index.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/ai/src/agents/index.ts), [src/buddy.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/ai/src/buddy.ts), [src/client.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/ai/src/client.ts), [src/context.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/ai/src/context.ts), [src/documents.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/ai/src/documents.ts), [src/drivers/index.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/ai/src/drivers/index.ts), [src/image.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/ai/src/image.ts), [src/mcp-server.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/ai/src/mcp-server.ts), [src/mcp.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/ai/src/mcp.ts), [src/models.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/ai/src/models.ts), [src/personalization.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/ai/src/personalization.ts), [src/search.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/ai/src/search.ts), [src/speech.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/ai/src/speech.ts), [src/text.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/ai/src/text.ts), [src/types.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/ai/src/types.ts), [src/utils/client-bedrock-runtime.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/ai/src/utils/client-bedrock-runtime.ts), [src/utils/client-bedrock.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/ai/src/utils/client-bedrock.ts), [src/utils/model-access.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/ai/src/utils/model-access.ts), [src/utils/retry.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/ai/src/utils/retry.ts), [src/utils/tokens.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/ai/src/utils/tokens.ts), [src/utils/tools.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/ai/src/utils/tools.ts), [src/utils/usage.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/ai/src/utils/usage.ts), [src/utils/vision.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/ai/src/utils/vision.ts).
131
131
 
132
- Evidence directory: [17 retained tests](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/ai/tests). These files are evidence pointers, not a claim that all tests were run for this catalog.
132
+ Evidence directory: [18 retained tests](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/ai/tests). These files are evidence pointers, not a claim that all tests were run for this catalog.
133
133
 
134
134
  ### @stacksjs/alias
135
135
 
@@ -217,7 +217,7 @@ Read the entrypoint and package export map for re-exported or type-only surfaces
217
217
 
218
218
  Local source modules: [src/commands/index.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/buddy/src/commands/index.ts).
219
219
 
220
- Evidence directory: [169 retained tests](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/buddy/tests). These files are evidence pointers, not a claim that all tests were run for this catalog.
220
+ Evidence directory: [170 retained tests](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/buddy/tests). These files are evidence pointers, not a claim that all tests were run for this catalog.
221
221
 
222
222
  ### @stacksjs/build
223
223
 
@@ -443,9 +443,9 @@ Guidance: [`stacks-env`](../stacks-env/SKILL.md). Source: [storage/framework/cor
443
443
 
444
444
  Read the entrypoint and package export map for re-exported or type-only surfaces.
445
445
 
446
- Local source modules: [src/cli.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/env/src/cli.ts), [src/crypto.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/env/src/crypto.ts), [src/deployment.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/env/src/deployment.ts), [src/integrations.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/env/src/integrations.ts), [src/numeric-keys.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/env/src/numeric-keys.ts), [src/parser.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/env/src/parser.ts), [src/plaintext.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/env/src/plaintext.ts), [src/plugin.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/env/src/plugin.ts), [src/runtime.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/env/src/runtime.ts), [src/tenants.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/env/src/tenants.ts), [src/types.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/env/src/types.ts), [src/utils.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/env/src/utils.ts).
446
+ Local source modules: [src/cli.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/env/src/cli.ts), [src/crypto.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/env/src/crypto.ts), [src/deployment.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/env/src/deployment.ts), [src/integrations.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/env/src/integrations.ts), [src/numeric-keys.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/env/src/numeric-keys.ts), [src/parser.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/env/src/parser.ts), [src/plaintext.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/env/src/plaintext.ts), [src/plugin.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/env/src/plugin.ts), [src/runtime.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/env/src/runtime.ts), [src/tenants.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/env/src/tenants.ts), [src/types.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/env/src/types.ts), [src/utils.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/env/src/utils.ts), [src/withheld.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/env/src/withheld.ts).
447
447
 
448
- Evidence directory: [30 retained tests](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/env/tests). These files are evidence pointers, not a claim that all tests were run for this catalog.
448
+ Evidence directory: [31 retained tests](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/env/tests). These files are evidence pointers, not a claim that all tests were run for this catalog.
449
449
 
450
450
  ### @stacksjs/error-handling
451
451
 
@@ -661,7 +661,7 @@ Declared runtime exports: `Payment`, `freshIdempotencyKey`, `stacksIdempotencyKe
661
661
 
662
662
  Local source modules: [src/billable/index.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/payments/src/billable/index.ts), [src/checkout-outcome.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/payments/src/checkout-outcome.ts), [src/connect/index.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/payments/src/connect/index.ts), [src/direct-debit.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/payments/src/direct-debit.ts), [src/driver/index.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/payments/src/driver/index.ts), [src/drivers/stripe.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/payments/src/drivers/stripe.ts), [src/idempotency.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/payments/src/idempotency.ts), [src/payment.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/payments/src/payment.ts), [src/subscription-state.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/payments/src/subscription-state.ts), [src/terminal.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/payments/src/terminal.ts).
663
663
 
664
- Evidence directory: [29 retained tests](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/payments/tests). These files are evidence pointers, not a claim that all tests were run for this catalog.
664
+ Evidence directory: [30 retained tests](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/payments/tests). These files are evidence pointers, not a claim that all tests were run for this catalog.
665
665
 
666
666
  ### @stacksjs/push
667
667
 
@@ -721,11 +721,11 @@ Evidence directory: [2 retained tests](https://github.com/stacksjs/stacks/blob/m
721
721
 
722
722
  Guidance: [`stacks-router`](../stacks-router/SKILL.md). Source: [storage/framework/core/router/src/index.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/router/src/index.ts).
723
723
 
724
- Declared runtime exports: `DASHBOARD_ROUTE_GROUPS`, `DEFAULT_ROUTE_BUNDLE_FEATURES`, `DEFAULT_ROUTE_REGISTRY`, `DatabaseSessionStore`, `EncryptedSessionStore`, `FileSessionStore`, `JSON_CONTENT_TYPE`, `MemorySessionStore`, `Middleware`, `PathParamError`, `RedisSessionStore`, `RequestBodyError`, `appRouteRegistry`, `assertRouteMiddlewareResolvable`, `bundleMounts`, `cacheRedisConnection`, `cacheRequestQuery`, `checkApplicationHealth`, `clearCsrfModuleCache`, `clearCurrentRequest`, `clearMiddlewareCache`, `clearRateLimit`, `clearRouteModelBindings`, `clearTrackedQueries`, `clientAddress`, `configureViewDirectories`, `createErrorResponse`, `createMiddlewareErrorResponse`, `createNotFoundResponse`, `createSessionStore`, `createStacksRouter`, `createStacksSessionStore`, `createTypedRouter`, `createValidationErrorResponse`, `dashboardGroupMiddleware`, `defineMiddleware`, `defineRouteModelBinding`, `describeUnknownDashboardGroups`, `disableViewRouting`, `enhanceRequest`, `fallbackStorage`, `findUnresolvableRouteMiddleware`, `getCurrentRequest`, `getQueryShapeCounts`, `getTraceId`, `installMiddlewareHotReload`, `isApiRequest`, `isRouterAction`, `listNamedRoutes`, `listRedirectRoutes`, `listRegisteredRoutes`, `listRootMountedAppRoutes`, `loadMiddlewareHandlers`, `loadRoutes`, `mountedDefaultRouteBundles`, `parseJsonText`, `rateLimit`, `rateLimitScale`, `rateLimitStatus`, `readJsonBody`, `readTextBody`, `request`, `resetBootHooks`, `resetRateLimiters`, `resetSharedRateLimitStorage`, `resolveDashboardAccess`, `resolveRouteModel`, `route`, `routeModelBindings`, `routeParams`, `runBootHooks`, `runHealthProbes`, `runWithRequest`, `safePathParam`, `sanitizePathParam`, `scaledLimit`, `serve`, `serverResponse`, `setCurrentRequest`, `setRouteModelFallback`, `sharedRateLimitStorage`, `signUrl`, `signedUrl`, `stream`, `trackQuery`, `trustedProxies`, `url`, `useRateLimitStorage`, `verifySignedUrl`, `verifySignedUrlMiddleware`, `warnOnMultipleRouterInstances`, `withRouteAdmission`, `withTraceId`, `wrapAction`.
724
+ Declared runtime exports: `DASHBOARD_ROUTE_GROUPS`, `DEFAULT_ROUTE_BUNDLE_FEATURES`, `DEFAULT_ROUTE_REGISTRY`, `DatabaseSessionStore`, `EncryptedSessionStore`, `FileSessionStore`, `JSON_CONTENT_TYPE`, `MemorySessionStore`, `Middleware`, `PathParamError`, `RedisSessionStore`, `RequestBodyError`, `appRouteRegistry`, `assertRouteMiddlewareResolvable`, `bundleMounts`, `cacheRedisConnection`, `cacheRequestQuery`, `checkApplicationHealth`, `clearCsrfModuleCache`, `clearCurrentRequest`, `clearMiddlewareCache`, `clearRateLimit`, `clearRouteModelBindings`, `clearTrackedQueries`, `clientAddress`, `configureViewDirectories`, `createErrorResponse`, `createMiddlewareErrorResponse`, `createNotFoundResponse`, `createSessionStore`, `createStacksRouter`, `createStacksSessionStore`, `createTypedRouter`, `createValidationErrorResponse`, `dashboardGroupMiddleware`, `defineMiddleware`, `defineRouteModelBinding`, `describeUnknownDashboardGroups`, `disableViewRouting`, `enhanceRequest`, `fallbackStorage`, `findUnresolvableRouteMiddleware`, `getCurrentRequest`, `getQueryShapeCounts`, `getTraceId`, `installMiddlewareHotReload`, `isApiRequest`, `isRouterAction`, `listNamedRoutes`, `listRedirectRoutes`, `listRegisteredRoutes`, `listRootMountedAppRoutes`, `loadMiddlewareHandlers`, `loadRoutes`, `mountedDefaultRouteBundles`, `parseJsonText`, `rateLimit`, `rateLimitScale`, `rateLimitStatus`, `readJsonBody`, `readTextBody`, `request`, `resetBootHooks`, `resetRateLimiters`, `resetSharedRateLimitStorage`, `resetShutdownHooks`, `resolveDashboardAccess`, `resolveRouteModel`, `route`, `routeModelBindings`, `routeParams`, `runBootHooks`, `runHealthProbes`, `runShutdownHooks`, `runWithRequest`, `safePathParam`, `sanitizePathParam`, `scaledLimit`, `serve`, `serverResponse`, `setCurrentRequest`, `setRouteModelFallback`, `sharedRateLimitStorage`, `signUrl`, `signedUrl`, `stream`, `trackQuery`, `trustedProxies`, `url`, `useRateLimitStorage`, `verifySignedUrl`, `verifySignedUrlMiddleware`, `warnOnMultipleRouterInstances`, `withRouteAdmission`, `withTraceId`, `wrapAction`.
725
725
 
726
726
  Local source modules: [src/api-shape.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/router/src/api-shape.ts), [src/body.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/router/src/body.ts), [src/dashboard-access.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/router/src/dashboard-access.ts), [src/encrypted-session-store.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/router/src/encrypted-session-store.ts), [src/error-handler.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/router/src/error-handler.ts), [src/health.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/router/src/health.ts), [src/json.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/router/src/json.ts), [src/middleware.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/router/src/middleware.ts), [src/path-sanitize.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/router/src/path-sanitize.ts), [src/rate-limit.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/router/src/rate-limit.ts), [src/request-augmentation.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/router/src/request-augmentation.ts), [src/request-context.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/router/src/request-context.ts), [src/route-loader.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/router/src/route-loader.ts), [src/route-model-binding.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/router/src/route-model-binding.ts), [src/security-settings.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/router/src/security-settings.ts), [src/session-factory.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/router/src/session-factory.ts), [src/signed-url.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/router/src/signed-url.ts), [src/stacks-router.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/router/src/stacks-router.ts), [src/typed-router.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/router/src/typed-router.ts).
727
727
 
728
- Evidence directory: [96 retained tests](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/router/tests). These files are evidence pointers, not a claim that all tests were run for this catalog.
728
+ Evidence directory: [97 retained tests](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/router/tests). These files are evidence pointers, not a claim that all tests were run for this catalog.
729
729
 
730
730
  ### @stacksjs/scheduler
731
731
 
@@ -417,6 +417,31 @@ await serve({ port: 3000 })
417
417
  const response = await serverResponse(request)
418
418
  ```
419
419
 
420
+ ### Boot and shutdown hooks
421
+
422
+ ```typescript
423
+ import { route } from '@stacksjs/router'
424
+
425
+ // Once, after the routes load and before the first request. Optional
426
+ // preparation only: a hook that throws is logged and the boot continues.
427
+ route.booting('warm grammars', () => highlighter.load())
428
+
429
+ // When the server is told to stop. For whatever would otherwise outlive the
430
+ // process or end unrecorded: open sessions, child processes, a buffer to flush.
431
+ route.stopping('terminal sessions', () => terminalSessions.closeAll())
432
+ ```
433
+
434
+ - Boot hooks run in order, once per process, from `serve()`.
435
+ - Shutdown hooks run concurrently, once per process (a second signal waits on
436
+ the same run). One that throws is logged and the rest still run.
437
+ - The production API server (`core/actions/src/serve/api.ts`) runs them on
438
+ SIGTERM alongside draining in-flight requests. `SHUTDOWN_GRACE_MS` (default
439
+ fifteen seconds) caps both, so a hook that hangs cannot hold the process.
440
+ - The list lives on `globalThis`, so a module registering through a second copy
441
+ of `@stacksjs/router` still reaches the one the server runs.
442
+ - `buddy dev:api` has no shutdown handler: Ctrl-C ends the process group, and
443
+ shutdown hooks do not run there.
444
+
420
445
  ## URL Generation
421
446
 
422
447
  ```typescript
@@ -1,13 +1,14 @@
1
1
  import type { UserModel } from '@stacksjs/orm'
2
2
  import type { RemoteHost } from './remote-commands'
3
3
  import type { TerminalAuditSink, TerminalEvent, TerminalProcess, TerminalSessionOptions } from './remote-terminal'
4
- import { describe, expect, it } from 'bun:test'
5
- import { existsSync, readdirSync } from 'node:fs'
4
+ import { describe, expect, it, jest } from 'bun:test'
5
+ import { existsSync } from 'node:fs'
6
6
  import { tmpdir } from 'node:os'
7
+ import { dirname } from 'node:path'
7
8
  import { sshTerminalArgv } from './remote-commands'
8
9
  import { TerminalSessions } from './remote-terminal'
9
10
  import { createPtySpawner } from './ssh-runner'
10
- import { encodeTerminalEvent, terminalEventStream } from './terminal-sessions'
11
+ import { encodeTerminalEvent, terminalEventStream, terminalSessions } from './terminal-sessions'
11
12
 
12
13
  /**
13
14
  * Interactive terminal sessions (stacksjs/stacks#960). Every rule is asserted
@@ -236,16 +237,28 @@ describe('a session does not outlive its purpose', () => {
236
237
  })
237
238
 
238
239
  it('input keeps it alive; the max duration ends it regardless', async () => {
239
- const { manager, fakes, audit } = sessions({ idleTimeoutMs: 40, maxDurationMs: 120, detachGraceMs: 10_000 })
240
- const { id } = await manager.open({ user: alice, hosts, hostKey: 'app', authorizer: allowAll, cols: 80, rows: 24 })
241
- for (let i = 0; i < 4; i++) {
242
- await Bun.sleep(25)
243
- await manager.input(id, alice, 'x')
240
+ // The clock is driven, not slept on: the deadlines under test run on the
241
+ // same clock a sleep waits on, so under load a sleep overshot them (#2978).
242
+ jest.useFakeTimers()
243
+ try {
244
+ const { manager, fakes, audit } = sessions({ idleTimeoutMs: 40, maxDurationMs: 120, detachGraceMs: 10_000 })
245
+ const { id } = await manager.open({ user: alice, hosts, hostKey: 'app', authorizer: allowAll, cols: 80, rows: 24 })
246
+ for (let i = 0; i < 4; i++) {
247
+ jest.advanceTimersByTime(25)
248
+ await manager.input(id, alice, 'x')
249
+ }
250
+ const ending = take(manager.watch(id, alice), 1)
251
+ // 119ms in: more than twice the idle timeout, kept open by the input.
252
+ jest.advanceTimersByTime(19)
253
+ expect(fakes[0]!.killed).toBe(false)
254
+ jest.advanceTimersByTime(1)
255
+ expect(fakes[0]!.killed).toBe(true)
256
+ expect((await ending).map(text)).toEqual(['<max-duration>'])
257
+ expect(audit.log.at(-1)).toStartWith('closed max-duration')
258
+ }
259
+ finally {
260
+ jest.useRealTimers()
244
261
  }
245
- expect(fakes[0]!.killed).toBe(false)
246
- await Bun.sleep(60)
247
- expect(fakes[0]!.killed).toBe(true)
248
- expect(audit.log.at(-1)).toStartWith('closed max-duration')
249
262
  })
250
263
 
251
264
  it('ends when nobody attaches, and a grace period after the last viewer leaves', async () => {
@@ -272,6 +285,40 @@ describe('a session does not outlive its purpose', () => {
272
285
  await expect(manager.close(id, alice)).rejects.toThrow('No such terminal session')
273
286
  expect(audit.log.slice(1)).toEqual(['input rm -rf /tmp/build', 'closed closed in=17 out=0'])
274
287
  })
288
+
289
+ it('shuts down only once every process has exited, so nothing it made outlives it', async () => {
290
+ // The real spawner removes its temp directory when the process exits; a
291
+ // shutdown that returned on the kill raced the process exiting (#2978).
292
+ let exit!: (code: number) => void
293
+ const { manager } = sessions({
294
+ spawn: () => ({ write: () => {}, resize: () => {}, kill: () => {}, exited: new Promise<number>((resolve) => { exit = resolve }) }),
295
+ })
296
+ await manager.open({ user: alice, hosts, hostKey: 'app', authorizer: allowAll, cols: 80, rows: 24 })
297
+
298
+ const closing = manager.closeAll().then(() => 'closed')
299
+ expect(await Promise.race([closing, Bun.sleep(20).then(() => 'waiting')])).toBe('waiting')
300
+ exit(-1)
301
+ expect(await closing).toBe('closed')
302
+ })
303
+
304
+ it('is closed by the server stopping, through the router\'s shutdown hooks (#3034)', async () => {
305
+ const registered = (globalThis as Record<symbol, { hooks: Array<{ name: string, run: () => unknown }> }>)[Symbol.for('@stacksjs/router:shutdown-hooks')]
306
+ const hook = registered?.hooks.find(entry => entry.name === 'terminal sessions')
307
+ expect(hook).toBeDefined()
308
+
309
+ // The app's own sessions, not a copy: the hook has to reach the manager
310
+ // the terminal actions open sessions on.
311
+ const closeAll = terminalSessions.closeAll
312
+ let closed = 0
313
+ terminalSessions.closeAll = async () => { closed += 1 }
314
+ try {
315
+ await hook!.run()
316
+ }
317
+ finally {
318
+ terminalSessions.closeAll = closeAll
319
+ }
320
+ expect(closed).toBe(1)
321
+ })
275
322
  })
276
323
 
277
324
  describe('the event stream', () => {
@@ -304,7 +351,6 @@ describe('the PTY spawner', () => {
304
351
  })
305
352
 
306
353
  it('carries output, input and a window resize through a real PTY, and cleans up', async () => {
307
- const before = readdirSync(tmpdir()).filter(name => name.startsWith('stacks-terminal-')).length
308
354
  let knownHosts = ''
309
355
  const spawn = createPtySpawner((_host, path) => {
310
356
  knownHosts = path
@@ -315,7 +361,10 @@ describe('the PTY spawner', () => {
315
361
  const process = await spawn(shellHost, { cols: 100, rows: 30 }, (bytes) => { output += new TextDecoder().decode(bytes) })
316
362
  expect(existsSync(knownHosts)).toBe(true)
317
363
 
318
- await Bun.sleep(150)
364
+ // Resize once the shell has reported the size it started at, not after a
365
+ // guess at how long that takes.
366
+ for (let i = 0; i < 200 && !output.includes('30 100'); i++)
367
+ await Bun.sleep(10)
319
368
  process.resize(132, 40)
320
369
  process.write('hello\r')
321
370
  expect(await process.exited).toBe(0)
@@ -324,8 +373,9 @@ describe('the PTY spawner', () => {
324
373
  expect(output).toContain('30 100')
325
374
  expect(output).toContain('40 132')
326
375
  expect(output).toContain('got:hello')
327
- expect(existsSync(knownHosts)).toBe(false)
328
- expect(readdirSync(tmpdir()).filter(name => name.startsWith('stacks-terminal-')).length).toBe(before)
376
+ // The directory this spawn created, not a count of the shared temp
377
+ // directory, which other processes change underneath the test (#2978).
378
+ expect(existsSync(dirname(knownHosts))).toBe(false)
329
379
  })
330
380
  })
331
381
 
@@ -261,9 +261,15 @@ export class TerminalSessions {
261
261
  await this.end(this.find(id, user), 'closed')
262
262
  }
263
263
 
264
- /** End every session, for a server shutting down. */
264
+ /**
265
+ * End every session, for a server shutting down, and wait for each process
266
+ * to exit: the spawner cleans up after the process, and a server that exits
267
+ * first leaves that behind.
268
+ */
265
269
  async closeAll(): Promise<void> {
266
- await Promise.all([...this.sessions.values()].map(session => this.end(session, 'shutdown')))
270
+ const open = [...this.sessions.values()]
271
+ await Promise.all(open.map(session => this.end(session, 'shutdown')))
272
+ await Promise.all(open.map(session => session.process.exited))
267
273
  }
268
274
 
269
275
  /**
@@ -1,5 +1,6 @@
1
1
  import type { TerminalAuthorizer, TerminalEvent } from './remote-terminal'
2
2
  import { Gate } from '@stacksjs/auth'
3
+ import { route } from '@stacksjs/router'
3
4
  import { TerminalSessions } from './remote-terminal'
4
5
  import { createPtySpawner, loggingTerminalAuditSink } from './ssh-runner'
5
6
 
@@ -18,6 +19,10 @@ export const terminalSessions: TerminalSessions = new TerminalSessions({
18
19
  audit: loggingTerminalAuditSink,
19
20
  })
20
21
 
22
+ // A server that stops closes its sessions first, so each one records why it
23
+ // ended and its temp directory goes with its process (stacksjs/stacks#3034).
24
+ route.stopping('terminal sessions', () => terminalSessions.closeAll())
25
+
21
26
  /**
22
27
  * The `open-remote-terminal` gate, or undefined when the app has not defined
23
28
  * it - and `TerminalSessions.open` refuses in that case rather than proceeding.
@@ -2,7 +2,7 @@
2
2
  "publisher": "Stacks",
3
3
  "name": "vscode-stacks",
4
4
  "displayName": "Stacks",
5
- "version": "0.75.336",
5
+ "version": "0.75.339",
6
6
  "description": "Stacks, built into VS Code: stx templates, pickier linting and formatting, encrypted env files, the dev server preview and every buddy command. No other extensions required.",
7
7
  "license": "MIT",
8
8
  "funding": "https://github.com/sponsors/chrisbbreuer",
package/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "name": "@stacksjs/defaults",
3
3
  "type": "module",
4
4
  "sideEffects": false,
5
- "version": "0.75.336",
5
+ "version": "0.75.339",
6
6
  "repository": {
7
7
  "type": "git",
8
8
  "url": "git+https://github.com/stacksjs/stacks.git",
@@ -45,7 +45,7 @@
45
45
  "dependencies": {
46
46
  "@iconify-json/f7": "^1.2.2",
47
47
  "@iconify-json/hugeicons": "^1.2.35",
48
- "@stacksjs/mobile": "0.75.336",
48
+ "@stacksjs/mobile": "0.75.339",
49
49
  "@stacksjs/sanitizer": "0.2.736",
50
50
  "@stacksjs/stx": "0.2.736",
51
51
  "@xterm/addon-fit": "^0.11.0",
@@ -221,7 +221,7 @@ onMount(() => {
221
221
  <span class="h-4 w-4 i-hugeicons-key-02"></span>
222
222
  Create Token
223
223
  </Button>
224
- <Button :if="!loading() && authenticationRequired()" tag="a" href="/login">
224
+ <Button :if="!loading() && authenticationRequired()" tag="a" href="/login?redirect=/access-tokens">
225
225
  <span class="h-4 w-4 i-hugeicons-login-03"></span>
226
226
  Sign in
227
227
  </Button>
@@ -271,7 +271,7 @@ onMount(() => {
271
271
  description="Personal access tokens belong to an authenticated account. Sign in to create, inspect, or revoke them."
272
272
  >
273
273
  <template #action>
274
- <Button tag="a" href="/login">
274
+ <Button tag="a" href="/login?redirect=/access-tokens">
275
275
  <span class="h-4 w-4 i-hugeicons-login-03"></span>
276
276
  Sign in
277
277
  </Button>
@@ -1,5 +1,6 @@
1
1
  <script client>
2
2
  import type { RemoteCommandResult, RemoteRegistry } from '../../../../functions/remote'
3
+ import { DashboardApiError } from '../../../../functions/dashboard-api'
3
4
  import { fetchRemoteRegistry, requestRemoteCommand } from '../../../../functions/remote'
4
5
  import { registerRemoteTerminal } from '../../../../functions/remote-terminal-element'
5
6
  import { pushToast } from '../../../../functions/toasts'
@@ -9,6 +10,9 @@ void registerRemoteTerminal()
9
10
  const registry = state<RemoteRegistry | null>(null)
10
11
  const isLoading = state(true)
11
12
  const loadError = state('')
13
+ // Remote access needs a signed-in account even where the rest of the dev
14
+ // dashboard does not, so a 401 here asks for a sign-in rather than failing.
15
+ const authenticationRequired = state(false)
12
16
  const selectedHost = state('')
13
17
  const selectedCommand = state('')
14
18
  const isRunning = state(false)
@@ -27,6 +31,7 @@ const commandsForHost = derived(() => {
27
31
  async function load(): Promise<void> {
28
32
  isLoading.set(true)
29
33
  loadError.set('')
34
+ authenticationRequired.set(false)
30
35
  try {
31
36
  const data = await fetchRemoteRegistry()
32
37
  registry.set(data)
@@ -34,7 +39,10 @@ async function load(): Promise<void> {
34
39
  selectedHost.set(data.hosts[0].key)
35
40
  }
36
41
  catch (error) {
37
- loadError.set(error instanceof Error ? error.message : 'The remote hosts could not be loaded.')
42
+ if (error instanceof DashboardApiError && error.status === 401)
43
+ authenticationRequired.set(true)
44
+ else
45
+ loadError.set(error instanceof Error ? error.message : 'The remote hosts could not be loaded.')
38
46
  }
39
47
  finally {
40
48
  isLoading.set(false)
@@ -87,6 +95,20 @@ onMount(() => {
87
95
 
88
96
  <div :if="isLoading()" class="text-gray-500 text-sm dark:text-neutral-400">Loading hosts</div>
89
97
 
98
+ <EmptyState
99
+ :if="!isLoading() && authenticationRequired()"
100
+ icon="i-hugeicons-user-lock-01"
101
+ title="Sign in to use remote access"
102
+ description="Running commands and opening terminals on your servers needs a signed-in account, in every environment. Each run and session is recorded under it."
103
+ >
104
+ <template #action>
105
+ <Button tag="a" href="/login?redirect=/remote">
106
+ <span class="h-4 w-4 i-hugeicons-login-03"></span>
107
+ Sign in
108
+ </Button>
109
+ </template>
110
+ </EmptyState>
111
+
90
112
  <section :if="!isLoading() && registry() && hosts().length === 0" class="p-6 bg-white dark:bg-neutral-800 border border-gray-200 rounded-lg dark:border-neutral-700">
91
113
  <h2 class="font-semibold text-gray-900 dark:text-white">No hosts are configured</h2>
92
114
  <p class="mt-1 max-w-2xl text-gray-500 text-sm dark:text-neutral-400">