@notis_ai/cli 0.2.1 → 0.2.3
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 +106 -170
- package/config/notis_app_boundary_rules.json +50 -0
- package/dist/scaffolds/notis-database/app/globals.css +44 -0
- package/dist/scaffolds/notis-database/app/layout.tsx +6 -0
- package/dist/scaffolds/notis-database/app/page.tsx +1091 -0
- package/dist/scaffolds/notis-database/components/ui/badge.tsx +28 -0
- package/dist/scaffolds/notis-database/components/ui/button.tsx +53 -0
- package/dist/scaffolds/notis-database/components/ui/card.tsx +56 -0
- package/dist/scaffolds/notis-database/components/ui/table.tsx +120 -0
- package/dist/scaffolds/notis-database/components.json +20 -0
- package/dist/scaffolds/notis-database/index.html +12 -0
- package/dist/scaffolds/notis-database/lib/types.ts +134 -0
- package/dist/scaffolds/notis-database/lib/utils.ts +6 -0
- package/dist/scaffolds/notis-database/metadata/cover.png +0 -0
- package/dist/scaffolds/notis-database/metadata/screenshot-1.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-database/notis.config.ts +27 -0
- package/dist/scaffolds/notis-database/package.json +31 -0
- package/dist/scaffolds/notis-database/postcss.config.mjs +8 -0
- package/dist/scaffolds/notis-database/src/dev-main.tsx +23 -0
- package/dist/scaffolds/notis-database/src/mock-runtime.ts +557 -0
- package/dist/scaffolds/notis-database/tailwind.config.ts +59 -0
- package/dist/scaffolds/notis-database/tsconfig.json +23 -0
- package/dist/scaffolds/notis-database/vite.config.ts +22 -0
- package/dist/scaffolds/notis-notes/app/globals.css +3 -0
- package/dist/scaffolds/notis-notes/app/layout.tsx +6 -0
- package/dist/scaffolds/notis-notes/app/page.tsx +2168 -0
- package/dist/scaffolds/notis-notes/components/ui/badge.tsx +28 -0
- package/dist/scaffolds/notis-notes/components/ui/button.tsx +53 -0
- package/dist/scaffolds/notis-notes/components/ui/card.tsx +56 -0
- package/dist/scaffolds/notis-notes/components.json +20 -0
- package/dist/scaffolds/notis-notes/lib/utils.ts +6 -0
- package/dist/scaffolds/notis-notes/metadata/cover.png +0 -0
- package/dist/scaffolds/notis-notes/metadata/screenshot-1.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-notes/notis.config.ts +36 -0
- package/dist/scaffolds/notis-notes/package.json +31 -0
- package/dist/scaffolds/notis-notes/postcss.config.mjs +8 -0
- package/dist/scaffolds/notis-notes/tailwind.config.ts +58 -0
- package/dist/scaffolds/notis-notes/tsconfig.json +23 -0
- package/dist/scaffolds/notis-notes/vite.config.ts +10 -0
- package/dist/scaffolds/notis-random/README.md +33 -0
- package/dist/scaffolds/notis-random/app/globals.css +11 -0
- package/dist/scaffolds/notis-random/app/history/page.tsx +66 -0
- package/dist/scaffolds/notis-random/app/layout.tsx +7 -0
- package/dist/scaffolds/notis-random/app/page.tsx +222 -0
- package/dist/scaffolds/notis-random/index.html +12 -0
- package/dist/scaffolds/notis-random/lib/notis-tools.ts +109 -0
- package/dist/scaffolds/notis-random/lib/rng.ts +42 -0
- package/dist/scaffolds/notis-random/lib/roll-record.ts +102 -0
- package/dist/scaffolds/notis-random/lib/utils.ts +25 -0
- package/dist/scaffolds/notis-random/metadata/cover.png +0 -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/metadata/screenshot-3.png +0 -0
- package/dist/scaffolds/notis-random/notis.config.ts +43 -0
- package/dist/scaffolds/notis-random/package.json +32 -0
- package/dist/scaffolds/notis-random/postcss.config.mjs +6 -0
- package/dist/scaffolds/notis-random/src/dev-main.tsx +70 -0
- package/dist/scaffolds/notis-random/src/mock-runtime.ts +128 -0
- package/dist/scaffolds/notis-random/tailwind.config.ts +43 -0
- package/dist/scaffolds/notis-random/tsconfig.json +23 -0
- package/dist/scaffolds/notis-random/vite.config.ts +11 -0
- package/dist/scaffolds.json +36 -0
- package/package.json +8 -3
- package/skills/notis-apps/SKILL.md +162 -0
- package/skills/notis-apps/cli.md +208 -0
- package/skills/notis-cli/SKILL.md +260 -0
- package/skills/notis-query/cli.md +39 -0
- package/src/cli.js +31 -3
- package/src/command-specs/apps.js +248 -56
- package/src/command-specs/helpers.js +72 -104
- package/src/command-specs/index.js +0 -6
- package/src/command-specs/meta.js +14 -13
- package/src/command-specs/tools.js +196 -115
- package/src/runtime/app-boundary-validator.js +65 -14
- package/src/runtime/app-dev-server.js +32 -4
- package/src/runtime/app-platform.js +404 -24
- package/src/runtime/profiles.js +12 -6
- package/src/runtime/transport.js +124 -39
- 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,
|
|
@@ -44,6 +46,7 @@ import {
|
|
|
44
46
|
import { getAvailablePort } from '../runtime/ports.js';
|
|
45
47
|
import { getCliMode } from '../runtime/cli-mode.js';
|
|
46
48
|
import {
|
|
49
|
+
localNotisToolSlug,
|
|
47
50
|
nextIdempotencyKey,
|
|
48
51
|
runToolCommand,
|
|
49
52
|
toolConflictToError,
|
|
@@ -52,8 +55,11 @@ import {
|
|
|
52
55
|
const DEFAULT_DEV_PORT = 5173;
|
|
53
56
|
const DEV_HEARTBEAT_INTERVAL_MS = 10_000;
|
|
54
57
|
const CONFIG_FILENAMES = ['notis.config.ts', 'notis.config.js', 'notis.config.mjs'];
|
|
55
|
-
const ENSURE_DEV_APP_INSTALLATION_TOOL = '
|
|
56
|
-
const GET_APP_TOOL = '
|
|
58
|
+
const ENSURE_DEV_APP_INSTALLATION_TOOL = 'LOCAL_NOTIS_ENSURE_DEV_APP_INSTALLATION';
|
|
59
|
+
const GET_APP_TOOL = 'LOCAL_NOTIS_GET_APP';
|
|
60
|
+
const LIST_APPS_TOOL = 'LOCAL_NOTIS_LIST_APPS';
|
|
61
|
+
const CREATE_APP_TOOL = 'LOCAL_NOTIS_CREATE_APP';
|
|
62
|
+
const SAVE_APP_FILES_TOOL = 'LOCAL_NOTIS_SAVE_APP_FILES';
|
|
57
63
|
|
|
58
64
|
// ---------------------------------------------------------------------------
|
|
59
65
|
// Formatters
|
|
@@ -68,6 +74,15 @@ function appsTable(apps) {
|
|
|
68
74
|
]);
|
|
69
75
|
}
|
|
70
76
|
|
|
77
|
+
function scaffoldsTable(scaffolds) {
|
|
78
|
+
return formatTable(scaffolds, [
|
|
79
|
+
{ label: 'Slug', value: (scaffold) => scaffold.slug || '' },
|
|
80
|
+
{ label: 'Name', value: (scaffold) => scaffold.name || scaffold.slug || '' },
|
|
81
|
+
{ label: 'Category', value: (scaffold) => (scaffold.categories || [])[0] || '' },
|
|
82
|
+
{ label: 'Tagline', value: (scaffold) => scaffold.tagline || scaffold.description || '' },
|
|
83
|
+
]);
|
|
84
|
+
}
|
|
85
|
+
|
|
71
86
|
function hasNotisConfig(dir) {
|
|
72
87
|
return CONFIG_FILENAMES.some((name) => existsSync(join(dir, name)));
|
|
73
88
|
}
|
|
@@ -155,6 +170,24 @@ function buildDevInstallSlug(appName) {
|
|
|
155
170
|
return base.endsWith('-dev') ? base : `${base}-dev`;
|
|
156
171
|
}
|
|
157
172
|
|
|
173
|
+
function timingMs(startedAt) {
|
|
174
|
+
return Number(process.hrtime.bigint() - startedAt) / 1_000_000;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
function logAppsTiming(label, details = {}) {
|
|
178
|
+
const suffix = Object.entries(details)
|
|
179
|
+
.map(([key, value]) => `${key}=${value}`)
|
|
180
|
+
.join(' ');
|
|
181
|
+
process.stderr.write(`[notis apps timing] ${label}${suffix ? ` ${suffix}` : ''}\n`);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
function nextDevInstallIdempotencyKey(globalOptions = {}, devSlug) {
|
|
185
|
+
if (globalOptions.idempotencyKey) {
|
|
186
|
+
return `${globalOptions.idempotencyKey}:${devSlug}`;
|
|
187
|
+
}
|
|
188
|
+
return nextIdempotencyKey(globalOptions);
|
|
189
|
+
}
|
|
190
|
+
|
|
158
191
|
function pickDefaultRouteSlug(manifest) {
|
|
159
192
|
const routes = Array.isArray(manifest?.routes) ? manifest.routes : [];
|
|
160
193
|
const explicit = routes.find((route) => route && route.default && typeof route.slug === 'string');
|
|
@@ -163,8 +196,29 @@ function pickDefaultRouteSlug(manifest) {
|
|
|
163
196
|
return firstWithSlug ? firstWithSlug.slug : null;
|
|
164
197
|
}
|
|
165
198
|
|
|
166
|
-
|
|
167
|
-
|
|
199
|
+
const DESKTOP_DEEP_LINK_SCHEME_PATTERN = /^[a-z][a-z0-9-]*$/;
|
|
200
|
+
|
|
201
|
+
export function resolveDevelopmentDesktopScheme(env = process.env) {
|
|
202
|
+
// Mirror the desktop app's own scheme resolution (electron main + forge config):
|
|
203
|
+
// local dev launches register `notis-dev`, while installed prod/beta builds claim
|
|
204
|
+
// `notis`. Hardcoding `notis` here is what made `apps dev` open the installed
|
|
205
|
+
// prod/beta app instead of the local dev app.
|
|
206
|
+
const scheme = (env.NOTIS_DESKTOP_DEEP_LINK_SCHEME || '').trim();
|
|
207
|
+
return DESKTOP_DEEP_LINK_SCHEME_PATTERN.test(scheme) ? scheme : 'notis';
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
export function buildDevelopmentDesktopUrl(appHref = null, scheme = 'notis') {
|
|
211
|
+
const route = String(appHref || '/store').replace(/^\/+/, '');
|
|
212
|
+
const normalizedScheme = DESKTOP_DEEP_LINK_SCHEME_PATTERN.test(scheme) ? scheme : 'notis';
|
|
213
|
+
return `${normalizedScheme}://${route || 'store'}`;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
export function shouldOpenDevelopmentTab(options = {}) {
|
|
217
|
+
// Commander stores the negatable `--no-open` flag as `options.open === false`;
|
|
218
|
+
// it never sets `options.noOpen`. Reading the non-existent `noOpen` key meant
|
|
219
|
+
// `--no-open` was silently ignored and `apps dev` always ran `open notis://…`,
|
|
220
|
+
// which macOS routes to the installed prod/beta desktop app.
|
|
221
|
+
return options.open !== false;
|
|
168
222
|
}
|
|
169
223
|
|
|
170
224
|
function buildAppHref({ appSlug, appId, manifest }) {
|
|
@@ -279,13 +333,16 @@ function assertHarnessResult(result, route, databaseSlugs) {
|
|
|
279
333
|
}
|
|
280
334
|
for (const databaseSlug of databaseSlugs) {
|
|
281
335
|
const seen = (result.runtimeCalls || []).some(
|
|
282
|
-
(call) =>
|
|
336
|
+
(call) =>
|
|
337
|
+
call?.op === 'callTool' &&
|
|
338
|
+
localNotisToolSlug(call?.args?.name) === 'LOCAL_NOTIS_DATABASE_QUERY' &&
|
|
339
|
+
call?.args?.arguments?.database_slug === databaseSlug,
|
|
283
340
|
);
|
|
284
341
|
if (!seen) {
|
|
285
342
|
assertions.push({
|
|
286
343
|
ok: false,
|
|
287
344
|
code: 'missing_database_query',
|
|
288
|
-
message: `Route "${route.slug}" did not call
|
|
345
|
+
message: `Route "${route.slug}" did not call LOCAL_NOTIS_DATABASE_QUERY for "${databaseSlug}".`,
|
|
289
346
|
details: { databaseSlug },
|
|
290
347
|
});
|
|
291
348
|
}
|
|
@@ -341,7 +398,26 @@ function buildManifestForDev(appConfig) {
|
|
|
341
398
|
};
|
|
342
399
|
}
|
|
343
400
|
|
|
344
|
-
|
|
401
|
+
export function buildEnsureDevInstallArguments({ appConfig, manifest, linkedState }) {
|
|
402
|
+
const devSlug = buildDevInstallSlug(appConfig.name);
|
|
403
|
+
const arguments_ = {
|
|
404
|
+
dev_slug: devSlug,
|
|
405
|
+
name: appConfig.name,
|
|
406
|
+
manifest,
|
|
407
|
+
};
|
|
408
|
+
if (linkedState?.dev_app_id) {
|
|
409
|
+
arguments_.app_id = linkedState.dev_app_id;
|
|
410
|
+
}
|
|
411
|
+
return arguments_;
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
export async function ensureDevInstall({
|
|
415
|
+
ctx,
|
|
416
|
+
appConfig,
|
|
417
|
+
projectDir,
|
|
418
|
+
idempotencyKey,
|
|
419
|
+
runTool = runToolCommand,
|
|
420
|
+
}) {
|
|
345
421
|
if (!appConfig.name) {
|
|
346
422
|
throw usageError(`notis.config.ts in ${projectDir} must define a non-empty name.`);
|
|
347
423
|
}
|
|
@@ -351,14 +427,12 @@ async function ensureDevInstall({ ctx, appConfig, projectDir, idempotencyKey })
|
|
|
351
427
|
}
|
|
352
428
|
|
|
353
429
|
const manifest = buildManifestForDev(appConfig);
|
|
354
|
-
const
|
|
430
|
+
const linkedState = readLinkedState(projectDir);
|
|
431
|
+
const ensureArguments = buildEnsureDevInstallArguments({ appConfig, manifest, linkedState });
|
|
432
|
+
const ensureResult = await runTool({
|
|
355
433
|
runtime: ctx.runtime,
|
|
356
434
|
toolName: ENSURE_DEV_APP_INSTALLATION_TOOL,
|
|
357
|
-
arguments_:
|
|
358
|
-
dev_slug: devSlug,
|
|
359
|
-
name: appConfig.name,
|
|
360
|
-
manifest,
|
|
361
|
-
},
|
|
435
|
+
arguments_: ensureArguments,
|
|
362
436
|
mutating: true,
|
|
363
437
|
idempotencyKey,
|
|
364
438
|
});
|
|
@@ -374,13 +448,30 @@ async function ensureDevInstall({ ctx, appConfig, projectDir, idempotencyKey })
|
|
|
374
448
|
projectDir,
|
|
375
449
|
manifest,
|
|
376
450
|
created: ensureResult.payload.created || false,
|
|
451
|
+
linkedAppId: linkedState?.app_id || null,
|
|
452
|
+
databaseMaterialization: ensureResult.payload.database_materialization || { created: [], unresolved: [] },
|
|
377
453
|
};
|
|
378
454
|
}
|
|
379
455
|
|
|
456
|
+
function databaseMaterializationWarnings(apps) {
|
|
457
|
+
const warnings = [];
|
|
458
|
+
for (const app of apps) {
|
|
459
|
+
const unresolved = app.databaseMaterialization?.unresolved || [];
|
|
460
|
+
if (!unresolved.length) {
|
|
461
|
+
continue;
|
|
462
|
+
}
|
|
463
|
+
warnings.push(
|
|
464
|
+
`${app.name}: database slug${unresolved.length === 1 ? '' : 's'} ${unresolved.join(', ')} ` +
|
|
465
|
+
'could not be created because no store snapshot schema exists. Create them manually or link to a store-installed app.',
|
|
466
|
+
);
|
|
467
|
+
}
|
|
468
|
+
return warnings;
|
|
469
|
+
}
|
|
470
|
+
|
|
380
471
|
async function assertDirectDeployAccess(runtime, appId) {
|
|
381
472
|
const result = await runToolCommand({
|
|
382
473
|
runtime,
|
|
383
|
-
toolName:
|
|
474
|
+
toolName: LIST_APPS_TOOL,
|
|
384
475
|
});
|
|
385
476
|
const apps = result.payload.apps || [];
|
|
386
477
|
const hasAccess = apps.some((app) => (app.app_id || app.id) === appId);
|
|
@@ -396,7 +487,7 @@ async function assertDirectDeployAccess(runtime, appId) {
|
|
|
396
487
|
async function appsListHandler(ctx) {
|
|
397
488
|
const result = await runToolCommand({
|
|
398
489
|
runtime: ctx.runtime,
|
|
399
|
-
toolName:
|
|
490
|
+
toolName: LIST_APPS_TOOL,
|
|
400
491
|
});
|
|
401
492
|
const apps = result.payload.apps || [];
|
|
402
493
|
return ctx.output.emitSuccess({
|
|
@@ -409,13 +500,16 @@ async function appsListHandler(ctx) {
|
|
|
409
500
|
|
|
410
501
|
async function appsInitHandler(ctx) {
|
|
411
502
|
const projectDir = resolveProjectDir(ctx.args.dir || ctx.args.name.toLowerCase().replace(/[^a-z0-9]+/g, '-'));
|
|
503
|
+
const fromSlug = ctx.options.from || null;
|
|
412
504
|
|
|
413
|
-
scaffoldProject({ projectDir, appName: ctx.args.name });
|
|
505
|
+
scaffoldProject({ projectDir, appName: ctx.args.name, fromSlug });
|
|
414
506
|
|
|
415
507
|
return ctx.output.emitSuccess({
|
|
416
508
|
command: ctx.spec.command_path.join(' '),
|
|
417
|
-
data: { project_dir: projectDir, app_name: ctx.args.name },
|
|
418
|
-
humanSummary:
|
|
509
|
+
data: { project_dir: projectDir, app_name: ctx.args.name, scaffold: fromSlug },
|
|
510
|
+
humanSummary: fromSlug
|
|
511
|
+
? `Scaffolded "${ctx.args.name}" from ${fromSlug} in ${projectDir}`
|
|
512
|
+
: `Scaffolded "${ctx.args.name}" in ${projectDir}`,
|
|
419
513
|
hints: [
|
|
420
514
|
{ command: `cd ${projectDir} && npm install`, reason: 'Install dependencies' },
|
|
421
515
|
{ command: `cd ${projectDir} && notis apps dev`, reason: 'Start the real-portal dev workflow' },
|
|
@@ -423,19 +517,29 @@ async function appsInitHandler(ctx) {
|
|
|
423
517
|
});
|
|
424
518
|
}
|
|
425
519
|
|
|
520
|
+
async function appsScaffoldsListHandler(ctx) {
|
|
521
|
+
const scaffolds = loadScaffoldCatalog();
|
|
522
|
+
return ctx.output.emitSuccess({
|
|
523
|
+
command: ctx.spec.command_path.join(' '),
|
|
524
|
+
data: { scaffolds },
|
|
525
|
+
humanSummary: scaffolds.length
|
|
526
|
+
? `Found ${scaffolds.length} bundled scaffolds`
|
|
527
|
+
: 'No bundled scaffolds found. Run the CLI build step to generate the scaffold catalog.',
|
|
528
|
+
renderHuman: () => (scaffolds.length ? scaffoldsTable(scaffolds) : 'No bundled scaffolds found.'),
|
|
529
|
+
});
|
|
530
|
+
}
|
|
531
|
+
|
|
426
532
|
async function appsCreateHandler(ctx) {
|
|
427
533
|
const projectDir = ctx.args.dir ? resolveProjectDir(ctx.args.dir) : null;
|
|
534
|
+
const appConfig = projectDir ? await loadAppConfig(projectDir) : null;
|
|
428
535
|
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
536
|
const result = await runToolCommand({
|
|
433
537
|
runtime: ctx.runtime,
|
|
434
|
-
toolName:
|
|
538
|
+
toolName: CREATE_APP_TOOL,
|
|
435
539
|
arguments_: {
|
|
436
540
|
name: ctx.args.name,
|
|
437
|
-
description:
|
|
438
|
-
icon,
|
|
541
|
+
description: appConfig?.description || undefined,
|
|
542
|
+
icon: appConfig?.icon || undefined,
|
|
439
543
|
},
|
|
440
544
|
mutating: true,
|
|
441
545
|
idempotencyKey,
|
|
@@ -490,25 +594,26 @@ async function appsDevHandler(ctx) {
|
|
|
490
594
|
const mode = getCliMode();
|
|
491
595
|
const identity = decodeJwtSub(ctx.runtime.jwt);
|
|
492
596
|
if (!identity) {
|
|
493
|
-
throw usageError('Could not determine the current user from the CLI auth token.
|
|
597
|
+
throw usageError('Could not determine the current user from the CLI auth token. Open the Notis desktop app, sign in, and retry.');
|
|
494
598
|
}
|
|
495
599
|
const apiBase = String(ctx.runtime.apiBase || '').replace(/\/$/, '');
|
|
496
600
|
const sessionId = randomUUID();
|
|
497
601
|
|
|
498
|
-
const
|
|
602
|
+
const candidates = [];
|
|
499
603
|
for (const projectDir of appDirs) {
|
|
500
604
|
const appConfig = await loadAppConfig(projectDir);
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
605
|
+
if (!appConfig.name) {
|
|
606
|
+
throw usageError(`notis.config.ts in ${projectDir} must define a non-empty name.`);
|
|
607
|
+
}
|
|
608
|
+
const devSlug = buildDevInstallSlug(appConfig.name);
|
|
609
|
+
if (!devSlug) {
|
|
610
|
+
throw usageError(`notis.config.ts name in ${projectDir} must slugify to a non-empty value.`);
|
|
611
|
+
}
|
|
612
|
+
candidates.push({ appConfig, devSlug, projectDir });
|
|
508
613
|
}
|
|
509
614
|
|
|
510
615
|
const seenSlugs = new Map();
|
|
511
|
-
for (const app of
|
|
616
|
+
for (const app of candidates) {
|
|
512
617
|
if (seenSlugs.has(app.devSlug)) {
|
|
513
618
|
throw usageError(
|
|
514
619
|
`Multiple apps slugify to "${app.devSlug}" (${seenSlugs.get(app.devSlug).projectDir} and ${app.projectDir}). Rename one in notis.config.ts.`,
|
|
@@ -517,6 +622,30 @@ async function appsDevHandler(ctx) {
|
|
|
517
622
|
seenSlugs.set(app.devSlug, app);
|
|
518
623
|
}
|
|
519
624
|
|
|
625
|
+
const registrationStartedAt = process.hrtime.bigint();
|
|
626
|
+
const registered = await Promise.all(
|
|
627
|
+
candidates.map(async ({ appConfig, devSlug, projectDir }) => {
|
|
628
|
+
const appStartedAt = process.hrtime.bigint();
|
|
629
|
+
try {
|
|
630
|
+
return await ensureDevInstall({
|
|
631
|
+
ctx,
|
|
632
|
+
appConfig,
|
|
633
|
+
projectDir,
|
|
634
|
+
idempotencyKey: nextDevInstallIdempotencyKey(ctx.globalOptions, devSlug),
|
|
635
|
+
});
|
|
636
|
+
} finally {
|
|
637
|
+
logAppsTiming('ensure-dev-install', {
|
|
638
|
+
slug: devSlug,
|
|
639
|
+
ms: timingMs(appStartedAt).toFixed(1),
|
|
640
|
+
});
|
|
641
|
+
}
|
|
642
|
+
}),
|
|
643
|
+
);
|
|
644
|
+
logAppsTiming('ensure-dev-install:all', {
|
|
645
|
+
apps: registered.length,
|
|
646
|
+
ms: timingMs(registrationStartedAt).toFixed(1),
|
|
647
|
+
});
|
|
648
|
+
|
|
520
649
|
const apps = registered.map((app) => {
|
|
521
650
|
const bundleBaseUrl = `http://127.0.0.1:${port}/a/${app.devSlug}`;
|
|
522
651
|
return {
|
|
@@ -529,10 +658,19 @@ async function appsDevHandler(ctx) {
|
|
|
529
658
|
}),
|
|
530
659
|
};
|
|
531
660
|
});
|
|
532
|
-
const developmentTabUrl = buildDevelopmentDesktopUrl(
|
|
661
|
+
const developmentTabUrl = buildDevelopmentDesktopUrl(
|
|
662
|
+
apps[0]?.appHref,
|
|
663
|
+
resolveDevelopmentDesktopScheme(),
|
|
664
|
+
);
|
|
665
|
+
const warnings = databaseMaterializationWarnings(apps);
|
|
533
666
|
|
|
534
667
|
const devServer = await startAppDevServer({
|
|
535
|
-
apps: apps.map((app) => ({
|
|
668
|
+
apps: apps.map((app) => ({
|
|
669
|
+
slug: app.devSlug,
|
|
670
|
+
projectDir: app.projectDir,
|
|
671
|
+
appId: app.appId,
|
|
672
|
+
userId: identity,
|
|
673
|
+
})),
|
|
536
674
|
port,
|
|
537
675
|
});
|
|
538
676
|
|
|
@@ -583,12 +721,15 @@ async function appsDevHandler(ctx) {
|
|
|
583
721
|
bundle_base_url: app.bundleBaseUrl,
|
|
584
722
|
app_href: app.appHref,
|
|
585
723
|
created: app.created,
|
|
724
|
+
linked_app_id: app.linkedAppId,
|
|
725
|
+
database_materialization: app.databaseMaterialization,
|
|
586
726
|
})),
|
|
587
727
|
},
|
|
728
|
+
warnings,
|
|
588
729
|
humanSummary: [
|
|
589
730
|
`Running apps dev against ${apiBase} as ${identity} (mode: ${mode})`,
|
|
590
731
|
'',
|
|
591
|
-
`
|
|
732
|
+
`Open in desktop: ${developmentTabUrl}`,
|
|
592
733
|
'',
|
|
593
734
|
...apps.map((app) => ` ${app.name.padEnd(24)} ${app.bundleBaseUrl} -> ${app.appHref}`),
|
|
594
735
|
'',
|
|
@@ -596,7 +737,7 @@ async function appsDevHandler(ctx) {
|
|
|
596
737
|
].join('\n'),
|
|
597
738
|
});
|
|
598
739
|
|
|
599
|
-
if (
|
|
740
|
+
if (shouldOpenDevelopmentTab(ctx.options)) {
|
|
600
741
|
openInBrowser(developmentTabUrl);
|
|
601
742
|
}
|
|
602
743
|
|
|
@@ -665,7 +806,7 @@ async function appsVerifyHandler(ctx) {
|
|
|
665
806
|
let linkedState = null;
|
|
666
807
|
if (mode === 'live') {
|
|
667
808
|
if (!ctx.runtime.jwt) {
|
|
668
|
-
throw usageError('Live verify mode requires CLI auth.
|
|
809
|
+
throw usageError('Live verify mode requires CLI auth. Open the Notis desktop app, sign in, and retry.');
|
|
669
810
|
}
|
|
670
811
|
linkedState = readLinkedState(projectDir);
|
|
671
812
|
if (!linkedState?.app_id) {
|
|
@@ -679,6 +820,11 @@ async function appsVerifyHandler(ctx) {
|
|
|
679
820
|
|
|
680
821
|
const manifest = readManifest(projectDir);
|
|
681
822
|
const appConfig = await loadAppConfig(projectDir);
|
|
823
|
+
const listing = inspectListingReadiness(projectDir, appConfig);
|
|
824
|
+
if (listing.errors.length) {
|
|
825
|
+
throw usageError(`Listing metadata has problems:\n${listing.errors.map((error) => ` - ${error}`).join('\n')}`);
|
|
826
|
+
}
|
|
827
|
+
const listingWarnings = listing.warnings;
|
|
682
828
|
const routes = routeSelection(manifest, parseRouteSlugs(ctx.options.routes));
|
|
683
829
|
const port = parsePort(ctx.options.port) || await getAvailablePort();
|
|
684
830
|
const appSlug = slugify(appConfig.name || manifest.app?.name || 'app') || 'app';
|
|
@@ -713,7 +859,7 @@ async function appsVerifyHandler(ctx) {
|
|
|
713
859
|
}));
|
|
714
860
|
|
|
715
861
|
let results;
|
|
716
|
-
const warnings = [];
|
|
862
|
+
const warnings = [...listingWarnings];
|
|
717
863
|
if (noBrowser) {
|
|
718
864
|
results = urls.map(({ route, url }) => ({
|
|
719
865
|
route: route.slug,
|
|
@@ -905,10 +1051,27 @@ async function appsPullHandler(ctx) {
|
|
|
905
1051
|
});
|
|
906
1052
|
}
|
|
907
1053
|
|
|
1054
|
+
function updateLinkedDeployState(projectDir, linkedState, appId, version) {
|
|
1055
|
+
if (!linkedState || linkedState.app_id !== appId || !Number.isFinite(version)) {
|
|
1056
|
+
return;
|
|
1057
|
+
}
|
|
1058
|
+
writeLinkedState(projectDir, {
|
|
1059
|
+
...linkedState,
|
|
1060
|
+
app_id: appId,
|
|
1061
|
+
version,
|
|
1062
|
+
linked_at: linkedState.linked_at || new Date().toISOString(),
|
|
1063
|
+
deployed_at: new Date().toISOString(),
|
|
1064
|
+
});
|
|
1065
|
+
}
|
|
1066
|
+
|
|
908
1067
|
async function appsDeployHandler(ctx) {
|
|
909
1068
|
const projectDir = resolveProjectDir(ctx.args.dir || '.');
|
|
910
1069
|
const appId = requireLinkedAppId(projectDir, ctx.options.appId);
|
|
911
1070
|
const idempotencyKey = nextIdempotencyKey(ctx.globalOptions);
|
|
1071
|
+
const linkedState = readLinkedState(projectDir);
|
|
1072
|
+
const baseVersion = linkedState?.app_id === appId && Number.isFinite(linkedState?.version)
|
|
1073
|
+
? linkedState.version
|
|
1074
|
+
: undefined;
|
|
912
1075
|
|
|
913
1076
|
// Build if needed
|
|
914
1077
|
if (!ctx.options.skipBuild) {
|
|
@@ -919,6 +1082,7 @@ async function appsDeployHandler(ctx) {
|
|
|
919
1082
|
if (ctx.options.direct) {
|
|
920
1083
|
await assertDirectDeployAccess(ctx.runtime, appId);
|
|
921
1084
|
const { version } = await directDeploy(projectDir, appId);
|
|
1085
|
+
updateLinkedDeployState(projectDir, linkedState, appId, version);
|
|
922
1086
|
return ctx.output.emitSuccess({
|
|
923
1087
|
command: ctx.spec.command_path.join(' '),
|
|
924
1088
|
data: { app_id: appId, version, mode: 'direct' },
|
|
@@ -936,12 +1100,13 @@ async function appsDeployHandler(ctx) {
|
|
|
936
1100
|
try {
|
|
937
1101
|
result = await runToolCommand({
|
|
938
1102
|
runtime: ctx.runtime,
|
|
939
|
-
toolName:
|
|
1103
|
+
toolName: SAVE_APP_FILES_TOOL,
|
|
940
1104
|
arguments_: {
|
|
941
1105
|
app_id: appId,
|
|
942
1106
|
files,
|
|
943
1107
|
source_files: sourceFiles,
|
|
944
1108
|
manifest,
|
|
1109
|
+
...(baseVersion !== undefined ? { base_version: baseVersion } : {}),
|
|
945
1110
|
},
|
|
946
1111
|
mutating: true,
|
|
947
1112
|
idempotencyKey,
|
|
@@ -968,6 +1133,7 @@ async function appsDeployHandler(ctx) {
|
|
|
968
1133
|
|
|
969
1134
|
try {
|
|
970
1135
|
const { version } = await directDeploy(projectDir, appId);
|
|
1136
|
+
updateLinkedDeployState(projectDir, linkedState, appId, version);
|
|
971
1137
|
return ctx.output.emitSuccess({
|
|
972
1138
|
command: ctx.spec.command_path.join(' '),
|
|
973
1139
|
data: { app_id: appId, version, mode: 'direct-fallback' },
|
|
@@ -986,6 +1152,7 @@ async function appsDeployHandler(ctx) {
|
|
|
986
1152
|
throw error;
|
|
987
1153
|
}
|
|
988
1154
|
|
|
1155
|
+
updateLinkedDeployState(projectDir, linkedState, appId, Number(result.payload.version));
|
|
989
1156
|
return ctx.output.emitSuccess({
|
|
990
1157
|
command: ctx.spec.command_path.join(' '),
|
|
991
1158
|
data: {
|
|
@@ -1009,13 +1176,21 @@ async function appsDoctorHandler(ctx) {
|
|
|
1009
1176
|
problems.push('Failed to load notis.config.ts');
|
|
1010
1177
|
}
|
|
1011
1178
|
const warnings = detectProjectWarnings(projectDir, appConfig);
|
|
1179
|
+
let listing = null;
|
|
1180
|
+
if (appConfig) {
|
|
1181
|
+
try {
|
|
1182
|
+
listing = inspectListingReadiness(projectDir, appConfig);
|
|
1183
|
+
} catch (error) {
|
|
1184
|
+
warnings.push(error instanceof Error ? error.message : String(error));
|
|
1185
|
+
}
|
|
1186
|
+
}
|
|
1012
1187
|
|
|
1013
1188
|
const linkedState = readLinkedState(projectDir);
|
|
1014
1189
|
const status = problems.length ? 'unhealthy' : warnings.length ? 'warnings' : 'healthy';
|
|
1015
1190
|
|
|
1016
1191
|
return ctx.output.emitSuccess({
|
|
1017
1192
|
command: ctx.spec.command_path.join(' '),
|
|
1018
|
-
data: { status, problems, warnings, linked: linkedState, config: appConfig },
|
|
1193
|
+
data: { status, problems, warnings, linked: linkedState, config: appConfig, listing },
|
|
1019
1194
|
humanSummary: problems.length
|
|
1020
1195
|
? `Found ${problems.length} problems:\n${problems.map((p) => ` - ${p}`).join('\n')}`
|
|
1021
1196
|
: warnings.length
|
|
@@ -1037,27 +1212,46 @@ export const appsCommandSpecs = [
|
|
|
1037
1212
|
examples: ['notis apps list', 'notis apps list --json'],
|
|
1038
1213
|
mutates: false,
|
|
1039
1214
|
idempotent: true,
|
|
1040
|
-
backend_call: { type: 'tool', name:
|
|
1215
|
+
backend_call: { type: 'tool', name: LIST_APPS_TOOL },
|
|
1041
1216
|
handler: appsListHandler,
|
|
1042
1217
|
},
|
|
1043
1218
|
{
|
|
1044
1219
|
command_path: ['apps', 'init'],
|
|
1045
1220
|
summary: 'Scaffold a new Notis app project.',
|
|
1046
|
-
when_to_use: 'Start a new Notis app.
|
|
1221
|
+
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
1222
|
args_schema: {
|
|
1048
1223
|
arguments: [
|
|
1049
1224
|
{ token: '<name>', description: 'Display name for the app.' },
|
|
1050
1225
|
{ token: '[dir]', key: 'dir', description: 'Target directory (defaults to kebab-case of name).' },
|
|
1051
1226
|
],
|
|
1052
|
-
options: [
|
|
1227
|
+
options: [
|
|
1228
|
+
{ flags: '--from <slug>', description: 'Start from a bundled scaffold listed by `notis apps scaffolds list`.' },
|
|
1229
|
+
],
|
|
1053
1230
|
},
|
|
1054
|
-
examples: [
|
|
1231
|
+
examples: [
|
|
1232
|
+
'notis apps scaffolds list',
|
|
1233
|
+
'notis apps init "Mind the Flo"',
|
|
1234
|
+
'notis apps init "My CRM" --from notis-database',
|
|
1235
|
+
'notis apps init "My App" ./my-app',
|
|
1236
|
+
],
|
|
1055
1237
|
mutates: true,
|
|
1056
1238
|
idempotent: false,
|
|
1057
1239
|
require_auth: false,
|
|
1058
1240
|
backend_call: { type: 'local', name: 'scaffold_project' },
|
|
1059
1241
|
handler: appsInitHandler,
|
|
1060
1242
|
},
|
|
1243
|
+
{
|
|
1244
|
+
command_path: ['apps', 'scaffolds', 'list'],
|
|
1245
|
+
summary: 'List bundled Notis app scaffolds.',
|
|
1246
|
+
when_to_use: 'Discover the fixed scaffold catalog shipped with the CLI before starting a new app.',
|
|
1247
|
+
args_schema: { arguments: [], options: [] },
|
|
1248
|
+
examples: ['notis apps scaffolds list', 'notis apps init "My App" --from notis-database'],
|
|
1249
|
+
mutates: false,
|
|
1250
|
+
idempotent: true,
|
|
1251
|
+
require_auth: false,
|
|
1252
|
+
backend_call: { type: 'local', name: 'list_scaffolds' },
|
|
1253
|
+
handler: appsScaffoldsListHandler,
|
|
1254
|
+
},
|
|
1061
1255
|
{
|
|
1062
1256
|
command_path: ['apps', 'create'],
|
|
1063
1257
|
summary: 'Create a new remote Notis app and optionally link a local project to it.',
|
|
@@ -1067,32 +1261,29 @@ export const appsCommandSpecs = [
|
|
|
1067
1261
|
{ token: '<name>', description: 'Display name for the remote app.' },
|
|
1068
1262
|
{ token: '[dir]', key: 'dir', description: 'Project directory to link after creation (default: do not link).' },
|
|
1069
1263
|
],
|
|
1070
|
-
options: [
|
|
1071
|
-
{ flags: '--description <text>', description: 'Optional app description.' },
|
|
1072
|
-
{ flags: '--icon <lucide:icon>', description: 'Optional Lucide icon, for example lucide:dices.' },
|
|
1073
|
-
],
|
|
1264
|
+
options: [],
|
|
1074
1265
|
},
|
|
1075
1266
|
examples: [
|
|
1076
1267
|
'notis apps create "My App"',
|
|
1077
|
-
'notis apps create "My App" .
|
|
1268
|
+
'notis apps create "My App" .',
|
|
1078
1269
|
],
|
|
1079
1270
|
mutates: true,
|
|
1080
1271
|
idempotent: false,
|
|
1081
|
-
backend_call: { type: 'tool', name:
|
|
1272
|
+
backend_call: { type: 'tool', name: CREATE_APP_TOOL },
|
|
1082
1273
|
handler: appsCreateHandler,
|
|
1083
1274
|
},
|
|
1084
1275
|
{
|
|
1085
1276
|
command_path: ['apps', 'dev'],
|
|
1086
1277
|
summary: 'Develop Notis apps inside the Electron desktop Portal with automatic local bundle reloads.',
|
|
1087
1278
|
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
|
|
1279
|
+
'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
1280
|
args_schema: {
|
|
1090
1281
|
arguments: [
|
|
1091
1282
|
{ token: '[dir]', key: 'dir', description: 'Project directory or monorepo root (default: current dir).' },
|
|
1092
1283
|
],
|
|
1093
1284
|
options: [
|
|
1094
1285
|
{ flags: '--port <number>', description: `Local bundle server port (default: ${DEFAULT_DEV_PORT}).` },
|
|
1095
|
-
{ flags: '--no-open', description: 'Do not auto-open the desktop Portal
|
|
1286
|
+
{ flags: '--no-open', description: 'Do not auto-open the desktop Portal local development app.' },
|
|
1096
1287
|
],
|
|
1097
1288
|
},
|
|
1098
1289
|
examples: [
|
|
@@ -1175,7 +1366,8 @@ export const appsCommandSpecs = [
|
|
|
1175
1366
|
{
|
|
1176
1367
|
command_path: ['apps', 'pull'],
|
|
1177
1368
|
summary: 'Download a Notis app source snapshot into a local project folder.',
|
|
1178
|
-
when_to_use:
|
|
1369
|
+
when_to_use:
|
|
1370
|
+
'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
1371
|
args_schema: {
|
|
1180
1372
|
arguments: [
|
|
1181
1373
|
{ token: '<app-id>', description: 'Remote app ID to pull.' },
|
|
@@ -1211,7 +1403,7 @@ export const appsCommandSpecs = [
|
|
|
1211
1403
|
examples: ['notis apps deploy', 'notis apps deploy --skip-build', 'notis apps deploy --app-id abc123', 'notis apps deploy --direct'],
|
|
1212
1404
|
mutates: true,
|
|
1213
1405
|
idempotent: true,
|
|
1214
|
-
backend_call: { type: 'tool', name:
|
|
1406
|
+
backend_call: { type: 'tool', name: SAVE_APP_FILES_TOOL },
|
|
1215
1407
|
handler: appsDeployHandler,
|
|
1216
1408
|
},
|
|
1217
1409
|
{
|