@notis_ai/cli 0.2.0-beta.151.1 → 0.2.0-beta.153.2
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/dist/agent-hooks/notis-agent-hook.mjs +76 -54
- package/dist/base-skills/notis-apps/SKILL.md +90 -29
- package/dist/base-skills/notis-cli/SKILL.md +80 -22
- package/package.json +1 -1
- package/src/command-specs/apps.js +70 -57
- package/src/runtime/sync-skills.js +20 -4
- package/template/packages/sdk/src/components/MultiSelectActionBar.tsx +7 -1
- package/template/packages/sdk/src/hooks/useCollectionInteractions.ts +23 -6
- package/template/packages/sdk/src/interactions/shortcuts.tsx +7 -1
|
@@ -14483,7 +14483,56 @@ ${problems.map((p) => ` - ${p}`).join("\n")}`);
|
|
|
14483
14483
|
desktopOwnerId: process.env.NOTIS_APPS_DEV_DESKTOP_OWNER_ID,
|
|
14484
14484
|
desktopOwnerScope: process.env.NOTIS_APPS_DEV_DESKTOP_OWNER_SCOPE
|
|
14485
14485
|
});
|
|
14486
|
-
let heartbeatTimer =
|
|
14486
|
+
let heartbeatTimer = null;
|
|
14487
|
+
let consumerTimer = null;
|
|
14488
|
+
let shuttingDown = false;
|
|
14489
|
+
const shutdown = async (signal) => {
|
|
14490
|
+
if (shuttingDown) return;
|
|
14491
|
+
shuttingDown = true;
|
|
14492
|
+
process.stdout.write(`
|
|
14493
|
+
[notis apps dev] stopping (${signal})...
|
|
14494
|
+
`);
|
|
14495
|
+
if (heartbeatTimer) {
|
|
14496
|
+
clearInterval(heartbeatTimer);
|
|
14497
|
+
heartbeatTimer = null;
|
|
14498
|
+
}
|
|
14499
|
+
if (consumerTimer) {
|
|
14500
|
+
clearInterval(consumerTimer);
|
|
14501
|
+
consumerTimer = null;
|
|
14502
|
+
}
|
|
14503
|
+
if (manualConsumerTimer) {
|
|
14504
|
+
clearInterval(manualConsumerTimer);
|
|
14505
|
+
manualConsumerTimer = null;
|
|
14506
|
+
}
|
|
14507
|
+
if (manualConsumerInstanceId) {
|
|
14508
|
+
try {
|
|
14509
|
+
removeAppDevConsumer(manualConsumerInstanceId);
|
|
14510
|
+
} catch {
|
|
14511
|
+
}
|
|
14512
|
+
}
|
|
14513
|
+
try {
|
|
14514
|
+
await devServer.close();
|
|
14515
|
+
} catch {
|
|
14516
|
+
}
|
|
14517
|
+
try {
|
|
14518
|
+
removeAppDevSession(sessionId, sessionsFilePath);
|
|
14519
|
+
} catch {
|
|
14520
|
+
}
|
|
14521
|
+
if (sourceHostLock) {
|
|
14522
|
+
releaseAppDevHostLock(sourceHostLock);
|
|
14523
|
+
sourceHostLock = null;
|
|
14524
|
+
}
|
|
14525
|
+
process.exit(EXIT_CODES.ok);
|
|
14526
|
+
};
|
|
14527
|
+
const handleSigint = () => {
|
|
14528
|
+
void shutdown("SIGINT");
|
|
14529
|
+
};
|
|
14530
|
+
const handleSigterm = () => {
|
|
14531
|
+
void shutdown("SIGTERM");
|
|
14532
|
+
};
|
|
14533
|
+
process.on("SIGINT", handleSigint);
|
|
14534
|
+
process.on("SIGTERM", handleSigterm);
|
|
14535
|
+
heartbeatTimer = setInterval(() => {
|
|
14487
14536
|
try {
|
|
14488
14537
|
heartbeatAppDevSession(sessionId, (/* @__PURE__ */ new Date()).toISOString(), sessionsFilePath);
|
|
14489
14538
|
} catch (error) {
|
|
@@ -14586,6 +14635,8 @@ ${problems.map((p) => ` - ${p}`).join("\n")}`);
|
|
|
14586
14635
|
}
|
|
14587
14636
|
}
|
|
14588
14637
|
if (apps.length === 0) {
|
|
14638
|
+
process.off("SIGINT", handleSigint);
|
|
14639
|
+
process.off("SIGTERM", handleSigterm);
|
|
14589
14640
|
clearInterval(heartbeatTimer);
|
|
14590
14641
|
heartbeatTimer = null;
|
|
14591
14642
|
try {
|
|
@@ -14616,7 +14667,6 @@ ${problems.map((p) => ` - ${p}`).join("\n")}`);
|
|
|
14616
14667
|
...liveDataWarnings(apps),
|
|
14617
14668
|
...versionPrecedenceWarnings(apps)
|
|
14618
14669
|
];
|
|
14619
|
-
let consumerTimer = null;
|
|
14620
14670
|
ctx.output.emitSuccess({
|
|
14621
14671
|
command: ctx.spec.command_path.join(" "),
|
|
14622
14672
|
data: {
|
|
@@ -14657,51 +14707,6 @@ ${problems.map((p) => ` - ${p}`).join("\n")}`);
|
|
|
14657
14707
|
"Press Ctrl-C to stop."
|
|
14658
14708
|
].join("\n")
|
|
14659
14709
|
});
|
|
14660
|
-
let shuttingDown = false;
|
|
14661
|
-
const shutdown = async (signal) => {
|
|
14662
|
-
if (shuttingDown) return;
|
|
14663
|
-
shuttingDown = true;
|
|
14664
|
-
process.stdout.write(`
|
|
14665
|
-
[notis apps dev] stopping (${signal})...
|
|
14666
|
-
`);
|
|
14667
|
-
if (heartbeatTimer) {
|
|
14668
|
-
clearInterval(heartbeatTimer);
|
|
14669
|
-
heartbeatTimer = null;
|
|
14670
|
-
}
|
|
14671
|
-
if (consumerTimer) {
|
|
14672
|
-
clearInterval(consumerTimer);
|
|
14673
|
-
consumerTimer = null;
|
|
14674
|
-
}
|
|
14675
|
-
if (manualConsumerTimer) {
|
|
14676
|
-
clearInterval(manualConsumerTimer);
|
|
14677
|
-
manualConsumerTimer = null;
|
|
14678
|
-
}
|
|
14679
|
-
if (manualConsumerInstanceId) {
|
|
14680
|
-
try {
|
|
14681
|
-
removeAppDevConsumer(manualConsumerInstanceId);
|
|
14682
|
-
} catch {
|
|
14683
|
-
}
|
|
14684
|
-
}
|
|
14685
|
-
try {
|
|
14686
|
-
await devServer.close();
|
|
14687
|
-
} catch {
|
|
14688
|
-
}
|
|
14689
|
-
try {
|
|
14690
|
-
removeAppDevSession(sessionId, sessionsFilePath);
|
|
14691
|
-
} catch {
|
|
14692
|
-
}
|
|
14693
|
-
if (sourceHostLock) {
|
|
14694
|
-
releaseAppDevHostLock(sourceHostLock);
|
|
14695
|
-
sourceHostLock = null;
|
|
14696
|
-
}
|
|
14697
|
-
process.exit(EXIT_CODES.ok);
|
|
14698
|
-
};
|
|
14699
|
-
process.on("SIGINT", () => {
|
|
14700
|
-
void shutdown("SIGINT");
|
|
14701
|
-
});
|
|
14702
|
-
process.on("SIGTERM", () => {
|
|
14703
|
-
void shutdown("SIGTERM");
|
|
14704
|
-
});
|
|
14705
14710
|
if (consumerMode === "machine" || consumerMode === "environment") {
|
|
14706
14711
|
consumerTimer = setInterval(() => {
|
|
14707
14712
|
if (!hasAppDevConsumer(readAppDevConsumers(), {
|
|
@@ -15199,13 +15204,19 @@ async function appsPullHandler(ctx) {
|
|
|
15199
15204
|
const appId = ctx.args.appId;
|
|
15200
15205
|
const result = await runToolCommand({
|
|
15201
15206
|
runtime: ctx.runtime,
|
|
15202
|
-
|
|
15203
|
-
|
|
15207
|
+
// Pull is source retrieval plus local link state. LIST_APPS is deliberately
|
|
15208
|
+
// non-materializing; GET_APP hydrates missing declared databases and would
|
|
15209
|
+
// turn a read-only pull into a remote mutation before build/verification.
|
|
15210
|
+
toolName: LIST_APPS_TOOL
|
|
15204
15211
|
});
|
|
15205
15212
|
if (ctx.runtime.credentialKind === "oauth" && !await ensureFreshOAuthCredential(ctx.runtime)) {
|
|
15206
15213
|
throw usageError("Pulling app source requires a current OAuth grant. Run `notis login` and retry.");
|
|
15207
15214
|
}
|
|
15208
|
-
const
|
|
15215
|
+
const apps = Array.isArray(result.payload?.apps) ? result.payload.apps : [];
|
|
15216
|
+
const app = apps.find((candidate) => (candidate?.app_id || candidate?.id) === appId);
|
|
15217
|
+
if (!app) {
|
|
15218
|
+
throw usageError(`App ${appId} is not accessible to the active profile.`);
|
|
15219
|
+
}
|
|
15209
15220
|
const defaultDir = slugify(app.slug) || slugify(app.name) || slugify(appId);
|
|
15210
15221
|
const targetDir = ctx.args.dir ? resolveProjectDir(ctx.args.dir) : defaultAppProjectDir(defaultDir);
|
|
15211
15222
|
const version = ctx.options.sourceVersion || "latest";
|
|
@@ -19711,6 +19722,20 @@ async function writeLockOwnerAtomically(lockDirectory, owner) {
|
|
|
19711
19722
|
await writeFile(temporaryOwnerPath, JSON.stringify(owner), { mode: 384 });
|
|
19712
19723
|
await rename(temporaryOwnerPath, ownerPath);
|
|
19713
19724
|
}
|
|
19725
|
+
async function releaseOwnedLock(lockDirectory, ownerId) {
|
|
19726
|
+
const owner = await lockSnapshot(lockDirectory);
|
|
19727
|
+
if (owner?.id !== ownerId) return;
|
|
19728
|
+
const quarantineRoot = join18(dirname17(lockDirectory), ".stale-operation-locks");
|
|
19729
|
+
const releasedDirectory = join18(quarantineRoot, `released.${ownerId}`);
|
|
19730
|
+
await mkdir(quarantineRoot, { recursive: true, mode: 448 });
|
|
19731
|
+
try {
|
|
19732
|
+
await rename(lockDirectory, releasedDirectory);
|
|
19733
|
+
} catch (error) {
|
|
19734
|
+
if (error?.code === "ENOENT") return;
|
|
19735
|
+
throw error;
|
|
19736
|
+
}
|
|
19737
|
+
await rm(releasedDirectory, { recursive: true, force: true });
|
|
19738
|
+
}
|
|
19714
19739
|
async function withSkillSyncLock(callback, {
|
|
19715
19740
|
home = homedir10(),
|
|
19716
19741
|
timeoutMs = DEFAULT_LOCK_TIMEOUT_MS,
|
|
@@ -19780,10 +19805,7 @@ async function withSkillSyncLock(callback, {
|
|
|
19780
19805
|
heartbeatStopped = true;
|
|
19781
19806
|
clearInterval(heartbeat);
|
|
19782
19807
|
await heartbeatInFlight;
|
|
19783
|
-
|
|
19784
|
-
if (owner?.id === ownerId) {
|
|
19785
|
-
await rm(lockDirectory, { recursive: true, force: true });
|
|
19786
|
-
}
|
|
19808
|
+
await releaseOwnedLock(lockDirectory, ownerId);
|
|
19787
19809
|
}
|
|
19788
19810
|
}
|
|
19789
19811
|
async function reconcileAllSkills({
|
|
@@ -23,10 +23,33 @@ All Notis apps are built using the Notis CLI, either locally in a repo workspace
|
|
|
23
23
|
## App Workspace Tool Rules
|
|
24
24
|
|
|
25
25
|
- Apps are the top-level packaging unit in Notis.
|
|
26
|
-
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
26
|
+
- Choose the execution path before changing an app. A prompt that says the
|
|
27
|
+
shell is a hosted/Vercel sandbox, or a shell rooted at `/vercel/sandbox`, is
|
|
28
|
+
the **hosted sandbox** path. A shell on the user's computer with Notis
|
|
29
|
+
Desktop available is the **local Desktop** path.
|
|
30
|
+
- In a hosted sandbox, do not run `apps dev`: the user's Desktop cannot mount
|
|
31
|
+
that sandbox filesystem. Unless the user explicitly requests preview-only,
|
|
32
|
+
read-only, or no deployment, a request to create or edit an app authorizes
|
|
33
|
+
deploying that app to the user's Workspace after `apps build` and automated
|
|
34
|
+
`apps verify` pass. An opt-out stops after those tests with no remote app
|
|
35
|
+
create/link, database mutation, deploy, or post-deploy checks. Pulling an existing app provides its exact link. For a new
|
|
36
|
+
app, test first, then reconcile profile state and `apps list --json` against
|
|
37
|
+
the canonical `notis.config.ts` `name` and intended personal/team scope: link
|
|
38
|
+
one exact editable non-development match after a metadata-only
|
|
39
|
+
(`include_documents: false`) detail read proves scope, fail on ambiguity or
|
|
40
|
+
scope mismatch, or create only when none exists. New CLI-created apps default
|
|
41
|
+
to personal scope. Before creation, prove that canonicalizing the config
|
|
42
|
+
`title` yields the config `name`. For personal scope, run `apps create
|
|
43
|
+
"<canonical-config-title>" . --json` exactly once. For explicitly requested
|
|
44
|
+
team scope, discover and inspect `LOCAL_NOTIS_CREATE_APP`, dry-run it, execute
|
|
45
|
+
it exactly once with team visibility and the verified current team scope,
|
|
46
|
+
verify the returned id/slug/team scope/edit permission, then `apps link` that
|
|
47
|
+
exact id. Stop for read-only
|
|
48
|
+
reconciliation if creation is ambiguous or outcome-unknown. In the local
|
|
49
|
+
Desktop path, use `apps dev [folder]`, let the user test the DEV app, and deploy
|
|
50
|
+
that development identity directly only after an explicit request. Use
|
|
51
|
+
`LOCAL_NOTIS_CREATE_APP` only for a hosted team-scoped creation or another
|
|
52
|
+
non-CLI administrative flow that explicitly requires a server-side app row.
|
|
30
53
|
- Use `LOCAL_NOTIS_UPDATE_APP` to update app metadata.
|
|
31
54
|
- Use `LOCAL_NOTIS_LIST_APPS` to discover the user's apps.
|
|
32
55
|
- The full app lifecycle uses the CLI in the shell. Always run it through the registry-resolved package, for example `npx --package @notis_ai/cli@latest -- notis apps init`; use the same prefix for `build` and `deploy`. In hosted shells, the CLI is pre-authenticated through `NOTIS_JWT`.
|
|
@@ -97,8 +120,8 @@ App code never accesses the runtime directly -- it uses SDK hooks (`useTool`, `u
|
|
|
97
120
|
12. **Portal-owned sidebars stay portal-owned** -- If a route uses `collection.sidebar`, treat that sidebar as platform chrome. Do not remove it, recreate it inside app JSX, or replace it with a custom in-app folder rail.
|
|
98
121
|
13. **Portal globals are off-limits** -- Never use `window.__NOTIS_RUNTIME__`, query portal-owned DOM hooks, or create global DOM portals.
|
|
99
122
|
14. **Prefer inline optimistic edits** -- Rename-like edits for collections, app-owned rows, and sidebar-backed entities should use inline editing with an optimistic UI update, then roll back on backend failure. Use modals only when the edit requires multiple fields or destructive confirmation.
|
|
100
|
-
15. **
|
|
101
|
-
16. **Installed app identity is exact and
|
|
123
|
+
15. **The execution environment determines the deploy gate** -- In the local Desktop path, run `apps dev [folder]`, let the **user** test the automatically mounted DEV app, and do not deploy until the user asks; first deploy promotes that `dev_app_id` directly, so never create a second app first. In a hosted sandbox, `apps dev` cannot reach the user's Desktop; bootstrap `agent-browser`, build and verify first, then resolve exact identity/resources and deploy to the user's Workspace, verify the remote version and live runtime, and return the exact Portal URL. Automatic deployment is the default for create/edit requests only; an explicit preview-only, read-only, or no-deploy request wins. This standing sandbox authorization does not authorize Store submission.
|
|
124
|
+
16. **Installed app identity is exact, editable, and scope-proven** -- Validate an explicit persisted link for this API/user profile before using it. Otherwise inspect every accessible exact-canonical-slug row, including development rows; link only one editable non-development candidate whose exact detail proves the intended personal/team scope. Fail closed on a development collision, multiple matches, missing scope proof, or scope mismatch, and never infer identity from display name. After first install, keep the validated profile-scoped link so Portal and CLI update the same app instead of creating duplicates.
|
|
102
125
|
17. **Development identities stay separate** -- `.notis/state.json` uses `dev_app_id` for the hidden development-runtime row and `app_id` only for an accessible installed workspace app, scoped under the authenticated environment. Never pass a runtime app whose manifest has `is_dev: true` to `notis apps link`.
|
|
103
126
|
18. **Automatic mounts are multi-instance and least-authority** -- Prod, Beta, and source-development Desktop instances may mount the same source simultaneously with independent authenticated runtimes. Automatic mounting never grants capabilities: reuse existing grants and leave restricted capabilities denied until approved. Consumer leases expire after crashes so the shared host exits after the last live instance. There are no offline rows or manual start/stop controls.
|
|
104
127
|
19. **Store submission is user-gated** -- Run `apps publish --confirm-ready` only after the user explicitly confirms the current App Details page and Store listing are ready. Deploy the exact approved local state first. The command must reject missing confirmation, incomplete listing media, a local/deployed version mismatch, private visibility, or an existing pending review.
|
|
@@ -107,6 +130,7 @@ App code never accesses the runtime directly -- it uses SDK hooks (`useTool`, `u
|
|
|
107
130
|
22. **Database rows are private unless explicitly seeded** -- A string declaration such as `databases: ['notes']` publishes schema only and never includes the developer's rows. Use `{ slug: 'templates', seedDocuments: true }` only for small, intentional starter content that every installer should receive. Never enable it for user-created notes, history, leads, or other personal data.
|
|
108
131
|
23. **Public submissions are complete, reviewable packages** -- The registry PR must contain the full editable source tree, Store assets, exact source-declared database schemas, and only explicitly seeded starter rows. Registry CI validates those boundaries before merge; do not hand-edit `notis-listing.json` or strip source files to make a check pass. Fix the app locally, redeploy, and resubmit.
|
|
109
132
|
24. **New projects default to `~/.notis/apps/<slug>`, and `[dir]` overrides it** -- `apps init` and `apps pull` use this stable, predictable home unless the app belongs in a specific repository, monorepo, or user-chosen location. In those cases, pass `[dir]` and report the resulting path. Do not nest an app inside a directory whose local workspace metadata selects an unrelated Notis runtime or profile: later CLI calls inherit that routing and may target the wrong environment.
|
|
133
|
+
25. **Machine names and display titles use different casing** -- In `notis.config.ts`, `name` is the stable machine identity and must be lowercase kebab-case (`name: 'link-building'`). `title` is the human-facing app name and must use deliberate display casing (`title: 'Link Building'`), preserving product spelling and acronyms such as `Notis` and `SEO`. Never put a title-cased phrase in `name`, never show a raw slug as the title, and never change an existing canonical `name` or remote slug merely to repair display casing. The persisted `apps.name`, Workspace sidebar, App Details, and Store listing must use `title`.
|
|
110
134
|
|
|
111
135
|
## Anti-patterns -- NEVER do these
|
|
112
136
|
|
|
@@ -115,7 +139,7 @@ These are the most common mistakes agents make. Each one wastes time and produce
|
|
|
115
139
|
- **NEVER assume app deploys create databases for you** -- Create or update databases through native Notis database tools or the assistant first, then reference them by slug in `notis.config.ts`. Database creation requires the owning app to exist: pass its slug or id in the `app` argument of `LOCAL_NOTIS_DATABASE_UPSERT_DATABASE` (create the app first with `LOCAL_NOTIS_CREATE_APP` if needed). A database can only be referenced by the app that owns it.
|
|
116
140
|
- **NEVER bypass the supported workflow by manually stitching together low-level save or lint calls from a local workspace** -- Local agents should go through the NPX Notis CLI for `apps pull`, `apps dev`, `apps build`, `apps verify`, `apps create`, `apps link`, and `apps deploy`.
|
|
117
141
|
- **NEVER use `apps pull` to clone a Store listing** -- `npx --package @notis_ai/cli@latest -- notis apps pull` only pulls source for an app the user can already access as an installed app. To fork a published Store app, run `npx --package @notis_ai/cli@latest -- notis apps init "My App" --from <slug>` instead: it downloads that app's source from the public registry, and installing the app first is not required.
|
|
118
|
-
- **NEVER deploy
|
|
142
|
+
- **NEVER apply the local deploy gate to a hosted sandbox** -- On the user's local computer, a clean `apps build` + `apps verify` is not deploy consent: hand off the DEV app and wait. In a hosted sandbox, the user's create or edit request is deploy consent for that app because `apps dev` cannot reach their Desktop; deploy only after both commands pass, then verify the remote version. Neither path authorizes Store submission.
|
|
119
143
|
- **NEVER submit without explicit approval** -- A deploy request alone does not authorize Store submission. Run `npx --package @notis_ai/cli@latest -- notis apps publish --confirm-ready` only when the user confirms App Details is ready for Store review.
|
|
120
144
|
- **NEVER write raw `views/<slug>/index.js` files** -- Write standard React pages in `app/`.
|
|
121
145
|
- **NEVER invent `npx --package @notis_ai/cli@latest -- notis apps push` or bypass the review flow** -- Source moves through `apps pull` and `apps deploy`; `apps publish --confirm-ready` submits the deployed snapshot through the same authenticated review endpoint as App Details.
|
|
@@ -131,22 +155,24 @@ These are the most common mistakes agents make. Each one wastes time and produce
|
|
|
131
155
|
|
|
132
156
|
1. **Find a starting point.** Run `npx --package @notis_ai/cli@latest -- notis apps scaffolds list` (add `--search <term>` to filter) to list the published Store apps. If something close matches, run `npx --package @notis_ai/cli@latest -- notis apps init "My App" --from <slug>` to download that app's source from the registry. Only run plain `notis apps init "My App"` when no published app fits. Either way the project lands in `~/.notis/apps/<slug>`; add a `[dir]` argument when the user wants it somewhere else (a tracked git repo, an existing monorepo), and report the path you used.
|
|
133
157
|
2. **Pull your own apps; fork Store apps with `--from`.** `apps pull` is for apps the user already has installed or deployed: run `npx --package @notis_ai/cli@latest -- notis apps list`, preserve any local edits in the target directory, then run `npx --package @notis_ai/cli@latest -- notis apps pull <app-id>` (lands in `~/.notis/apps/<app-slug>`; pass a `[dir]` argument to place it elsewhere). A pull reproduces the installed release, so increment `package.json` `notisAppVersion` above that release before `apps dev`; until then the online bundle remains active. To fork a published Store app, use `apps init --from <slug>` instead -- it downloads the source from the registry and does not require installing the app first.
|
|
134
|
-
3. **Edit the listing source.**
|
|
158
|
+
3. **Edit the listing source.** In `notis.config.ts`, set `name` to the stable lowercase kebab-case identity and set `title` to the correctly cased human-facing name; for example, `name: 'link-building'` with `title: 'Link Building'`. Treat acronym and brand casing as editorial input, not something to derive mechanically from the slug. Then update description, icon, accent, author, categories, tagline, databases, routes, and tools. Declare a database as a string for schema-only Store packaging; use `{ slug: 'templates', seedDocuments: true }` only when its rows are deliberate starter content for every installer. Keep the complete Store release history in the root `CHANGELOG.md`, newest entry first, using `## [Release title] - YYYY-MM-DD` (or `{PR_MERGE_DATE}` before publication). The first entry powers **What’s New** and the same file powers **Version History**. `icon` is a `phosphor:<name>` value or `metadata/icon.png`; when unset the app shows its **two-letter initials** everywhere (store, sidebar, app details). `accent` optionally pins the avatar color to one of `blue|violet|emerald|amber|rose|sky|fuchsia|teal` (default derived from the app id). Icon/accent flow through deploy onto the app row + listing and can also be set later via the `update_app` tool.
|
|
135
159
|
4. **Build pages in `app/`.** Reuse scaffold code wherever it fits.
|
|
136
|
-
5. **
|
|
137
|
-
6. **
|
|
138
|
-
7. **Verify locally.** Run `npm install`, then `npx --package @notis_ai/cli@latest -- notis apps build` and `npx --package @notis_ai/cli@latest -- notis apps verify`. Surface the verify report and fix failures. Incomplete listing media is only a `Store readiness:` warning there; run `notis apps verify --listing` before publish to make it a failure.
|
|
139
|
-
8. **Local-development-first handoff — STOP HERE.** Hand off after the user can see and test the app in its DEV-badged Workspace row. Building a new app to this point, without deploying, is a **complete and expected** result. Do NOT proceed to `apps create` / `apps deploy` yet. **Before handing off, complete all three acceptance checks:**
|
|
160
|
+
5. **Test the source before remote mutation.** Before changing a linked installed app, increment `package.json` `notisAppVersion` above the installed release. In a fresh hosted sandbox, bootstrap Agent Browser with `npm exec --yes --package agent-browser@latest -- agent-browser install`. Generate configured screenshots; use `theme: 'dark'` or `theme: 'light'` where appropriate and reserve screenshot `--raw` for diagnostics. Run `npm install`, run `npx --package @notis_ai/cli@latest -- notis apps build`, then run `npx --yes --package @notis_ai/cli@latest --package agent-browser@latest -- notis apps verify` in the sandbox (or the normal NPX verification command locally). Fix every failure. Do not create an app, mutate a database, or deploy before both checks pass. `--no-browser` is manual triage, not a passing automated gate.
|
|
161
|
+
6. **Finish the local Desktop path at the DEV handoff.** Run `npx --package @notis_ai/cli@latest -- notis apps dev [folder]`; it creates the development identity and materializes available scaffold database snapshots without requiring a hosted app id. Hand off after the user can see and test the app in its DEV-badged Workspace row. Do not deploy until the user asks, and then run `apps deploy` directly so the existing `dev_app_id` is promoted in place; never run `apps create` after `apps dev`. **Before handing off, complete all three acceptance checks:**
|
|
140
162
|
1. Root: `apps roots list` contains the intended folder (or the app is under the implicit default root).
|
|
141
163
|
2. Bundle: the loopback `/snapshot` responds successfully and contains the expected manifest/routes.
|
|
142
164
|
3. Mount and render: the app appears exactly once with a compact `DEV` badge and its default route renders. For multi-instance work, verify each requested Desktop independently.
|
|
143
165
|
See Troubleshooting → *App is missing from the sidebar* if any check fails.
|
|
144
|
-
|
|
166
|
+
7. **Gate and resolve one hosted identity after tests pass.** If the request is preview-only, read-only, or no-deploy, stop after step 5: do not create or link an app, mutate a database, deploy, or run post-deploy checks. Otherwise, existing edits keep the exact profile-scoped id linked by `apps pull`, after a metadata-only (`include_documents: false`) app-detail read validates its edit permission and scope without materializing databases. For a new hosted app, default the intended scope to personal unless the user explicitly requests team scope, inspect `.notis/state.json`, and run `apps list --json`. Consider every accessible exact canonical-slug row, including development rows. Link only one editable, non-development candidate whose metadata-only exact app-detail read proves the intended scope; fail on multiple matches, development-row collisions, missing scope proof, or scope mismatch. Create only when there are zero exact-slug rows. First prove that lowercasing the config `title`, replacing non-alphanumeric runs with `-`, and trimming hyphens yields the config `name`. For personal scope, run `npx --package @notis_ai/cli@latest -- notis apps create "<canonical-config-title>" . --json` exactly once and verify the returned id, remote slug, edit permission, and personal scope. For explicitly requested team scope, use `notis tools search` to discover the team-capable app-creation tool, inspect its schema, dry-run it, then execute `LOCAL_NOTIS_CREATE_APP` exactly once with the canonical display title, `visibility: "team"`, and the exact current `team_id` when resolved. Verify the result's id, canonical slug, `team_id`, team visibility, and edit permission, then run `notis apps link <returned-id> .` before database reconciliation. Never retry an outcome-unknown create; reconcile read-only and stop on ambiguity or any returned identity/scope mismatch.
|
|
167
|
+
8. **Reconcile hosted database schemas safely.** Read the exact app detail and current schemas first; mutate only missing or changed declarations. For creation, pass the exact app id in the database tool's `app` argument. For an update, resolve the exact `database_id`, verify its `owner_app_id` equals the linked app id, update by that `database_id`, then read back slug, owner, and schema. Apply only backward-compatible schema expansion before deployment. Stage breaking or destructive changes through an expand-contract sequence and obtain the required destructive approval; never make the currently deployed bundle incompatible before its replacement is live.
|
|
168
|
+
9. **Deploy and prove the hosted sandbox result.** Use only the exact id established in step 7. Run `npx --package @notis_ai/cli@latest -- notis apps deploy`, read the matching row back with `npx --package @notis_ai/cli@latest -- notis apps list --json`, confirm its id and deployed version, then run `npx --yes --package @notis_ai/cli@latest --package agent-browser@latest -- notis apps verify --mode live`. Return that row's exact profile-appropriate `portal_url` only after every proof passes. Report state precisely: a definite pre-commit rejection is **tested but not deployed**; a timeout/network/incomplete mutation response is **tested, deployment outcome unknown**; a confirmed deploy followed by failed readback is **deployed but not remotely verified**; a failed live check is **deployed but live verification failed**. Never retry an outcome-unknown mutation.
|
|
145
169
|
10. **Submit only after confirmation.** When the user explicitly confirms the current App Details page is ready, ensure the approved state is deployed, then run `npx --package @notis_ai/cli@latest -- notis apps publish --confirm-ready`. The command submits Team apps immediately or opens the Public Store registry review PR. Without that confirmation, stop after deploy.
|
|
146
170
|
|
|
147
171
|
### Quick start
|
|
148
172
|
|
|
149
|
-
|
|
173
|
+
Choose the local or hosted finish after verification. Local deployment is
|
|
174
|
+
user-gated; hosted-sandbox deployment is the default for app create or edit
|
|
175
|
+
tasks unless the user explicitly requests preview-only, read-only, or no deploy.
|
|
150
176
|
|
|
151
177
|
```bash
|
|
152
178
|
# 1. Pick a published Store app as the scaffold (catalog comes from the public registry)
|
|
@@ -157,22 +183,39 @@ npx --package @notis_ai/cli@latest -- notis apps init "My App" --from <slug>
|
|
|
157
183
|
cd ~/.notis/apps/my-app
|
|
158
184
|
npm install
|
|
159
185
|
|
|
160
|
-
# 2.
|
|
161
|
-
#
|
|
162
|
-
# sidebar group. This is the finish line for a build request.
|
|
186
|
+
# 2. LOCAL COMPUTER: register with Desktop and iterate. After the user approves
|
|
187
|
+
# deployment, run deploy directly to promote the existing dev_app_id.
|
|
163
188
|
npx --package @notis_ai/cli@latest -- notis apps dev
|
|
164
|
-
# ...
|
|
165
|
-
|
|
166
|
-
# 3. Build, capture listing screenshots, and verify (still local — no deploy)
|
|
189
|
+
# ... user tests the DEV-badged app ...
|
|
167
190
|
npx --package @notis_ai/cli@latest -- notis apps build
|
|
168
191
|
npx --package @notis_ai/cli@latest -- notis apps screenshot
|
|
169
192
|
npx --package @notis_ai/cli@latest -- notis apps verify
|
|
193
|
+
npx --package @notis_ai/cli@latest -- notis apps deploy
|
|
194
|
+
```
|
|
170
195
|
|
|
171
|
-
|
|
172
|
-
|
|
196
|
+
Hosted sandbox finish — never run `apps dev`. Test first, then reconcile the
|
|
197
|
+
exact app identity and changed databases before deployment:
|
|
198
|
+
|
|
199
|
+
```bash
|
|
200
|
+
npm exec --yes --package agent-browser@latest -- agent-browser install
|
|
201
|
+
npx --yes --package @notis_ai/cli@latest --package agent-browser@latest -- notis apps screenshot
|
|
202
|
+
npx --package @notis_ai/cli@latest -- notis apps build
|
|
203
|
+
npx --yes --package @notis_ai/cli@latest --package agent-browser@latest -- notis apps verify
|
|
204
|
+
# New unlinked app only: reconcile exact canonical slug with apps list. If no
|
|
205
|
+
# match exists, create once with the config title and verify the returned slug.
|
|
206
|
+
npx --package @notis_ai/cli@latest -- notis apps list --json
|
|
207
|
+
# Before create, prove canonicalize(config.title) == config.name.
|
|
208
|
+
npx --package @notis_ai/cli@latest -- notis apps create "<canonical-config-title>" . --json
|
|
209
|
+
# Compare schemas, then create only missing databases or update changed ones by
|
|
210
|
+
# verified database_id and read back owner/schema before continuing.
|
|
173
211
|
npx --package @notis_ai/cli@latest -- notis apps deploy
|
|
212
|
+
npx --package @notis_ai/cli@latest -- notis apps list --json
|
|
213
|
+
npx --yes --package @notis_ai/cli@latest --package agent-browser@latest -- notis apps verify --mode live
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
Store submission on either path remains a separate approval-gated action:
|
|
174
217
|
|
|
175
|
-
|
|
218
|
+
```bash
|
|
176
219
|
npx --package @notis_ai/cli@latest -- notis apps publish --confirm-ready
|
|
177
220
|
```
|
|
178
221
|
|
|
@@ -184,7 +227,7 @@ npx --package @notis_ai/cli@latest -- notis apps link <app-id> .
|
|
|
184
227
|
npx --package @notis_ai/cli@latest -- notis apps deploy
|
|
185
228
|
```
|
|
186
229
|
|
|
187
|
-
Or if editing an installed app:
|
|
230
|
+
Or if editing an installed app locally:
|
|
188
231
|
|
|
189
232
|
```bash
|
|
190
233
|
npx --package @notis_ai/cli@latest -- notis apps list
|
|
@@ -200,12 +243,20 @@ npx --package @notis_ai/cli@latest -- notis apps link <installed-app-id> .
|
|
|
200
243
|
npx --package @notis_ai/cli@latest -- notis apps deploy
|
|
201
244
|
```
|
|
202
245
|
|
|
246
|
+
In a hosted sandbox, use the same pull/build/verify sequence but omit `apps
|
|
247
|
+
dev`; bootstrap `agent-browser`, materialize any new or changed database schemas
|
|
248
|
+
against the exact linked id, deploy automatically after verification, read back
|
|
249
|
+
the exact app id/version and `portal_url` with `apps list --json`, run live verification, and return that
|
|
250
|
+
exact Portal URL. Never run `apps publish --confirm-ready` without separate
|
|
251
|
+
Store approval.
|
|
252
|
+
|
|
203
253
|
## Building an App
|
|
204
254
|
|
|
205
255
|
### Step 1: Define the config
|
|
206
256
|
|
|
207
257
|
Create `notis.config.ts` with:
|
|
208
|
-
- **name** --
|
|
258
|
+
- **name** -- Stable machine identity in lowercase kebab-case, such as `link-building`; do not use display casing here
|
|
259
|
+
- **title** -- Human-facing app name with deliberate casing, such as `Link Building`; preserve brands and acronyms exactly
|
|
209
260
|
- **databases** -- Slug references to existing Notis databases
|
|
210
261
|
- **routes** -- Route-first sidebar entries with explicit `slug`, optional `parentSlug`, and optional `collection.sidebar` tree config
|
|
211
262
|
- **tools** -- Final tool names the app can call at runtime. Use the shared discovery flow (`COMPOSIO_SEARCH_TOOLS`, then `COMPOSIO_GET_TOOL_SCHEMAS`) while building the app, and copy the returned final names into this list. Examples include `LOCAL_NOTIS_DATABASE_QUERY`, `LOCAL_NOTIS_MONID_RUN`, `GMAIL_SEND_EMAIL`, `LOCAL_POSTFORME_CREATE_POST`, and `LOCAL_MCP_<SERVER>_<TOOL>`. App code calls each declared name directly through `useTool`; it does not wrap provider or MCP calls in `COMPOSIO_MULTI_EXECUTE_TOOL`. Access stays scoped to the signed-in user's own connections, native database tools stay scoped to the app's databases unless `capabilities.workspaceDatabases: 'read'` is granted, and metered tools use the CLI-equivalent credit-cap and fail-closed usage-billing path.
|
|
@@ -344,7 +395,7 @@ Generated by `npx --package @notis_ai/cli@latest -- notis apps build` at `.notis
|
|
|
344
395
|
{
|
|
345
396
|
"version": 1,
|
|
346
397
|
"spec_version": 4,
|
|
347
|
-
"app": { "name": "My App", "description": "...", "icon": "phosphor:..." },
|
|
398
|
+
"app": { "name": "My App", "slug": "my-app", "title": "My App", "description": "...", "icon": "phosphor:..." },
|
|
348
399
|
"routes": [
|
|
349
400
|
{
|
|
350
401
|
"path": "/",
|
|
@@ -506,6 +557,16 @@ const result = await queryTasks.call({ database_id: 'tasks-db-id', query: { page
|
|
|
506
557
|
|
|
507
558
|
## Development Modes
|
|
508
559
|
|
|
560
|
+
### Hosted sandbox development
|
|
561
|
+
|
|
562
|
+
Do not run `apps dev` in a hosted sandbox. The sandbox filesystem is not on the
|
|
563
|
+
user's computer, so Desktop cannot mount it. Bootstrap sandbox `agent-browser`,
|
|
564
|
+
then build and verify before any remote mutation. For a create or edit request,
|
|
565
|
+
unless the user explicitly says preview-only, read-only, or no-deploy, resolve
|
|
566
|
+
one exact identity, safely materialize only missing or changed database schemas,
|
|
567
|
+
deploy, read back the exact app id/version, run live verification, and return
|
|
568
|
+
the exact Portal URL. Inspection, review, and diagnosis remain read-only.
|
|
569
|
+
|
|
509
570
|
### Canonical local development
|
|
510
571
|
|
|
511
572
|
```bash
|
|
@@ -517,13 +578,13 @@ Runs the real desktop-local development workflow. The CLI should discover all ap
|
|
|
517
578
|
## Testing
|
|
518
579
|
|
|
519
580
|
1. **Build validation**: `npx --package @notis_ai/cli@latest -- notis apps build` must succeed without errors. Vite surfaces TypeScript and bundling errors during this step.
|
|
520
|
-
2. **Headless render verification** (recommended after every build): run `npx --package @notis_ai/cli@latest -- notis apps verify`. It builds unless `--skip-build` is passed, spins up a loopback harness, drives `agent-browser` against every route, and reports per-route pass/fail with captured render errors and runtime calls.
|
|
581
|
+
2. **Headless render verification** (recommended after every build): run `npx --package @notis_ai/cli@latest -- notis apps verify` locally. In a hosted sandbox, first run `npm exec --yes --package agent-browser@latest -- agent-browser install`, then run `npx --yes --package @notis_ai/cli@latest --package agent-browser@latest -- notis apps verify`. It builds unless `--skip-build` is passed, spins up a loopback harness, drives `agent-browser` against every route, and reports per-route pass/fail with captured render errors and runtime calls.
|
|
521
582
|
3. **Local development acceptance**: Run `notis apps dev [folder]` once to register the root, then verify each signed-in Desktop instance independently. For an unpublished app, expect one DEV-badged Workspace row. For a linked app, first confirm local `notisAppVersion` is strictly greater than installed `release_version`, then expect one substituted DEV-badged row; equal or lower must keep the online row and bundle. Verify the default route renders and live edits appear without restarting the CLI or Desktop. Use `notis apps roots list` as the persistence proof. Loopback bundle health alone does not prove that an authenticated instance mounted or rendered the app.
|
|
522
|
-
4. **Post-deploy**:
|
|
583
|
+
4. **Post-deploy**: Read back the exact app id, version, and `portal_url` with `apps list --json`, run `apps verify --mode live`, verify the deployed bundle via `/portal_views/get` -> `runtime_descriptor.bundle.js_url`, and return that profile-appropriate exact Portal URL. A confirmed deploy followed by failed readback is deployed but not remotely verified; a failed live check is deployed but live verification failed. The portal renders app bundles directly as React components, so navigate to the app page when an authenticated browser is available.
|
|
523
584
|
|
|
524
585
|
### Headless harness verification
|
|
525
586
|
|
|
526
|
-
Run `npx --package @notis_ai/cli@latest -- notis apps verify` after `npx --package @notis_ai/cli@latest -- notis apps build`. Use `--mode live` after deploy to exercise the real `/portal_views/runtime_query` with the CLI JWT instead of stub data; live mode also fails a route whose runtime calls all errored, which a well-behaved error state would otherwise hide.
|
|
587
|
+
Run `npx --package @notis_ai/cli@latest -- notis apps verify` after `npx --package @notis_ai/cli@latest -- notis apps build`. Use `--mode live` after deploy to exercise the real `/portal_views/runtime_query` with the CLI JWT instead of stub data; live mode also fails a route whose runtime calls all errored, which a well-behaved error state would otherwise hide. In a hosted sandbox, put `agent-browser` on the verification process's `PATH` with the combined-package command above. `--no-browser` only prints URLs for manual triage and does not satisfy the automated deployment gate.
|
|
527
588
|
|
|
528
589
|
#### What the harness catches that `npx --package @notis_ai/cli@latest -- notis apps build` does not
|
|
529
590
|
|
|
@@ -73,43 +73,99 @@ Treat the Notis CLI the same way you would treat a Composio-style tool router fl
|
|
|
73
73
|
|
|
74
74
|
## Section 1: Developing Notis Apps
|
|
75
75
|
|
|
76
|
-
Use this section when the goal is to create or update a Notis app from a local
|
|
76
|
+
Use this section when the goal is to create or update a Notis app from a local
|
|
77
|
+
workspace or a hosted sandbox.
|
|
77
78
|
|
|
78
|
-
Notis apps are Vite + React projects using `@notis/sdk`.
|
|
79
|
+
Notis apps are Vite + React projects using `@notis/sdk`. After init or pull and editing, choose one terminal branch: local Desktop uses `dev → build → verify → user approval → deploy`; hosted sandbox uses `build → verify → identity/schema reconciliation → deploy`; Store submission is a later, separately confirmed action.
|
|
79
80
|
|
|
80
81
|
Important: `deploy` only updates the installed app artifact for the current user or team. Store submission is a separate, user-gated step. After the user explicitly confirms that the current App Details page and Store listing are ready, `apps publish --confirm-ready` submits the matching deployed version through the backend review flow.
|
|
81
82
|
|
|
83
|
+
Choose the execution path before changing the app:
|
|
84
|
+
|
|
85
|
+
- **Hosted sandbox**: a sandbox instruction or `/vercel/sandbox` working tree
|
|
86
|
+
proves that Desktop cannot mount the source. Do not run `apps dev`. A create
|
|
87
|
+
or edit request authorizes `apps deploy` after `apps build` and automated
|
|
88
|
+
`apps verify` pass unless the user explicitly requests preview-only,
|
|
89
|
+
read-only, or no deployment; inspection, review, and diagnosis stay
|
|
90
|
+
read-only. `apps pull <app-id>` links an existing app. Test before any remote
|
|
91
|
+
mutation. If an opt-out applies, stop there without create/link, database
|
|
92
|
+
mutation, deploy, or post-deploy checks. Otherwise, for a new unlinked app,
|
|
93
|
+
compare profile state and `apps list --json` with the canonical
|
|
94
|
+
`notis.config.ts` `name` and intended personal/team scope: link one exact
|
|
95
|
+
editable non-development match after a metadata-only (`include_documents:
|
|
96
|
+
false`) detail read proves scope, fail on any ambiguity/development
|
|
97
|
+
collision/scope mismatch, or create only on zero exact matches. Default new
|
|
98
|
+
apps to personal scope unless the user explicitly asks for team scope. Prove
|
|
99
|
+
canonicalizing the config title yields its name. For personal creation, run
|
|
100
|
+
`apps create "<canonical-config-title>" . --json` exactly once. For explicit
|
|
101
|
+
team scope, discover/describe and dry-run `LOCAL_NOTIS_CREATE_APP`, execute it
|
|
102
|
+
exactly once with team visibility and verified current team scope, verify the
|
|
103
|
+
returned identity/scope, and link that exact id. In a fresh sandbox, install Agent Browser with `npm exec --yes --package
|
|
104
|
+
agent-browser@latest -- agent-browser install` and run verification through
|
|
105
|
+
`npx --yes --package @notis_ai/cli@latest --package agent-browser@latest --
|
|
106
|
+
notis apps verify`. Read back the exact app id/version and Portal URL with `apps list --json`, run
|
|
107
|
+
the same combined command with `apps verify --mode live`, and return the exact
|
|
108
|
+
Portal URL. Stop before deployment on test failure and before retry on an
|
|
109
|
+
outcome-unknown create or deploy.
|
|
110
|
+
- **Local computer**: run `apps dev`, let the user test the DEV-badged app, and
|
|
111
|
+
deploy that development identity directly only after the user explicitly
|
|
112
|
+
asks; never run `apps create` after `apps dev`.
|
|
113
|
+
|
|
82
114
|
### App development workflow
|
|
83
115
|
|
|
84
116
|
1. Scaffold a new app (every published Store app is a scaffold; `notis apps scaffolds list [--search <term>]` lists them from the public registry, and `--from <slug>` downloads that app's source):
|
|
85
117
|
- `npx --package @notis_ai/cli@latest -- notis apps init ["My App"] [--from <slug>]`
|
|
86
|
-
2. Or pull an existing app's source to edit it
|
|
118
|
+
2. Or pull an existing app's source to edit it (the project is linked automatically):
|
|
87
119
|
- `npx --package @notis_ai/cli@latest -- notis apps pull <app-id>`
|
|
88
|
-
- then run `npm install`, `
|
|
89
|
-
3.
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
120
|
+
- then run `npm install`, increment `notisAppVersion`, and edit
|
|
121
|
+
3. **Local Desktop branch:** run `apps dev` for live testing, confirm the DEV
|
|
122
|
+
app and root/mount acceptance checks, then run `apps build` and `apps verify`.
|
|
123
|
+
Let the user test the DEV app and stop for explicit user approval. When
|
|
124
|
+
approved, run exactly one `apps deploy` to promote the existing `dev_app_id`
|
|
125
|
+
(or update the already linked app), then read it back. Never run `apps create`
|
|
126
|
+
after `apps dev`.
|
|
127
|
+
4. **Hosted sandbox branch:** bootstrap Agent Browser as described above, then
|
|
128
|
+
run `apps build` followed by the automated hosted `apps verify`. If automatic
|
|
129
|
+
deployment is opted out, stop after those tests with no app create/link,
|
|
130
|
+
database mutation, deploy, or post-deploy check. Otherwise, for a new hosted
|
|
131
|
+
app, reconcile `.notis/state.json` and `apps list --json` against
|
|
132
|
+
the canonical config `name` and intended scope (personal by default; team
|
|
133
|
+
only when explicitly requested). Include
|
|
134
|
+
development rows as collision checks. Link one exact editable non-dev match
|
|
135
|
+
only after a metadata-only (`include_documents: false`) exact app detail
|
|
136
|
+
proves scope; fail on multiple/scope mismatch, or create only on zero. Prove
|
|
137
|
+
canonicalize(config title) equals config name. For personal creation, run
|
|
138
|
+
`apps create "<canonical-config-title>" . --json` exactly once and verify id,
|
|
139
|
+
slug, edit permission, and personal scope. For explicit team scope, discover
|
|
140
|
+
and describe `LOCAL_NOTIS_CREATE_APP`, dry-run it, then execute it exactly once
|
|
141
|
+
with the canonical title, team visibility, and verified current team scope;
|
|
142
|
+
verify id/slug/team scope/edit permission and `apps link` that exact id. Then
|
|
143
|
+
compare declared databases, mutate only
|
|
144
|
+
missing/changed backward-compatible schemas with ownership proof, run exactly
|
|
145
|
+
one `apps deploy`, and read back id/version/Portal URL before live verify.
|
|
146
|
+
5. Check project health:
|
|
102
147
|
- `npx --package @notis_ai/cli@latest -- notis apps doctor`
|
|
103
|
-
|
|
148
|
+
6. Only after the user explicitly approves the current Store preview, submit the deployed version:
|
|
104
149
|
- `npx --package @notis_ai/cli@latest -- notis apps publish --confirm-ready`
|
|
105
150
|
|
|
106
151
|
### App development rules
|
|
107
152
|
|
|
108
153
|
- Always `build` before `deploy`; run `verify` before deploy when validating an app change.
|
|
154
|
+
- Never run `apps dev` in a hosted sandbox. Deploy every successfully verified
|
|
155
|
+
sandbox app create or edit unless explicitly told preview-only, read-only, or
|
|
156
|
+
no-deploy, then prove the remote id/version and live runtime. Other app tasks
|
|
157
|
+
do not authorize mutation.
|
|
158
|
+
- Deploy does not create databases. Compare first; materialize only missing or
|
|
159
|
+
changed hosted-app schemas, and verify exact ownership before an update.
|
|
160
|
+
- Build and verify before hosted app creation or database mutation. Only
|
|
161
|
+
backward-compatible schema expansion may happen before deployment.
|
|
162
|
+
- In hosted sandboxes, bootstrap `agent-browser` and include its package on the
|
|
163
|
+
verification command's `PATH`; `--no-browser` is not a passing automated gate.
|
|
164
|
+
- Never deploy a local Desktop edit until the user tests the DEV app and asks.
|
|
109
165
|
- Prefer `npx --package @notis_ai/cli@latest -- notis apps deploy` for the first deploy of a project already run with `apps dev`; it promotes the development app in place.
|
|
110
166
|
- Link before `deploy`, or pass `--app-id <id>` when intentionally deploying without writing local link state.
|
|
111
167
|
- Use `npx --package @notis_ai/cli@latest -- notis apps doctor` to diagnose configuration or dependency issues.
|
|
112
|
-
- Use `npx --package @notis_ai/cli@latest -- notis apps list` to discover
|
|
168
|
+
- Use `npx --package @notis_ai/cli@latest -- notis apps list --json` to discover exact slugs, permissions, versions, and Portal links before linking and after deployment; use a metadata-only (`include_documents: false`) exact app-detail read to prove personal/team scope without materializing databases.
|
|
113
169
|
- Never treat deploy approval as Store approval. Set visibility to Team or Public first, then run `apps publish --confirm-ready` only after the user explicitly confirms the current App Details page and Store listing.
|
|
114
170
|
- `apps publish --confirm-ready` submits the deployed snapshot through the same backend review flow as App Details. It must reject missing confirmation, incomplete listing media, a local/deployed version mismatch, private visibility, or an existing pending review.
|
|
115
171
|
|
|
@@ -134,14 +190,16 @@ If the task is specifically about app structure, runtime behavior, or database/v
|
|
|
134
190
|
|
|
135
191
|
## IMPORTANT: When NOT to use tool access for app development
|
|
136
192
|
|
|
137
|
-
When building or deploying a Notis app, do NOT use `npx --package @notis_ai/cli@latest -- notis tools exec` for
|
|
193
|
+
When building or deploying a Notis app, do NOT use `npx --package @notis_ai/cli@latest -- notis tools exec` for app file operations:
|
|
138
194
|
|
|
139
|
-
- Creating databases -- declare them in `notis.config.ts` instead
|
|
140
195
|
- Loading or saving app files -- use `npx --package @notis_ai/cli@latest -- notis apps build` and `npx --package @notis_ai/cli@latest -- notis apps deploy`
|
|
141
196
|
- Linting app files -- use `npx --package @notis_ai/cli@latest -- notis apps build` which validates automatically
|
|
142
197
|
- Managing app routes -- write standard Vite + React pages in `app/`, not raw JS files
|
|
143
198
|
|
|
144
|
-
|
|
199
|
+
Database schemas are the exception: declaring a slug in `notis.config.ts` does
|
|
200
|
+
not create it. Use the discovery-first native database tool workflow to
|
|
201
|
+
create/update and read back each app-owned schema before deployment. Tool calls
|
|
202
|
+
are also valid for testing runtime behavior after deployment.
|
|
145
203
|
|
|
146
204
|
## Section 2: Accessing Tools Through the Notis CLI
|
|
147
205
|
|
package/package.json
CHANGED
|
@@ -1408,7 +1408,65 @@ async function appsDevHandler(ctx) {
|
|
|
1408
1408
|
desktopOwnerScope: process.env.NOTIS_APPS_DEV_DESKTOP_OWNER_SCOPE,
|
|
1409
1409
|
});
|
|
1410
1410
|
|
|
1411
|
-
let heartbeatTimer =
|
|
1411
|
+
let heartbeatTimer = null;
|
|
1412
|
+
let consumerTimer = null;
|
|
1413
|
+
let shuttingDown = false;
|
|
1414
|
+
const shutdown = async (signal) => {
|
|
1415
|
+
if (shuttingDown) return;
|
|
1416
|
+
shuttingDown = true;
|
|
1417
|
+
process.stdout.write(`\n[notis apps dev] stopping (${signal})...\n`);
|
|
1418
|
+
if (heartbeatTimer) {
|
|
1419
|
+
clearInterval(heartbeatTimer);
|
|
1420
|
+
heartbeatTimer = null;
|
|
1421
|
+
}
|
|
1422
|
+
if (consumerTimer) {
|
|
1423
|
+
clearInterval(consumerTimer);
|
|
1424
|
+
consumerTimer = null;
|
|
1425
|
+
}
|
|
1426
|
+
if (manualConsumerTimer) {
|
|
1427
|
+
clearInterval(manualConsumerTimer);
|
|
1428
|
+
manualConsumerTimer = null;
|
|
1429
|
+
}
|
|
1430
|
+
if (manualConsumerInstanceId) {
|
|
1431
|
+
try {
|
|
1432
|
+
removeAppDevConsumer(manualConsumerInstanceId);
|
|
1433
|
+
} catch {
|
|
1434
|
+
// A crashed CLI lease expires automatically after the heartbeat window.
|
|
1435
|
+
}
|
|
1436
|
+
}
|
|
1437
|
+
try {
|
|
1438
|
+
await devServer.close();
|
|
1439
|
+
} catch {
|
|
1440
|
+
// ignore cleanup failures during shutdown
|
|
1441
|
+
}
|
|
1442
|
+
// Keep ownership records until every watcher group has stopped. If the
|
|
1443
|
+
// Desktop must force this host down, the next launch can still recover a
|
|
1444
|
+
// verified orphan instead of losing its only ownership proof.
|
|
1445
|
+
try {
|
|
1446
|
+
removeAppDevSession(sessionId, sessionsFilePath);
|
|
1447
|
+
} catch {
|
|
1448
|
+
// ignore cleanup failures during shutdown
|
|
1449
|
+
}
|
|
1450
|
+
if (sourceHostLock) {
|
|
1451
|
+
releaseAppDevHostLock(sourceHostLock);
|
|
1452
|
+
sourceHostLock = null;
|
|
1453
|
+
}
|
|
1454
|
+
process.exit(EXIT_CODES.ok);
|
|
1455
|
+
};
|
|
1456
|
+
const handleSigint = () => {
|
|
1457
|
+
void shutdown('SIGINT');
|
|
1458
|
+
};
|
|
1459
|
+
const handleSigterm = () => {
|
|
1460
|
+
void shutdown('SIGTERM');
|
|
1461
|
+
};
|
|
1462
|
+
|
|
1463
|
+
// Electron can stop a partially registered host. Install cleanup before the
|
|
1464
|
+
// first remote registration so every watcher group is still terminated when
|
|
1465
|
+
// registration is slow or stuck.
|
|
1466
|
+
process.on('SIGINT', handleSigint);
|
|
1467
|
+
process.on('SIGTERM', handleSigterm);
|
|
1468
|
+
|
|
1469
|
+
heartbeatTimer = setInterval(() => {
|
|
1412
1470
|
try {
|
|
1413
1471
|
heartbeatAppDevSession(sessionId, new Date().toISOString(), sessionsFilePath);
|
|
1414
1472
|
} catch (error) {
|
|
@@ -1514,6 +1572,8 @@ async function appsDevHandler(ctx) {
|
|
|
1514
1572
|
}
|
|
1515
1573
|
}
|
|
1516
1574
|
if (apps.length === 0) {
|
|
1575
|
+
process.off('SIGINT', handleSigint);
|
|
1576
|
+
process.off('SIGTERM', handleSigterm);
|
|
1517
1577
|
clearInterval(heartbeatTimer);
|
|
1518
1578
|
heartbeatTimer = null;
|
|
1519
1579
|
try {
|
|
@@ -1548,8 +1608,6 @@ async function appsDevHandler(ctx) {
|
|
|
1548
1608
|
...versionPrecedenceWarnings(apps),
|
|
1549
1609
|
];
|
|
1550
1610
|
|
|
1551
|
-
let consumerTimer = null;
|
|
1552
|
-
|
|
1553
1611
|
ctx.output.emitSuccess({
|
|
1554
1612
|
command: ctx.spec.command_path.join(' '),
|
|
1555
1613
|
data: {
|
|
@@ -1599,57 +1657,6 @@ async function appsDevHandler(ctx) {
|
|
|
1599
1657
|
].join('\n'),
|
|
1600
1658
|
});
|
|
1601
1659
|
|
|
1602
|
-
let shuttingDown = false;
|
|
1603
|
-
const shutdown = async (signal) => {
|
|
1604
|
-
if (shuttingDown) return;
|
|
1605
|
-
shuttingDown = true;
|
|
1606
|
-
process.stdout.write(`\n[notis apps dev] stopping (${signal})...\n`);
|
|
1607
|
-
if (heartbeatTimer) {
|
|
1608
|
-
clearInterval(heartbeatTimer);
|
|
1609
|
-
heartbeatTimer = null;
|
|
1610
|
-
}
|
|
1611
|
-
if (consumerTimer) {
|
|
1612
|
-
clearInterval(consumerTimer);
|
|
1613
|
-
consumerTimer = null;
|
|
1614
|
-
}
|
|
1615
|
-
if (manualConsumerTimer) {
|
|
1616
|
-
clearInterval(manualConsumerTimer);
|
|
1617
|
-
manualConsumerTimer = null;
|
|
1618
|
-
}
|
|
1619
|
-
if (manualConsumerInstanceId) {
|
|
1620
|
-
try {
|
|
1621
|
-
removeAppDevConsumer(manualConsumerInstanceId);
|
|
1622
|
-
} catch {
|
|
1623
|
-
// A crashed CLI lease expires automatically after the heartbeat window.
|
|
1624
|
-
}
|
|
1625
|
-
}
|
|
1626
|
-
try {
|
|
1627
|
-
await devServer.close();
|
|
1628
|
-
} catch {
|
|
1629
|
-
// ignore cleanup failures during shutdown
|
|
1630
|
-
}
|
|
1631
|
-
// Keep ownership records until every watcher group has stopped. If the
|
|
1632
|
-
// Desktop must force this host down, the next launch can still recover a
|
|
1633
|
-
// verified orphan instead of losing its only ownership proof.
|
|
1634
|
-
try {
|
|
1635
|
-
removeAppDevSession(sessionId, sessionsFilePath);
|
|
1636
|
-
} catch {
|
|
1637
|
-
// ignore cleanup failures during shutdown
|
|
1638
|
-
}
|
|
1639
|
-
if (sourceHostLock) {
|
|
1640
|
-
releaseAppDevHostLock(sourceHostLock);
|
|
1641
|
-
sourceHostLock = null;
|
|
1642
|
-
}
|
|
1643
|
-
process.exit(EXIT_CODES.ok);
|
|
1644
|
-
};
|
|
1645
|
-
|
|
1646
|
-
process.on('SIGINT', () => {
|
|
1647
|
-
void shutdown('SIGINT');
|
|
1648
|
-
});
|
|
1649
|
-
process.on('SIGTERM', () => {
|
|
1650
|
-
void shutdown('SIGTERM');
|
|
1651
|
-
});
|
|
1652
|
-
|
|
1653
1660
|
if (consumerMode === 'machine' || consumerMode === 'environment') {
|
|
1654
1661
|
consumerTimer = setInterval(() => {
|
|
1655
1662
|
if (!hasAppDevConsumer(readAppDevConsumers(), {
|
|
@@ -2214,8 +2221,10 @@ async function appsPullHandler(ctx) {
|
|
|
2214
2221
|
const appId = ctx.args.appId;
|
|
2215
2222
|
const result = await runToolCommand({
|
|
2216
2223
|
runtime: ctx.runtime,
|
|
2217
|
-
|
|
2218
|
-
|
|
2224
|
+
// Pull is source retrieval plus local link state. LIST_APPS is deliberately
|
|
2225
|
+
// non-materializing; GET_APP hydrates missing declared databases and would
|
|
2226
|
+
// turn a read-only pull into a remote mutation before build/verification.
|
|
2227
|
+
toolName: LIST_APPS_TOOL,
|
|
2219
2228
|
});
|
|
2220
2229
|
if (
|
|
2221
2230
|
ctx.runtime.credentialKind === 'oauth'
|
|
@@ -2223,7 +2232,11 @@ async function appsPullHandler(ctx) {
|
|
|
2223
2232
|
) {
|
|
2224
2233
|
throw usageError('Pulling app source requires a current OAuth grant. Run `notis login` and retry.');
|
|
2225
2234
|
}
|
|
2226
|
-
const
|
|
2235
|
+
const apps = Array.isArray(result.payload?.apps) ? result.payload.apps : [];
|
|
2236
|
+
const app = apps.find((candidate) => (candidate?.app_id || candidate?.id) === appId);
|
|
2237
|
+
if (!app) {
|
|
2238
|
+
throw usageError(`App ${appId} is not accessible to the active profile.`);
|
|
2239
|
+
}
|
|
2227
2240
|
const defaultDir = slugify(app.slug) || slugify(app.name) || slugify(appId);
|
|
2228
2241
|
const targetDir = ctx.args.dir
|
|
2229
2242
|
? resolveProjectDir(ctx.args.dir)
|
|
@@ -84,6 +84,25 @@ async function writeLockOwnerAtomically(lockDirectory, owner) {
|
|
|
84
84
|
await rename(temporaryOwnerPath, ownerPath);
|
|
85
85
|
}
|
|
86
86
|
|
|
87
|
+
async function releaseOwnedLock(lockDirectory, ownerId) {
|
|
88
|
+
const owner = await lockSnapshot(lockDirectory);
|
|
89
|
+
if (owner?.id !== ownerId) return;
|
|
90
|
+
|
|
91
|
+
const quarantineRoot = join(dirname(lockDirectory), '.stale-operation-locks');
|
|
92
|
+
const releasedDirectory = join(quarantineRoot, `released.${ownerId}`);
|
|
93
|
+
await mkdir(quarantineRoot, { recursive: true, mode: 0o700 });
|
|
94
|
+
try {
|
|
95
|
+
// Moving the owned directory releases the shared pathname atomically.
|
|
96
|
+
// Delete only the private destination so a waiter can safely acquire a new
|
|
97
|
+
// lock without racing this owner's recursive cleanup.
|
|
98
|
+
await rename(lockDirectory, releasedDirectory);
|
|
99
|
+
} catch (error) {
|
|
100
|
+
if (error?.code === 'ENOENT') return;
|
|
101
|
+
throw error;
|
|
102
|
+
}
|
|
103
|
+
await rm(releasedDirectory, { recursive: true, force: true });
|
|
104
|
+
}
|
|
105
|
+
|
|
87
106
|
/** Serialize Desktop and terminal skill sync across processes on one Mac. */
|
|
88
107
|
export async function withSkillSyncLock(callback, {
|
|
89
108
|
home = homedir(),
|
|
@@ -174,10 +193,7 @@ export async function withSkillSyncLock(callback, {
|
|
|
174
193
|
heartbeatStopped = true;
|
|
175
194
|
clearInterval(heartbeat);
|
|
176
195
|
await heartbeatInFlight;
|
|
177
|
-
|
|
178
|
-
if (owner?.id === ownerId) {
|
|
179
|
-
await rm(lockDirectory, { recursive: true, force: true });
|
|
180
|
-
}
|
|
196
|
+
await releaseOwnedLock(lockDirectory, ownerId);
|
|
181
197
|
}
|
|
182
198
|
}
|
|
183
199
|
|
|
@@ -49,10 +49,12 @@ const containerBaseStyle: CSSProperties = {
|
|
|
49
49
|
};
|
|
50
50
|
|
|
51
51
|
const countStyle: CSSProperties = {
|
|
52
|
+
flexShrink: 0,
|
|
52
53
|
padding: '0 0.5rem',
|
|
53
54
|
fontSize: '12px',
|
|
54
55
|
fontWeight: 500,
|
|
55
56
|
fontVariantNumeric: 'tabular-nums',
|
|
57
|
+
whiteSpace: 'nowrap',
|
|
56
58
|
color: 'color-mix(in srgb, hsl(var(--background)) 70%, transparent)',
|
|
57
59
|
};
|
|
58
60
|
|
|
@@ -64,6 +66,7 @@ const dividerStyle: CSSProperties = {
|
|
|
64
66
|
};
|
|
65
67
|
|
|
66
68
|
const baseButtonStyle: CSSProperties = {
|
|
69
|
+
flexShrink: 0,
|
|
67
70
|
display: 'inline-flex',
|
|
68
71
|
alignItems: 'center',
|
|
69
72
|
gap: '0.375rem',
|
|
@@ -75,6 +78,7 @@ const baseButtonStyle: CSSProperties = {
|
|
|
75
78
|
cursor: 'pointer',
|
|
76
79
|
fontSize: '13px',
|
|
77
80
|
fontFamily: 'inherit',
|
|
81
|
+
whiteSpace: 'nowrap',
|
|
78
82
|
transition: 'background-color 120ms ease, color 120ms ease',
|
|
79
83
|
};
|
|
80
84
|
|
|
@@ -188,7 +192,9 @@ function ActionButton({ action }: { action: MultiSelectAction }) {
|
|
|
188
192
|
onBlur={() => setHover(false)}
|
|
189
193
|
style={buttonStyle}
|
|
190
194
|
>
|
|
191
|
-
{action.
|
|
195
|
+
{!action.shortcut && action.icon ? (
|
|
196
|
+
<span aria-hidden style={iconSlotStyle}>{action.icon}</span>
|
|
197
|
+
) : null}
|
|
192
198
|
{display ? <kbd aria-hidden style={keycapStyle}>{display}</kbd> : null}
|
|
193
199
|
<span>{action.pending ? `${action.label}…` : action.label}</span>
|
|
194
200
|
</button>
|
|
@@ -64,8 +64,8 @@ export interface CollectionKeyboardShortcuts {
|
|
|
64
64
|
clear: string | false;
|
|
65
65
|
selectAll: string | false;
|
|
66
66
|
toggle: string | false;
|
|
67
|
-
extendNext: string | false;
|
|
68
|
-
extendPrevious: string | false;
|
|
67
|
+
extendNext: string | string[] | false;
|
|
68
|
+
extendPrevious: string | string[] | false;
|
|
69
69
|
next: string | string[] | false;
|
|
70
70
|
previous: string | string[] | false;
|
|
71
71
|
up: string | false;
|
|
@@ -158,8 +158,8 @@ const DEFAULT_SHORTCUTS: CollectionKeyboardShortcuts = {
|
|
|
158
158
|
clear: 'Escape',
|
|
159
159
|
selectAll: 'Mod+A',
|
|
160
160
|
toggle: 'X',
|
|
161
|
-
extendNext: 'Shift+ArrowDown',
|
|
162
|
-
extendPrevious: 'Shift+ArrowUp',
|
|
161
|
+
extendNext: ['Shift+ArrowDown', 'Shift+ArrowRight'],
|
|
162
|
+
extendPrevious: ['Shift+ArrowUp', 'Shift+ArrowLeft'],
|
|
163
163
|
next: 'J',
|
|
164
164
|
previous: 'K',
|
|
165
165
|
up: 'ArrowUp',
|
|
@@ -169,6 +169,16 @@ const DEFAULT_SHORTCUTS: CollectionKeyboardShortcuts = {
|
|
|
169
169
|
activate: 'Enter',
|
|
170
170
|
};
|
|
171
171
|
|
|
172
|
+
function filterShortcutKeys(
|
|
173
|
+
keys: string | string[] | false,
|
|
174
|
+
predicate: (key: string) => boolean,
|
|
175
|
+
): string | string[] | false {
|
|
176
|
+
if (!keys) return false;
|
|
177
|
+
const filtered = (Array.isArray(keys) ? keys : [keys]).filter(predicate);
|
|
178
|
+
if (filtered.length === 0) return false;
|
|
179
|
+
return filtered.length === 1 ? filtered[0] : filtered;
|
|
180
|
+
}
|
|
181
|
+
|
|
172
182
|
function setsEqual(a: ReadonlySet<string>, b: ReadonlySet<string>): boolean {
|
|
173
183
|
if (a.size !== b.size) return false;
|
|
174
184
|
for (const id of a) if (!b.has(id)) return false;
|
|
@@ -442,14 +452,20 @@ export function useCollectionInteractions<T>(
|
|
|
442
452
|
if (!keys) return;
|
|
443
453
|
definitions.push({ id, label, keys, onTrigger });
|
|
444
454
|
};
|
|
455
|
+
const extendRight = filterShortcutKeys(keyboard.extendNext, (key) => key.toLowerCase().endsWith('arrowright'));
|
|
456
|
+
const extendDown = filterShortcutKeys(keyboard.extendNext, (key) => !key.toLowerCase().endsWith('arrowright'));
|
|
457
|
+
const extendLeft = filterShortcutKeys(keyboard.extendPrevious, (key) => key.toLowerCase().endsWith('arrowleft'));
|
|
458
|
+
const extendUp = filterShortcutKeys(keyboard.extendPrevious, (key) => !key.toLowerCase().endsWith('arrowleft'));
|
|
445
459
|
add('collection.clear', 'Clear selection', keyboard.clear, clear);
|
|
446
460
|
add('collection.select-all', 'Select all visible items', selectionMode === 'none' ? false : keyboard.selectAll, selectAll);
|
|
447
461
|
add('collection.toggle', 'Toggle active item', selectionMode === 'none' ? false : keyboard.toggle, () => {
|
|
448
462
|
const id = activeIdRef.current ?? anchorIdRef.current;
|
|
449
463
|
if (id) toggle(id);
|
|
450
464
|
});
|
|
451
|
-
add('collection.extend-
|
|
452
|
-
add('collection.extend-
|
|
465
|
+
add('collection.extend-down', 'Extend selection down', selectionMode !== 'multiple' ? false : extendDown, () => moveActive('down', true));
|
|
466
|
+
add('collection.extend-right', 'Extend selection right', selectionMode !== 'multiple' ? false : extendRight, () => moveActive('right', true));
|
|
467
|
+
add('collection.extend-up', 'Extend selection up', selectionMode !== 'multiple' ? false : extendUp, () => moveActive('up', true));
|
|
468
|
+
add('collection.extend-left', 'Extend selection left', selectionMode !== 'multiple' ? false : extendLeft, () => moveActive('left', true));
|
|
453
469
|
add('collection.next', 'Next item', keyboard.next, () => moveActive('next'));
|
|
454
470
|
add('collection.previous', 'Previous item', keyboard.previous, () => moveActive('previous'));
|
|
455
471
|
add('collection.up', 'Move up', keyboard.up, () => moveActive('up'));
|
|
@@ -634,6 +650,7 @@ export function useCollectionInteractions<T>(
|
|
|
634
650
|
},
|
|
635
651
|
onKeyDown: (event: ReactKeyboardEvent) => {
|
|
636
652
|
if (event.key !== ' ' || event.metaKey || event.ctrlKey || event.altKey) return;
|
|
653
|
+
if (event.target !== event.currentTarget) return;
|
|
637
654
|
event.preventDefault();
|
|
638
655
|
if (selectionMode === 'none') activate(id);
|
|
639
656
|
else toggle(id);
|
|
@@ -182,7 +182,13 @@ export function isEditableShortcutEvent(event: Event): boolean {
|
|
|
182
182
|
if (!(target instanceof HTMLElement)) return false;
|
|
183
183
|
if (target.isContentEditable || target.getAttribute('contenteditable') === 'true') return true;
|
|
184
184
|
const tag = target.tagName;
|
|
185
|
-
|
|
185
|
+
if (tag === 'INPUT' || tag === 'TEXTAREA' || tag === 'SELECT' || tag === 'BUTTON' || tag === 'SUMMARY') {
|
|
186
|
+
return true;
|
|
187
|
+
}
|
|
188
|
+
if (tag === 'A' && target.hasAttribute('href')) return true;
|
|
189
|
+
return ['button', 'link', 'menuitem', 'option', 'switch', 'tab'].includes(
|
|
190
|
+
target.getAttribute('role') || '',
|
|
191
|
+
);
|
|
186
192
|
});
|
|
187
193
|
}
|
|
188
194
|
|