@notis_ai/cli 0.2.0-beta.155.1 → 0.2.0-beta.157.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.
Files changed (47) hide show
  1. package/README.md +11 -45
  2. package/config/notis_app_design_rules.json +135 -0
  3. package/dist/agent-hooks/notis-agent-hook.mjs +5180 -7281
  4. package/dist/base-skills/notis-apps/SKILL.md +141 -224
  5. package/dist/base-skills/notis-cli/SKILL.md +64 -131
  6. package/package.json +1 -2
  7. package/skills/notis-apps/cli.md +34 -95
  8. package/skills/notis-cli/AGENT_INSTRUCTIONS.md +1 -1
  9. package/src/command-specs/apps.js +326 -1562
  10. package/src/runtime/agent-browser.js +169 -1
  11. package/src/runtime/app-boundary-validator.js +221 -0
  12. package/src/runtime/app-platform.js +359 -233
  13. package/src/runtime/app-test-server.js +292 -0
  14. package/template/app/page.tsx +47 -45
  15. package/template/components/page-heading.tsx +23 -0
  16. package/template/components/ui/badge.tsx +7 -4
  17. package/template/components/ui/card.tsx +24 -11
  18. package/template/components/ui/native-select.tsx +24 -0
  19. package/template/notis.config.ts +0 -1
  20. package/template/package.json +2 -2
  21. package/template/packages/sdk/package.json +1 -2
  22. package/template/packages/sdk/src/components/MultiSelectActionBar.tsx +62 -8
  23. package/template/packages/sdk/src/components/Skeleton.tsx +24 -0
  24. package/template/packages/sdk/src/config.ts +0 -2
  25. package/template/packages/sdk/src/hooks/useCloudComputer.ts +15 -48
  26. package/template/packages/sdk/src/hooks/useDatabaseSchema.ts +17 -53
  27. package/template/packages/sdk/src/hooks/useDatabaseSubscription.ts +2 -2
  28. package/template/packages/sdk/src/hooks/useDocument.ts +12 -47
  29. package/template/packages/sdk/src/hooks/useDocuments.ts +18 -58
  30. package/template/packages/sdk/src/hooks/useQuery.ts +71 -0
  31. package/template/packages/sdk/src/hooks/useToolQuery.ts +12 -0
  32. package/template/packages/sdk/src/hooks/useTopBarSearch.ts +15 -7
  33. package/template/packages/sdk/src/index.ts +8 -0
  34. package/template/packages/sdk/src/interactions.ts +2 -1
  35. package/template/packages/sdk/src/queryCache.ts +162 -0
  36. package/template/packages/sdk/src/runtime.ts +5 -0
  37. package/template/packages/sdk/src/styles.css +28 -1
  38. package/src/runtime/app-dev-build-supervisor.js +0 -47
  39. package/src/runtime/app-dev-build.js +0 -41
  40. package/src/runtime/app-dev-consumers.js +0 -154
  41. package/src/runtime/app-dev-host-lock.js +0 -80
  42. package/src/runtime/app-dev-process-identity.js +0 -111
  43. package/src/runtime/app-dev-roots.js +0 -284
  44. package/src/runtime/app-dev-server.js +0 -1136
  45. package/src/runtime/app-dev-sessions.js +0 -185
  46. package/src/runtime/cli-mode.generated.js +0 -5
  47. package/src/runtime/cli-mode.js +0 -34
@@ -1,111 +0,0 @@
1
- import { execFileSync } from 'node:child_process';
2
- import { readlinkSync, realpathSync } from 'node:fs';
3
-
4
- export const NOTIS_APP_BUILD_COMMAND_FINGERPRINT = 'npm:run-build:watch:v1';
5
- export const NOTIS_APPS_DEV_HOST_COMMAND_FINGERPRINT = 'notis:apps-dev:v1';
6
-
7
- function normalizePath(value) {
8
- if (typeof value !== 'string' || !value.trim()) return null;
9
- try {
10
- return realpathSync(value.trim());
11
- } catch {
12
- return null;
13
- }
14
- }
15
-
16
- export function isExpectedNotisBuildCommand(command) {
17
- if (typeof command !== 'string') return false;
18
- return /(?:^|[/\s])npm(?:\s|$)/.test(command)
19
- && /\brun\s+build\b/.test(command)
20
- && /(?:^|\s)--watch(?:\s|$)/.test(command);
21
- }
22
-
23
- export function isExpectedNotisAppsDevHostCommand(command) {
24
- return typeof command === 'string'
25
- && /(?:notis(?:\.js)?|@notis_ai[/\\]cli)/.test(command)
26
- && /\bapps\s+dev\b/.test(command);
27
- }
28
-
29
- function readDarwinProcessCwd(pid, execute) {
30
- const output = execute('lsof', ['-a', '-p', String(pid), '-d', 'cwd', '-Fn'], {
31
- encoding: 'utf8',
32
- stdio: ['ignore', 'pipe', 'ignore'],
33
- });
34
- const line = output.split('\n').find((entry) => entry.startsWith('n'));
35
- return line ? line.slice(1) : null;
36
- }
37
-
38
- export function inspectAppDevWatcherProcess(pid, {
39
- platform = process.platform,
40
- execute = execFileSync,
41
- readLink = readlinkSync,
42
- } = {}) {
43
- if (!Number.isSafeInteger(pid) || pid <= 0 || platform === 'win32') return null;
44
- try {
45
- const ps = (field) => execute('ps', ['-o', `${field}=`, '-p', String(pid)], {
46
- encoding: 'utf8',
47
- stdio: ['ignore', 'pipe', 'ignore'],
48
- }).trim();
49
- const processGroupPid = Number.parseInt(ps('pgid'), 10);
50
- const startIdentity = ps('lstart').replace(/\s+/g, ' ').trim();
51
- const command = ps('command');
52
- const cwd = platform === 'linux'
53
- ? readLink(`/proc/${pid}/cwd`)
54
- : readDarwinProcessCwd(pid, execute);
55
- const projectDir = normalizePath(cwd);
56
- if (!Number.isSafeInteger(processGroupPid) || processGroupPid <= 0) return null;
57
- if (!startIdentity || !command || !projectDir) return null;
58
- return { pid, processGroupPid, startIdentity, command, projectDir };
59
- } catch {
60
- return null;
61
- }
62
- }
63
-
64
- export function captureDesktopWatcherOwnership({
65
- pid,
66
- projectDir,
67
- desktopOwnerId,
68
- desktopOwnerScope,
69
- inspect = inspectAppDevWatcherProcess,
70
- } = {}) {
71
- const owner = typeof desktopOwnerId === 'string' ? desktopOwnerId.trim() : '';
72
- const ownerScope = typeof desktopOwnerScope === 'string' ? desktopOwnerScope.trim() : '';
73
- const expectedProjectDir = normalizePath(projectDir);
74
- if (!owner || !ownerScope || !expectedProjectDir) return null;
75
- const identity = inspect(pid);
76
- if (
77
- !identity
78
- || identity.processGroupPid !== pid
79
- || identity.projectDir !== expectedProjectDir
80
- || !isExpectedNotisBuildCommand(identity.command)
81
- ) {
82
- return null;
83
- }
84
- return {
85
- desktopOwnerId: owner,
86
- desktopOwnerScope: ownerScope,
87
- watcherProcessGroupPid: identity.processGroupPid,
88
- watcherStartIdentity: identity.startIdentity,
89
- watcherProjectDir: identity.projectDir,
90
- watcherCommandFingerprint: NOTIS_APP_BUILD_COMMAND_FINGERPRINT,
91
- };
92
- }
93
-
94
- export function captureDesktopHostOwnership({
95
- pid = process.pid,
96
- desktopOwnerId,
97
- desktopOwnerScope,
98
- inspect = inspectAppDevWatcherProcess,
99
- } = {}) {
100
- const owner = typeof desktopOwnerId === 'string' ? desktopOwnerId.trim() : '';
101
- const ownerScope = typeof desktopOwnerScope === 'string' ? desktopOwnerScope.trim() : '';
102
- if (!owner || !ownerScope) return null;
103
- const identity = inspect(pid);
104
- if (!identity || !isExpectedNotisAppsDevHostCommand(identity.command)) return null;
105
- return {
106
- desktopOwnerId: owner,
107
- desktopOwnerScope: ownerScope,
108
- desktopHostStartIdentity: identity.startIdentity,
109
- desktopHostCommandFingerprint: NOTIS_APPS_DEV_HOST_COMMAND_FINGERPRINT,
110
- };
111
- }
@@ -1,284 +0,0 @@
1
- import { randomUUID } from 'node:crypto';
2
- import {
3
- existsSync,
4
- lstatSync,
5
- mkdirSync,
6
- readFileSync,
7
- readdirSync,
8
- realpathSync,
9
- renameSync,
10
- rmSync,
11
- statSync,
12
- writeFileSync,
13
- } from 'node:fs';
14
- import { homedir } from 'node:os';
15
- import { dirname, join, parse as parsePath, resolve } from 'node:path';
16
-
17
- import { usageError } from './errors.js';
18
- import { CONFIG_DIR } from './profiles.js';
19
-
20
- export const APP_DEV_ROOTS_VERSION = 1;
21
- export const DEFAULT_APP_DEV_ROOT = join(homedir(), '.notis', 'apps');
22
- export const DEFAULT_APP_DEV_ROOTS_FILE = join(CONFIG_DIR, 'app-dev-roots.json');
23
- const CONFIG_FILENAMES = ['notis.config.ts', 'notis.config.js', 'notis.config.mjs'];
24
- const LOCK_TIMEOUT_MS = 5_000;
25
- const lockWait = new Int32Array(new SharedArrayBuffer(4));
26
-
27
- function rootsFile(filePath) {
28
- if (filePath) return resolve(filePath);
29
- const envPath = process.env.NOTIS_APP_DEV_ROOTS_FILE;
30
- return typeof envPath === 'string' && envPath.trim()
31
- ? resolve(envPath.trim())
32
- : DEFAULT_APP_DEV_ROOTS_FILE;
33
- }
34
-
35
- function legacyProjectsFile(filePath) {
36
- const sessionsFile = filePath
37
- || join(CONFIG_DIR, 'app-dev-sessions.json');
38
- const parsed = parsePath(resolve(sessionsFile));
39
- return join(parsed.dir, `${parsed.name}-projects${parsed.ext || '.json'}`);
40
- }
41
-
42
- function readLockOwner(lockPath) {
43
- try {
44
- return readFileSync(join(lockPath, 'owner'), 'utf8').trim();
45
- } catch {
46
- return null;
47
- }
48
- }
49
-
50
- function withLock(filePath, callback) {
51
- mkdirSync(dirname(filePath), { recursive: true, mode: 0o700 });
52
- const lockPath = `${filePath}.lock`;
53
- const owner = `${process.pid}.${randomUUID()}`;
54
- const startedAt = Date.now();
55
- while (true) {
56
- try {
57
- mkdirSync(lockPath, { mode: 0o700 });
58
- writeFileSync(join(lockPath, 'owner'), owner, { mode: 0o600 });
59
- break;
60
- } catch (error) {
61
- if (error?.code !== 'EEXIST') throw error;
62
- try {
63
- const stat = lstatSync(lockPath);
64
- if (!stat.isDirectory() || stat.isSymbolicLink()) {
65
- throw new Error(`Refusing unsafe app development roots lock: ${lockPath}`);
66
- }
67
- const existingOwner = readLockOwner(lockPath);
68
- const pid = Number.parseInt(String(existingOwner || '').split('.')[0] || '', 10);
69
- if (Number.isInteger(pid) && pid > 0) {
70
- try {
71
- process.kill(pid, 0);
72
- } catch (ownerError) {
73
- if (ownerError?.code === 'ESRCH') {
74
- rmSync(lockPath, { recursive: true, force: true });
75
- continue;
76
- }
77
- }
78
- }
79
- } catch (statError) {
80
- if (statError?.code === 'ENOENT') continue;
81
- throw statError;
82
- }
83
- if (Date.now() - startedAt >= LOCK_TIMEOUT_MS) {
84
- throw new Error(`Timed out waiting for app development roots lock: ${filePath}`);
85
- }
86
- Atomics.wait(lockWait, 0, 0, 10);
87
- }
88
- }
89
- try {
90
- return callback();
91
- } finally {
92
- if (readLockOwner(lockPath) === owner) {
93
- rmSync(lockPath, { recursive: true, force: true });
94
- }
95
- }
96
- }
97
-
98
- function canonicalExistingDirectory(inputPath) {
99
- const absolute = resolve(String(inputPath || ''));
100
- let stat;
101
- try {
102
- stat = statSync(absolute);
103
- } catch {
104
- throw usageError(`App development root does not exist: ${absolute}`);
105
- }
106
- if (!stat.isDirectory()) {
107
- throw usageError(`App development root is not a directory: ${absolute}`);
108
- }
109
- return realpathSync(absolute);
110
- }
111
-
112
- function normalizeRegistry(raw) {
113
- const roots = Array.isArray(raw?.roots) ? raw.roots : [];
114
- const seen = new Set();
115
- return roots
116
- .map((entry) => typeof entry === 'string' ? { path: entry } : entry)
117
- .filter((entry) => entry && typeof entry.path === 'string' && entry.path.trim())
118
- .map((entry) => ({
119
- path: resolve(entry.path),
120
- registeredAt: typeof entry.registeredAt === 'string'
121
- ? entry.registeredAt
122
- : new Date(0).toISOString(),
123
- }))
124
- .filter((entry) => {
125
- if (seen.has(entry.path)) return false;
126
- seen.add(entry.path);
127
- return true;
128
- });
129
- }
130
-
131
- function readRaw(filePath) {
132
- if (!existsSync(filePath)) return { version: APP_DEV_ROOTS_VERSION, roots: [] };
133
- try {
134
- return {
135
- version: APP_DEV_ROOTS_VERSION,
136
- roots: normalizeRegistry(JSON.parse(readFileSync(filePath, 'utf8'))),
137
- };
138
- } catch {
139
- return { version: APP_DEV_ROOTS_VERSION, roots: [] };
140
- }
141
- }
142
-
143
- function writeRaw(registry, filePath) {
144
- mkdirSync(dirname(filePath), { recursive: true, mode: 0o700 });
145
- const normalized = {
146
- version: APP_DEV_ROOTS_VERSION,
147
- roots: normalizeRegistry(registry),
148
- };
149
- const temporary = `${filePath}.${process.pid}.${randomUUID()}.tmp`;
150
- writeFileSync(temporary, JSON.stringify(normalized, null, 2), { mode: 0o600 });
151
- renameSync(temporary, filePath);
152
- return normalized;
153
- }
154
-
155
- function migrateLegacyProjectsUnlocked(registry, options) {
156
- const path = legacyProjectsFile(options.legacySessionsFilePath);
157
- if (!existsSync(path)) return registry;
158
- let projects = [];
159
- try {
160
- const raw = JSON.parse(readFileSync(path, 'utf8'));
161
- projects = Array.isArray(raw?.projects) ? raw.projects : [];
162
- } catch {
163
- return registry;
164
- }
165
- const byPath = new Map(registry.roots.map((entry) => [entry.path, entry]));
166
- for (const project of projects) {
167
- if (typeof project?.projectDir !== 'string' || !project.projectDir.trim()) continue;
168
- try {
169
- const path = canonicalExistingDirectory(project.projectDir);
170
- if (path === realpathIfExists(DEFAULT_APP_DEV_ROOT)) continue;
171
- if (!byPath.has(path)) {
172
- byPath.set(path, {
173
- path,
174
- registeredAt: typeof project.lastMountedAt === 'string'
175
- ? project.lastMountedAt
176
- : new Date().toISOString(),
177
- });
178
- }
179
- } catch {
180
- // Deleted or inaccessible legacy projects are intentionally not migrated.
181
- }
182
- }
183
- rmSync(path, { force: true });
184
- return { version: APP_DEV_ROOTS_VERSION, roots: [...byPath.values()] };
185
- }
186
-
187
- function realpathIfExists(path) {
188
- try {
189
- return realpathSync(path);
190
- } catch {
191
- return resolve(path);
192
- }
193
- }
194
-
195
- export function readAppDevRoots(options = {}) {
196
- const filePath = rootsFile(options.filePath);
197
- let registry = readRaw(filePath);
198
- if (options.migrateLegacy !== false) {
199
- registry = withLock(filePath, () => {
200
- const before = readRaw(filePath);
201
- const after = migrateLegacyProjectsUnlocked(before, options);
202
- const changed = JSON.stringify(before.roots) !== JSON.stringify(after.roots);
203
- return changed ? writeRaw(after, filePath) : before;
204
- });
205
- }
206
- const implicit = realpathIfExists(options.defaultRoot || DEFAULT_APP_DEV_ROOT);
207
- const roots = [
208
- { path: implicit, registeredAt: null, implicit: true },
209
- ...registry.roots
210
- .filter((entry) => entry.path !== implicit)
211
- .map((entry) => ({ ...entry, implicit: false })),
212
- ];
213
- return { version: APP_DEV_ROOTS_VERSION, roots };
214
- }
215
-
216
- export function registerAppDevRoot(inputPath, options = {}) {
217
- const path = canonicalExistingDirectory(inputPath);
218
- const implicit = realpathIfExists(options.defaultRoot || DEFAULT_APP_DEV_ROOT);
219
- if (path === implicit) return readAppDevRoots(options);
220
- const filePath = rootsFile(options.filePath);
221
- withLock(filePath, () => {
222
- const registry = readRaw(filePath);
223
- if (!registry.roots.some((entry) => entry.path === path)) {
224
- registry.roots.push({ path, registeredAt: new Date().toISOString() });
225
- writeRaw(registry, filePath);
226
- }
227
- });
228
- return readAppDevRoots({ ...options, migrateLegacy: false });
229
- }
230
-
231
- export function removeAppDevRoot(inputPath, options = {}) {
232
- const path = realpathIfExists(resolve(String(inputPath || '')));
233
- const implicit = realpathIfExists(options.defaultRoot || DEFAULT_APP_DEV_ROOT);
234
- if (path === implicit) {
235
- throw usageError(`The default app development root cannot be removed: ${implicit}`);
236
- }
237
- const filePath = rootsFile(options.filePath);
238
- let removed = false;
239
- withLock(filePath, () => {
240
- const registry = readRaw(filePath);
241
- const next = registry.roots.filter((entry) => entry.path !== path);
242
- removed = next.length !== registry.roots.length;
243
- if (removed) writeRaw({ ...registry, roots: next }, filePath);
244
- });
245
- return { removed, path, registry: readAppDevRoots({ ...options, migrateLegacy: false }) };
246
- }
247
-
248
- function hasConfig(path) {
249
- return CONFIG_FILENAMES.some((name) => existsSync(join(path, name)));
250
- }
251
-
252
- function childAppDirs(parent) {
253
- if (!existsSync(parent)) return [];
254
- let entries = [];
255
- try {
256
- entries = readdirSync(parent, { withFileTypes: true });
257
- } catch {
258
- return [];
259
- }
260
- return entries
261
- .filter((entry) => entry.isDirectory())
262
- .filter((entry) => !entry.name.startsWith('.') && !entry.name.startsWith('_'))
263
- .filter((entry) => entry.name !== 'node_modules')
264
- .map((entry) => join(parent, entry.name))
265
- .filter(hasConfig);
266
- }
267
-
268
- export function discoverAppProjectsInRoot(inputRoot) {
269
- const root = realpathIfExists(resolve(inputRoot));
270
- const candidates = [];
271
- if (hasConfig(root)) candidates.push(root);
272
- candidates.push(...childAppDirs(root));
273
- candidates.push(...childAppDirs(join(root, 'apps')));
274
- return [...new Set(candidates.map(realpathIfExists))].sort();
275
- }
276
-
277
- export function discoverRegisteredAppProjects(options = {}) {
278
- const roots = readAppDevRoots(options).roots;
279
- return [...new Set(roots.flatMap((entry) => discoverAppProjectsInRoot(entry.path)))].sort();
280
- }
281
-
282
- export function getAppDevRootsFile(filePath) {
283
- return rootsFile(filePath);
284
- }