@notis_ai/cli 0.2.5 → 0.2.6
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 +31 -9
- package/dist/scaffolds/notis-database/CHANGELOG.md +5 -0
- package/dist/scaffolds/notis-database/notis.config.ts +0 -1
- package/dist/scaffolds/notis-database/src/mock-runtime.ts +1 -0
- package/dist/scaffolds/notis-journal/CHANGELOG.md +25 -0
- package/dist/scaffolds/notis-journal/app/globals.css +37 -0
- package/dist/scaffolds/notis-journal/app/insights/page.tsx +513 -0
- package/dist/scaffolds/notis-journal/app/journal-core.tsx +362 -0
- package/dist/scaffolds/notis-journal/app/journal-ui.tsx +337 -0
- package/dist/scaffolds/notis-journal/app/layout.tsx +6 -0
- package/dist/scaffolds/notis-journal/app/page.tsx +485 -0
- package/dist/scaffolds/notis-journal/components/ui/badge.tsx +28 -0
- package/dist/scaffolds/notis-journal/components/ui/button.tsx +53 -0
- package/dist/scaffolds/notis-journal/components/ui/card.tsx +56 -0
- package/dist/scaffolds/notis-journal/components.json +20 -0
- package/dist/scaffolds/notis-journal/index.html +12 -0
- package/dist/scaffolds/notis-journal/lib/utils.ts +6 -0
- package/dist/scaffolds/notis-journal/metadata/screenshot-1.png +0 -0
- package/dist/scaffolds/notis-journal/metadata/screenshot-2.png +0 -0
- package/dist/scaffolds/notis-journal/metadata/screenshot-3.png +0 -0
- package/dist/scaffolds/notis-journal/metadata/screenshot-4.png +0 -0
- package/dist/scaffolds/notis-journal/metadata/screenshot-5.png +0 -0
- package/dist/scaffolds/notis-journal/metadata/screenshot-6.png +0 -0
- package/dist/scaffolds/notis-journal/metadata/screenshot-fixtures.json +132 -0
- package/dist/scaffolds/notis-journal/notis.config.ts +93 -0
- package/dist/scaffolds/notis-journal/package.json +34 -0
- package/dist/scaffolds/notis-journal/packages/sdk/package.json +36 -0
- package/dist/scaffolds/notis-journal/packages/sdk/src/components/DocumentEditor.tsx +93 -0
- package/dist/scaffolds/notis-journal/packages/sdk/src/components/Markdown.tsx +60 -0
- package/dist/scaffolds/notis-journal/packages/sdk/src/components/MultiSelectActionBar.tsx +278 -0
- package/dist/scaffolds/notis-journal/packages/sdk/src/components/MultiSelectCheckbox.tsx +91 -0
- package/dist/scaffolds/notis-journal/packages/sdk/src/components/MultiSelectDragOverlay.tsx +39 -0
- package/dist/scaffolds/notis-journal/packages/sdk/src/config.ts +145 -0
- package/dist/scaffolds/notis-journal/packages/sdk/src/documents.ts +229 -0
- package/dist/scaffolds/notis-journal/packages/sdk/src/hooks/useBackend.ts +41 -0
- package/dist/scaffolds/notis-journal/packages/sdk/src/hooks/useDatabaseSchema.ts +85 -0
- package/dist/scaffolds/notis-journal/packages/sdk/src/hooks/useDocument.ts +78 -0
- package/dist/scaffolds/notis-journal/packages/sdk/src/hooks/useDocuments.ts +121 -0
- package/dist/scaffolds/notis-journal/packages/sdk/src/hooks/useMultiSelect.ts +539 -0
- package/dist/scaffolds/notis-journal/packages/sdk/src/hooks/useNotis.ts +34 -0
- package/dist/scaffolds/notis-journal/packages/sdk/src/hooks/useNotisNavigation.ts +49 -0
- package/dist/scaffolds/notis-journal/packages/sdk/src/hooks/useTool.ts +64 -0
- package/dist/scaffolds/notis-journal/packages/sdk/src/hooks/useTools.ts +56 -0
- package/dist/scaffolds/notis-journal/packages/sdk/src/hooks/useTopBarSearch.ts +73 -0
- package/dist/scaffolds/notis-journal/packages/sdk/src/hooks/useUpsertDocument.ts +95 -0
- package/dist/scaffolds/notis-journal/packages/sdk/src/index.ts +83 -0
- package/dist/scaffolds/notis-journal/packages/sdk/src/provider.tsx +43 -0
- package/dist/scaffolds/notis-journal/packages/sdk/src/runtime.ts +220 -0
- package/dist/scaffolds/notis-journal/packages/sdk/src/styles.css +186 -0
- package/dist/scaffolds/notis-journal/packages/sdk/src/ui.ts +15 -0
- package/dist/scaffolds/notis-journal/packages/sdk/src/vite.ts +56 -0
- package/dist/scaffolds/notis-journal/packages/sdk/tsconfig.json +15 -0
- package/dist/scaffolds/notis-journal/postcss.config.mjs +8 -0
- package/dist/scaffolds/notis-journal/skills/journal-onboarding/SKILL.md +120 -0
- package/dist/scaffolds/notis-journal/src/dev-main.tsx +58 -0
- package/dist/scaffolds/notis-journal/src/mock-runtime.ts +197 -0
- package/dist/scaffolds/notis-journal/tailwind.config.ts +58 -0
- package/dist/scaffolds/notis-journal/tsconfig.json +23 -0
- package/dist/scaffolds/notis-journal/vite.config.ts +10 -0
- package/dist/scaffolds/notis-notes/CHANGELOG.md +5 -0
- package/dist/scaffolds/notis-notes/app/page.tsx +17 -373
- package/dist/scaffolds/notis-notes/notis.config.ts +0 -1
- package/dist/scaffolds/notis-random/CHANGELOG.md +5 -0
- package/dist/scaffolds/notis-random/notis.config.ts +0 -1
- package/dist/scaffolds/notis-random/src/mock-runtime.ts +1 -0
- package/dist/scaffolds.json +11 -0
- package/package.json +3 -3
- package/skills/notis-apps/SKILL.md +43 -5
- package/skills/notis-apps/cli.md +22 -6
- package/skills/notis-cli/SKILL.md +20 -2
- package/skills/notis-query/cli.md +1 -1
- package/src/command-specs/apps.js +307 -36
- package/src/command-specs/index.js +1 -1
- package/src/command-specs/meta.js +8 -2
- package/src/command-specs/tools.js +50 -37
- package/src/runtime/agent-browser.js +204 -21
- package/src/runtime/app-changelog.js +79 -0
- package/src/runtime/app-dev-server.js +21 -4
- package/src/runtime/app-dev-sessions.js +99 -1
- package/src/runtime/app-platform.js +197 -18
- package/src/runtime/assets/store-screenshot-dark.png +0 -0
- package/src/runtime/desktop-auth.js +93 -0
- package/src/runtime/profiles.js +37 -15
- package/src/runtime/store-screenshot.js +138 -0
- package/src/runtime/transport.js +21 -82
- package/template/.harness/index.html.tmpl +128 -6
- package/template/CHANGELOG.md +5 -0
- package/template/app/page.tsx +11 -41
- package/template/notis.config.ts +0 -1
- package/template/package-lock.json +4137 -0
- package/template/package.json +1 -0
- package/template/packages/sdk/package.json +4 -0
- package/template/packages/sdk/src/components/DocumentEditor.tsx +93 -0
- package/template/packages/sdk/src/components/Markdown.tsx +60 -0
- package/template/packages/sdk/src/components/MultiSelectActionBar.tsx +7 -2
- package/template/packages/sdk/src/config.ts +74 -0
- package/template/packages/sdk/src/documents.ts +229 -0
- package/template/packages/sdk/src/hooks/useDatabaseSchema.ts +85 -0
- package/template/packages/sdk/src/hooks/useDocument.ts +78 -0
- package/template/packages/sdk/src/hooks/useDocuments.ts +121 -0
- package/template/packages/sdk/src/hooks/useMultiSelect.ts +38 -2
- package/template/packages/sdk/src/hooks/useUpsertDocument.ts +54 -9
- package/template/packages/sdk/src/index.ts +30 -1
- package/template/packages/sdk/src/runtime.ts +76 -17
- package/template/packages/sdk/src/styles.css +148 -0
- package/template/packages/sdk/src/hooks/useDatabase.ts +0 -76
|
@@ -9,12 +9,13 @@
|
|
|
9
9
|
import { spawn } from 'node:child_process';
|
|
10
10
|
import { cpSync, existsSync, mkdirSync, readFileSync, writeFileSync, readdirSync, rmSync, statSync } from 'node:fs';
|
|
11
11
|
import { createRequire } from 'node:module';
|
|
12
|
-
import { dirname, join, resolve } from 'node:path';
|
|
12
|
+
import { dirname, join, relative, resolve } from 'node:path';
|
|
13
13
|
import { fileURLToPath } from 'node:url';
|
|
14
14
|
import { gunzipSync } from 'node:zlib';
|
|
15
15
|
|
|
16
16
|
import { usageError } from './errors.js';
|
|
17
17
|
import { validateArtifactBoundary, validateProjectBoundary } from './app-boundary-validator.js';
|
|
18
|
+
import { readAppChangelog } from './app-changelog.js';
|
|
18
19
|
|
|
19
20
|
const NOTIS_DIR = '.notis';
|
|
20
21
|
const STATE_FILE = join(NOTIS_DIR, 'state.json');
|
|
@@ -35,6 +36,7 @@ export const NOTIS_APP_CATEGORIES = [
|
|
|
35
36
|
'Product & Engineering',
|
|
36
37
|
'Personal',
|
|
37
38
|
];
|
|
39
|
+
export const MIN_LISTING_SCREENSHOTS = 3;
|
|
38
40
|
const SOURCE_COPY_EXCLUDES = new Set([
|
|
39
41
|
'node_modules',
|
|
40
42
|
'.notis',
|
|
@@ -236,7 +238,7 @@ export function detectProjectWarnings(projectDir, appConfig = null) {
|
|
|
236
238
|
if (appConfig) {
|
|
237
239
|
try {
|
|
238
240
|
const listing = inspectListingReadiness(projectDir, appConfig);
|
|
239
|
-
warnings.push(...listing.warnings);
|
|
241
|
+
warnings.push(...listing.errors, ...listing.warnings);
|
|
240
242
|
} catch (error) {
|
|
241
243
|
warnings.push(error instanceof Error ? error.message : String(error));
|
|
242
244
|
}
|
|
@@ -291,11 +293,10 @@ function pngDimensions(buffer) {
|
|
|
291
293
|
};
|
|
292
294
|
}
|
|
293
295
|
|
|
294
|
-
function imageAssetMeta(projectDir, relPath, { maxBytes
|
|
296
|
+
function imageAssetMeta(projectDir, relPath, { maxBytes }) {
|
|
295
297
|
const fullPath = join(projectDir, relPath);
|
|
296
298
|
const content = readFileSync(fullPath);
|
|
297
299
|
const dimensions = pngDimensions(content);
|
|
298
|
-
const ratio = dimensions?.height ? dimensions.width / dimensions.height : null;
|
|
299
300
|
const errors = [];
|
|
300
301
|
const warnings = [];
|
|
301
302
|
|
|
@@ -305,10 +306,10 @@ function imageAssetMeta(projectDir, relPath, { maxBytes, recommendedRatio = 1.6
|
|
|
305
306
|
if (content.length > maxBytes) {
|
|
306
307
|
errors.push(`${relPath} must be ${Math.round(maxBytes / 1024 / 1024)} MB or smaller.`);
|
|
307
308
|
}
|
|
308
|
-
if (
|
|
309
|
-
errors.push(`${relPath}
|
|
310
|
-
} else if (
|
|
311
|
-
|
|
309
|
+
if (!dimensions) {
|
|
310
|
+
errors.push(`${relPath} is not a valid PNG file.`);
|
|
311
|
+
} else if (dimensions.width !== 2000 || dimensions.height !== 1250) {
|
|
312
|
+
errors.push(`${relPath} must be exactly 2000x1250 pixels.`);
|
|
312
313
|
}
|
|
313
314
|
|
|
314
315
|
return {
|
|
@@ -346,33 +347,135 @@ export function discoverMetadataAssets(projectDir) {
|
|
|
346
347
|
};
|
|
347
348
|
}
|
|
348
349
|
|
|
350
|
+
function normalizeConfiguredScreenshots(appConfig = null) {
|
|
351
|
+
if (!Array.isArray(appConfig?.screenshots) || appConfig.screenshots.length === 0) {
|
|
352
|
+
return null;
|
|
353
|
+
}
|
|
354
|
+
if (appConfig.screenshots.length > 6) {
|
|
355
|
+
throw usageError('Configure at most six listing screenshots.');
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
const seenPaths = new Set();
|
|
359
|
+
return appConfig.screenshots.map((entry, index) => {
|
|
360
|
+
const path = typeof entry?.path === 'string' ? entry.path.trim().replace(/\\/g, '/') : '';
|
|
361
|
+
const alt = typeof entry?.alt === 'string' ? entry.alt.trim() : '';
|
|
362
|
+
const route = typeof entry?.route === 'string' ? entry.route.trim() : '';
|
|
363
|
+
const scenario = typeof entry?.scenario === 'string' ? entry.scenario.trim() : '';
|
|
364
|
+
const focus = typeof entry?.focus === 'string' ? entry.focus.trim() : '';
|
|
365
|
+
const theme = typeof entry?.theme === 'string' ? entry.theme.trim().toLowerCase() : '';
|
|
366
|
+
if (!/^metadata\/screenshot-\d+\.png$/i.test(path)) {
|
|
367
|
+
throw usageError(`screenshots[${index}].path must match metadata/screenshot-N.png.`);
|
|
368
|
+
}
|
|
369
|
+
const expectedPath = `metadata/screenshot-${index + 1}.png`;
|
|
370
|
+
if (path !== expectedPath) {
|
|
371
|
+
throw usageError(`screenshots[${index}].path must be ${expectedPath} so capture order stays stable.`);
|
|
372
|
+
}
|
|
373
|
+
if (seenPaths.has(path)) {
|
|
374
|
+
throw usageError(`Duplicate listing screenshot path: ${path}`);
|
|
375
|
+
}
|
|
376
|
+
if (theme && theme !== 'light' && theme !== 'dark') {
|
|
377
|
+
throw usageError(`screenshots[${index}].theme must be light or dark.`);
|
|
378
|
+
}
|
|
379
|
+
seenPaths.add(path);
|
|
380
|
+
return {
|
|
381
|
+
path,
|
|
382
|
+
alt,
|
|
383
|
+
route: route || null,
|
|
384
|
+
scenario: scenario || null,
|
|
385
|
+
focus: focus || null,
|
|
386
|
+
theme: theme || 'light',
|
|
387
|
+
};
|
|
388
|
+
});
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
/** Resolve listing screenshot files in their configured editorial order. */
|
|
392
|
+
export function resolveListingScreenshots(projectDir, appConfig = null) {
|
|
393
|
+
const discovered = discoverMetadataAssets(projectDir).screenshots;
|
|
394
|
+
const configured = normalizeConfiguredScreenshots(appConfig);
|
|
395
|
+
if (!configured) {
|
|
396
|
+
return discovered.map((asset) => ({
|
|
397
|
+
...asset,
|
|
398
|
+
alt: null,
|
|
399
|
+
route: null,
|
|
400
|
+
scenario: null,
|
|
401
|
+
focus: null,
|
|
402
|
+
theme: 'light',
|
|
403
|
+
}));
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
const byPath = new Map(discovered.map((asset) => [asset.path.replace(/\\/g, '/'), asset]));
|
|
407
|
+
return configured.map((entry) => {
|
|
408
|
+
const asset = byPath.get(entry.path);
|
|
409
|
+
if (!asset) {
|
|
410
|
+
return {
|
|
411
|
+
...entry,
|
|
412
|
+
content_type: 'image/png',
|
|
413
|
+
bytes: null,
|
|
414
|
+
width: null,
|
|
415
|
+
height: null,
|
|
416
|
+
errors: [`${entry.path} is configured but the file does not exist.`],
|
|
417
|
+
warnings: [],
|
|
418
|
+
};
|
|
419
|
+
}
|
|
420
|
+
return { ...asset, ...entry };
|
|
421
|
+
});
|
|
422
|
+
}
|
|
423
|
+
|
|
349
424
|
export function inspectListingReadiness(projectDir, appConfig = null) {
|
|
350
425
|
const config = appConfig || {};
|
|
351
426
|
const warnings = [];
|
|
352
427
|
const errors = [];
|
|
353
|
-
const
|
|
428
|
+
const screenshots = resolveListingScreenshots(projectDir, config);
|
|
429
|
+
const metadata = { screenshots };
|
|
354
430
|
const categories = normalizeCategories(config.categories || []);
|
|
431
|
+
const changelog = readAppChangelog(projectDir);
|
|
432
|
+
errors.push(...changelog.errors);
|
|
433
|
+
|
|
434
|
+
const packagePath = join(projectDir, 'package.json');
|
|
435
|
+
if (!existsSync(packagePath)) {
|
|
436
|
+
errors.push('package.json is required for Store publication.');
|
|
437
|
+
} else {
|
|
438
|
+
try {
|
|
439
|
+
const packageJson = JSON.parse(readFileSync(packagePath, 'utf-8'));
|
|
440
|
+
const notisAppVersion = String(packageJson.notisAppVersion || '').trim();
|
|
441
|
+
if (!/^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?(?:\+[0-9A-Za-z.-]+)?$/.test(notisAppVersion)) {
|
|
442
|
+
errors.push('package.json must include a semver `notisAppVersion` (for example, "0.1.0").');
|
|
443
|
+
}
|
|
444
|
+
} catch {
|
|
445
|
+
errors.push('package.json must contain valid JSON for Store publication.');
|
|
446
|
+
}
|
|
447
|
+
}
|
|
355
448
|
|
|
356
449
|
if (!String(config.tagline || '').trim()) {
|
|
357
|
-
|
|
450
|
+
errors.push('Listing tagline missing in notis.config.ts.');
|
|
358
451
|
}
|
|
359
452
|
if (categories.length === 0) {
|
|
360
|
-
|
|
453
|
+
errors.push(`Listing category missing in notis.config.ts. Use one of: ${NOTIS_APP_CATEGORIES.join(', ')}.`);
|
|
361
454
|
}
|
|
362
|
-
if (metadata.screenshots.length <
|
|
363
|
-
|
|
455
|
+
if (metadata.screenshots.length < MIN_LISTING_SCREENSHOTS) {
|
|
456
|
+
errors.push(`Listing screenshots: ${metadata.screenshots.length}/${MIN_LISTING_SCREENSHOTS} minimum in metadata/screenshot-N.png. Run \`notis apps screenshot\` to generate them.`);
|
|
364
457
|
}
|
|
365
458
|
for (const screenshot of metadata.screenshots) {
|
|
366
459
|
errors.push(...screenshot.errors);
|
|
367
460
|
warnings.push(...screenshot.warnings);
|
|
461
|
+
if (!screenshot.alt) {
|
|
462
|
+
errors.push(`${screenshot.path} is missing descriptive alt text in notis.config.ts -> screenshots.`);
|
|
463
|
+
}
|
|
368
464
|
}
|
|
369
465
|
|
|
370
466
|
return {
|
|
371
|
-
ready:
|
|
467
|
+
ready:
|
|
468
|
+
errors.length === 0 &&
|
|
469
|
+
Boolean(String(config.tagline || '').trim()) &&
|
|
470
|
+
categories.length > 0 &&
|
|
471
|
+
metadata.screenshots.length >= MIN_LISTING_SCREENSHOTS &&
|
|
472
|
+
metadata.screenshots.every((screenshot) => Boolean(screenshot.alt)) &&
|
|
473
|
+
changelog.entries.length > 0,
|
|
372
474
|
warnings,
|
|
373
475
|
errors,
|
|
374
476
|
metadata,
|
|
375
477
|
categories,
|
|
478
|
+
changelog,
|
|
376
479
|
};
|
|
377
480
|
}
|
|
378
481
|
|
|
@@ -606,9 +709,15 @@ export function generateManifest(appConfig, projectDir) {
|
|
|
606
709
|
|
|
607
710
|
const databases = appConfig.databases || [];
|
|
608
711
|
const categories = normalizeCategories(appConfig.categories || []);
|
|
609
|
-
const metadata =
|
|
712
|
+
const metadata = { screenshots: resolveListingScreenshots(projectDir, appConfig) };
|
|
610
713
|
const appSlug = safeKebab(appConfig.name);
|
|
611
714
|
const displayTitle = appConfig.title || appConfig.displayName || appConfig.name;
|
|
715
|
+
const skills = (Array.isArray(appConfig.skills) ? appConfig.skills : []).map((skill) => ({
|
|
716
|
+
key: skill.key,
|
|
717
|
+
path: String(skill.path || '').replace(/^\.\/+/, ''),
|
|
718
|
+
name: skill.name,
|
|
719
|
+
description: skill.description || null,
|
|
720
|
+
}));
|
|
612
721
|
const listingMedia = {
|
|
613
722
|
screenshots: metadata.screenshots.map((screenshot) => ({
|
|
614
723
|
path: screenshot.path,
|
|
@@ -616,8 +725,27 @@ export function generateManifest(appConfig, projectDir) {
|
|
|
616
725
|
width: screenshot.width,
|
|
617
726
|
height: screenshot.height,
|
|
618
727
|
bytes: screenshot.bytes,
|
|
728
|
+
alt: screenshot.alt || null,
|
|
729
|
+
theme: screenshot.theme || 'light',
|
|
619
730
|
})),
|
|
620
731
|
};
|
|
732
|
+
const changelog = readAppChangelog(projectDir);
|
|
733
|
+
if (changelog.exists && changelog.errors.length > 0) {
|
|
734
|
+
throw usageError(changelog.errors.join('\n'));
|
|
735
|
+
}
|
|
736
|
+
const latestChangelogEntry = changelog.entries[0] || null;
|
|
737
|
+
const legacyVersionNotes = latestChangelogEntry?.body
|
|
738
|
+
|| appConfig.versionNotes
|
|
739
|
+
|| appConfig.version_notes
|
|
740
|
+
|| null;
|
|
741
|
+
let releaseVersion = null;
|
|
742
|
+
try {
|
|
743
|
+
const packageJson = JSON.parse(readFileSync(join(projectDir, 'package.json'), 'utf-8'));
|
|
744
|
+
releaseVersion = String(packageJson.notisAppVersion || '').trim() || null;
|
|
745
|
+
} catch {
|
|
746
|
+
// Store readiness reports missing or malformed package metadata. Ordinary
|
|
747
|
+
// development builds remain usable without a Store release version.
|
|
748
|
+
}
|
|
621
749
|
|
|
622
750
|
return {
|
|
623
751
|
version: 1,
|
|
@@ -632,14 +760,23 @@ export function generateManifest(appConfig, projectDir) {
|
|
|
632
760
|
tagline: appConfig.tagline || null,
|
|
633
761
|
categories,
|
|
634
762
|
author: appConfig.author || null,
|
|
635
|
-
|
|
763
|
+
release_version: releaseVersion,
|
|
764
|
+
version_notes: legacyVersionNotes,
|
|
636
765
|
},
|
|
637
766
|
listing: {
|
|
638
767
|
title: displayTitle,
|
|
639
768
|
tagline: appConfig.tagline || null,
|
|
640
769
|
categories,
|
|
641
770
|
author: appConfig.author || null,
|
|
642
|
-
version_notes:
|
|
771
|
+
version_notes: legacyVersionNotes,
|
|
772
|
+
...(changelog.exists
|
|
773
|
+
? {
|
|
774
|
+
changelog: {
|
|
775
|
+
source_path: changelog.source_path,
|
|
776
|
+
entries: changelog.entries,
|
|
777
|
+
},
|
|
778
|
+
}
|
|
779
|
+
: {}),
|
|
643
780
|
media: listingMedia,
|
|
644
781
|
},
|
|
645
782
|
metadata: listingMedia,
|
|
@@ -650,9 +787,47 @@ export function generateManifest(appConfig, projectDir) {
|
|
|
650
787
|
},
|
|
651
788
|
databases,
|
|
652
789
|
tools: appConfig.tools || [],
|
|
790
|
+
skills,
|
|
791
|
+
onboarding: appConfig.onboarding || null,
|
|
653
792
|
};
|
|
654
793
|
}
|
|
655
794
|
|
|
795
|
+
export function resolveConfiguredAppSkills(appConfig, projectDir) {
|
|
796
|
+
const configured = Array.isArray(appConfig.skills) ? appConfig.skills : [];
|
|
797
|
+
const projectRoot = resolve(projectDir);
|
|
798
|
+
const seenKeys = new Set();
|
|
799
|
+
|
|
800
|
+
return configured.map((skill, index) => {
|
|
801
|
+
const key = typeof skill?.key === 'string' ? skill.key.trim() : '';
|
|
802
|
+
const sourcePath = typeof skill?.path === 'string' ? skill.path.trim() : '';
|
|
803
|
+
const name = typeof skill?.name === 'string' ? skill.name.trim() : '';
|
|
804
|
+
if (!key || !sourcePath || !name) {
|
|
805
|
+
throw usageError(`skills[${index}] must define non-empty key, path, and name values.`);
|
|
806
|
+
}
|
|
807
|
+
if (seenKeys.has(key)) {
|
|
808
|
+
throw usageError(`Duplicate app skill key: ${key}`);
|
|
809
|
+
}
|
|
810
|
+
seenKeys.add(key);
|
|
811
|
+
|
|
812
|
+
const absolutePath = resolve(projectRoot, sourcePath);
|
|
813
|
+
const relativePath = relative(projectRoot, absolutePath).replace(/\\/g, '/');
|
|
814
|
+
if (!relativePath || relativePath.startsWith('../') || relativePath === '..') {
|
|
815
|
+
throw usageError(`App skill path must stay inside the project: ${sourcePath}`);
|
|
816
|
+
}
|
|
817
|
+
if (!existsSync(absolutePath) || !statSync(absolutePath).isFile()) {
|
|
818
|
+
throw usageError(`App skill entrypoint not found: ${sourcePath}`);
|
|
819
|
+
}
|
|
820
|
+
|
|
821
|
+
return {
|
|
822
|
+
key,
|
|
823
|
+
path: relativePath,
|
|
824
|
+
name,
|
|
825
|
+
description: typeof skill.description === 'string' ? skill.description.trim() : null,
|
|
826
|
+
skill_md: readFileSync(absolutePath, 'utf8'),
|
|
827
|
+
};
|
|
828
|
+
});
|
|
829
|
+
}
|
|
830
|
+
|
|
656
831
|
/**
|
|
657
832
|
* Build the app bundle: generate entry file, run `vite build`, package into .notis/output/.
|
|
658
833
|
*/
|
|
@@ -695,10 +870,14 @@ export function readManifest(projectDir) {
|
|
|
695
870
|
|
|
696
871
|
function copyMetadataAssets(projectDir) {
|
|
697
872
|
const metadataDir = join(projectDir, METADATA_DIR);
|
|
873
|
+
const outputMetadataDir = join(projectDir, OUTPUT_DIR, METADATA_DIR);
|
|
874
|
+
// Vite clears the bundle directory, not sibling listing media. Always make
|
|
875
|
+
// the packaged metadata an exact reflection of the source tree so removed
|
|
876
|
+
// placeholders can never survive into a later deploy.
|
|
877
|
+
rmSync(outputMetadataDir, { recursive: true, force: true });
|
|
698
878
|
if (!existsSync(metadataDir)) {
|
|
699
879
|
return;
|
|
700
880
|
}
|
|
701
|
-
const outputMetadataDir = join(projectDir, OUTPUT_DIR, METADATA_DIR);
|
|
702
881
|
mkdirSync(outputMetadataDir, { recursive: true });
|
|
703
882
|
for (const entry of readdirSync(metadataDir, { withFileTypes: true })) {
|
|
704
883
|
if (!entry.isFile()) continue;
|
|
Binary file
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import { CliError, EXIT_CODES } from './errors.js';
|
|
2
|
+
|
|
3
|
+
function isPidRunning(pid) {
|
|
4
|
+
if (!Number.isInteger(pid) || pid <= 0) {
|
|
5
|
+
return false;
|
|
6
|
+
}
|
|
7
|
+
try {
|
|
8
|
+
process.kill(pid, 0);
|
|
9
|
+
return true;
|
|
10
|
+
} catch (error) {
|
|
11
|
+
return error?.code === 'EPERM';
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function quoteShellArgument(value) {
|
|
16
|
+
return `'${String(value).replace(/'/g, `'"'"'`)}'`;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function defaultDesktopAppName(apiBase) {
|
|
20
|
+
try {
|
|
21
|
+
return new URL(apiBase).hostname === 'api-beta.notis.ai' ? 'Notis Beta' : 'Notis';
|
|
22
|
+
} catch {
|
|
23
|
+
return 'Notis';
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function getDesktopAuthRecovery(runtime) {
|
|
28
|
+
const desktopRunning = isPidRunning(runtime.desktopPid);
|
|
29
|
+
const appName = runtime.desktopAppName || defaultDesktopAppName(runtime.apiBase);
|
|
30
|
+
let command = 'Start the Notis desktop app';
|
|
31
|
+
if (process.platform === 'darwin') {
|
|
32
|
+
command = `open -a ${quoteShellArgument(appName)}`;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
return {
|
|
36
|
+
desktopRunning,
|
|
37
|
+
appName,
|
|
38
|
+
hints: [
|
|
39
|
+
{
|
|
40
|
+
command,
|
|
41
|
+
reason: desktopRunning
|
|
42
|
+
? `Bring ${appName} forward so it can renew CLI authentication, then retry`
|
|
43
|
+
: `Start ${appName} to renew expired CLI authentication, then retry`,
|
|
44
|
+
},
|
|
45
|
+
{
|
|
46
|
+
command: 'notis doctor',
|
|
47
|
+
reason: 'Retry the auth and API checks after the desktop app is ready',
|
|
48
|
+
},
|
|
49
|
+
],
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function createExpiredAuthError(runtime) {
|
|
54
|
+
if (runtime.credentialSource === 'env') {
|
|
55
|
+
return new CliError({
|
|
56
|
+
code: 'auth_expired',
|
|
57
|
+
message: 'NOTIS_JWT is expired',
|
|
58
|
+
exitCode: EXIT_CODES.auth,
|
|
59
|
+
details: { credential_source: 'env' },
|
|
60
|
+
hints: [
|
|
61
|
+
{
|
|
62
|
+
command: 'Set NOTIS_JWT to a fresh token',
|
|
63
|
+
reason: 'The explicit environment credential overrides desktop-managed auth',
|
|
64
|
+
},
|
|
65
|
+
],
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const recovery = getDesktopAuthRecovery(runtime);
|
|
70
|
+
return new CliError({
|
|
71
|
+
code: 'auth_expired',
|
|
72
|
+
message: 'Notis CLI authentication has expired',
|
|
73
|
+
exitCode: EXIT_CODES.auth,
|
|
74
|
+
details: {
|
|
75
|
+
credential_source: 'desktop',
|
|
76
|
+
desktop_running: recovery.desktopRunning,
|
|
77
|
+
desktop_app_name: recovery.appName,
|
|
78
|
+
},
|
|
79
|
+
hints: recovery.hints,
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export function createInvalidAuthHints(runtime) {
|
|
84
|
+
if (runtime?.credentialSource === 'env') {
|
|
85
|
+
return [
|
|
86
|
+
{
|
|
87
|
+
command: 'Set NOTIS_JWT to a fresh token',
|
|
88
|
+
reason: 'The explicit environment credential was rejected',
|
|
89
|
+
},
|
|
90
|
+
];
|
|
91
|
+
}
|
|
92
|
+
return getDesktopAuthRecovery(runtime || {}).hints;
|
|
93
|
+
}
|
package/src/runtime/profiles.js
CHANGED
|
@@ -2,6 +2,7 @@ import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
|
2
2
|
import { homedir } from 'node:os';
|
|
3
3
|
import { join } from 'node:path';
|
|
4
4
|
import { CliError, EXIT_CODES } from './errors.js';
|
|
5
|
+
import { getDesktopAuthRecovery } from './desktop-auth.js';
|
|
5
6
|
|
|
6
7
|
export const CONFIG_DIR = join(homedir(), '.notis');
|
|
7
8
|
export const CONFIG_FILE = join(CONFIG_DIR, 'config.json');
|
|
@@ -37,6 +38,9 @@ export function normalizeConfig(rawConfig = {}) {
|
|
|
37
38
|
typeof profile.access_expires_at === 'number' ? profile.access_expires_at : undefined,
|
|
38
39
|
refresh_expires_at:
|
|
39
40
|
typeof profile.refresh_expires_at === 'number' ? profile.refresh_expires_at : undefined,
|
|
41
|
+
desktop_app_name:
|
|
42
|
+
typeof profile.desktop_app_name === 'string' ? profile.desktop_app_name : undefined,
|
|
43
|
+
desktop_pid: typeof profile.desktop_pid === 'number' ? profile.desktop_pid : undefined,
|
|
40
44
|
};
|
|
41
45
|
}
|
|
42
46
|
|
|
@@ -65,6 +69,9 @@ export function normalizeConfig(rawConfig = {}) {
|
|
|
65
69
|
typeof raw.access_expires_at === 'number' ? raw.access_expires_at : undefined,
|
|
66
70
|
refresh_expires_at:
|
|
67
71
|
typeof raw.refresh_expires_at === 'number' ? raw.refresh_expires_at : undefined,
|
|
72
|
+
desktop_app_name:
|
|
73
|
+
typeof raw.desktop_app_name === 'string' ? raw.desktop_app_name : undefined,
|
|
74
|
+
desktop_pid: typeof raw.desktop_pid === 'number' ? raw.desktop_pid : undefined,
|
|
68
75
|
},
|
|
69
76
|
},
|
|
70
77
|
};
|
|
@@ -199,34 +206,30 @@ export function resolveRuntimeProfile(globalOptions = {}, { requireAuth = true }
|
|
|
199
206
|
const timeoutMs = resolveTimeoutMs(globalOptions);
|
|
200
207
|
|
|
201
208
|
if (requireAuth && !jwt) {
|
|
209
|
+
const recovery = getDesktopAuthRecovery({
|
|
210
|
+
apiBase,
|
|
211
|
+
desktopAppName: profile.desktop_app_name,
|
|
212
|
+
desktopPid: profile.desktop_pid,
|
|
213
|
+
});
|
|
202
214
|
throw new CliError({
|
|
203
215
|
code: 'auth_missing',
|
|
204
216
|
message: `No JWT configured for profile ${profileName}`,
|
|
205
217
|
exitCode: EXIT_CODES.auth,
|
|
206
|
-
hints:
|
|
207
|
-
{
|
|
208
|
-
command: 'Open the Notis desktop app and sign in',
|
|
209
|
-
reason: 'Set NOTIS_JWT or sign in through the Notis desktop app',
|
|
210
|
-
},
|
|
211
|
-
],
|
|
218
|
+
hints: recovery.hints,
|
|
212
219
|
});
|
|
213
220
|
}
|
|
214
221
|
|
|
215
|
-
// An explicit NOTIS_JWT is a complete credential override
|
|
216
|
-
//
|
|
217
|
-
// stale or belong to a different session). Without this, a long-lived shell with an
|
|
218
|
-
// expired profile refresh_token fails every command with invalid_grant even though
|
|
219
|
-
// the supplied NOTIS_JWT is valid.
|
|
222
|
+
// An explicit NOTIS_JWT is a complete credential override. Use it verbatim
|
|
223
|
+
// and never replace it with a token later synced by the desktop profile.
|
|
220
224
|
const usingEnvJwt = Boolean(process.env.NOTIS_JWT);
|
|
221
225
|
return {
|
|
222
226
|
config,
|
|
223
227
|
profileName,
|
|
224
228
|
apiBase,
|
|
225
229
|
jwt,
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
refreshExpiresAt: usingEnvJwt ? undefined : profile.refresh_expires_at,
|
|
230
|
+
credentialSource: usingEnvJwt ? 'env' : 'profile',
|
|
231
|
+
desktopAppName: usingEnvJwt ? undefined : profile.desktop_app_name,
|
|
232
|
+
desktopPid: usingEnvJwt ? undefined : profile.desktop_pid,
|
|
230
233
|
agentMode,
|
|
231
234
|
nonInteractive,
|
|
232
235
|
outputMode,
|
|
@@ -234,6 +237,25 @@ export function resolveRuntimeProfile(globalOptions = {}, { requireAuth = true }
|
|
|
234
237
|
};
|
|
235
238
|
}
|
|
236
239
|
|
|
240
|
+
export function getJwtExpiration(jwt) {
|
|
241
|
+
if (typeof jwt !== 'string' || !jwt) {
|
|
242
|
+
return null;
|
|
243
|
+
}
|
|
244
|
+
try {
|
|
245
|
+
const parts = jwt.split('.');
|
|
246
|
+
if (parts.length !== 3) return null;
|
|
247
|
+
const payload = JSON.parse(Buffer.from(parts[1], 'base64url').toString());
|
|
248
|
+
return typeof payload.exp === 'number' ? payload.exp : null;
|
|
249
|
+
} catch {
|
|
250
|
+
return null;
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
export function isJwtExpired(jwt, nowSeconds = Math.floor(Date.now() / 1000)) {
|
|
255
|
+
const expiration = getJwtExpiration(jwt);
|
|
256
|
+
return expiration !== null && expiration <= nowSeconds;
|
|
257
|
+
}
|
|
258
|
+
|
|
237
259
|
export function workspacePath(appId) {
|
|
238
260
|
return join(WORKSPACE_DIR, appId);
|
|
239
261
|
}
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
import sharp from 'sharp';
|
|
2
|
+
import { fileURLToPath } from 'node:url';
|
|
3
|
+
|
|
4
|
+
const STORE_SCREENSHOT_ASPECT = 16 / 10;
|
|
5
|
+
const SHARED_BACKDROP_PATH = fileURLToPath(
|
|
6
|
+
new URL('./assets/store-screenshot-dark.png', import.meta.url),
|
|
7
|
+
);
|
|
8
|
+
|
|
9
|
+
const ACCENT_PALETTES = {
|
|
10
|
+
blue: { glow: '#4f7cff', glow2: '#274690' },
|
|
11
|
+
violet: { glow: '#9b87f5', glow2: '#5038a1' },
|
|
12
|
+
emerald: { glow: '#34d399', glow2: '#176b55' },
|
|
13
|
+
amber: { glow: '#f2a94a', glow2: '#8b4f20' },
|
|
14
|
+
rose: { glow: '#fb7185', glow2: '#8f3048' },
|
|
15
|
+
sky: { glow: '#38bdf8', glow2: '#176887' },
|
|
16
|
+
fuchsia: { glow: '#d946ef', glow2: '#772786' },
|
|
17
|
+
teal: { glow: '#2dd4bf', glow2: '#176e68' },
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
const ACCENT_NAMES = Object.keys(ACCENT_PALETTES);
|
|
21
|
+
|
|
22
|
+
function stableHash(value) {
|
|
23
|
+
let hash = 2166136261;
|
|
24
|
+
for (const character of String(value || 'notis-app')) {
|
|
25
|
+
hash ^= character.codePointAt(0);
|
|
26
|
+
hash = Math.imul(hash, 16777619);
|
|
27
|
+
}
|
|
28
|
+
return hash >>> 0;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function resolveStoreScreenshotAccent(accent, seed = 'notis-app') {
|
|
32
|
+
if (accent && ACCENT_PALETTES[accent]) {
|
|
33
|
+
return accent;
|
|
34
|
+
}
|
|
35
|
+
return ACCENT_NAMES[stableHash(seed) % ACCENT_NAMES.length];
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function storeScreenshotLayout(width = 2000, height = 1250) {
|
|
39
|
+
const horizontalInset = Math.max(48, Math.round(width * 0.06));
|
|
40
|
+
const verticalInset = Math.max(40, Math.round(height * 0.06));
|
|
41
|
+
let cardWidth = width - horizontalInset * 2;
|
|
42
|
+
let cardHeight = Math.round(cardWidth / STORE_SCREENSHOT_ASPECT);
|
|
43
|
+
const maximumCardHeight = height - verticalInset * 2;
|
|
44
|
+
if (cardHeight > maximumCardHeight) {
|
|
45
|
+
cardHeight = maximumCardHeight;
|
|
46
|
+
cardWidth = Math.round(cardHeight * STORE_SCREENSHOT_ASPECT);
|
|
47
|
+
}
|
|
48
|
+
const cardLeft = Math.round((width - cardWidth) / 2);
|
|
49
|
+
const cardTop = Math.round((height - cardHeight) / 2) - Math.round(height * 0.008);
|
|
50
|
+
return {
|
|
51
|
+
width,
|
|
52
|
+
height,
|
|
53
|
+
cardWidth,
|
|
54
|
+
cardHeight,
|
|
55
|
+
cardLeft,
|
|
56
|
+
cardTop,
|
|
57
|
+
radius: Math.max(18, Math.round(width * 0.016)),
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
async function backdropImage(layout) {
|
|
62
|
+
return sharp(SHARED_BACKDROP_PATH)
|
|
63
|
+
.resize(layout.width, layout.height, { fit: 'cover', position: 'centre' })
|
|
64
|
+
.png()
|
|
65
|
+
.toBuffer();
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function roundedRectSvg(width, height, radius, { fill = '#ffffff', stroke = null } = {}) {
|
|
69
|
+
const strokeAttribute = stroke
|
|
70
|
+
? ` fill="none" stroke="${stroke}" stroke-width="2"`
|
|
71
|
+
: ` fill="${fill}"`;
|
|
72
|
+
return Buffer.from(
|
|
73
|
+
`<svg xmlns="http://www.w3.org/2000/svg" width="${width}" height="${height}"><rect x="1" y="1" width="${width - 2}" height="${height - 2}" rx="${radius}" ry="${radius}"${strokeAttribute}/></svg>`,
|
|
74
|
+
);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Composite a truthful browser capture into the deterministic Store frame.
|
|
79
|
+
* The source pixels are only resized/cropped; the app UI is never redrawn.
|
|
80
|
+
*/
|
|
81
|
+
export async function composeStoreScreenshot({
|
|
82
|
+
inputPath,
|
|
83
|
+
outputPath,
|
|
84
|
+
width = 2000,
|
|
85
|
+
height = 1250,
|
|
86
|
+
accent = null,
|
|
87
|
+
seed = 'notis-app',
|
|
88
|
+
focused = false,
|
|
89
|
+
theme = 'light',
|
|
90
|
+
}) {
|
|
91
|
+
const resolvedTheme = theme === 'dark' ? 'dark' : 'light';
|
|
92
|
+
const layout = storeScreenshotLayout(width, height);
|
|
93
|
+
const resolvedAccent = resolveStoreScreenshotAccent(accent, seed);
|
|
94
|
+
|
|
95
|
+
const roundedCapture = await sharp(inputPath)
|
|
96
|
+
.resize(layout.cardWidth, layout.cardHeight, {
|
|
97
|
+
fit: 'cover',
|
|
98
|
+
position: focused ? 'top' : 'centre',
|
|
99
|
+
})
|
|
100
|
+
.composite([{
|
|
101
|
+
input: roundedRectSvg(layout.cardWidth, layout.cardHeight, layout.radius),
|
|
102
|
+
blend: 'dest-in',
|
|
103
|
+
}])
|
|
104
|
+
.png()
|
|
105
|
+
.toBuffer();
|
|
106
|
+
|
|
107
|
+
const border = roundedRectSvg(
|
|
108
|
+
layout.cardWidth,
|
|
109
|
+
layout.cardHeight,
|
|
110
|
+
layout.radius,
|
|
111
|
+
{ stroke: resolvedTheme === 'dark' ? '#FFFFFF2A' : '#0F172A22' },
|
|
112
|
+
);
|
|
113
|
+
|
|
114
|
+
await sharp(await backdropImage(layout))
|
|
115
|
+
.composite([
|
|
116
|
+
{ input: roundedCapture, left: layout.cardLeft, top: layout.cardTop },
|
|
117
|
+
{ input: border, left: layout.cardLeft, top: layout.cardTop },
|
|
118
|
+
])
|
|
119
|
+
.png({
|
|
120
|
+
compressionLevel: 9,
|
|
121
|
+
adaptiveFiltering: true,
|
|
122
|
+
})
|
|
123
|
+
.toFile(outputPath);
|
|
124
|
+
|
|
125
|
+
return {
|
|
126
|
+
mode: 'framed',
|
|
127
|
+
accent: resolvedAccent,
|
|
128
|
+
focused: Boolean(focused),
|
|
129
|
+
theme: resolvedTheme,
|
|
130
|
+
card: {
|
|
131
|
+
left: layout.cardLeft,
|
|
132
|
+
top: layout.cardTop,
|
|
133
|
+
width: layout.cardWidth,
|
|
134
|
+
height: layout.cardHeight,
|
|
135
|
+
radius: layout.radius,
|
|
136
|
+
},
|
|
137
|
+
};
|
|
138
|
+
}
|