@notis_ai/cli 0.2.0-beta.114.1 → 0.2.0-beta.117.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 CHANGED
@@ -4,7 +4,7 @@ Agent-first Notis CLI for apps and generic tool execution.
4
4
 
5
5
  ## Install
6
6
 
7
- Use the Notis CLI through NPX. Install and sign into Notis Desktop to keep the local CLI auth profile current; do not rely on an installed `notis` command. Related skills such as `notis-cli` and `notis-apps` are delivered through normal Notis skill sync for the signed-in user.
7
+ Use the Notis CLI through NPX; do not rely on an installed `notis` command. When Notis Desktop is signed in, its local credential remains the fastest path. On a server, container, or machine without Desktop, run `notis login` to authorize a scoped, revocable OAuth credential in the browser.
8
8
 
9
9
  For CI, hosted agents, or internal scripts, pass a non-persisted token with `NOTIS_JWT=<token>`.
10
10
 
@@ -12,12 +12,13 @@ For CI, hosted agents, or internal scripts, pass a non-persisted token with `NOT
12
12
 
13
13
  ```bash
14
14
  npx --package @notis_ai/cli@latest -- notis --help
15
+ npx --package @notis_ai/cli@latest -- notis login
15
16
  npx --package @notis_ai/cli@latest -- notis doctor
16
17
  npx --package @notis_ai/cli@latest -- notis apps list
17
18
  npx --package @notis_ai/cli@latest -- notis tools search "list Notis databases"
18
19
  ```
19
20
 
20
- The desktop app owns sign-in and keeps the CLI profile in sync automatically.
21
+ Credential precedence is worktree runtime, `NOTIS_JWT`, a valid Desktop credential, then OAuth. `notis logout` revokes and removes OAuth without signing Desktop out. Use `notis login --paste-code` for the HTTPS copy-paste fallback on a remote machine.
21
22
 
22
23
  The CLI defaults to `json` output in agent or non-TTY contexts and `table` output in interactive terminals.
23
24
 
@@ -31,6 +32,44 @@ The CLI defaults to `json` output in agent or non-TTY contexts and `table` outpu
31
32
  - `--timeout-ms <n>` — HTTP timeout in milliseconds
32
33
  - `--idempotency-key <key>` — Override the generated idempotency key for mutating commands
33
34
 
35
+ ## Authentication
36
+
37
+ ### `npx --package @notis_ai/cli@latest -- notis login`
38
+
39
+ Authorize the Notis CLI in a browser with scoped OAuth access.
40
+
41
+ When to use: Use this on a machine where Notis Desktop is unavailable, signed out, or should not own CLI authentication.
42
+
43
+ Options:
44
+ - `--no-browser` — Print the authorization URL without opening a browser.
45
+ - `--print-url` — Print the authorization URL even when opening a browser.
46
+ - `--paste-code` — Use the copy-paste callback for SSH and headless machines.
47
+ - `--force` — Create an independent OAuth grant even when Desktop is signed in.
48
+ - `--timeout-seconds <n>` — How long to wait for authorization (default 300).
49
+ - `--scope <scope>` — OAuth permission to request (repeatable).
50
+ - `--code <code>` — Redeem the code shown in the browser after a non-interactive login.
51
+
52
+ Examples:
53
+ - `npx --package @notis_ai/cli@latest -- notis login`
54
+ - `npx --package @notis_ai/cli@latest -- notis login --no-browser --print-url`
55
+ - `npx --package @notis_ai/cli@latest -- notis login --paste-code`
56
+ - `npx --package @notis_ai/cli@latest -- notis login --code 4f3c2b1a`
57
+ - `npx --package @notis_ai/cli@latest -- notis login --force`
58
+
59
+ ### `npx --package @notis_ai/cli@latest -- notis logout`
60
+
61
+ Revoke and remove the scoped OAuth credential for the active CLI profile.
62
+
63
+ When to use: Use this to disconnect the command line without signing Notis Desktop out.
64
+
65
+ Options:
66
+ - `--all-profiles` — Remove OAuth credentials from every CLI profile.
67
+
68
+ Examples:
69
+ - `npx --package @notis_ai/cli@latest -- notis logout`
70
+ - `npx --package @notis_ai/cli@latest -- notis logout --all-profiles`
71
+
72
+
34
73
  ## Apps
35
74
 
36
75
  ### `npx --package @notis_ai/cli@latest -- notis apps list`
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@notis_ai/cli",
3
- "version": "0.2.0-beta.114.1",
3
+ "version": "0.2.0-beta.117.1",
4
4
  "description": "Agent-first Notis CLI for apps and generic tool execution",
5
5
  "type": "module",
6
6
  "bin": {
@@ -9,8 +9,6 @@ Use this skill when the user wants a packaged Notis app -- task manager, CRM, da
9
9
 
10
10
  Run the Notis CLI through NPX, for example `npx --package @notis_ai/cli@latest -- notis apps list`. Notis Desktop keeps the CLI auth profile current. This `notis-apps` skill is delivered through normal Notis skill sync for the signed-in user, alongside other curated skills.
11
11
 
12
- For the platform model, architecture, and local development workflow, read [docs/notis-apps-platform.md](../../../docs/notis-apps-platform.md) first. This skill is the execution guide for using that platform correctly.
13
-
14
12
  ## How Apps Are Built
15
13
 
16
14
  All Notis apps are built using the Notis CLI, either locally in a repo workspace or inside a Vercel Sandbox. The platform contract is the same in both cases:
@@ -94,7 +92,7 @@ App code never accesses the runtime directly -- it uses SDK hooks (`useTool`, `u
94
92
  15. **Local development first; deploy is user-gated** -- Iterate with `apps dev` and let the **user** test the app in the desktop **Local development** sidebar group. Do NOT run `apps create` or `apps deploy` on your own initiative, even after a clean build and verify. `deploy` installs the app onto the user's account and is a one-directional, outward-facing action — treat it like publishing: only run it when the user has tested the local build and **explicitly asks you to deploy**. Building a new app end-to-end without deploying is the expected, complete outcome. (See the **Local-development-first handoff** in the Workflow.)
95
93
  16. **Installed app links are explicit** -- A mounted dev session updates an installed workspace app only when the local checkout is linked by app id in `.notis/state.json` and the dev-session registry mirrors that id. Name or slug matches may be suggestions, never update targets. After first install, keep that link so Portal and CLI show/update the same app instead of creating duplicates.
96
94
  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. The Electron registry mirrors the installed id as `targetAppId`. Never pass a runtime app whose manifest has `is_dev: true` to `notis apps link`; it is not an install/update target. Current CLIs reject that link and repair stale hidden, deleted, or inaccessible targets on the next `apps dev` without erasing valid state on transport or authentication failures.
97
- 18. **Mounted means Portal-acknowledged** -- A running process, registry row, or HTTP 200 proves only that the app is serving. Before telling the user an app is mounted, require the current `apps dev` process to report `Mounted in Notis`. That status comes from an exact session/app/slug/nonce acknowledgment written by Electron only after the app enters the Portal's final **Local development** sidebar model. Do not require a manual Electron route/render check merely to prove the mount; route rendering is a separate behavior or visual verification task.
95
+ 18. **Mounted means Portal-acknowledged** -- A running process or HTTP 200 proves only that the app is serving. Before telling the user an app is mounted, require the current `apps dev` process to report `Mounted in <target desktop>`. The CLI selects the target from the active Notis profile: normal CLI runs target the signed-in Notis or Notis Beta desktop, while a CLI running inside an active Notis source workspace targets that workspace's matching desktop instance. The exact session/app/slug/nonce acknowledgment is accepted only from the visible target window after the app enters its final **Local development** sidebar model. Route rendering is a separate proof: when UI verification is required, also open the app and require `Rendered in <target desktop>`.
98
96
  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.
99
97
  20. **Bump `notisAppVersion` for every Store update** -- `package.json` must contain a semver `notisAppVersion`. For an existing Store app, increment it beyond the currently published registry version before deploy and submission; registry CI rejects equal or lower versions.
100
98
  21. **`CHANGELOG.md` owns release history** -- Keep the complete release history in one root `CHANGELOG.md`, newest entry first. Do not add new `versionNotes` values to `notis.config.ts`. Use `## [Release title] - YYYY-MM-DD`, or `{PR_MERGE_DATE}` for an unpublished entry. App Details reads **What’s New** and **Version History** from the deployed package manifest, while the Store reads them from the latest published snapshot; unpublished workspace edits must never change the Store page. The manifest also exposes `package.json` `notisAppVersion` as the package version shown in App Details.
@@ -126,13 +124,13 @@ These are the most common mistakes agents make. Each one wastes time and produce
126
124
  2. **Pull only installed apps.** If the user explicitly wants to fork an app they already installed, run `npx --package @notis_ai/cli@latest -- notis apps list`, then `npx --package @notis_ai/cli@latest -- notis apps pull <app-id> ./<dir>`. To fork a Store app that is not installed, tell the user to install it from `/store` first.
127
125
  3. **Edit the listing source.** Update `name` (slug), `title`, description, icon, accent, author, categories, tagline, databases, routes, and tools in `notis.config.ts`. 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.
128
126
  4. **Build pages in `app/`.** Reuse scaffold code wherever it fits.
129
- 5. **Iterate live.** Run `npx --package @notis_ai/cli@latest -- notis apps dev` so the desktop sidebar's Local development group discovers the app and the Electron Portal renders the local bundle. In the Notis monorepo's supervised `dev.sh` stack, keep app projects under `apps/<slug>/`; the root supervisor discovers that directory, owns the long-running process, and restarts it. A root-level app folder is not supervised and can disappear as soon as an agent-owned terminal exits.
127
+ 5. **Iterate live.** Run `npx --package @notis_ai/cli@latest -- notis apps dev` so the target desktop's **Local development** sidebar group discovers the app and renders the local bundle. Keep this command running for as long as the user is testing; stopping it removes the temporary Local development entry. Read the command's `Target desktop` line instead of guessing between Notis, Notis Beta, or a source-workspace desktop.
130
128
  6. **Capture listing screenshots.** Declare 3–6 screenshots in `notis.config.ts`, each with a stable `path`, descriptive `alt`, and optional `route`/`scenario`/`focus`/`theme`, then run `npx --package @notis_ai/cli@latest -- notis apps screenshot`. Use `focus` to frame a real app root without empty browser canvas; use `theme: 'light'` or `theme: 'dark'` to match both the Portal render and Store backdrop, and pair both modes when that best represents the app. It renders the configured states in a headless harness and writes exact 2000x1250 PNGs under `metadata/`, using the deterministic Store presentation by default (`--raw` is diagnostic only). Apps are icon-led like Raycast — the icon set in `notis.config.ts` represents the app, so there is no cover image, only these screenshots. Never hand-author the PNGs; regenerate them when routes or UI change.
131
129
  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.
132
- 8. **Local-development-first handoff — STOP HERE.** Keep `apps dev` running and hand off to the user: tell them the app is live in the desktop **Local development** sidebar group (green `DEV` badge) and ask them to test it there. Building a new app to this point, without deploying, is a **complete and expected** result. Do NOT proceed to `apps create` / `apps deploy` yet — wait for the user to test and explicitly ask to deploy. (`apps dev` is what puts the app in Local development; without a running session the app never appears there.) **Before handing off, complete all three acceptance checks:**
133
- 1. Registry: `cat "$CONDUCTOR_WORKSPACE_PATH/.context/app-dev-sessions.json"` lists the app's `devSlug` with a fresh `lastHeartbeatAt`. If it is absent (the session landed in global `~/.notis` instead), restart with `NOTIS_APP_DEV_SESSIONS_FILE="$CONDUCTOR_WORKSPACE_PATH/.context/app-dev-sessions.json"`.
134
- 2. Bundle: the session's loopback `/snapshot` URL responds successfully and contains the expected manifest/routes.
135
- 3. Mount acknowledgment: capture the current `apps dev` status `Mounted in Notis: <app name>`. This is the deterministic proof that the nonce-backed session reached the Portal's final Local development sidebar model. If the CLI says only `Serving locally`, do not claim the app is mounted. Manual Electron rendering is optional here; use it when the task specifically requires UI/behavior verification, not as the mount handshake.
130
+ 8. **Local-development-first handoff — STOP HERE.** Keep `apps dev` running and hand off to the user: tell them the app is live in the target desktop's **Local development** sidebar group (green `DEV` badge) and ask them to test it there. Building a new app to this point, without deploying, is a **complete and expected** result. Do NOT proceed to `apps create` / `apps deploy` yet — wait for the user to test and explicitly ask to deploy. (`apps dev` is what puts the app in Local development; without a running session the app never appears there.) **Before handing off, complete all three acceptance checks:**
131
+ 1. Target: capture the CLI's `Target desktop: <name>` line and make sure that exact desktop app is running and signed in.
132
+ 2. Bundle: the reported loopback `/snapshot` URL responds successfully and contains the expected manifest/routes.
133
+ 3. Mount and render: require `Mounted in <target desktop>: <app name>`. If the task includes UI or runtime behavior, open the default route and also require `Rendered in <target desktop>: <app name>`. If the CLI says only `Serving locally`, do not claim the app is mounted.
136
134
  See Troubleshooting → *App is missing from Local development* if any check fails.
137
135
  9. **Deploy only when the user asks.** Once the user has tested locally and explicitly requests a deploy, run `npx --package @notis_ai/cli@latest -- notis apps create "<name>" .` (first time) then `npx --package @notis_ai/cli@latest -- notis apps deploy --direct`, or link first with `npx --package @notis_ai/cli@latest -- notis apps link <id> .` / pass `--app-id <id>` for an existing app. Deploy installs or updates the app on the user's account (it appears under **Workspace**, not Local development). After first install, `.notis/state.json` must point at the installed app id so future local-dev actions become **Update**, not another **Install**.
138
136
  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.
@@ -484,7 +482,7 @@ This uploads the bundle and editable source snapshot directly to Supabase storag
484
482
 
485
483
  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.
486
484
  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.
487
- 3. **Local development acceptance**: Require the running CLI to report `Mounted in Notis`, which proves the exact nonce-backed session entered the current Electron Portal's final `Local development` sidebar model. Registry presence and bundle HTTP health alone prove only `Serving locally`. Opening the default route, asserting shadow-root content, and exercising hot reload remain the right checks when the task includes UI, runtime behavior, or visual acceptance, but they are not required merely to prove mounting.
485
+ 3. **Local development acceptance**: Require the running CLI to name the intended desktop and report `Mounted in <target desktop>`, which proves the exact nonce-backed session entered that visible desktop's final `Local development` sidebar model. Bundle HTTP health alone proves only `Serving locally`. When the task includes UI, runtime behavior, or visual acceptance, open the default route and also require `Rendered in <target desktop>` before claiming the app works.
488
486
  4. **Post-deploy**: Verify the deployed bundle via `/portal_views/get` -> `runtime_descriptor.bundle.js_url`, then verify the app renders in the portal. The portal renders app bundles directly as React components, so the fastest verification is navigating to the app page in the portal.
489
487
 
490
488
  ### Headless harness verification
@@ -511,15 +509,6 @@ Run `npx --package @notis_ai/cli@latest -- notis apps verify` after `npx --packa
511
509
 
512
510
  - **Deploy fails with network error**: Backend server not running. Use `npx --package @notis_ai/cli@latest -- notis apps deploy --direct` or start the server.
513
511
  - **App shows old code after deploy**: Bundle cache is stale. Hard refresh (Cmd+Shift+R) or clear site data in DevTools.
514
- - **App is missing from Local development**: First confirm the app lives in the directory owned by the long-running supervisor (`apps/<slug>/` in the Notis monorepo), the current worktree's Electron app is running, and `npx --package @notis_ai/cli@latest -- notis apps dev` is bound to loopback. Then check the **dev-session registry**: `apps dev` records its session in a JSON registry the desktop polls, and the desktop reads a **per-workspace** file at `<workspace>/.context/app-dev-sessions.json` (it sets `NOTIS_APP_DEV_SESSIONS_FILE` on its own runs). An agent-launched `apps dev` that does not resolve that file writes to the global `~/.notis/app-dev-sessions.json` instead the bundle serves fine but the app never shows in that workspace's sidebar. Verify your session is in the workspace file:
515
- ```bash
516
- cat "$CONDUCTOR_WORKSPACE_PATH/.context/app-dev-sessions.json" # your devSlug must be present with a fresh lastHeartbeatAt
517
- ```
518
- If it is missing, restart `apps dev` pointed at that file:
519
- ```bash
520
- NOTIS_APP_DEV_SESSIONS_FILE="$CONDUCTOR_WORKSPACE_PATH/.context/app-dev-sessions.json" \
521
- npx --package @notis_ai/cli@latest -- notis apps dev --no-open
522
- ```
523
- Current CLI auto-resolves the nearest `.context` registry, so the explicit env is only a fallback for older CLIs. Startup and heartbeat preserve other sessions, so this coexists with the desktop's own dev sessions. If the registry row has a `targetAppId`, that id must resolve to an installed app whose manifest is not `is_dev: true`; never repair this by linking to another development-runtime id. Restart the current CLI and let it clear stale hidden/deleted targets automatically. Then wait for `Mounted in Notis`; Electron writes the matching nonce acknowledgment beside the registry only after the app reaches the final Local development sidebar model. A corrected registry row or HTTP 200 is not mount proof by itself. Open and render the route separately only when validating UI or runtime behavior.
512
+ - **App is missing from Local development**: Read the `Target desktop` line from `apps dev`, then bring that exact Notis app forward and confirm it is signed into the same account reported by `npx --package @notis_ai/cli@latest -- notis whoami`. Keep `apps dev` running. If it still says only `Serving locally`, run `npx --package @notis_ai/cli@latest -- notis doctor`, restart the target desktop, and retry `apps dev`. Do not redirect internal registry files manually: the CLI selects the normal Notis/Notis Beta target from the active profile and selects a source-workspace desktop only when an active workspace runtime identifies it. Wait for `Mounted in <target desktop>` before claiming success; when validating UI or runtime behavior, open the route and wait for `Rendered in <target desktop>` too.
524
513
  - **`LOCAL_NOTIS_DATABASE_QUERY` returns empty documents**: Check that the database ID passed to the tool matches the intended database. Use `npx --package @notis_ai/cli@latest -- notis tools exec LOCAL_NOTIS_DATABASE_LIST_DATABASES --arguments '{}'` to verify the ID; use the database slug only as a fallback.
525
514
  - **Properties are `undefined`**: Keep app-local result types for `useTool<TArgs, TResult>` and guard optional nested properties when reading live data.
@@ -8,7 +8,7 @@ Important: `notis apps deploy` updates the linked installed app. It is not an ap
8
8
 
9
9
  ## Setup
10
10
 
11
- Install and sign into Notis Desktop to keep the CLI profile current. Run the CLI through NPX, for example `npx --package @notis_ai/cli@latest -- notis apps list`. Related skills such as `notis-cli` and `notis-apps` are delivered through normal Notis skill sync for the signed-in user.
11
+ Sign into Notis Desktop or run `npx --package @notis_ai/cli@latest -- notis login` to authorize the CLI. Run commands through NPX, for example `npx --package @notis_ai/cli@latest -- notis apps list`.
12
12
 
13
13
  For CI, hosted agents, or internal scripts, pass a non-persisted token with `NOTIS_JWT=<token>` and use `--api-base <server-url>` when targeting a non-default server.
14
14
 
@@ -4,7 +4,7 @@ Use the generic `notis tools` workflow through NPX for native Notis Database ope
4
4
 
5
5
  ## Setup
6
6
 
7
- Install and sign into Notis Desktop to keep the CLI profile current. Run the CLI through NPX, for example `npx --package @notis_ai/cli@latest -- notis tools search "list Notis databases"`.
7
+ Sign into Notis Desktop or run `npx --package @notis_ai/cli@latest -- notis login` to authorize the CLI. Run commands through NPX, for example `npx --package @notis_ai/cli@latest -- notis tools search "list Notis databases"`.
8
8
 
9
9
  For CI, hosted agents, or internal scripts, pass a non-persisted token with `NOTIS_JWT=<token>` and use `--api-base <server-url>` when targeting a non-default server.
10
10
 
@@ -44,15 +44,18 @@ import {
44
44
  runHarnessRoute,
45
45
  } from '../runtime/agent-browser.js';
46
46
  import {
47
+ getAppDevSessionsFile,
47
48
  heartbeatAppDevSession,
48
49
  removeAppDevSession,
49
50
  upsertAppDevSessions,
50
51
  waitForAppDevSessionMountAcknowledgements,
52
+ waitForAppDevSessionRenderAcknowledgements,
51
53
  } from '../runtime/app-dev-sessions.js';
52
54
  import { getAvailablePort } from '../runtime/ports.js';
53
55
  import { getCliMode } from '../runtime/cli-mode.js';
54
56
  import { composeStoreScreenshot } from '../runtime/store-screenshot.js';
55
57
  import { httpRequest } from '../runtime/transport.js';
58
+ import { ensureFreshOAuthCredential } from '../runtime/oauth.js';
56
59
  import {
57
60
  localNotisToolSlug,
58
61
  nextIdempotencyKey,
@@ -140,13 +143,24 @@ function decodeJwtSub(jwt) {
140
143
  }
141
144
  }
142
145
 
143
- function openInBrowser(url) {
144
- const platform = process.platform;
146
+ export function developmentDesktopOpenCommand(
147
+ url,
148
+ {
149
+ platform = process.platform,
150
+ appName = null,
151
+ bundleId = null,
152
+ scheme = 'notis',
153
+ } = {},
154
+ ) {
145
155
  let command;
146
156
  let args;
147
157
  if (platform === 'darwin') {
148
158
  command = 'open';
149
- args = [url];
159
+ args = bundleId && scheme === 'notis'
160
+ ? ['-b', bundleId, url]
161
+ : appName && scheme === 'notis'
162
+ ? ['-a', appName, url]
163
+ : [url];
150
164
  } else if (platform === 'win32') {
151
165
  command = 'cmd';
152
166
  args = ['/c', 'start', '', url];
@@ -154,10 +168,14 @@ function openInBrowser(url) {
154
168
  command = 'xdg-open';
155
169
  args = [url];
156
170
  }
171
+ return { command, args };
172
+ }
173
+
174
+ function openInBrowser(url, options = {}) {
175
+ const { command, args } = developmentDesktopOpenCommand(url, options);
157
176
  try {
158
- const child = spawn(command, args, { stdio: 'ignore', detached: true });
177
+ const child = spawn(command, args, { stdio: 'ignore' });
159
178
  child.on('error', () => {});
160
- child.unref();
161
179
  } catch {
162
180
  // Non-fatal. The URL is printed in the CLI output.
163
181
  }
@@ -207,21 +225,55 @@ function pickDefaultRouteSlug(manifest) {
207
225
 
208
226
  const DESKTOP_DEEP_LINK_SCHEME_PATTERN = /^[a-z][a-z0-9-]*$/;
209
227
 
210
- export function resolveDevelopmentDesktopScheme(env = process.env) {
228
+ export function resolveDevelopmentDesktopScheme(env = process.env, worktreeRuntime = null) {
211
229
  // Mirror the desktop app's own scheme resolution (electron main + forge config):
212
230
  // local dev launches register `notis-dev`, while installed prod/beta builds claim
213
231
  // `notis`. Hardcoding `notis` here is what made `apps dev` open the installed
214
232
  // prod/beta app instead of the local dev app.
215
- const scheme = (env.NOTIS_DESKTOP_DEEP_LINK_SCHEME || '').trim();
233
+ const scheme = (
234
+ worktreeRuntime?.desktop_deep_link_scheme
235
+ || env.NOTIS_DESKTOP_DEEP_LINK_SCHEME
236
+ || ''
237
+ ).trim();
216
238
  return DESKTOP_DEEP_LINK_SCHEME_PATTERN.test(scheme) ? scheme : 'notis';
217
239
  }
218
240
 
241
+ export function resolveDevelopmentDesktopAppName(runtime = {}) {
242
+ const explicit = String(
243
+ runtime.desktopAppName
244
+ || runtime.worktreeRuntime?.desktop_app_name
245
+ || '',
246
+ ).trim();
247
+ if (explicit) {
248
+ return explicit;
249
+ }
250
+ try {
251
+ return new URL(runtime.apiBase).hostname === 'api-beta.notis.ai'
252
+ ? 'Notis Beta'
253
+ : 'Notis';
254
+ } catch {
255
+ return 'Notis';
256
+ }
257
+ }
258
+
259
+ export function resolveDevelopmentDesktopBundleId(runtime = {}) {
260
+ return resolveDevelopmentDesktopAppName(runtime) === 'Notis Beta'
261
+ ? 'ai.notis.desktop.beta'
262
+ : 'ai.notis.desktop';
263
+ }
264
+
219
265
  export function buildDevelopmentDesktopUrl(appHref = null, scheme = 'notis') {
220
266
  const route = String(appHref || '/store').replace(/^\/+/, '');
221
267
  const normalizedScheme = DESKTOP_DEEP_LINK_SCHEME_PATTERN.test(scheme) ? scheme : 'notis';
222
268
  return `${normalizedScheme}://${route || 'store'}`;
223
269
  }
224
270
 
271
+ export function buildMountedDevelopmentDesktopUrl(appHref, scheme, sessionId) {
272
+ const url = new URL(buildDevelopmentDesktopUrl(appHref, scheme));
273
+ url.searchParams.set('notis_dev_session', sessionId);
274
+ return url.toString();
275
+ }
276
+
225
277
  export function shouldOpenDevelopmentTab(options = {}) {
226
278
  // Commander stores the negatable `--no-open` flag as `options.open === false`;
227
279
  // it never sets `options.noOpen`. Reading the non-existent `noOpen` key meant
@@ -230,8 +282,17 @@ export function shouldOpenDevelopmentTab(options = {}) {
230
282
  return options.open !== false;
231
283
  }
232
284
 
233
- function buildAppHref({ appSlug, appId, manifest }) {
234
- const originlessBase = `/apps/${appSlug}-${appId}`;
285
+ export function buildDevelopmentAppHref({
286
+ appSlug,
287
+ appId,
288
+ devSlug,
289
+ targetAppId = null,
290
+ targetAppSlug = null,
291
+ manifest,
292
+ }) {
293
+ const routeAppId = `${targetAppId || appId}__local_dev__${devSlug}`;
294
+ const routeAppSlug = targetAppSlug || devSlug || appSlug;
295
+ const originlessBase = `/apps/${routeAppSlug}-${routeAppId}`;
235
296
  const routeSlug = pickDefaultRouteSlug(manifest);
236
297
  return routeSlug ? `${originlessBase}/${routeSlug}` : originlessBase;
237
298
  }
@@ -499,8 +560,9 @@ export async function ensureDevInstall({
499
560
  const manifest = buildManifestForDev(appConfig);
500
561
  const skills = resolveConfiguredAppSkills(appConfig, projectDir);
501
562
  let linkedState = readLinkedState(projectDir);
563
+ let linkedApp = null;
502
564
  if (linkedState?.app_id) {
503
- const linkedApp = await getAccessibleApp(ctx.runtime, linkedState.app_id, runTool);
565
+ linkedApp = await getAccessibleApp(ctx.runtime, linkedState.app_id, runTool);
504
566
  if (linkedApp?.manifest?.is_dev === true) {
505
567
  const { app_id: legacyDevAppId, linked_at: _linkedAt, deployed_at: _deployedAt, version: _version, ...rest } = linkedState;
506
568
  const devAppId = linkedState.dev_app_id || legacyDevAppId;
@@ -512,6 +574,7 @@ export async function ensureDevInstall({
512
574
  } : {}),
513
575
  };
514
576
  writeLinkedState(projectDir, linkedState);
577
+ linkedApp = null;
515
578
  }
516
579
  }
517
580
  const ensureArguments = buildEnsureDevInstallArguments({ appConfig, manifest, linkedState, skills });
@@ -544,6 +607,7 @@ export async function ensureDevInstall({
544
607
  created: ensureResult.payload.created || false,
545
608
  linkedAppId: linkedState?.app_id || null,
546
609
  targetAppId: linkedState?.app_id || null,
610
+ targetAppSlug: linkedApp?.slug || null,
547
611
  databaseMaterialization: ensureResult.payload.database_materialization || { created: [], unresolved: [] },
548
612
  };
549
613
  }
@@ -725,6 +789,15 @@ async function appsDevHandler(ctx) {
725
789
  throw usageError('Could not determine the current user from the CLI auth token. Open the Notis desktop app, sign in, and retry.');
726
790
  }
727
791
  const apiBase = String(ctx.runtime.apiBase || '').replace(/\/$/, '');
792
+ const sessionsFilePath = getAppDevSessionsFile(
793
+ ctx.runtime.worktreeRuntime?.app_dev_sessions_file,
794
+ );
795
+ const desktopScheme = resolveDevelopmentDesktopScheme(
796
+ process.env,
797
+ ctx.runtime.worktreeRuntime,
798
+ );
799
+ const desktopAppName = resolveDevelopmentDesktopAppName(ctx.runtime);
800
+ const desktopBundleId = resolveDevelopmentDesktopBundleId(ctx.runtime);
728
801
  const sessionId = randomUUID();
729
802
 
730
803
  const candidates = [];
@@ -780,16 +853,25 @@ async function appsDevHandler(ctx) {
780
853
  ...app,
781
854
  bundleBaseUrl,
782
855
  mountNonce: randomUUID(),
783
- appHref: buildAppHref({
856
+ appHref: buildDevelopmentAppHref({
784
857
  appSlug: app.slug,
785
858
  appId: app.appId,
859
+ devSlug: app.devSlug,
860
+ targetAppId: app.targetAppId,
861
+ targetAppSlug: app.targetAppSlug,
786
862
  manifest: app.manifest,
787
863
  }),
788
864
  };
789
865
  });
790
866
  const developmentTabUrl = buildDevelopmentDesktopUrl(
791
867
  apps[0]?.appHref,
792
- resolveDevelopmentDesktopScheme(),
868
+ desktopScheme,
869
+ );
870
+ const desktopWakeUrl = buildDevelopmentDesktopUrl('/manage', desktopScheme);
871
+ const mountedDevelopmentTabUrl = buildMountedDevelopmentDesktopUrl(
872
+ apps[0]?.appHref,
873
+ desktopScheme,
874
+ sessionId,
793
875
  );
794
876
  const warnings = databaseMaterializationWarnings(apps);
795
877
 
@@ -802,6 +884,7 @@ async function appsDevHandler(ctx) {
802
884
  userId: identity,
803
885
  })),
804
886
  port,
887
+ sessionsFilePath,
805
888
  });
806
889
 
807
890
  try {
@@ -818,7 +901,8 @@ async function appsDevHandler(ctx) {
818
901
  projectDir: app.projectDir,
819
902
  startedAt: now,
820
903
  lastHeartbeatAt: now,
821
- })));
904
+ desktopAppName,
905
+ })), sessionsFilePath);
822
906
  } catch (error) {
823
907
  try {
824
908
  await devServer.close();
@@ -830,7 +914,7 @@ async function appsDevHandler(ctx) {
830
914
 
831
915
  let heartbeatTimer = setInterval(() => {
832
916
  try {
833
- heartbeatAppDevSession(sessionId, new Date().toISOString());
917
+ heartbeatAppDevSession(sessionId, new Date().toISOString(), sessionsFilePath);
834
918
  } catch (error) {
835
919
  const message = error instanceof Error ? error.message : String(error);
836
920
  process.stderr.write(`[notis apps dev] heartbeat failed: ${message}\n`);
@@ -852,6 +936,8 @@ async function appsDevHandler(ctx) {
852
936
  development_url: developmentTabUrl,
853
937
  session_id: sessionId,
854
938
  mount_status: 'serving',
939
+ render_status: 'waiting_for_route',
940
+ desktop_target: desktopAppName,
855
941
  identity,
856
942
  apps: apps.map((app) => ({
857
943
  slug: app.devSlug,
@@ -869,36 +955,67 @@ async function appsDevHandler(ctx) {
869
955
  warnings,
870
956
  humanSummary: [
871
957
  `Running apps dev against ${apiBase} as ${identity} (mode: ${mode})`,
958
+ `Target desktop: ${desktopAppName}`,
872
959
  '',
873
960
  `Open in desktop: ${developmentTabUrl}`,
874
961
  '',
875
962
  ...apps.map((app) => ` ${app.name.padEnd(24)} ${app.bundleBaseUrl} -> ${app.appHref}`),
876
963
  '',
877
- `Serving locally; waiting for Notis to mount ${apps.length === 1 ? 'the app' : `${apps.length} apps`}.`,
964
+ `Serving locally; waiting for ${desktopAppName} to mount ${apps.length === 1 ? 'the app' : `${apps.length} apps`}.`,
878
965
  '',
879
966
  'Press Ctrl-C to stop.',
880
967
  ].join('\n'),
881
968
  });
882
969
 
883
970
  if (shouldOpenDevelopmentTab(ctx.options)) {
884
- openInBrowser(developmentTabUrl);
971
+ openInBrowser(desktopWakeUrl, {
972
+ appName: desktopAppName,
973
+ bundleId: desktopBundleId,
974
+ scheme: desktopScheme,
975
+ });
885
976
  }
886
977
 
887
978
  void (async () => {
888
- let result = await waitForAppDevSessionMountAcknowledgements(expectedMountAcknowledgements);
979
+ let result = await waitForAppDevSessionMountAcknowledgements(expectedMountAcknowledgements, {
980
+ sessionsFilePath,
981
+ });
889
982
  if (!result.mounted) {
890
983
  process.stderr.write(
891
- `[notis apps dev] Serving locally, but Notis has not acknowledged ${result.missing.length === 1 ? 'the app' : `${result.missing.length} apps`} in its Local development sidebar yet. Keep this command running and open Notis.\n`,
984
+ `[notis apps dev] Serving locally, but ${desktopAppName} has not acknowledged ${result.missing.length === 1 ? 'the app' : `${result.missing.length} apps`} in its Local development sidebar yet. Keep this command running and open ${desktopAppName}.\n`,
892
985
  );
893
986
  }
894
987
  while (!result.mounted) {
895
988
  result = await waitForAppDevSessionMountAcknowledgements(expectedMountAcknowledgements, {
989
+ sessionsFilePath,
990
+ timeoutMs: 60_000,
991
+ pollIntervalMs: 250,
992
+ });
993
+ }
994
+ process.stderr.write(
995
+ `[notis apps dev] Mounted in ${desktopAppName}: ${apps.map((app) => app.name).join(', ')}.\n`,
996
+ );
997
+ if (shouldOpenDevelopmentTab(ctx.options)) {
998
+ openInBrowser(mountedDevelopmentTabUrl, {
999
+ appName: desktopAppName,
1000
+ bundleId: desktopBundleId,
1001
+ scheme: desktopScheme,
1002
+ });
1003
+ }
1004
+
1005
+ const firstAppRender = expectedMountAcknowledgements.slice(0, 1);
1006
+ let renderResult = await waitForAppDevSessionRenderAcknowledgements(firstAppRender, {
1007
+ sessionsFilePath,
1008
+ timeoutMs: 0,
1009
+ });
1010
+ while (!renderResult.mounted) {
1011
+ renderResult = await waitForAppDevSessionRenderAcknowledgements(firstAppRender, {
1012
+ sessionsFilePath,
896
1013
  timeoutMs: 60_000,
897
1014
  pollIntervalMs: 250,
898
1015
  });
899
1016
  }
900
1017
  process.stderr.write(
901
- `[notis apps dev] Mounted in Notis: ${apps.map((app) => app.name).join(', ')}.\n`,
1018
+ `[notis apps dev] Rendered in ${desktopAppName}: ${apps[0].name}.\n`,
902
1019
  );
903
1020
  })().catch((error) => {
904
1021
  const message = error instanceof Error ? error.message : String(error);
@@ -915,7 +1032,7 @@ async function appsDevHandler(ctx) {
915
1032
  heartbeatTimer = null;
916
1033
  }
917
1034
  try {
918
- removeAppDevSession(sessionId);
1035
+ removeAppDevSession(sessionId, sessionsFilePath);
919
1036
  } catch {
920
1037
  // ignore cleanup failures during shutdown
921
1038
  }
@@ -971,6 +1088,12 @@ async function appsVerifyHandler(ctx) {
971
1088
 
972
1089
  let linkedState = null;
973
1090
  if (mode === 'live') {
1091
+ if (
1092
+ ctx.runtime.credentialKind === 'oauth'
1093
+ && !await ensureFreshOAuthCredential(ctx.runtime)
1094
+ ) {
1095
+ throw usageError('Live verify mode requires a current OAuth grant. Run `notis login` and retry.');
1096
+ }
974
1097
  if (!ctx.runtime.jwt) {
975
1098
  throw usageError('Live verify mode requires CLI auth. Open the Notis desktop app, sign in, and retry.');
976
1099
  }
@@ -1185,6 +1308,12 @@ async function appsScreenshotHandler(ctx) {
1185
1308
 
1186
1309
  let linkedState = null;
1187
1310
  if (mode === 'live') {
1311
+ if (
1312
+ ctx.runtime.credentialKind === 'oauth'
1313
+ && !await ensureFreshOAuthCredential(ctx.runtime)
1314
+ ) {
1315
+ throw usageError('Live mode requires a current OAuth grant. Run `notis login` and retry.');
1316
+ }
1188
1317
  if (!ctx.runtime.jwt) {
1189
1318
  throw usageError('Live mode requires CLI auth. Open the Notis desktop app, sign in, and retry.');
1190
1319
  }
@@ -1423,6 +1552,12 @@ async function appsPullHandler(ctx) {
1423
1552
  toolName: GET_APP_TOOL,
1424
1553
  arguments_: { app_id: appId },
1425
1554
  });
1555
+ if (
1556
+ ctx.runtime.credentialKind === 'oauth'
1557
+ && !await ensureFreshOAuthCredential(ctx.runtime)
1558
+ ) {
1559
+ throw usageError('Pulling app source requires a current OAuth grant. Run `notis login` and retry.');
1560
+ }
1426
1561
  const app = result.payload?.app || {};
1427
1562
  const defaultDir = app.slug || slugify(app.name) || appId;
1428
1563
  const targetDir = resolveProjectDir(ctx.args.dir || defaultDir);