@notis_ai/cli 0.2.4 → 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 +47 -5
- package/dist/scaffolds/notis-database/CHANGELOG.md +5 -0
- package/dist/scaffolds/notis-database/metadata/screenshot-1.png +0 -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 +325 -543
- package/dist/scaffolds/notis-notes/metadata/screenshot-1.png +0 -0
- package/dist/scaffolds/notis-notes/notis.config.ts +0 -1
- package/dist/scaffolds/notis-random/CHANGELOG.md +5 -0
- package/dist/scaffolds/notis-random/metadata/screenshot-1.png +0 -0
- package/dist/scaffolds/notis-random/metadata/screenshot-2.png +0 -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 +64 -6
- package/skills/notis-apps/cli.md +38 -2
- package/skills/notis-cli/SKILL.md +20 -2
- package/skills/notis-query/cli.md +1 -1
- package/src/command-specs/apps.js +529 -16
- 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 +273 -1
- package/src/runtime/app-changelog.js +79 -0
- package/src/runtime/app-dev-server.js +113 -6
- package/src/runtime/app-dev-sessions.js +176 -12
- package/src/runtime/app-platform.js +200 -39
- 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/dist/scaffolds/notis-database/metadata/cover.png +0 -0
- package/dist/scaffolds/notis-database/metadata/screenshot-2.png +0 -0
- package/dist/scaffolds/notis-database/metadata/screenshot-3.png +0 -0
- package/dist/scaffolds/notis-notes/metadata/cover.png +0 -0
- package/dist/scaffolds/notis-notes/metadata/screenshot-2.png +0 -0
- package/dist/scaffolds/notis-notes/metadata/screenshot-3.png +0 -0
- package/dist/scaffolds/notis-random/metadata/cover.png +0 -0
- package/dist/scaffolds/notis-random/metadata/screenshot-3.png +0 -0
- package/template/metadata/cover.png +0 -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 {
|
|
@@ -324,10 +325,6 @@ function imageAssetMeta(projectDir, relPath, { maxBytes, recommendedRatio = 1.6
|
|
|
324
325
|
|
|
325
326
|
export function discoverMetadataAssets(projectDir) {
|
|
326
327
|
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
328
|
const screenshots = [];
|
|
332
329
|
|
|
333
330
|
if (existsSync(metadataDir)) {
|
|
@@ -346,44 +343,139 @@ export function discoverMetadataAssets(projectDir) {
|
|
|
346
343
|
|
|
347
344
|
screenshots.sort((a, b) => a.index - b.index);
|
|
348
345
|
return {
|
|
349
|
-
cover,
|
|
350
346
|
screenshots: screenshots.slice(0, 6),
|
|
351
347
|
};
|
|
352
348
|
}
|
|
353
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
|
+
|
|
354
424
|
export function inspectListingReadiness(projectDir, appConfig = null) {
|
|
355
425
|
const config = appConfig || {};
|
|
356
426
|
const warnings = [];
|
|
357
427
|
const errors = [];
|
|
358
|
-
const
|
|
428
|
+
const screenshots = resolveListingScreenshots(projectDir, config);
|
|
429
|
+
const metadata = { screenshots };
|
|
359
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
|
+
}
|
|
360
448
|
|
|
361
449
|
if (!String(config.tagline || '').trim()) {
|
|
362
|
-
|
|
450
|
+
errors.push('Listing tagline missing in notis.config.ts.');
|
|
363
451
|
}
|
|
364
452
|
if (categories.length === 0) {
|
|
365
|
-
|
|
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);
|
|
453
|
+
errors.push(`Listing category missing in notis.config.ts. Use one of: ${NOTIS_APP_CATEGORIES.join(', ')}.`);
|
|
372
454
|
}
|
|
373
|
-
if (metadata.screenshots.length <
|
|
374
|
-
|
|
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.`);
|
|
375
457
|
}
|
|
376
458
|
for (const screenshot of metadata.screenshots) {
|
|
377
459
|
errors.push(...screenshot.errors);
|
|
378
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
|
+
}
|
|
379
464
|
}
|
|
380
465
|
|
|
381
466
|
return {
|
|
382
|
-
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,
|
|
383
474
|
warnings,
|
|
384
475
|
errors,
|
|
385
476
|
metadata,
|
|
386
477
|
categories,
|
|
478
|
+
changelog,
|
|
387
479
|
};
|
|
388
480
|
}
|
|
389
481
|
|
|
@@ -617,27 +709,43 @@ export function generateManifest(appConfig, projectDir) {
|
|
|
617
709
|
|
|
618
710
|
const databases = appConfig.databases || [];
|
|
619
711
|
const categories = normalizeCategories(appConfig.categories || []);
|
|
620
|
-
const metadata =
|
|
712
|
+
const metadata = { screenshots: resolveListingScreenshots(projectDir, appConfig) };
|
|
621
713
|
const appSlug = safeKebab(appConfig.name);
|
|
622
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
|
+
}));
|
|
623
721
|
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
722
|
screenshots: metadata.screenshots.map((screenshot) => ({
|
|
634
723
|
path: screenshot.path,
|
|
635
724
|
content_type: screenshot.content_type,
|
|
636
725
|
width: screenshot.width,
|
|
637
726
|
height: screenshot.height,
|
|
638
727
|
bytes: screenshot.bytes,
|
|
728
|
+
alt: screenshot.alt || null,
|
|
729
|
+
theme: screenshot.theme || 'light',
|
|
639
730
|
})),
|
|
640
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
|
+
}
|
|
641
749
|
|
|
642
750
|
return {
|
|
643
751
|
version: 1,
|
|
@@ -647,18 +755,28 @@ export function generateManifest(appConfig, projectDir) {
|
|
|
647
755
|
slug: appSlug || null,
|
|
648
756
|
description: appConfig.description || null,
|
|
649
757
|
icon: appConfig.icon || null,
|
|
758
|
+
accent: appConfig.accent || null,
|
|
650
759
|
title: displayTitle,
|
|
651
760
|
tagline: appConfig.tagline || null,
|
|
652
761
|
categories,
|
|
653
762
|
author: appConfig.author || null,
|
|
654
|
-
|
|
763
|
+
release_version: releaseVersion,
|
|
764
|
+
version_notes: legacyVersionNotes,
|
|
655
765
|
},
|
|
656
766
|
listing: {
|
|
657
767
|
title: displayTitle,
|
|
658
768
|
tagline: appConfig.tagline || null,
|
|
659
769
|
categories,
|
|
660
770
|
author: appConfig.author || null,
|
|
661
|
-
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
|
+
: {}),
|
|
662
780
|
media: listingMedia,
|
|
663
781
|
},
|
|
664
782
|
metadata: listingMedia,
|
|
@@ -669,9 +787,47 @@ export function generateManifest(appConfig, projectDir) {
|
|
|
669
787
|
},
|
|
670
788
|
databases,
|
|
671
789
|
tools: appConfig.tools || [],
|
|
790
|
+
skills,
|
|
791
|
+
onboarding: appConfig.onboarding || null,
|
|
672
792
|
};
|
|
673
793
|
}
|
|
674
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
|
+
|
|
675
831
|
/**
|
|
676
832
|
* Build the app bundle: generate entry file, run `vite build`, package into .notis/output/.
|
|
677
833
|
*/
|
|
@@ -714,14 +870,18 @@ export function readManifest(projectDir) {
|
|
|
714
870
|
|
|
715
871
|
function copyMetadataAssets(projectDir) {
|
|
716
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 });
|
|
717
878
|
if (!existsSync(metadataDir)) {
|
|
718
879
|
return;
|
|
719
880
|
}
|
|
720
|
-
const outputMetadataDir = join(projectDir, OUTPUT_DIR, METADATA_DIR);
|
|
721
881
|
mkdirSync(outputMetadataDir, { recursive: true });
|
|
722
882
|
for (const entry of readdirSync(metadataDir, { withFileTypes: true })) {
|
|
723
883
|
if (!entry.isFile()) continue;
|
|
724
|
-
if (
|
|
884
|
+
if (!/^screenshot-\d+\.png$/i.test(entry.name)) {
|
|
725
885
|
continue;
|
|
726
886
|
}
|
|
727
887
|
cpSync(join(metadataDir, entry.name), join(outputMetadataDir, entry.name));
|
|
@@ -1259,6 +1419,7 @@ async function updateAppVersion(supabaseUrl, supabaseKey, appId, newVersion, man
|
|
|
1259
1419
|
},
|
|
1260
1420
|
body: JSON.stringify({
|
|
1261
1421
|
manifest: { ...manifest, version: newVersion },
|
|
1422
|
+
accent: manifest?.app?.accent ?? null,
|
|
1262
1423
|
updated_at: new Date().toISOString(),
|
|
1263
1424
|
}),
|
|
1264
1425
|
});
|
|
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
|
}
|