handoff-app 2.0.0-10 → 2.0.0-12
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/README.md +421 -149
- package/UPGRADE.md +57 -9
- package/dist/ai/connections.d.ts +2 -0
- package/dist/ai/connections.js +14 -1
- package/dist/ai/types.d.ts +2 -3
- package/dist/app-builder/build.d.ts +3 -8
- package/dist/app-builder/build.js +97 -53
- package/dist/app-builder/client-config.js +2 -1
- package/dist/app-builder/paths.d.ts +0 -12
- package/dist/app-builder/paths.js +3 -20
- package/dist/app-builder/static-export.js +1 -1
- package/dist/app-builder/vercel-output.d.ts +1 -1
- package/dist/app-builder/vercel-output.js +5 -4
- package/dist/cache/build-cache.d.ts +27 -4
- package/dist/cache/build-cache.js +114 -47
- package/dist/cache/build-inputs.d.ts +13 -0
- package/dist/cache/build-inputs.js +19 -0
- package/dist/cache/index.d.ts +2 -1
- package/dist/cache/index.js +5 -1
- package/dist/commands/build/index.d.ts +1 -1
- package/dist/commands/build/index.js +1 -1
- package/dist/commands/utils.d.ts +3 -3
- package/dist/config/defaults.js +7 -4
- package/dist/config/from-env.d.ts +4 -0
- package/dist/config/from-env.js +38 -1
- package/dist/config/helpers.js +2 -2
- package/dist/config/index.d.ts +0 -1
- package/dist/config/index.js +1 -4
- package/dist/config/loader.js +27 -2
- package/dist/config/resolve-env.js +14 -11
- package/dist/config/runtime.js +1 -1
- package/dist/define.d.ts +10 -0
- package/dist/define.js +13 -0
- package/dist/index.d.ts +9 -6
- package/dist/index.js +17 -23
- package/dist/pipeline/figma.d.ts +13 -2
- package/dist/pipeline/figma.js +86 -46
- package/dist/pipeline/styles.js +1 -1
- package/dist/pipeline/validation.d.ts +1 -1
- package/dist/pipeline/validation.js +3 -3
- package/dist/registry/asset-storage/resolve.d.ts +7 -14
- package/dist/registry/asset-storage/resolve.js +31 -18
- package/dist/registry/asset-storage/types.d.ts +28 -8
- package/dist/registry/asset-storage/types.js +5 -5
- package/dist/registry/checkout/index.d.ts +2 -1
- package/dist/registry/checkout/index.js +57 -127
- package/dist/registry/client.js +53 -17
- package/dist/registry/db/driver.js +3 -3
- package/dist/registry/db/schema.d.ts +70 -2
- package/dist/registry/db/schema.js +5 -1
- package/dist/registry/email/adapters/resend.d.ts +7 -0
- package/dist/registry/email/adapters/resend.js +35 -0
- package/dist/registry/email/adapters/smtp.d.ts +10 -0
- package/dist/registry/email/adapters/smtp.js +73 -0
- package/dist/registry/email/define.d.ts +10 -0
- package/dist/registry/email/define.js +13 -0
- package/dist/registry/email/resolve.d.ts +29 -0
- package/dist/registry/email/resolve.js +76 -0
- package/dist/registry/email/types.d.ts +24 -0
- package/dist/registry/email/types.js +6 -0
- package/dist/registry/path.d.ts +2 -0
- package/dist/registry/path.js +11 -1
- package/dist/registry/publish/package.js +8 -0
- package/dist/registry/transfer.d.ts +4 -0
- package/dist/transformers/preview/component/builder.d.ts +1 -9
- package/dist/transformers/preview/component/builder.js +17 -17
- package/dist/transformers/preview/component/css.d.ts +2 -1
- package/dist/transformers/preview/component/css.js +8 -5
- package/dist/transformers/preview/component/html.d.ts +3 -1
- package/dist/transformers/preview/component/html.js +6 -2
- package/dist/transformers/preview/component/javascript.d.ts +2 -1
- package/dist/transformers/preview/component/javascript.js +10 -4
- package/dist/transformers/utils/build.d.ts +5 -0
- package/dist/transformers/utils/build.js +21 -4
- package/dist/types/config.d.ts +84 -18
- package/dist/utils/path.d.ts +1 -5
- package/dist/utils/path.js +6 -23
- package/docs/api.md +6 -4
- package/docs/cli.md +1 -1
- package/drizzle/0008_add_source_dir.sql +2 -0
- package/drizzle/meta/0008_snapshot.json +2025 -0
- package/drizzle/meta/_journal.json +7 -0
- package/package.json +15 -7
- package/src/ai/README.md +2 -2
- package/src/ai/connections.ts +18 -1
- package/src/ai/types.ts +2 -3
- package/src/app/components/AiAssistant/AiConversation.tsx +93 -30
- package/src/app/components/AiAssistant/toolLabel.ts +2 -0
- package/src/app/components/AiAssistant/usePageContext.ts +8 -4
- package/src/app/components/Auth/AccountLayout.tsx +6 -4
- package/src/app/components/Auth/AuthShell.tsx +17 -29
- package/src/app/components/Auth/RegistryInstallGate.tsx +4 -8
- package/src/app/components/Auth/installStatus.ts +29 -0
- package/src/app/components/Layout/Header.tsx +18 -14
- package/src/app/components/context/NavProvider.tsx +7 -3
- package/src/app/components/ui/confirm-dialog.tsx +87 -0
- package/src/app/components/ui/dialog.tsx +6 -1
- package/src/app/lib/ai/context.ts +5 -2
- package/src/app/lib/ai/model.ts +3 -9
- package/src/app/lib/ai/prompt.ts +2 -2
- package/src/app/lib/ai/tools.ts +1 -1
- package/src/app/lib/asset-storage.ts +23 -24
- package/src/app/lib/auth/email.ts +97 -23
- package/src/app/lib/docs-api/runtime-config.ts +14 -4
- package/src/app/lib/mcp/{shape.ts → components.ts} +186 -291
- package/src/app/lib/mcp/page-url.ts +24 -0
- package/src/app/lib/mcp/pages.ts +84 -0
- package/src/app/lib/mcp/result.ts +33 -0
- package/src/app/lib/mcp/server.ts +38 -331
- package/src/app/lib/mcp/tokens.ts +334 -0
- package/src/app/lib/mcp/tool.ts +19 -0
- package/src/app/lib/registry-api/asset-transfer.ts +112 -4
- package/src/app/lib/registry-api/errors.ts +2 -0
- package/src/app/lib/registry-api/handler.ts +7 -1
- package/src/app/lib/registry-api/transfer.ts +28 -6
- package/src/app/lib/server-module.ts +34 -0
- package/src/app/next.config.mjs +11 -37
- package/src/app/pages/account/ai.tsx +27 -6
- package/src/app/pages/account/index.tsx +9 -4
- package/src/app/pages/account/tokens.tsx +36 -6
- package/src/app/pages/account/users.tsx +74 -14
- package/src/app/pages/api/admin/users/[id]/[action].ts +80 -11
- package/src/app/pages/api/admin/users/index.ts +2 -2
- package/src/app/pages/api/auth/request-reset.ts +14 -16
- package/src/app/pages/api/install.ts +3 -3
- package/src/app/pages/api/registry/transfer/assets/blobs/[hash]/complete.ts +11 -0
- package/src/app/pages/api/registry/transfer/assets/blobs/[hash]/upload.ts +12 -0
- package/src/app/pages/cli/device.tsx +9 -4
- package/src/app/pages/install.tsx +7 -2
- package/src/app/pages/login.tsx +24 -24
- package/src/app/pages/reset-password.tsx +15 -19
- package/src/app-builder/README.md +2 -1
- package/src/app-builder/build.ts +110 -55
- package/src/app-builder/client-config.ts +2 -1
- package/src/app-builder/paths.ts +3 -22
- package/src/app-builder/static-export.ts +1 -1
- package/src/app-builder/vercel-output.ts +5 -4
- package/src/cache/README.md +4 -1
- package/src/cache/build-cache.ts +128 -43
- package/src/cache/build-inputs.ts +28 -0
- package/src/cache/index.ts +4 -0
- package/src/commands/build/index.ts +1 -1
- package/src/config/README.md +9 -8
- package/src/config/defaults.ts +7 -4
- package/src/config/from-env.ts +33 -0
- package/src/config/helpers.ts +1 -3
- package/src/config/index.ts +0 -3
- package/src/config/loader.ts +26 -3
- package/src/config/resolve-env.ts +14 -14
- package/src/config/runtime.ts +1 -1
- package/src/define.ts +18 -0
- package/src/index.ts +17 -21
- package/src/pipeline/README.md +1 -1
- package/src/pipeline/figma.ts +98 -49
- package/src/pipeline/styles.ts +1 -1
- package/src/pipeline/validation.ts +3 -3
- package/src/registry/asset-storage/resolve.ts +36 -31
- package/src/registry/asset-storage/types.ts +30 -8
- package/src/registry/checkout/index.ts +60 -138
- package/src/registry/client.ts +60 -20
- package/src/registry/db/driver.ts +2 -2
- package/src/registry/db/schema.ts +7 -3
- package/src/registry/email/adapters/resend.ts +22 -0
- package/src/registry/email/adapters/smtp.ts +39 -0
- package/src/registry/email/define.ts +15 -0
- package/src/registry/email/resolve.ts +94 -0
- package/src/registry/email/types.ts +28 -0
- package/src/registry/path.ts +11 -0
- package/src/registry/publish/package.ts +9 -0
- package/src/registry/transfer.ts +4 -0
- package/src/transformers/preview/component/builder.ts +19 -25
- package/src/transformers/preview/component/css.ts +9 -4
- package/src/transformers/preview/component/html.ts +6 -2
- package/src/transformers/preview/component/javascript.ts +14 -4
- package/src/transformers/utils/build.ts +16 -3
- package/src/types/config.ts +86 -21
- package/src/utils/README.md +1 -1
- package/src/utils/path.ts +6 -29
- package/templates/default/handoff.config.js.tpl +10 -2
- package/templates/default/handoff.config.ts.tpl +10 -2
- package/templates/default/handoff.config.with-examples.js.tpl +10 -2
- package/templates/default/handoff.config.with-examples.ts.tpl +10 -2
- package/templates/default/package.json.tpl +2 -2
- package/dist/config/validator.d.ts +0 -9
- package/dist/config/validator.js +0 -24
- package/dist/registry/asset-storage/adapters/vercel-blob.d.ts +0 -17
- package/dist/registry/asset-storage/adapters/vercel-blob.js +0 -87
- package/src/config/validator.ts +0 -22
- package/src/registry/asset-storage/adapters/vercel-blob.ts +0 -51
package/README.md
CHANGED
|
@@ -24,8 +24,8 @@ registry-mode application is created automatically by a registry build.
|
|
|
24
24
|
|
|
25
25
|
## Requirements
|
|
26
26
|
|
|
27
|
-
- Node.js
|
|
28
|
-
- npm
|
|
27
|
+
- Node.js 22 or newer; Node.js 24 LTS is recommended
|
|
28
|
+
- npm 10 or newer
|
|
29
29
|
- A paid Figma account when fetching a Figma library
|
|
30
30
|
- PostgreSQL when running a registry
|
|
31
31
|
|
|
@@ -53,6 +53,11 @@ npm run db:migrate # registry database migrations
|
|
|
53
53
|
npm run validate
|
|
54
54
|
```
|
|
55
55
|
|
|
56
|
+
## Upgrade from version 1.x.x to version 2.x.x
|
|
57
|
+
|
|
58
|
+
See the [version 1.x.x to version 2.x.x migration guide](UPGRADE.md#version-1xx-to-version-2xx)
|
|
59
|
+
for breaking changes, migration steps, and upgrade verification.
|
|
60
|
+
|
|
56
61
|
## Project layout
|
|
57
62
|
|
|
58
63
|
Every command runs against a working directory: the directory holding
|
|
@@ -69,13 +74,13 @@ Every command runs against a working directory: the directory holding
|
|
|
69
74
|
├─ public/api/ # generated docs API; gitignored
|
|
70
75
|
├─ out/ # build output; gitignored
|
|
71
76
|
├─ .vercel/ # `--package vercel` output; gitignored
|
|
72
|
-
└─ .handoff/ #
|
|
77
|
+
└─ .handoff/ # local Handoff state; gitignored
|
|
73
78
|
```
|
|
74
79
|
|
|
75
80
|
In a repository of its own, which is what `init` scaffolds, the working
|
|
76
81
|
directory is the repository root.
|
|
77
82
|
|
|
78
|
-
|
|
83
|
+
## Adding Handoff to an existing application
|
|
79
84
|
|
|
80
85
|
Declarations sit beside the components they document, so the catalog points
|
|
81
86
|
into the application source:
|
|
@@ -313,8 +318,6 @@ both.
|
|
|
313
318
|
|
|
314
319
|
If two declarations claim one id, Handoff keeps the first and skips the second. The warning names
|
|
315
320
|
both files. `publish` refuses to run until each id is unique.
|
|
316
|
-
|
|
317
|
-
|
|
318
321
|
### Registration
|
|
319
322
|
|
|
320
323
|
Catalog directories are registered in one place. A path is either an item directory or a collection
|
|
@@ -336,10 +339,7 @@ export default defineConfig({
|
|
|
336
339
|
});
|
|
337
340
|
```
|
|
338
341
|
|
|
339
|
-
###
|
|
340
|
-
|
|
341
|
-
See the [version 1.x.x to version 2.x.x migration guide](UPGRADE.md#version-1xx-to-version-2xx)
|
|
342
|
-
for breaking changes, migration steps, and upgrade verification.
|
|
342
|
+
### Pages
|
|
343
343
|
|
|
344
344
|
Custom documentation pages are Markdown files under `pages/`. Their relative
|
|
345
345
|
paths become their routes and registry IDs.
|
|
@@ -358,6 +358,23 @@ HANDOFF_FIGMA_PROJECT_ID=figma-file-id
|
|
|
358
358
|
HANDOFF_DEV_ACCESS_TOKEN=figma-personal-access-token
|
|
359
359
|
```
|
|
360
360
|
|
|
361
|
+
These are the defaults of `integrations.figma` in `handoff.config`. Set the block
|
|
362
|
+
only to use other variable names, to commit the file ID, or to change the file
|
|
363
|
+
for a profile. The access token accepts only an environment reference:
|
|
364
|
+
|
|
365
|
+
```ts
|
|
366
|
+
integrations: {
|
|
367
|
+
figma: {
|
|
368
|
+
projectId: fromEnv("HANDOFF_FIGMA_PROJECT_ID", { default: null }),
|
|
369
|
+
accessToken: fromEnv("HANDOFF_DEV_ACCESS_TOKEN", { default: null }),
|
|
370
|
+
},
|
|
371
|
+
},
|
|
372
|
+
```
|
|
373
|
+
|
|
374
|
+
If a value is empty, `fetch` asks for it in a terminal and can save it to `.env`,
|
|
375
|
+
or to `.env.<profile>` when a profile is selected. Without a terminal, such as
|
|
376
|
+
in CI, `fetch` stops with an error that names the missing variable.
|
|
377
|
+
|
|
361
378
|
The `file_content:read` and `library_content:read` scopes must be granted to the
|
|
362
379
|
personal access token. The configured library is fetched with:
|
|
363
380
|
|
|
@@ -369,9 +386,9 @@ After design changes, the Figma library should be republished and the command
|
|
|
369
386
|
should be run again so the latest foundations are pulled.
|
|
370
387
|
|
|
371
388
|
Generated tokens, CSS, Sass, and available assets are written below
|
|
372
|
-
`exported
|
|
389
|
+
`exported/`. The `exported/` directory should be committed so the
|
|
373
390
|
same inputs are used by local and CI builds. A successful fetch can be verified
|
|
374
|
-
by checking for generated token data such as `exported
|
|
391
|
+
by checking for generated token data such as `exported/tokens.json`;
|
|
375
392
|
an empty foundation route does not prove that the fetch succeeded.
|
|
376
393
|
|
|
377
394
|
## Workspace
|
|
@@ -404,9 +421,20 @@ A static documentation site is built with:
|
|
|
404
421
|
npm run build
|
|
405
422
|
```
|
|
406
423
|
|
|
407
|
-
Static output is written below `out
|
|
424
|
+
Static output is written below `out/static` and can be served by any
|
|
408
425
|
static file server or CDN.
|
|
409
426
|
|
|
427
|
+
Components whose files have not changed since the last build are not built
|
|
428
|
+
again. The build cache in `.handoff/.cache/` records the files of each
|
|
429
|
+
component and every file that its last build read, Sass partials included.
|
|
430
|
+
Files under `node_modules` are covered by the lockfile instead. Assets that a
|
|
431
|
+
stylesheet loads through `url()` are not tracked. Every component is built
|
|
432
|
+
again with:
|
|
433
|
+
|
|
434
|
+
```bash
|
|
435
|
+
npm run build -- --force
|
|
436
|
+
```
|
|
437
|
+
|
|
410
438
|
A standalone registry application is built with:
|
|
411
439
|
|
|
412
440
|
```bash
|
|
@@ -418,6 +446,83 @@ command. The database-backed Next.js bundle is written to `out/registry`, and
|
|
|
418
446
|
workspace source is not compiled or served. A Vercel Build Output bundle can be
|
|
419
447
|
produced for either target by adding `--package vercel`.
|
|
420
448
|
|
|
449
|
+
## Configuration
|
|
450
|
+
|
|
451
|
+
Configuration is read from `handoff.config.ts`, `.js`, `.cjs`, or `.json`, in
|
|
452
|
+
that order. `defineConfig` provides typed authoring. Values merge onto the
|
|
453
|
+
defaults: plain objects merge recursively, and arrays, scalars, `null`, and
|
|
454
|
+
functions replace.
|
|
455
|
+
|
|
456
|
+
Useful environment variables:
|
|
457
|
+
|
|
458
|
+
| Variable | Purpose |
|
|
459
|
+
| --- | --- |
|
|
460
|
+
| `HANDOFF_PROFILE` | Config profile merged onto the base config |
|
|
461
|
+
| `HANDOFF_FIGMA_PROJECT_ID` | Figma file ID used by `fetch` |
|
|
462
|
+
| `HANDOFF_DEV_ACCESS_TOKEN` | Figma personal access token used by `fetch` |
|
|
463
|
+
| `HANDOFF_REGISTRY_URL` | Connected workspace registry URL |
|
|
464
|
+
| `HANDOFF_REGISTRY_ACCESS_TOKEN` | Registry access token used by a connected workspace |
|
|
465
|
+
| `HANDOFF_SYNC_SECRET` | Optional deployment-wide registry credential |
|
|
466
|
+
| `DATABASE_URL` | Registry PostgreSQL connection string |
|
|
467
|
+
| `AUTH_SECRET` | Registry session-signing secret, at least 32 characters |
|
|
468
|
+
| `AUTH_URL` | Canonical public registry URL |
|
|
469
|
+
| `PORT` | Standalone registry server port |
|
|
470
|
+
| `HOSTNAME` | Standalone registry bind hostname |
|
|
471
|
+
| `HANDOFF_AI_KEY_SECRET` | Encrypts reader-supplied AI provider keys, at least 32 characters |
|
|
472
|
+
| `HANDOFF_AI_CONNECTIONS` | JSON array of AI connections, merged over the baked list by `id` |
|
|
473
|
+
| `HANDOFF_OUTPUT_DIR` | Override the fetched output directory |
|
|
474
|
+
| `HANDOFF_CREATE_ASSETS_ZIP_FILES` | Set to `false` to skip the icon and logo zip files in `fetch` |
|
|
475
|
+
| `HANDOFF_SITES_DIR` | Override the build output directory |
|
|
476
|
+
| `HANDOFF_WORKING_PATH` | Directory holding `handoff.config.ts`; defaults to the current directory |
|
|
477
|
+
| `HANDOFF_APP_PORT` | Workspace documentation server port |
|
|
478
|
+
| `HANDOFF_WEBSOCKET_PORT` | Workspace live-reload server port |
|
|
479
|
+
|
|
480
|
+
## Profiles
|
|
481
|
+
|
|
482
|
+
A profile is a sidecar config file that merges onto the base config. It holds
|
|
483
|
+
only what changes between environments, so a project keeps one shared config
|
|
484
|
+
instead of a second complete copy of it.
|
|
485
|
+
|
|
486
|
+
- The base config is `handoff.config.ts`, `.js`, `.cjs`, or `.json`. It is the
|
|
487
|
+
only config file loaded when no profile is selected.
|
|
488
|
+
- A profile file is `handoff.config.<profile>.*`, with the same four
|
|
489
|
+
extensions.
|
|
490
|
+
- Select a profile with `--profile <name>` or with `HANDOFF_PROFILE`.
|
|
491
|
+
`--profile` takes precedence over `HANDOFF_PROFILE`.
|
|
492
|
+
- Profile names are not predefined. A name can contain lowercase letters,
|
|
493
|
+
numbers, and hyphens.
|
|
494
|
+
- A selected profile must exist. If no `handoff.config.<name>.*` file is found,
|
|
495
|
+
the command stops with an error.
|
|
496
|
+
- `login`, `logout`, `publish`, and `checkout` also accept a profile that has
|
|
497
|
+
only a saved registry login, because there the profile selects the registry.
|
|
498
|
+
Every other command still needs the config file.
|
|
499
|
+
- Layers resolve as defaults, base config, profile, then programmatic config,
|
|
500
|
+
with the merge rules described under [Configuration](#configuration).
|
|
501
|
+
- A selected profile also reads `.env.<profile>` on top of `.env`, when that
|
|
502
|
+
file exists. Both are read from the directory the command runs in, and a
|
|
503
|
+
variable already set in the environment beats both files.
|
|
504
|
+
|
|
505
|
+
```bash
|
|
506
|
+
npx handoff-app build --target registry --profile registry
|
|
507
|
+
```
|
|
508
|
+
|
|
509
|
+
`build --target registry` always packages a registry-mode application, so a
|
|
510
|
+
profile does not need to set `runtime.mode`. This makes a profile a good place
|
|
511
|
+
for the registry build settings: the database driver, the database
|
|
512
|
+
environment-variable name, the asset storage, and whether MCP is enabled. A
|
|
513
|
+
hosting provider or a CI job selects the profile through `HANDOFF_PROFILE`.
|
|
514
|
+
|
|
515
|
+
`defineConfig` types a profile as well as a base config. The generated
|
|
516
|
+
`.gitignore` lists `handoff.config.local.*`, which makes `local` the usual name
|
|
517
|
+
for a profile that stays on one machine.
|
|
518
|
+
|
|
519
|
+
Arrays replace, so a profile that declares `catalog.include` also decides what
|
|
520
|
+
`make` and `checkout` register.
|
|
521
|
+
|
|
522
|
+
`.env.<profile>` is read once at startup rather than watched.
|
|
523
|
+
`HANDOFF_WORKING_PATH` cannot be set from it, because the working path is
|
|
524
|
+
resolved before any profile is known.
|
|
525
|
+
|
|
421
526
|
## Registry setup
|
|
422
527
|
|
|
423
528
|
### 1. Database migrations
|
|
@@ -450,57 +555,10 @@ server also reads `PORT` and `HOSTNAME`. The generated entrypoint is
|
|
|
450
555
|
`out/registry/server.js`; how it is started and hosted depends on the deployment
|
|
451
556
|
environment.
|
|
452
557
|
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
#### Vercel packaging
|
|
459
|
-
|
|
460
|
-
A registry bundle that follows the Vercel Build Output API is produced with:
|
|
461
|
-
|
|
462
|
-
```bash
|
|
463
|
-
npm run build -- --target registry --package vercel
|
|
464
|
-
```
|
|
465
|
-
|
|
466
|
-
The generated deployment output is written to `.vercel/output`. The registry
|
|
467
|
-
runtime variables listed above are supplied through the deployment environment.
|
|
468
|
-
|
|
469
|
-
#### Neon PostgreSQL
|
|
470
|
-
|
|
471
|
-
Import `fromEnv` from `handoff-app`. Select the Neon connection driver in `handoff.config.ts`:
|
|
472
|
-
|
|
473
|
-
```ts
|
|
474
|
-
runtime: {
|
|
475
|
-
registry: {
|
|
476
|
-
databaseUrl: fromEnv('DATABASE_URL'),
|
|
477
|
-
database: {
|
|
478
|
-
driver: 'neon',
|
|
479
|
-
},
|
|
480
|
-
},
|
|
481
|
-
},
|
|
482
|
-
```
|
|
483
|
-
|
|
484
|
-
A Neon PostgreSQL connection string is supplied through `DATABASE_URL`. The
|
|
485
|
-
same explicit database migration step is required before deployment.
|
|
486
|
-
|
|
487
|
-
#### Optional Vercel Blob storage
|
|
488
|
-
|
|
489
|
-
Registry assets are stored in PostgreSQL by default. Vercel Blob can be selected
|
|
490
|
-
when external asset storage is required:
|
|
491
|
-
|
|
492
|
-
```ts
|
|
493
|
-
runtime: {
|
|
494
|
-
registry: {
|
|
495
|
-
assetStorage: {
|
|
496
|
-
adapter: 'vercel-blob',
|
|
497
|
-
},
|
|
498
|
-
},
|
|
499
|
-
},
|
|
500
|
-
```
|
|
501
|
-
|
|
502
|
-
The associated `BLOB_READ_WRITE_TOKEN` is supplied through the deployment
|
|
503
|
-
environment.
|
|
558
|
+
The defaults work without more configuration: asset blobs are stored in
|
|
559
|
+
PostgreSQL, and email is not sent. Other providers are baked into the build, so
|
|
560
|
+
they must be selected under [Registry providers](#registry-providers) before
|
|
561
|
+
the registry is built.
|
|
504
562
|
|
|
505
563
|
### 3. Installation
|
|
506
564
|
|
|
@@ -632,6 +690,298 @@ runtime: {
|
|
|
632
690
|
},
|
|
633
691
|
```
|
|
634
692
|
|
|
693
|
+
## Registry providers
|
|
694
|
+
|
|
695
|
+
### Vercel and Neon
|
|
696
|
+
|
|
697
|
+
Vercel packaging and the Neon PostgreSQL driver are supported independently.
|
|
698
|
+
Either integration can be used without the other.
|
|
699
|
+
|
|
700
|
+
#### Vercel packaging
|
|
701
|
+
|
|
702
|
+
A registry bundle that follows the Vercel Build Output API is produced with:
|
|
703
|
+
|
|
704
|
+
```bash
|
|
705
|
+
npm run build -- --target registry --package vercel
|
|
706
|
+
```
|
|
707
|
+
|
|
708
|
+
The generated deployment output is written to `.vercel/output`. The registry
|
|
709
|
+
runtime variables listed above are supplied through the deployment environment.
|
|
710
|
+
|
|
711
|
+
#### Neon PostgreSQL
|
|
712
|
+
|
|
713
|
+
Import `fromEnv` from `handoff-app`. Select the Neon connection driver in `handoff.config.ts`:
|
|
714
|
+
|
|
715
|
+
```ts
|
|
716
|
+
runtime: {
|
|
717
|
+
registry: {
|
|
718
|
+
database: {
|
|
719
|
+
url: fromEnv('DATABASE_URL'),
|
|
720
|
+
driver: 'neon',
|
|
721
|
+
},
|
|
722
|
+
},
|
|
723
|
+
},
|
|
724
|
+
```
|
|
725
|
+
|
|
726
|
+
A Neon PostgreSQL connection string is supplied through `DATABASE_URL`. The
|
|
727
|
+
same explicit database migration step is required before deployment.
|
|
728
|
+
|
|
729
|
+
### Asset storage
|
|
730
|
+
|
|
731
|
+
The registry stores asset blobs in PostgreSQL by default, with a limit of 4 MB
|
|
732
|
+
for each blob. `maxInlineBytes` changes this limit.
|
|
733
|
+
|
|
734
|
+
#### Custom adapter
|
|
735
|
+
|
|
736
|
+
A [custom server module](#custom-server-modules) can store blobs in a
|
|
737
|
+
different service. The module default-exports a `defineAssetStorage()` adapter
|
|
738
|
+
with these methods:
|
|
739
|
+
|
|
740
|
+
- `put` stores a blob and returns the `storageRef` that the registry records.
|
|
741
|
+
- `get` returns a blob by its `storageRef`, as `{ kind: 'stream', stream }`,
|
|
742
|
+
`{ kind: 'bytes', bytes }`, or `{ kind: 'redirect', url }`.
|
|
743
|
+
- `delete` removes a blob by its `storageRef`.
|
|
744
|
+
- `createUpload` is optional. It enables [direct uploads](#direct-uploads).
|
|
745
|
+
|
|
746
|
+
Literals in `options` are baked into the build. The deployed registry reads
|
|
747
|
+
each `fromEnv()` value at request time and gives the resolved `options` to the
|
|
748
|
+
module. Thus secrets must use `fromEnv()`.
|
|
749
|
+
|
|
750
|
+
The S3 and Vercel Blob examples that follow are complete adapters with direct
|
|
751
|
+
uploads.
|
|
752
|
+
|
|
753
|
+
##### Direct uploads
|
|
754
|
+
|
|
755
|
+
By default, `publish` sends each blob to the registry, and the registry sends
|
|
756
|
+
it to storage. A serverless host limits the size of one request. Vercel permits
|
|
757
|
+
about 4.5 MB, so a larger blob fails.
|
|
758
|
+
|
|
759
|
+
With `createUpload`, the registry gives the CLI a signed URL, and the CLI sends
|
|
760
|
+
the blob directly to storage. The signed URL must make storage reject bytes
|
|
761
|
+
that do not match `hash`. The registry records the blob only after `get` finds
|
|
762
|
+
the stored object. The registry does not delete an uploaded object that it
|
|
763
|
+
never records.
|
|
764
|
+
|
|
765
|
+
The `database` provider always uploads through the registry, with its
|
|
766
|
+
`maxInlineBytes` limit.
|
|
767
|
+
|
|
768
|
+
##### S3-compatible storage
|
|
769
|
+
|
|
770
|
+
This adapter works with Amazon S3, Cloudflare R2, MinIO, SeaweedFS, and other
|
|
771
|
+
services with an S3 API. It uses `@aws-sdk/client-s3` and
|
|
772
|
+
`@aws-sdk/s3-request-presigner`.
|
|
773
|
+
|
|
774
|
+
```ts
|
|
775
|
+
runtime: {
|
|
776
|
+
registry: {
|
|
777
|
+
assetStorage: {
|
|
778
|
+
adapter: 'custom',
|
|
779
|
+
module: './server/storage/s3.mjs',
|
|
780
|
+
options: {
|
|
781
|
+
endpoint: 'http://127.0.0.1:8333',
|
|
782
|
+
bucket: 'handoff-assets',
|
|
783
|
+
accessKey: fromEnv('S3_ACCESS_KEY'),
|
|
784
|
+
secretKey: fromEnv('S3_SECRET_KEY'),
|
|
785
|
+
},
|
|
786
|
+
},
|
|
787
|
+
},
|
|
788
|
+
},
|
|
789
|
+
```
|
|
790
|
+
|
|
791
|
+
```js
|
|
792
|
+
// server/storage/s3.mjs
|
|
793
|
+
import { DeleteObjectCommand, GetObjectCommand, PutObjectCommand, S3Client } from '@aws-sdk/client-s3';
|
|
794
|
+
import { getSignedUrl } from '@aws-sdk/s3-request-presigner';
|
|
795
|
+
import { defineAssetStorage } from 'handoff-app/define';
|
|
796
|
+
|
|
797
|
+
export default defineAssetStorage(({ options }) => {
|
|
798
|
+
const client = new S3Client({
|
|
799
|
+
endpoint: options.endpoint,
|
|
800
|
+
region: 'us-east-1',
|
|
801
|
+
forcePathStyle: true,
|
|
802
|
+
credentials: { accessKeyId: options.accessKey, secretAccessKey: options.secretKey },
|
|
803
|
+
});
|
|
804
|
+
const Bucket = options.bucket;
|
|
805
|
+
return {
|
|
806
|
+
async put({ hash, bytes, contentType }) {
|
|
807
|
+
await client.send(new PutObjectCommand({ Bucket, Key: hash, Body: bytes, ContentType: contentType }));
|
|
808
|
+
return { storageRef: hash };
|
|
809
|
+
},
|
|
810
|
+
async get(storageRef) {
|
|
811
|
+
const result = await client.send(new GetObjectCommand({ Bucket, Key: storageRef }));
|
|
812
|
+
return { kind: 'stream', stream: result.Body, contentType: result.ContentType };
|
|
813
|
+
},
|
|
814
|
+
async delete(storageRef) {
|
|
815
|
+
await client.send(new DeleteObjectCommand({ Bucket, Key: storageRef }));
|
|
816
|
+
},
|
|
817
|
+
async createUpload({ hash, contentType }) {
|
|
818
|
+
const checksum = Buffer.from(hash, 'hex').toString('base64');
|
|
819
|
+
const command = new PutObjectCommand({ Bucket, Key: hash, ContentType: contentType, ChecksumSHA256: checksum });
|
|
820
|
+
const url = await getSignedUrl(client, command, {
|
|
821
|
+
expiresIn: 900,
|
|
822
|
+
unhoistableHeaders: new Set(['x-amz-checksum-sha256']),
|
|
823
|
+
});
|
|
824
|
+
return { url, headers: { 'Content-Type': contentType, 'x-amz-checksum-sha256': checksum }, storageRef: hash };
|
|
825
|
+
},
|
|
826
|
+
};
|
|
827
|
+
});
|
|
828
|
+
```
|
|
829
|
+
|
|
830
|
+
`createUpload` signs a SHA-256 checksum header. Thus storage rejects bytes
|
|
831
|
+
that do not match `hash`.
|
|
832
|
+
|
|
833
|
+
##### Vercel Blob
|
|
834
|
+
|
|
835
|
+
This adapter stores blobs in a private Vercel Blob store. It uses
|
|
836
|
+
`@vercel/blob` 2.x. When the store is connected to the Vercel project, the SDK
|
|
837
|
+
reads the store credentials from the deployment environment. Thus the adapter
|
|
838
|
+
needs no `options`.
|
|
839
|
+
|
|
840
|
+
```ts
|
|
841
|
+
runtime: {
|
|
842
|
+
registry: {
|
|
843
|
+
assetStorage: {
|
|
844
|
+
adapter: 'custom',
|
|
845
|
+
module: './server/storage/vercel-blob.mjs',
|
|
846
|
+
},
|
|
847
|
+
},
|
|
848
|
+
},
|
|
849
|
+
```
|
|
850
|
+
|
|
851
|
+
```js
|
|
852
|
+
// server/storage/vercel-blob.mjs
|
|
853
|
+
import { Readable } from 'node:stream';
|
|
854
|
+
import { del, get, issueSignedToken, parseStoreIdFromDelegationToken, presignUrl, put } from '@vercel/blob';
|
|
855
|
+
import { defineAssetStorage } from 'handoff-app/define';
|
|
856
|
+
|
|
857
|
+
const access = 'private';
|
|
858
|
+
const pathnameFor = (hash) => `assets/${hash}`;
|
|
859
|
+
|
|
860
|
+
export default defineAssetStorage({
|
|
861
|
+
async put({ hash, bytes, contentType }) {
|
|
862
|
+
const blob = await put(pathnameFor(hash), bytes, { access, contentType, addRandomSuffix: false, allowOverwrite: true });
|
|
863
|
+
return { storageRef: blob.pathname };
|
|
864
|
+
},
|
|
865
|
+
async get(storageRef) {
|
|
866
|
+
const result = await get(storageRef, { access });
|
|
867
|
+
if (!result?.stream) {
|
|
868
|
+
throw new Error(`Blob "${storageRef}" does not exist.`);
|
|
869
|
+
}
|
|
870
|
+
return { kind: 'stream', stream: Readable.fromWeb(result.stream), contentType: result.blob.contentType };
|
|
871
|
+
},
|
|
872
|
+
async delete(storageRef) {
|
|
873
|
+
await del(storageRef);
|
|
874
|
+
},
|
|
875
|
+
async createUpload({ hash, size, contentType }) {
|
|
876
|
+
const pathname = pathnameFor(hash);
|
|
877
|
+
const token = await issueSignedToken({
|
|
878
|
+
pathname,
|
|
879
|
+
operations: ['put'],
|
|
880
|
+
validUntil: Date.now() + 15 * 60 * 1000,
|
|
881
|
+
allowedContentTypes: [contentType],
|
|
882
|
+
maximumSizeInBytes: size,
|
|
883
|
+
});
|
|
884
|
+
const { presignedUrl } = await presignUrl(token, { operation: 'put', pathname, access, allowOverwrite: true });
|
|
885
|
+
return {
|
|
886
|
+
url: presignedUrl,
|
|
887
|
+
headers: {
|
|
888
|
+
'x-api-version': '12',
|
|
889
|
+
'x-vercel-blob-store-id': parseStoreIdFromDelegationToken(token.delegationToken),
|
|
890
|
+
'x-vercel-blob-access': access,
|
|
891
|
+
'x-content-type': contentType,
|
|
892
|
+
},
|
|
893
|
+
storageRef: pathname,
|
|
894
|
+
};
|
|
895
|
+
},
|
|
896
|
+
});
|
|
897
|
+
```
|
|
898
|
+
|
|
899
|
+
This adapter is different from the S3 example in these ways:
|
|
900
|
+
|
|
901
|
+
- `get` converts the stream. The SDK returns a web stream, but the registry
|
|
902
|
+
needs a Node stream.
|
|
903
|
+
- A private blob has no public URL. Thus the registry sends each blob to the
|
|
904
|
+
reader itself.
|
|
905
|
+
- **Vercel Blob cannot reject bytes that do not match `hash`.** The signed URL
|
|
906
|
+
limits only the path, the size, and the content type. Thus the registry does
|
|
907
|
+
not verify a blob that the CLI uploads directly.
|
|
908
|
+
- Without `createUpload`, each blob goes through the registry, which verifies
|
|
909
|
+
its hash. The Vercel limit of about 4.5 MB then applies to each blob.
|
|
910
|
+
- The CLI sends a plain `PUT` request. Thus `createUpload` returns the headers
|
|
911
|
+
that the SDK sends for a presigned upload. These headers are not a documented
|
|
912
|
+
API, so a new version of `@vercel/blob` can change them.
|
|
913
|
+
|
|
914
|
+
#### Changing the provider
|
|
915
|
+
|
|
916
|
+
The registry records on each blob whether PostgreSQL or the custom adapter
|
|
917
|
+
stores it. Blobs in PostgreSQL stay readable after you select a custom adapter.
|
|
918
|
+
A publish does not move blobs. Thus, if you change the custom adapter or its
|
|
919
|
+
storage, copy the stored objects first. The new adapter must find each object
|
|
920
|
+
by the `storageRef` that the previous adapter returned. After a change back to
|
|
921
|
+
`database`, the registry cannot read the blobs of the custom adapter.
|
|
922
|
+
|
|
923
|
+
### Email delivery
|
|
924
|
+
|
|
925
|
+
The registry sends invitation and password-reset emails when `email.from` is
|
|
926
|
+
set. Without it, an administrator sees each invitation link once and delivers
|
|
927
|
+
it manually, and password reset is not available.
|
|
928
|
+
|
|
929
|
+
Resend is the default provider. It reads its key from `RESEND_API_KEY`, unless
|
|
930
|
+
`options.apiKey` names a different variable:
|
|
931
|
+
|
|
932
|
+
```ts
|
|
933
|
+
runtime: {
|
|
934
|
+
registry: {
|
|
935
|
+
email: { from: 'Handoff <no-reply@example.com>' },
|
|
936
|
+
},
|
|
937
|
+
},
|
|
938
|
+
```
|
|
939
|
+
|
|
940
|
+
For an SMTP server, for example Amazon SES, SendGrid, or Microsoft 365:
|
|
941
|
+
|
|
942
|
+
```ts
|
|
943
|
+
email: {
|
|
944
|
+
from: 'Handoff <no-reply@example.com>',
|
|
945
|
+
provider: 'smtp',
|
|
946
|
+
options: {
|
|
947
|
+
host: fromEnv('SMTP_HOST'),
|
|
948
|
+
port: 465, // default, uses TLS. 587 uses STARTTLS.
|
|
949
|
+
user: fromEnv('SMTP_USER'),
|
|
950
|
+
password: fromEnv('SMTP_PASSWORD'),
|
|
951
|
+
},
|
|
952
|
+
},
|
|
953
|
+
```
|
|
954
|
+
|
|
955
|
+
For a different service, set `provider: 'custom'` and set `module` to a
|
|
956
|
+
[custom server module](#custom-server-modules). The factory gets `options`
|
|
957
|
+
with `fromEnv()` values resolved:
|
|
958
|
+
|
|
959
|
+
```ts
|
|
960
|
+
import { defineEmailProvider } from 'handoff-app/define';
|
|
961
|
+
|
|
962
|
+
export default defineEmailProvider(({ options }) => ({
|
|
963
|
+
async send({ from, to, subject, html, text }) {
|
|
964
|
+
// Deliver the message. Throw an error if it is not accepted.
|
|
965
|
+
},
|
|
966
|
+
}));
|
|
967
|
+
```
|
|
968
|
+
|
|
969
|
+
Literals in `options` are baked into the build. `fromEnv()` values are read at
|
|
970
|
+
request time, so they can change without a rebuild. `apiKey` and `password`
|
|
971
|
+
must use `fromEnv()`. A profile can set a different sender or provider. When a
|
|
972
|
+
profile changes the provider, it replaces `options`.
|
|
973
|
+
|
|
974
|
+
### Custom server modules
|
|
975
|
+
|
|
976
|
+
Asset storage, email, and AI connections can name a custom server module. Each
|
|
977
|
+
module obeys these rules:
|
|
978
|
+
|
|
979
|
+
- The path is relative to the working directory.
|
|
980
|
+
- The file is `.js` or `.mjs`.
|
|
981
|
+
- The default export is the object, or a factory that returns it.
|
|
982
|
+
- The module imports its define helper from `handoff-app/define`. The registry build stops when a module imports `handoff-app`.
|
|
983
|
+
- The packages that the module imports are installed in the project. The registry build copies them into the bundle.
|
|
984
|
+
|
|
635
985
|
## MCP
|
|
636
986
|
|
|
637
987
|
The documentation app serves a Model Context Protocol endpoint at `/api/mcp/`, so
|
|
@@ -727,8 +1077,10 @@ Every connection speaks the OpenAI-compatible `/chat/completions` API. This does
|
|
|
727
1077
|
not limit which models a reader can use. Ollama serves an OpenAI-compatible API
|
|
728
1078
|
at `/v1`, and LiteLLM, OpenRouter, vLLM, LM Studio and Azure OpenAI each front
|
|
729
1079
|
Anthropic, Google and xAI models. A provider that fits nothing else names a
|
|
730
|
-
server
|
|
731
|
-
`
|
|
1080
|
+
[custom server module](#custom-server-modules) that default-exports
|
|
1081
|
+
`defineAiProvider()`, in place of `baseUrl`. The factory gets `options` with
|
|
1082
|
+
`fromEnv()` values resolved. Literals in `options` are baked into the build, so
|
|
1083
|
+
secrets must use `fromEnv()`.
|
|
732
1084
|
|
|
733
1085
|
### Reader keys
|
|
734
1086
|
|
|
@@ -753,93 +1105,13 @@ instead of holding its value:
|
|
|
753
1105
|
"apiKeyEnv": "LITELLM_API_KEY", "models": ["gpt-4o"] }]
|
|
754
1106
|
```
|
|
755
1107
|
|
|
1108
|
+
A connection in this list can use a `module` only if a connection in the config
|
|
1109
|
+
names the same module. The build copies only those modules into the registry.
|
|
1110
|
+
|
|
756
1111
|
Spend control belongs to the gateway. LiteLLM and OpenRouter both enforce
|
|
757
1112
|
budgets and rate limits per key. The assistant caps only how many steps one
|
|
758
1113
|
question can take.
|
|
759
1114
|
|
|
760
|
-
## Configuration
|
|
761
|
-
|
|
762
|
-
Configuration is read from `handoff.config.ts`, `.js`, `.cjs`, or `.json`, in
|
|
763
|
-
that order. `defineConfig` provides typed authoring. Values merge onto the
|
|
764
|
-
defaults: plain objects merge recursively, and arrays, scalars, `null`, and
|
|
765
|
-
functions replace.
|
|
766
|
-
|
|
767
|
-
Useful environment variables:
|
|
768
|
-
|
|
769
|
-
| Variable | Purpose |
|
|
770
|
-
| --- | --- |
|
|
771
|
-
| `HANDOFF_PROFILE` | Config profile merged onto the base config |
|
|
772
|
-
| `HANDOFF_FIGMA_PROJECT_ID` | Figma file ID used by `fetch` |
|
|
773
|
-
| `HANDOFF_DEV_ACCESS_TOKEN` | Figma personal access token used by `fetch` |
|
|
774
|
-
| `HANDOFF_REGISTRY_URL` | Connected workspace registry URL |
|
|
775
|
-
| `HANDOFF_REGISTRY_ACCESS_TOKEN` | Registry access token used by a connected workspace |
|
|
776
|
-
| `HANDOFF_SYNC_SECRET` | Optional deployment-wide registry credential |
|
|
777
|
-
| `DATABASE_URL` | Registry PostgreSQL connection string |
|
|
778
|
-
| `AUTH_SECRET` | Registry session-signing secret, at least 32 characters |
|
|
779
|
-
| `AUTH_URL` | Canonical public registry URL |
|
|
780
|
-
| `PORT` | Standalone registry server port |
|
|
781
|
-
| `HOSTNAME` | Standalone registry bind hostname |
|
|
782
|
-
| `BLOB_READ_WRITE_TOKEN` | Credential for the Vercel Blob asset adapter |
|
|
783
|
-
| `HANDOFF_AI_KEY_SECRET` | Encrypts reader-supplied AI provider keys, at least 32 characters |
|
|
784
|
-
| `HANDOFF_AI_CONNECTIONS` | JSON array of AI connections, merged over the baked list by `id` |
|
|
785
|
-
| `HANDOFF_OUTPUT_DIR` | Override the fetched output directory |
|
|
786
|
-
| `HANDOFF_SITES_DIR` | Override the build output directory |
|
|
787
|
-
| `HANDOFF_WORKING_PATH` | Directory holding `handoff.config.ts`; defaults to the current directory |
|
|
788
|
-
| `HANDOFF_APP_PORT` | Workspace documentation server port |
|
|
789
|
-
| `HANDOFF_WEBSOCKET_PORT` | Workspace live-reload server port |
|
|
790
|
-
|
|
791
|
-
Registry assets are stored in PostgreSQL by default with a 4 MB per-blob limit.
|
|
792
|
-
Vercel Blob or a custom adapter can be selected through
|
|
793
|
-
`runtime.registry.assetStorage`. The `pg` or `neon` connection driver can be
|
|
794
|
-
selected through `runtime.registry.database.driver`; PostgreSQL is used by
|
|
795
|
-
both.
|
|
796
|
-
|
|
797
|
-
## Profiles
|
|
798
|
-
|
|
799
|
-
A profile is a sidecar config file that merges onto the base config. It holds
|
|
800
|
-
only what changes between environments, so a project keeps one shared config
|
|
801
|
-
instead of a second complete copy of it.
|
|
802
|
-
|
|
803
|
-
- The base config is `handoff.config.ts`, `.js`, `.cjs`, or `.json`. It is the
|
|
804
|
-
only config file loaded when no profile is selected.
|
|
805
|
-
- A profile file is `handoff.config.<profile>.*`, with the same four
|
|
806
|
-
extensions.
|
|
807
|
-
- Select a profile with `--profile <name>` or with `HANDOFF_PROFILE`.
|
|
808
|
-
`--profile` takes precedence over `HANDOFF_PROFILE`.
|
|
809
|
-
- Profile names are not predefined. A name can contain lowercase letters,
|
|
810
|
-
numbers, and hyphens.
|
|
811
|
-
- A selected profile must exist. If no `handoff.config.<name>.*` file is found,
|
|
812
|
-
the command stops with an error.
|
|
813
|
-
- `login`, `logout`, `publish`, and `checkout` also accept a profile that has
|
|
814
|
-
only a saved registry login, because there the profile selects the registry.
|
|
815
|
-
Every other command still needs the config file.
|
|
816
|
-
- Layers resolve as defaults, base config, profile, then programmatic config,
|
|
817
|
-
with the merge rules described under [Configuration](#configuration).
|
|
818
|
-
- A selected profile also reads `.env.<profile>` on top of `.env`, when that
|
|
819
|
-
file exists. Both are read from the directory the command runs in, and a
|
|
820
|
-
variable already set in the environment beats both files.
|
|
821
|
-
|
|
822
|
-
```bash
|
|
823
|
-
npx handoff-app build --target registry --profile registry
|
|
824
|
-
```
|
|
825
|
-
|
|
826
|
-
`build --target registry` always packages a registry-mode application, so a
|
|
827
|
-
profile does not need to set `runtime.mode`. This makes a profile a good place
|
|
828
|
-
for the registry build settings: the database driver, the database
|
|
829
|
-
environment-variable name, the asset storage, and whether MCP is enabled. A
|
|
830
|
-
hosting provider or a CI job selects the profile through `HANDOFF_PROFILE`.
|
|
831
|
-
|
|
832
|
-
`defineConfig` types a profile as well as a base config. The generated
|
|
833
|
-
`.gitignore` lists `handoff.config.local.*`, which makes `local` the usual name
|
|
834
|
-
for a profile that stays on one machine.
|
|
835
|
-
|
|
836
|
-
Arrays replace, so a profile that declares `catalog.include` also decides what
|
|
837
|
-
`make` and `checkout` register.
|
|
838
|
-
|
|
839
|
-
`.env.<profile>` is read once at startup rather than watched.
|
|
840
|
-
`HANDOFF_WORKING_PATH` cannot be set from it, because the working path is
|
|
841
|
-
resolved before any profile is known.
|
|
842
|
-
|
|
843
1115
|
## CLI reference
|
|
844
1116
|
|
|
845
1117
|
| Command | Description |
|