@notis_ai/cli 0.2.5 → 0.2.7
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 +33 -9
- package/dist/scaffolds/notis-affiliate-prospects/CHANGELOG.md +7 -0
- package/dist/scaffolds/notis-affiliate-prospects/README.md +20 -0
- package/dist/scaffolds/notis-affiliate-prospects/app/globals.css +42 -0
- package/dist/scaffolds/notis-affiliate-prospects/app/layout.tsx +7 -0
- package/dist/scaffolds/notis-affiliate-prospects/app/page.tsx +248 -0
- package/dist/scaffolds/notis-affiliate-prospects/app/prospects/page.tsx +134 -0
- package/dist/scaffolds/notis-affiliate-prospects/app/segments/page.tsx +108 -0
- package/dist/scaffolds/notis-affiliate-prospects/components/ui/card.tsx +35 -0
- package/dist/scaffolds/notis-affiliate-prospects/components.json +20 -0
- package/dist/scaffolds/notis-affiliate-prospects/index.html +12 -0
- package/dist/scaffolds/notis-affiliate-prospects/lib/affiliate-data.ts +218 -0
- package/dist/scaffolds/notis-affiliate-prospects/lib/demo-prospects.ts +52 -0
- package/dist/scaffolds/notis-affiliate-prospects/lib/notis-tools.ts +100 -0
- package/dist/scaffolds/notis-affiliate-prospects/lib/utils.ts +25 -0
- package/dist/scaffolds/notis-affiliate-prospects/metadata/screenshot-1.png +0 -0
- package/dist/scaffolds/notis-affiliate-prospects/metadata/screenshot-2.png +0 -0
- package/dist/scaffolds/notis-affiliate-prospects/metadata/screenshot-3.png +0 -0
- package/dist/scaffolds/notis-affiliate-prospects/metadata/screenshot-fixtures.json +187 -0
- package/dist/scaffolds/notis-affiliate-prospects/notis.config.ts +65 -0
- package/dist/scaffolds/notis-affiliate-prospects/package.json +32 -0
- package/dist/scaffolds/notis-affiliate-prospects/packages/sdk/package.json +32 -0
- package/dist/scaffolds/notis-affiliate-prospects/packages/sdk/src/components/MultiSelectActionBar.tsx +273 -0
- package/dist/scaffolds/notis-affiliate-prospects/packages/sdk/src/components/MultiSelectCheckbox.tsx +91 -0
- package/dist/scaffolds/notis-affiliate-prospects/packages/sdk/src/components/MultiSelectDragOverlay.tsx +39 -0
- package/dist/scaffolds/notis-affiliate-prospects/packages/sdk/src/config.ts +90 -0
- package/dist/scaffolds/notis-affiliate-prospects/packages/sdk/src/hooks/useBackend.ts +41 -0
- package/dist/scaffolds/notis-affiliate-prospects/packages/sdk/src/hooks/useMultiSelect.ts +503 -0
- package/dist/scaffolds/notis-affiliate-prospects/packages/sdk/src/hooks/useNotis.ts +34 -0
- package/dist/scaffolds/notis-affiliate-prospects/packages/sdk/src/hooks/useNotisNavigation.ts +49 -0
- package/dist/scaffolds/notis-affiliate-prospects/packages/sdk/src/hooks/useTool.ts +64 -0
- package/dist/scaffolds/notis-affiliate-prospects/packages/sdk/src/hooks/useTools.ts +56 -0
- package/dist/scaffolds/notis-affiliate-prospects/packages/sdk/src/hooks/useTopBarSearch.ts +73 -0
- package/dist/scaffolds/notis-affiliate-prospects/packages/sdk/src/hooks/useUpsertDocument.ts +50 -0
- package/dist/scaffolds/notis-affiliate-prospects/packages/sdk/src/index.ts +54 -0
- package/dist/scaffolds/notis-affiliate-prospects/packages/sdk/src/provider.tsx +43 -0
- package/dist/scaffolds/notis-affiliate-prospects/packages/sdk/src/runtime.ts +161 -0
- package/dist/scaffolds/notis-affiliate-prospects/packages/sdk/src/styles.css +38 -0
- package/dist/scaffolds/notis-affiliate-prospects/packages/sdk/src/ui.ts +15 -0
- package/dist/scaffolds/notis-affiliate-prospects/packages/sdk/src/vite.ts +56 -0
- package/dist/scaffolds/notis-affiliate-prospects/packages/sdk/tsconfig.json +15 -0
- package/dist/scaffolds/notis-affiliate-prospects/postcss.config.mjs +6 -0
- package/dist/scaffolds/notis-affiliate-prospects/src/dev-main.tsx +72 -0
- package/dist/scaffolds/notis-affiliate-prospects/src/mock-runtime.ts +242 -0
- package/dist/scaffolds/notis-affiliate-prospects/tailwind.config.ts +51 -0
- package/dist/scaffolds/notis-affiliate-prospects/tsconfig.json +23 -0
- package/dist/scaffolds/notis-affiliate-prospects/vite.config.ts +11 -0
- package/dist/scaffolds/notis-database/CHANGELOG.md +5 -0
- package/dist/scaffolds/notis-database/notis.config.ts +0 -1
- package/dist/scaffolds/notis-database/src/mock-runtime.ts +1 -0
- package/dist/scaffolds/notis-journal/CHANGELOG.md +25 -0
- package/dist/scaffolds/notis-journal/app/globals.css +37 -0
- package/dist/scaffolds/notis-journal/app/insights/page.tsx +513 -0
- package/dist/scaffolds/notis-journal/app/journal-core.tsx +362 -0
- package/dist/scaffolds/notis-journal/app/journal-ui.tsx +337 -0
- package/dist/scaffolds/notis-journal/app/layout.tsx +6 -0
- package/dist/scaffolds/notis-journal/app/page.tsx +485 -0
- package/dist/scaffolds/notis-journal/components/ui/badge.tsx +28 -0
- package/dist/scaffolds/notis-journal/components/ui/button.tsx +53 -0
- package/dist/scaffolds/notis-journal/components/ui/card.tsx +56 -0
- package/dist/scaffolds/notis-journal/components.json +20 -0
- package/dist/scaffolds/notis-journal/index.html +12 -0
- package/dist/scaffolds/notis-journal/lib/utils.ts +6 -0
- package/dist/scaffolds/notis-journal/metadata/screenshot-1.png +0 -0
- package/dist/scaffolds/notis-journal/metadata/screenshot-2.png +0 -0
- package/dist/scaffolds/notis-journal/metadata/screenshot-3.png +0 -0
- package/dist/scaffolds/notis-journal/metadata/screenshot-4.png +0 -0
- package/dist/scaffolds/notis-journal/metadata/screenshot-5.png +0 -0
- package/dist/scaffolds/notis-journal/metadata/screenshot-6.png +0 -0
- package/dist/scaffolds/notis-journal/metadata/screenshot-fixtures.json +132 -0
- package/dist/scaffolds/notis-journal/notis.config.ts +93 -0
- package/dist/scaffolds/notis-journal/package.json +34 -0
- package/dist/scaffolds/notis-journal/packages/sdk/package.json +36 -0
- package/dist/scaffolds/notis-journal/packages/sdk/src/components/DocumentEditor.tsx +93 -0
- package/dist/scaffolds/notis-journal/packages/sdk/src/components/Markdown.tsx +60 -0
- package/dist/scaffolds/notis-journal/packages/sdk/src/components/MultiSelectActionBar.tsx +278 -0
- package/dist/scaffolds/notis-journal/packages/sdk/src/components/MultiSelectCheckbox.tsx +91 -0
- package/dist/scaffolds/notis-journal/packages/sdk/src/components/MultiSelectDragOverlay.tsx +39 -0
- package/dist/scaffolds/notis-journal/packages/sdk/src/config.ts +145 -0
- package/dist/scaffolds/notis-journal/packages/sdk/src/documents.ts +229 -0
- package/dist/scaffolds/notis-journal/packages/sdk/src/hooks/useBackend.ts +41 -0
- package/dist/scaffolds/notis-journal/packages/sdk/src/hooks/useDatabaseSchema.ts +85 -0
- package/dist/scaffolds/notis-journal/packages/sdk/src/hooks/useDocument.ts +78 -0
- package/dist/scaffolds/notis-journal/packages/sdk/src/hooks/useDocuments.ts +121 -0
- package/dist/scaffolds/notis-journal/packages/sdk/src/hooks/useMultiSelect.ts +539 -0
- package/dist/scaffolds/notis-journal/packages/sdk/src/hooks/useNotis.ts +34 -0
- package/dist/scaffolds/notis-journal/packages/sdk/src/hooks/useNotisNavigation.ts +49 -0
- package/dist/scaffolds/notis-journal/packages/sdk/src/hooks/useTool.ts +64 -0
- package/dist/scaffolds/notis-journal/packages/sdk/src/hooks/useTools.ts +56 -0
- package/dist/scaffolds/notis-journal/packages/sdk/src/hooks/useTopBarSearch.ts +73 -0
- package/dist/scaffolds/notis-journal/packages/sdk/src/hooks/useUpsertDocument.ts +95 -0
- package/dist/scaffolds/notis-journal/packages/sdk/src/index.ts +83 -0
- package/dist/scaffolds/notis-journal/packages/sdk/src/provider.tsx +43 -0
- package/dist/scaffolds/notis-journal/packages/sdk/src/runtime.ts +220 -0
- package/dist/scaffolds/notis-journal/packages/sdk/src/styles.css +186 -0
- package/dist/scaffolds/notis-journal/packages/sdk/src/ui.ts +15 -0
- package/dist/scaffolds/notis-journal/packages/sdk/src/vite.ts +56 -0
- package/dist/scaffolds/notis-journal/packages/sdk/tsconfig.json +15 -0
- package/dist/scaffolds/notis-journal/postcss.config.mjs +8 -0
- package/dist/scaffolds/notis-journal/skills/journal-onboarding/SKILL.md +120 -0
- package/dist/scaffolds/notis-journal/src/dev-main.tsx +58 -0
- package/dist/scaffolds/notis-journal/src/mock-runtime.ts +197 -0
- package/dist/scaffolds/notis-journal/tailwind.config.ts +58 -0
- package/dist/scaffolds/notis-journal/tsconfig.json +23 -0
- package/dist/scaffolds/notis-journal/vite.config.ts +10 -0
- package/dist/scaffolds/notis-notes/CHANGELOG.md +5 -0
- package/dist/scaffolds/notis-notes/app/page.tsx +17 -373
- package/dist/scaffolds/notis-notes/notis.config.ts +0 -1
- package/dist/scaffolds/notis-random/CHANGELOG.md +5 -0
- package/dist/scaffolds/notis-random/notis.config.ts +0 -1
- package/dist/scaffolds/notis-random/src/mock-runtime.ts +1 -0
- package/dist/scaffolds.json +22 -0
- package/package.json +3 -3
- package/skills/notis-apps/SKILL.md +43 -5
- package/skills/notis-apps/cli.md +22 -6
- package/skills/notis-cli/SKILL.md +20 -2
- package/skills/notis-query/cli.md +1 -1
- package/src/cli.js +8 -1
- package/src/command-specs/apps.js +307 -36
- package/src/command-specs/diagnostics.js +674 -0
- package/src/command-specs/helpers.js +4 -1
- package/src/command-specs/index.js +7 -1
- package/src/command-specs/meta.js +8 -2
- package/src/command-specs/smoke.js +386 -0
- package/src/command-specs/tools.js +237 -44
- package/src/runtime/agent-browser.js +204 -21
- package/src/runtime/app-changelog.js +79 -0
- package/src/runtime/app-dev-server.js +21 -4
- package/src/runtime/app-dev-sessions.js +99 -1
- package/src/runtime/app-platform.js +197 -18
- package/src/runtime/assets/store-screenshot-dark.png +0 -0
- package/src/runtime/desktop-auth.js +93 -0
- package/src/runtime/output.js +12 -1
- package/src/runtime/profiles.js +72 -16
- package/src/runtime/store-screenshot.js +138 -0
- package/src/runtime/transport.js +24 -82
- package/template/.harness/index.html.tmpl +128 -6
- package/template/CHANGELOG.md +5 -0
- package/template/app/page.tsx +11 -41
- package/template/notis.config.ts +0 -1
- package/template/package-lock.json +4137 -0
- package/template/package.json +1 -0
- package/template/packages/sdk/package.json +4 -0
- package/template/packages/sdk/src/components/DocumentEditor.tsx +93 -0
- package/template/packages/sdk/src/components/Markdown.tsx +60 -0
- package/template/packages/sdk/src/components/MultiSelectActionBar.tsx +7 -2
- package/template/packages/sdk/src/config.ts +74 -0
- package/template/packages/sdk/src/documents.ts +229 -0
- package/template/packages/sdk/src/hooks/useDatabaseSchema.ts +85 -0
- package/template/packages/sdk/src/hooks/useDocument.ts +78 -0
- package/template/packages/sdk/src/hooks/useDocuments.ts +121 -0
- package/template/packages/sdk/src/hooks/useMultiSelect.ts +38 -2
- package/template/packages/sdk/src/hooks/useUpsertDocument.ts +54 -9
- package/template/packages/sdk/src/index.ts +30 -1
- package/template/packages/sdk/src/runtime.ts +76 -17
- package/template/packages/sdk/src/styles.css +148 -0
- /package/{template → dist/scaffolds/notis-affiliate-prospects}/packages/sdk/src/hooks/useDatabase.ts +0 -0
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
|
|
4
|
+
export const APP_CHANGELOG_FILE = 'CHANGELOG.md';
|
|
5
|
+
export const CHANGELOG_MERGE_DATE = '{PR_MERGE_DATE}';
|
|
6
|
+
|
|
7
|
+
const ENTRY_HEADING = /^##\s+\[([^\]]+)]\s+-\s+(\{PR_MERGE_DATE\}|\d{4}-\d{2}-\d{2})\s*$/;
|
|
8
|
+
|
|
9
|
+
function validCalendarDate(value) {
|
|
10
|
+
if (!/^\d{4}-\d{2}-\d{2}$/.test(value)) return false;
|
|
11
|
+
const date = new Date(`${value}T00:00:00Z`);
|
|
12
|
+
return !Number.isNaN(date.getTime()) && date.toISOString().slice(0, 10) === value;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function parseAppChangelog(markdown, { sourcePath = APP_CHANGELOG_FILE } = {}) {
|
|
16
|
+
const lines = String(markdown || '').replace(/\r\n?/g, '\n').split('\n');
|
|
17
|
+
const entries = [];
|
|
18
|
+
const errors = [];
|
|
19
|
+
let current = null;
|
|
20
|
+
|
|
21
|
+
function finishCurrent() {
|
|
22
|
+
if (!current) return;
|
|
23
|
+
entries.push({
|
|
24
|
+
title: current.title,
|
|
25
|
+
date: current.date,
|
|
26
|
+
body: current.body.join('\n').trim(),
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
for (const [index, line] of lines.entries()) {
|
|
31
|
+
// Only a level-2 heading (`## ...`, not `###`/`####`) can start a release
|
|
32
|
+
// entry; deeper sub-section headings are legitimate release-body content.
|
|
33
|
+
if (!/^##(?!#)/.test(line)) {
|
|
34
|
+
if (current) current.body.push(line);
|
|
35
|
+
continue;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const match = line.match(ENTRY_HEADING);
|
|
39
|
+
if (!match) {
|
|
40
|
+
errors.push(
|
|
41
|
+
`${sourcePath}:${index + 1} must use \`## [Release title] - YYYY-MM-DD\` or \`## [Release title] - {PR_MERGE_DATE}\`.`,
|
|
42
|
+
);
|
|
43
|
+
continue;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
finishCurrent();
|
|
47
|
+
const title = match[1].trim();
|
|
48
|
+
const date = match[2];
|
|
49
|
+
if (!title) {
|
|
50
|
+
errors.push(`${sourcePath}:${index + 1} must include a release title inside square brackets.`);
|
|
51
|
+
}
|
|
52
|
+
if (date !== CHANGELOG_MERGE_DATE && !validCalendarDate(date)) {
|
|
53
|
+
errors.push(`${sourcePath}:${index + 1} contains an invalid calendar date.`);
|
|
54
|
+
}
|
|
55
|
+
current = { title, date, body: [] };
|
|
56
|
+
}
|
|
57
|
+
finishCurrent();
|
|
58
|
+
|
|
59
|
+
if (entries.length === 0 && errors.length === 0) {
|
|
60
|
+
errors.push(
|
|
61
|
+
`${sourcePath} must include at least one \`## [Release title] - YYYY-MM-DD\` entry.`,
|
|
62
|
+
);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
return { source_path: sourcePath, entries, errors, exists: true };
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export function readAppChangelog(projectDir) {
|
|
69
|
+
const path = join(projectDir, APP_CHANGELOG_FILE);
|
|
70
|
+
if (!existsSync(path)) {
|
|
71
|
+
return {
|
|
72
|
+
source_path: APP_CHANGELOG_FILE,
|
|
73
|
+
entries: [],
|
|
74
|
+
errors: [`${APP_CHANGELOG_FILE} is required for Store publication.`],
|
|
75
|
+
exists: false,
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
return parseAppChangelog(readFileSync(path, 'utf-8'));
|
|
79
|
+
}
|
|
@@ -216,7 +216,7 @@ function findHarnessRoute(manifest, routeSlug) {
|
|
|
216
216
|
return routes.find((route) => route?.slug === routeSlug) || null;
|
|
217
217
|
}
|
|
218
218
|
|
|
219
|
-
function buildHarnessDescriptor({ state, manifest, appConfig, route }) {
|
|
219
|
+
function buildHarnessDescriptor({ state, manifest, appConfig, route, scenario = null }) {
|
|
220
220
|
const databases = normalizeDatabaseDescriptors(
|
|
221
221
|
Array.isArray(appConfig?.databases) && appConfig.databases.length
|
|
222
222
|
? appConfig.databases
|
|
@@ -246,14 +246,28 @@ function buildHarnessDescriptor({ state, manifest, appConfig, route }) {
|
|
|
246
246
|
collection: route.collection || null,
|
|
247
247
|
},
|
|
248
248
|
databases,
|
|
249
|
-
context: { collectionItem: null },
|
|
249
|
+
context: { collectionItem: null, screenshotScenario: scenario },
|
|
250
250
|
tools,
|
|
251
251
|
};
|
|
252
252
|
}
|
|
253
253
|
|
|
254
|
-
function
|
|
254
|
+
function harnessFixtures(projectDir, scenario) {
|
|
255
|
+
const fixtureConfig = readJsonFile(join(projectDir, 'metadata', 'screenshot-fixtures.json')) || {};
|
|
256
|
+
const scenarios = fixtureConfig.scenarios && typeof fixtureConfig.scenarios === 'object'
|
|
257
|
+
? fixtureConfig.scenarios
|
|
258
|
+
: {};
|
|
259
|
+
return {
|
|
260
|
+
tools: fixtureConfig.tools && typeof fixtureConfig.tools === 'object' ? fixtureConfig.tools : {},
|
|
261
|
+
requests: fixtureConfig.requests && typeof fixtureConfig.requests === 'object' ? fixtureConfig.requests : {},
|
|
262
|
+
scenario: scenario && scenarios[scenario] && typeof scenarios[scenario] === 'object'
|
|
263
|
+
? scenarios[scenario]
|
|
264
|
+
: null,
|
|
265
|
+
};
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
function renderHarnessHtml({ state, manifest, appConfig, route, harnessOptions, scenario = null }) {
|
|
255
269
|
const template = readFileSync(HARNESS_TEMPLATE_PATH, 'utf-8');
|
|
256
|
-
const descriptor = buildHarnessDescriptor({ state, manifest, appConfig, route });
|
|
270
|
+
const descriptor = buildHarnessDescriptor({ state, manifest, appConfig, route, scenario });
|
|
257
271
|
const routeExport = route.export_name || route.exportName || exportNameFromPath(route.path || '/');
|
|
258
272
|
const replacements = {
|
|
259
273
|
'{{REACT_VERSION}}': resolveHarnessReactVersion(state.projectDir),
|
|
@@ -262,6 +276,7 @@ function renderHarnessHtml({ state, manifest, appConfig, route, harnessOptions }
|
|
|
262
276
|
'{{MODE}}': scriptJson(harnessOptions.mode || 'stub'),
|
|
263
277
|
'{{API_BASE}}': scriptJson(harnessOptions.apiBase || null),
|
|
264
278
|
'{{JWT}}': scriptJson(harnessOptions.jwt || null),
|
|
279
|
+
'{{FIXTURES}}': scriptJson(harnessFixtures(state.projectDir, scenario)),
|
|
265
280
|
};
|
|
266
281
|
let html = template;
|
|
267
282
|
for (const [token, value] of Object.entries(replacements)) {
|
|
@@ -370,6 +385,7 @@ export async function startAppDevServer({
|
|
|
370
385
|
}
|
|
371
386
|
|
|
372
387
|
const routeSlug = url.searchParams.get('route') || '';
|
|
388
|
+
const scenario = url.searchParams.get('scenario') || null;
|
|
373
389
|
const route = findHarnessRoute(manifest, routeSlug);
|
|
374
390
|
if (!route) {
|
|
375
391
|
res.writeHead(404, {
|
|
@@ -386,6 +402,7 @@ export async function startAppDevServer({
|
|
|
386
402
|
appConfig,
|
|
387
403
|
route,
|
|
388
404
|
harnessOptions: harness,
|
|
405
|
+
scenario,
|
|
389
406
|
});
|
|
390
407
|
res.writeHead(200, {
|
|
391
408
|
...headers,
|
|
@@ -7,6 +7,7 @@ import { CONFIG_DIR } from './profiles.js';
|
|
|
7
7
|
export const DEFAULT_APP_DEV_SESSIONS_FILE = join(CONFIG_DIR, 'app-dev-sessions.json');
|
|
8
8
|
export const APP_DEV_SESSIONS_FILE = DEFAULT_APP_DEV_SESSIONS_FILE;
|
|
9
9
|
export const APP_DEV_SESSIONS_VERSION = 1;
|
|
10
|
+
export const APP_DEV_SESSION_MOUNT_ACKS_VERSION = 1;
|
|
10
11
|
|
|
11
12
|
/**
|
|
12
13
|
* Walk up from `startDir` looking for a workspace-scoped dev-sessions registry.
|
|
@@ -54,6 +55,12 @@ export function getAppDevSessionsFile(filePath) {
|
|
|
54
55
|
return DEFAULT_APP_DEV_SESSIONS_FILE;
|
|
55
56
|
}
|
|
56
57
|
|
|
58
|
+
export function getAppDevSessionMountAcksFile(sessionsFilePath) {
|
|
59
|
+
const resolvedSessionsFile = getAppDevSessionsFile(sessionsFilePath);
|
|
60
|
+
const parsed = parsePath(resolvedSessionsFile);
|
|
61
|
+
return join(parsed.dir, `${parsed.name}-mount-acks${parsed.ext || '.json'}`);
|
|
62
|
+
}
|
|
63
|
+
|
|
57
64
|
function normalizeSessions(raw) {
|
|
58
65
|
if (!raw || typeof raw !== 'object' || !Array.isArray(raw.sessions)) {
|
|
59
66
|
return [];
|
|
@@ -149,5 +156,96 @@ export function linkAppDevSessionTarget({ appId, devSlug, targetAppId, lastHeart
|
|
|
149
156
|
export function removeAppDevSession(sessionId, filePath) {
|
|
150
157
|
const registry = readAppDevSessions(filePath);
|
|
151
158
|
registry.sessions = registry.sessions.filter((session) => session.sessionId !== sessionId);
|
|
152
|
-
|
|
159
|
+
const result = writeAppDevSessions(registry, filePath);
|
|
160
|
+
removeAppDevSessionMountAcknowledgements(sessionId, filePath);
|
|
161
|
+
return result;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function normalizeMountAcknowledgements(raw) {
|
|
165
|
+
if (!raw || typeof raw !== 'object' || !Array.isArray(raw.acknowledgements)) {
|
|
166
|
+
return [];
|
|
167
|
+
}
|
|
168
|
+
return raw.acknowledgements.filter((acknowledgement) =>
|
|
169
|
+
acknowledgement &&
|
|
170
|
+
typeof acknowledgement === 'object' &&
|
|
171
|
+
typeof acknowledgement.sessionId === 'string' &&
|
|
172
|
+
typeof acknowledgement.appId === 'string' &&
|
|
173
|
+
typeof acknowledgement.devSlug === 'string' &&
|
|
174
|
+
typeof acknowledgement.mountNonce === 'string' &&
|
|
175
|
+
typeof acknowledgement.acknowledgedAt === 'string' &&
|
|
176
|
+
acknowledgement.stage === 'listed',
|
|
177
|
+
);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
export function readAppDevSessionMountAcknowledgements(sessionsFilePath) {
|
|
181
|
+
const filePath = getAppDevSessionMountAcksFile(sessionsFilePath);
|
|
182
|
+
if (!existsSync(filePath)) {
|
|
183
|
+
return { version: APP_DEV_SESSION_MOUNT_ACKS_VERSION, acknowledgements: [] };
|
|
184
|
+
}
|
|
185
|
+
try {
|
|
186
|
+
const raw = JSON.parse(readFileSync(filePath, 'utf8'));
|
|
187
|
+
return {
|
|
188
|
+
version: APP_DEV_SESSION_MOUNT_ACKS_VERSION,
|
|
189
|
+
acknowledgements: normalizeMountAcknowledgements(raw),
|
|
190
|
+
};
|
|
191
|
+
} catch {
|
|
192
|
+
return { version: APP_DEV_SESSION_MOUNT_ACKS_VERSION, acknowledgements: [] };
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
function writeAppDevSessionMountAcknowledgements(registry, sessionsFilePath) {
|
|
197
|
+
const filePath = getAppDevSessionMountAcksFile(sessionsFilePath);
|
|
198
|
+
mkdirSync(dirname(filePath), { recursive: true, mode: 0o700 });
|
|
199
|
+
const normalized = {
|
|
200
|
+
version: APP_DEV_SESSION_MOUNT_ACKS_VERSION,
|
|
201
|
+
acknowledgements: normalizeMountAcknowledgements(registry),
|
|
202
|
+
};
|
|
203
|
+
const tempPath = `${filePath}.${process.pid}.${randomUUID()}.tmp`;
|
|
204
|
+
writeFileSync(tempPath, JSON.stringify(normalized, null, 2), { mode: 0o600 });
|
|
205
|
+
renameSync(tempPath, filePath);
|
|
206
|
+
return normalized;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
export function removeAppDevSessionMountAcknowledgements(sessionId, sessionsFilePath) {
|
|
210
|
+
const filePath = getAppDevSessionMountAcksFile(sessionsFilePath);
|
|
211
|
+
if (!existsSync(filePath)) {
|
|
212
|
+
return { version: APP_DEV_SESSION_MOUNT_ACKS_VERSION, acknowledgements: [] };
|
|
213
|
+
}
|
|
214
|
+
const registry = readAppDevSessionMountAcknowledgements(sessionsFilePath);
|
|
215
|
+
registry.acknowledgements = registry.acknowledgements.filter(
|
|
216
|
+
(acknowledgement) => acknowledgement.sessionId !== sessionId,
|
|
217
|
+
);
|
|
218
|
+
return writeAppDevSessionMountAcknowledgements(registry, sessionsFilePath);
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
function mountAcknowledgementKey(value) {
|
|
222
|
+
return `${value.sessionId}:${value.appId}:${value.devSlug}:${value.mountNonce}`;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
export async function waitForAppDevSessionMountAcknowledgements(expectedSessions, options = {}) {
|
|
226
|
+
const expected = Array.isArray(expectedSessions) ? expectedSessions : [expectedSessions];
|
|
227
|
+
const timeoutMs = Number.isFinite(options.timeoutMs) ? Math.max(0, options.timeoutMs) : 15_000;
|
|
228
|
+
const pollIntervalMs = Number.isFinite(options.pollIntervalMs)
|
|
229
|
+
? Math.max(1, options.pollIntervalMs)
|
|
230
|
+
: 100;
|
|
231
|
+
const now = typeof options.now === 'function' ? options.now : Date.now;
|
|
232
|
+
const sleep = typeof options.sleep === 'function'
|
|
233
|
+
? options.sleep
|
|
234
|
+
: (delayMs) => new Promise((resolve) => setTimeout(resolve, delayMs));
|
|
235
|
+
const startedAt = now();
|
|
236
|
+
|
|
237
|
+
while (true) {
|
|
238
|
+
const registry = readAppDevSessionMountAcknowledgements(options.sessionsFilePath);
|
|
239
|
+
const acknowledgedKeys = new Set(registry.acknowledgements.map(mountAcknowledgementKey));
|
|
240
|
+
const missing = expected.filter((session) => !acknowledgedKeys.has(mountAcknowledgementKey(session)));
|
|
241
|
+
if (missing.length === 0) {
|
|
242
|
+
return { mounted: true, missing: [], acknowledgements: registry.acknowledgements };
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
const elapsedMs = now() - startedAt;
|
|
246
|
+
if (elapsedMs >= timeoutMs) {
|
|
247
|
+
return { mounted: false, missing, acknowledgements: registry.acknowledgements };
|
|
248
|
+
}
|
|
249
|
+
await sleep(Math.min(pollIntervalMs, timeoutMs - elapsedMs));
|
|
250
|
+
}
|
|
153
251
|
}
|
|
@@ -9,12 +9,13 @@
|
|
|
9
9
|
import { spawn } from 'node:child_process';
|
|
10
10
|
import { cpSync, existsSync, mkdirSync, readFileSync, writeFileSync, readdirSync, rmSync, statSync } from 'node:fs';
|
|
11
11
|
import { createRequire } from 'node:module';
|
|
12
|
-
import { dirname, join, resolve } from 'node:path';
|
|
12
|
+
import { dirname, join, relative, resolve } from 'node:path';
|
|
13
13
|
import { fileURLToPath } from 'node:url';
|
|
14
14
|
import { gunzipSync } from 'node:zlib';
|
|
15
15
|
|
|
16
16
|
import { usageError } from './errors.js';
|
|
17
17
|
import { validateArtifactBoundary, validateProjectBoundary } from './app-boundary-validator.js';
|
|
18
|
+
import { readAppChangelog } from './app-changelog.js';
|
|
18
19
|
|
|
19
20
|
const NOTIS_DIR = '.notis';
|
|
20
21
|
const STATE_FILE = join(NOTIS_DIR, 'state.json');
|
|
@@ -35,6 +36,7 @@ export const NOTIS_APP_CATEGORIES = [
|
|
|
35
36
|
'Product & Engineering',
|
|
36
37
|
'Personal',
|
|
37
38
|
];
|
|
39
|
+
export const MIN_LISTING_SCREENSHOTS = 3;
|
|
38
40
|
const SOURCE_COPY_EXCLUDES = new Set([
|
|
39
41
|
'node_modules',
|
|
40
42
|
'.notis',
|
|
@@ -236,7 +238,7 @@ export function detectProjectWarnings(projectDir, appConfig = null) {
|
|
|
236
238
|
if (appConfig) {
|
|
237
239
|
try {
|
|
238
240
|
const listing = inspectListingReadiness(projectDir, appConfig);
|
|
239
|
-
warnings.push(...listing.warnings);
|
|
241
|
+
warnings.push(...listing.errors, ...listing.warnings);
|
|
240
242
|
} catch (error) {
|
|
241
243
|
warnings.push(error instanceof Error ? error.message : String(error));
|
|
242
244
|
}
|
|
@@ -291,11 +293,10 @@ function pngDimensions(buffer) {
|
|
|
291
293
|
};
|
|
292
294
|
}
|
|
293
295
|
|
|
294
|
-
function imageAssetMeta(projectDir, relPath, { maxBytes
|
|
296
|
+
function imageAssetMeta(projectDir, relPath, { maxBytes }) {
|
|
295
297
|
const fullPath = join(projectDir, relPath);
|
|
296
298
|
const content = readFileSync(fullPath);
|
|
297
299
|
const dimensions = pngDimensions(content);
|
|
298
|
-
const ratio = dimensions?.height ? dimensions.width / dimensions.height : null;
|
|
299
300
|
const errors = [];
|
|
300
301
|
const warnings = [];
|
|
301
302
|
|
|
@@ -305,10 +306,10 @@ function imageAssetMeta(projectDir, relPath, { maxBytes, recommendedRatio = 1.6
|
|
|
305
306
|
if (content.length > maxBytes) {
|
|
306
307
|
errors.push(`${relPath} must be ${Math.round(maxBytes / 1024 / 1024)} MB or smaller.`);
|
|
307
308
|
}
|
|
308
|
-
if (
|
|
309
|
-
errors.push(`${relPath}
|
|
310
|
-
} else if (
|
|
311
|
-
|
|
309
|
+
if (!dimensions) {
|
|
310
|
+
errors.push(`${relPath} is not a valid PNG file.`);
|
|
311
|
+
} else if (dimensions.width !== 2000 || dimensions.height !== 1250) {
|
|
312
|
+
errors.push(`${relPath} must be exactly 2000x1250 pixels.`);
|
|
312
313
|
}
|
|
313
314
|
|
|
314
315
|
return {
|
|
@@ -346,33 +347,135 @@ export function discoverMetadataAssets(projectDir) {
|
|
|
346
347
|
};
|
|
347
348
|
}
|
|
348
349
|
|
|
350
|
+
function normalizeConfiguredScreenshots(appConfig = null) {
|
|
351
|
+
if (!Array.isArray(appConfig?.screenshots) || appConfig.screenshots.length === 0) {
|
|
352
|
+
return null;
|
|
353
|
+
}
|
|
354
|
+
if (appConfig.screenshots.length > 6) {
|
|
355
|
+
throw usageError('Configure at most six listing screenshots.');
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
const seenPaths = new Set();
|
|
359
|
+
return appConfig.screenshots.map((entry, index) => {
|
|
360
|
+
const path = typeof entry?.path === 'string' ? entry.path.trim().replace(/\\/g, '/') : '';
|
|
361
|
+
const alt = typeof entry?.alt === 'string' ? entry.alt.trim() : '';
|
|
362
|
+
const route = typeof entry?.route === 'string' ? entry.route.trim() : '';
|
|
363
|
+
const scenario = typeof entry?.scenario === 'string' ? entry.scenario.trim() : '';
|
|
364
|
+
const focus = typeof entry?.focus === 'string' ? entry.focus.trim() : '';
|
|
365
|
+
const theme = typeof entry?.theme === 'string' ? entry.theme.trim().toLowerCase() : '';
|
|
366
|
+
if (!/^metadata\/screenshot-\d+\.png$/i.test(path)) {
|
|
367
|
+
throw usageError(`screenshots[${index}].path must match metadata/screenshot-N.png.`);
|
|
368
|
+
}
|
|
369
|
+
const expectedPath = `metadata/screenshot-${index + 1}.png`;
|
|
370
|
+
if (path !== expectedPath) {
|
|
371
|
+
throw usageError(`screenshots[${index}].path must be ${expectedPath} so capture order stays stable.`);
|
|
372
|
+
}
|
|
373
|
+
if (seenPaths.has(path)) {
|
|
374
|
+
throw usageError(`Duplicate listing screenshot path: ${path}`);
|
|
375
|
+
}
|
|
376
|
+
if (theme && theme !== 'light' && theme !== 'dark') {
|
|
377
|
+
throw usageError(`screenshots[${index}].theme must be light or dark.`);
|
|
378
|
+
}
|
|
379
|
+
seenPaths.add(path);
|
|
380
|
+
return {
|
|
381
|
+
path,
|
|
382
|
+
alt,
|
|
383
|
+
route: route || null,
|
|
384
|
+
scenario: scenario || null,
|
|
385
|
+
focus: focus || null,
|
|
386
|
+
theme: theme || 'light',
|
|
387
|
+
};
|
|
388
|
+
});
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
/** Resolve listing screenshot files in their configured editorial order. */
|
|
392
|
+
export function resolveListingScreenshots(projectDir, appConfig = null) {
|
|
393
|
+
const discovered = discoverMetadataAssets(projectDir).screenshots;
|
|
394
|
+
const configured = normalizeConfiguredScreenshots(appConfig);
|
|
395
|
+
if (!configured) {
|
|
396
|
+
return discovered.map((asset) => ({
|
|
397
|
+
...asset,
|
|
398
|
+
alt: null,
|
|
399
|
+
route: null,
|
|
400
|
+
scenario: null,
|
|
401
|
+
focus: null,
|
|
402
|
+
theme: 'light',
|
|
403
|
+
}));
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
const byPath = new Map(discovered.map((asset) => [asset.path.replace(/\\/g, '/'), asset]));
|
|
407
|
+
return configured.map((entry) => {
|
|
408
|
+
const asset = byPath.get(entry.path);
|
|
409
|
+
if (!asset) {
|
|
410
|
+
return {
|
|
411
|
+
...entry,
|
|
412
|
+
content_type: 'image/png',
|
|
413
|
+
bytes: null,
|
|
414
|
+
width: null,
|
|
415
|
+
height: null,
|
|
416
|
+
errors: [`${entry.path} is configured but the file does not exist.`],
|
|
417
|
+
warnings: [],
|
|
418
|
+
};
|
|
419
|
+
}
|
|
420
|
+
return { ...asset, ...entry };
|
|
421
|
+
});
|
|
422
|
+
}
|
|
423
|
+
|
|
349
424
|
export function inspectListingReadiness(projectDir, appConfig = null) {
|
|
350
425
|
const config = appConfig || {};
|
|
351
426
|
const warnings = [];
|
|
352
427
|
const errors = [];
|
|
353
|
-
const
|
|
428
|
+
const screenshots = resolveListingScreenshots(projectDir, config);
|
|
429
|
+
const metadata = { screenshots };
|
|
354
430
|
const categories = normalizeCategories(config.categories || []);
|
|
431
|
+
const changelog = readAppChangelog(projectDir);
|
|
432
|
+
errors.push(...changelog.errors);
|
|
433
|
+
|
|
434
|
+
const packagePath = join(projectDir, 'package.json');
|
|
435
|
+
if (!existsSync(packagePath)) {
|
|
436
|
+
errors.push('package.json is required for Store publication.');
|
|
437
|
+
} else {
|
|
438
|
+
try {
|
|
439
|
+
const packageJson = JSON.parse(readFileSync(packagePath, 'utf-8'));
|
|
440
|
+
const notisAppVersion = String(packageJson.notisAppVersion || '').trim();
|
|
441
|
+
if (!/^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?(?:\+[0-9A-Za-z.-]+)?$/.test(notisAppVersion)) {
|
|
442
|
+
errors.push('package.json must include a semver `notisAppVersion` (for example, "0.1.0").');
|
|
443
|
+
}
|
|
444
|
+
} catch {
|
|
445
|
+
errors.push('package.json must contain valid JSON for Store publication.');
|
|
446
|
+
}
|
|
447
|
+
}
|
|
355
448
|
|
|
356
449
|
if (!String(config.tagline || '').trim()) {
|
|
357
|
-
|
|
450
|
+
errors.push('Listing tagline missing in notis.config.ts.');
|
|
358
451
|
}
|
|
359
452
|
if (categories.length === 0) {
|
|
360
|
-
|
|
453
|
+
errors.push(`Listing category missing in notis.config.ts. Use one of: ${NOTIS_APP_CATEGORIES.join(', ')}.`);
|
|
361
454
|
}
|
|
362
|
-
if (metadata.screenshots.length <
|
|
363
|
-
|
|
455
|
+
if (metadata.screenshots.length < MIN_LISTING_SCREENSHOTS) {
|
|
456
|
+
errors.push(`Listing screenshots: ${metadata.screenshots.length}/${MIN_LISTING_SCREENSHOTS} minimum in metadata/screenshot-N.png. Run \`notis apps screenshot\` to generate them.`);
|
|
364
457
|
}
|
|
365
458
|
for (const screenshot of metadata.screenshots) {
|
|
366
459
|
errors.push(...screenshot.errors);
|
|
367
460
|
warnings.push(...screenshot.warnings);
|
|
461
|
+
if (!screenshot.alt) {
|
|
462
|
+
errors.push(`${screenshot.path} is missing descriptive alt text in notis.config.ts -> screenshots.`);
|
|
463
|
+
}
|
|
368
464
|
}
|
|
369
465
|
|
|
370
466
|
return {
|
|
371
|
-
ready:
|
|
467
|
+
ready:
|
|
468
|
+
errors.length === 0 &&
|
|
469
|
+
Boolean(String(config.tagline || '').trim()) &&
|
|
470
|
+
categories.length > 0 &&
|
|
471
|
+
metadata.screenshots.length >= MIN_LISTING_SCREENSHOTS &&
|
|
472
|
+
metadata.screenshots.every((screenshot) => Boolean(screenshot.alt)) &&
|
|
473
|
+
changelog.entries.length > 0,
|
|
372
474
|
warnings,
|
|
373
475
|
errors,
|
|
374
476
|
metadata,
|
|
375
477
|
categories,
|
|
478
|
+
changelog,
|
|
376
479
|
};
|
|
377
480
|
}
|
|
378
481
|
|
|
@@ -606,9 +709,15 @@ export function generateManifest(appConfig, projectDir) {
|
|
|
606
709
|
|
|
607
710
|
const databases = appConfig.databases || [];
|
|
608
711
|
const categories = normalizeCategories(appConfig.categories || []);
|
|
609
|
-
const metadata =
|
|
712
|
+
const metadata = { screenshots: resolveListingScreenshots(projectDir, appConfig) };
|
|
610
713
|
const appSlug = safeKebab(appConfig.name);
|
|
611
714
|
const displayTitle = appConfig.title || appConfig.displayName || appConfig.name;
|
|
715
|
+
const skills = (Array.isArray(appConfig.skills) ? appConfig.skills : []).map((skill) => ({
|
|
716
|
+
key: skill.key,
|
|
717
|
+
path: String(skill.path || '').replace(/^\.\/+/, ''),
|
|
718
|
+
name: skill.name,
|
|
719
|
+
description: skill.description || null,
|
|
720
|
+
}));
|
|
612
721
|
const listingMedia = {
|
|
613
722
|
screenshots: metadata.screenshots.map((screenshot) => ({
|
|
614
723
|
path: screenshot.path,
|
|
@@ -616,8 +725,27 @@ export function generateManifest(appConfig, projectDir) {
|
|
|
616
725
|
width: screenshot.width,
|
|
617
726
|
height: screenshot.height,
|
|
618
727
|
bytes: screenshot.bytes,
|
|
728
|
+
alt: screenshot.alt || null,
|
|
729
|
+
theme: screenshot.theme || 'light',
|
|
619
730
|
})),
|
|
620
731
|
};
|
|
732
|
+
const changelog = readAppChangelog(projectDir);
|
|
733
|
+
if (changelog.exists && changelog.errors.length > 0) {
|
|
734
|
+
throw usageError(changelog.errors.join('\n'));
|
|
735
|
+
}
|
|
736
|
+
const latestChangelogEntry = changelog.entries[0] || null;
|
|
737
|
+
const legacyVersionNotes = latestChangelogEntry?.body
|
|
738
|
+
|| appConfig.versionNotes
|
|
739
|
+
|| appConfig.version_notes
|
|
740
|
+
|| null;
|
|
741
|
+
let releaseVersion = null;
|
|
742
|
+
try {
|
|
743
|
+
const packageJson = JSON.parse(readFileSync(join(projectDir, 'package.json'), 'utf-8'));
|
|
744
|
+
releaseVersion = String(packageJson.notisAppVersion || '').trim() || null;
|
|
745
|
+
} catch {
|
|
746
|
+
// Store readiness reports missing or malformed package metadata. Ordinary
|
|
747
|
+
// development builds remain usable without a Store release version.
|
|
748
|
+
}
|
|
621
749
|
|
|
622
750
|
return {
|
|
623
751
|
version: 1,
|
|
@@ -632,14 +760,23 @@ export function generateManifest(appConfig, projectDir) {
|
|
|
632
760
|
tagline: appConfig.tagline || null,
|
|
633
761
|
categories,
|
|
634
762
|
author: appConfig.author || null,
|
|
635
|
-
|
|
763
|
+
release_version: releaseVersion,
|
|
764
|
+
version_notes: legacyVersionNotes,
|
|
636
765
|
},
|
|
637
766
|
listing: {
|
|
638
767
|
title: displayTitle,
|
|
639
768
|
tagline: appConfig.tagline || null,
|
|
640
769
|
categories,
|
|
641
770
|
author: appConfig.author || null,
|
|
642
|
-
version_notes:
|
|
771
|
+
version_notes: legacyVersionNotes,
|
|
772
|
+
...(changelog.exists
|
|
773
|
+
? {
|
|
774
|
+
changelog: {
|
|
775
|
+
source_path: changelog.source_path,
|
|
776
|
+
entries: changelog.entries,
|
|
777
|
+
},
|
|
778
|
+
}
|
|
779
|
+
: {}),
|
|
643
780
|
media: listingMedia,
|
|
644
781
|
},
|
|
645
782
|
metadata: listingMedia,
|
|
@@ -650,9 +787,47 @@ export function generateManifest(appConfig, projectDir) {
|
|
|
650
787
|
},
|
|
651
788
|
databases,
|
|
652
789
|
tools: appConfig.tools || [],
|
|
790
|
+
skills,
|
|
791
|
+
onboarding: appConfig.onboarding || null,
|
|
653
792
|
};
|
|
654
793
|
}
|
|
655
794
|
|
|
795
|
+
export function resolveConfiguredAppSkills(appConfig, projectDir) {
|
|
796
|
+
const configured = Array.isArray(appConfig.skills) ? appConfig.skills : [];
|
|
797
|
+
const projectRoot = resolve(projectDir);
|
|
798
|
+
const seenKeys = new Set();
|
|
799
|
+
|
|
800
|
+
return configured.map((skill, index) => {
|
|
801
|
+
const key = typeof skill?.key === 'string' ? skill.key.trim() : '';
|
|
802
|
+
const sourcePath = typeof skill?.path === 'string' ? skill.path.trim() : '';
|
|
803
|
+
const name = typeof skill?.name === 'string' ? skill.name.trim() : '';
|
|
804
|
+
if (!key || !sourcePath || !name) {
|
|
805
|
+
throw usageError(`skills[${index}] must define non-empty key, path, and name values.`);
|
|
806
|
+
}
|
|
807
|
+
if (seenKeys.has(key)) {
|
|
808
|
+
throw usageError(`Duplicate app skill key: ${key}`);
|
|
809
|
+
}
|
|
810
|
+
seenKeys.add(key);
|
|
811
|
+
|
|
812
|
+
const absolutePath = resolve(projectRoot, sourcePath);
|
|
813
|
+
const relativePath = relative(projectRoot, absolutePath).replace(/\\/g, '/');
|
|
814
|
+
if (!relativePath || relativePath.startsWith('../') || relativePath === '..') {
|
|
815
|
+
throw usageError(`App skill path must stay inside the project: ${sourcePath}`);
|
|
816
|
+
}
|
|
817
|
+
if (!existsSync(absolutePath) || !statSync(absolutePath).isFile()) {
|
|
818
|
+
throw usageError(`App skill entrypoint not found: ${sourcePath}`);
|
|
819
|
+
}
|
|
820
|
+
|
|
821
|
+
return {
|
|
822
|
+
key,
|
|
823
|
+
path: relativePath,
|
|
824
|
+
name,
|
|
825
|
+
description: typeof skill.description === 'string' ? skill.description.trim() : null,
|
|
826
|
+
skill_md: readFileSync(absolutePath, 'utf8'),
|
|
827
|
+
};
|
|
828
|
+
});
|
|
829
|
+
}
|
|
830
|
+
|
|
656
831
|
/**
|
|
657
832
|
* Build the app bundle: generate entry file, run `vite build`, package into .notis/output/.
|
|
658
833
|
*/
|
|
@@ -695,10 +870,14 @@ export function readManifest(projectDir) {
|
|
|
695
870
|
|
|
696
871
|
function copyMetadataAssets(projectDir) {
|
|
697
872
|
const metadataDir = join(projectDir, METADATA_DIR);
|
|
873
|
+
const outputMetadataDir = join(projectDir, OUTPUT_DIR, METADATA_DIR);
|
|
874
|
+
// Vite clears the bundle directory, not sibling listing media. Always make
|
|
875
|
+
// the packaged metadata an exact reflection of the source tree so removed
|
|
876
|
+
// placeholders can never survive into a later deploy.
|
|
877
|
+
rmSync(outputMetadataDir, { recursive: true, force: true });
|
|
698
878
|
if (!existsSync(metadataDir)) {
|
|
699
879
|
return;
|
|
700
880
|
}
|
|
701
|
-
const outputMetadataDir = join(projectDir, OUTPUT_DIR, METADATA_DIR);
|
|
702
881
|
mkdirSync(outputMetadataDir, { recursive: true });
|
|
703
882
|
for (const entry of readdirSync(metadataDir, { withFileTypes: true })) {
|
|
704
883
|
if (!entry.isFile()) continue;
|
|
Binary file
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import { CliError, EXIT_CODES } from './errors.js';
|
|
2
|
+
|
|
3
|
+
function isPidRunning(pid) {
|
|
4
|
+
if (!Number.isInteger(pid) || pid <= 0) {
|
|
5
|
+
return false;
|
|
6
|
+
}
|
|
7
|
+
try {
|
|
8
|
+
process.kill(pid, 0);
|
|
9
|
+
return true;
|
|
10
|
+
} catch (error) {
|
|
11
|
+
return error?.code === 'EPERM';
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function quoteShellArgument(value) {
|
|
16
|
+
return `'${String(value).replace(/'/g, `'"'"'`)}'`;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function defaultDesktopAppName(apiBase) {
|
|
20
|
+
try {
|
|
21
|
+
return new URL(apiBase).hostname === 'api-beta.notis.ai' ? 'Notis Beta' : 'Notis';
|
|
22
|
+
} catch {
|
|
23
|
+
return 'Notis';
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function getDesktopAuthRecovery(runtime) {
|
|
28
|
+
const desktopRunning = isPidRunning(runtime.desktopPid);
|
|
29
|
+
const appName = runtime.desktopAppName || defaultDesktopAppName(runtime.apiBase);
|
|
30
|
+
let command = 'Start the Notis desktop app';
|
|
31
|
+
if (process.platform === 'darwin') {
|
|
32
|
+
command = `open -a ${quoteShellArgument(appName)}`;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
return {
|
|
36
|
+
desktopRunning,
|
|
37
|
+
appName,
|
|
38
|
+
hints: [
|
|
39
|
+
{
|
|
40
|
+
command,
|
|
41
|
+
reason: desktopRunning
|
|
42
|
+
? `Bring ${appName} forward so it can renew CLI authentication, then retry`
|
|
43
|
+
: `Start ${appName} to renew expired CLI authentication, then retry`,
|
|
44
|
+
},
|
|
45
|
+
{
|
|
46
|
+
command: 'notis doctor',
|
|
47
|
+
reason: 'Retry the auth and API checks after the desktop app is ready',
|
|
48
|
+
},
|
|
49
|
+
],
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function createExpiredAuthError(runtime) {
|
|
54
|
+
if (runtime.credentialSource === 'env') {
|
|
55
|
+
return new CliError({
|
|
56
|
+
code: 'auth_expired',
|
|
57
|
+
message: 'NOTIS_JWT is expired',
|
|
58
|
+
exitCode: EXIT_CODES.auth,
|
|
59
|
+
details: { credential_source: 'env' },
|
|
60
|
+
hints: [
|
|
61
|
+
{
|
|
62
|
+
command: 'Set NOTIS_JWT to a fresh token',
|
|
63
|
+
reason: 'The explicit environment credential overrides desktop-managed auth',
|
|
64
|
+
},
|
|
65
|
+
],
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const recovery = getDesktopAuthRecovery(runtime);
|
|
70
|
+
return new CliError({
|
|
71
|
+
code: 'auth_expired',
|
|
72
|
+
message: 'Notis CLI authentication has expired',
|
|
73
|
+
exitCode: EXIT_CODES.auth,
|
|
74
|
+
details: {
|
|
75
|
+
credential_source: 'desktop',
|
|
76
|
+
desktop_running: recovery.desktopRunning,
|
|
77
|
+
desktop_app_name: recovery.appName,
|
|
78
|
+
},
|
|
79
|
+
hints: recovery.hints,
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export function createInvalidAuthHints(runtime) {
|
|
84
|
+
if (runtime?.credentialSource === 'env') {
|
|
85
|
+
return [
|
|
86
|
+
{
|
|
87
|
+
command: 'Set NOTIS_JWT to a fresh token',
|
|
88
|
+
reason: 'The explicit environment credential was rejected',
|
|
89
|
+
},
|
|
90
|
+
];
|
|
91
|
+
}
|
|
92
|
+
return getDesktopAuthRecovery(runtime || {}).hints;
|
|
93
|
+
}
|