@entrinsik/vite-plugin-informer 1.0.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 +57 -0
- package/bin/init.js +434 -0
- package/package.json +30 -0
- package/src/client.js +114 -0
- package/src/deploy.js +156 -0
- package/src/index.js +88 -0
- package/templates/skill/SKILL.md +388 -0
- package/templates/skill/references/api-reference.md +242 -0
- package/templates/skill/references/report-templates.md +299 -0
package/bin/deploy.js
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import dotenv from 'dotenv';
|
|
4
|
+
import { readFile } from 'node:fs/promises';
|
|
5
|
+
import { resolve } from 'node:path';
|
|
6
|
+
import { deploy } from '../src/deploy.js';
|
|
7
|
+
|
|
8
|
+
dotenv.config();
|
|
9
|
+
|
|
10
|
+
const baseUrl = process.env.INFORMER_URL;
|
|
11
|
+
const apiKey = process.env.INFORMER_API_KEY;
|
|
12
|
+
const user = process.env.INFORMER_USER;
|
|
13
|
+
const pass = process.env.INFORMER_PASS;
|
|
14
|
+
|
|
15
|
+
if (!baseUrl || (!apiKey && (!user || !pass))) {
|
|
16
|
+
console.error('Missing required environment variables.');
|
|
17
|
+
console.error('Create a .env file with:');
|
|
18
|
+
console.error(' INFORMER_URL=http://localhost:3000');
|
|
19
|
+
console.error(' INFORMER_API_KEY=your-api-key');
|
|
20
|
+
console.error('Or use basic auth:');
|
|
21
|
+
console.error(' INFORMER_USER=admin');
|
|
22
|
+
console.error(' INFORMER_PASS=yourpassword');
|
|
23
|
+
process.exit(1);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// Read project metadata from package.json
|
|
27
|
+
let slug;
|
|
28
|
+
let displayName;
|
|
29
|
+
let description;
|
|
30
|
+
try {
|
|
31
|
+
const pkg = JSON.parse(await readFile(resolve('package.json'), 'utf8'));
|
|
32
|
+
slug = pkg.name;
|
|
33
|
+
// Strip scope prefix if present (e.g. @org/my-report -> my-report)
|
|
34
|
+
if (slug && slug.startsWith('@') && slug.includes('/')) {
|
|
35
|
+
slug = slug.split('/')[1];
|
|
36
|
+
}
|
|
37
|
+
// Read display name and description from informer config section
|
|
38
|
+
displayName = pkg.informer?.name;
|
|
39
|
+
description = pkg.informer?.description;
|
|
40
|
+
} catch {
|
|
41
|
+
console.error('Could not read package.json in current directory.');
|
|
42
|
+
process.exit(1);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
if (!slug) {
|
|
46
|
+
console.error('package.json must have a "name" field.');
|
|
47
|
+
process.exit(1);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const distDir = resolve('dist');
|
|
51
|
+
|
|
52
|
+
try {
|
|
53
|
+
await deploy({ baseUrl, apiKey, user, pass, slug, distDir, name: displayName, description });
|
|
54
|
+
} catch (err) {
|
|
55
|
+
console.error('Deploy failed:', err.message);
|
|
56
|
+
process.exit(1);
|
|
57
|
+
}
|
package/bin/init.js
ADDED
|
@@ -0,0 +1,434 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { readFile, writeFile, mkdir, cp, access, readdir } from 'node:fs/promises';
|
|
4
|
+
import { resolve, dirname, join, basename } from 'node:path';
|
|
5
|
+
import { fileURLToPath } from 'node:url';
|
|
6
|
+
import { createInterface } from 'node:readline';
|
|
7
|
+
|
|
8
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
9
|
+
const cwd = process.cwd();
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Generate the default data-access.yaml content with helpful comments
|
|
13
|
+
*/
|
|
14
|
+
function generateDataAccessYaml() {
|
|
15
|
+
return `# Data Access Configuration
|
|
16
|
+
# ========================
|
|
17
|
+
# This file controls which Informer APIs your report can access.
|
|
18
|
+
# Without this file, all API access is blocked (secure by default).
|
|
19
|
+
#
|
|
20
|
+
# Documentation: https://docs.entrinsik.com/informer/magic-reports/data-access
|
|
21
|
+
|
|
22
|
+
# ============================================================================
|
|
23
|
+
# DATASETS
|
|
24
|
+
# ============================================================================
|
|
25
|
+
# Grant access to dataset search and field metadata.
|
|
26
|
+
# Each entry generates:
|
|
27
|
+
# - POST /api/datasets/{id}/_search
|
|
28
|
+
# - GET /api/datasets/{id}/fields
|
|
29
|
+
#
|
|
30
|
+
# Simple access (full dataset):
|
|
31
|
+
# datasets:
|
|
32
|
+
# - admin:sales-data
|
|
33
|
+
# - admin:customers
|
|
34
|
+
#
|
|
35
|
+
# With row-level security (filter injected server-side):
|
|
36
|
+
# datasets:
|
|
37
|
+
# - id: admin:orders
|
|
38
|
+
# filter:
|
|
39
|
+
# region: $user.custom.region # User only sees their region
|
|
40
|
+
# sales_rep: $user.username # User only sees their own records
|
|
41
|
+
#
|
|
42
|
+
datasets: []
|
|
43
|
+
|
|
44
|
+
# ============================================================================
|
|
45
|
+
# QUERIES
|
|
46
|
+
# ============================================================================
|
|
47
|
+
# Grant access to execute saved queries.
|
|
48
|
+
# Each entry generates:
|
|
49
|
+
# - POST /api/queries/{id}/_execute
|
|
50
|
+
#
|
|
51
|
+
# Example:
|
|
52
|
+
# queries:
|
|
53
|
+
# - admin:daily-summary
|
|
54
|
+
# - admin:monthly-report
|
|
55
|
+
#
|
|
56
|
+
queries: []
|
|
57
|
+
|
|
58
|
+
# ============================================================================
|
|
59
|
+
# INTEGRATIONS
|
|
60
|
+
# ============================================================================
|
|
61
|
+
# Grant access to make requests through integrations (Salesforce, REST APIs, etc.)
|
|
62
|
+
# Each entry generates:
|
|
63
|
+
# - POST /api/integrations/{id}/request
|
|
64
|
+
#
|
|
65
|
+
# Simple access:
|
|
66
|
+
# integrations:
|
|
67
|
+
# - salesforce
|
|
68
|
+
# - quickbooks
|
|
69
|
+
#
|
|
70
|
+
# With credential injection (headers/params expanded server-side, never exposed to JS):
|
|
71
|
+
# integrations:
|
|
72
|
+
# - id: partner-api
|
|
73
|
+
# headers:
|
|
74
|
+
# Authorization: Bearer $user.custom.partnerToken
|
|
75
|
+
# X-Client-ID: $tenant.id
|
|
76
|
+
# params:
|
|
77
|
+
# user_id: $user.custom.externalId
|
|
78
|
+
#
|
|
79
|
+
# With path restrictions (only allow specific endpoints):
|
|
80
|
+
# integrations:
|
|
81
|
+
# - id: salesforce
|
|
82
|
+
# paths:
|
|
83
|
+
# - /data/*/query
|
|
84
|
+
# - /data/*/sobjects/*
|
|
85
|
+
#
|
|
86
|
+
integrations: []
|
|
87
|
+
|
|
88
|
+
# ============================================================================
|
|
89
|
+
# DATASOURCES
|
|
90
|
+
# ============================================================================
|
|
91
|
+
# Grant access to run SQL queries against datasources.
|
|
92
|
+
# Each entry generates:
|
|
93
|
+
# - POST /api/datasources/{id}/_query
|
|
94
|
+
#
|
|
95
|
+
# Example:
|
|
96
|
+
# datasources:
|
|
97
|
+
# - postgres-main
|
|
98
|
+
# - mysql-analytics
|
|
99
|
+
#
|
|
100
|
+
datasources: []
|
|
101
|
+
|
|
102
|
+
# ============================================================================
|
|
103
|
+
# LIBRARIES
|
|
104
|
+
# ============================================================================
|
|
105
|
+
# Grant access to read files from other libraries.
|
|
106
|
+
# Each entry generates:
|
|
107
|
+
# - GET /api/libraries/{id}/contents/*
|
|
108
|
+
#
|
|
109
|
+
# Example:
|
|
110
|
+
# libraries:
|
|
111
|
+
# - admin:shared-assets
|
|
112
|
+
# - admin:common-templates
|
|
113
|
+
#
|
|
114
|
+
libraries: []
|
|
115
|
+
|
|
116
|
+
# ============================================================================
|
|
117
|
+
# RAW API ACCESS (Advanced)
|
|
118
|
+
# ============================================================================
|
|
119
|
+
# For edge cases not covered by resource types above.
|
|
120
|
+
# Specify exact method and path.
|
|
121
|
+
#
|
|
122
|
+
# Example:
|
|
123
|
+
# apis:
|
|
124
|
+
# - POST /api/custom/endpoint
|
|
125
|
+
# - GET /api/special/resource
|
|
126
|
+
#
|
|
127
|
+
apis: []
|
|
128
|
+
|
|
129
|
+
# ============================================================================
|
|
130
|
+
# VARIABLE REFERENCE
|
|
131
|
+
# ============================================================================
|
|
132
|
+
# Variables are expanded server-side, keeping sensitive values secure.
|
|
133
|
+
#
|
|
134
|
+
# User variables:
|
|
135
|
+
# $user.username - Login name
|
|
136
|
+
# $user.email - Email address
|
|
137
|
+
# $user.displayName - Full name (e.g., "John Smith")
|
|
138
|
+
# $user.custom.xxx - Custom user field value
|
|
139
|
+
#
|
|
140
|
+
# Tenant variables:
|
|
141
|
+
# $tenant.id - Tenant identifier
|
|
142
|
+
#
|
|
143
|
+
# Report variables:
|
|
144
|
+
# $report.id - Report UUID
|
|
145
|
+
# $report.name - Report display name
|
|
146
|
+
#
|
|
147
|
+
`;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
async function exists(path) {
|
|
151
|
+
try {
|
|
152
|
+
await access(path);
|
|
153
|
+
return true;
|
|
154
|
+
} catch {
|
|
155
|
+
return false;
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Prompt user for input with a default value.
|
|
161
|
+
*/
|
|
162
|
+
function prompt(question, defaultValue) {
|
|
163
|
+
const rl = createInterface({
|
|
164
|
+
input: process.stdin,
|
|
165
|
+
output: process.stdout
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
const displayDefault = defaultValue ? ` (${defaultValue})` : '';
|
|
169
|
+
|
|
170
|
+
return new Promise(resolve => {
|
|
171
|
+
rl.question(`${question}${displayDefault}: `, answer => {
|
|
172
|
+
rl.close();
|
|
173
|
+
resolve(answer.trim() || defaultValue);
|
|
174
|
+
});
|
|
175
|
+
});
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* Convert slug to friendly name.
|
|
180
|
+
* "magic-quickbooks-report" -> "Magic Quickbooks Report"
|
|
181
|
+
*/
|
|
182
|
+
function slugToName(slug) {
|
|
183
|
+
return slug
|
|
184
|
+
.replace(/^@[^/]+\//, '') // Remove scope
|
|
185
|
+
.replace(/[-_]/g, ' ')
|
|
186
|
+
.replace(/\b\w/g, c => c.toUpperCase());
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* Convert name to slug.
|
|
191
|
+
* "Magic Quickbooks Report" -> "magic-quickbooks-report"
|
|
192
|
+
*/
|
|
193
|
+
function nameToSlug(name) {
|
|
194
|
+
return name
|
|
195
|
+
.toLowerCase()
|
|
196
|
+
.replace(/[^a-z0-9]+/g, '-')
|
|
197
|
+
.replace(/^-|-$/g, '');
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
async function init() {
|
|
201
|
+
console.log('Initializing Informer Magic Report project...\n');
|
|
202
|
+
|
|
203
|
+
// 1. Check for package.json
|
|
204
|
+
const pkgPath = resolve(cwd, 'package.json');
|
|
205
|
+
if (!await exists(pkgPath)) {
|
|
206
|
+
console.error('No package.json found. Run this in a Vite project directory.');
|
|
207
|
+
console.error('Create one first with: npm create vite@latest');
|
|
208
|
+
process.exit(1);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
const pkg = JSON.parse(await readFile(pkgPath, 'utf8'));
|
|
212
|
+
|
|
213
|
+
// 2. Check for Vite
|
|
214
|
+
const hasVite = pkg.devDependencies?.vite || pkg.dependencies?.vite;
|
|
215
|
+
if (!hasVite) {
|
|
216
|
+
console.error('Vite not found in dependencies.');
|
|
217
|
+
console.error('Create a Vite project first: npm create vite@latest');
|
|
218
|
+
process.exit(1);
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
// 3. Get report info from user
|
|
222
|
+
const defaultSlug = (pkg.name || basename(cwd)).replace(/^@[^/]+\//, '');
|
|
223
|
+
const existingName = pkg.informer?.name;
|
|
224
|
+
const defaultName = existingName || slugToName(defaultSlug);
|
|
225
|
+
|
|
226
|
+
console.log('Configure your Magic Report:\n');
|
|
227
|
+
|
|
228
|
+
const reportName = await prompt('Report name', defaultName);
|
|
229
|
+
const reportSlug = await prompt('Report slug', nameToSlug(reportName) || defaultSlug);
|
|
230
|
+
|
|
231
|
+
console.log('');
|
|
232
|
+
|
|
233
|
+
// 4. Update package.json
|
|
234
|
+
if (!pkg.informer) pkg.informer = {};
|
|
235
|
+
pkg.informer.name = reportName;
|
|
236
|
+
|
|
237
|
+
// Update package name to match slug if it was the default
|
|
238
|
+
if (pkg.name === defaultSlug || !pkg.name) {
|
|
239
|
+
pkg.name = reportSlug;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
// 5. Add plugin to dependencies if not present
|
|
243
|
+
if (!pkg.devDependencies) pkg.devDependencies = {};
|
|
244
|
+
if (!pkg.devDependencies['@entrinsik/vite-plugin-informer']) {
|
|
245
|
+
pkg.devDependencies['@entrinsik/vite-plugin-informer'] = '^1.0.0';
|
|
246
|
+
console.log('Added @entrinsik/vite-plugin-informer to devDependencies');
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
// 6. Add deploy script if not present
|
|
250
|
+
if (!pkg.scripts) pkg.scripts = {};
|
|
251
|
+
if (!pkg.scripts.deploy) {
|
|
252
|
+
pkg.scripts.deploy = 'npm run build && informer-deploy';
|
|
253
|
+
console.log('Added "deploy" script to package.json');
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
await writeFile(pkgPath, JSON.stringify(pkg, null, 2) + '\n');
|
|
257
|
+
console.log(`Configured report: "${reportName}" (slug: ${reportSlug})`);
|
|
258
|
+
|
|
259
|
+
// 7. Update vite.config
|
|
260
|
+
await updateViteConfig();
|
|
261
|
+
|
|
262
|
+
// 8. Create .env.example
|
|
263
|
+
const envExample = `# Informer connection settings
|
|
264
|
+
INFORMER_URL=http://localhost:3000
|
|
265
|
+
|
|
266
|
+
# Option 1: API Key (recommended)
|
|
267
|
+
INFORMER_API_KEY=your-api-key
|
|
268
|
+
|
|
269
|
+
# Option 2: Basic auth
|
|
270
|
+
# INFORMER_USER=admin
|
|
271
|
+
# INFORMER_PASS=yourpassword
|
|
272
|
+
|
|
273
|
+
# Optional: defaults to current user
|
|
274
|
+
# INFORMER_OWNER=admin
|
|
275
|
+
`;
|
|
276
|
+
|
|
277
|
+
const envExamplePath = resolve(cwd, '.env.example');
|
|
278
|
+
if (!await exists(envExamplePath)) {
|
|
279
|
+
await writeFile(envExamplePath, envExample);
|
|
280
|
+
console.log('Created .env.example');
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
// Also create .env if it doesn't exist
|
|
284
|
+
const envPath = resolve(cwd, '.env');
|
|
285
|
+
if (!await exists(envPath)) {
|
|
286
|
+
await writeFile(envPath, envExample);
|
|
287
|
+
console.log('Created .env (update with your credentials)');
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
// 9. Create data-access.yaml if it doesn't exist
|
|
291
|
+
const dataAccessPath = resolve(cwd, 'data-access.yaml');
|
|
292
|
+
if (!await exists(dataAccessPath)) {
|
|
293
|
+
await writeFile(dataAccessPath, generateDataAccessYaml());
|
|
294
|
+
console.log('Created data-access.yaml (configure API access for your report)');
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
// 10. Copy Claude skill files
|
|
298
|
+
await copySkillFiles();
|
|
299
|
+
|
|
300
|
+
// 11. Add .env to .gitignore if not present
|
|
301
|
+
await updateGitignore();
|
|
302
|
+
|
|
303
|
+
console.log('\nSetup complete!\n');
|
|
304
|
+
console.log('Next steps:');
|
|
305
|
+
console.log(' 1. Update .env with your Informer credentials');
|
|
306
|
+
console.log(' 2. Update data-access.yaml with the datasets/APIs your report needs');
|
|
307
|
+
console.log(' 3. Run: npm install');
|
|
308
|
+
console.log(' 4. Run: npm run dev');
|
|
309
|
+
console.log(' 5. Open a terminal and run: claude');
|
|
310
|
+
console.log(' The informer skill is available in .claude/skills/');
|
|
311
|
+
console.log('');
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
async function updateViteConfig() {
|
|
315
|
+
// Find vite config file
|
|
316
|
+
const configFiles = ['vite.config.js', 'vite.config.ts', 'vite.config.mjs', 'vite.config.mts'];
|
|
317
|
+
let configPath = null;
|
|
318
|
+
let configContent = null;
|
|
319
|
+
|
|
320
|
+
for (const file of configFiles) {
|
|
321
|
+
const path = resolve(cwd, file);
|
|
322
|
+
if (await exists(path)) {
|
|
323
|
+
configPath = path;
|
|
324
|
+
configContent = await readFile(path, 'utf8');
|
|
325
|
+
break;
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
if (!configPath) {
|
|
330
|
+
// Create a basic vite.config.js
|
|
331
|
+
configPath = resolve(cwd, 'vite.config.js');
|
|
332
|
+
configContent = `import { defineConfig } from 'vite';
|
|
333
|
+
import informer from '@entrinsik/vite-plugin-informer';
|
|
334
|
+
|
|
335
|
+
export default defineConfig({
|
|
336
|
+
plugins: [informer()]
|
|
337
|
+
});
|
|
338
|
+
`;
|
|
339
|
+
await writeFile(configPath, configContent);
|
|
340
|
+
console.log('Created vite.config.js with informer plugin');
|
|
341
|
+
return;
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
// Check if plugin is already added
|
|
345
|
+
if (configContent.includes('vite-plugin-informer') || configContent.includes("informer()")) {
|
|
346
|
+
console.log('Informer plugin already in vite config');
|
|
347
|
+
return;
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
// Add import and plugin to existing config
|
|
351
|
+
const importLine = "import informer from '@entrinsik/vite-plugin-informer';\n";
|
|
352
|
+
|
|
353
|
+
// Add import after other imports
|
|
354
|
+
const lastImportMatch = configContent.match(/^import .+$/gm);
|
|
355
|
+
if (lastImportMatch) {
|
|
356
|
+
const lastImport = lastImportMatch[lastImportMatch.length - 1];
|
|
357
|
+
const lastImportIndex = configContent.lastIndexOf(lastImport) + lastImport.length;
|
|
358
|
+
configContent = configContent.slice(0, lastImportIndex) + '\n' + importLine + configContent.slice(lastImportIndex + 1);
|
|
359
|
+
} else {
|
|
360
|
+
configContent = importLine + configContent;
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
// Add to plugins array
|
|
364
|
+
const pluginsMatch = configContent.match(/plugins\s*:\s*\[/);
|
|
365
|
+
if (pluginsMatch) {
|
|
366
|
+
const insertIndex = pluginsMatch.index + pluginsMatch[0].length;
|
|
367
|
+
configContent = configContent.slice(0, insertIndex) + '\n informer(),' + configContent.slice(insertIndex);
|
|
368
|
+
} else {
|
|
369
|
+
// No plugins array, need to add one - this is trickier
|
|
370
|
+
// For now, warn the user
|
|
371
|
+
console.log('Could not automatically add plugin to vite config.');
|
|
372
|
+
console.log('Please add manually:');
|
|
373
|
+
console.log(" import informer from '@entrinsik/vite-plugin-informer';");
|
|
374
|
+
console.log(' // Then add informer() to plugins array');
|
|
375
|
+
return;
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
await writeFile(configPath, configContent);
|
|
379
|
+
console.log(`Updated ${configPath.split('/').pop()} with informer plugin`);
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
async function copySkillFiles() {
|
|
383
|
+
const skillSrc = resolve(__dirname, '..', 'templates', 'skill');
|
|
384
|
+
const skillDest = resolve(cwd, '.claude', 'skills', 'informer');
|
|
385
|
+
|
|
386
|
+
// Create destination directory
|
|
387
|
+
await mkdir(skillDest, { recursive: true });
|
|
388
|
+
|
|
389
|
+
// Copy files recursively
|
|
390
|
+
await copyDir(skillSrc, skillDest);
|
|
391
|
+
console.log('Copied Claude skill files to .claude/skills/informer/');
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
async function copyDir(src, dest) {
|
|
395
|
+
const entries = await readdir(src, { withFileTypes: true });
|
|
396
|
+
|
|
397
|
+
for (const entry of entries) {
|
|
398
|
+
const srcPath = join(src, entry.name);
|
|
399
|
+
const destPath = join(dest, entry.name);
|
|
400
|
+
|
|
401
|
+
if (entry.isDirectory()) {
|
|
402
|
+
await mkdir(destPath, { recursive: true });
|
|
403
|
+
await copyDir(srcPath, destPath);
|
|
404
|
+
} else {
|
|
405
|
+
await cp(srcPath, destPath);
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
async function updateGitignore() {
|
|
411
|
+
const gitignorePath = resolve(cwd, '.gitignore');
|
|
412
|
+
let content = '';
|
|
413
|
+
|
|
414
|
+
if (await exists(gitignorePath)) {
|
|
415
|
+
content = await readFile(gitignorePath, 'utf8');
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
const additions = [];
|
|
419
|
+
|
|
420
|
+
if (!content.includes('.env')) {
|
|
421
|
+
additions.push('.env');
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
if (additions.length > 0) {
|
|
425
|
+
const newContent = content.trimEnd() + '\n\n# Informer\n' + additions.join('\n') + '\n';
|
|
426
|
+
await writeFile(gitignorePath, newContent);
|
|
427
|
+
console.log('Updated .gitignore');
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
init().catch(err => {
|
|
432
|
+
console.error('Init failed:', err.message);
|
|
433
|
+
process.exit(1);
|
|
434
|
+
});
|
package/package.json
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@entrinsik/vite-plugin-informer",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Vite plugin and publish tool for local Magic Report development",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./src/index.js",
|
|
7
|
+
"exports": {
|
|
8
|
+
".": "./src/index.js",
|
|
9
|
+
"./deploy": "./src/deploy.js"
|
|
10
|
+
},
|
|
11
|
+
"bin": {
|
|
12
|
+
"informer-deploy": "./bin/deploy.js",
|
|
13
|
+
"informer-init": "./bin/init.js"
|
|
14
|
+
},
|
|
15
|
+
"files": [
|
|
16
|
+
"src",
|
|
17
|
+
"bin",
|
|
18
|
+
"templates"
|
|
19
|
+
],
|
|
20
|
+
"peerDependencies": {
|
|
21
|
+
"vite": ">=5.0.0"
|
|
22
|
+
},
|
|
23
|
+
"dependencies": {
|
|
24
|
+
"dotenv": "^16.4.0"
|
|
25
|
+
},
|
|
26
|
+
"engines": {
|
|
27
|
+
"node": ">=18.0.0"
|
|
28
|
+
},
|
|
29
|
+
"license": "UNLICENSED"
|
|
30
|
+
}
|
package/src/client.js
ADDED
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Informer API client using native fetch with Basic auth.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
class InformerApiError extends Error {
|
|
6
|
+
constructor(status, statusText, body, url) {
|
|
7
|
+
super(`Informer API ${status} ${statusText}${url ? ` (${url})` : ''}`);
|
|
8
|
+
this.name = 'InformerApiError';
|
|
9
|
+
this.status = status;
|
|
10
|
+
this.statusText = statusText;
|
|
11
|
+
this.body = body;
|
|
12
|
+
this.url = url;
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export { InformerApiError };
|
|
17
|
+
|
|
18
|
+
const CHUNK_SIZE = 1024 * 1024; // 1MB
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Create an authenticated Informer API client.
|
|
22
|
+
* @param {{ baseUrl: string, apiKey?: string, user?: string, pass?: string }} options
|
|
23
|
+
* @returns {{ get, post, put, del, uploadChunked }}
|
|
24
|
+
*/
|
|
25
|
+
export function createClient({ baseUrl, apiKey, user, pass }) {
|
|
26
|
+
const auth = apiKey
|
|
27
|
+
? 'Bearer ' + apiKey
|
|
28
|
+
: 'Basic ' + Buffer.from(`${user}:${pass}`).toString('base64');
|
|
29
|
+
const origin = baseUrl.replace(/\/+$/, '');
|
|
30
|
+
|
|
31
|
+
async function request(method, path, body) {
|
|
32
|
+
const url = `${origin}/api/${path}`;
|
|
33
|
+
const headers = {
|
|
34
|
+
Authorization: auth,
|
|
35
|
+
Accept: 'application/json'
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
const opts = { method, headers };
|
|
39
|
+
|
|
40
|
+
if (body !== undefined) {
|
|
41
|
+
headers['Content-Type'] = 'application/json';
|
|
42
|
+
opts.body = JSON.stringify(body);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const res = await fetch(url, opts);
|
|
46
|
+
|
|
47
|
+
if (res.status === 404) return null;
|
|
48
|
+
|
|
49
|
+
if (!res.ok) {
|
|
50
|
+
let text;
|
|
51
|
+
try { text = await res.text(); } catch { text = ''; }
|
|
52
|
+
throw new InformerApiError(res.status, res.statusText, text, url);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const ct = res.headers.get('content-type') || '';
|
|
56
|
+
if (ct.includes('application/json') || ct.includes('hal+json')) {
|
|
57
|
+
return res.json();
|
|
58
|
+
}
|
|
59
|
+
return res.text();
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Upload a file using chunked Flow.js protocol, then assemble into a report's library.
|
|
64
|
+
* @param {{ reportId: string, path: string, buffer: Buffer, filename: string }} opts
|
|
65
|
+
*/
|
|
66
|
+
async function uploadChunked({ reportId, path, buffer, filename }) {
|
|
67
|
+
const totalChunks = Math.ceil(buffer.length / CHUNK_SIZE);
|
|
68
|
+
const uploadId = `publish-${Date.now()}-${Math.random().toString(36).slice(2, 9)}`;
|
|
69
|
+
|
|
70
|
+
// Upload each chunk via Flow.js protocol
|
|
71
|
+
for (let i = 1; i <= totalChunks; i++) {
|
|
72
|
+
const start = (i - 1) * CHUNK_SIZE;
|
|
73
|
+
const end = Math.min(start + CHUNK_SIZE, buffer.length);
|
|
74
|
+
const chunk = buffer.slice(start, end);
|
|
75
|
+
|
|
76
|
+
const formData = new FormData();
|
|
77
|
+
formData.append('file', new Blob([chunk]), filename);
|
|
78
|
+
formData.append('flowChunkNumber', String(i));
|
|
79
|
+
formData.append('flowChunkSize', String(CHUNK_SIZE));
|
|
80
|
+
formData.append('flowCurrentChunkSize', String(chunk.length));
|
|
81
|
+
formData.append('flowFilename', filename);
|
|
82
|
+
formData.append('flowIdentifier', uploadId);
|
|
83
|
+
formData.append('flowRelativePath', filename);
|
|
84
|
+
formData.append('flowTotalChunks', String(totalChunks));
|
|
85
|
+
formData.append('flowTotalSize', String(buffer.length));
|
|
86
|
+
|
|
87
|
+
const res = await fetch(`${origin}/api/upload/flow`, {
|
|
88
|
+
method: 'POST',
|
|
89
|
+
headers: { Authorization: auth },
|
|
90
|
+
body: formData
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
if (!res.ok) {
|
|
94
|
+
let text;
|
|
95
|
+
try { text = await res.text(); } catch { text = ''; }
|
|
96
|
+
throw new InformerApiError(res.status, res.statusText, text, `${origin}/api/upload/flow`);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// Assemble chunks into a file in the report's library at the specified path
|
|
101
|
+
await request('POST', `reports/${reportId}/_upload`, {
|
|
102
|
+
uploadId,
|
|
103
|
+
path
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
return {
|
|
108
|
+
get: (path) => request('GET', path),
|
|
109
|
+
post: (path, body) => request('POST', path, body),
|
|
110
|
+
put: (path, body) => request('PUT', path, body),
|
|
111
|
+
del: (path) => request('DELETE', path),
|
|
112
|
+
uploadChunked
|
|
113
|
+
};
|
|
114
|
+
}
|