@notis_ai/cli 0.2.1 → 0.2.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +98 -163
- package/dist/scaffolds/notes/app/globals.css +3 -0
- package/dist/scaffolds/notes/app/layout.tsx +6 -0
- package/dist/scaffolds/notes/app/page.tsx +1465 -0
- package/dist/scaffolds/notes/components/ui/badge.tsx +28 -0
- package/dist/scaffolds/notes/components/ui/button.tsx +53 -0
- package/dist/scaffolds/notes/components/ui/card.tsx +56 -0
- package/dist/scaffolds/notes/components.json +20 -0
- package/dist/scaffolds/notes/lib/utils.ts +6 -0
- package/dist/scaffolds/notes/notis.config.ts +31 -0
- package/dist/scaffolds/notes/package.json +31 -0
- package/dist/scaffolds/notes/postcss.config.mjs +8 -0
- package/dist/scaffolds/notes/tailwind.config.ts +58 -0
- package/dist/scaffolds/notes/tsconfig.json +22 -0
- package/dist/scaffolds/notes/vite.config.ts +10 -0
- package/dist/scaffolds.json +13 -0
- package/package.json +7 -3
- package/skills/notis-apps/SKILL.md +162 -0
- package/skills/notis-apps/cli.md +208 -0
- package/skills/notis-cli/SKILL.md +258 -0
- package/skills/notis-query/cli.md +40 -0
- package/src/cli.js +1 -1
- package/src/command-specs/apps.js +211 -46
- package/src/command-specs/helpers.js +0 -60
- package/src/command-specs/index.js +0 -6
- package/src/command-specs/meta.js +7 -6
- package/src/command-specs/tools.js +1 -33
- package/src/runtime/app-dev-server.js +32 -4
- package/src/runtime/app-platform.js +404 -24
- package/src/runtime/profiles.js +2 -2
- package/src/runtime/transport.js +2 -2
- package/template/.harness/index.html.tmpl +1 -50
- package/template/app/page.tsx +41 -6
- package/template/metadata/cover.png +0 -0
- package/template/metadata/screenshot-1.png +0 -0
- package/template/metadata/screenshot-2.png +0 -0
- package/template/metadata/screenshot-3.png +0 -0
- package/template/notis.config.ts +10 -6
- package/template/package.json +2 -2
- package/template/packages/{notis-sdk → sdk}/package.json +6 -0
- package/template/packages/{notis-sdk → sdk}/src/components/MultiSelectActionBar.tsx +2 -1
- package/template/packages/{notis-sdk → sdk}/src/components/MultiSelectCheckbox.tsx +2 -2
- package/template/packages/{notis-sdk → sdk}/src/components/MultiSelectDragOverlay.tsx +2 -2
- package/template/packages/{notis-sdk → sdk}/src/config.ts +1 -1
- package/template/packages/{notis-sdk → sdk}/src/hooks/useDatabase.ts +1 -13
- package/template/packages/{notis-sdk → sdk}/src/hooks/useMultiSelect.ts +4 -3
- package/template/packages/{notis-sdk → sdk}/src/hooks/useNotis.ts +4 -3
- package/template/packages/{notis-sdk → sdk}/src/hooks/useNotisNavigation.ts +3 -3
- package/template/packages/{notis-sdk → sdk}/src/hooks/useTool.ts +22 -7
- package/template/packages/{notis-sdk → sdk}/src/hooks/useUpsertDocument.ts +0 -8
- package/template/packages/{notis-sdk → sdk}/src/index.ts +2 -15
- package/template/packages/{notis-sdk → sdk}/src/provider.tsx +1 -1
- package/template/packages/{notis-sdk → sdk}/src/runtime.ts +5 -26
- package/src/command-specs/auth.js +0 -178
- package/src/command-specs/db.js +0 -163
- package/template/packages/notis-sdk/src/helpers.ts +0 -131
- package/template/packages/notis-sdk/src/hooks/useAppState.ts +0 -50
- package/template/packages/notis-sdk/src/hooks/useCollectionItem.ts +0 -58
- package/template/packages/notis-sdk/src/hooks/useDocument.ts +0 -61
- /package/template/packages/{notis-sdk → sdk}/src/hooks/useBackend.ts +0 -0
- /package/template/packages/{notis-sdk → sdk}/src/hooks/useTools.ts +0 -0
- /package/template/packages/{notis-sdk → sdk}/src/hooks/useTopBarSearch.ts +0 -0
- /package/template/packages/{notis-sdk → sdk}/src/styles.css +0 -0
- /package/template/packages/{notis-sdk → sdk}/src/ui.ts +0 -0
- /package/template/packages/{notis-sdk → sdk}/src/vite.ts +0 -0
- /package/template/packages/{notis-sdk → sdk}/tsconfig.json +0 -0
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
9
|
import { spawn } from 'node:child_process';
|
|
10
|
-
import { cpSync, existsSync, mkdirSync, readFileSync, writeFileSync, readdirSync, rmSync } from 'node:fs';
|
|
10
|
+
import { cpSync, existsSync, mkdirSync, readFileSync, writeFileSync, readdirSync, rmSync, statSync } from 'node:fs';
|
|
11
11
|
import { createRequire } from 'node:module';
|
|
12
12
|
import { dirname, join, resolve } from 'node:path';
|
|
13
13
|
import { fileURLToPath } from 'node:url';
|
|
@@ -21,15 +21,34 @@ const STATE_FILE = join(NOTIS_DIR, 'state.json');
|
|
|
21
21
|
const OUTPUT_DIR = join(NOTIS_DIR, 'output');
|
|
22
22
|
const BUNDLE_DIR = join(OUTPUT_DIR, 'bundle');
|
|
23
23
|
const MANIFEST_FILE = join(OUTPUT_DIR, 'manifest.json');
|
|
24
|
+
const METADATA_DIR = 'metadata';
|
|
25
|
+
const CLI_ROOT = resolve(dirname(fileURLToPath(import.meta.url)), '../..');
|
|
26
|
+
const MONOREPO_APPS_DIR = resolve(CLI_ROOT, '../..', 'apps');
|
|
27
|
+
const DIST_DIR = join(CLI_ROOT, 'dist');
|
|
28
|
+
const SCAFFOLD_CATALOG_FILE = join(DIST_DIR, 'scaffolds.json');
|
|
29
|
+
const SCAFFOLD_SOURCE_DIR = join(DIST_DIR, 'scaffolds');
|
|
30
|
+
const TEMPLATE_SDK_DIR = join(CLI_ROOT, 'template', 'packages', 'sdk');
|
|
31
|
+
export const NOTIS_APP_CATEGORIES = [
|
|
32
|
+
'Productivity',
|
|
33
|
+
'Sales & Marketing',
|
|
34
|
+
'Operations',
|
|
35
|
+
'Product & Engineering',
|
|
36
|
+
'Personal',
|
|
37
|
+
];
|
|
24
38
|
const SOURCE_COPY_EXCLUDES = new Set([
|
|
25
39
|
'node_modules',
|
|
26
40
|
'.notis',
|
|
27
41
|
'.git',
|
|
28
42
|
'dist',
|
|
29
|
-
'package-lock.json',
|
|
30
43
|
'tsconfig.tsbuildinfo',
|
|
31
44
|
'.DS_Store',
|
|
32
45
|
]);
|
|
46
|
+
const SCAFFOLD_COPY_EXCLUDES = new Set([
|
|
47
|
+
...SOURCE_COPY_EXCLUDES,
|
|
48
|
+
'coverage',
|
|
49
|
+
'.next',
|
|
50
|
+
'.turbo',
|
|
51
|
+
]);
|
|
33
52
|
let appConfigImportNonce = 0;
|
|
34
53
|
|
|
35
54
|
// ---------------------------------------------------------------------------
|
|
@@ -214,9 +233,160 @@ export function detectProjectWarnings(projectDir, appConfig = null) {
|
|
|
214
233
|
warnings.push('No database references declared in notis.config.ts.');
|
|
215
234
|
}
|
|
216
235
|
|
|
236
|
+
if (appConfig) {
|
|
237
|
+
try {
|
|
238
|
+
const listing = inspectListingReadiness(projectDir, appConfig);
|
|
239
|
+
warnings.push(...listing.warnings);
|
|
240
|
+
} catch (error) {
|
|
241
|
+
warnings.push(error instanceof Error ? error.message : String(error));
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
|
|
217
245
|
return warnings;
|
|
218
246
|
}
|
|
219
247
|
|
|
248
|
+
function safeKebab(value) {
|
|
249
|
+
return String(value || '')
|
|
250
|
+
.trim()
|
|
251
|
+
.toLowerCase()
|
|
252
|
+
.replace(/[^a-z0-9]+/g, '-')
|
|
253
|
+
.replace(/(^-|-$)+/g, '');
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
function jsStringLiteral(value) {
|
|
257
|
+
return `'${String(value).replace(/\\/g, '\\\\').replace(/'/g, "\\'")}'`;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
function normalizeCategories(categories = []) {
|
|
261
|
+
if (!Array.isArray(categories)) {
|
|
262
|
+
return [];
|
|
263
|
+
}
|
|
264
|
+
const allowed = new Set(NOTIS_APP_CATEGORIES);
|
|
265
|
+
const normalized = [];
|
|
266
|
+
for (const category of categories) {
|
|
267
|
+
if (typeof category !== 'string' || !category.trim()) {
|
|
268
|
+
continue;
|
|
269
|
+
}
|
|
270
|
+
const trimmed = category.trim();
|
|
271
|
+
if (!allowed.has(trimmed)) {
|
|
272
|
+
throw usageError(
|
|
273
|
+
`Invalid Notis app category "${trimmed}". Use one of: ${NOTIS_APP_CATEGORIES.join(', ')}.`,
|
|
274
|
+
);
|
|
275
|
+
}
|
|
276
|
+
if (!normalized.includes(trimmed)) {
|
|
277
|
+
normalized.push(trimmed);
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
return normalized;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
function pngDimensions(buffer) {
|
|
284
|
+
const signature = '89504e470d0a1a0a';
|
|
285
|
+
if (!Buffer.isBuffer(buffer) || buffer.length < 24 || buffer.subarray(0, 8).toString('hex') !== signature) {
|
|
286
|
+
return null;
|
|
287
|
+
}
|
|
288
|
+
return {
|
|
289
|
+
width: buffer.readUInt32BE(16),
|
|
290
|
+
height: buffer.readUInt32BE(20),
|
|
291
|
+
};
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
function imageAssetMeta(projectDir, relPath, { maxBytes, recommendedRatio = 1.6 }) {
|
|
295
|
+
const fullPath = join(projectDir, relPath);
|
|
296
|
+
const content = readFileSync(fullPath);
|
|
297
|
+
const dimensions = pngDimensions(content);
|
|
298
|
+
const ratio = dimensions?.height ? dimensions.width / dimensions.height : null;
|
|
299
|
+
const errors = [];
|
|
300
|
+
const warnings = [];
|
|
301
|
+
|
|
302
|
+
if (!relPath.toLowerCase().endsWith('.png')) {
|
|
303
|
+
errors.push(`${relPath} must be a PNG file.`);
|
|
304
|
+
}
|
|
305
|
+
if (content.length > maxBytes) {
|
|
306
|
+
errors.push(`${relPath} must be ${Math.round(maxBytes / 1024 / 1024)} MB or smaller.`);
|
|
307
|
+
}
|
|
308
|
+
if (ratio !== null && (ratio < 1.3 || ratio > 1.9)) {
|
|
309
|
+
errors.push(`${relPath} should be close to a 16:10 aspect ratio.`);
|
|
310
|
+
} else if (ratio !== null && Math.abs(ratio - recommendedRatio) > 0.08) {
|
|
311
|
+
warnings.push(`${relPath} is not 2000x1250; 16:10 PNG is recommended.`);
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
return {
|
|
315
|
+
path: relPath,
|
|
316
|
+
content_type: 'image/png',
|
|
317
|
+
bytes: content.length,
|
|
318
|
+
width: dimensions?.width ?? null,
|
|
319
|
+
height: dimensions?.height ?? null,
|
|
320
|
+
errors,
|
|
321
|
+
warnings,
|
|
322
|
+
};
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
export function discoverMetadataAssets(projectDir) {
|
|
326
|
+
const metadataDir = join(projectDir, METADATA_DIR);
|
|
327
|
+
const coverPath = join(METADATA_DIR, 'cover.png');
|
|
328
|
+
const cover = existsSync(join(projectDir, coverPath))
|
|
329
|
+
? imageAssetMeta(projectDir, coverPath, { maxBytes: 5 * 1024 * 1024 })
|
|
330
|
+
: null;
|
|
331
|
+
const screenshots = [];
|
|
332
|
+
|
|
333
|
+
if (existsSync(metadataDir)) {
|
|
334
|
+
for (const entry of readdirSync(metadataDir, { withFileTypes: true })) {
|
|
335
|
+
if (!entry.isFile()) continue;
|
|
336
|
+
const match = /^screenshot-(\d+)\.png$/i.exec(entry.name);
|
|
337
|
+
if (!match) continue;
|
|
338
|
+
screenshots.push({
|
|
339
|
+
index: Number.parseInt(match[1], 10),
|
|
340
|
+
...imageAssetMeta(projectDir, join(METADATA_DIR, entry.name), {
|
|
341
|
+
maxBytes: 2 * 1024 * 1024,
|
|
342
|
+
}),
|
|
343
|
+
});
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
screenshots.sort((a, b) => a.index - b.index);
|
|
348
|
+
return {
|
|
349
|
+
cover,
|
|
350
|
+
screenshots: screenshots.slice(0, 6),
|
|
351
|
+
};
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
export function inspectListingReadiness(projectDir, appConfig = null) {
|
|
355
|
+
const config = appConfig || {};
|
|
356
|
+
const warnings = [];
|
|
357
|
+
const errors = [];
|
|
358
|
+
const metadata = discoverMetadataAssets(projectDir);
|
|
359
|
+
const categories = normalizeCategories(config.categories || []);
|
|
360
|
+
|
|
361
|
+
if (!String(config.tagline || '').trim()) {
|
|
362
|
+
warnings.push('Listing tagline missing in notis.config.ts.');
|
|
363
|
+
}
|
|
364
|
+
if (categories.length === 0) {
|
|
365
|
+
warnings.push(`Listing category missing in notis.config.ts. Use one of: ${NOTIS_APP_CATEGORIES.join(', ')}.`);
|
|
366
|
+
}
|
|
367
|
+
if (!metadata.cover) {
|
|
368
|
+
warnings.push('Listing cover missing at metadata/cover.png.');
|
|
369
|
+
} else {
|
|
370
|
+
errors.push(...metadata.cover.errors);
|
|
371
|
+
warnings.push(...metadata.cover.warnings);
|
|
372
|
+
}
|
|
373
|
+
if (metadata.screenshots.length < 3) {
|
|
374
|
+
warnings.push(`Listing screenshots: ${metadata.screenshots.length}/3 minimum in metadata/screenshot-N.png.`);
|
|
375
|
+
}
|
|
376
|
+
for (const screenshot of metadata.screenshots) {
|
|
377
|
+
errors.push(...screenshot.errors);
|
|
378
|
+
warnings.push(...screenshot.warnings);
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
return {
|
|
382
|
+
ready: errors.length === 0 && Boolean(config.tagline) && categories.length > 0 && Boolean(metadata.cover) && metadata.screenshots.length >= 1,
|
|
383
|
+
warnings,
|
|
384
|
+
errors,
|
|
385
|
+
metadata,
|
|
386
|
+
categories,
|
|
387
|
+
};
|
|
388
|
+
}
|
|
389
|
+
|
|
220
390
|
// ---------------------------------------------------------------------------
|
|
221
391
|
// Build
|
|
222
392
|
// ---------------------------------------------------------------------------
|
|
@@ -409,6 +579,21 @@ function generateEntryFile(projectDir, routes) {
|
|
|
409
579
|
return entryPath;
|
|
410
580
|
}
|
|
411
581
|
|
|
582
|
+
export async function prepareArtifactBuild(projectDir) {
|
|
583
|
+
validateProjectBoundary(projectDir);
|
|
584
|
+
const appConfig = await loadAppConfig(projectDir);
|
|
585
|
+
const detectedRoutes = resolveConfiguredRoutes(appConfig, projectDir);
|
|
586
|
+
|
|
587
|
+
generateEntryFile(projectDir, detectedRoutes);
|
|
588
|
+
|
|
589
|
+
const manifest = generateManifest(appConfig, projectDir);
|
|
590
|
+
const manifestPath = join(projectDir, MANIFEST_FILE);
|
|
591
|
+
mkdirSync(dirname(manifestPath), { recursive: true });
|
|
592
|
+
writeFileSync(manifestPath, JSON.stringify(manifest, null, 2) + '\n');
|
|
593
|
+
|
|
594
|
+
return { appConfig, manifest, routes: detectedRoutes };
|
|
595
|
+
}
|
|
596
|
+
|
|
412
597
|
/**
|
|
413
598
|
* Generate the manifest from app config and build output.
|
|
414
599
|
*/
|
|
@@ -431,15 +616,52 @@ export function generateManifest(appConfig, projectDir) {
|
|
|
431
616
|
});
|
|
432
617
|
|
|
433
618
|
const databases = appConfig.databases || [];
|
|
619
|
+
const categories = normalizeCategories(appConfig.categories || []);
|
|
620
|
+
const metadata = discoverMetadataAssets(projectDir);
|
|
621
|
+
const appSlug = safeKebab(appConfig.name);
|
|
622
|
+
const displayTitle = appConfig.title || appConfig.displayName || appConfig.name;
|
|
623
|
+
const listingMedia = {
|
|
624
|
+
cover: metadata.cover
|
|
625
|
+
? {
|
|
626
|
+
path: metadata.cover.path,
|
|
627
|
+
content_type: metadata.cover.content_type,
|
|
628
|
+
width: metadata.cover.width,
|
|
629
|
+
height: metadata.cover.height,
|
|
630
|
+
bytes: metadata.cover.bytes,
|
|
631
|
+
}
|
|
632
|
+
: null,
|
|
633
|
+
screenshots: metadata.screenshots.map((screenshot) => ({
|
|
634
|
+
path: screenshot.path,
|
|
635
|
+
content_type: screenshot.content_type,
|
|
636
|
+
width: screenshot.width,
|
|
637
|
+
height: screenshot.height,
|
|
638
|
+
bytes: screenshot.bytes,
|
|
639
|
+
})),
|
|
640
|
+
};
|
|
434
641
|
|
|
435
642
|
return {
|
|
436
643
|
version: 1,
|
|
437
|
-
spec_version:
|
|
644
|
+
spec_version: 4,
|
|
438
645
|
app: {
|
|
439
|
-
name:
|
|
646
|
+
name: displayTitle,
|
|
647
|
+
slug: appSlug || null,
|
|
440
648
|
description: appConfig.description || null,
|
|
441
649
|
icon: appConfig.icon || null,
|
|
650
|
+
title: displayTitle,
|
|
651
|
+
tagline: appConfig.tagline || null,
|
|
652
|
+
categories,
|
|
653
|
+
author: appConfig.author || null,
|
|
654
|
+
version_notes: appConfig.versionNotes || appConfig.version_notes || null,
|
|
442
655
|
},
|
|
656
|
+
listing: {
|
|
657
|
+
title: displayTitle,
|
|
658
|
+
tagline: appConfig.tagline || null,
|
|
659
|
+
categories,
|
|
660
|
+
author: appConfig.author || null,
|
|
661
|
+
version_notes: appConfig.versionNotes || appConfig.version_notes || null,
|
|
662
|
+
media: listingMedia,
|
|
663
|
+
},
|
|
664
|
+
metadata: listingMedia,
|
|
443
665
|
routes,
|
|
444
666
|
bundle: {
|
|
445
667
|
js: 'bundle/app.js',
|
|
@@ -454,14 +676,7 @@ export function generateManifest(appConfig, projectDir) {
|
|
|
454
676
|
* Build the app bundle: generate entry file, run `vite build`, package into .notis/output/.
|
|
455
677
|
*/
|
|
456
678
|
export async function buildArtifact(projectDir) {
|
|
457
|
-
|
|
458
|
-
const appConfig = await loadAppConfig(projectDir);
|
|
459
|
-
|
|
460
|
-
// Auto-detect or use configured routes
|
|
461
|
-
const detectedRoutes = resolveConfiguredRoutes(appConfig, projectDir);
|
|
462
|
-
|
|
463
|
-
// Generate the entry file that re-exports all route page components
|
|
464
|
-
generateEntryFile(projectDir, detectedRoutes);
|
|
679
|
+
await prepareArtifactBuild(projectDir);
|
|
465
680
|
|
|
466
681
|
// Run Vite build
|
|
467
682
|
await runProjectScript({
|
|
@@ -477,14 +692,11 @@ export async function buildArtifact(projectDir) {
|
|
|
477
692
|
);
|
|
478
693
|
}
|
|
479
694
|
normalizeBundleStylesheets(projectDir);
|
|
695
|
+
copyMetadataAssets(projectDir);
|
|
480
696
|
|
|
481
697
|
validateArtifactBoundary(readArtifactFiles(projectDir));
|
|
482
698
|
|
|
483
|
-
|
|
484
|
-
const manifest = generateManifest(appConfig, projectDir);
|
|
485
|
-
const manifestPath = join(projectDir, MANIFEST_FILE);
|
|
486
|
-
mkdirSync(dirname(manifestPath), { recursive: true });
|
|
487
|
-
writeFileSync(manifestPath, JSON.stringify(manifest, null, 2) + '\n');
|
|
699
|
+
const manifest = readManifest(projectDir);
|
|
488
700
|
|
|
489
701
|
return { manifest, outputDir: join(projectDir, OUTPUT_DIR) };
|
|
490
702
|
}
|
|
@@ -500,6 +712,22 @@ export function readManifest(projectDir) {
|
|
|
500
712
|
return JSON.parse(readFileSync(manifestPath, 'utf-8'));
|
|
501
713
|
}
|
|
502
714
|
|
|
715
|
+
function copyMetadataAssets(projectDir) {
|
|
716
|
+
const metadataDir = join(projectDir, METADATA_DIR);
|
|
717
|
+
if (!existsSync(metadataDir)) {
|
|
718
|
+
return;
|
|
719
|
+
}
|
|
720
|
+
const outputMetadataDir = join(projectDir, OUTPUT_DIR, METADATA_DIR);
|
|
721
|
+
mkdirSync(outputMetadataDir, { recursive: true });
|
|
722
|
+
for (const entry of readdirSync(metadataDir, { withFileTypes: true })) {
|
|
723
|
+
if (!entry.isFile()) continue;
|
|
724
|
+
if (entry.name !== 'cover.png' && !/^screenshot-\d+\.png$/i.test(entry.name)) {
|
|
725
|
+
continue;
|
|
726
|
+
}
|
|
727
|
+
cpSync(join(metadataDir, entry.name), join(outputMetadataDir, entry.name));
|
|
728
|
+
}
|
|
729
|
+
}
|
|
730
|
+
|
|
503
731
|
// ---------------------------------------------------------------------------
|
|
504
732
|
// Linking
|
|
505
733
|
// ---------------------------------------------------------------------------
|
|
@@ -530,24 +758,37 @@ export function requireLinkedAppId(projectDir, explicitAppId) {
|
|
|
530
758
|
/**
|
|
531
759
|
* Scaffold a new Notis app project from the SDK template.
|
|
532
760
|
*/
|
|
533
|
-
export function
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
761
|
+
export function loadScaffoldCatalog() {
|
|
762
|
+
if (existsSync(SCAFFOLD_CATALOG_FILE)) {
|
|
763
|
+
const parsed = JSON.parse(readFileSync(SCAFFOLD_CATALOG_FILE, 'utf-8'));
|
|
764
|
+
const scaffolds = Array.isArray(parsed?.scaffolds) ? parsed.scaffolds : parsed;
|
|
765
|
+
return Array.isArray(scaffolds)
|
|
766
|
+
? scaffolds.filter((entry) => entry && typeof entry.slug === 'string')
|
|
767
|
+
: [];
|
|
768
|
+
}
|
|
769
|
+
return loadMonorepoScaffoldCatalog();
|
|
770
|
+
}
|
|
771
|
+
|
|
772
|
+
export function scaffoldProject({ projectDir, appName, fromSlug = null }) {
|
|
773
|
+
const templateDir = fromSlug ? resolveScaffoldSourceDir(fromSlug) : join(CLI_ROOT, 'template');
|
|
538
774
|
|
|
539
775
|
if (!existsSync(templateDir)) {
|
|
776
|
+
if (fromSlug) {
|
|
777
|
+
const known = loadScaffoldCatalog().map((entry) => entry.slug).join(', ') || 'none';
|
|
778
|
+
throw usageError(`Unknown scaffold "${fromSlug}". Available scaffolds: ${known}.`);
|
|
779
|
+
}
|
|
540
780
|
throw usageError(`SDK template not found at ${templateDir}. Ensure @notis_ai/cli is installed correctly.`);
|
|
541
781
|
}
|
|
542
782
|
|
|
543
783
|
mkdirSync(projectDir, { recursive: true });
|
|
544
|
-
|
|
784
|
+
copyScaffoldSource(templateDir, projectDir);
|
|
545
785
|
|
|
546
786
|
// Update package.json with the app name
|
|
547
787
|
const pkgPath = join(projectDir, 'package.json');
|
|
548
788
|
if (existsSync(pkgPath)) {
|
|
549
789
|
const pkg = JSON.parse(readFileSync(pkgPath, 'utf-8'));
|
|
550
790
|
pkg.name = appName.toLowerCase().replace(/[^a-z0-9-]/g, '-').replace(/-+/g, '-');
|
|
791
|
+
ensureScaffoldLocalSdk(projectDir, pkg);
|
|
551
792
|
writeFileSync(pkgPath, JSON.stringify(pkg, null, 2) + '\n');
|
|
552
793
|
}
|
|
553
794
|
|
|
@@ -555,13 +796,141 @@ export function scaffoldProject({ projectDir, appName }) {
|
|
|
555
796
|
const configPath = join(projectDir, 'notis.config.ts');
|
|
556
797
|
if (existsSync(configPath)) {
|
|
557
798
|
let config = readFileSync(configPath, 'utf-8');
|
|
558
|
-
|
|
799
|
+
const displayName = jsStringLiteral(appName);
|
|
800
|
+
const slugName = jsStringLiteral(safeKebab(appName) || 'my-notis-app');
|
|
801
|
+
if (fromSlug) {
|
|
802
|
+
if (/name\s*:/.test(config)) {
|
|
803
|
+
config = config.replace(/name\s*:\s*(['"`])[\s\S]*?\1/, `name: ${slugName}`);
|
|
804
|
+
}
|
|
805
|
+
if (/title\s*:/.test(config)) {
|
|
806
|
+
config = config.replace(/title\s*:\s*(['"`])[\s\S]*?\1/, `title: ${displayName}`);
|
|
807
|
+
} else {
|
|
808
|
+
config = config.replace(/name\s*:\s*(['"`])[\s\S]*?\1/, `name: ${slugName},\n title: ${displayName}`);
|
|
809
|
+
}
|
|
810
|
+
} else {
|
|
811
|
+
if (/name\s*:/.test(config)) {
|
|
812
|
+
config = config.replace(/name\s*:\s*(['"`])[\s\S]*?\1/, `name: ${slugName}`);
|
|
813
|
+
}
|
|
814
|
+
if (/title\s*:/.test(config)) {
|
|
815
|
+
config = config.replace(/title\s*:\s*(['"`])[\s\S]*?\1/, `title: ${displayName}`);
|
|
816
|
+
} else {
|
|
817
|
+
config = config.replace(/'My Notis App'/, displayName);
|
|
818
|
+
}
|
|
819
|
+
}
|
|
559
820
|
writeFileSync(configPath, config);
|
|
560
821
|
}
|
|
561
822
|
|
|
562
823
|
return { projectDir };
|
|
563
824
|
}
|
|
564
825
|
|
|
826
|
+
function ensureScaffoldLocalSdk(projectDir, pkg) {
|
|
827
|
+
let shouldInstallLocalSdk = false;
|
|
828
|
+
for (const dependencyGroup of ['dependencies', 'devDependencies']) {
|
|
829
|
+
const dependencyValue = pkg[dependencyGroup]?.['@notis/sdk'];
|
|
830
|
+
if (typeof dependencyValue === 'string' && dependencyValue.startsWith('file:')) {
|
|
831
|
+
pkg[dependencyGroup]['@notis/sdk'] = 'file:./packages/sdk';
|
|
832
|
+
shouldInstallLocalSdk = true;
|
|
833
|
+
}
|
|
834
|
+
}
|
|
835
|
+
|
|
836
|
+
if (!shouldInstallLocalSdk) {
|
|
837
|
+
return;
|
|
838
|
+
}
|
|
839
|
+
|
|
840
|
+
const localSdkDir = join(projectDir, 'packages', 'sdk');
|
|
841
|
+
if (existsSync(join(localSdkDir, 'package.json'))) {
|
|
842
|
+
return;
|
|
843
|
+
}
|
|
844
|
+
if (!existsSync(join(TEMPLATE_SDK_DIR, 'package.json'))) {
|
|
845
|
+
throw usageError(`SDK template not found at ${TEMPLATE_SDK_DIR}. Ensure @notis_ai/cli is installed correctly.`);
|
|
846
|
+
}
|
|
847
|
+
|
|
848
|
+
mkdirSync(dirname(localSdkDir), { recursive: true });
|
|
849
|
+
cpSync(TEMPLATE_SDK_DIR, localSdkDir, { recursive: true, dereference: true });
|
|
850
|
+
}
|
|
851
|
+
|
|
852
|
+
function resolveScaffoldSourceDir(fromSlug) {
|
|
853
|
+
const bundledDir = join(SCAFFOLD_SOURCE_DIR, fromSlug);
|
|
854
|
+
if (existsSync(bundledDir)) {
|
|
855
|
+
return bundledDir;
|
|
856
|
+
}
|
|
857
|
+
const monorepoDir = join(MONOREPO_APPS_DIR, fromSlug);
|
|
858
|
+
if (existsSync(join(monorepoDir, 'notis.config.ts'))) {
|
|
859
|
+
return monorepoDir;
|
|
860
|
+
}
|
|
861
|
+
return bundledDir;
|
|
862
|
+
}
|
|
863
|
+
|
|
864
|
+
function loadMonorepoScaffoldCatalog() {
|
|
865
|
+
if (!existsSync(MONOREPO_APPS_DIR)) {
|
|
866
|
+
return [];
|
|
867
|
+
}
|
|
868
|
+
const scaffolds = [];
|
|
869
|
+
for (const entry of readdirSync(MONOREPO_APPS_DIR, { withFileTypes: true })) {
|
|
870
|
+
if (!entry.isDirectory() || entry.name.startsWith('.') || entry.name.startsWith('_')) {
|
|
871
|
+
continue;
|
|
872
|
+
}
|
|
873
|
+
const configPath = join(MONOREPO_APPS_DIR, entry.name, 'notis.config.ts');
|
|
874
|
+
if (!existsSync(configPath)) {
|
|
875
|
+
continue;
|
|
876
|
+
}
|
|
877
|
+
const configSource = readFileSync(configPath, 'utf-8');
|
|
878
|
+
const description = readTsStringProperty(configSource, 'description') || '';
|
|
879
|
+
scaffolds.push({
|
|
880
|
+
slug: entry.name,
|
|
881
|
+
name: readTsStringProperty(configSource, 'title') || readTsStringProperty(configSource, 'name') || entry.name,
|
|
882
|
+
description,
|
|
883
|
+
icon: readTsStringProperty(configSource, 'icon') || 'phosphor:squares-four',
|
|
884
|
+
categories: readTsStringArrayProperty(configSource, 'categories'),
|
|
885
|
+
tagline: readTsStringProperty(configSource, 'tagline') || description,
|
|
886
|
+
});
|
|
887
|
+
}
|
|
888
|
+
return scaffolds.sort((a, b) => a.slug.localeCompare(b.slug));
|
|
889
|
+
}
|
|
890
|
+
|
|
891
|
+
function readTsStringProperty(source, propertyName) {
|
|
892
|
+
const match = source.match(new RegExp(`\\b${propertyName}\\s*:\\s*(['"\`])([\\s\\S]*?)\\1`));
|
|
893
|
+
return match ? match[2] : null;
|
|
894
|
+
}
|
|
895
|
+
|
|
896
|
+
function readTsStringArrayProperty(source, propertyName) {
|
|
897
|
+
const match = source.match(new RegExp(`\\b${propertyName}\\s*:\\s*\\[([\\s\\S]*?)\\]`));
|
|
898
|
+
if (!match) {
|
|
899
|
+
return [];
|
|
900
|
+
}
|
|
901
|
+
return Array.from(match[1].matchAll(/(['"`])([\s\S]*?)\1/g), (entry) => entry[2].trim()).filter(Boolean);
|
|
902
|
+
}
|
|
903
|
+
|
|
904
|
+
function copyScaffoldSource(sourceDir, targetDir) {
|
|
905
|
+
function shouldCopy(path) {
|
|
906
|
+
const name = path.split(/[\\/]/).pop();
|
|
907
|
+
if (!name) return true;
|
|
908
|
+
return !SCAFFOLD_COPY_EXCLUDES.has(name)
|
|
909
|
+
&& !name.startsWith('.env')
|
|
910
|
+
&& !/\.(test|spec)\.[cm]?[jt]sx?$/i.test(name);
|
|
911
|
+
}
|
|
912
|
+
|
|
913
|
+
function walk(src, dest) {
|
|
914
|
+
if (!shouldCopy(src)) {
|
|
915
|
+
return;
|
|
916
|
+
}
|
|
917
|
+
const stat = statSync(src);
|
|
918
|
+
if (stat.isDirectory()) {
|
|
919
|
+
mkdirSync(dest, { recursive: true });
|
|
920
|
+
for (const entry of readdirSync(src)) {
|
|
921
|
+
walk(join(src, entry), join(dest, entry));
|
|
922
|
+
}
|
|
923
|
+
return;
|
|
924
|
+
}
|
|
925
|
+
if (stat.isFile()) {
|
|
926
|
+
mkdirSync(dirname(dest), { recursive: true });
|
|
927
|
+
cpSync(src, dest);
|
|
928
|
+
}
|
|
929
|
+
}
|
|
930
|
+
|
|
931
|
+
walk(sourceDir, targetDir);
|
|
932
|
+
}
|
|
933
|
+
|
|
565
934
|
// ---------------------------------------------------------------------------
|
|
566
935
|
// Deploy helpers
|
|
567
936
|
// ---------------------------------------------------------------------------
|
|
@@ -700,8 +1069,14 @@ export async function pullAppSource({
|
|
|
700
1069
|
const versionMatch = /-v(\d+)\.tar\.gz/i.exec(contentDisposition);
|
|
701
1070
|
const pulledVersion = versionMatch ? Number.parseInt(versionMatch[1], 10) : null;
|
|
702
1071
|
extractTarGz(Buffer.from(await response.arrayBuffer()), targetDir);
|
|
1072
|
+
const linkedVersion = pulledVersion || (
|
|
1073
|
+
String(version || 'latest') === 'latest'
|
|
1074
|
+
? undefined
|
|
1075
|
+
: Number.parseInt(String(version), 10)
|
|
1076
|
+
);
|
|
703
1077
|
writeLinkedState(targetDir, {
|
|
704
1078
|
app_id: appId,
|
|
1079
|
+
...(Number.isFinite(linkedVersion) ? { version: linkedVersion } : {}),
|
|
705
1080
|
linked_at: new Date().toISOString(),
|
|
706
1081
|
});
|
|
707
1082
|
|
|
@@ -898,6 +1273,9 @@ const CONTENT_TYPE_MAP = {
|
|
|
898
1273
|
'.css': 'text/css',
|
|
899
1274
|
'.json': 'application/json',
|
|
900
1275
|
'.html': 'text/html',
|
|
1276
|
+
'.png': 'image/png',
|
|
1277
|
+
'.jpg': 'image/jpeg',
|
|
1278
|
+
'.jpeg': 'image/jpeg',
|
|
901
1279
|
};
|
|
902
1280
|
|
|
903
1281
|
/**
|
|
@@ -952,6 +1330,8 @@ export async function directDeploy(projectDir, appId) {
|
|
|
952
1330
|
version: newVersion,
|
|
953
1331
|
storage_bucket: bucket,
|
|
954
1332
|
storage_prefix: storagePrefix,
|
|
1333
|
+
source_storage_bucket: 'app-source',
|
|
1334
|
+
source_storage_prefix: storagePrefix,
|
|
955
1335
|
};
|
|
956
1336
|
await updateAppVersion(supabaseUrl, supabaseKey, appId, newVersion, deployManifest);
|
|
957
1337
|
|
package/src/runtime/profiles.js
CHANGED
|
@@ -205,8 +205,8 @@ export function resolveRuntimeProfile(globalOptions = {}, { requireAuth = true }
|
|
|
205
205
|
exitCode: EXIT_CODES.auth,
|
|
206
206
|
hints: [
|
|
207
207
|
{
|
|
208
|
-
command: '
|
|
209
|
-
reason: '
|
|
208
|
+
command: 'Open the Notis desktop app and sign in',
|
|
209
|
+
reason: 'Set NOTIS_JWT or sign in through the Notis desktop app',
|
|
210
210
|
},
|
|
211
211
|
],
|
|
212
212
|
});
|
package/src/runtime/transport.js
CHANGED
|
@@ -51,8 +51,8 @@ function normalizeBackendError(status, payload) {
|
|
|
51
51
|
exitCode: EXIT_CODES.auth,
|
|
52
52
|
hints: [
|
|
53
53
|
{
|
|
54
|
-
command: '
|
|
55
|
-
reason: '
|
|
54
|
+
command: 'Open the Notis desktop app and sign in',
|
|
55
|
+
reason: 'Open the Notis desktop app to refresh CLI auth, or set NOTIS_JWT',
|
|
56
56
|
},
|
|
57
57
|
],
|
|
58
58
|
details: payload || {},
|
|
@@ -107,8 +107,7 @@
|
|
|
107
107
|
return {
|
|
108
108
|
app: descriptor.app,
|
|
109
109
|
route: descriptor.route,
|
|
110
|
-
|
|
111
|
-
context: descriptor.context || { collectionItem: null },
|
|
110
|
+
context: descriptor.context || {},
|
|
112
111
|
navigate: (args) => record('navigate', args),
|
|
113
112
|
registerTopBarSearch: () => {},
|
|
114
113
|
setTopBarSearchValue: () => {},
|
|
@@ -121,34 +120,6 @@
|
|
|
121
120
|
record('callTool', { name, arguments: args || {} });
|
|
122
121
|
return { ok: true, result: null };
|
|
123
122
|
},
|
|
124
|
-
queryDatabase: async (args) => {
|
|
125
|
-
record('queryDatabase', args || {});
|
|
126
|
-
return { documents: [], next_offset: null };
|
|
127
|
-
},
|
|
128
|
-
getDocument: async (args) => {
|
|
129
|
-
record('getDocument', args || {});
|
|
130
|
-
return null;
|
|
131
|
-
},
|
|
132
|
-
upsertDocument: async (args) => {
|
|
133
|
-
record('upsertDocument', args || {});
|
|
134
|
-
return {
|
|
135
|
-
status: 'ok',
|
|
136
|
-
document: {
|
|
137
|
-
id: (args && args.documentId) || 'mock-doc',
|
|
138
|
-
title: (args && args.title) || '',
|
|
139
|
-
properties: (args && args.properties) || {},
|
|
140
|
-
databaseSlug: args && args.databaseSlug,
|
|
141
|
-
},
|
|
142
|
-
};
|
|
143
|
-
},
|
|
144
|
-
listCollectionItems: async (args) => {
|
|
145
|
-
record('listCollectionItems', args || {});
|
|
146
|
-
return { items: [] };
|
|
147
|
-
},
|
|
148
|
-
listCollectionTree: async (args) => {
|
|
149
|
-
record('listCollectionTree', args || {});
|
|
150
|
-
return { items: [] };
|
|
151
|
-
},
|
|
152
123
|
request: async (path, options) => {
|
|
153
124
|
record('request', { path, options });
|
|
154
125
|
return null;
|
|
@@ -168,26 +139,6 @@
|
|
|
168
139
|
record('callTool', { name, arguments: args || {} });
|
|
169
140
|
return runtimeQuery({ method: 'tools/call', name, arguments: args || {} });
|
|
170
141
|
},
|
|
171
|
-
queryDatabase: async (args) => {
|
|
172
|
-
record('queryDatabase', args || {});
|
|
173
|
-
const result = await runtimeQuery({ method: 'tools/call', name: 'notis_query_database', arguments: args || {} });
|
|
174
|
-
return result.result || result;
|
|
175
|
-
},
|
|
176
|
-
getDocument: async (args) => {
|
|
177
|
-
record('getDocument', args || {});
|
|
178
|
-
const result = await runtimeQuery({ method: 'tools/call', name: 'notis_get_document', arguments: args || {} });
|
|
179
|
-
return result.result || result;
|
|
180
|
-
},
|
|
181
|
-
upsertDocument: async (args) => {
|
|
182
|
-
record('upsertDocument', args || {});
|
|
183
|
-
const result = await runtimeQuery({ method: 'tools/call', name: 'notis_upsert_document', arguments: args || {} });
|
|
184
|
-
return result.result || result;
|
|
185
|
-
},
|
|
186
|
-
listCollectionItems: async (args) => {
|
|
187
|
-
record('listCollectionItems', args || {});
|
|
188
|
-
const result = await runtimeQuery({ method: 'tools/call', name: 'notis_list_collection_items', arguments: args || {} });
|
|
189
|
-
return result.result || result;
|
|
190
|
-
},
|
|
191
142
|
request: async (path, options) => {
|
|
192
143
|
record('request', { path, options });
|
|
193
144
|
const response = await fetch(`${String(apiBase).replace(/\/$/, '')}${path}`, {
|