@entrinsik/vite-plugin-informer 2.7.0-beta.0 → 2.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +129 -0
- package/bin/ci.js +52 -51
- package/bin/publish.js +11 -4
- package/package.json +1 -1
- package/src/agent-dev.js +26 -12
- package/src/assemble.js +5 -1
- package/src/compat.js +252 -0
- package/src/deploy.js +116 -43
- package/src/dev-channel-shim.js +150 -0
- package/src/dev-channels.js +191 -0
- package/src/dev-dependencies.js +44 -17
- package/src/dev-platform.js +41 -0
- package/src/dev-streams.js +660 -0
- package/src/fan-out.js +129 -0
- package/src/index.js +131 -16
- package/src/marketplaces.js +28 -2
- package/src/publish-payload.js +55 -7
- package/src/publish.js +25 -1
- package/src/server-routes.js +79 -21
- package/src/streams-client.js +200 -0
package/README.md
ADDED
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
# @entrinsik/vite-plugin-informer
|
|
2
|
+
|
|
3
|
+
Vite plugin and deploy tool for Informer App development: a local dev server wired to a live Informer, and a deploy pipeline that assembles your project and ships it to an App's library.
|
|
4
|
+
|
|
5
|
+
## Quick start
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm create vite@latest my-app -- --template react
|
|
9
|
+
cd my-app
|
|
10
|
+
npm install -D @entrinsik/vite-plugin-informer
|
|
11
|
+
npx informer-init # wires vite config, informer.yaml, .env, deploy scripts
|
|
12
|
+
npm run dev # local dev against the Informer in .env
|
|
13
|
+
npm run deploy # assemble + upload + deploy
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
## What a deploy ships
|
|
17
|
+
|
|
18
|
+
The assembler uploads your built frontend plus these source trees, matching how an App's library is structured in Informer:
|
|
19
|
+
|
|
20
|
+
| Folder | Becomes |
|
|
21
|
+
|---|---|
|
|
22
|
+
| `server/` | API routes, dispatched under the App's `/api` surface |
|
|
23
|
+
| `webhooks/` | Publicly callable webhook routes |
|
|
24
|
+
| `tools/` | AI tools, private to the App's agents |
|
|
25
|
+
| `mcp/` | Tools exposed over the App's MCP endpoint |
|
|
26
|
+
| `channels/` | Live channel join/leave handlers |
|
|
27
|
+
| `migrations/` | Workspace database migrations, run in order on deploy |
|
|
28
|
+
| `embeddings/` | Declarative embedding use cases (see below) |
|
|
29
|
+
| `lib/`, `shared/` | Modules importable by the trees above |
|
|
30
|
+
|
|
31
|
+
`informer.yaml` declares dependencies, raw API access, roles, and (optionally) a minimum Informer version.
|
|
32
|
+
|
|
33
|
+
## Server compatibility
|
|
34
|
+
|
|
35
|
+
A deploy begins by asking the target server what it is (`GET /api/about`, one unauthenticated request) and adapts to the answer, so one version of this plugin can drive a fleet on mixed releases.
|
|
36
|
+
|
|
37
|
+
Everything below needs **Informer 2026.1.3**: App Channels (`channels/`, the `channels:` block, `broadcast()`), the embedding pump (`embeddings/`, `embed()`), staged uploads/downloads (`uploads`/`downloads` on the handler bag, `__INFORMER__.upload()`), and the `platform` descriptor itself.
|
|
38
|
+
|
|
39
|
+
Against an older server the CLI:
|
|
40
|
+
|
|
41
|
+
- **Leaves `channels/` and `embeddings/` on your disk.** Those releases don't recognise the folders as server-side, so anything uploaded there is served to any App viewer — your workspace SQL and channel authorization logic included. The rest of the App deploys normally; the warning names what was skipped and why.
|
|
42
|
+
- **Names every feature that would be inert**, so a green deploy can't be mistaken for a working one.
|
|
43
|
+
- **Enforces `requires: { informer: … }` itself.** Releases before 2026.1.3 ignore the block, so without this an App that declared a floor would deploy clean onto a server that can't run it. The deploy is refused before the App's library is touched.
|
|
44
|
+
|
|
45
|
+
A server whose version isn't comparable (a `dev` build, or an `/about` behind a proxy) is treated as current and warned about — gating it would break deploys to builds made from source.
|
|
46
|
+
|
|
47
|
+
Feature-detect at runtime with optional chaining, since `platform` is absent entirely before 2026.1.3:
|
|
48
|
+
|
|
49
|
+
```javascript
|
|
50
|
+
if (platform?.capabilities?.channels) broadcast('tickets', { … });
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
A marketplace publish is unaffected: the archive always carries every source tree, because the server that installs it isn't known at publish time.
|
|
54
|
+
|
|
55
|
+
## Embeddings
|
|
56
|
+
|
|
57
|
+
Apps can maintain vector embeddings over their own workspace data declaratively. You ship an `embeddings/` folder with **one file per use case**; the Informer platform acts as an *embedding pump* that asks your App what is pending, chunks and embeds the content in billed batches, and hands the vectors back for your App to store in its own workspace tables. You never call an embedding provider yourself, and the platform never keeps a copy of your corpus — your own tables are the ledger.
|
|
58
|
+
|
|
59
|
+
Each use case file exports `GET` and `POST` (a missing handler fails the deploy) and, optionally, a `config` literal; omitted keys take the defaults below, and a use case with no triggers in its config only runs on deploy and on a manual run:
|
|
60
|
+
|
|
61
|
+
```javascript
|
|
62
|
+
// embeddings/tickets.js
|
|
63
|
+
export const config = { chunking: 'prose', on: ['ticket.created'], cron: '0 * * * *', revision: 1 };
|
|
64
|
+
|
|
65
|
+
// GET: the rows that still need embedding. The query IS the watermark:
|
|
66
|
+
// never embedded, stale, or written under an older revision.
|
|
67
|
+
export async function GET({ query, batch }) {
|
|
68
|
+
return await query(`
|
|
69
|
+
SELECT t.id, t.subject || ' ' || t.body AS content
|
|
70
|
+
FROM tickets t
|
|
71
|
+
LEFT JOIN ticket_embeddings e ON e.ticket_id = t.id AND e.seq = 0
|
|
72
|
+
WHERE e.ticket_id IS NULL OR e.embedded_at < t.updated_at OR e.revision <> $2
|
|
73
|
+
ORDER BY t.id
|
|
74
|
+
LIMIT $1
|
|
75
|
+
`, [batch.limit, batch.revision]);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// POST: store the finished vectors wherever your migrations put them.
|
|
79
|
+
export async function POST({ query, batch }) {
|
|
80
|
+
for (const doc of batch.docs) {
|
|
81
|
+
await query('DELETE FROM ticket_embeddings WHERE ticket_id = $1', [doc.id]);
|
|
82
|
+
for (const c of doc.chunks) {
|
|
83
|
+
await query(
|
|
84
|
+
'INSERT INTO ticket_embeddings (ticket_id, seq, embedding, content, revision) VALUES ($1, $2, $3::vector, $4, $5)',
|
|
85
|
+
[doc.id, c.seq, JSON.stringify(c.embedding), c.content, batch.revision]
|
|
86
|
+
);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
return { stored: batch.docs.length };
|
|
90
|
+
}
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
How it works, briefly:
|
|
94
|
+
|
|
95
|
+
- **Your storage is the ledger.** Because `GET` anti-joins your own embedding table, the pump is idempotent and crash-tolerant: a run that dies mid-way finds the same rows pending next time.
|
|
96
|
+
- **Triggers converge.** Deploy pokes each use case (initial backfill), `emit()` events matching `on:` poke, `cron` desugars into a scheduled poke, and the platform sweep runs whatever is poked. A manual run claims the same single-flight lease directly, so nothing double-embeds.
|
|
97
|
+
- **Failures are bounded.** A run that fails for a reason the next attempt might not hit (a provider blip, a handler timeout) retries with a doubling delay and is parked after five consecutive failures; a run that cannot succeed as-is (a rejected key, an exhausted budget, a contract violation) is parked at once. Every park keeps the error on the use case's status row; the next trigger gives it one more attempt, and a successful run resets the count.
|
|
98
|
+
- **`revision` is the invalidation.** The string the pump hands you couples your config (chunking profile, budgets, `config.revision`) to the resolved platform embedding model. Store it beside each vector and compare it in `GET`: bumping the config, changing chunking, or an admin repointing the embedding model all surface as pending re-embed work automatically.
|
|
99
|
+
- **Unembeddable documents are tombstoned** platform-side by content hash and re-reported with `skipped: true` on every run — stamp them in your own table and exclude them from `GET`.
|
|
100
|
+
- **Workspace pgvector.** Apps with the embeddings capability get the `vector` extension provisioned where the server may create extensions (otherwise the deploy log names the cause and a migration using the `vector` type fails), so migrations can declare real `vector(...)` columns; `ON DELETE CASCADE` foreign keys are the cleanup story.
|
|
101
|
+
- **Query time.** Every handler bag (`server/`, `webhooks/`, `tools/`, `mcp/`) gets `embed(name, text)`, which returns `{ embedding, revision }`. `name` must be a declared use case. Filter on the revision as well as ordering by distance — an admin repointing the embedding model puts your query vectors in a new space while the stored rows are still in the old one, and an unfiltered `ORDER BY` then returns near-random neighbours with no error:
|
|
102
|
+
|
|
103
|
+
```javascript
|
|
104
|
+
const { embedding, revision } = await embed('tickets', q);
|
|
105
|
+
const hits = await query(
|
|
106
|
+
`SELECT ticket_id, content
|
|
107
|
+
FROM ticket_embeddings
|
|
108
|
+
WHERE revision = $2
|
|
109
|
+
ORDER BY embedding <=> $1::vector
|
|
110
|
+
LIMIT 10`,
|
|
111
|
+
[JSON.stringify(embedding), revision]
|
|
112
|
+
);
|
|
113
|
+
```
|
|
114
|
+
- **Billing.** Embedding calls bill to the App, one usage entry per batch of up to 128 chunks regardless of how many provider requests run underneath; pump compute meters against the App's compute budget.
|
|
115
|
+
- **`config` is read as a literal.** Deploy lifts `export const config = { … }` out of the source and evaluates it with none of the file's imports in scope, so it must be self-contained: no imported constants, no spreads of imported objects, no `export { config }` re-export. Other modules may import the literal from the use-case file; the reverse fails the deploy. The same self-contained-literal rule applies to `export const config`, `schema`, and `description` in `server/`, `webhooks/`, `tools/`, and `mcp/`; there a `config` violation fails the deploy while a `schema` or `description` violation is a deploy warning (nothing is published for that export).
|
|
116
|
+
|
|
117
|
+
### Dev loop
|
|
118
|
+
|
|
119
|
+
`npm run deploy` uploads `embeddings/` like any other source tree — there is no manifest block to add (plugin ≥ 2.10.0; earlier versions never upload the folder, so the use case silently does not exist on the server). `npm run dev` never runs the pump, and its `embed()` throws an explanation rather than embedding (the dev mirror reports `platform.capabilities.embeddings: false`), so a search route is exercised against a deployed App. Feature-detect with `platform?.capabilities?.embeddings` — `typeof embed === 'function'` is true on both, since the binding exists either way. Then, in the App admin panel, the **Embeddings** tab lists each use case with its pump status (queued, running, up to date, last run, last error, skipped docs) and a **Run now** action; the same surface exists as API routes (`GET /apps/{id}/embeddings`, `POST /apps/{id}/embeddings/{name}/_run`). Deploy, run, read the error, fix, repeat.
|
|
120
|
+
|
|
121
|
+
### Upgrading an App that already has an `embeddings/` folder
|
|
122
|
+
|
|
123
|
+
`embeddings/` is a server-side folder from this release on, like `server/`: its files are uploaded with the library, never served to browsers, and scanned as pump handlers. An App that kept anything else there (assets, data files) loses those files in the browser after redeploying with plugin 2.10.0 or later. A `.js` file there that exports `GET` or `POST` but not both fails the deploy; one that exports neither is never scanned as a use case at all and lands as a warning on an otherwise successful deploy. Move such content elsewhere before upgrading; the deploy names every stray entry as a warning.
|
|
124
|
+
|
|
125
|
+
### Older Informer releases
|
|
126
|
+
|
|
127
|
+
The `embeddings/` folder, `embed()`, and the pump routes exist only on Informer releases that ship the feature. An App that must also run on older servers feature-detects with optional chaining (`platform?.capabilities?.embeddings` on the handler bag, `window.__INFORMER__.platform?.capabilities?.embeddings` in the browser) — releases before 2026.1.3 inject no `platform` descriptor at all, so a bare `platform.capabilities.embeddings` throws there instead of reporting `false`. It gates its vector DDL with a `-- requires: embeddings` header so the migration is skipped where pgvector is absent, and states a floor in `informer.yaml` (`requires: { informer: '>=…' }`) only if it cannot work without the feature.
|
|
128
|
+
|
|
129
|
+
Full reference: the **Embeddings** article under Developer → API → App in the Informer docs.
|
package/bin/ci.js
CHANGED
|
@@ -14,7 +14,9 @@
|
|
|
14
14
|
* INFORMER_MARKETPLACES: ${{ vars.INFORMER_MARKETPLACES }}
|
|
15
15
|
*
|
|
16
16
|
* Everything interesting lives here rather than in YAML, so fixing it is an
|
|
17
|
-
* npm release rather than a pull request into every customer's repo.
|
|
17
|
+
* npm release rather than a pull request into every customer's repo. And
|
|
18
|
+
* everything testable lives in src/fan-out.js rather than here — this file
|
|
19
|
+
* is only argv/env handling, console output, and the exit code.
|
|
18
20
|
*
|
|
19
21
|
* For a single marketplace with a `lmpub_` key — locally, or on CI that is not
|
|
20
22
|
* GitHub Actions — use `informer-publish` instead. This command needs the OIDC
|
|
@@ -24,8 +26,8 @@
|
|
|
24
26
|
import { appendFile } from 'node:fs/promises';
|
|
25
27
|
import { resolve } from 'node:path';
|
|
26
28
|
import { publish } from '../src/publish.js';
|
|
27
|
-
import { preparePublish, tagVersion } from '../src/publish-payload.js';
|
|
28
|
-
import {
|
|
29
|
+
import { preparePublish, tagVersion, isVersionTag, looksLikeVersion } from '../src/publish-payload.js';
|
|
30
|
+
import { resolveTargets, fanOut, summaryTable, failedTargets } from '../src/fan-out.js';
|
|
29
31
|
import { canMintIdToken, mintIdToken } from '../src/github-oidc.js';
|
|
30
32
|
import { loadEnv, parseModeArg } from '../src/env.js';
|
|
31
33
|
|
|
@@ -34,12 +36,12 @@ loadEnv({ mode });
|
|
|
34
36
|
|
|
35
37
|
const projectRoot = resolve('.');
|
|
36
38
|
|
|
37
|
-
// --- Targets: explicit flags replace the configured list entirely, so a
|
|
38
|
-
// workflow_dispatch can re-publish to a subset without editing config. ---
|
|
39
39
|
let targets;
|
|
40
40
|
try {
|
|
41
|
-
|
|
42
|
-
|
|
41
|
+
targets = resolveTargets({
|
|
42
|
+
explicit: argValues('--marketplace'),
|
|
43
|
+
configured: process.env.INFORMER_MARKETPLACES
|
|
44
|
+
});
|
|
43
45
|
} catch (err) {
|
|
44
46
|
fail(err.message);
|
|
45
47
|
}
|
|
@@ -60,13 +62,28 @@ if (!canMintIdToken()) {
|
|
|
60
62
|
);
|
|
61
63
|
}
|
|
62
64
|
|
|
63
|
-
//
|
|
65
|
+
// The version comes from the tag that triggered the run. On a branch push or
|
|
66
|
+
// a workflow_dispatch against a branch, GITHUB_REF_NAME is the branch name —
|
|
67
|
+
// and without this guard the run would publish a version literally called
|
|
68
|
+
// "main", on the STABLE channel (no prerelease dash), to every marketplace.
|
|
69
|
+
// The predicate is shared with informer-publish (src/publish-payload.js).
|
|
70
|
+
const explicitVersion = argValue('--version');
|
|
71
|
+
if (explicitVersion && !looksLikeVersion(explicitVersion)) {
|
|
72
|
+
fail(`--version "${explicitVersion}" doesn't look like a version (x.y.z, or x.y.z-beta.n).`);
|
|
73
|
+
}
|
|
74
|
+
if (!explicitVersion && !isVersionTag(process.env.GITHUB_REF_TYPE, process.env.GITHUB_REF_NAME)) {
|
|
75
|
+
fail(
|
|
76
|
+
`informer-ci publishes the version named by the tag that triggered the run, ` +
|
|
77
|
+
`but this run's ref is ${process.env.GITHUB_REF_TYPE || 'ref'} "${process.env.GITHUB_REF_NAME || '(unset)'}" — not a version tag.\n` +
|
|
78
|
+
' Run the workflow against a vX.Y.Z tag, or pass --version <x.y.z>.'
|
|
79
|
+
);
|
|
80
|
+
}
|
|
81
|
+
const refVersion = tagVersion(process.env.GITHUB_REF_NAME);
|
|
82
|
+
|
|
83
|
+
// --- Build the payload once: every marketplace gets the same file set. ---
|
|
64
84
|
let payload;
|
|
65
85
|
try {
|
|
66
|
-
payload = await preparePublish({
|
|
67
|
-
projectRoot,
|
|
68
|
-
version: argValue('--version') || tagVersion(process.env.GITHUB_REF_NAME)
|
|
69
|
-
});
|
|
86
|
+
payload = await preparePublish({ projectRoot, version: explicitVersion || refVersion });
|
|
70
87
|
} catch (err) {
|
|
71
88
|
fail(err.message);
|
|
72
89
|
}
|
|
@@ -79,28 +96,30 @@ console.log(
|
|
|
79
96
|
`${files.length} files${screenshots.length ? `, ${screenshots.length} screenshot${screenshots.length === 1 ? '' : 's'}` : ''}`
|
|
80
97
|
);
|
|
81
98
|
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
// Audience is the marketplace itself, so a token minted for one
|
|
89
|
-
// License Manager is not replayable at another.
|
|
90
|
-
const token = await mintIdToken(marketplaceUrl);
|
|
91
|
-
const result = await publish({ marketplaceUrl, token, files, icon, screenshots, fields: payload.fields });
|
|
99
|
+
const results = await fanOut({
|
|
100
|
+
targets,
|
|
101
|
+
mint: mintIdToken,
|
|
102
|
+
publishOne: ({ marketplaceUrl, token }) =>
|
|
103
|
+
publish({ marketplaceUrl, token, files, icon, screenshots, fields: payload.fields }),
|
|
104
|
+
onSuccess: (label, result) => {
|
|
92
105
|
for (const w of result.warnings || []) console.warn(` warning (${label}): ${w}`);
|
|
93
106
|
console.log(` ✓ ${label} — published ${slug} v${version} (${result.channel || channel})`);
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
console.error(` ✗ ${label} — ${
|
|
97
|
-
results.push({ label, ok: false, error: err.message });
|
|
107
|
+
},
|
|
108
|
+
onFailure: (label, message) => {
|
|
109
|
+
console.error(` ✗ ${label} — ${message}`);
|
|
98
110
|
}
|
|
99
|
-
}
|
|
111
|
+
});
|
|
100
112
|
|
|
101
113
|
await writeSummary(results);
|
|
102
114
|
|
|
103
|
-
|
|
115
|
+
// A row that published but whose reporting threw: the ✓ line above is missing
|
|
116
|
+
// for it, so say what happened rather than leaving a silent gap. Not a failure
|
|
117
|
+
// — the release is live, and exiting 1 here invites a re-run that 409s.
|
|
118
|
+
for (const r of results.filter(r => r.reportError)) {
|
|
119
|
+
console.warn(` warning (${r.label}): published, but reporting failed — ${r.reportError}`);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
const failed = failedTargets(results);
|
|
104
123
|
if (failed.length) {
|
|
105
124
|
console.error(`\n${failed.length} of ${results.length} marketplaces failed.`);
|
|
106
125
|
process.exit(1);
|
|
@@ -122,32 +141,14 @@ function argValues(flag) {
|
|
|
122
141
|
return out;
|
|
123
142
|
}
|
|
124
143
|
|
|
125
|
-
function hostOf(url) {
|
|
126
|
-
try {
|
|
127
|
-
return new URL(url).host;
|
|
128
|
-
} catch {
|
|
129
|
-
return url;
|
|
130
|
-
}
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
/**
|
|
134
|
-
* A per-target line in the job summary. Without this the Actions UI shows one
|
|
135
|
-
* opaque step for what is really N independent publishes — the observability
|
|
136
|
-
* the old matrix-of-jobs gave up when fan-out moved in here.
|
|
137
|
-
*/
|
|
138
144
|
async function writeSummary(rows) {
|
|
139
145
|
const path = process.env.GITHUB_STEP_SUMMARY;
|
|
140
146
|
if (!path || !rows.length) return;
|
|
141
|
-
const body =
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
'
|
|
145
|
-
|
|
146
|
-
...rows.map(r => `| ${r.label} | ${r.ok ? 'published' : `failed — ${r.error.replace(/\|/g, '\\|')}`} |`),
|
|
147
|
-
''
|
|
148
|
-
].join('\n');
|
|
149
|
-
await appendFile(path, body).catch(() => {
|
|
150
|
-
// A summary is a nicety; never fail a good publish over it.
|
|
147
|
+
const body = summaryTable({ title: `${name} v${version} (${channel})`, rows });
|
|
148
|
+
await appendFile(path, body).catch(err => {
|
|
149
|
+
// A summary is a nicety; never fail a good publish over it — but say
|
|
150
|
+
// why it's missing rather than leaving its absence a mystery.
|
|
151
|
+
console.warn(` warning: could not write the job summary: ${err.message}`);
|
|
151
152
|
});
|
|
152
153
|
}
|
|
153
154
|
|
package/bin/publish.js
CHANGED
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
|
|
11
11
|
import { resolve } from 'node:path';
|
|
12
12
|
import { publish } from '../src/publish.js';
|
|
13
|
-
import { preparePublish, tagVersion } from '../src/publish-payload.js';
|
|
13
|
+
import { preparePublish, tagVersion, isVersionTag } from '../src/publish-payload.js';
|
|
14
14
|
import { loadEnv, parseModeArg } from '../src/env.js';
|
|
15
15
|
|
|
16
16
|
const mode = parseModeArg(process.argv);
|
|
@@ -39,12 +39,19 @@ if (process.env.INFORMER_MARKETPLACES && process.env.GITHUB_ACTIONS) {
|
|
|
39
39
|
|
|
40
40
|
const projectRoot = resolve('.');
|
|
41
41
|
|
|
42
|
+
// Prefer an explicit --version, then the git tag (CI: GITHUB_REF_NAME=vX.Y.Z),
|
|
43
|
+
// then package.json. A branch ref is NOT a version tag: without the gate, a
|
|
44
|
+
// branch push on Actions published a version literally called "main" — so a
|
|
45
|
+
// non-tag ref falls through to package.json, and says so.
|
|
46
|
+
const refIsTag = isVersionTag(process.env.GITHUB_REF_TYPE, process.env.GITHUB_REF_NAME);
|
|
47
|
+
if (process.env.GITHUB_REF_NAME && !refIsTag && !argValue('--version')) {
|
|
48
|
+
console.warn(`Note: ref "${process.env.GITHUB_REF_NAME}" is not a version tag — using the package.json version.`);
|
|
49
|
+
}
|
|
50
|
+
|
|
42
51
|
let payload;
|
|
43
52
|
try {
|
|
44
53
|
payload = await preparePublish({
|
|
45
|
-
|
|
46
|
-
// then package.json.
|
|
47
|
-
version: argValue('--version') || tagVersion(process.env.GITHUB_REF_NAME),
|
|
54
|
+
version: argValue('--version') || (refIsTag ? tagVersion(process.env.GITHUB_REF_NAME) : undefined),
|
|
48
55
|
projectRoot
|
|
49
56
|
});
|
|
50
57
|
} catch (err) {
|
package/package.json
CHANGED
package/src/agent-dev.js
CHANGED
|
@@ -2,7 +2,18 @@ import { readFile, readdir, stat, access } from 'node:fs/promises';
|
|
|
2
2
|
import { join } from 'node:path';
|
|
3
3
|
import { parse as parseUrl } from 'node:url';
|
|
4
4
|
import yaml from 'yaml';
|
|
5
|
-
import {
|
|
5
|
+
import { manifestBlock, buildDevContext, buildDevCrypto, buildDevMessaging, normalizeFetchPath } from './dev-dependencies.js';
|
|
6
|
+
import { createDevChannels, createDevEmit } from './dev-channels.js';
|
|
7
|
+
import { devPlatform } from './dev-platform.js';
|
|
8
|
+
|
|
9
|
+
// embed() is present on a real install even without the embeddings
|
|
10
|
+
// capability, where it throws a written explanation. Mirroring that here
|
|
11
|
+
// keeps the dev failure the same lesson as the deployed one, instead of a
|
|
12
|
+
// bare "embed is not a function" that reads like a missing binding.
|
|
13
|
+
const embed = async () => {
|
|
14
|
+
throw new Error('embed() is not available in the dev mirror: the embeddings capability needs a real Informer (platform.capabilities.embeddings is false)');
|
|
15
|
+
};
|
|
16
|
+
|
|
6
17
|
const parseYaml = yaml.parse;
|
|
7
18
|
|
|
8
19
|
const MAX_STEPS = 20;
|
|
@@ -149,9 +160,11 @@ async function readSSE(response) {
|
|
|
149
160
|
*
|
|
150
161
|
* @param {Object} viteServer - Vite dev server instance
|
|
151
162
|
* @param {Object} opts - Configuration
|
|
163
|
+
* @param {ReturnType<typeof createDevChannels>} [opts.channels] - the dev channels hub
|
|
164
|
+
* behind `broadcast()` and the `channels:` relay (a private one when omitted)
|
|
152
165
|
* @returns {Function} Connect middleware
|
|
153
166
|
*/
|
|
154
|
-
export function createAgentMiddleware(viteServer, { serverOrigin, authHeader, devWorkspaceId, projectRoot, devBindings, appToken }) {
|
|
167
|
+
export function createAgentMiddleware(viteServer, { serverOrigin, authHeader, devWorkspaceId, projectRoot, devBindings, appToken, channels = createDevChannels() }) {
|
|
155
168
|
|
|
156
169
|
// query() — proxies to the workspace _sql endpoint (same as server-routes.js)
|
|
157
170
|
async function query(sql, params) {
|
|
@@ -226,12 +239,6 @@ export function createAgentMiddleware(viteServer, { serverOrigin, authHeader, de
|
|
|
226
239
|
? async (path, opts = {}) => await fetchAs(appAuth, path, { ...opts, headers: { ...(opts.headers || {}), 'x-informer-app-depth': '1' } })
|
|
227
240
|
: null;
|
|
228
241
|
|
|
229
|
-
// emit() — no-op in dev mode (logs to console)
|
|
230
|
-
function emit(event, payload) {
|
|
231
|
-
console.log(`[agent-dev] emit("${event}",`, JSON.stringify(payload), ')');
|
|
232
|
-
return { ok: true };
|
|
233
|
-
}
|
|
234
|
-
|
|
235
242
|
// notify/email — delivery is a console-logged no-op in dev, but required-field
|
|
236
243
|
// validation mirrors prod so a tool that passes here won't 500 in production.
|
|
237
244
|
const { notify, email } = buildDevMessaging('[agent-dev]');
|
|
@@ -319,10 +326,17 @@ export function createAgentMiddleware(viteServer, { serverOrigin, authHeader, de
|
|
|
319
326
|
// Build the typed dependency context + env the same way the
|
|
320
327
|
// server-routes dev middleware does, so tools using
|
|
321
328
|
// `context.<slot>.<method>(...)` and `env` work locally and match
|
|
322
|
-
// the prod sandbox bag.
|
|
323
|
-
const deps =
|
|
329
|
+
// the prod sandbox bag. The manifest is already in hand.
|
|
330
|
+
const deps = manifestBlock(yaml, 'dependencies');
|
|
324
331
|
const context = buildDevContext({ deps, apiFetch, devBindings, appFetch });
|
|
325
|
-
const env =
|
|
332
|
+
const env = manifestBlock(yaml, 'env');
|
|
333
|
+
|
|
334
|
+
// emit() writes no app_event row in dev, but still relays a
|
|
335
|
+
// listed event to its `channels:` channel; broadcast() publishes
|
|
336
|
+
// a live frame to the page (see dev-channels.js).
|
|
337
|
+
const manifestChannels = manifestBlock(yaml, 'channels');
|
|
338
|
+
const emit = createDevEmit({ channels, manifestChannels, logPrefix: '[agent-dev]' });
|
|
339
|
+
const { broadcast } = channels;
|
|
326
340
|
|
|
327
341
|
// Load tool handlers via ssrLoadModule
|
|
328
342
|
const localTools = await scanLocalTools(projectRoot);
|
|
@@ -441,7 +455,7 @@ export function createAgentMiddleware(viteServer, { serverOrigin, authHeader, de
|
|
|
441
455
|
|
|
442
456
|
if (tool) {
|
|
443
457
|
try {
|
|
444
|
-
result = await tool.handler({ args: tc.input, run: { agentName, trigger: triggerEvent }, context, query, fetch: apiFetch, emit, notify, email, crypto: cryptoHelper, markdown, log, env });
|
|
458
|
+
result = await tool.handler({ args: tc.input, run: { agentName, trigger: triggerEvent }, context, query, fetch: apiFetch, emit, broadcast, notify, email, embed, crypto: cryptoHelper, markdown, log, env, platform: devPlatform() });
|
|
445
459
|
} catch (err) {
|
|
446
460
|
console.error(`[agent-dev] Tool "${tc.name}" failed:`, err.message);
|
|
447
461
|
result = { error: err.message };
|
package/src/assemble.js
CHANGED
|
@@ -22,7 +22,11 @@ import { join, relative, posix } from 'node:path';
|
|
|
22
22
|
// the Vite build lands it at the library root). Without README.md here, a
|
|
23
23
|
// CLI-deployed app could never light those surfaces up via its README.
|
|
24
24
|
const ROOT_CONFIG_FILES = ['informer.yaml', 'data-access.yaml', 'API.md', 'README.md'];
|
|
25
|
-
|
|
25
|
+
// channels/ holds live-channel join/leave handlers (App Channels); the server
|
|
26
|
+
// scans and bundles it like server/ and webhooks/. `embeddings` holds the pump
|
|
27
|
+
// use-case files: server-side like server/, so they ship with the library and
|
|
28
|
+
// the platform can scan them.
|
|
29
|
+
const SOURCE_DIRS = ['migrations', 'tools', 'mcp', 'server', 'webhooks', 'channels', 'embeddings', 'lib', 'shared'];
|
|
26
30
|
|
|
27
31
|
// Entries never worth shipping in an app's server-side library: OS/editor
|
|
28
32
|
// dotfiles (.DS_Store, .env), nested dependency trees, and test files. Applied
|