@notis_ai/cli 0.2.0-beta.156.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 (35) 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 +5168 -7271
  4. package/dist/base-skills/notis-apps/SKILL.md +108 -194
  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 +322 -1560
  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 +45 -44
  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 +20 -7
  23. package/template/packages/sdk/src/config.ts +0 -2
  24. package/template/packages/sdk/src/interactions.ts +2 -1
  25. package/template/packages/sdk/src/styles.css +28 -1
  26. package/src/runtime/app-dev-build-supervisor.js +0 -47
  27. package/src/runtime/app-dev-build.js +0 -41
  28. package/src/runtime/app-dev-consumers.js +0 -154
  29. package/src/runtime/app-dev-host-lock.js +0 -80
  30. package/src/runtime/app-dev-process-identity.js +0 -111
  31. package/src/runtime/app-dev-roots.js +0 -284
  32. package/src/runtime/app-dev-server.js +0 -1136
  33. package/src/runtime/app-dev-sessions.js +0 -185
  34. package/src/runtime/cli-mode.generated.js +0 -5
  35. package/src/runtime/cli-mode.js +0 -34
@@ -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
- }