@notis_ai/cli 0.2.0-beta.32.1 → 0.2.0-beta.35.1
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 +41 -106
- 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 +168 -0
- package/skills/notis-apps/cli.md +208 -0
- package/skills/notis-cli/SKILL.md +263 -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 +30 -2
- package/src/runtime/app-platform.js +376 -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}/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/useMultiSelect.ts +4 -3
- package/template/packages/sdk/src/hooks/useNotis.ts +30 -0
- 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/index.ts +3 -24
- package/template/packages/{notis-sdk → sdk}/src/provider.tsx +1 -1
- package/template/packages/sdk/src/runtime.ts +80 -0
- 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/useDatabase.ts +0 -88
- package/template/packages/notis-sdk/src/hooks/useDocument.ts +0 -61
- package/template/packages/notis-sdk/src/hooks/useNotis.ts +0 -33
- package/template/packages/notis-sdk/src/hooks/useUpsertDocument.ts +0 -58
- package/template/packages/notis-sdk/src/runtime.ts +0 -182
- /package/template/packages/{notis-sdk → sdk}/package.json +0 -0
- /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
|
@@ -171,37 +171,6 @@ async function toolsExecHandler(ctx) {
|
|
|
171
171
|
|
|
172
172
|
const idempotencyKey = nextIdempotencyKey(ctx.globalOptions);
|
|
173
173
|
|
|
174
|
-
if (ctx.options.watch) {
|
|
175
|
-
const intervalSec = Math.max(1, parseInt(ctx.options.watch, 10));
|
|
176
|
-
const intervalMs = intervalSec * 1000;
|
|
177
|
-
process.on('SIGINT', () => process.exit(0));
|
|
178
|
-
|
|
179
|
-
while (true) {
|
|
180
|
-
try {
|
|
181
|
-
const result = await runToolCommand({
|
|
182
|
-
runtime: ctx.runtime,
|
|
183
|
-
toolName: 'notis_execute_tool',
|
|
184
|
-
arguments_: {
|
|
185
|
-
tool_name: ctx.args.toolName,
|
|
186
|
-
arguments: args,
|
|
187
|
-
...(toolkits.length ? { toolkits } : {}),
|
|
188
|
-
},
|
|
189
|
-
mutating: true,
|
|
190
|
-
idempotencyKey: nextIdempotencyKey(ctx.globalOptions),
|
|
191
|
-
});
|
|
192
|
-
ctx.output.emitSuccess({
|
|
193
|
-
command: ctx.spec.command_path.join(' '),
|
|
194
|
-
data: result.payload,
|
|
195
|
-
humanSummary: `[${new Date().toISOString()}] ${ctx.args.toolName}`,
|
|
196
|
-
renderHuman: () => JSON.stringify(result.payload, null, 2),
|
|
197
|
-
});
|
|
198
|
-
} catch (error) {
|
|
199
|
-
process.stderr.write(`[${new Date().toISOString()}] Error: ${error.message}\n`);
|
|
200
|
-
}
|
|
201
|
-
await new Promise((r) => setTimeout(r, intervalMs));
|
|
202
|
-
}
|
|
203
|
-
}
|
|
204
|
-
|
|
205
174
|
const result = await runToolCommand({
|
|
206
175
|
runtime: ctx.runtime,
|
|
207
176
|
toolName: 'notis_execute_tool',
|
|
@@ -359,17 +328,16 @@ export const toolsCommandSpecs = [
|
|
|
359
328
|
{ flags: '--arguments <json>', description: 'JSON object, @file path, or - for stdin.' },
|
|
360
329
|
{ flags: '--get-schema', description: 'Display the tool parameter schema without executing.' },
|
|
361
330
|
{ flags: '--dry-run', description: 'Validate arguments against the tool schema without executing.' },
|
|
362
|
-
{ flags: '--watch <seconds>', description: 'Re-execute on an interval and stream results.' },
|
|
363
331
|
{ flags: '--toolkits <csv-or-json>', description: 'Optional toolkit namespace(s) to use when resolving the tool.' },
|
|
364
332
|
],
|
|
365
333
|
},
|
|
366
334
|
examples: [
|
|
367
335
|
'notis tools exec notis-default-query --arguments \'{"database_slug":"tasks","query":{}}\'',
|
|
336
|
+
'notis tools exec notis-default-get_database --arguments \'{"database_slug":"tasks"}\'',
|
|
368
337
|
'notis tools exec notis-default-query --get-schema',
|
|
369
338
|
'notis tools exec notis-default-query --dry-run --arguments \'{"database_slug":"tasks","query":{}}\'',
|
|
370
339
|
'notis tools exec notis-default-query --arguments @query.json',
|
|
371
340
|
'notis tools exec notis-default-query --arguments - < query.json',
|
|
372
|
-
'notis tools exec notis-default-query --watch 10 --arguments \'{"database_slug":"tasks","query":{}}\'',
|
|
373
341
|
],
|
|
374
342
|
output_schema: 'Returns the raw tool execution payload.',
|
|
375
343
|
mutates: true,
|
|
@@ -23,6 +23,7 @@ import {
|
|
|
23
23
|
exportNameFromPath,
|
|
24
24
|
getBundleDir,
|
|
25
25
|
loadAppConfig,
|
|
26
|
+
prepareArtifactBuild,
|
|
26
27
|
readManifest,
|
|
27
28
|
} from './app-platform.js';
|
|
28
29
|
|
|
@@ -77,6 +78,14 @@ function scriptJson(value) {
|
|
|
77
78
|
.replace(/\u2029/g, '\\u2029');
|
|
78
79
|
}
|
|
79
80
|
|
|
81
|
+
function timingMs(startedAt) {
|
|
82
|
+
return Number(process.hrtime.bigint() - startedAt) / 1_000_000;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function formatTimingMs(value) {
|
|
86
|
+
return value.toFixed(value < 10 ? 2 : 1);
|
|
87
|
+
}
|
|
88
|
+
|
|
80
89
|
function readJsonFile(path) {
|
|
81
90
|
if (!existsSync(path)) {
|
|
82
91
|
return null;
|
|
@@ -234,7 +243,7 @@ function renderHarnessHtml({ state, manifest, appConfig, route, harnessOptions }
|
|
|
234
243
|
/**
|
|
235
244
|
* Start the dev server for one or more apps.
|
|
236
245
|
*
|
|
237
|
-
* @param {{apps: Array<{slug: string, projectDir: string, appId?: string}>, port: number, watch?: boolean, harness?: { mode?: string, apiBase?: string, jwt?: string }, log?: (m: string) => void, logError?: (m: string) => void}} options
|
|
246
|
+
* @param {{apps: Array<{slug: string, projectDir: string, appId?: string, userId?: string}>, port: number, watch?: boolean, harness?: { mode?: string, apiBase?: string, jwt?: string }, log?: (m: string) => void, logError?: (m: string) => void}} options
|
|
238
247
|
*/
|
|
239
248
|
export async function startAppDevServer({
|
|
240
249
|
apps,
|
|
@@ -254,6 +263,7 @@ export async function startAppDevServer({
|
|
|
254
263
|
slug: app.slug,
|
|
255
264
|
projectDir: app.projectDir,
|
|
256
265
|
appId: app.appId || null,
|
|
266
|
+
userId: app.userId || null,
|
|
257
267
|
canonicalBundleDir: getBundleDir(app.projectDir),
|
|
258
268
|
bundleDir: null,
|
|
259
269
|
jsPath: null,
|
|
@@ -381,11 +391,21 @@ export async function startAppDevServer({
|
|
|
381
391
|
}
|
|
382
392
|
|
|
383
393
|
if (url.pathname === '/healthz') {
|
|
394
|
+
const now = new Date().toISOString();
|
|
395
|
+
const sessions = Array.from(appState.values()).map((state) => ({
|
|
396
|
+
appId: state.appId,
|
|
397
|
+
userId: state.userId,
|
|
398
|
+
devSlug: state.slug,
|
|
399
|
+
bundleBaseUrl: `http://127.0.0.1:${port}/a/${state.slug}`,
|
|
400
|
+
projectDir: state.projectDir,
|
|
401
|
+
lastHeartbeatAt: now,
|
|
402
|
+
status: 'connected',
|
|
403
|
+
}));
|
|
384
404
|
res.writeHead(200, {
|
|
385
405
|
...headers,
|
|
386
406
|
'Content-Type': 'application/json; charset=utf-8',
|
|
387
407
|
});
|
|
388
|
-
res.end(JSON.stringify({ ok: true, apps: Array.from(appState.keys()) }));
|
|
408
|
+
res.end(JSON.stringify({ ok: true, apps: Array.from(appState.keys()), sessions }));
|
|
389
409
|
return;
|
|
390
410
|
}
|
|
391
411
|
|
|
@@ -454,17 +474,20 @@ export async function startAppDevServer({
|
|
|
454
474
|
}
|
|
455
475
|
|
|
456
476
|
if (routed.rest.startsWith('bundle/')) {
|
|
477
|
+
const startedAt = process.hrtime.bigint();
|
|
457
478
|
updateBundleDir(state, resolveBundleDir(state));
|
|
458
479
|
const rel = routed.rest.slice('bundle/'.length);
|
|
459
480
|
if (!state.bundleDir) {
|
|
460
481
|
res.writeHead(404, headers);
|
|
461
482
|
res.end('not found');
|
|
483
|
+
log(`[notis apps timing] ${state.slug}: GET bundle/${rel} -> 404 in ${formatTimingMs(timingMs(startedAt))}ms`);
|
|
462
484
|
return;
|
|
463
485
|
}
|
|
464
486
|
const full = safeJoin(state.bundleDir, rel);
|
|
465
487
|
if (!full || !existsSync(full)) {
|
|
466
488
|
res.writeHead(404, headers);
|
|
467
489
|
res.end('not found');
|
|
490
|
+
log(`[notis apps timing] ${state.slug}: GET bundle/${rel} -> 404 in ${formatTimingMs(timingMs(startedAt))}ms`);
|
|
468
491
|
return;
|
|
469
492
|
}
|
|
470
493
|
const ext = extFor(rel);
|
|
@@ -480,6 +503,10 @@ export async function startAppDevServer({
|
|
|
480
503
|
} else {
|
|
481
504
|
res.end(content);
|
|
482
505
|
}
|
|
506
|
+
log(
|
|
507
|
+
`[notis apps timing] ${state.slug}: GET bundle/${rel} -> 200 ` +
|
|
508
|
+
`${content.byteLength}B in ${formatTimingMs(timingMs(startedAt))}ms`,
|
|
509
|
+
);
|
|
483
510
|
return;
|
|
484
511
|
}
|
|
485
512
|
|
|
@@ -527,6 +554,7 @@ export async function startAppDevServer({
|
|
|
527
554
|
|
|
528
555
|
for (const state of appState.values()) {
|
|
529
556
|
if (watch) {
|
|
557
|
+
await prepareArtifactBuild(state.projectDir);
|
|
530
558
|
pollForBundleAndWatch(state);
|
|
531
559
|
|
|
532
560
|
state.buildProcess = spawn('npm', ['run', 'build', '--', '--watch'], {
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
9
|
import { spawn } from 'node:child_process';
|
|
10
|
-
import { cpSync, existsSync, mkdirSync, readFileSync, writeFileSync, readdirSync, rmSync } from 'node:fs';
|
|
10
|
+
import { cpSync, existsSync, mkdirSync, readFileSync, writeFileSync, readdirSync, rmSync, statSync } from 'node:fs';
|
|
11
11
|
import { createRequire } from 'node:module';
|
|
12
12
|
import { dirname, join, resolve } from 'node:path';
|
|
13
13
|
import { fileURLToPath } from 'node:url';
|
|
@@ -21,15 +21,33 @@ const STATE_FILE = join(NOTIS_DIR, 'state.json');
|
|
|
21
21
|
const OUTPUT_DIR = join(NOTIS_DIR, 'output');
|
|
22
22
|
const BUNDLE_DIR = join(OUTPUT_DIR, 'bundle');
|
|
23
23
|
const MANIFEST_FILE = join(OUTPUT_DIR, 'manifest.json');
|
|
24
|
+
const METADATA_DIR = 'metadata';
|
|
25
|
+
const CLI_ROOT = resolve(dirname(fileURLToPath(import.meta.url)), '../..');
|
|
26
|
+
const MONOREPO_APPS_DIR = resolve(CLI_ROOT, '../..', 'apps');
|
|
27
|
+
const DIST_DIR = join(CLI_ROOT, 'dist');
|
|
28
|
+
const SCAFFOLD_CATALOG_FILE = join(DIST_DIR, 'scaffolds.json');
|
|
29
|
+
const SCAFFOLD_SOURCE_DIR = join(DIST_DIR, 'scaffolds');
|
|
30
|
+
export const NOTIS_APP_CATEGORIES = [
|
|
31
|
+
'Productivity',
|
|
32
|
+
'Sales & Marketing',
|
|
33
|
+
'Operations',
|
|
34
|
+
'Product & Engineering',
|
|
35
|
+
'Personal',
|
|
36
|
+
];
|
|
24
37
|
const SOURCE_COPY_EXCLUDES = new Set([
|
|
25
38
|
'node_modules',
|
|
26
39
|
'.notis',
|
|
27
40
|
'.git',
|
|
28
41
|
'dist',
|
|
29
|
-
'package-lock.json',
|
|
30
42
|
'tsconfig.tsbuildinfo',
|
|
31
43
|
'.DS_Store',
|
|
32
44
|
]);
|
|
45
|
+
const SCAFFOLD_COPY_EXCLUDES = new Set([
|
|
46
|
+
...SOURCE_COPY_EXCLUDES,
|
|
47
|
+
'coverage',
|
|
48
|
+
'.next',
|
|
49
|
+
'.turbo',
|
|
50
|
+
]);
|
|
33
51
|
let appConfigImportNonce = 0;
|
|
34
52
|
|
|
35
53
|
// ---------------------------------------------------------------------------
|
|
@@ -214,9 +232,160 @@ export function detectProjectWarnings(projectDir, appConfig = null) {
|
|
|
214
232
|
warnings.push('No database references declared in notis.config.ts.');
|
|
215
233
|
}
|
|
216
234
|
|
|
235
|
+
if (appConfig) {
|
|
236
|
+
try {
|
|
237
|
+
const listing = inspectListingReadiness(projectDir, appConfig);
|
|
238
|
+
warnings.push(...listing.warnings);
|
|
239
|
+
} catch (error) {
|
|
240
|
+
warnings.push(error instanceof Error ? error.message : String(error));
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
|
|
217
244
|
return warnings;
|
|
218
245
|
}
|
|
219
246
|
|
|
247
|
+
function safeKebab(value) {
|
|
248
|
+
return String(value || '')
|
|
249
|
+
.trim()
|
|
250
|
+
.toLowerCase()
|
|
251
|
+
.replace(/[^a-z0-9]+/g, '-')
|
|
252
|
+
.replace(/(^-|-$)+/g, '');
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
function jsStringLiteral(value) {
|
|
256
|
+
return `'${String(value).replace(/\\/g, '\\\\').replace(/'/g, "\\'")}'`;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
function normalizeCategories(categories = []) {
|
|
260
|
+
if (!Array.isArray(categories)) {
|
|
261
|
+
return [];
|
|
262
|
+
}
|
|
263
|
+
const allowed = new Set(NOTIS_APP_CATEGORIES);
|
|
264
|
+
const normalized = [];
|
|
265
|
+
for (const category of categories) {
|
|
266
|
+
if (typeof category !== 'string' || !category.trim()) {
|
|
267
|
+
continue;
|
|
268
|
+
}
|
|
269
|
+
const trimmed = category.trim();
|
|
270
|
+
if (!allowed.has(trimmed)) {
|
|
271
|
+
throw usageError(
|
|
272
|
+
`Invalid Notis app category "${trimmed}". Use one of: ${NOTIS_APP_CATEGORIES.join(', ')}.`,
|
|
273
|
+
);
|
|
274
|
+
}
|
|
275
|
+
if (!normalized.includes(trimmed)) {
|
|
276
|
+
normalized.push(trimmed);
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
return normalized;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
function pngDimensions(buffer) {
|
|
283
|
+
const signature = '89504e470d0a1a0a';
|
|
284
|
+
if (!Buffer.isBuffer(buffer) || buffer.length < 24 || buffer.subarray(0, 8).toString('hex') !== signature) {
|
|
285
|
+
return null;
|
|
286
|
+
}
|
|
287
|
+
return {
|
|
288
|
+
width: buffer.readUInt32BE(16),
|
|
289
|
+
height: buffer.readUInt32BE(20),
|
|
290
|
+
};
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
function imageAssetMeta(projectDir, relPath, { maxBytes, recommendedRatio = 1.6 }) {
|
|
294
|
+
const fullPath = join(projectDir, relPath);
|
|
295
|
+
const content = readFileSync(fullPath);
|
|
296
|
+
const dimensions = pngDimensions(content);
|
|
297
|
+
const ratio = dimensions?.height ? dimensions.width / dimensions.height : null;
|
|
298
|
+
const errors = [];
|
|
299
|
+
const warnings = [];
|
|
300
|
+
|
|
301
|
+
if (!relPath.toLowerCase().endsWith('.png')) {
|
|
302
|
+
errors.push(`${relPath} must be a PNG file.`);
|
|
303
|
+
}
|
|
304
|
+
if (content.length > maxBytes) {
|
|
305
|
+
errors.push(`${relPath} must be ${Math.round(maxBytes / 1024 / 1024)} MB or smaller.`);
|
|
306
|
+
}
|
|
307
|
+
if (ratio !== null && (ratio < 1.3 || ratio > 1.9)) {
|
|
308
|
+
errors.push(`${relPath} should be close to a 16:10 aspect ratio.`);
|
|
309
|
+
} else if (ratio !== null && Math.abs(ratio - recommendedRatio) > 0.08) {
|
|
310
|
+
warnings.push(`${relPath} is not 2000x1250; 16:10 PNG is recommended.`);
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
return {
|
|
314
|
+
path: relPath,
|
|
315
|
+
content_type: 'image/png',
|
|
316
|
+
bytes: content.length,
|
|
317
|
+
width: dimensions?.width ?? null,
|
|
318
|
+
height: dimensions?.height ?? null,
|
|
319
|
+
errors,
|
|
320
|
+
warnings,
|
|
321
|
+
};
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
export function discoverMetadataAssets(projectDir) {
|
|
325
|
+
const metadataDir = join(projectDir, METADATA_DIR);
|
|
326
|
+
const coverPath = join(METADATA_DIR, 'cover.png');
|
|
327
|
+
const cover = existsSync(join(projectDir, coverPath))
|
|
328
|
+
? imageAssetMeta(projectDir, coverPath, { maxBytes: 5 * 1024 * 1024 })
|
|
329
|
+
: null;
|
|
330
|
+
const screenshots = [];
|
|
331
|
+
|
|
332
|
+
if (existsSync(metadataDir)) {
|
|
333
|
+
for (const entry of readdirSync(metadataDir, { withFileTypes: true })) {
|
|
334
|
+
if (!entry.isFile()) continue;
|
|
335
|
+
const match = /^screenshot-(\d+)\.png$/i.exec(entry.name);
|
|
336
|
+
if (!match) continue;
|
|
337
|
+
screenshots.push({
|
|
338
|
+
index: Number.parseInt(match[1], 10),
|
|
339
|
+
...imageAssetMeta(projectDir, join(METADATA_DIR, entry.name), {
|
|
340
|
+
maxBytes: 2 * 1024 * 1024,
|
|
341
|
+
}),
|
|
342
|
+
});
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
screenshots.sort((a, b) => a.index - b.index);
|
|
347
|
+
return {
|
|
348
|
+
cover,
|
|
349
|
+
screenshots: screenshots.slice(0, 6),
|
|
350
|
+
};
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
export function inspectListingReadiness(projectDir, appConfig = null) {
|
|
354
|
+
const config = appConfig || {};
|
|
355
|
+
const warnings = [];
|
|
356
|
+
const errors = [];
|
|
357
|
+
const metadata = discoverMetadataAssets(projectDir);
|
|
358
|
+
const categories = normalizeCategories(config.categories || []);
|
|
359
|
+
|
|
360
|
+
if (!String(config.tagline || '').trim()) {
|
|
361
|
+
warnings.push('Listing tagline missing in notis.config.ts.');
|
|
362
|
+
}
|
|
363
|
+
if (categories.length === 0) {
|
|
364
|
+
warnings.push(`Listing category missing in notis.config.ts. Use one of: ${NOTIS_APP_CATEGORIES.join(', ')}.`);
|
|
365
|
+
}
|
|
366
|
+
if (!metadata.cover) {
|
|
367
|
+
warnings.push('Listing cover missing at metadata/cover.png.');
|
|
368
|
+
} else {
|
|
369
|
+
errors.push(...metadata.cover.errors);
|
|
370
|
+
warnings.push(...metadata.cover.warnings);
|
|
371
|
+
}
|
|
372
|
+
if (metadata.screenshots.length < 3) {
|
|
373
|
+
warnings.push(`Listing screenshots: ${metadata.screenshots.length}/3 minimum in metadata/screenshot-N.png.`);
|
|
374
|
+
}
|
|
375
|
+
for (const screenshot of metadata.screenshots) {
|
|
376
|
+
errors.push(...screenshot.errors);
|
|
377
|
+
warnings.push(...screenshot.warnings);
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
return {
|
|
381
|
+
ready: errors.length === 0 && Boolean(config.tagline) && categories.length > 0 && Boolean(metadata.cover) && metadata.screenshots.length >= 1,
|
|
382
|
+
warnings,
|
|
383
|
+
errors,
|
|
384
|
+
metadata,
|
|
385
|
+
categories,
|
|
386
|
+
};
|
|
387
|
+
}
|
|
388
|
+
|
|
220
389
|
// ---------------------------------------------------------------------------
|
|
221
390
|
// Build
|
|
222
391
|
// ---------------------------------------------------------------------------
|
|
@@ -409,6 +578,21 @@ function generateEntryFile(projectDir, routes) {
|
|
|
409
578
|
return entryPath;
|
|
410
579
|
}
|
|
411
580
|
|
|
581
|
+
export async function prepareArtifactBuild(projectDir) {
|
|
582
|
+
validateProjectBoundary(projectDir);
|
|
583
|
+
const appConfig = await loadAppConfig(projectDir);
|
|
584
|
+
const detectedRoutes = resolveConfiguredRoutes(appConfig, projectDir);
|
|
585
|
+
|
|
586
|
+
generateEntryFile(projectDir, detectedRoutes);
|
|
587
|
+
|
|
588
|
+
const manifest = generateManifest(appConfig, projectDir);
|
|
589
|
+
const manifestPath = join(projectDir, MANIFEST_FILE);
|
|
590
|
+
mkdirSync(dirname(manifestPath), { recursive: true });
|
|
591
|
+
writeFileSync(manifestPath, JSON.stringify(manifest, null, 2) + '\n');
|
|
592
|
+
|
|
593
|
+
return { appConfig, manifest, routes: detectedRoutes };
|
|
594
|
+
}
|
|
595
|
+
|
|
412
596
|
/**
|
|
413
597
|
* Generate the manifest from app config and build output.
|
|
414
598
|
*/
|
|
@@ -431,15 +615,52 @@ export function generateManifest(appConfig, projectDir) {
|
|
|
431
615
|
});
|
|
432
616
|
|
|
433
617
|
const databases = appConfig.databases || [];
|
|
618
|
+
const categories = normalizeCategories(appConfig.categories || []);
|
|
619
|
+
const metadata = discoverMetadataAssets(projectDir);
|
|
620
|
+
const appSlug = safeKebab(appConfig.name);
|
|
621
|
+
const displayTitle = appConfig.title || appConfig.displayName || appConfig.name;
|
|
622
|
+
const listingMedia = {
|
|
623
|
+
cover: metadata.cover
|
|
624
|
+
? {
|
|
625
|
+
path: metadata.cover.path,
|
|
626
|
+
content_type: metadata.cover.content_type,
|
|
627
|
+
width: metadata.cover.width,
|
|
628
|
+
height: metadata.cover.height,
|
|
629
|
+
bytes: metadata.cover.bytes,
|
|
630
|
+
}
|
|
631
|
+
: null,
|
|
632
|
+
screenshots: metadata.screenshots.map((screenshot) => ({
|
|
633
|
+
path: screenshot.path,
|
|
634
|
+
content_type: screenshot.content_type,
|
|
635
|
+
width: screenshot.width,
|
|
636
|
+
height: screenshot.height,
|
|
637
|
+
bytes: screenshot.bytes,
|
|
638
|
+
})),
|
|
639
|
+
};
|
|
434
640
|
|
|
435
641
|
return {
|
|
436
642
|
version: 1,
|
|
437
|
-
spec_version:
|
|
643
|
+
spec_version: 4,
|
|
438
644
|
app: {
|
|
439
|
-
name:
|
|
645
|
+
name: displayTitle,
|
|
646
|
+
slug: appSlug || null,
|
|
440
647
|
description: appConfig.description || null,
|
|
441
648
|
icon: appConfig.icon || null,
|
|
649
|
+
title: displayTitle,
|
|
650
|
+
tagline: appConfig.tagline || null,
|
|
651
|
+
categories,
|
|
652
|
+
author: appConfig.author || null,
|
|
653
|
+
version_notes: appConfig.versionNotes || appConfig.version_notes || null,
|
|
654
|
+
},
|
|
655
|
+
listing: {
|
|
656
|
+
title: displayTitle,
|
|
657
|
+
tagline: appConfig.tagline || null,
|
|
658
|
+
categories,
|
|
659
|
+
author: appConfig.author || null,
|
|
660
|
+
version_notes: appConfig.versionNotes || appConfig.version_notes || null,
|
|
661
|
+
media: listingMedia,
|
|
442
662
|
},
|
|
663
|
+
metadata: listingMedia,
|
|
443
664
|
routes,
|
|
444
665
|
bundle: {
|
|
445
666
|
js: 'bundle/app.js',
|
|
@@ -454,14 +675,7 @@ export function generateManifest(appConfig, projectDir) {
|
|
|
454
675
|
* Build the app bundle: generate entry file, run `vite build`, package into .notis/output/.
|
|
455
676
|
*/
|
|
456
677
|
export async function buildArtifact(projectDir) {
|
|
457
|
-
|
|
458
|
-
const appConfig = await loadAppConfig(projectDir);
|
|
459
|
-
|
|
460
|
-
// Auto-detect or use configured routes
|
|
461
|
-
const detectedRoutes = resolveConfiguredRoutes(appConfig, projectDir);
|
|
462
|
-
|
|
463
|
-
// Generate the entry file that re-exports all route page components
|
|
464
|
-
generateEntryFile(projectDir, detectedRoutes);
|
|
678
|
+
await prepareArtifactBuild(projectDir);
|
|
465
679
|
|
|
466
680
|
// Run Vite build
|
|
467
681
|
await runProjectScript({
|
|
@@ -477,14 +691,11 @@ export async function buildArtifact(projectDir) {
|
|
|
477
691
|
);
|
|
478
692
|
}
|
|
479
693
|
normalizeBundleStylesheets(projectDir);
|
|
694
|
+
copyMetadataAssets(projectDir);
|
|
480
695
|
|
|
481
696
|
validateArtifactBoundary(readArtifactFiles(projectDir));
|
|
482
697
|
|
|
483
|
-
|
|
484
|
-
const manifest = generateManifest(appConfig, projectDir);
|
|
485
|
-
const manifestPath = join(projectDir, MANIFEST_FILE);
|
|
486
|
-
mkdirSync(dirname(manifestPath), { recursive: true });
|
|
487
|
-
writeFileSync(manifestPath, JSON.stringify(manifest, null, 2) + '\n');
|
|
698
|
+
const manifest = readManifest(projectDir);
|
|
488
699
|
|
|
489
700
|
return { manifest, outputDir: join(projectDir, OUTPUT_DIR) };
|
|
490
701
|
}
|
|
@@ -500,6 +711,22 @@ export function readManifest(projectDir) {
|
|
|
500
711
|
return JSON.parse(readFileSync(manifestPath, 'utf-8'));
|
|
501
712
|
}
|
|
502
713
|
|
|
714
|
+
function copyMetadataAssets(projectDir) {
|
|
715
|
+
const metadataDir = join(projectDir, METADATA_DIR);
|
|
716
|
+
if (!existsSync(metadataDir)) {
|
|
717
|
+
return;
|
|
718
|
+
}
|
|
719
|
+
const outputMetadataDir = join(projectDir, OUTPUT_DIR, METADATA_DIR);
|
|
720
|
+
mkdirSync(outputMetadataDir, { recursive: true });
|
|
721
|
+
for (const entry of readdirSync(metadataDir, { withFileTypes: true })) {
|
|
722
|
+
if (!entry.isFile()) continue;
|
|
723
|
+
if (entry.name !== 'cover.png' && !/^screenshot-\d+\.png$/i.test(entry.name)) {
|
|
724
|
+
continue;
|
|
725
|
+
}
|
|
726
|
+
cpSync(join(metadataDir, entry.name), join(outputMetadataDir, entry.name));
|
|
727
|
+
}
|
|
728
|
+
}
|
|
729
|
+
|
|
503
730
|
// ---------------------------------------------------------------------------
|
|
504
731
|
// Linking
|
|
505
732
|
// ---------------------------------------------------------------------------
|
|
@@ -530,18 +757,30 @@ export function requireLinkedAppId(projectDir, explicitAppId) {
|
|
|
530
757
|
/**
|
|
531
758
|
* Scaffold a new Notis app project from the SDK template.
|
|
532
759
|
*/
|
|
533
|
-
export function
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
760
|
+
export function loadScaffoldCatalog() {
|
|
761
|
+
if (existsSync(SCAFFOLD_CATALOG_FILE)) {
|
|
762
|
+
const parsed = JSON.parse(readFileSync(SCAFFOLD_CATALOG_FILE, 'utf-8'));
|
|
763
|
+
const scaffolds = Array.isArray(parsed?.scaffolds) ? parsed.scaffolds : parsed;
|
|
764
|
+
return Array.isArray(scaffolds)
|
|
765
|
+
? scaffolds.filter((entry) => entry && typeof entry.slug === 'string')
|
|
766
|
+
: [];
|
|
767
|
+
}
|
|
768
|
+
return loadMonorepoScaffoldCatalog();
|
|
769
|
+
}
|
|
770
|
+
|
|
771
|
+
export function scaffoldProject({ projectDir, appName, fromSlug = null }) {
|
|
772
|
+
const templateDir = fromSlug ? resolveScaffoldSourceDir(fromSlug) : join(CLI_ROOT, 'template');
|
|
538
773
|
|
|
539
774
|
if (!existsSync(templateDir)) {
|
|
775
|
+
if (fromSlug) {
|
|
776
|
+
const known = loadScaffoldCatalog().map((entry) => entry.slug).join(', ') || 'none';
|
|
777
|
+
throw usageError(`Unknown scaffold "${fromSlug}". Available scaffolds: ${known}.`);
|
|
778
|
+
}
|
|
540
779
|
throw usageError(`SDK template not found at ${templateDir}. Ensure @notis_ai/cli is installed correctly.`);
|
|
541
780
|
}
|
|
542
781
|
|
|
543
782
|
mkdirSync(projectDir, { recursive: true });
|
|
544
|
-
|
|
783
|
+
copyScaffoldSource(templateDir, projectDir);
|
|
545
784
|
|
|
546
785
|
// Update package.json with the app name
|
|
547
786
|
const pkgPath = join(projectDir, 'package.json');
|
|
@@ -555,13 +794,115 @@ export function scaffoldProject({ projectDir, appName }) {
|
|
|
555
794
|
const configPath = join(projectDir, 'notis.config.ts');
|
|
556
795
|
if (existsSync(configPath)) {
|
|
557
796
|
let config = readFileSync(configPath, 'utf-8');
|
|
558
|
-
|
|
797
|
+
const displayName = jsStringLiteral(appName);
|
|
798
|
+
const slugName = jsStringLiteral(safeKebab(appName) || 'my-notis-app');
|
|
799
|
+
if (fromSlug) {
|
|
800
|
+
if (/name\s*:/.test(config)) {
|
|
801
|
+
config = config.replace(/name\s*:\s*(['"`])[\s\S]*?\1/, `name: ${slugName}`);
|
|
802
|
+
}
|
|
803
|
+
if (/title\s*:/.test(config)) {
|
|
804
|
+
config = config.replace(/title\s*:\s*(['"`])[\s\S]*?\1/, `title: ${displayName}`);
|
|
805
|
+
} else {
|
|
806
|
+
config = config.replace(/name\s*:\s*(['"`])[\s\S]*?\1/, `name: ${slugName},\n title: ${displayName}`);
|
|
807
|
+
}
|
|
808
|
+
} else {
|
|
809
|
+
if (/name\s*:/.test(config)) {
|
|
810
|
+
config = config.replace(/name\s*:\s*(['"`])[\s\S]*?\1/, `name: ${slugName}`);
|
|
811
|
+
}
|
|
812
|
+
if (/title\s*:/.test(config)) {
|
|
813
|
+
config = config.replace(/title\s*:\s*(['"`])[\s\S]*?\1/, `title: ${displayName}`);
|
|
814
|
+
} else {
|
|
815
|
+
config = config.replace(/'My Notis App'/, displayName);
|
|
816
|
+
}
|
|
817
|
+
}
|
|
559
818
|
writeFileSync(configPath, config);
|
|
560
819
|
}
|
|
561
820
|
|
|
562
821
|
return { projectDir };
|
|
563
822
|
}
|
|
564
823
|
|
|
824
|
+
function resolveScaffoldSourceDir(fromSlug) {
|
|
825
|
+
const bundledDir = join(SCAFFOLD_SOURCE_DIR, fromSlug);
|
|
826
|
+
if (existsSync(bundledDir)) {
|
|
827
|
+
return bundledDir;
|
|
828
|
+
}
|
|
829
|
+
const monorepoDir = join(MONOREPO_APPS_DIR, fromSlug);
|
|
830
|
+
if (existsSync(join(monorepoDir, 'notis.config.ts'))) {
|
|
831
|
+
return monorepoDir;
|
|
832
|
+
}
|
|
833
|
+
return bundledDir;
|
|
834
|
+
}
|
|
835
|
+
|
|
836
|
+
function loadMonorepoScaffoldCatalog() {
|
|
837
|
+
if (!existsSync(MONOREPO_APPS_DIR)) {
|
|
838
|
+
return [];
|
|
839
|
+
}
|
|
840
|
+
const scaffolds = [];
|
|
841
|
+
for (const entry of readdirSync(MONOREPO_APPS_DIR, { withFileTypes: true })) {
|
|
842
|
+
if (!entry.isDirectory() || entry.name.startsWith('.') || entry.name.startsWith('_')) {
|
|
843
|
+
continue;
|
|
844
|
+
}
|
|
845
|
+
const configPath = join(MONOREPO_APPS_DIR, entry.name, 'notis.config.ts');
|
|
846
|
+
if (!existsSync(configPath)) {
|
|
847
|
+
continue;
|
|
848
|
+
}
|
|
849
|
+
const configSource = readFileSync(configPath, 'utf-8');
|
|
850
|
+
const description = readTsStringProperty(configSource, 'description') || '';
|
|
851
|
+
scaffolds.push({
|
|
852
|
+
slug: entry.name,
|
|
853
|
+
name: readTsStringProperty(configSource, 'title') || readTsStringProperty(configSource, 'name') || entry.name,
|
|
854
|
+
description,
|
|
855
|
+
icon: readTsStringProperty(configSource, 'icon') || 'phosphor:squares-four',
|
|
856
|
+
categories: readTsStringArrayProperty(configSource, 'categories'),
|
|
857
|
+
tagline: readTsStringProperty(configSource, 'tagline') || description,
|
|
858
|
+
});
|
|
859
|
+
}
|
|
860
|
+
return scaffolds.sort((a, b) => a.slug.localeCompare(b.slug));
|
|
861
|
+
}
|
|
862
|
+
|
|
863
|
+
function readTsStringProperty(source, propertyName) {
|
|
864
|
+
const match = source.match(new RegExp(`\\b${propertyName}\\s*:\\s*(['"\`])([\\s\\S]*?)\\1`));
|
|
865
|
+
return match ? match[2] : null;
|
|
866
|
+
}
|
|
867
|
+
|
|
868
|
+
function readTsStringArrayProperty(source, propertyName) {
|
|
869
|
+
const match = source.match(new RegExp(`\\b${propertyName}\\s*:\\s*\\[([\\s\\S]*?)\\]`));
|
|
870
|
+
if (!match) {
|
|
871
|
+
return [];
|
|
872
|
+
}
|
|
873
|
+
return Array.from(match[1].matchAll(/(['"`])([\s\S]*?)\1/g), (entry) => entry[2].trim()).filter(Boolean);
|
|
874
|
+
}
|
|
875
|
+
|
|
876
|
+
function copyScaffoldSource(sourceDir, targetDir) {
|
|
877
|
+
function shouldCopy(path) {
|
|
878
|
+
const name = path.split(/[\\/]/).pop();
|
|
879
|
+
if (!name) return true;
|
|
880
|
+
return !SCAFFOLD_COPY_EXCLUDES.has(name)
|
|
881
|
+
&& !name.startsWith('.env')
|
|
882
|
+
&& !/\.(test|spec)\.[cm]?[jt]sx?$/i.test(name);
|
|
883
|
+
}
|
|
884
|
+
|
|
885
|
+
function walk(src, dest) {
|
|
886
|
+
if (!shouldCopy(src)) {
|
|
887
|
+
return;
|
|
888
|
+
}
|
|
889
|
+
const stat = statSync(src);
|
|
890
|
+
if (stat.isDirectory()) {
|
|
891
|
+
mkdirSync(dest, { recursive: true });
|
|
892
|
+
for (const entry of readdirSync(src)) {
|
|
893
|
+
walk(join(src, entry), join(dest, entry));
|
|
894
|
+
}
|
|
895
|
+
return;
|
|
896
|
+
}
|
|
897
|
+
if (stat.isFile()) {
|
|
898
|
+
mkdirSync(dirname(dest), { recursive: true });
|
|
899
|
+
cpSync(src, dest);
|
|
900
|
+
}
|
|
901
|
+
}
|
|
902
|
+
|
|
903
|
+
walk(sourceDir, targetDir);
|
|
904
|
+
}
|
|
905
|
+
|
|
565
906
|
// ---------------------------------------------------------------------------
|
|
566
907
|
// Deploy helpers
|
|
567
908
|
// ---------------------------------------------------------------------------
|
|
@@ -700,8 +1041,14 @@ export async function pullAppSource({
|
|
|
700
1041
|
const versionMatch = /-v(\d+)\.tar\.gz/i.exec(contentDisposition);
|
|
701
1042
|
const pulledVersion = versionMatch ? Number.parseInt(versionMatch[1], 10) : null;
|
|
702
1043
|
extractTarGz(Buffer.from(await response.arrayBuffer()), targetDir);
|
|
1044
|
+
const linkedVersion = pulledVersion || (
|
|
1045
|
+
String(version || 'latest') === 'latest'
|
|
1046
|
+
? undefined
|
|
1047
|
+
: Number.parseInt(String(version), 10)
|
|
1048
|
+
);
|
|
703
1049
|
writeLinkedState(targetDir, {
|
|
704
1050
|
app_id: appId,
|
|
1051
|
+
...(Number.isFinite(linkedVersion) ? { version: linkedVersion } : {}),
|
|
705
1052
|
linked_at: new Date().toISOString(),
|
|
706
1053
|
});
|
|
707
1054
|
|
|
@@ -898,6 +1245,9 @@ const CONTENT_TYPE_MAP = {
|
|
|
898
1245
|
'.css': 'text/css',
|
|
899
1246
|
'.json': 'application/json',
|
|
900
1247
|
'.html': 'text/html',
|
|
1248
|
+
'.png': 'image/png',
|
|
1249
|
+
'.jpg': 'image/jpeg',
|
|
1250
|
+
'.jpeg': 'image/jpeg',
|
|
901
1251
|
};
|
|
902
1252
|
|
|
903
1253
|
/**
|
|
@@ -952,6 +1302,8 @@ export async function directDeploy(projectDir, appId) {
|
|
|
952
1302
|
version: newVersion,
|
|
953
1303
|
storage_bucket: bucket,
|
|
954
1304
|
storage_prefix: storagePrefix,
|
|
1305
|
+
source_storage_bucket: 'app-source',
|
|
1306
|
+
source_storage_prefix: storagePrefix,
|
|
955
1307
|
};
|
|
956
1308
|
await updateAppVersion(supabaseUrl, supabaseKey, appId, newVersion, deployManifest);
|
|
957
1309
|
|
package/src/runtime/profiles.js
CHANGED
|
@@ -205,8 +205,8 @@ export function resolveRuntimeProfile(globalOptions = {}, { requireAuth = true }
|
|
|
205
205
|
exitCode: EXIT_CODES.auth,
|
|
206
206
|
hints: [
|
|
207
207
|
{
|
|
208
|
-
command: '
|
|
209
|
-
reason: '
|
|
208
|
+
command: 'Open the Notis desktop app and sign in',
|
|
209
|
+
reason: 'Set NOTIS_JWT or sign in through the Notis desktop app',
|
|
210
210
|
},
|
|
211
211
|
],
|
|
212
212
|
});
|