infaira-canvas 0.1.9

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.
@@ -0,0 +1,647 @@
1
+ import fs from 'fs';
2
+ import path from 'path';
3
+ import crypto from 'crypto';
4
+ import https from 'https';
5
+ import http from 'http';
6
+ import { fileURLToPath } from 'url';
7
+ // ─── Remote fetch ─────────────────────────────────────────────────────────────
8
+ const ICAN_TYPES_URL = 'https://infaira.ai/dist/client/ican.d.ts';
9
+ function fetchText(url) {
10
+ return new Promise((resolve, reject) => {
11
+ const client = url.startsWith('https') ? https : http;
12
+ client.get(url, (res) => {
13
+ if (res.statusCode && res.statusCode >= 300 && res.statusCode < 400 && res.headers.location) {
14
+ fetchText(res.headers.location).then(resolve).catch(reject);
15
+ return;
16
+ }
17
+ if (res.statusCode !== 200) {
18
+ reject(new Error(`HTTP ${res.statusCode ?? 'unknown'} fetching ${url}`));
19
+ return;
20
+ }
21
+ const chunks = [];
22
+ res.on('data', (chunk) => chunks.push(chunk));
23
+ res.on('end', () => resolve(Buffer.concat(chunks).toString('utf-8')));
24
+ res.on('error', reject);
25
+ }).on('error', reject);
26
+ });
27
+ }
28
+ // ─── Helpers ──────────────────────────────────────────────────────────────────
29
+ const __filename = fileURLToPath(import.meta.url);
30
+ const __dirname = path.dirname(__filename);
31
+ function toKebabCase(name) {
32
+ return name
33
+ .trim()
34
+ // Insert hyphen before an uppercase letter that follows a lowercase letter or digit
35
+ // e.g. "SampleWidget" → "Sample-Widget", "myWidget" → "my-Widget"
36
+ .replace(/([a-z0-9])([A-Z])/g, '$1-$2')
37
+ // Insert hyphen between a run of uppercase letters and the start of a new word
38
+ // e.g. "XMLParser" → "XML-Parser"
39
+ .replace(/([A-Z]+)([A-Z][a-z])/g, '$1-$2')
40
+ .toLowerCase()
41
+ .replace(/[^a-z0-9]+/g, '-')
42
+ .replace(/^-+|-+$/g, '');
43
+ }
44
+ function toPascalCase(name) {
45
+ return toKebabCase(name)
46
+ .split('-')
47
+ .map((part) => part.charAt(0).toUpperCase() + part.slice(1))
48
+ .join('');
49
+ }
50
+ function toTitleCase(name) {
51
+ return toKebabCase(name)
52
+ .split('-')
53
+ .map((part) => part.charAt(0).toUpperCase() + part.slice(1))
54
+ .join(' ');
55
+ }
56
+ function writeFile(dir, relPath, content) {
57
+ const fullPath = path.join(dir, relPath);
58
+ const parentDir = path.dirname(fullPath);
59
+ try {
60
+ if (!fs.existsSync(parentDir)) {
61
+ fs.mkdirSync(parentDir, { recursive: true });
62
+ }
63
+ fs.writeFileSync(fullPath, content, 'utf-8');
64
+ console.log(` Writing ${relPath}`);
65
+ }
66
+ catch (err) {
67
+ const msg = err instanceof Error ? err.message : String(err);
68
+ console.error(` Error writing ${relPath}: ${msg}`);
69
+ process.exit(1);
70
+ }
71
+ }
72
+ function copyTemplate(targetDir, templateRelPath, destRelPath) {
73
+ const templatePath = path.join(__dirname, '..', '..', 'templates', templateRelPath);
74
+ const destPath = path.join(targetDir, destRelPath);
75
+ const parentDir = path.dirname(destPath);
76
+ try {
77
+ if (!fs.existsSync(parentDir)) {
78
+ fs.mkdirSync(parentDir, { recursive: true });
79
+ }
80
+ fs.copyFileSync(templatePath, destPath);
81
+ console.log(` Copying ${destRelPath}`);
82
+ }
83
+ catch (err) {
84
+ const msg = err instanceof Error ? err.message : String(err);
85
+ console.error(` Error copying ${destRelPath}: ${msg}`);
86
+ process.exit(1);
87
+ }
88
+ }
89
+ // ─── Template generators ──────────────────────────────────────────────────────
90
+ function makeBundleJson(bundleId, widgetId, widgetName) {
91
+ return JSON.stringify({
92
+ id: bundleId,
93
+ name: widgetName,
94
+ version: '1.0.0',
95
+ author: '',
96
+ widgets: [
97
+ {
98
+ id: widgetId,
99
+ name: widgetName,
100
+ description: 'A sample widget',
101
+ icon: '',
102
+ tags: [],
103
+ category: '',
104
+ isTemplate: false,
105
+ },
106
+ ],
107
+ sidebarLinks: [],
108
+ uis: [],
109
+ menuItems: [],
110
+ }, null, 2);
111
+ }
112
+ function makeSampleBundleJson(bundleId, widgetId, widgetName) {
113
+ // Valid JSON — no comments allowed in JSON
114
+ return JSON.stringify({
115
+ id: bundleId,
116
+ author: '',
117
+ widgets: [
118
+ {
119
+ id: widgetId,
120
+ name: widgetName,
121
+ description: 'A sample widget',
122
+ icon: '',
123
+ tags: [],
124
+ category: '',
125
+ isTemplate: false,
126
+ },
127
+ ],
128
+ sidebarLinks: [
129
+ {
130
+ id: `${widgetId}-link`,
131
+ label: '',
132
+ description: '',
133
+ target: '',
134
+ icon: '',
135
+ roles: [],
136
+ },
137
+ ],
138
+ uis: [
139
+ {
140
+ id: `${widgetId}-ui`,
141
+ label: '',
142
+ description: '',
143
+ roles: [],
144
+ },
145
+ ],
146
+ menuItems: [
147
+ {
148
+ id: `${widgetId}-menu`,
149
+ title: widgetName,
150
+ },
151
+ ],
152
+ }, null, 2);
153
+ }
154
+ function makeSampleLocalizationJson() {
155
+ return JSON.stringify({
156
+ 'ican.widget-title': {
157
+ en: 'Widget Title',
158
+ ar: '<arabic translation>',
159
+ },
160
+ }, null, 2);
161
+ }
162
+ function makeIndexTsx(componentName, widgetName, widgetId) {
163
+ return `import * as React from 'react';
164
+ import { registerWidget } from './ican';
165
+ import type { IContextProvider } from './ican';
166
+ import './styles.scss';
167
+
168
+ export interface IWidgetProps {
169
+ icanContext?: IContextProvider;
170
+ instanceId?: string;
171
+ [key: string]: unknown;
172
+ }
173
+
174
+ const ${componentName}: React.FC<IWidgetProps> = ({ icanContext, instanceId: _instanceId }) => {
175
+ const [data, setData] = React.useState<unknown>(null);
176
+ const [loading, setLoading] = React.useState(true);
177
+ const [error, setError] = React.useState<string | null>(null);
178
+
179
+ React.useEffect(() => {
180
+ if (!icanContext) {
181
+ setLoading(false);
182
+ return;
183
+ }
184
+ // Example: fetch data from Orch
185
+ // icanContext.executeAction('MyModel', 'GetAll', {}, { json: true })
186
+ // .then((result) => { setData(result); setLoading(false); })
187
+ // .catch((err: unknown) => { setError(String(err)); setLoading(false); });
188
+ setData('Hello from ${widgetName}!');
189
+ setLoading(false);
190
+ }, [icanContext]);
191
+
192
+ if (loading) {
193
+ return (
194
+ <div className="widget-loading">
195
+ <div className="widget-spinner" />
196
+ </div>
197
+ );
198
+ }
199
+
200
+ if (error) {
201
+ return (
202
+ <div className="widget-error">
203
+ <p className="widget-error-message">{error}</p>
204
+ </div>
205
+ );
206
+ }
207
+
208
+ return (
209
+ <div className="widget-root">
210
+ <p>{String(data)}</p>
211
+ </div>
212
+ );
213
+ };
214
+
215
+ registerWidget({
216
+ id: '${widgetId}',
217
+ widget: ${componentName},
218
+ configs: {
219
+ layout: {
220
+ // w: 10,
221
+ // h: 8,
222
+ // minW: 4,
223
+ // minH: 4,
224
+ },
225
+ },
226
+ });
227
+ `;
228
+ }
229
+ function makeStylesScss() {
230
+ return `.widget-root {
231
+ width: 100%;
232
+ height: 100%;
233
+ box-sizing: border-box;
234
+ font-family: 'Comfortaa', 'Inter', system-ui, sans-serif;
235
+ color: #FFFFFF;
236
+ padding: 20px;
237
+
238
+ * { box-sizing: border-box; }
239
+ }
240
+
241
+ .widget-loading {
242
+ width: 100%;
243
+ height: 100%;
244
+ display: flex;
245
+ align-items: center;
246
+ justify-content: center;
247
+ }
248
+
249
+ .widget-spinner {
250
+ width: 32px;
251
+ height: 32px;
252
+ border: 3px solid rgba(255, 255, 255, 0.15);
253
+ border-top-color: rgba(255, 255, 255, 0.7);
254
+ border-radius: 50%;
255
+ animation: spin 0.8s linear infinite;
256
+ }
257
+
258
+ .widget-error {
259
+ width: 100%;
260
+ height: 100%;
261
+ display: flex;
262
+ align-items: center;
263
+ justify-content: center;
264
+ padding: 20px;
265
+ }
266
+
267
+ .widget-error-message {
268
+ font-family: 'Comfortaa', sans-serif;
269
+ font-size: 0.85rem;
270
+ color: rgba(249, 115, 96, 0.9);
271
+ text-align: center;
272
+ background: rgba(249, 115, 96, 0.08);
273
+ border: 1px solid rgba(249, 115, 96, 0.25);
274
+ border-radius: 10px;
275
+ padding: 14px 20px;
276
+ }
277
+
278
+ @keyframes spin {
279
+ to { transform: rotate(360deg); }
280
+ }
281
+ `;
282
+ }
283
+ function makeIcanTs() {
284
+ return `/* eslint-disable @typescript-eslint/no-explicit-any */
285
+ import BundleConfig from '../bundle.json';
286
+ import LocalizationMessages from '../localization.json';
287
+
288
+ // ─── Types ────────────────────────────────────────────────────────────────────
289
+
290
+ export interface IActionOptions {
291
+ json?: boolean;
292
+ key?: string;
293
+ cancelPrevious?: boolean;
294
+ executeImmediately?: boolean;
295
+ }
296
+
297
+ export interface IContextProvider {
298
+ environment: 'dev' | 'prod';
299
+ orchUrl?: string;
300
+ userKey: string;
301
+ root: string;
302
+ scriptFiles?: string[];
303
+ executeAction(model: string, action: string, parameters: unknown, options?: IActionOptions): Promise<unknown>;
304
+ executeService(app: string, service: string, parameters: unknown, options?: IActionOptions): Promise<unknown>;
305
+ fireEvent(eventId: string): Promise<void>;
306
+ hasAppRole(app: string, role: string): boolean;
307
+ themeName?: string;
308
+ themeType?: 'Light' | 'Dark' | 'Glass-Dark' | 'Glass-Light';
309
+ language: string;
310
+ $L(code: string, params?: Record<string, string>): string;
311
+ }
312
+
313
+ export interface ILayout {
314
+ w?: number; h?: number; minW?: number; minH?: number;
315
+ maxW?: number; maxH?: number; isDraggable?: boolean; isResizable?: boolean; static?: boolean;
316
+ }
317
+
318
+ export interface IWidgetPropConfig {
319
+ name: string; label: string;
320
+ type: 'text' | 'string' | 'password' | 'number' | 'email' | 'checkbox' | 'toggle' | 'select' | 'date' | 'time' | 'json';
321
+ value?: string | number | boolean;
322
+ placeholder?: string;
323
+ options?: Array<{ label: string; value: string }>;
324
+ validate?: { required?: boolean; minLength?: number; maxLength?: number; minVal?: number; maxVal?: number; };
325
+ }
326
+
327
+ export interface IWidgetObject {
328
+ id: string;
329
+ widget: React.ComponentType<any>;
330
+ configs?: { layout?: ILayout; props?: IWidgetPropConfig[]; preLoader?: string; };
331
+ defaultProps?: Record<string, unknown>;
332
+ isTemplate?: boolean;
333
+ }
334
+
335
+ export interface ISidebarLink { id: string; click?: () => void; component?: unknown; link?: string; }
336
+ export interface IRenderUIItemProps { id: string; component: unknown; uiProps?: unknown; showDefaultHeader?: boolean; }
337
+ export interface IMenuItem { id: string; title?: string; content: () => unknown; link?: string; component?: unknown; }
338
+
339
+ declare global {
340
+ interface Window {
341
+ registerWidget(config: unknown): void;
342
+ registerLink(config: unknown): void;
343
+ registerUI(config: unknown): void;
344
+ registerMenuItem(config: unknown): void;
345
+ registerLocalization(messages: unknown): void;
346
+ ICanComponents: Record<string, unknown>;
347
+ WidgetDesignerComponents: Record<string, unknown>;
348
+ }
349
+ }
350
+
351
+ // ─── Registration ─────────────────────────────────────────────────────────────
352
+
353
+ export function registerWidget(_widget: IWidgetObject): void {
354
+ const id = ((BundleConfig.id as string) + '/widget/' + _widget.id).toLowerCase();
355
+
356
+ if (!(window as any).registerWidget) {
357
+ console.error('[ICan] registerWidget called outside of ICan portal context');
358
+ return;
359
+ }
360
+
361
+ const widgetDetails = (BundleConfig.widgets as Array<{ id: string }>)?.find(w => w.id === _widget.id);
362
+ if (!widgetDetails) {
363
+ throw new Error(\`[ICan] Widget "\${_widget.id}" is not in bundle.json. Add it to the widgets array first.\`);
364
+ }
365
+
366
+ const merged: Record<string, unknown> = { ..._widget, ...widgetDetails, id };
367
+ if (merged['widget'] && !merged['component']) {
368
+ merged['component'] = merged['widget'];
369
+ }
370
+ (window as any).registerWidget(merged);
371
+ }
372
+
373
+ export function registerLink(_link: ISidebarLink): void {
374
+ const id = ((BundleConfig.id as string) + '/sidebarlink/' + _link.id).toLowerCase();
375
+ if (!(window as any).registerLink) { console.error('[ICan] Not in ICan portal context'); return; }
376
+ const details = (BundleConfig.sidebarLinks as Array<{ id: string }>)?.find(s => s.id === _link.id);
377
+ if (!details) throw new Error(\`[ICan] Sidebar link "\${_link.id}" not in bundle.json\`);
378
+ (window as any).registerLink({ ..._link, ...details, id });
379
+ }
380
+
381
+ export function registerUI(_ui: IRenderUIItemProps): void {
382
+ const id = ((BundleConfig.id as string) + '/ui/' + _ui.id).toLowerCase();
383
+ if (!(window as any).registerUI) { console.error('[ICan] Not in ICan portal context'); return; }
384
+ const details = (BundleConfig.uis as Array<{ id: string }>)?.find(s => s.id === _ui.id);
385
+ if (!details) throw new Error(\`[ICan] UI "\${_ui.id}" not in bundle.json\`);
386
+ (window as any).registerUI({ ..._ui, ...details, id });
387
+ }
388
+
389
+ export function registerMenuItem(_menuItem: IMenuItem): void {
390
+ const id = ((BundleConfig.id as string) + '/menuitem/' + _menuItem.id).toLowerCase();
391
+ if (!(window as any).registerMenuItem) { console.error('[ICan] Not in ICan portal context'); return; }
392
+ const details = (BundleConfig.menuItems as Array<{ id: string }>)?.find(s => s.id === _menuItem.id);
393
+ if (!details) throw new Error(\`[ICan] Menu item "\${_menuItem.id}" not in bundle.json\`);
394
+ (window as any).registerMenuItem({ ..._menuItem, ...details, id });
395
+ }
396
+
397
+ export function enableLocalization(): void {
398
+ (window as any).registerLocalization(LocalizationMessages);
399
+ }
400
+ `;
401
+ }
402
+ function makeWebpackConfig() {
403
+ return `var path = require('path');
404
+
405
+ module.exports = {
406
+ mode: "development",
407
+
408
+ devtool: "source-map",
409
+
410
+ resolve: {
411
+ extensions: [".ts", ".tsx", ".js", ".json"]
412
+ },
413
+
414
+ entry: "./src/index.tsx",
415
+ output: {
416
+ path: path.join(__dirname, '/dist'),
417
+ publicPath: "/dist/",
418
+ filename: '[name].js'
419
+ },
420
+
421
+ module: {
422
+ rules: [
423
+ {
424
+ test: /\\.css$/,
425
+ use: ["style-loader", "css-loader"],
426
+ },
427
+ {
428
+ test: /\\.scss$/,
429
+ use: ["style-loader", "css-loader", { loader: "sass-loader", options: { api: "modern" } }]
430
+ },
431
+ {
432
+ test: /\\.ts(x?)$/,
433
+ exclude: /node_modules/,
434
+ use: [{ loader: "ts-loader" }]
435
+ },
436
+ {
437
+ test: /\\.svg$/,
438
+ use: [{ loader: 'svg-url-loader', options: { limit: 10000 } }],
439
+ },
440
+ { enforce: "pre", test: /\\.js$/, loader: "source-map-loader" }
441
+ ]
442
+ },
443
+
444
+ // React and portal-provided libs stay external — never bundle them.
445
+ // Everything else (e.g. apexcharts) gets bundled into main.js automatically.
446
+ externals: {
447
+ "react": "React",
448
+ "react-dom": "ReactDOM",
449
+ "recharts": "Recharts",
450
+ "ican/components": "ICanComponents",
451
+ "widget-designer/components": "WidgetDesignerComponents",
452
+ },
453
+
454
+ devServer: {
455
+ static: { directory: path.join(__dirname, '/') },
456
+ compress: true,
457
+ liveReload: true,
458
+ }
459
+ };
460
+ `;
461
+ }
462
+ function makeWidgetTsConfig() {
463
+ return JSON.stringify({
464
+ compilerOptions: {
465
+ outDir: './dist/',
466
+ sourceMap: true,
467
+ strict: true,
468
+ noImplicitAny: true,
469
+ strictNullChecks: true,
470
+ noUnusedLocals: true,
471
+ noUnusedParameters: true,
472
+ noImplicitReturns: true,
473
+ skipLibCheck: true,
474
+ module: 'commonjs',
475
+ target: 'es6',
476
+ jsx: 'react',
477
+ resolveJsonModule: true,
478
+ esModuleInterop: true,
479
+ },
480
+ include: ['src/**/*', 'ican.d.ts', 'designer.d.ts'],
481
+ }, null, 2);
482
+ }
483
+ function makeWidgetPackageJson(widgetId) {
484
+ return JSON.stringify({
485
+ name: widgetId,
486
+ version: '1.0.0',
487
+ private: true,
488
+ scripts: {
489
+ build: 'webpack --config webpack.config.js --mode production --progress',
490
+ 'build:dev': 'webpack --config webpack.config.js',
491
+ watch: 'webpack --watch --config webpack.config.js',
492
+ dev: 'webpack serve',
493
+ typecheck: 'tsc --noEmit',
494
+ },
495
+ dependencies: {
496
+ react: '^16.13.1',
497
+ 'react-dom': '^16.13.1',
498
+ },
499
+ devDependencies: {
500
+ '@types/react': '^16.9.43',
501
+ '@types/react-dom': '^16.9.8',
502
+ 'css-loader': '^6.8.1',
503
+ sass: '^1.69.0',
504
+ 'sass-loader': '^13.3.2',
505
+ 'source-map-loader': '^4.0.1',
506
+ 'style-loader': '^3.3.3',
507
+ 'svg-url-loader': '^8.0.0',
508
+ 'ts-loader': '^9.5.0',
509
+ typescript: '^5.2.0',
510
+ webpack: '^5.88.0',
511
+ 'webpack-cli': '^5.1.4',
512
+ 'webpack-dev-server': '^4.15.1',
513
+ },
514
+ }, null, 2);
515
+ }
516
+ function makeEnvExample(portalUrl) {
517
+ return `# ICan portal URL — used by infaira-canvas upload
518
+ ICAN_URL=${portalUrl}
519
+
520
+ # Auth token — run "infaira-canvas login" or paste your JWT here
521
+ # NEVER commit the real .env file to git
522
+ ICAN_TOKEN=
523
+ `;
524
+ }
525
+ export function validateBundleJson(raw) {
526
+ let parsed;
527
+ try {
528
+ parsed = JSON.parse(raw);
529
+ }
530
+ catch (e) {
531
+ return { valid: false, error: `bundle.json is not valid JSON: ${e instanceof Error ? e.message : String(e)}` };
532
+ }
533
+ if (typeof parsed !== 'object' || parsed === null) {
534
+ return { valid: false, error: 'bundle.json must be a JSON object' };
535
+ }
536
+ const obj = parsed;
537
+ if (typeof obj['id'] !== 'string' || obj['id'].trim() === '') {
538
+ return { valid: false, error: 'bundle.json is missing required field "id" (string)' };
539
+ }
540
+ if (!Array.isArray(obj['widgets']) || obj['widgets'].length === 0) {
541
+ return { valid: false, error: 'bundle.json must have a non-empty "widgets" array' };
542
+ }
543
+ for (const w of obj['widgets']) {
544
+ if (typeof w !== 'object' || w === null) {
545
+ return { valid: false, error: 'Each entry in "widgets" must be an object' };
546
+ }
547
+ const widget = w;
548
+ if (typeof widget['id'] !== 'string' || widget['id'].trim() === '') {
549
+ return { valid: false, error: 'Each widget must have a non-empty "id" string' };
550
+ }
551
+ if (typeof widget['name'] !== 'string' || widget['name'].trim() === '') {
552
+ return { valid: false, error: `Widget "${widget['id']}" is missing a "name" field` };
553
+ }
554
+ }
555
+ return { valid: true, data: obj };
556
+ }
557
+ // ─── Main export ──────────────────────────────────────────────────────────────
558
+ export async function handleInit(name) {
559
+ const rawName = name.trim();
560
+ const kebabName = toKebabCase(name);
561
+ if (!kebabName) {
562
+ console.error('Error: widget name must contain at least one alphanumeric character.');
563
+ process.exit(1);
564
+ }
565
+ const widgetTitle = toTitleCase(name);
566
+ const componentName = toPascalCase(name);
567
+ const bundleId = crypto.randomUUID();
568
+ const widgetId = kebabName;
569
+ const targetDir = path.resolve(process.cwd(), rawName);
570
+ if (fs.existsSync(targetDir)) {
571
+ console.error(`Error: directory "${rawName}" already exists.`);
572
+ process.exit(1);
573
+ }
574
+ try {
575
+ fs.mkdirSync(targetDir, { recursive: true });
576
+ }
577
+ catch (err) {
578
+ const msg = err instanceof Error ? err.message : String(err);
579
+ console.error(`Error: could not create directory "${rawName}": ${msg}`);
580
+ process.exit(1);
581
+ }
582
+ console.log(`\nScaffolding widget: ${widgetTitle}\n`);
583
+ // ── ican.d.ts — fetch from server, fall back to bundled template ──
584
+ process.stdout.write(` Fetching ican.d.ts from ${ICAN_TYPES_URL} ...\n`);
585
+ let icanDts;
586
+ try {
587
+ icanDts = await fetchText(ICAN_TYPES_URL);
588
+ writeFile(targetDir, 'ican.d.ts', icanDts);
589
+ }
590
+ catch {
591
+ process.stdout.write(` Warning: could not fetch remote ican.d.ts — using bundled version.\n`);
592
+ copyTemplate(targetDir, 'ican.d.ts', 'ican.d.ts');
593
+ }
594
+ // ── Static template files ──
595
+ copyTemplate(targetDir, 'designer.d.ts', 'designer.d.ts');
596
+ copyTemplate(targetDir, 'index.html', 'index.html');
597
+ copyTemplate(targetDir, 'ui.html', 'ui.html');
598
+ copyTemplate(targetDir, 'resources/ican-components.js', 'resources/ican-components.js');
599
+ copyTemplate(targetDir, 'resources/infaira-logo.png', 'resources/infaira-logo.png');
600
+ copyTemplate(targetDir, 'resources/infaira-icon.png', 'resources/infaira-icon.png');
601
+ copyTemplate(targetDir, 'resources/favicon.ico', 'resources/favicon.ico');
602
+ copyTemplate(targetDir, 'site.webmanifest', 'site.webmanifest');
603
+ copyTemplate(targetDir, 'README.md', 'README.md');
604
+ copyTemplate(targetDir, 'ICan-Widget-Theming-Guide.md', 'ICan-Widget-Theming-Guide.md');
605
+ copyTemplate(targetDir, 'ICan-Customizing-Components.md', 'ICan-Customizing-Components.md');
606
+ copyTemplate(targetDir, 'ICan-Widget-Styling-Patterns.md', 'ICan-Widget-Styling-Patterns.md');
607
+ copyTemplate(targetDir, 'ICan-Widget-Development-Guide.md', 'ICan-Widget-Development-Guide.md');
608
+ // ── Generated files ──
609
+ writeFile(targetDir, 'bundle.json', makeBundleJson(bundleId, widgetId, widgetTitle));
610
+ writeFile(targetDir, 'src/index.tsx', makeIndexTsx(componentName, widgetTitle, widgetId));
611
+ writeFile(targetDir, 'src/ican.ts', makeIcanTs());
612
+ writeFile(targetDir, 'src/styles.scss', makeStylesScss());
613
+ writeFile(targetDir, 'webpack.config.js', makeWebpackConfig());
614
+ writeFile(targetDir, 'tsconfig.json', makeWidgetTsConfig());
615
+ writeFile(targetDir, 'package.json', makeWidgetPackageJson(widgetId));
616
+ writeFile(targetDir, 'localization.json', '{}\n');
617
+ writeFile(targetDir, '.env', makeEnvExample('http://localhost:4000'));
618
+ writeFile(targetDir, 'sample-bundle.json', makeSampleBundleJson(bundleId, widgetId, widgetTitle));
619
+ writeFile(targetDir, 'sample-localization.json', makeSampleLocalizationJson());
620
+ writeFile(targetDir, '.gitignore', 'node_modules/\ndist/\n*.tsbuildinfo\n.DS_Store\n.env\n\n# Fetched from AWS at init time — never commit\nican.d.ts\n');
621
+ const box = `
622
+ \u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510
623
+ \u2502 \u2502
624
+ \u2502 Widget scaffolded: ${widgetTitle.padEnd(28)} \u2502
625
+ \u2502 \u2502
626
+ \u2502 Setup: \u2502
627
+ \u2502 cd ${rawName.padEnd(46)} \u2502
628
+ \u2502 npm install \u2502
629
+ \u2502 \u2502
630
+ \u2502 Development: \u2502
631
+ \u2502 npm run dev \u2192 http://localhost:8080 \u2502
632
+ \u2502 \u2502
633
+ \u2502 Type-check: \u2502
634
+ \u2502 npm run typecheck \u2502
635
+ \u2502 \u2502
636
+ \u2502 Build for upload: \u2502
637
+ \u2502 npm run build \u2502
638
+ \u2502 \u2502
639
+ \u2502 Upload to portal: \u2502
640
+ \u2502 infaira-canvas upload \\ \u2502
641
+ \u2502 --bundle bundle.json \\ \u2502
642
+ \u2502 --script dist/main.js \u2502
643
+ \u2502 (reads ICAN_URL + ICAN_TOKEN from .env) \u2502
644
+ \u2502 \u2502
645
+ \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518`;
646
+ console.log(box);
647
+ }
@@ -0,0 +1,8 @@
1
+ interface IUploadOptions {
2
+ url: string;
3
+ token: string;
4
+ bundlePath: string;
5
+ scriptPath: string;
6
+ }
7
+ export declare function handleUpload(opts: IUploadOptions): Promise<void>;
8
+ export {};