@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.
- package/README.md +11 -45
- package/config/notis_app_design_rules.json +135 -0
- package/dist/agent-hooks/notis-agent-hook.mjs +5180 -7281
- package/dist/base-skills/notis-apps/SKILL.md +141 -224
- package/dist/base-skills/notis-cli/SKILL.md +64 -131
- package/package.json +1 -2
- package/skills/notis-apps/cli.md +34 -95
- package/skills/notis-cli/AGENT_INSTRUCTIONS.md +1 -1
- package/src/command-specs/apps.js +326 -1562
- package/src/runtime/agent-browser.js +169 -1
- package/src/runtime/app-boundary-validator.js +221 -0
- package/src/runtime/app-platform.js +359 -233
- package/src/runtime/app-test-server.js +292 -0
- package/template/app/page.tsx +47 -45
- package/template/components/page-heading.tsx +23 -0
- package/template/components/ui/badge.tsx +7 -4
- package/template/components/ui/card.tsx +24 -11
- package/template/components/ui/native-select.tsx +24 -0
- package/template/notis.config.ts +0 -1
- package/template/package.json +2 -2
- package/template/packages/sdk/package.json +1 -2
- package/template/packages/sdk/src/components/MultiSelectActionBar.tsx +62 -8
- package/template/packages/sdk/src/components/Skeleton.tsx +24 -0
- package/template/packages/sdk/src/config.ts +0 -2
- package/template/packages/sdk/src/hooks/useCloudComputer.ts +15 -48
- package/template/packages/sdk/src/hooks/useDatabaseSchema.ts +17 -53
- package/template/packages/sdk/src/hooks/useDatabaseSubscription.ts +2 -2
- package/template/packages/sdk/src/hooks/useDocument.ts +12 -47
- package/template/packages/sdk/src/hooks/useDocuments.ts +18 -58
- package/template/packages/sdk/src/hooks/useQuery.ts +71 -0
- package/template/packages/sdk/src/hooks/useToolQuery.ts +12 -0
- package/template/packages/sdk/src/hooks/useTopBarSearch.ts +15 -7
- package/template/packages/sdk/src/index.ts +8 -0
- package/template/packages/sdk/src/interactions.ts +2 -1
- package/template/packages/sdk/src/queryCache.ts +162 -0
- package/template/packages/sdk/src/runtime.ts +5 -0
- package/template/packages/sdk/src/styles.css +28 -1
- package/src/runtime/app-dev-build-supervisor.js +0 -47
- package/src/runtime/app-dev-build.js +0 -41
- package/src/runtime/app-dev-consumers.js +0 -154
- package/src/runtime/app-dev-host-lock.js +0 -80
- package/src/runtime/app-dev-process-identity.js +0 -111
- package/src/runtime/app-dev-roots.js +0 -284
- package/src/runtime/app-dev-server.js +0 -1136
- package/src/runtime/app-dev-sessions.js +0 -185
- package/src/runtime/cli-mode.generated.js +0 -5
- package/src/runtime/cli-mode.js +0 -34
|
@@ -1,185 +0,0 @@
|
|
|
1
|
-
import { randomUUID } from 'node:crypto';
|
|
2
|
-
import { existsSync, lstatSync, mkdirSync, readFileSync, renameSync, rmSync, writeFileSync } from 'node:fs';
|
|
3
|
-
import { dirname, join } from 'node:path';
|
|
4
|
-
import { CONFIG_DIR } from './profiles.js';
|
|
5
|
-
|
|
6
|
-
export const DEFAULT_APP_DEV_SESSIONS_FILE = join(CONFIG_DIR, 'app-dev-sessions.json');
|
|
7
|
-
export const APP_DEV_SESSIONS_FILE = DEFAULT_APP_DEV_SESSIONS_FILE;
|
|
8
|
-
export const APP_DEV_SESSIONS_VERSION = 1;
|
|
9
|
-
const REGISTRY_LOCK_TIMEOUT_MS = 5_000;
|
|
10
|
-
const registryLockWait = new Int32Array(new SharedArrayBuffer(4));
|
|
11
|
-
|
|
12
|
-
function readRegistryLockOwner(lockPath) {
|
|
13
|
-
try {
|
|
14
|
-
return readFileSync(join(lockPath, 'owner'), 'utf8').trim();
|
|
15
|
-
} catch {
|
|
16
|
-
return null;
|
|
17
|
-
}
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
function withRegistryLock(filePath, callback) {
|
|
21
|
-
mkdirSync(dirname(filePath), { recursive: true, mode: 0o700 });
|
|
22
|
-
const lockPath = `${filePath}.lock`;
|
|
23
|
-
const ownerId = `${process.pid}.${randomUUID()}`;
|
|
24
|
-
const startedAt = Date.now();
|
|
25
|
-
while (true) {
|
|
26
|
-
try {
|
|
27
|
-
mkdirSync(lockPath, { mode: 0o700 });
|
|
28
|
-
try {
|
|
29
|
-
writeFileSync(join(lockPath, 'owner'), ownerId, { mode: 0o600 });
|
|
30
|
-
} catch (error) {
|
|
31
|
-
rmSync(lockPath, { recursive: true, force: true });
|
|
32
|
-
throw error;
|
|
33
|
-
}
|
|
34
|
-
break;
|
|
35
|
-
} catch (error) {
|
|
36
|
-
if (error?.code !== 'EEXIST') throw error;
|
|
37
|
-
try {
|
|
38
|
-
const lockStat = lstatSync(lockPath);
|
|
39
|
-
if (lockStat.isSymbolicLink() || !lockStat.isDirectory()) {
|
|
40
|
-
throw new Error(`Refusing unsafe app development registry lock: ${lockPath}`);
|
|
41
|
-
}
|
|
42
|
-
} catch (statError) {
|
|
43
|
-
if (statError?.code === 'ENOENT') continue;
|
|
44
|
-
throw statError;
|
|
45
|
-
}
|
|
46
|
-
if (Date.now() - startedAt >= REGISTRY_LOCK_TIMEOUT_MS) {
|
|
47
|
-
throw new Error(
|
|
48
|
-
`Timed out waiting for app development registry lock: ${filePath}. `
|
|
49
|
-
+ `If no app development process is running, remove the orphaned lock: ${lockPath}`,
|
|
50
|
-
);
|
|
51
|
-
}
|
|
52
|
-
Atomics.wait(registryLockWait, 0, 0, 10);
|
|
53
|
-
}
|
|
54
|
-
}
|
|
55
|
-
try {
|
|
56
|
-
return callback();
|
|
57
|
-
} finally {
|
|
58
|
-
try {
|
|
59
|
-
if (readRegistryLockOwner(lockPath) === ownerId) {
|
|
60
|
-
rmSync(lockPath, { recursive: true, force: true });
|
|
61
|
-
}
|
|
62
|
-
} catch {
|
|
63
|
-
// Never remove or disrupt a lock that no longer belongs to this writer.
|
|
64
|
-
}
|
|
65
|
-
}
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
export function getAppDevSessionsFile(filePath) {
|
|
69
|
-
if (filePath) {
|
|
70
|
-
return filePath;
|
|
71
|
-
}
|
|
72
|
-
return DEFAULT_APP_DEV_SESSIONS_FILE;
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
function normalizeSessions(raw) {
|
|
76
|
-
if (!raw || typeof raw !== 'object' || !Array.isArray(raw.sessions)) {
|
|
77
|
-
return [];
|
|
78
|
-
}
|
|
79
|
-
return raw.sessions
|
|
80
|
-
.filter((session) =>
|
|
81
|
-
session &&
|
|
82
|
-
typeof session === 'object' &&
|
|
83
|
-
typeof session.sessionId === 'string' &&
|
|
84
|
-
typeof session.appId === 'string' &&
|
|
85
|
-
typeof session.bundleBaseUrl === 'string',
|
|
86
|
-
)
|
|
87
|
-
.map((session) => {
|
|
88
|
-
if (typeof session.targetAppId === 'string' && session.targetAppId.trim()) {
|
|
89
|
-
return { ...session, targetAppId: session.targetAppId.trim() };
|
|
90
|
-
}
|
|
91
|
-
const { targetAppId: _targetAppId, ...rest } = session;
|
|
92
|
-
return rest;
|
|
93
|
-
});
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
export function readAppDevSessions(filePath) {
|
|
97
|
-
const resolvedFilePath = getAppDevSessionsFile(filePath);
|
|
98
|
-
if (!existsSync(resolvedFilePath)) {
|
|
99
|
-
return { version: APP_DEV_SESSIONS_VERSION, sessions: [] };
|
|
100
|
-
}
|
|
101
|
-
try {
|
|
102
|
-
const raw = JSON.parse(readFileSync(resolvedFilePath, 'utf8'));
|
|
103
|
-
return {
|
|
104
|
-
version: APP_DEV_SESSIONS_VERSION,
|
|
105
|
-
sessions: normalizeSessions(raw),
|
|
106
|
-
};
|
|
107
|
-
} catch {
|
|
108
|
-
return { version: APP_DEV_SESSIONS_VERSION, sessions: [] };
|
|
109
|
-
}
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
function writeAppDevSessionsUnlocked(registry, filePath) {
|
|
113
|
-
const resolvedFilePath = getAppDevSessionsFile(filePath);
|
|
114
|
-
mkdirSync(dirname(resolvedFilePath), { recursive: true, mode: 0o700 });
|
|
115
|
-
const normalized = {
|
|
116
|
-
version: APP_DEV_SESSIONS_VERSION,
|
|
117
|
-
sessions: normalizeSessions(registry),
|
|
118
|
-
};
|
|
119
|
-
const tempPath = `${resolvedFilePath}.${process.pid}.${randomUUID()}.tmp`;
|
|
120
|
-
writeFileSync(tempPath, JSON.stringify(normalized, null, 2), { mode: 0o600 });
|
|
121
|
-
renameSync(tempPath, resolvedFilePath);
|
|
122
|
-
return normalized;
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
export function writeAppDevSessions(registry, filePath) {
|
|
126
|
-
const resolvedFilePath = getAppDevSessionsFile(filePath);
|
|
127
|
-
return withRegistryLock(resolvedFilePath, () => writeAppDevSessionsUnlocked(registry, resolvedFilePath));
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
export function upsertAppDevSessions(sessions, filePath) {
|
|
131
|
-
const nextSessions = Array.isArray(sessions) ? sessions : [sessions];
|
|
132
|
-
const resolvedFilePath = getAppDevSessionsFile(filePath);
|
|
133
|
-
return withRegistryLock(resolvedFilePath, () => {
|
|
134
|
-
const registry = readAppDevSessions(resolvedFilePath);
|
|
135
|
-
const keys = new Set(nextSessions.map((session) => `${session.sessionId}:${session.appId}`));
|
|
136
|
-
registry.sessions = [
|
|
137
|
-
...registry.sessions.filter((session) => !keys.has(`${session.sessionId}:${session.appId}`)),
|
|
138
|
-
...nextSessions,
|
|
139
|
-
];
|
|
140
|
-
return writeAppDevSessionsUnlocked(registry, resolvedFilePath);
|
|
141
|
-
});
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
export function heartbeatAppDevSession(sessionId, lastHeartbeatAt, filePath) {
|
|
145
|
-
const resolvedFilePath = getAppDevSessionsFile(filePath);
|
|
146
|
-
return withRegistryLock(resolvedFilePath, () => {
|
|
147
|
-
const registry = readAppDevSessions(resolvedFilePath);
|
|
148
|
-
registry.sessions = registry.sessions.map((session) =>
|
|
149
|
-
session.sessionId === sessionId ? { ...session, lastHeartbeatAt } : session,
|
|
150
|
-
);
|
|
151
|
-
return writeAppDevSessionsUnlocked(registry, resolvedFilePath);
|
|
152
|
-
});
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
export function linkAppDevSessionTarget({ sessionId, appId, devSlug, targetAppId, lastHeartbeatAt = new Date().toISOString() }, filePath) {
|
|
156
|
-
const target = typeof targetAppId === 'string' ? targetAppId.trim() : '';
|
|
157
|
-
if (!target) {
|
|
158
|
-
return readAppDevSessions(filePath);
|
|
159
|
-
}
|
|
160
|
-
const resolvedFilePath = getAppDevSessionsFile(filePath);
|
|
161
|
-
const result = withRegistryLock(resolvedFilePath, () => {
|
|
162
|
-
const registry = readAppDevSessions(resolvedFilePath);
|
|
163
|
-
registry.sessions = registry.sessions.map((session) => {
|
|
164
|
-
const hasSelector = Boolean(appId || devSlug);
|
|
165
|
-
const matchesSession = !sessionId || session.sessionId === sessionId;
|
|
166
|
-
const matchesApp = !appId || session.appId === appId;
|
|
167
|
-
const matchesSlug = !devSlug || session.devSlug === devSlug;
|
|
168
|
-
if ((!hasSelector && !sessionId) || !matchesSession || !matchesApp || !matchesSlug) return session;
|
|
169
|
-
const linked = { ...session, targetAppId: target, lastHeartbeatAt };
|
|
170
|
-
return linked;
|
|
171
|
-
});
|
|
172
|
-
return writeAppDevSessionsUnlocked(registry, resolvedFilePath);
|
|
173
|
-
});
|
|
174
|
-
return result;
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
export function removeAppDevSession(sessionId, filePath) {
|
|
178
|
-
const resolvedFilePath = getAppDevSessionsFile(filePath);
|
|
179
|
-
const result = withRegistryLock(resolvedFilePath, () => {
|
|
180
|
-
const registry = readAppDevSessions(resolvedFilePath);
|
|
181
|
-
registry.sessions = registry.sessions.filter((session) => session.sessionId !== sessionId);
|
|
182
|
-
return writeAppDevSessionsUnlocked(registry, resolvedFilePath);
|
|
183
|
-
});
|
|
184
|
-
return result;
|
|
185
|
-
}
|
|
@@ -1,5 +0,0 @@
|
|
|
1
|
-
// Auto-generated on npm publish.
|
|
2
|
-
// Committed value is 'published' so the CLI defaults to the live Notis API.
|
|
3
|
-
// Localhost overrides come only from the worktree test lease (`./dev.sh`).
|
|
4
|
-
// scripts/set-cli-mode.js can rewrite this for publish/labeling experiments.
|
|
5
|
-
export const MODE = 'published';
|
package/src/runtime/cli-mode.js
DELETED
|
@@ -1,34 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* CLI mode detection for `notis apps dev`.
|
|
3
|
-
*
|
|
4
|
-
* The published npm CLI and the repo-local checkout both default to the live
|
|
5
|
-
* Notis API (`api.notis.ai` / `api-beta.notis.ai`). Localhost is not a CLI
|
|
6
|
-
* default — the `/notis-tests` worktree lease (`./dev.sh`) is the only
|
|
7
|
-
* supported loopback override for Notis developers.
|
|
8
|
-
*
|
|
9
|
-
* Mode is baked into `cli-mode.generated.js` at publish time. The
|
|
10
|
-
* `NOTIS_CLI_MODE` env var is honored for internal labeling only (e.g. the
|
|
11
|
-
* `apps` auto-dev loop inside `./dev.sh`).
|
|
12
|
-
*/
|
|
13
|
-
|
|
14
|
-
import { MODE as BAKED_MODE } from './cli-mode.generated.js';
|
|
15
|
-
|
|
16
|
-
export function getCliMode() {
|
|
17
|
-
const override = process.env.NOTIS_CLI_MODE;
|
|
18
|
-
if (override === 'local' || override === 'published') {
|
|
19
|
-
return override;
|
|
20
|
-
}
|
|
21
|
-
return BAKED_MODE === 'published' ? 'published' : 'local';
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
export function getDefaultApiBase(mode = getCliMode(), { beta = false } = {}) {
|
|
25
|
-
// Mode no longer switches the default API to localhost. Local loopback is
|
|
26
|
-
// reserved for the worktree test lease; `mode` only affects portal origin
|
|
27
|
-
// labeling for the in-repo apps-dev helper.
|
|
28
|
-
void mode;
|
|
29
|
-
return beta ? 'https://api-beta.notis.ai' : 'https://api.notis.ai';
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
export function getDefaultPortalOrigin(mode = getCliMode()) {
|
|
33
|
-
return mode === 'local' ? 'http://localhost:3000' : 'https://app.notis.ai';
|
|
34
|
-
}
|