@entrinsik/vite-plugin-informer 2.3.0 → 2.5.0-beta.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/bin/deploy.js +3 -2
- package/bin/init.js +55 -27
- package/bin/publish.js +128 -0
- package/bin/workspace.js +11 -5
- package/package.json +3 -1
- package/src/agent-dev.js +43 -2
- package/src/assemble.js +87 -0
- package/src/changelog.js +39 -0
- package/src/deploy.js +20 -159
- package/src/dev-dependencies.js +363 -0
- package/src/env.js +86 -0
- package/src/index.js +22 -5
- package/src/publish.js +97 -0
- package/src/server-routes.js +80 -10
- package/src/workspace.js +2 -2
package/bin/deploy.js
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
|
-
import dotenv from 'dotenv';
|
|
4
3
|
import { readFile, writeFile } from 'node:fs/promises';
|
|
5
4
|
import { resolve } from 'node:path';
|
|
6
5
|
import { deploy } from '../src/deploy.js';
|
|
6
|
+
import { loadEnv, parseModeArg } from '../src/env.js';
|
|
7
7
|
|
|
8
|
-
|
|
8
|
+
const mode = parseModeArg(process.argv);
|
|
9
|
+
loadEnv({ mode });
|
|
9
10
|
|
|
10
11
|
const baseUrl = process.env.INFORMER_URL;
|
|
11
12
|
const apiKey = process.env.INFORMER_API_KEY;
|
package/bin/init.js
CHANGED
|
@@ -13,40 +13,66 @@ const cwd = process.cwd();
|
|
|
13
13
|
function generateInformerYaml() {
|
|
14
14
|
return `# Informer App Configuration
|
|
15
15
|
# =========================
|
|
16
|
-
# This file controls
|
|
17
|
-
#
|
|
16
|
+
# This file controls the data your app depends on and defines custom
|
|
17
|
+
# roles for role-based UIs.
|
|
18
18
|
#
|
|
19
|
-
# Without
|
|
19
|
+
# Without dependencies: or access:, all API access is blocked (secure
|
|
20
|
+
# by default).
|
|
20
21
|
|
|
21
22
|
# ============================================================================
|
|
22
|
-
#
|
|
23
|
+
# DEPENDENCIES (preferred)
|
|
23
24
|
# ============================================================================
|
|
24
|
-
#
|
|
25
|
+
# Typed slots that the installer binds to actual resources at deploy
|
|
26
|
+
# time. Each slot becomes a property on the handler context object —
|
|
27
|
+
# call methods on it instead of building raw API URLs.
|
|
25
28
|
#
|
|
26
|
-
#
|
|
27
|
-
#
|
|
28
|
-
#
|
|
29
|
-
#
|
|
30
|
-
#
|
|
31
|
-
# region: $user.custom.region
|
|
29
|
+
# Slot methods by target:
|
|
30
|
+
# dataset → context.<slot>.search(esQuery) / context.<slot>.fields()
|
|
31
|
+
# query → context.<slot>.execute(params)
|
|
32
|
+
# datasource → context.<slot>.query(payload)
|
|
33
|
+
# integration → context.<slot>.request({ method, path, body })
|
|
32
34
|
#
|
|
33
|
-
#
|
|
34
|
-
#
|
|
35
|
+
# defaultBinding must be a UUID (not a configId). Look up UUIDs via:
|
|
36
|
+
# GET /api/datasets-list → for target: dataset
|
|
37
|
+
# GET /api/queries-list → for target: query
|
|
38
|
+
# GET /api/datasources-list → for target: datasource
|
|
39
|
+
# GET /api/integrations-list → for target: integration
|
|
35
40
|
#
|
|
36
|
-
#
|
|
37
|
-
# - salesforce
|
|
41
|
+
# Example:
|
|
38
42
|
#
|
|
39
|
-
#
|
|
40
|
-
#
|
|
43
|
+
# dependencies:
|
|
44
|
+
# sales:
|
|
45
|
+
# target: dataset
|
|
46
|
+
# defaultBinding: 7d5a9b1e-0c83-4bde-9e2a-3a4b5c6d7e8f
|
|
47
|
+
# orders:
|
|
48
|
+
# target: dataset
|
|
49
|
+
# defaultBinding: 1f2e3d4c-5b6a-7980-1234-56789abcdef0
|
|
50
|
+
# options:
|
|
51
|
+
# filter:
|
|
52
|
+
# region: $user.custom.region # Row-level security
|
|
53
|
+
# monthly_summary:
|
|
54
|
+
# target: query
|
|
55
|
+
# defaultBinding: 9a8b7c6d-5e4f-3a2b-1c0d-fedcba987654
|
|
56
|
+
# salesforce:
|
|
57
|
+
# target: integration # No defaultBinding — installer picks
|
|
58
|
+
# options:
|
|
59
|
+
# headers:
|
|
60
|
+
# Authorization: Bearer $user.custom.sfToken
|
|
61
|
+
|
|
62
|
+
dependencies: {}
|
|
63
|
+
|
|
64
|
+
# ============================================================================
|
|
65
|
+
# ACCESS (raw API allowlist — for paths that don't fit the typed-slot model)
|
|
66
|
+
# ============================================================================
|
|
67
|
+
# Use access.apis for endpoints not covered by dependencies: slots,
|
|
68
|
+
# such as AI model routes or custom server endpoints.
|
|
41
69
|
#
|
|
42
|
-
#
|
|
70
|
+
# access:
|
|
71
|
+
# apis:
|
|
72
|
+
# - POST /api/models/go_everyday/_object
|
|
73
|
+
# - POST /api/models/go_everyday/_chat
|
|
43
74
|
# - POST /api/custom/endpoint
|
|
44
75
|
|
|
45
|
-
access:
|
|
46
|
-
datasets: []
|
|
47
|
-
queries: []
|
|
48
|
-
integrations: []
|
|
49
|
-
|
|
50
76
|
# ============================================================================
|
|
51
77
|
# ROLES (optional)
|
|
52
78
|
# ============================================================================
|
|
@@ -111,7 +137,8 @@ async function init() {
|
|
|
111
137
|
const pkgPath = resolve(cwd, 'package.json');
|
|
112
138
|
if (!await exists(pkgPath)) {
|
|
113
139
|
console.error('No package.json found. Run this in a Vite project directory.');
|
|
114
|
-
console.error('Create one first with: npm create vite@latest');
|
|
140
|
+
console.error('Create one first with: npm create vite@latest . -- --template react');
|
|
141
|
+
console.error('(Use --template react-ts for TypeScript, or vanilla/vue/svelte/etc.)');
|
|
115
142
|
process.exit(1);
|
|
116
143
|
}
|
|
117
144
|
|
|
@@ -121,7 +148,8 @@ async function init() {
|
|
|
121
148
|
const hasVite = pkg.devDependencies?.vite || pkg.dependencies?.vite;
|
|
122
149
|
if (!hasVite) {
|
|
123
150
|
console.error('Vite not found in dependencies.');
|
|
124
|
-
console.error('Create a Vite project first: npm create vite@latest');
|
|
151
|
+
console.error('Create a Vite project first: npm create vite@latest . -- --template react');
|
|
152
|
+
console.error('(Use --template react-ts for TypeScript, or vanilla/vue/svelte/etc.)');
|
|
125
153
|
process.exit(1);
|
|
126
154
|
}
|
|
127
155
|
|
|
@@ -198,7 +226,7 @@ INFORMER_API_KEY=your-api-key
|
|
|
198
226
|
const informerYamlPath = resolve(cwd, 'informer.yaml');
|
|
199
227
|
if (!await exists(informerYamlPath)) {
|
|
200
228
|
await writeFile(informerYamlPath, generateInformerYaml());
|
|
201
|
-
console.log('Created informer.yaml (
|
|
229
|
+
console.log('Created informer.yaml (declare your data dependencies and roles)');
|
|
202
230
|
}
|
|
203
231
|
|
|
204
232
|
// 10. Add .env to .gitignore if not present
|
|
@@ -207,7 +235,7 @@ INFORMER_API_KEY=your-api-key
|
|
|
207
235
|
console.log('\nSetup complete!\n');
|
|
208
236
|
console.log('Next steps:');
|
|
209
237
|
console.log(' 1. Update .env with your Informer credentials');
|
|
210
|
-
console.log(' 2.
|
|
238
|
+
console.log(' 2. Declare your data dependencies in informer.yaml (see comments inside)');
|
|
211
239
|
console.log(' 3. Run: npm install');
|
|
212
240
|
console.log(' 4. Run: npm run dev');
|
|
213
241
|
console.log('');
|
package/bin/publish.js
ADDED
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { readFile, access } from 'node:fs/promises';
|
|
4
|
+
import { resolve, join } from 'node:path';
|
|
5
|
+
import { collectAppFiles } from '../src/assemble.js';
|
|
6
|
+
import { extractNotes } from '../src/changelog.js';
|
|
7
|
+
import { publish } from '../src/publish.js';
|
|
8
|
+
import { loadEnv, parseModeArg } from '../src/env.js';
|
|
9
|
+
|
|
10
|
+
const mode = parseModeArg(process.argv);
|
|
11
|
+
loadEnv({ mode });
|
|
12
|
+
|
|
13
|
+
// --- Target + auth. You already have API keys sorted — just point these at the
|
|
14
|
+
// License Manager that fronts the marketplace cloud-api. ---
|
|
15
|
+
const marketplaceUrl = process.env.INFORMER_MARKETPLACE_URL;
|
|
16
|
+
const token = process.env.INFORMER_PUBLISH_TOKEN || process.env.INFORMER_API_KEY;
|
|
17
|
+
|
|
18
|
+
if (!marketplaceUrl || !token) {
|
|
19
|
+
console.error('Missing required environment variables.');
|
|
20
|
+
console.error(' INFORMER_MARKETPLACE_URL=https://<license-manager-host>/cloud-api');
|
|
21
|
+
console.error(' INFORMER_PUBLISH_TOKEN=lmpub_<vendor publish key>');
|
|
22
|
+
process.exit(1);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// --- App + listing metadata from package.json "informer" block ---
|
|
26
|
+
const projectRoot = resolve('.');
|
|
27
|
+
let pkg;
|
|
28
|
+
try {
|
|
29
|
+
pkg = JSON.parse(await readFile(join(projectRoot, 'package.json'), 'utf8'));
|
|
30
|
+
} catch {
|
|
31
|
+
console.error('Could not read package.json in current directory.');
|
|
32
|
+
process.exit(1);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const inf = pkg.informer || {};
|
|
36
|
+
const name = inf.name || pkg.name;
|
|
37
|
+
const slug = inf.slug || defaultSlug(name);
|
|
38
|
+
|
|
39
|
+
// Version: prefer the git tag (CI: GITHUB_REF_NAME=vX.Y.Z), then --version, then package.json.
|
|
40
|
+
const version = argValue('--version') || tagVersion(process.env.GITHUB_REF_NAME) || pkg.version;
|
|
41
|
+
|
|
42
|
+
if (!name || !slug || !version) {
|
|
43
|
+
console.error('Need a name, slug, and version. Set package.json "informer.name"/"informer.slug" and tag a release (vX.Y.Z) or pass --version.');
|
|
44
|
+
process.exit(1);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// Release notes: the section for this version, else the cumulative Unreleased block.
|
|
48
|
+
const changeNotes = await extractNotes(join(projectRoot, 'CHANGELOG.md'), version);
|
|
49
|
+
|
|
50
|
+
// Provenance (GitHub Actions). All optional — best-effort audit trail.
|
|
51
|
+
const sourceRepo = process.env.GITHUB_REPOSITORY || undefined;
|
|
52
|
+
const sourceCommit = process.env.GITHUB_SHA || undefined;
|
|
53
|
+
const ciRun = process.env.GITHUB_RUN_ID
|
|
54
|
+
? `${process.env.GITHUB_SERVER_URL || 'https://github.com'}/${process.env.GITHUB_REPOSITORY}/actions/runs/${process.env.GITHUB_RUN_ID}`
|
|
55
|
+
: undefined;
|
|
56
|
+
|
|
57
|
+
// Assemble the same file set a deploy produces.
|
|
58
|
+
let files;
|
|
59
|
+
try {
|
|
60
|
+
files = await collectAppFiles({ distDir: join(projectRoot, 'dist'), projectRoot });
|
|
61
|
+
} catch (err) {
|
|
62
|
+
console.error(err.message);
|
|
63
|
+
process.exit(1);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const icon = await resolveIcon(projectRoot, inf.icon);
|
|
67
|
+
const channel = version.includes('-') ? 'beta' : 'stable';
|
|
68
|
+
|
|
69
|
+
console.log(`Publishing ${name} v${version} (${channel}) — ${files.length} files${changeNotes ? '' : ' — no release notes found'}`);
|
|
70
|
+
|
|
71
|
+
try {
|
|
72
|
+
const result = await publish({
|
|
73
|
+
marketplaceUrl,
|
|
74
|
+
token,
|
|
75
|
+
files,
|
|
76
|
+
icon,
|
|
77
|
+
fields: {
|
|
78
|
+
name,
|
|
79
|
+
slug,
|
|
80
|
+
version,
|
|
81
|
+
shortDescription: inf.shortDescription,
|
|
82
|
+
description: inf.description || pkg.description,
|
|
83
|
+
categories: inf.categories,
|
|
84
|
+
documentationUrl: inf.documentationUrl,
|
|
85
|
+
requires: inf.requires,
|
|
86
|
+
metadata: inf.metadata,
|
|
87
|
+
changeNotes,
|
|
88
|
+
sourceRepo,
|
|
89
|
+
sourceCommit,
|
|
90
|
+
ciRun
|
|
91
|
+
}
|
|
92
|
+
});
|
|
93
|
+
console.log(`Published ${slug} v${version} (${result.channel || channel}).`);
|
|
94
|
+
} catch (err) {
|
|
95
|
+
console.error(err.message);
|
|
96
|
+
process.exit(1);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// --- helpers ---
|
|
100
|
+
|
|
101
|
+
function argValue(flag) {
|
|
102
|
+
const i = process.argv.indexOf(flag);
|
|
103
|
+
return i !== -1 && process.argv[i + 1] ? process.argv[i + 1] : undefined;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function tagVersion(ref) {
|
|
107
|
+
return ref ? ref.replace(/^v/, '') : undefined;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function defaultSlug(value) {
|
|
111
|
+
if (!value) return undefined;
|
|
112
|
+
const base = value.startsWith('@') && value.includes('/') ? value.split('/')[1] : value;
|
|
113
|
+
return base.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, '');
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
async function resolveIcon(root, configured) {
|
|
117
|
+
const candidates = [configured, 'favicon.svg', join('public', 'favicon.svg'), join('dist', 'favicon.svg')].filter(Boolean);
|
|
118
|
+
for (const rel of candidates) {
|
|
119
|
+
const abs = resolve(root, rel);
|
|
120
|
+
try {
|
|
121
|
+
await access(abs);
|
|
122
|
+
return { abs, filename: rel.split(/[/\\]/).pop() };
|
|
123
|
+
} catch {
|
|
124
|
+
// try next candidate
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
return undefined;
|
|
128
|
+
}
|
package/bin/workspace.js
CHANGED
|
@@ -1,22 +1,28 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
|
-
import dotenv from 'dotenv';
|
|
4
3
|
import { readFile } from 'node:fs/promises';
|
|
5
4
|
import { resolve } from 'node:path';
|
|
6
5
|
import { createClient } from '../src/client.js';
|
|
6
|
+
import { loadEnv, envWritePath, parseModeArg } from '../src/env.js';
|
|
7
7
|
import { init, migrate, reset } from '../src/workspace.js';
|
|
8
8
|
|
|
9
|
-
|
|
9
|
+
const mode = parseModeArg(process.argv);
|
|
10
|
+
loadEnv({ mode });
|
|
10
11
|
|
|
11
|
-
|
|
12
|
+
// Strip --mode <value> from argv before parsing the command
|
|
13
|
+
const args = process.argv.slice(2).filter((arg, i, arr) => arg !== '--mode' && arr[i - 1] !== '--mode');
|
|
14
|
+
const command = args[0];
|
|
12
15
|
|
|
13
16
|
if (!command || !['init', 'migrate', 'reset'].includes(command)) {
|
|
14
|
-
console.error('Usage: informer-workspace <init|migrate|reset>');
|
|
17
|
+
console.error('Usage: informer-workspace <init|migrate|reset> [--mode <name>]');
|
|
15
18
|
console.error('');
|
|
16
19
|
console.error('Commands:');
|
|
17
20
|
console.error(' init Create a dev workspace datasource and run migrations');
|
|
18
21
|
console.error(' migrate Run pending migrations against the dev workspace');
|
|
19
22
|
console.error(' reset Drop all tables and re-run all migrations');
|
|
23
|
+
console.error('');
|
|
24
|
+
console.error('Options:');
|
|
25
|
+
console.error(' --mode <name> Load .env.<name> (e.g. --mode test, --mode production)');
|
|
20
26
|
process.exit(1);
|
|
21
27
|
}
|
|
22
28
|
|
|
@@ -33,7 +39,7 @@ if (!baseUrl || (!apiKey && (!user || !pass))) {
|
|
|
33
39
|
|
|
34
40
|
const api = createClient({ baseUrl, apiKey, user, pass });
|
|
35
41
|
const migrationsDir = resolve('migrations');
|
|
36
|
-
const envPath =
|
|
42
|
+
const envPath = envWritePath({ mode });
|
|
37
43
|
|
|
38
44
|
try {
|
|
39
45
|
if (command === 'init') {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@entrinsik/vite-plugin-informer",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.5.0-beta.0",
|
|
4
4
|
"description": "Vite plugin and deploy tool for Informer App development",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -24,10 +24,12 @@
|
|
|
24
24
|
"default": "./src/index.js"
|
|
25
25
|
},
|
|
26
26
|
"./deploy": "./src/deploy.js",
|
|
27
|
+
"./publish": "./src/publish.js",
|
|
27
28
|
"./workspace": "./src/workspace.js"
|
|
28
29
|
},
|
|
29
30
|
"bin": {
|
|
30
31
|
"informer-deploy": "./bin/deploy.js",
|
|
32
|
+
"informer-publish": "./bin/publish.js",
|
|
31
33
|
"informer-init": "./bin/init.js",
|
|
32
34
|
"informer-workspace": "./bin/workspace.js",
|
|
33
35
|
"create-magic-report": "./bin/init.js"
|
package/src/agent-dev.js
CHANGED
|
@@ -2,6 +2,7 @@ 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 { loadDependencies, buildDevContext, loadAppEnv, buildDevCrypto, buildDevMessaging, normalizeFetchPath } from './dev-dependencies.js';
|
|
5
6
|
const parseYaml = yaml.parse;
|
|
6
7
|
|
|
7
8
|
const MAX_STEPS = 20;
|
|
@@ -170,7 +171,12 @@ export function createAgentMiddleware(viteServer, { serverOrigin, authHeader, de
|
|
|
170
171
|
// fetch() — proxies API calls to the Informer server (same as server-routes.js)
|
|
171
172
|
async function apiFetch(path, opts = {}) {
|
|
172
173
|
const method = (opts.method || 'GET').toUpperCase();
|
|
173
|
-
|
|
174
|
+
// Same strict canonicalization as prod (app-sandbox.js normalizeFetchPath).
|
|
175
|
+
const apiPath = normalizeFetchPath(path);
|
|
176
|
+
if (!apiPath) {
|
|
177
|
+
return { status: 400, body: { error: `Invalid fetch path: ${String(path).slice(0, 80)}` } };
|
|
178
|
+
}
|
|
179
|
+
const url = `${serverOrigin}${apiPath}`;
|
|
174
180
|
const fetchOpts = {
|
|
175
181
|
method,
|
|
176
182
|
headers: { Authorization: authHeader, 'Content-Type': 'application/json' }
|
|
@@ -192,6 +198,33 @@ export function createAgentMiddleware(viteServer, { serverOrigin, authHeader, de
|
|
|
192
198
|
return { ok: true };
|
|
193
199
|
}
|
|
194
200
|
|
|
201
|
+
// notify/email — delivery is a console-logged no-op in dev, but required-field
|
|
202
|
+
// validation mirrors prod so a tool that passes here won't 500 in production.
|
|
203
|
+
const { notify, email } = buildDevMessaging('[agent-dev]');
|
|
204
|
+
|
|
205
|
+
// crypto helper — mirrors the prod sandbox crypto surface
|
|
206
|
+
const cryptoHelper = buildDevCrypto();
|
|
207
|
+
|
|
208
|
+
// log helper — mirrors sandbox log(message, data) + log.info/warn/error/debug
|
|
209
|
+
const logCall = (level, message, data) => {
|
|
210
|
+
const msg = typeof message === 'string' ? message : JSON.stringify(message);
|
|
211
|
+
const args = [`[app-log] [${level}] ${msg}`];
|
|
212
|
+
if (data) args.push(data);
|
|
213
|
+
console.log(...args);
|
|
214
|
+
};
|
|
215
|
+
const log = Object.assign(
|
|
216
|
+
(message, data) => logCall('info', message, data),
|
|
217
|
+
{
|
|
218
|
+
debug: (message, data) => logCall('debug', message, data),
|
|
219
|
+
info: (message, data) => logCall('info', message, data),
|
|
220
|
+
warn: (message, data) => logCall('warn', message, data),
|
|
221
|
+
error: (message, data) => logCall('error', message, data)
|
|
222
|
+
}
|
|
223
|
+
);
|
|
224
|
+
|
|
225
|
+
// markdown helper — passthrough in dev (production uses `marked`)
|
|
226
|
+
const markdown = (text) => text;
|
|
227
|
+
|
|
195
228
|
return async function agentDevMiddleware(req, res, next) {
|
|
196
229
|
try {
|
|
197
230
|
const parsed = parseUrl(req.url, true);
|
|
@@ -249,6 +282,14 @@ export function createAgentMiddleware(viteServer, { serverOrigin, authHeader, de
|
|
|
249
282
|
const instructions = agentDef.instructions || '';
|
|
250
283
|
const agentToolNames = agentDef.tools || [];
|
|
251
284
|
|
|
285
|
+
// Build the typed dependency context + env the same way the
|
|
286
|
+
// server-routes dev middleware does, so tools using
|
|
287
|
+
// `context.<slot>.<method>(...)` and `env` work locally and match
|
|
288
|
+
// the prod sandbox bag.
|
|
289
|
+
const deps = await loadDependencies(projectRoot);
|
|
290
|
+
const context = buildDevContext({ deps, apiFetch });
|
|
291
|
+
const env = await loadAppEnv(projectRoot);
|
|
292
|
+
|
|
252
293
|
// Load tool handlers via ssrLoadModule
|
|
253
294
|
const localTools = await scanLocalTools(projectRoot);
|
|
254
295
|
const toolMap = new Map(localTools.map(t => [t.name, t]));
|
|
@@ -366,7 +407,7 @@ export function createAgentMiddleware(viteServer, { serverOrigin, authHeader, de
|
|
|
366
407
|
|
|
367
408
|
if (tool) {
|
|
368
409
|
try {
|
|
369
|
-
result = await tool.handler(tc.input, { query, fetch: apiFetch, emit,
|
|
410
|
+
result = await tool.handler({ args: tc.input, run: { agentName, trigger: triggerEvent }, context, query, fetch: apiFetch, emit, notify, email, crypto: cryptoHelper, markdown, log, env });
|
|
370
411
|
} catch (err) {
|
|
371
412
|
console.error(`[agent-dev] Tool "${tc.name}" failed:`, err.message);
|
|
372
413
|
result = { error: err.message };
|
package/src/assemble.js
ADDED
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import { readdir, stat, access } from 'node:fs/promises';
|
|
2
|
+
import { join, relative, posix } from 'node:path';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* The canonical set of files that make up a deployable Informer app library.
|
|
6
|
+
*
|
|
7
|
+
* This is the SINGLE SOURCE OF TRUTH for what an app "is" on disk, shared by
|
|
8
|
+
* both `informer-deploy` (uploads each file to a live instance) and
|
|
9
|
+
* `informer-publish` (tars the same set for the marketplace). Keeping the
|
|
10
|
+
* enumeration in one place is what guarantees a published artifact is
|
|
11
|
+
* byte-identical to what a normal deploy produces.
|
|
12
|
+
*
|
|
13
|
+
* Layout rule: the Vite build output (`dist/`) lands at the library ROOT
|
|
14
|
+
* (index.html, assets/…), while the source trees keep their directory prefix
|
|
15
|
+
* (server/…, tools/…) — matching how an app's library is structured in Informer.
|
|
16
|
+
*/
|
|
17
|
+
const ROOT_CONFIG_FILES = ['informer.yaml', 'data-access.yaml'];
|
|
18
|
+
const SOURCE_DIRS = ['migrations', 'tools', 'server', 'webhooks'];
|
|
19
|
+
|
|
20
|
+
async function exists(path) {
|
|
21
|
+
try {
|
|
22
|
+
await access(path);
|
|
23
|
+
return true;
|
|
24
|
+
} catch {
|
|
25
|
+
return false;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** Recursively walk a directory, returning absolute file paths (not dirs). */
|
|
30
|
+
async function walkDir(dir) {
|
|
31
|
+
const results = [];
|
|
32
|
+
const items = await readdir(dir);
|
|
33
|
+
for (const item of items) {
|
|
34
|
+
const full = join(dir, item);
|
|
35
|
+
const s = await stat(full);
|
|
36
|
+
if (s.isDirectory()) {
|
|
37
|
+
results.push(...await walkDir(full));
|
|
38
|
+
} else {
|
|
39
|
+
results.push(full);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
return results;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** Normalise a path to forward slashes for use as a library/tar entry name. */
|
|
46
|
+
function toLibraryPath(p) {
|
|
47
|
+
return posix.normalize(p.split('\\').join('/'));
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Collect every file that belongs in the app library, each as { abs, rel },
|
|
52
|
+
* where `rel` is the path it should occupy in the library (and in the tar).
|
|
53
|
+
*
|
|
54
|
+
* @param {{ distDir: string, projectRoot: string }} opts
|
|
55
|
+
* @returns {Promise<Array<{ abs: string, rel: string }>>}
|
|
56
|
+
*/
|
|
57
|
+
export async function collectAppFiles({ distDir, projectRoot }) {
|
|
58
|
+
if (!await exists(distDir)) {
|
|
59
|
+
throw new Error(`Build output not found at "${distDir}" — run the build first.`);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const files = [];
|
|
63
|
+
|
|
64
|
+
// dist/** -> library root
|
|
65
|
+
for (const abs of await walkDir(distDir)) {
|
|
66
|
+
files.push({ abs, rel: toLibraryPath(relative(distDir, abs)) });
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// root config files -> library root
|
|
70
|
+
for (const name of ROOT_CONFIG_FILES) {
|
|
71
|
+
const abs = join(projectRoot, name);
|
|
72
|
+
if (await exists(abs)) files.push({ abs, rel: name });
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// source trees -> keep their prefix (server/…, migrations/…, tools/…, webhooks/…)
|
|
76
|
+
for (const dir of SOURCE_DIRS) {
|
|
77
|
+
const root = join(projectRoot, dir);
|
|
78
|
+
if (!await exists(root)) continue;
|
|
79
|
+
for (const abs of await walkDir(root)) {
|
|
80
|
+
files.push({ abs, rel: toLibraryPath(relative(projectRoot, abs)) });
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
return files;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export { ROOT_CONFIG_FILES, SOURCE_DIRS };
|
package/src/changelog.js
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { readFile } from 'node:fs/promises';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Extract release notes for a version from a Keep-a-Changelog style CHANGELOG.md.
|
|
5
|
+
*
|
|
6
|
+
* Looks for the section header whose name matches `version` (e.g. `## [1.4.0]`,
|
|
7
|
+
* `## 1.4.0`, `## v1.4.0`); if absent, falls back to `## [Unreleased]`. Returns
|
|
8
|
+
* the section body trimmed, or '' if nothing matches / the file is missing.
|
|
9
|
+
*
|
|
10
|
+
* This is what makes the dual notes flow work: betas ship the cumulative
|
|
11
|
+
* `Unreleased` block, while a stable release promotes `Unreleased` to a
|
|
12
|
+
* versioned heading before tagging so its exact section is matched.
|
|
13
|
+
*
|
|
14
|
+
* @param {string} changelogPath
|
|
15
|
+
* @param {string} version
|
|
16
|
+
* @returns {Promise<string>}
|
|
17
|
+
*/
|
|
18
|
+
export async function extractNotes(changelogPath, version) {
|
|
19
|
+
let text;
|
|
20
|
+
try {
|
|
21
|
+
text = await readFile(changelogPath, 'utf8');
|
|
22
|
+
} catch {
|
|
23
|
+
return '';
|
|
24
|
+
}
|
|
25
|
+
return sectionBody(text, version) ?? sectionBody(text, 'Unreleased') ?? '';
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** Body of the first `## [name]` / `## name` section, up to the next `## `. */
|
|
29
|
+
function sectionBody(text, name) {
|
|
30
|
+
const escaped = name.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
31
|
+
const header = new RegExp(`^##\\s+\\[?v?${escaped}\\]?.*$`, 'im');
|
|
32
|
+
const match = header.exec(text);
|
|
33
|
+
if (!match) return null;
|
|
34
|
+
|
|
35
|
+
const after = text.slice(match.index + match[0].length);
|
|
36
|
+
const next = after.search(/^##\s/m);
|
|
37
|
+
const body = (next === -1 ? after : after.slice(0, next)).trim();
|
|
38
|
+
return body || null;
|
|
39
|
+
}
|