@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
|
@@ -25,6 +25,8 @@ import {
|
|
|
25
25
|
writeLinkedState,
|
|
26
26
|
requireLinkedAppId,
|
|
27
27
|
scaffoldProject,
|
|
28
|
+
loadScaffoldCatalog,
|
|
29
|
+
inspectListingReadiness,
|
|
28
30
|
collectArtifactFiles,
|
|
29
31
|
collectSourceFiles,
|
|
30
32
|
directDeploy,
|
|
@@ -68,6 +70,15 @@ function appsTable(apps) {
|
|
|
68
70
|
]);
|
|
69
71
|
}
|
|
70
72
|
|
|
73
|
+
function scaffoldsTable(scaffolds) {
|
|
74
|
+
return formatTable(scaffolds, [
|
|
75
|
+
{ label: 'Slug', value: (scaffold) => scaffold.slug || '' },
|
|
76
|
+
{ label: 'Name', value: (scaffold) => scaffold.name || scaffold.slug || '' },
|
|
77
|
+
{ label: 'Category', value: (scaffold) => (scaffold.categories || [])[0] || '' },
|
|
78
|
+
{ label: 'Tagline', value: (scaffold) => scaffold.tagline || scaffold.description || '' },
|
|
79
|
+
]);
|
|
80
|
+
}
|
|
81
|
+
|
|
71
82
|
function hasNotisConfig(dir) {
|
|
72
83
|
return CONFIG_FILENAMES.some((name) => existsSync(join(dir, name)));
|
|
73
84
|
}
|
|
@@ -155,6 +166,24 @@ function buildDevInstallSlug(appName) {
|
|
|
155
166
|
return base.endsWith('-dev') ? base : `${base}-dev`;
|
|
156
167
|
}
|
|
157
168
|
|
|
169
|
+
function timingMs(startedAt) {
|
|
170
|
+
return Number(process.hrtime.bigint() - startedAt) / 1_000_000;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
function logAppsTiming(label, details = {}) {
|
|
174
|
+
const suffix = Object.entries(details)
|
|
175
|
+
.map(([key, value]) => `${key}=${value}`)
|
|
176
|
+
.join(' ');
|
|
177
|
+
process.stderr.write(`[notis apps timing] ${label}${suffix ? ` ${suffix}` : ''}\n`);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
function nextDevInstallIdempotencyKey(globalOptions = {}, devSlug) {
|
|
181
|
+
if (globalOptions.idempotencyKey) {
|
|
182
|
+
return `${globalOptions.idempotencyKey}:${devSlug}`;
|
|
183
|
+
}
|
|
184
|
+
return nextIdempotencyKey(globalOptions);
|
|
185
|
+
}
|
|
186
|
+
|
|
158
187
|
function pickDefaultRouteSlug(manifest) {
|
|
159
188
|
const routes = Array.isArray(manifest?.routes) ? manifest.routes : [];
|
|
160
189
|
const explicit = routes.find((route) => route && route.default && typeof route.slug === 'string');
|
|
@@ -163,8 +192,9 @@ function pickDefaultRouteSlug(manifest) {
|
|
|
163
192
|
return firstWithSlug ? firstWithSlug.slug : null;
|
|
164
193
|
}
|
|
165
194
|
|
|
166
|
-
export function buildDevelopmentDesktopUrl() {
|
|
167
|
-
|
|
195
|
+
export function buildDevelopmentDesktopUrl(appHref = null) {
|
|
196
|
+
const route = String(appHref || '/store').replace(/^\/+/, '');
|
|
197
|
+
return `notis://${route || 'store'}`;
|
|
168
198
|
}
|
|
169
199
|
|
|
170
200
|
function buildAppHref({ appSlug, appId, manifest }) {
|
|
@@ -279,13 +309,16 @@ function assertHarnessResult(result, route, databaseSlugs) {
|
|
|
279
309
|
}
|
|
280
310
|
for (const databaseSlug of databaseSlugs) {
|
|
281
311
|
const seen = (result.runtimeCalls || []).some(
|
|
282
|
-
(call) =>
|
|
312
|
+
(call) =>
|
|
313
|
+
call?.op === 'callTool' &&
|
|
314
|
+
call?.args?.name === 'notis-default-query' &&
|
|
315
|
+
call?.args?.arguments?.database_slug === databaseSlug,
|
|
283
316
|
);
|
|
284
317
|
if (!seen) {
|
|
285
318
|
assertions.push({
|
|
286
319
|
ok: false,
|
|
287
320
|
code: 'missing_database_query',
|
|
288
|
-
message: `Route "${route.slug}" did not call
|
|
321
|
+
message: `Route "${route.slug}" did not call notis-default-query for "${databaseSlug}".`,
|
|
289
322
|
details: { databaseSlug },
|
|
290
323
|
});
|
|
291
324
|
}
|
|
@@ -341,7 +374,26 @@ function buildManifestForDev(appConfig) {
|
|
|
341
374
|
};
|
|
342
375
|
}
|
|
343
376
|
|
|
344
|
-
|
|
377
|
+
export function buildEnsureDevInstallArguments({ appConfig, manifest, linkedState }) {
|
|
378
|
+
const devSlug = buildDevInstallSlug(appConfig.name);
|
|
379
|
+
const arguments_ = {
|
|
380
|
+
dev_slug: devSlug,
|
|
381
|
+
name: appConfig.name,
|
|
382
|
+
manifest,
|
|
383
|
+
};
|
|
384
|
+
if (linkedState?.dev_app_id) {
|
|
385
|
+
arguments_.app_id = linkedState.dev_app_id;
|
|
386
|
+
}
|
|
387
|
+
return arguments_;
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
export async function ensureDevInstall({
|
|
391
|
+
ctx,
|
|
392
|
+
appConfig,
|
|
393
|
+
projectDir,
|
|
394
|
+
idempotencyKey,
|
|
395
|
+
runTool = runToolCommand,
|
|
396
|
+
}) {
|
|
345
397
|
if (!appConfig.name) {
|
|
346
398
|
throw usageError(`notis.config.ts in ${projectDir} must define a non-empty name.`);
|
|
347
399
|
}
|
|
@@ -351,14 +403,12 @@ async function ensureDevInstall({ ctx, appConfig, projectDir, idempotencyKey })
|
|
|
351
403
|
}
|
|
352
404
|
|
|
353
405
|
const manifest = buildManifestForDev(appConfig);
|
|
354
|
-
const
|
|
406
|
+
const linkedState = readLinkedState(projectDir);
|
|
407
|
+
const ensureArguments = buildEnsureDevInstallArguments({ appConfig, manifest, linkedState });
|
|
408
|
+
const ensureResult = await runTool({
|
|
355
409
|
runtime: ctx.runtime,
|
|
356
410
|
toolName: ENSURE_DEV_APP_INSTALLATION_TOOL,
|
|
357
|
-
arguments_:
|
|
358
|
-
dev_slug: devSlug,
|
|
359
|
-
name: appConfig.name,
|
|
360
|
-
manifest,
|
|
361
|
-
},
|
|
411
|
+
arguments_: ensureArguments,
|
|
362
412
|
mutating: true,
|
|
363
413
|
idempotencyKey,
|
|
364
414
|
});
|
|
@@ -374,9 +424,26 @@ async function ensureDevInstall({ ctx, appConfig, projectDir, idempotencyKey })
|
|
|
374
424
|
projectDir,
|
|
375
425
|
manifest,
|
|
376
426
|
created: ensureResult.payload.created || false,
|
|
427
|
+
linkedAppId: linkedState?.app_id || null,
|
|
428
|
+
databaseMaterialization: ensureResult.payload.database_materialization || { created: [], unresolved: [] },
|
|
377
429
|
};
|
|
378
430
|
}
|
|
379
431
|
|
|
432
|
+
function databaseMaterializationWarnings(apps) {
|
|
433
|
+
const warnings = [];
|
|
434
|
+
for (const app of apps) {
|
|
435
|
+
const unresolved = app.databaseMaterialization?.unresolved || [];
|
|
436
|
+
if (!unresolved.length) {
|
|
437
|
+
continue;
|
|
438
|
+
}
|
|
439
|
+
warnings.push(
|
|
440
|
+
`${app.name}: database slug${unresolved.length === 1 ? '' : 's'} ${unresolved.join(', ')} ` +
|
|
441
|
+
'could not be created because no store snapshot schema exists. Create them manually or link to a store-installed app.',
|
|
442
|
+
);
|
|
443
|
+
}
|
|
444
|
+
return warnings;
|
|
445
|
+
}
|
|
446
|
+
|
|
380
447
|
async function assertDirectDeployAccess(runtime, appId) {
|
|
381
448
|
const result = await runToolCommand({
|
|
382
449
|
runtime,
|
|
@@ -409,13 +476,16 @@ async function appsListHandler(ctx) {
|
|
|
409
476
|
|
|
410
477
|
async function appsInitHandler(ctx) {
|
|
411
478
|
const projectDir = resolveProjectDir(ctx.args.dir || ctx.args.name.toLowerCase().replace(/[^a-z0-9]+/g, '-'));
|
|
479
|
+
const fromSlug = ctx.options.from || null;
|
|
412
480
|
|
|
413
|
-
scaffoldProject({ projectDir, appName: ctx.args.name });
|
|
481
|
+
scaffoldProject({ projectDir, appName: ctx.args.name, fromSlug });
|
|
414
482
|
|
|
415
483
|
return ctx.output.emitSuccess({
|
|
416
484
|
command: ctx.spec.command_path.join(' '),
|
|
417
|
-
data: { project_dir: projectDir, app_name: ctx.args.name },
|
|
418
|
-
humanSummary:
|
|
485
|
+
data: { project_dir: projectDir, app_name: ctx.args.name, scaffold: fromSlug },
|
|
486
|
+
humanSummary: fromSlug
|
|
487
|
+
? `Scaffolded "${ctx.args.name}" from ${fromSlug} in ${projectDir}`
|
|
488
|
+
: `Scaffolded "${ctx.args.name}" in ${projectDir}`,
|
|
419
489
|
hints: [
|
|
420
490
|
{ command: `cd ${projectDir} && npm install`, reason: 'Install dependencies' },
|
|
421
491
|
{ command: `cd ${projectDir} && notis apps dev`, reason: 'Start the real-portal dev workflow' },
|
|
@@ -423,19 +493,29 @@ async function appsInitHandler(ctx) {
|
|
|
423
493
|
});
|
|
424
494
|
}
|
|
425
495
|
|
|
496
|
+
async function appsScaffoldsListHandler(ctx) {
|
|
497
|
+
const scaffolds = loadScaffoldCatalog();
|
|
498
|
+
return ctx.output.emitSuccess({
|
|
499
|
+
command: ctx.spec.command_path.join(' '),
|
|
500
|
+
data: { scaffolds },
|
|
501
|
+
humanSummary: scaffolds.length
|
|
502
|
+
? `Found ${scaffolds.length} bundled scaffolds`
|
|
503
|
+
: 'No bundled scaffolds found. Run the CLI build step to generate the scaffold catalog.',
|
|
504
|
+
renderHuman: () => (scaffolds.length ? scaffoldsTable(scaffolds) : 'No bundled scaffolds found.'),
|
|
505
|
+
});
|
|
506
|
+
}
|
|
507
|
+
|
|
426
508
|
async function appsCreateHandler(ctx) {
|
|
427
509
|
const projectDir = ctx.args.dir ? resolveProjectDir(ctx.args.dir) : null;
|
|
510
|
+
const appConfig = projectDir ? await loadAppConfig(projectDir) : null;
|
|
428
511
|
const idempotencyKey = nextIdempotencyKey(ctx.globalOptions);
|
|
429
|
-
const icon = ctx.options.icon
|
|
430
|
-
? (ctx.options.icon.startsWith('lucide:') ? ctx.options.icon : `lucide:${ctx.options.icon}`)
|
|
431
|
-
: undefined;
|
|
432
512
|
const result = await runToolCommand({
|
|
433
513
|
runtime: ctx.runtime,
|
|
434
514
|
toolName: 'notis_create_app',
|
|
435
515
|
arguments_: {
|
|
436
516
|
name: ctx.args.name,
|
|
437
|
-
description:
|
|
438
|
-
icon,
|
|
517
|
+
description: appConfig?.description || undefined,
|
|
518
|
+
icon: appConfig?.icon || undefined,
|
|
439
519
|
},
|
|
440
520
|
mutating: true,
|
|
441
521
|
idempotencyKey,
|
|
@@ -490,25 +570,26 @@ async function appsDevHandler(ctx) {
|
|
|
490
570
|
const mode = getCliMode();
|
|
491
571
|
const identity = decodeJwtSub(ctx.runtime.jwt);
|
|
492
572
|
if (!identity) {
|
|
493
|
-
throw usageError('Could not determine the current user from the CLI auth token.
|
|
573
|
+
throw usageError('Could not determine the current user from the CLI auth token. Open the Notis desktop app, sign in, and retry.');
|
|
494
574
|
}
|
|
495
575
|
const apiBase = String(ctx.runtime.apiBase || '').replace(/\/$/, '');
|
|
496
576
|
const sessionId = randomUUID();
|
|
497
577
|
|
|
498
|
-
const
|
|
578
|
+
const candidates = [];
|
|
499
579
|
for (const projectDir of appDirs) {
|
|
500
580
|
const appConfig = await loadAppConfig(projectDir);
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
581
|
+
if (!appConfig.name) {
|
|
582
|
+
throw usageError(`notis.config.ts in ${projectDir} must define a non-empty name.`);
|
|
583
|
+
}
|
|
584
|
+
const devSlug = buildDevInstallSlug(appConfig.name);
|
|
585
|
+
if (!devSlug) {
|
|
586
|
+
throw usageError(`notis.config.ts name in ${projectDir} must slugify to a non-empty value.`);
|
|
587
|
+
}
|
|
588
|
+
candidates.push({ appConfig, devSlug, projectDir });
|
|
508
589
|
}
|
|
509
590
|
|
|
510
591
|
const seenSlugs = new Map();
|
|
511
|
-
for (const app of
|
|
592
|
+
for (const app of candidates) {
|
|
512
593
|
if (seenSlugs.has(app.devSlug)) {
|
|
513
594
|
throw usageError(
|
|
514
595
|
`Multiple apps slugify to "${app.devSlug}" (${seenSlugs.get(app.devSlug).projectDir} and ${app.projectDir}). Rename one in notis.config.ts.`,
|
|
@@ -517,6 +598,30 @@ async function appsDevHandler(ctx) {
|
|
|
517
598
|
seenSlugs.set(app.devSlug, app);
|
|
518
599
|
}
|
|
519
600
|
|
|
601
|
+
const registrationStartedAt = process.hrtime.bigint();
|
|
602
|
+
const registered = await Promise.all(
|
|
603
|
+
candidates.map(async ({ appConfig, devSlug, projectDir }) => {
|
|
604
|
+
const appStartedAt = process.hrtime.bigint();
|
|
605
|
+
try {
|
|
606
|
+
return await ensureDevInstall({
|
|
607
|
+
ctx,
|
|
608
|
+
appConfig,
|
|
609
|
+
projectDir,
|
|
610
|
+
idempotencyKey: nextDevInstallIdempotencyKey(ctx.globalOptions, devSlug),
|
|
611
|
+
});
|
|
612
|
+
} finally {
|
|
613
|
+
logAppsTiming('ensure-dev-install', {
|
|
614
|
+
slug: devSlug,
|
|
615
|
+
ms: timingMs(appStartedAt).toFixed(1),
|
|
616
|
+
});
|
|
617
|
+
}
|
|
618
|
+
}),
|
|
619
|
+
);
|
|
620
|
+
logAppsTiming('ensure-dev-install:all', {
|
|
621
|
+
apps: registered.length,
|
|
622
|
+
ms: timingMs(registrationStartedAt).toFixed(1),
|
|
623
|
+
});
|
|
624
|
+
|
|
520
625
|
const apps = registered.map((app) => {
|
|
521
626
|
const bundleBaseUrl = `http://127.0.0.1:${port}/a/${app.devSlug}`;
|
|
522
627
|
return {
|
|
@@ -529,10 +634,16 @@ async function appsDevHandler(ctx) {
|
|
|
529
634
|
}),
|
|
530
635
|
};
|
|
531
636
|
});
|
|
532
|
-
const developmentTabUrl = buildDevelopmentDesktopUrl();
|
|
637
|
+
const developmentTabUrl = buildDevelopmentDesktopUrl(apps[0]?.appHref);
|
|
638
|
+
const warnings = databaseMaterializationWarnings(apps);
|
|
533
639
|
|
|
534
640
|
const devServer = await startAppDevServer({
|
|
535
|
-
apps: apps.map((app) => ({
|
|
641
|
+
apps: apps.map((app) => ({
|
|
642
|
+
slug: app.devSlug,
|
|
643
|
+
projectDir: app.projectDir,
|
|
644
|
+
appId: app.appId,
|
|
645
|
+
userId: identity,
|
|
646
|
+
})),
|
|
536
647
|
port,
|
|
537
648
|
});
|
|
538
649
|
|
|
@@ -583,12 +694,15 @@ async function appsDevHandler(ctx) {
|
|
|
583
694
|
bundle_base_url: app.bundleBaseUrl,
|
|
584
695
|
app_href: app.appHref,
|
|
585
696
|
created: app.created,
|
|
697
|
+
linked_app_id: app.linkedAppId,
|
|
698
|
+
database_materialization: app.databaseMaterialization,
|
|
586
699
|
})),
|
|
587
700
|
},
|
|
701
|
+
warnings,
|
|
588
702
|
humanSummary: [
|
|
589
703
|
`Running apps dev against ${apiBase} as ${identity} (mode: ${mode})`,
|
|
590
704
|
'',
|
|
591
|
-
`
|
|
705
|
+
`Open in desktop: ${developmentTabUrl}`,
|
|
592
706
|
'',
|
|
593
707
|
...apps.map((app) => ` ${app.name.padEnd(24)} ${app.bundleBaseUrl} -> ${app.appHref}`),
|
|
594
708
|
'',
|
|
@@ -665,7 +779,7 @@ async function appsVerifyHandler(ctx) {
|
|
|
665
779
|
let linkedState = null;
|
|
666
780
|
if (mode === 'live') {
|
|
667
781
|
if (!ctx.runtime.jwt) {
|
|
668
|
-
throw usageError('Live verify mode requires CLI auth.
|
|
782
|
+
throw usageError('Live verify mode requires CLI auth. Open the Notis desktop app, sign in, and retry.');
|
|
669
783
|
}
|
|
670
784
|
linkedState = readLinkedState(projectDir);
|
|
671
785
|
if (!linkedState?.app_id) {
|
|
@@ -679,6 +793,11 @@ async function appsVerifyHandler(ctx) {
|
|
|
679
793
|
|
|
680
794
|
const manifest = readManifest(projectDir);
|
|
681
795
|
const appConfig = await loadAppConfig(projectDir);
|
|
796
|
+
const listing = inspectListingReadiness(projectDir, appConfig);
|
|
797
|
+
if (listing.errors.length) {
|
|
798
|
+
throw usageError(`Listing metadata has problems:\n${listing.errors.map((error) => ` - ${error}`).join('\n')}`);
|
|
799
|
+
}
|
|
800
|
+
const listingWarnings = listing.warnings;
|
|
682
801
|
const routes = routeSelection(manifest, parseRouteSlugs(ctx.options.routes));
|
|
683
802
|
const port = parsePort(ctx.options.port) || await getAvailablePort();
|
|
684
803
|
const appSlug = slugify(appConfig.name || manifest.app?.name || 'app') || 'app';
|
|
@@ -713,7 +832,7 @@ async function appsVerifyHandler(ctx) {
|
|
|
713
832
|
}));
|
|
714
833
|
|
|
715
834
|
let results;
|
|
716
|
-
const warnings = [];
|
|
835
|
+
const warnings = [...listingWarnings];
|
|
717
836
|
if (noBrowser) {
|
|
718
837
|
results = urls.map(({ route, url }) => ({
|
|
719
838
|
route: route.slug,
|
|
@@ -905,10 +1024,27 @@ async function appsPullHandler(ctx) {
|
|
|
905
1024
|
});
|
|
906
1025
|
}
|
|
907
1026
|
|
|
1027
|
+
function updateLinkedDeployState(projectDir, linkedState, appId, version) {
|
|
1028
|
+
if (!linkedState || linkedState.app_id !== appId || !Number.isFinite(version)) {
|
|
1029
|
+
return;
|
|
1030
|
+
}
|
|
1031
|
+
writeLinkedState(projectDir, {
|
|
1032
|
+
...linkedState,
|
|
1033
|
+
app_id: appId,
|
|
1034
|
+
version,
|
|
1035
|
+
linked_at: linkedState.linked_at || new Date().toISOString(),
|
|
1036
|
+
deployed_at: new Date().toISOString(),
|
|
1037
|
+
});
|
|
1038
|
+
}
|
|
1039
|
+
|
|
908
1040
|
async function appsDeployHandler(ctx) {
|
|
909
1041
|
const projectDir = resolveProjectDir(ctx.args.dir || '.');
|
|
910
1042
|
const appId = requireLinkedAppId(projectDir, ctx.options.appId);
|
|
911
1043
|
const idempotencyKey = nextIdempotencyKey(ctx.globalOptions);
|
|
1044
|
+
const linkedState = readLinkedState(projectDir);
|
|
1045
|
+
const baseVersion = linkedState?.app_id === appId && Number.isFinite(linkedState?.version)
|
|
1046
|
+
? linkedState.version
|
|
1047
|
+
: undefined;
|
|
912
1048
|
|
|
913
1049
|
// Build if needed
|
|
914
1050
|
if (!ctx.options.skipBuild) {
|
|
@@ -919,6 +1055,7 @@ async function appsDeployHandler(ctx) {
|
|
|
919
1055
|
if (ctx.options.direct) {
|
|
920
1056
|
await assertDirectDeployAccess(ctx.runtime, appId);
|
|
921
1057
|
const { version } = await directDeploy(projectDir, appId);
|
|
1058
|
+
updateLinkedDeployState(projectDir, linkedState, appId, version);
|
|
922
1059
|
return ctx.output.emitSuccess({
|
|
923
1060
|
command: ctx.spec.command_path.join(' '),
|
|
924
1061
|
data: { app_id: appId, version, mode: 'direct' },
|
|
@@ -942,6 +1079,7 @@ async function appsDeployHandler(ctx) {
|
|
|
942
1079
|
files,
|
|
943
1080
|
source_files: sourceFiles,
|
|
944
1081
|
manifest,
|
|
1082
|
+
...(baseVersion !== undefined ? { base_version: baseVersion } : {}),
|
|
945
1083
|
},
|
|
946
1084
|
mutating: true,
|
|
947
1085
|
idempotencyKey,
|
|
@@ -968,6 +1106,7 @@ async function appsDeployHandler(ctx) {
|
|
|
968
1106
|
|
|
969
1107
|
try {
|
|
970
1108
|
const { version } = await directDeploy(projectDir, appId);
|
|
1109
|
+
updateLinkedDeployState(projectDir, linkedState, appId, version);
|
|
971
1110
|
return ctx.output.emitSuccess({
|
|
972
1111
|
command: ctx.spec.command_path.join(' '),
|
|
973
1112
|
data: { app_id: appId, version, mode: 'direct-fallback' },
|
|
@@ -986,6 +1125,7 @@ async function appsDeployHandler(ctx) {
|
|
|
986
1125
|
throw error;
|
|
987
1126
|
}
|
|
988
1127
|
|
|
1128
|
+
updateLinkedDeployState(projectDir, linkedState, appId, Number(result.payload.version));
|
|
989
1129
|
return ctx.output.emitSuccess({
|
|
990
1130
|
command: ctx.spec.command_path.join(' '),
|
|
991
1131
|
data: {
|
|
@@ -1009,13 +1149,21 @@ async function appsDoctorHandler(ctx) {
|
|
|
1009
1149
|
problems.push('Failed to load notis.config.ts');
|
|
1010
1150
|
}
|
|
1011
1151
|
const warnings = detectProjectWarnings(projectDir, appConfig);
|
|
1152
|
+
let listing = null;
|
|
1153
|
+
if (appConfig) {
|
|
1154
|
+
try {
|
|
1155
|
+
listing = inspectListingReadiness(projectDir, appConfig);
|
|
1156
|
+
} catch (error) {
|
|
1157
|
+
warnings.push(error instanceof Error ? error.message : String(error));
|
|
1158
|
+
}
|
|
1159
|
+
}
|
|
1012
1160
|
|
|
1013
1161
|
const linkedState = readLinkedState(projectDir);
|
|
1014
1162
|
const status = problems.length ? 'unhealthy' : warnings.length ? 'warnings' : 'healthy';
|
|
1015
1163
|
|
|
1016
1164
|
return ctx.output.emitSuccess({
|
|
1017
1165
|
command: ctx.spec.command_path.join(' '),
|
|
1018
|
-
data: { status, problems, warnings, linked: linkedState, config: appConfig },
|
|
1166
|
+
data: { status, problems, warnings, linked: linkedState, config: appConfig, listing },
|
|
1019
1167
|
humanSummary: problems.length
|
|
1020
1168
|
? `Found ${problems.length} problems:\n${problems.map((p) => ` - ${p}`).join('\n')}`
|
|
1021
1169
|
: warnings.length
|
|
@@ -1043,21 +1191,40 @@ export const appsCommandSpecs = [
|
|
|
1043
1191
|
{
|
|
1044
1192
|
command_path: ['apps', 'init'],
|
|
1045
1193
|
summary: 'Scaffold a new Notis app project.',
|
|
1046
|
-
when_to_use: 'Start a new Notis app.
|
|
1194
|
+
when_to_use: 'Start a new Notis app. Use --from with a bundled scaffold when one is close to the desired app; otherwise creates the bare Vite + React project.',
|
|
1047
1195
|
args_schema: {
|
|
1048
1196
|
arguments: [
|
|
1049
1197
|
{ token: '<name>', description: 'Display name for the app.' },
|
|
1050
1198
|
{ token: '[dir]', key: 'dir', description: 'Target directory (defaults to kebab-case of name).' },
|
|
1051
1199
|
],
|
|
1052
|
-
options: [
|
|
1200
|
+
options: [
|
|
1201
|
+
{ flags: '--from <slug>', description: 'Start from a bundled scaffold listed by `notis apps scaffolds list`.' },
|
|
1202
|
+
],
|
|
1053
1203
|
},
|
|
1054
|
-
examples: [
|
|
1204
|
+
examples: [
|
|
1205
|
+
'notis apps scaffolds list',
|
|
1206
|
+
'notis apps init "Mind the Flo"',
|
|
1207
|
+
'notis apps init "My CRM" --from notis-database',
|
|
1208
|
+
'notis apps init "My App" ./my-app',
|
|
1209
|
+
],
|
|
1055
1210
|
mutates: true,
|
|
1056
1211
|
idempotent: false,
|
|
1057
1212
|
require_auth: false,
|
|
1058
1213
|
backend_call: { type: 'local', name: 'scaffold_project' },
|
|
1059
1214
|
handler: appsInitHandler,
|
|
1060
1215
|
},
|
|
1216
|
+
{
|
|
1217
|
+
command_path: ['apps', 'scaffolds', 'list'],
|
|
1218
|
+
summary: 'List bundled Notis app scaffolds.',
|
|
1219
|
+
when_to_use: 'Discover the fixed scaffold catalog shipped with the CLI before starting a new app.',
|
|
1220
|
+
args_schema: { arguments: [], options: [] },
|
|
1221
|
+
examples: ['notis apps scaffolds list', 'notis apps init "My App" --from notis-database'],
|
|
1222
|
+
mutates: false,
|
|
1223
|
+
idempotent: true,
|
|
1224
|
+
require_auth: false,
|
|
1225
|
+
backend_call: { type: 'local', name: 'list_scaffolds' },
|
|
1226
|
+
handler: appsScaffoldsListHandler,
|
|
1227
|
+
},
|
|
1061
1228
|
{
|
|
1062
1229
|
command_path: ['apps', 'create'],
|
|
1063
1230
|
summary: 'Create a new remote Notis app and optionally link a local project to it.',
|
|
@@ -1067,14 +1234,11 @@ export const appsCommandSpecs = [
|
|
|
1067
1234
|
{ token: '<name>', description: 'Display name for the remote app.' },
|
|
1068
1235
|
{ token: '[dir]', key: 'dir', description: 'Project directory to link after creation (default: do not link).' },
|
|
1069
1236
|
],
|
|
1070
|
-
options: [
|
|
1071
|
-
{ flags: '--description <text>', description: 'Optional app description.' },
|
|
1072
|
-
{ flags: '--icon <lucide:icon>', description: 'Optional Lucide icon, for example lucide:dices.' },
|
|
1073
|
-
],
|
|
1237
|
+
options: [],
|
|
1074
1238
|
},
|
|
1075
1239
|
examples: [
|
|
1076
1240
|
'notis apps create "My App"',
|
|
1077
|
-
'notis apps create "My App" .
|
|
1241
|
+
'notis apps create "My App" .',
|
|
1078
1242
|
],
|
|
1079
1243
|
mutates: true,
|
|
1080
1244
|
idempotent: false,
|
|
@@ -1085,14 +1249,14 @@ export const appsCommandSpecs = [
|
|
|
1085
1249
|
command_path: ['apps', 'dev'],
|
|
1086
1250
|
summary: 'Develop Notis apps inside the Electron desktop Portal with automatic local bundle reloads.',
|
|
1087
1251
|
when_to_use:
|
|
1088
|
-
'Run this inside a single app or a monorepo root with apps/<name>/notis.config.ts. It discovers every app, starts the local bundle server, registers desktop-local dev sessions, and opens the Electron Portal
|
|
1252
|
+
'Run this inside a single app or a monorepo root with apps/<name>/notis.config.ts. It discovers every app, starts the local bundle server, registers desktop-local dev sessions, and opens the Electron Portal to the local development app.',
|
|
1089
1253
|
args_schema: {
|
|
1090
1254
|
arguments: [
|
|
1091
1255
|
{ token: '[dir]', key: 'dir', description: 'Project directory or monorepo root (default: current dir).' },
|
|
1092
1256
|
],
|
|
1093
1257
|
options: [
|
|
1094
1258
|
{ flags: '--port <number>', description: `Local bundle server port (default: ${DEFAULT_DEV_PORT}).` },
|
|
1095
|
-
{ flags: '--no-open', description: 'Do not auto-open the desktop Portal
|
|
1259
|
+
{ flags: '--no-open', description: 'Do not auto-open the desktop Portal local development app.' },
|
|
1096
1260
|
],
|
|
1097
1261
|
},
|
|
1098
1262
|
examples: [
|
|
@@ -1175,7 +1339,8 @@ export const appsCommandSpecs = [
|
|
|
1175
1339
|
{
|
|
1176
1340
|
command_path: ['apps', 'pull'],
|
|
1177
1341
|
summary: 'Download a Notis app source snapshot into a local project folder.',
|
|
1178
|
-
when_to_use:
|
|
1342
|
+
when_to_use:
|
|
1343
|
+
'Edit an installed app locally. Pulls the persisted source, links the directory to the app/version, then continue with npm install, notis apps dev, notis apps build, and notis apps deploy.',
|
|
1179
1344
|
args_schema: {
|
|
1180
1345
|
arguments: [
|
|
1181
1346
|
{ token: '<app-id>', description: 'Remote app ID to pull.' },
|
|
@@ -1,13 +1,6 @@
|
|
|
1
|
-
import { createInterface } from 'node:readline/promises';
|
|
2
1
|
import { randomUUID } from 'node:crypto';
|
|
3
2
|
import { CliError, EXIT_CODES, usageError } from '../runtime/errors.js';
|
|
4
3
|
import { callTool, httpRequest } from '../runtime/transport.js';
|
|
5
|
-
import {
|
|
6
|
-
DEFAULT_PROFILE,
|
|
7
|
-
ensureProfile,
|
|
8
|
-
loadConfig,
|
|
9
|
-
saveConfig,
|
|
10
|
-
} from '../runtime/profiles.js';
|
|
11
4
|
|
|
12
5
|
export function parseJson(value, label) {
|
|
13
6
|
try {
|
|
@@ -41,16 +34,6 @@ export function normalizeToolkits(value) {
|
|
|
41
34
|
.filter(Boolean);
|
|
42
35
|
}
|
|
43
36
|
|
|
44
|
-
export async function promptForJwt() {
|
|
45
|
-
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
46
|
-
try {
|
|
47
|
-
const jwt = await rl.question('Paste your Notis JWT: ');
|
|
48
|
-
return jwt.trim();
|
|
49
|
-
} finally {
|
|
50
|
-
rl.close();
|
|
51
|
-
}
|
|
52
|
-
}
|
|
53
|
-
|
|
54
37
|
export function nextIdempotencyKey(globalOptions) {
|
|
55
38
|
return globalOptions.idempotencyKey || randomUUID();
|
|
56
39
|
}
|
|
@@ -105,49 +88,6 @@ export async function healthCheck(runtime) {
|
|
|
105
88
|
});
|
|
106
89
|
}
|
|
107
90
|
|
|
108
|
-
export function updateStoredProfile({
|
|
109
|
-
profileName,
|
|
110
|
-
jwt,
|
|
111
|
-
apiBase,
|
|
112
|
-
authMode,
|
|
113
|
-
refreshToken,
|
|
114
|
-
accessExpiresAt,
|
|
115
|
-
refreshExpiresAt,
|
|
116
|
-
setCurrent = true,
|
|
117
|
-
}) {
|
|
118
|
-
let config = ensureProfile(loadConfig(), profileName || DEFAULT_PROFILE);
|
|
119
|
-
const resolvedName = profileName || DEFAULT_PROFILE;
|
|
120
|
-
config.profiles[resolvedName] = {
|
|
121
|
-
...config.profiles[resolvedName],
|
|
122
|
-
...(jwt ? { jwt } : {}),
|
|
123
|
-
...(apiBase ? { api_base: apiBase } : {}),
|
|
124
|
-
...(authMode ? { auth_mode: authMode } : {}),
|
|
125
|
-
...(refreshToken ? { refresh_token: refreshToken } : {}),
|
|
126
|
-
...(typeof accessExpiresAt === 'number'
|
|
127
|
-
? { access_expires_at: accessExpiresAt }
|
|
128
|
-
: {}),
|
|
129
|
-
...(typeof refreshExpiresAt === 'number'
|
|
130
|
-
? { refresh_expires_at: refreshExpiresAt }
|
|
131
|
-
: {}),
|
|
132
|
-
};
|
|
133
|
-
if (setCurrent) {
|
|
134
|
-
config.current_profile = resolvedName;
|
|
135
|
-
}
|
|
136
|
-
saveConfig(config);
|
|
137
|
-
return config;
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
export function clearStoredJwt(profileName) {
|
|
141
|
-
const config = ensureProfile(loadConfig(), profileName || DEFAULT_PROFILE);
|
|
142
|
-
delete config.profiles[profileName || DEFAULT_PROFILE].jwt;
|
|
143
|
-
delete config.profiles[profileName || DEFAULT_PROFILE].auth_mode;
|
|
144
|
-
delete config.profiles[profileName || DEFAULT_PROFILE].refresh_token;
|
|
145
|
-
delete config.profiles[profileName || DEFAULT_PROFILE].access_expires_at;
|
|
146
|
-
delete config.profiles[profileName || DEFAULT_PROFILE].refresh_expires_at;
|
|
147
|
-
saveConfig(config);
|
|
148
|
-
return config;
|
|
149
|
-
}
|
|
150
|
-
|
|
151
91
|
export async function fetchToolSchema(runtime, toolName, rawToolkits) {
|
|
152
92
|
const toolkits = normalizeToolkits(rawToolkits);
|
|
153
93
|
const result = await runToolCommand({
|
|
@@ -1,20 +1,14 @@
|
|
|
1
|
-
import { authCommandSpecs } from './auth.js';
|
|
2
1
|
import { appsCommandSpecs } from './apps.js';
|
|
3
|
-
import { dbCommandSpecs } from './db.js';
|
|
4
2
|
import { toolsCommandSpecs } from './tools.js';
|
|
5
3
|
import { metaCommandSpecs } from './meta.js';
|
|
6
4
|
|
|
7
5
|
export const GROUP_SUMMARIES = {
|
|
8
|
-
auth: 'Authentication and profile management.',
|
|
9
6
|
apps: 'Develop, build, and deploy Notis Apps.',
|
|
10
|
-
db: 'List, query, and update native Notis Databases.',
|
|
11
7
|
tools: 'Discover and execute generic tools exposed through Notis.',
|
|
12
8
|
};
|
|
13
9
|
|
|
14
10
|
export const COMMAND_SPECS = [
|
|
15
|
-
...authCommandSpecs,
|
|
16
11
|
...appsCommandSpecs,
|
|
17
|
-
...dbCommandSpecs,
|
|
18
12
|
...toolsCommandSpecs,
|
|
19
13
|
...metaCommandSpecs,
|
|
20
14
|
];
|
|
@@ -29,10 +29,10 @@ async function doctorHandler(ctx) {
|
|
|
29
29
|
|
|
30
30
|
const hints = [];
|
|
31
31
|
if (checks.auth === 'missing') {
|
|
32
|
-
hints.push({ command: '
|
|
32
|
+
hints.push({ command: 'Open the Notis desktop app and sign in', reason: 'Configure CLI credentials' });
|
|
33
33
|
}
|
|
34
34
|
if (checks.health === 'error') {
|
|
35
|
-
hints.push({ command: '
|
|
35
|
+
hints.push({ command: 'Open the Notis desktop app and sign in again', reason: 'Refresh local CLI configuration' });
|
|
36
36
|
}
|
|
37
37
|
if (checks.tool_roundtrip === 'error') {
|
|
38
38
|
hints.push({ command: 'notis whoami', reason: 'Verify your account and permissions' });
|
|
@@ -117,7 +117,7 @@ export const metaCommandSpecs = [
|
|
|
117
117
|
output_schema: 'Returns profile, api_base, user_id, toolkit count, and CLI version.',
|
|
118
118
|
mutates: false,
|
|
119
119
|
idempotent: true,
|
|
120
|
-
related_commands: ['notis
|
|
120
|
+
related_commands: ['notis doctor'],
|
|
121
121
|
backend_call: { type: 'tool', name: 'notis_find_toolkits' },
|
|
122
122
|
handler: whoamiHandler,
|
|
123
123
|
},
|
|
@@ -130,7 +130,8 @@ export const metaCommandSpecs = [
|
|
|
130
130
|
output_schema: 'Returns config, auth, health, and roundtrip check statuses.',
|
|
131
131
|
mutates: false,
|
|
132
132
|
idempotent: true,
|
|
133
|
-
|
|
133
|
+
require_auth: false,
|
|
134
|
+
related_commands: ['notis tools toolkits'],
|
|
134
135
|
backend_call: { type: 'health+tool_roundtrip' },
|
|
135
136
|
handler: doctorHandler,
|
|
136
137
|
},
|
|
@@ -139,10 +140,10 @@ export const metaCommandSpecs = [
|
|
|
139
140
|
summary: 'Describe a first-class CLI command in detail.',
|
|
140
141
|
when_to_use: 'Use this when an agent or human needs the exact shape, examples, and semantics of a command.',
|
|
141
142
|
args_schema: {
|
|
142
|
-
arguments: [{ token: '<command...>', key: 'commandPath', description: 'Command path to describe, such as "apps
|
|
143
|
+
arguments: [{ token: '<command...>', key: 'commandPath', description: 'Command path to describe, such as "apps deploy".' }],
|
|
143
144
|
options: [],
|
|
144
145
|
},
|
|
145
|
-
examples: ['notis describe apps
|
|
146
|
+
examples: ['notis describe apps deploy', 'notis describe tools exec'],
|
|
146
147
|
output_schema: 'Returns the command spec metadata for the requested command.',
|
|
147
148
|
mutates: false,
|
|
148
149
|
idempotent: true,
|