@entrinsik/vite-plugin-informer 2.5.0 → 2.6.0-beta.1
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/publish.js +147 -0
- package/index.d.ts +25 -1
- package/package.json +6 -4
- package/src/agent-dev.js +41 -9
- package/src/assemble.js +96 -0
- package/src/changelog.js +39 -0
- package/src/deploy.js +18 -144
- package/src/dev-dependencies.js +243 -13
- package/src/index.js +138 -17
- package/src/openapi-to-dts.js +168 -0
- package/src/publish.js +103 -0
- package/src/server-routes.js +56 -10
package/bin/publish.js
ADDED
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { readFile, access, readdir } 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 screenshots = await resolveScreenshots(projectRoot, inf.screenshots);
|
|
68
|
+
const channel = version.includes('-') ? 'beta' : 'stable';
|
|
69
|
+
|
|
70
|
+
console.log(`Publishing ${name} v${version} (${channel}) — ${files.length} files${screenshots.length ? `, ${screenshots.length} screenshot${screenshots.length === 1 ? '' : 's'}` : ''}${changeNotes ? '' : ' — no release notes found'}`);
|
|
71
|
+
|
|
72
|
+
try {
|
|
73
|
+
const result = await publish({
|
|
74
|
+
marketplaceUrl,
|
|
75
|
+
token,
|
|
76
|
+
files,
|
|
77
|
+
icon,
|
|
78
|
+
screenshots,
|
|
79
|
+
fields: {
|
|
80
|
+
name,
|
|
81
|
+
slug,
|
|
82
|
+
version,
|
|
83
|
+
shortDescription: inf.shortDescription,
|
|
84
|
+
description: inf.description || pkg.description,
|
|
85
|
+
categories: inf.categories,
|
|
86
|
+
documentationUrl: inf.documentationUrl,
|
|
87
|
+
requires: inf.requires,
|
|
88
|
+
metadata: inf.metadata,
|
|
89
|
+
changeNotes,
|
|
90
|
+
sourceRepo,
|
|
91
|
+
sourceCommit,
|
|
92
|
+
ciRun
|
|
93
|
+
}
|
|
94
|
+
});
|
|
95
|
+
console.log(`Published ${slug} v${version} (${result.channel || channel}).`);
|
|
96
|
+
} catch (err) {
|
|
97
|
+
console.error(err.message);
|
|
98
|
+
process.exit(1);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// --- helpers ---
|
|
102
|
+
|
|
103
|
+
function argValue(flag) {
|
|
104
|
+
const i = process.argv.indexOf(flag);
|
|
105
|
+
return i !== -1 && process.argv[i + 1] ? process.argv[i + 1] : undefined;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function tagVersion(ref) {
|
|
109
|
+
return ref ? ref.replace(/^v/, '') : undefined;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function defaultSlug(value) {
|
|
113
|
+
if (!value) return undefined;
|
|
114
|
+
const base = value.startsWith('@') && value.includes('/') ? value.split('/')[1] : value;
|
|
115
|
+
return base.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, '');
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
async function resolveIcon(root, configured) {
|
|
119
|
+
const candidates = [configured, 'favicon.svg', join('public', 'favicon.svg'), join('dist', 'favicon.svg')].filter(Boolean);
|
|
120
|
+
for (const rel of candidates) {
|
|
121
|
+
const abs = resolve(root, rel);
|
|
122
|
+
try {
|
|
123
|
+
await access(abs);
|
|
124
|
+
return { abs, filename: rel.split(/[/\\]/).pop() };
|
|
125
|
+
} catch {
|
|
126
|
+
// try next candidate
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
return undefined;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// Collect listing screenshots from a `screenshots/` directory (override via
|
|
133
|
+
// package.json informer.screenshots), ordered by filename. Returns
|
|
134
|
+
// [{abs, filename}] for the publisher to upload; empty when none exist.
|
|
135
|
+
async function resolveScreenshots(root, configuredDir) {
|
|
136
|
+
const dir = resolve(root, configuredDir || 'screenshots');
|
|
137
|
+
let names;
|
|
138
|
+
try {
|
|
139
|
+
names = await readdir(dir);
|
|
140
|
+
} catch {
|
|
141
|
+
return []; // no screenshots directory — nothing to upload
|
|
142
|
+
}
|
|
143
|
+
return names
|
|
144
|
+
.filter(n => /\.(png|jpe?g|webp|gif)$/i.test(n))
|
|
145
|
+
.sort((a, b) => a.localeCompare(b, undefined, { numeric: true }))
|
|
146
|
+
.map(n => ({ abs: join(dir, n), filename: n }));
|
|
147
|
+
}
|
package/index.d.ts
CHANGED
|
@@ -1,2 +1,26 @@
|
|
|
1
1
|
import type { Plugin } from 'vite';
|
|
2
|
-
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Dev-only binding for a `target: app` dependency slot. Points `request()` at
|
|
5
|
+
* the target app in dev; overrides the manifest `defaultBinding` when both are
|
|
6
|
+
* present.
|
|
7
|
+
*
|
|
8
|
+
* - `app` — the target app (`owner:slug` or UUID). Powers `request()`.
|
|
9
|
+
*
|
|
10
|
+
* A bare string is shorthand for `{ app }`.
|
|
11
|
+
*/
|
|
12
|
+
export interface AppDevBinding {
|
|
13
|
+
app?: string;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export interface InformerPluginOptions {
|
|
17
|
+
mock?: {
|
|
18
|
+
report?: { id?: string; name?: string };
|
|
19
|
+
theme?: 'light' | 'dark';
|
|
20
|
+
roles?: string[];
|
|
21
|
+
};
|
|
22
|
+
devBindings?: Record<string, string | AppDevBinding>;
|
|
23
|
+
proxy?: Record<string, unknown>;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export default function informer(options?: InformerPluginOptions): Plugin;
|
package/package.json
CHANGED
|
@@ -1,15 +1,15 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@entrinsik/vite-plugin-informer",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.6.0-beta.1",
|
|
4
4
|
"description": "Vite plugin and deploy tool for Informer App development",
|
|
5
|
+
"scripts": {
|
|
6
|
+
"test": "node --test"
|
|
7
|
+
},
|
|
5
8
|
"repository": {
|
|
6
9
|
"type": "git",
|
|
7
10
|
"url": "https://github.com/entrinsik-org/i5.git",
|
|
8
11
|
"directory": "packages/vite-plugin-informer"
|
|
9
12
|
},
|
|
10
|
-
"publishConfig": {
|
|
11
|
-
"registry": "https://docker.entrinsik.com/repository/entNPM/"
|
|
12
|
-
},
|
|
13
13
|
"author": "Entrinsik Inc.",
|
|
14
14
|
"license": "UNLICENSED",
|
|
15
15
|
"type": "module",
|
|
@@ -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
|
@@ -7,6 +7,10 @@ const parseYaml = yaml.parse;
|
|
|
7
7
|
|
|
8
8
|
const MAX_STEPS = 20;
|
|
9
9
|
|
|
10
|
+
// Cap dev proxy calls at 30s so a hung upstream fails loudly. (The AI _chat
|
|
11
|
+
// stream below uses its own fetch.)
|
|
12
|
+
const FETCH_TIMEOUT_MS = 30000;
|
|
13
|
+
|
|
10
14
|
/**
|
|
11
15
|
* Read and parse informer.yaml from the project root.
|
|
12
16
|
*
|
|
@@ -145,7 +149,7 @@ async function readSSE(response) {
|
|
|
145
149
|
* @param {Object} opts - Configuration
|
|
146
150
|
* @returns {Function} Connect middleware
|
|
147
151
|
*/
|
|
148
|
-
export function createAgentMiddleware(viteServer, { serverOrigin, authHeader, devWorkspaceId, projectRoot }) {
|
|
152
|
+
export function createAgentMiddleware(viteServer, { serverOrigin, authHeader, devWorkspaceId, projectRoot, devBindings, appToken }) {
|
|
149
153
|
|
|
150
154
|
// query() — proxies to the workspace _sql endpoint (same as server-routes.js)
|
|
151
155
|
async function query(sql, params) {
|
|
@@ -169,29 +173,57 @@ export function createAgentMiddleware(viteServer, { serverOrigin, authHeader, de
|
|
|
169
173
|
}
|
|
170
174
|
|
|
171
175
|
// fetch() — proxies API calls to the Informer server (same as server-routes.js)
|
|
172
|
-
async function
|
|
176
|
+
async function fetchAs(auth, path, opts = {}) {
|
|
173
177
|
const method = (opts.method || 'GET').toUpperCase();
|
|
174
178
|
// Same strict canonicalization as prod (app-sandbox.js normalizeFetchPath).
|
|
175
179
|
const apiPath = normalizeFetchPath(path);
|
|
176
180
|
if (!apiPath) {
|
|
177
|
-
return { status: 400, body: { error: `Invalid fetch path: ${String(path).slice(0, 80)}` } };
|
|
181
|
+
return { status: 400, body: { error: `Invalid fetch path: ${String(path).slice(0, 80)}` }, contentType: 'application/json' };
|
|
178
182
|
}
|
|
179
183
|
const url = `${serverOrigin}${apiPath}`;
|
|
180
184
|
const fetchOpts = {
|
|
181
185
|
method,
|
|
182
|
-
headers: { Authorization:
|
|
186
|
+
headers: { Authorization: auth, 'Content-Type': 'application/json', ...(opts.headers || {}) }
|
|
183
187
|
};
|
|
184
188
|
|
|
185
|
-
if (opts.body && ['POST', 'PUT', 'PATCH'].includes(method)) {
|
|
189
|
+
if (opts.body && ['POST', 'PUT', 'PATCH', 'DELETE'].includes(method)) {
|
|
186
190
|
fetchOpts.body = JSON.stringify(opts.body);
|
|
187
191
|
}
|
|
188
192
|
|
|
189
|
-
|
|
193
|
+
// Read the stream once — `.json()` consumes it, so a `.text()` fallback
|
|
194
|
+
// would throw "Body is unusable" on any non-JSON response. Transport
|
|
195
|
+
// failure/timeout → synthetic 502 so the dep layer names it. See server-routes.js.
|
|
196
|
+
let status, contentType, text;
|
|
197
|
+
try {
|
|
198
|
+
const resp = await globalThis.fetch(url, { ...fetchOpts, signal: AbortSignal.timeout(FETCH_TIMEOUT_MS) });
|
|
199
|
+
status = resp.status;
|
|
200
|
+
contentType = resp.headers.get('content-type') || '';
|
|
201
|
+
text = await resp.text();
|
|
202
|
+
} catch (err) {
|
|
203
|
+
const reason = (err.cause && err.cause.message) || err.message;
|
|
204
|
+
return { status: 502, body: { message: `fetch to ${url} failed: ${reason}` }, contentType: 'application/json' };
|
|
205
|
+
}
|
|
190
206
|
let body;
|
|
191
|
-
try { body =
|
|
192
|
-
return { status
|
|
207
|
+
try { body = JSON.parse(text); } catch { body = text; }
|
|
208
|
+
return { status, body, contentType };
|
|
193
209
|
}
|
|
194
210
|
|
|
211
|
+
async function apiFetch(path, opts = {}) {
|
|
212
|
+
return await fetchAs(authHeader, path, opts);
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
// Cross-app request() hits the target's /view/_/ dispatch, which accepts only
|
|
216
|
+
// the token/session strategies — not basic auth. In API-key mode the
|
|
217
|
+
// INFORMER_API_KEY Bearer is reused; under basic auth INFORMER_APP_TOKEN is
|
|
218
|
+
// required. appFetch stamps x-informer-app-depth:1 for the one-hop guard.
|
|
219
|
+
// See server-routes.js.
|
|
220
|
+
const appAuth = appToken
|
|
221
|
+
? `Bearer ${appToken}`
|
|
222
|
+
: (authHeader && authHeader.startsWith('Bearer ') ? authHeader : null);
|
|
223
|
+
const appFetch = appAuth
|
|
224
|
+
? async (path, opts = {}) => await fetchAs(appAuth, path, { ...opts, headers: { ...(opts.headers || {}), 'x-informer-app-depth': '1' } })
|
|
225
|
+
: null;
|
|
226
|
+
|
|
195
227
|
// emit() — no-op in dev mode (logs to console)
|
|
196
228
|
function emit(event, payload) {
|
|
197
229
|
console.log(`[agent-dev] emit("${event}",`, JSON.stringify(payload), ')');
|
|
@@ -287,7 +319,7 @@ export function createAgentMiddleware(viteServer, { serverOrigin, authHeader, de
|
|
|
287
319
|
// `context.<slot>.<method>(...)` and `env` work locally and match
|
|
288
320
|
// the prod sandbox bag.
|
|
289
321
|
const deps = await loadDependencies(projectRoot);
|
|
290
|
-
const context = buildDevContext({ deps, apiFetch });
|
|
322
|
+
const context = buildDevContext({ deps, apiFetch, devBindings, appFetch });
|
|
291
323
|
const env = await loadAppEnv(projectRoot);
|
|
292
324
|
|
|
293
325
|
// Load tool handlers via ssrLoadModule
|
package/src/assemble.js
ADDED
|
@@ -0,0 +1,96 @@
|
|
|
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', 'lib', 'shared'];
|
|
19
|
+
|
|
20
|
+
// Entries never worth shipping in an app's server-side library: OS/editor
|
|
21
|
+
// dotfiles (.DS_Store, .env), nested dependency trees, and test files. Applied
|
|
22
|
+
// only to the source-tree dirs (lib/, shared/, etc.) — which use generic names
|
|
23
|
+
// and are walked raw from the project root — not to the Vite dist/ output,
|
|
24
|
+
// where a dot-directory (e.g. .well-known/) can be a real asset.
|
|
25
|
+
const isExcludedSourceEntry = (name) =>
|
|
26
|
+
name === 'node_modules' || name.startsWith('.') || name.endsWith('.test.js');
|
|
27
|
+
|
|
28
|
+
async function exists(path) {
|
|
29
|
+
try {
|
|
30
|
+
await access(path);
|
|
31
|
+
return true;
|
|
32
|
+
} catch {
|
|
33
|
+
return false;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** Recursively walk a directory, returning absolute file paths (not dirs). */
|
|
38
|
+
async function walkDir(dir, exclude) {
|
|
39
|
+
const results = [];
|
|
40
|
+
const items = await readdir(dir);
|
|
41
|
+
for (const item of items) {
|
|
42
|
+
if (exclude && exclude(item)) continue;
|
|
43
|
+
const full = join(dir, item);
|
|
44
|
+
const s = await stat(full);
|
|
45
|
+
if (s.isDirectory()) {
|
|
46
|
+
results.push(...await walkDir(full, exclude));
|
|
47
|
+
} else {
|
|
48
|
+
results.push(full);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
return results;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** Normalise a path to forward slashes for use as a library/tar entry name. */
|
|
55
|
+
function toLibraryPath(p) {
|
|
56
|
+
return posix.normalize(p.split('\\').join('/'));
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Collect every file that belongs in the app library, each as { abs, rel },
|
|
61
|
+
* where `rel` is the path it should occupy in the library (and in the tar).
|
|
62
|
+
*
|
|
63
|
+
* @param {{ distDir: string, projectRoot: string }} opts
|
|
64
|
+
* @returns {Promise<Array<{ abs: string, rel: string }>>}
|
|
65
|
+
*/
|
|
66
|
+
export async function collectAppFiles({ distDir, projectRoot }) {
|
|
67
|
+
if (!await exists(distDir)) {
|
|
68
|
+
throw new Error(`Build output not found at "${distDir}" — run the build first.`);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const files = [];
|
|
72
|
+
|
|
73
|
+
// dist/** -> library root
|
|
74
|
+
for (const abs of await walkDir(distDir)) {
|
|
75
|
+
files.push({ abs, rel: toLibraryPath(relative(distDir, abs)) });
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// root config files -> library root
|
|
79
|
+
for (const name of ROOT_CONFIG_FILES) {
|
|
80
|
+
const abs = join(projectRoot, name);
|
|
81
|
+
if (await exists(abs)) files.push({ abs, rel: name });
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// source trees -> keep their prefix (server/…, migrations/…, lib/…, shared/…)
|
|
85
|
+
for (const dir of SOURCE_DIRS) {
|
|
86
|
+
const root = join(projectRoot, dir);
|
|
87
|
+
if (!await exists(root)) continue;
|
|
88
|
+
for (const abs of await walkDir(root, isExcludedSourceEntry)) {
|
|
89
|
+
files.push({ abs, rel: toLibraryPath(relative(projectRoot, abs)) });
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
return files;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
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
|
+
}
|
package/src/deploy.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { createClient } from './client.js';
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
2
|
+
import { collectAppFiles } from './assemble.js';
|
|
3
|
+
import { readFile } from 'node:fs/promises';
|
|
4
|
+
import { basename, dirname } from 'node:path';
|
|
4
5
|
|
|
5
6
|
const TEXT_EXTENSIONS = new Set([
|
|
6
7
|
'.html', '.css', '.js', '.mjs', '.json', '.svg',
|
|
@@ -10,9 +11,6 @@ const TEXT_EXTENSIONS = new Set([
|
|
|
10
11
|
// Files above this size use chunked upload via Flow.js protocol
|
|
11
12
|
const CHUNK_THRESHOLD = 512 * 1024; // 512KB
|
|
12
13
|
|
|
13
|
-
// Config files to upload from project root (if they exist)
|
|
14
|
-
const ROOT_CONFIG_FILES = ['informer.yaml', 'data-access.yaml'];
|
|
15
|
-
|
|
16
14
|
/**
|
|
17
15
|
* Deploy a built Vite project to Informer as an App.
|
|
18
16
|
*
|
|
@@ -106,144 +104,38 @@ export async function deploy({ baseUrl, apiKey, user, pass, distDir, name, descr
|
|
|
106
104
|
console.log('Clearing existing files...');
|
|
107
105
|
await api.post(`${entityPath}/files/_clear`);
|
|
108
106
|
|
|
109
|
-
// 6. Upload dist
|
|
110
|
-
|
|
111
|
-
|
|
107
|
+
// 6. Upload the app-library file set: dist output at the library root, plus
|
|
108
|
+
// informer.yaml / data-access.yaml and the server/tools/migrations/webhooks
|
|
109
|
+
// source trees. Sourced from the shared collectAppFiles() so a deploy and a
|
|
110
|
+
// marketplace publish package byte-identical contents.
|
|
111
|
+
console.log('Uploading files...');
|
|
112
|
+
const files = await collectAppFiles({ distDir, projectRoot: dirname(distDir) });
|
|
112
113
|
|
|
113
|
-
for (const
|
|
114
|
-
const
|
|
115
|
-
const content = await readFile(filePath);
|
|
114
|
+
for (const { abs, rel } of files) {
|
|
115
|
+
const content = await readFile(abs);
|
|
116
116
|
|
|
117
117
|
if (content.length > CHUNK_THRESHOLD) {
|
|
118
118
|
// Large file: chunked upload via Flow.js protocol
|
|
119
119
|
await api.uploadChunked({
|
|
120
120
|
entityPath,
|
|
121
|
-
path:
|
|
121
|
+
path: rel,
|
|
122
122
|
buffer: content,
|
|
123
|
-
filename: basename(
|
|
123
|
+
filename: basename(abs)
|
|
124
124
|
});
|
|
125
|
-
console.log(` ${
|
|
125
|
+
console.log(` ${rel} (${formatSize(content.length)}, chunked)`);
|
|
126
126
|
} else {
|
|
127
127
|
// Small file: direct JSON upload
|
|
128
|
-
const ext = '.' +
|
|
128
|
+
const ext = '.' + rel.split('.').pop();
|
|
129
129
|
const isText = TEXT_EXTENSIONS.has(ext.toLowerCase());
|
|
130
130
|
const payload = isText
|
|
131
131
|
? { content: content.toString('utf8'), encoding: 'utf8' }
|
|
132
132
|
: { content: content.toString('base64'), encoding: 'base64' };
|
|
133
133
|
|
|
134
|
-
await api.put(`${entityPath}/contents/${
|
|
135
|
-
console.log(` ${
|
|
136
|
-
}
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
// 7. Upload config files from project root (informer.yaml, data-access.yaml)
|
|
140
|
-
const projectRoot = dirname(distDir);
|
|
141
|
-
let configCount = 0;
|
|
142
|
-
for (const configFile of ROOT_CONFIG_FILES) {
|
|
143
|
-
const configPath = join(projectRoot, configFile);
|
|
144
|
-
try {
|
|
145
|
-
await access(configPath);
|
|
146
|
-
const content = await readFile(configPath, 'utf8');
|
|
147
|
-
await api.put(`${entityPath}/contents/${configFile}`, {
|
|
148
|
-
content,
|
|
149
|
-
encoding: 'utf8'
|
|
150
|
-
});
|
|
151
|
-
console.log(` ${configFile} (from project root)`);
|
|
152
|
-
configCount++;
|
|
153
|
-
} catch {
|
|
154
|
-
// File doesn't exist, skip
|
|
134
|
+
await api.put(`${entityPath}/contents/${rel}`, payload);
|
|
135
|
+
console.log(` ${rel}`);
|
|
155
136
|
}
|
|
156
137
|
}
|
|
157
138
|
|
|
158
|
-
// 8. Upload migrations/ directory from project root (if it exists)
|
|
159
|
-
const migrationsDir = join(projectRoot, 'migrations');
|
|
160
|
-
let migrationsCount = 0;
|
|
161
|
-
try {
|
|
162
|
-
await access(migrationsDir);
|
|
163
|
-
const migrationFiles = await walkDir(migrationsDir);
|
|
164
|
-
for (const filePath of migrationFiles) {
|
|
165
|
-
const relPath = posix.normalize(relative(projectRoot, filePath).split('\\').join('/'));
|
|
166
|
-
const content = await readFile(filePath, 'utf8');
|
|
167
|
-
await api.put(`${entityPath}/contents/${relPath}`, {
|
|
168
|
-
content,
|
|
169
|
-
encoding: 'utf8'
|
|
170
|
-
});
|
|
171
|
-
console.log(` ${relPath} (from project root)`);
|
|
172
|
-
migrationsCount++;
|
|
173
|
-
}
|
|
174
|
-
} catch {
|
|
175
|
-
// No migrations directory, skip
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
// 9. Upload tools/ directory from project root (if it exists)
|
|
179
|
-
const toolsDir = join(projectRoot, 'tools');
|
|
180
|
-
let toolsCount = 0;
|
|
181
|
-
try {
|
|
182
|
-
await access(toolsDir);
|
|
183
|
-
const toolFiles = await walkDir(toolsDir);
|
|
184
|
-
for (const filePath of toolFiles) {
|
|
185
|
-
const relPath = posix.normalize(relative(projectRoot, filePath).split('\\').join('/'));
|
|
186
|
-
const content = await readFile(filePath);
|
|
187
|
-
const ext = '.' + relPath.split('.').pop();
|
|
188
|
-
const isText = TEXT_EXTENSIONS.has(ext.toLowerCase());
|
|
189
|
-
const payload = isText
|
|
190
|
-
? { content: content.toString('utf8'), encoding: 'utf8' }
|
|
191
|
-
: { content: content.toString('base64'), encoding: 'base64' };
|
|
192
|
-
|
|
193
|
-
await api.put(`${entityPath}/contents/${relPath}`, payload);
|
|
194
|
-
console.log(` ${relPath} (from project root)`);
|
|
195
|
-
toolsCount++;
|
|
196
|
-
}
|
|
197
|
-
} catch {
|
|
198
|
-
// No tools directory, skip
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
// 10. Upload server/ directory from project root (if it exists)
|
|
202
|
-
const serverDir = join(projectRoot, 'server');
|
|
203
|
-
let serverCount = 0;
|
|
204
|
-
try {
|
|
205
|
-
await access(serverDir);
|
|
206
|
-
const serverFiles = await walkDir(serverDir);
|
|
207
|
-
for (const filePath of serverFiles) {
|
|
208
|
-
const relPath = posix.normalize(relative(projectRoot, filePath).split('\\').join('/'));
|
|
209
|
-
const content = await readFile(filePath);
|
|
210
|
-
const ext = '.' + relPath.split('.').pop();
|
|
211
|
-
const isText = TEXT_EXTENSIONS.has(ext.toLowerCase());
|
|
212
|
-
const payload = isText
|
|
213
|
-
? { content: content.toString('utf8'), encoding: 'utf8' }
|
|
214
|
-
: { content: content.toString('base64'), encoding: 'base64' };
|
|
215
|
-
|
|
216
|
-
await api.put(`${entityPath}/contents/${relPath}`, payload);
|
|
217
|
-
console.log(` ${relPath} (from project root)`);
|
|
218
|
-
serverCount++;
|
|
219
|
-
}
|
|
220
|
-
} catch {
|
|
221
|
-
// No server directory, skip
|
|
222
|
-
}
|
|
223
|
-
|
|
224
|
-
// 11. Upload webhooks/ directory from project root (if it exists)
|
|
225
|
-
const webhooksDir = join(projectRoot, 'webhooks');
|
|
226
|
-
let webhooksCount = 0;
|
|
227
|
-
try {
|
|
228
|
-
await access(webhooksDir);
|
|
229
|
-
const webhookFiles = await walkDir(webhooksDir);
|
|
230
|
-
for (const filePath of webhookFiles) {
|
|
231
|
-
const relPath = posix.normalize(relative(projectRoot, filePath).split('\\').join('/'));
|
|
232
|
-
const content = await readFile(filePath);
|
|
233
|
-
const ext = '.' + relPath.split('.').pop();
|
|
234
|
-
const isText = TEXT_EXTENSIONS.has(ext.toLowerCase());
|
|
235
|
-
const payload = isText
|
|
236
|
-
? { content: content.toString('utf8'), encoding: 'utf8' }
|
|
237
|
-
: { content: content.toString('base64'), encoding: 'base64' };
|
|
238
|
-
|
|
239
|
-
await api.put(`${entityPath}/contents/${relPath}`, payload);
|
|
240
|
-
console.log(` ${relPath} (from project root)`);
|
|
241
|
-
webhooksCount++;
|
|
242
|
-
}
|
|
243
|
-
} catch {
|
|
244
|
-
// No webhooks directory, skip
|
|
245
|
-
}
|
|
246
|
-
|
|
247
139
|
// 12. Deploy: run migrations + scan/bundle server routes + webhooks + tools + agents
|
|
248
140
|
if (apiPrefix === 'apps') {
|
|
249
141
|
try {
|
|
@@ -283,7 +175,7 @@ export async function deploy({ baseUrl, apiKey, user, pass, distDir, name, descr
|
|
|
283
175
|
}
|
|
284
176
|
|
|
285
177
|
// 13. Print URL and return UUID for saving
|
|
286
|
-
const totalFiles =
|
|
178
|
+
const totalFiles = files.length;
|
|
287
179
|
const base = baseUrl.replace(/\/+$/, '');
|
|
288
180
|
const entityUrl = apiPrefix === 'apps'
|
|
289
181
|
? `${base}/api/apps/${naturalId}/view`
|
|
@@ -293,24 +185,6 @@ export async function deploy({ baseUrl, apiKey, user, pass, distDir, name, descr
|
|
|
293
185
|
return { id: entity.id, url: entityUrl };
|
|
294
186
|
}
|
|
295
187
|
|
|
296
|
-
/**
|
|
297
|
-
* Recursively walk a directory, returning file paths (not directories).
|
|
298
|
-
*/
|
|
299
|
-
async function walkDir(dir) {
|
|
300
|
-
const results = [];
|
|
301
|
-
const items = await readdir(dir);
|
|
302
|
-
for (const item of items) {
|
|
303
|
-
const full = join(dir, item);
|
|
304
|
-
const s = await stat(full);
|
|
305
|
-
if (s.isDirectory()) {
|
|
306
|
-
results.push(...await walkDir(full));
|
|
307
|
-
} else {
|
|
308
|
-
results.push(full);
|
|
309
|
-
}
|
|
310
|
-
}
|
|
311
|
-
return results;
|
|
312
|
-
}
|
|
313
|
-
|
|
314
188
|
function formatSize(bytes) {
|
|
315
189
|
if (bytes < 1024) return `${bytes} B`;
|
|
316
190
|
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
|