@notis_ai/cli 0.2.7 → 0.2.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (183) hide show
  1. package/README.md +56 -2
  2. package/dist/scaffolds/notis-database/app/page.tsx +38 -41
  3. package/dist/scaffolds/notis-database/lib/types.ts +5 -7
  4. package/dist/scaffolds/notis-database/metadata/screenshot-1.png +0 -0
  5. package/dist/scaffolds/notis-database/metadata/screenshot-2.png +0 -0
  6. package/dist/scaffolds/notis-database/metadata/screenshot-3.png +0 -0
  7. package/dist/scaffolds/notis-database/metadata/screenshot-4.png +0 -0
  8. package/dist/scaffolds/notis-database/metadata/screenshot-5.png +0 -0
  9. package/dist/scaffolds/notis-database/metadata/screenshot-fixtures.json +1839 -0
  10. package/dist/scaffolds/notis-database/notis.config.ts +48 -1
  11. package/dist/scaffolds/notis-database/package-lock.json +3935 -0
  12. package/dist/scaffolds/notis-database/package.json +2 -1
  13. package/dist/scaffolds/{notis-affiliate-prospects → notis-database}/packages/sdk/package.json +4 -0
  14. package/dist/scaffolds/notis-database/packages/sdk/src/components/DocumentEditor.tsx +93 -0
  15. package/dist/scaffolds/notis-database/packages/sdk/src/components/Markdown.tsx +60 -0
  16. package/dist/scaffolds/{notis-affiliate-prospects → notis-database}/packages/sdk/src/components/MultiSelectActionBar.tsx +7 -2
  17. package/dist/scaffolds/notis-database/packages/sdk/src/config.ts +196 -0
  18. package/dist/scaffolds/notis-database/packages/sdk/src/documents.ts +229 -0
  19. package/dist/scaffolds/notis-database/packages/sdk/src/hooks/useDatabaseSchema.ts +85 -0
  20. package/dist/scaffolds/notis-database/packages/sdk/src/hooks/useDocument.ts +78 -0
  21. package/dist/scaffolds/notis-database/packages/sdk/src/hooks/useDocuments.ts +121 -0
  22. package/dist/scaffolds/{notis-affiliate-prospects → notis-database}/packages/sdk/src/hooks/useMultiSelect.ts +38 -2
  23. package/dist/scaffolds/notis-database/packages/sdk/src/hooks/useUpsertDocument.ts +95 -0
  24. package/dist/scaffolds/{notis-affiliate-prospects → notis-database}/packages/sdk/src/index.ts +30 -1
  25. package/dist/scaffolds/{notis-affiliate-prospects → notis-database}/packages/sdk/src/runtime.ts +76 -17
  26. package/dist/scaffolds/notis-database/packages/sdk/src/styles.css +186 -0
  27. package/dist/scaffolds/notis-database/src/mock-runtime.ts +4 -1
  28. package/dist/scaffolds/notis-journal/CHANGELOG.md +4 -0
  29. package/dist/scaffolds/notis-journal/app/page.tsx +1 -0
  30. package/dist/scaffolds/notis-journal/package-lock.json +4615 -0
  31. package/dist/scaffolds/notis-journal/package.json +2 -2
  32. package/dist/scaffolds/notis-journal/packages/sdk/src/config.ts +52 -1
  33. package/dist/scaffolds/notis-notes/app/page.tsx +39 -46
  34. package/dist/scaffolds/notis-notes/app/phosphor-icons.ts +596 -0
  35. package/dist/scaffolds/notis-notes/lib/visible-properties.ts +144 -0
  36. package/dist/scaffolds/notis-notes/metadata/screenshot-1.png +0 -0
  37. package/dist/scaffolds/notis-notes/metadata/screenshot-2.png +0 -0
  38. package/dist/scaffolds/notis-notes/metadata/screenshot-3.png +0 -0
  39. package/dist/scaffolds/notis-notes/metadata/screenshot-4.png +0 -0
  40. package/dist/scaffolds/notis-notes/metadata/screenshot-5.png +0 -0
  41. package/dist/scaffolds/notis-notes/metadata/screenshot-fixtures.json +752 -0
  42. package/dist/scaffolds/notis-notes/notis.config.ts +48 -3
  43. package/dist/scaffolds/notis-notes/package-lock.json +4636 -0
  44. package/dist/scaffolds/notis-notes/package.json +7 -3
  45. package/dist/scaffolds/notis-notes/packages/sdk/package.json +36 -0
  46. package/dist/scaffolds/notis-notes/packages/sdk/src/components/DocumentEditor.tsx +93 -0
  47. package/dist/scaffolds/notis-notes/packages/sdk/src/components/Markdown.tsx +60 -0
  48. package/dist/scaffolds/notis-notes/packages/sdk/src/components/MultiSelectActionBar.tsx +278 -0
  49. package/dist/scaffolds/notis-notes/packages/sdk/src/components/MultiSelectCheckbox.tsx +91 -0
  50. package/dist/scaffolds/notis-notes/packages/sdk/src/components/MultiSelectDragOverlay.tsx +39 -0
  51. package/dist/scaffolds/notis-notes/packages/sdk/src/config.ts +196 -0
  52. package/dist/scaffolds/notis-notes/packages/sdk/src/documents.ts +229 -0
  53. package/dist/scaffolds/notis-notes/packages/sdk/src/hooks/useBackend.ts +41 -0
  54. package/dist/scaffolds/notis-notes/packages/sdk/src/hooks/useDatabaseSchema.ts +85 -0
  55. package/dist/scaffolds/notis-notes/packages/sdk/src/hooks/useDocument.ts +78 -0
  56. package/dist/scaffolds/notis-notes/packages/sdk/src/hooks/useDocuments.ts +121 -0
  57. package/dist/scaffolds/notis-notes/packages/sdk/src/hooks/useMultiSelect.ts +539 -0
  58. package/dist/scaffolds/notis-notes/packages/sdk/src/hooks/useNotis.ts +34 -0
  59. package/dist/scaffolds/notis-notes/packages/sdk/src/hooks/useNotisNavigation.ts +49 -0
  60. package/dist/scaffolds/notis-notes/packages/sdk/src/hooks/useTool.ts +64 -0
  61. package/dist/scaffolds/notis-notes/packages/sdk/src/hooks/useTools.ts +56 -0
  62. package/dist/scaffolds/notis-notes/packages/sdk/src/hooks/useTopBarSearch.ts +73 -0
  63. package/dist/scaffolds/notis-notes/packages/sdk/src/hooks/useUpsertDocument.ts +95 -0
  64. package/dist/scaffolds/notis-notes/packages/sdk/src/index.ts +83 -0
  65. package/dist/scaffolds/notis-notes/packages/sdk/src/provider.tsx +43 -0
  66. package/dist/scaffolds/notis-notes/packages/sdk/src/runtime.ts +220 -0
  67. package/dist/scaffolds/notis-notes/packages/sdk/src/styles.css +186 -0
  68. package/dist/scaffolds/notis-notes/packages/sdk/src/ui.ts +15 -0
  69. package/dist/scaffolds/notis-notes/packages/sdk/src/vite.ts +56 -0
  70. package/dist/scaffolds/notis-notes/packages/sdk/tsconfig.json +15 -0
  71. package/dist/scaffolds/notis-random/CHANGELOG.md +12 -2
  72. package/dist/scaffolds/notis-random/app/history/page.tsx +5 -4
  73. package/dist/scaffolds/notis-random/app/page.tsx +141 -74
  74. package/dist/scaffolds/notis-random/components/ui/button.tsx +50 -0
  75. package/dist/scaffolds/notis-random/components/ui/card.tsx +16 -0
  76. package/dist/scaffolds/notis-random/components/ui/input.tsx +23 -0
  77. package/dist/scaffolds/notis-random/lib/notis-tools.ts +26 -7
  78. package/dist/scaffolds/notis-random/lib/rng.ts +179 -19
  79. package/dist/scaffolds/notis-random/lib/roll-record.ts +103 -16
  80. package/dist/scaffolds/notis-random/metadata/screenshot-1.png +0 -0
  81. package/dist/scaffolds/notis-random/metadata/screenshot-2.png +0 -0
  82. package/dist/scaffolds/notis-random/metadata/screenshot-3.png +0 -0
  83. package/dist/scaffolds/notis-random/metadata/screenshot-4.png +0 -0
  84. package/dist/scaffolds/notis-random/metadata/screenshot-5.png +0 -0
  85. package/dist/scaffolds/notis-random/metadata/screenshot-fixtures.json +753 -0
  86. package/dist/scaffolds/notis-random/notis.config.ts +47 -3
  87. package/dist/scaffolds/notis-random/package-lock.json +4513 -0
  88. package/dist/scaffolds/notis-random/package.json +6 -2
  89. package/dist/scaffolds/notis-random/packages/sdk/package.json +36 -0
  90. package/dist/scaffolds/notis-random/packages/sdk/src/components/DocumentEditor.tsx +93 -0
  91. package/dist/scaffolds/notis-random/packages/sdk/src/components/Markdown.tsx +60 -0
  92. package/dist/scaffolds/notis-random/packages/sdk/src/components/MultiSelectActionBar.tsx +278 -0
  93. package/dist/scaffolds/notis-random/packages/sdk/src/components/MultiSelectCheckbox.tsx +91 -0
  94. package/dist/scaffolds/notis-random/packages/sdk/src/components/MultiSelectDragOverlay.tsx +39 -0
  95. package/dist/scaffolds/notis-random/packages/sdk/src/config.ts +196 -0
  96. package/dist/scaffolds/notis-random/packages/sdk/src/documents.ts +229 -0
  97. package/dist/scaffolds/notis-random/packages/sdk/src/hooks/useBackend.ts +41 -0
  98. package/dist/scaffolds/notis-random/packages/sdk/src/hooks/useDatabaseSchema.ts +85 -0
  99. package/dist/scaffolds/notis-random/packages/sdk/src/hooks/useDocument.ts +78 -0
  100. package/dist/scaffolds/notis-random/packages/sdk/src/hooks/useDocuments.ts +121 -0
  101. package/dist/scaffolds/notis-random/packages/sdk/src/hooks/useMultiSelect.ts +539 -0
  102. package/dist/scaffolds/notis-random/packages/sdk/src/hooks/useNotis.ts +34 -0
  103. package/dist/scaffolds/notis-random/packages/sdk/src/hooks/useNotisNavigation.ts +49 -0
  104. package/dist/scaffolds/notis-random/packages/sdk/src/hooks/useTool.ts +64 -0
  105. package/dist/scaffolds/notis-random/packages/sdk/src/hooks/useTools.ts +56 -0
  106. package/dist/scaffolds/notis-random/packages/sdk/src/hooks/useTopBarSearch.ts +73 -0
  107. package/dist/scaffolds/notis-random/packages/sdk/src/hooks/useUpsertDocument.ts +95 -0
  108. package/dist/scaffolds/notis-random/packages/sdk/src/index.ts +83 -0
  109. package/dist/scaffolds/notis-random/packages/sdk/src/provider.tsx +43 -0
  110. package/dist/scaffolds/notis-random/packages/sdk/src/runtime.ts +220 -0
  111. package/dist/scaffolds/notis-random/packages/sdk/src/styles.css +186 -0
  112. package/dist/scaffolds/notis-random/packages/sdk/src/ui.ts +15 -0
  113. package/dist/scaffolds/notis-random/packages/sdk/src/vite.ts +56 -0
  114. package/dist/scaffolds/notis-random/packages/sdk/tsconfig.json +15 -0
  115. package/dist/scaffolds/notis-random/tailwind.config.ts +8 -1
  116. package/dist/scaffolds.json +8 -17
  117. package/package.json +2 -1
  118. package/skills/notis-apps/SKILL.md +438 -144
  119. package/skills/notis-apps/cli.md +16 -1
  120. package/skills/notis-cli/SKILL.md +11 -4
  121. package/skills/notis-onboarding/BRIEF.md +93 -0
  122. package/skills/notis-query/cli.md +1 -1
  123. package/src/cli.js +50 -12
  124. package/src/command-specs/apps.js +309 -41
  125. package/src/command-specs/auth.js +107 -0
  126. package/src/command-specs/index.js +4 -0
  127. package/src/command-specs/meta.js +68 -6
  128. package/src/command-specs/onboarding.js +304 -0
  129. package/src/command-specs/tools.js +8 -4
  130. package/src/runtime/app-dev-server.js +3 -2
  131. package/src/runtime/app-dev-sessions.js +14 -40
  132. package/src/runtime/app-platform.js +65 -4
  133. package/src/runtime/desktop-auth.js +86 -17
  134. package/src/runtime/errors.js +1 -0
  135. package/src/runtime/oauth.js +1105 -0
  136. package/src/runtime/output.js +16 -2
  137. package/src/runtime/profiles.js +542 -18
  138. package/src/runtime/telemetry.js +92 -0
  139. package/src/runtime/transport.js +66 -11
  140. package/template/.harness/index.html.tmpl +30 -2
  141. package/template/packages/sdk/src/config.ts +38 -1
  142. package/dist/scaffolds/notis-affiliate-prospects/CHANGELOG.md +0 -7
  143. package/dist/scaffolds/notis-affiliate-prospects/README.md +0 -20
  144. package/dist/scaffolds/notis-affiliate-prospects/app/globals.css +0 -42
  145. package/dist/scaffolds/notis-affiliate-prospects/app/layout.tsx +0 -7
  146. package/dist/scaffolds/notis-affiliate-prospects/app/page.tsx +0 -248
  147. package/dist/scaffolds/notis-affiliate-prospects/app/prospects/page.tsx +0 -134
  148. package/dist/scaffolds/notis-affiliate-prospects/app/segments/page.tsx +0 -108
  149. package/dist/scaffolds/notis-affiliate-prospects/components/ui/card.tsx +0 -35
  150. package/dist/scaffolds/notis-affiliate-prospects/index.html +0 -12
  151. package/dist/scaffolds/notis-affiliate-prospects/lib/affiliate-data.ts +0 -218
  152. package/dist/scaffolds/notis-affiliate-prospects/lib/demo-prospects.ts +0 -52
  153. package/dist/scaffolds/notis-affiliate-prospects/lib/notis-tools.ts +0 -100
  154. package/dist/scaffolds/notis-affiliate-prospects/lib/utils.ts +0 -25
  155. package/dist/scaffolds/notis-affiliate-prospects/metadata/screenshot-1.png +0 -0
  156. package/dist/scaffolds/notis-affiliate-prospects/metadata/screenshot-2.png +0 -0
  157. package/dist/scaffolds/notis-affiliate-prospects/metadata/screenshot-3.png +0 -0
  158. package/dist/scaffolds/notis-affiliate-prospects/metadata/screenshot-fixtures.json +0 -187
  159. package/dist/scaffolds/notis-affiliate-prospects/notis.config.ts +0 -65
  160. package/dist/scaffolds/notis-affiliate-prospects/package.json +0 -32
  161. package/dist/scaffolds/notis-affiliate-prospects/packages/sdk/src/config.ts +0 -90
  162. package/dist/scaffolds/notis-affiliate-prospects/packages/sdk/src/hooks/useDatabase.ts +0 -76
  163. package/dist/scaffolds/notis-affiliate-prospects/packages/sdk/src/hooks/useUpsertDocument.ts +0 -50
  164. package/dist/scaffolds/notis-affiliate-prospects/packages/sdk/src/styles.css +0 -38
  165. package/dist/scaffolds/notis-affiliate-prospects/postcss.config.mjs +0 -6
  166. package/dist/scaffolds/notis-affiliate-prospects/src/dev-main.tsx +0 -72
  167. package/dist/scaffolds/notis-affiliate-prospects/src/mock-runtime.ts +0 -242
  168. package/dist/scaffolds/notis-affiliate-prospects/tailwind.config.ts +0 -51
  169. package/dist/scaffolds/notis-affiliate-prospects/tsconfig.json +0 -23
  170. package/dist/scaffolds/notis-affiliate-prospects/vite.config.ts +0 -11
  171. /package/dist/scaffolds/{notis-affiliate-prospects → notis-database}/packages/sdk/src/components/MultiSelectCheckbox.tsx +0 -0
  172. /package/dist/scaffolds/{notis-affiliate-prospects → notis-database}/packages/sdk/src/components/MultiSelectDragOverlay.tsx +0 -0
  173. /package/dist/scaffolds/{notis-affiliate-prospects → notis-database}/packages/sdk/src/hooks/useBackend.ts +0 -0
  174. /package/dist/scaffolds/{notis-affiliate-prospects → notis-database}/packages/sdk/src/hooks/useNotis.ts +0 -0
  175. /package/dist/scaffolds/{notis-affiliate-prospects → notis-database}/packages/sdk/src/hooks/useNotisNavigation.ts +0 -0
  176. /package/dist/scaffolds/{notis-affiliate-prospects → notis-database}/packages/sdk/src/hooks/useTool.ts +0 -0
  177. /package/dist/scaffolds/{notis-affiliate-prospects → notis-database}/packages/sdk/src/hooks/useTools.ts +0 -0
  178. /package/dist/scaffolds/{notis-affiliate-prospects → notis-database}/packages/sdk/src/hooks/useTopBarSearch.ts +0 -0
  179. /package/dist/scaffolds/{notis-affiliate-prospects → notis-database}/packages/sdk/src/provider.tsx +0 -0
  180. /package/dist/scaffolds/{notis-affiliate-prospects → notis-database}/packages/sdk/src/ui.ts +0 -0
  181. /package/dist/scaffolds/{notis-affiliate-prospects → notis-database}/packages/sdk/src/vite.ts +0 -0
  182. /package/dist/scaffolds/{notis-affiliate-prospects → notis-database}/packages/sdk/tsconfig.json +0 -0
  183. /package/dist/scaffolds/{notis-affiliate-prospects → notis-random}/components.json +0 -0
@@ -32,6 +32,7 @@ import {
32
32
  collectArtifactFiles,
33
33
  collectSourceFiles,
34
34
  resolveConfiguredAppSkills,
35
+ normalizeAppCapabilities,
35
36
  directDeploy,
36
37
  pullAppSource,
37
38
  } from '../runtime/app-platform.js';
@@ -43,15 +44,18 @@ import {
43
44
  runHarnessRoute,
44
45
  } from '../runtime/agent-browser.js';
45
46
  import {
47
+ getAppDevSessionsFile,
46
48
  heartbeatAppDevSession,
47
49
  removeAppDevSession,
48
50
  upsertAppDevSessions,
49
51
  waitForAppDevSessionMountAcknowledgements,
52
+ waitForAppDevSessionRenderAcknowledgements,
50
53
  } from '../runtime/app-dev-sessions.js';
51
54
  import { getAvailablePort } from '../runtime/ports.js';
52
55
  import { getCliMode } from '../runtime/cli-mode.js';
53
56
  import { composeStoreScreenshot } from '../runtime/store-screenshot.js';
54
57
  import { httpRequest } from '../runtime/transport.js';
58
+ import { ensureFreshOAuthCredential } from '../runtime/oauth.js';
55
59
  import {
56
60
  localNotisToolSlug,
57
61
  nextIdempotencyKey,
@@ -66,6 +70,7 @@ const ENSURE_DEV_APP_INSTALLATION_TOOL = 'LOCAL_NOTIS_ENSURE_DEV_APP_INSTALLATIO
66
70
  const GET_APP_TOOL = 'LOCAL_NOTIS_GET_APP';
67
71
  const LIST_APPS_TOOL = 'LOCAL_NOTIS_LIST_APPS';
68
72
  const CREATE_APP_TOOL = 'LOCAL_NOTIS_CREATE_APP';
73
+ const DUPLICATE_APP_TOOL = 'LOCAL_NOTIS_DUPLICATE_APP';
69
74
  const SAVE_APP_FILES_TOOL = 'LOCAL_NOTIS_SAVE_APP_FILES';
70
75
 
71
76
  // ---------------------------------------------------------------------------
@@ -138,13 +143,24 @@ function decodeJwtSub(jwt) {
138
143
  }
139
144
  }
140
145
 
141
- function openInBrowser(url) {
142
- 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
+ ) {
143
155
  let command;
144
156
  let args;
145
157
  if (platform === 'darwin') {
146
158
  command = 'open';
147
- args = [url];
159
+ args = bundleId && scheme === 'notis'
160
+ ? ['-b', bundleId, url]
161
+ : appName && scheme === 'notis'
162
+ ? ['-a', appName, url]
163
+ : [url];
148
164
  } else if (platform === 'win32') {
149
165
  command = 'cmd';
150
166
  args = ['/c', 'start', '', url];
@@ -152,10 +168,14 @@ function openInBrowser(url) {
152
168
  command = 'xdg-open';
153
169
  args = [url];
154
170
  }
171
+ return { command, args };
172
+ }
173
+
174
+ function openInBrowser(url, options = {}) {
175
+ const { command, args } = developmentDesktopOpenCommand(url, options);
155
176
  try {
156
- const child = spawn(command, args, { stdio: 'ignore', detached: true });
177
+ const child = spawn(command, args, { stdio: 'ignore' });
157
178
  child.on('error', () => {});
158
- child.unref();
159
179
  } catch {
160
180
  // Non-fatal. The URL is printed in the CLI output.
161
181
  }
@@ -205,21 +225,55 @@ function pickDefaultRouteSlug(manifest) {
205
225
 
206
226
  const DESKTOP_DEEP_LINK_SCHEME_PATTERN = /^[a-z][a-z0-9-]*$/;
207
227
 
208
- export function resolveDevelopmentDesktopScheme(env = process.env) {
228
+ export function resolveDevelopmentDesktopScheme(env = process.env, worktreeRuntime = null) {
209
229
  // Mirror the desktop app's own scheme resolution (electron main + forge config):
210
230
  // local dev launches register `notis-dev`, while installed prod/beta builds claim
211
231
  // `notis`. Hardcoding `notis` here is what made `apps dev` open the installed
212
232
  // prod/beta app instead of the local dev app.
213
- 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();
214
238
  return DESKTOP_DEEP_LINK_SCHEME_PATTERN.test(scheme) ? scheme : 'notis';
215
239
  }
216
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
+
217
265
  export function buildDevelopmentDesktopUrl(appHref = null, scheme = 'notis') {
218
266
  const route = String(appHref || '/store').replace(/^\/+/, '');
219
267
  const normalizedScheme = DESKTOP_DEEP_LINK_SCHEME_PATTERN.test(scheme) ? scheme : 'notis';
220
268
  return `${normalizedScheme}://${route || 'store'}`;
221
269
  }
222
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
+
223
277
  export function shouldOpenDevelopmentTab(options = {}) {
224
278
  // Commander stores the negatable `--no-open` flag as `options.open === false`;
225
279
  // it never sets `options.noOpen`. Reading the non-existent `noOpen` key meant
@@ -228,8 +282,17 @@ export function shouldOpenDevelopmentTab(options = {}) {
228
282
  return options.open !== false;
229
283
  }
230
284
 
231
- function buildAppHref({ appSlug, appId, manifest }) {
232
- 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}`;
233
296
  const routeSlug = pickDefaultRouteSlug(manifest);
234
297
  return routeSlug ? `${originlessBase}/${routeSlug}` : originlessBase;
235
298
  }
@@ -377,22 +440,35 @@ function assertHarnessResult(result, route, databaseSlugs) {
377
440
  details: error,
378
441
  });
379
442
  }
380
- for (const databaseSlug of databaseSlugs) {
381
- const seen = (result.runtimeCalls || []).some(
382
- (call) =>
383
- call?.op === 'callTool' &&
384
- localNotisToolSlug(call?.args?.name) === 'LOCAL_NOTIS_DATABASE_QUERY' &&
385
- call?.args?.arguments?.database_slug === databaseSlug,
386
- );
387
- if (!seen) {
443
+ const declaredDatabaseSet = new Set(databaseSlugs);
444
+ const databaseQueries = (result.runtimeCalls || []).filter(
445
+ (call) =>
446
+ call?.op === 'callTool'
447
+ && localNotisToolSlug(call?.args?.name) === 'LOCAL_NOTIS_DATABASE_QUERY',
448
+ );
449
+ for (const call of databaseQueries) {
450
+ const databaseSlug = call?.args?.arguments?.database_slug;
451
+ if (databaseSlug && !declaredDatabaseSet.has(databaseSlug)) {
388
452
  assertions.push({
389
453
  ok: false,
390
- code: 'missing_database_query',
391
- message: `Route "${route.slug}" did not call LOCAL_NOTIS_DATABASE_QUERY for "${databaseSlug}".`,
454
+ code: 'undeclared_database_query',
455
+ message: `Route "${route.slug}" queried undeclared database "${databaseSlug}".`,
392
456
  details: { databaseSlug },
393
457
  });
394
458
  }
395
459
  }
460
+ const collectionDatabase = route?.collection?.database;
461
+ if (
462
+ collectionDatabase
463
+ && !databaseQueries.some((call) => call?.args?.arguments?.database_slug === collectionDatabase)
464
+ ) {
465
+ assertions.push({
466
+ ok: false,
467
+ code: 'missing_collection_database_query',
468
+ message: `Collection route "${route.slug}" did not query "${collectionDatabase}".`,
469
+ details: { databaseSlug: collectionDatabase },
470
+ });
471
+ }
396
472
  return assertions;
397
473
  }
398
474
 
@@ -440,6 +516,7 @@ function buildManifestForDev(appConfig) {
440
516
  css: 'bundle/app.css',
441
517
  },
442
518
  databases: appConfig.databases || [],
519
+ capabilities: normalizeAppCapabilities(appConfig.capabilities),
443
520
  tools: appConfig.tools || [],
444
521
  skills: (appConfig.skills || []).map((skill) => ({
445
522
  key: skill.key,
@@ -483,11 +560,12 @@ export async function ensureDevInstall({
483
560
  const manifest = buildManifestForDev(appConfig);
484
561
  const skills = resolveConfiguredAppSkills(appConfig, projectDir);
485
562
  let linkedState = readLinkedState(projectDir);
563
+ let linkedApp = null;
486
564
  if (linkedState?.app_id) {
487
- const linkedApp = await getAccessibleApp(ctx.runtime, linkedState.app_id, runTool);
488
- if (!linkedApp || linkedApp.manifest?.is_dev === true) {
565
+ linkedApp = await getAccessibleApp(ctx.runtime, linkedState.app_id, runTool);
566
+ if (linkedApp?.manifest?.is_dev === true) {
489
567
  const { app_id: legacyDevAppId, linked_at: _linkedAt, deployed_at: _deployedAt, version: _version, ...rest } = linkedState;
490
- const devAppId = linkedState.dev_app_id || (linkedApp ? legacyDevAppId : null);
568
+ const devAppId = linkedState.dev_app_id || legacyDevAppId;
491
569
  linkedState = {
492
570
  ...rest,
493
571
  ...(devAppId ? { dev_app_id: devAppId } : {}),
@@ -496,6 +574,7 @@ export async function ensureDevInstall({
496
574
  } : {}),
497
575
  };
498
576
  writeLinkedState(projectDir, linkedState);
577
+ linkedApp = null;
499
578
  }
500
579
  }
501
580
  const ensureArguments = buildEnsureDevInstallArguments({ appConfig, manifest, linkedState, skills });
@@ -528,6 +607,7 @@ export async function ensureDevInstall({
528
607
  created: ensureResult.payload.created || false,
529
608
  linkedAppId: linkedState?.app_id || null,
530
609
  targetAppId: linkedState?.app_id || null,
610
+ targetAppSlug: linkedApp?.slug || null,
531
611
  databaseMaterialization: ensureResult.payload.database_materialization || { created: [], unresolved: [] },
532
612
  };
533
613
  }
@@ -709,6 +789,15 @@ async function appsDevHandler(ctx) {
709
789
  throw usageError('Could not determine the current user from the CLI auth token. Open the Notis desktop app, sign in, and retry.');
710
790
  }
711
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);
712
801
  const sessionId = randomUUID();
713
802
 
714
803
  const candidates = [];
@@ -764,16 +853,25 @@ async function appsDevHandler(ctx) {
764
853
  ...app,
765
854
  bundleBaseUrl,
766
855
  mountNonce: randomUUID(),
767
- appHref: buildAppHref({
856
+ appHref: buildDevelopmentAppHref({
768
857
  appSlug: app.slug,
769
858
  appId: app.appId,
859
+ devSlug: app.devSlug,
860
+ targetAppId: app.targetAppId,
861
+ targetAppSlug: app.targetAppSlug,
770
862
  manifest: app.manifest,
771
863
  }),
772
864
  };
773
865
  });
774
866
  const developmentTabUrl = buildDevelopmentDesktopUrl(
775
867
  apps[0]?.appHref,
776
- resolveDevelopmentDesktopScheme(),
868
+ desktopScheme,
869
+ );
870
+ const desktopWakeUrl = buildDevelopmentDesktopUrl('/manage', desktopScheme);
871
+ const mountedDevelopmentTabUrl = buildMountedDevelopmentDesktopUrl(
872
+ apps[0]?.appHref,
873
+ desktopScheme,
874
+ sessionId,
777
875
  );
778
876
  const warnings = databaseMaterializationWarnings(apps);
779
877
 
@@ -786,6 +884,7 @@ async function appsDevHandler(ctx) {
786
884
  userId: identity,
787
885
  })),
788
886
  port,
887
+ sessionsFilePath,
789
888
  });
790
889
 
791
890
  try {
@@ -802,7 +901,8 @@ async function appsDevHandler(ctx) {
802
901
  projectDir: app.projectDir,
803
902
  startedAt: now,
804
903
  lastHeartbeatAt: now,
805
- })));
904
+ desktopAppName,
905
+ })), sessionsFilePath);
806
906
  } catch (error) {
807
907
  try {
808
908
  await devServer.close();
@@ -814,7 +914,7 @@ async function appsDevHandler(ctx) {
814
914
 
815
915
  let heartbeatTimer = setInterval(() => {
816
916
  try {
817
- heartbeatAppDevSession(sessionId, new Date().toISOString());
917
+ heartbeatAppDevSession(sessionId, new Date().toISOString(), sessionsFilePath);
818
918
  } catch (error) {
819
919
  const message = error instanceof Error ? error.message : String(error);
820
920
  process.stderr.write(`[notis apps dev] heartbeat failed: ${message}\n`);
@@ -836,6 +936,8 @@ async function appsDevHandler(ctx) {
836
936
  development_url: developmentTabUrl,
837
937
  session_id: sessionId,
838
938
  mount_status: 'serving',
939
+ render_status: 'waiting_for_route',
940
+ desktop_target: desktopAppName,
839
941
  identity,
840
942
  apps: apps.map((app) => ({
841
943
  slug: app.devSlug,
@@ -853,36 +955,67 @@ async function appsDevHandler(ctx) {
853
955
  warnings,
854
956
  humanSummary: [
855
957
  `Running apps dev against ${apiBase} as ${identity} (mode: ${mode})`,
958
+ `Target desktop: ${desktopAppName}`,
856
959
  '',
857
960
  `Open in desktop: ${developmentTabUrl}`,
858
961
  '',
859
962
  ...apps.map((app) => ` ${app.name.padEnd(24)} ${app.bundleBaseUrl} -> ${app.appHref}`),
860
963
  '',
861
- `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`}.`,
862
965
  '',
863
966
  'Press Ctrl-C to stop.',
864
967
  ].join('\n'),
865
968
  });
866
969
 
867
970
  if (shouldOpenDevelopmentTab(ctx.options)) {
868
- openInBrowser(developmentTabUrl);
971
+ openInBrowser(desktopWakeUrl, {
972
+ appName: desktopAppName,
973
+ bundleId: desktopBundleId,
974
+ scheme: desktopScheme,
975
+ });
869
976
  }
870
977
 
871
978
  void (async () => {
872
- let result = await waitForAppDevSessionMountAcknowledgements(expectedMountAcknowledgements);
979
+ let result = await waitForAppDevSessionMountAcknowledgements(expectedMountAcknowledgements, {
980
+ sessionsFilePath,
981
+ });
873
982
  if (!result.mounted) {
874
983
  process.stderr.write(
875
- `[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`,
876
985
  );
877
986
  }
878
987
  while (!result.mounted) {
879
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,
880
1013
  timeoutMs: 60_000,
881
1014
  pollIntervalMs: 250,
882
1015
  });
883
1016
  }
884
1017
  process.stderr.write(
885
- `[notis apps dev] Mounted in Notis: ${apps.map((app) => app.name).join(', ')}.\n`,
1018
+ `[notis apps dev] Rendered in ${desktopAppName}: ${apps[0].name}.\n`,
886
1019
  );
887
1020
  })().catch((error) => {
888
1021
  const message = error instanceof Error ? error.message : String(error);
@@ -899,7 +1032,7 @@ async function appsDevHandler(ctx) {
899
1032
  heartbeatTimer = null;
900
1033
  }
901
1034
  try {
902
- removeAppDevSession(sessionId);
1035
+ removeAppDevSession(sessionId, sessionsFilePath);
903
1036
  } catch {
904
1037
  // ignore cleanup failures during shutdown
905
1038
  }
@@ -930,7 +1063,9 @@ async function appsBuildHandler(ctx) {
930
1063
  throw usageError(`Project has problems:\n${problems.map((p) => ` - ${p}`).join('\n')}`);
931
1064
  }
932
1065
 
933
- const { manifest } = await buildArtifact(projectDir);
1066
+ const { manifest } = await buildArtifact(projectDir, {
1067
+ stdio: ctx.output.isMachineMode() ? 'pipe' : 'inherit',
1068
+ });
934
1069
 
935
1070
  return ctx.output.emitSuccess({
936
1071
  command: ctx.spec.command_path.join(' '),
@@ -953,6 +1088,12 @@ async function appsVerifyHandler(ctx) {
953
1088
 
954
1089
  let linkedState = null;
955
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
+ }
956
1097
  if (!ctx.runtime.jwt) {
957
1098
  throw usageError('Live verify mode requires CLI auth. Open the Notis desktop app, sign in, and retry.');
958
1099
  }
@@ -963,7 +1104,9 @@ async function appsVerifyHandler(ctx) {
963
1104
  }
964
1105
 
965
1106
  if (!ctx.options.skipBuild) {
966
- await buildArtifact(projectDir);
1107
+ await buildArtifact(projectDir, {
1108
+ stdio: ctx.output.isMachineMode() ? 'pipe' : 'inherit',
1109
+ });
967
1110
  }
968
1111
 
969
1112
  const manifest = readManifest(projectDir);
@@ -1165,6 +1308,12 @@ async function appsScreenshotHandler(ctx) {
1165
1308
 
1166
1309
  let linkedState = null;
1167
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
+ }
1168
1317
  if (!ctx.runtime.jwt) {
1169
1318
  throw usageError('Live mode requires CLI auth. Open the Notis desktop app, sign in, and retry.');
1170
1319
  }
@@ -1175,7 +1324,9 @@ async function appsScreenshotHandler(ctx) {
1175
1324
  }
1176
1325
 
1177
1326
  if (!ctx.options.skipBuild) {
1178
- await buildArtifact(projectDir);
1327
+ await buildArtifact(projectDir, {
1328
+ stdio: ctx.output.isMachineMode() ? 'pipe' : 'inherit',
1329
+ });
1179
1330
  }
1180
1331
 
1181
1332
  const manifest = readManifest(projectDir);
@@ -1364,16 +1515,25 @@ async function appsScreenshotHandler(ctx) {
1364
1515
  }
1365
1516
  }
1366
1517
 
1518
+ export function buildLinkedAppState(existingState, appId, linkedAt = new Date().toISOString()) {
1519
+ return {
1520
+ ...(existingState?.dev_app_id ? { dev_app_id: existingState.dev_app_id } : {}),
1521
+ ...(existingState?.dev_linked_at ? { dev_linked_at: existingState.dev_linked_at } : {}),
1522
+ app_id: appId,
1523
+ linked_at: linkedAt,
1524
+ };
1525
+ }
1526
+
1367
1527
  async function appsLinkHandler(ctx) {
1368
1528
  const projectDir = resolveProjectDir(ctx.args.dir || '.');
1369
1529
  const appId = ctx.args.appId;
1370
1530
 
1371
1531
  await assertLinkTarget(ctx.runtime, appId);
1372
1532
 
1373
- writeLinkedState(projectDir, {
1374
- app_id: appId,
1375
- linked_at: new Date().toISOString(),
1376
- });
1533
+ writeLinkedState(
1534
+ projectDir,
1535
+ buildLinkedAppState(readLinkedState(projectDir), appId),
1536
+ );
1377
1537
 
1378
1538
  return ctx.output.emitSuccess({
1379
1539
  command: ctx.spec.command_path.join(' '),
@@ -1392,6 +1552,12 @@ async function appsPullHandler(ctx) {
1392
1552
  toolName: GET_APP_TOOL,
1393
1553
  arguments_: { app_id: appId },
1394
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
+ }
1395
1561
  const app = result.payload?.app || {};
1396
1562
  const defaultDir = app.slug || slugify(app.name) || appId;
1397
1563
  const targetDir = resolveProjectDir(ctx.args.dir || defaultDir);
@@ -1442,7 +1608,9 @@ async function appsDeployHandler(ctx) {
1442
1608
 
1443
1609
  // Build if needed
1444
1610
  if (!ctx.options.skipBuild) {
1445
- await buildArtifact(projectDir);
1611
+ await buildArtifact(projectDir, {
1612
+ stdio: ctx.output.isMachineMode() ? 'pipe' : 'inherit',
1613
+ });
1446
1614
  }
1447
1615
 
1448
1616
  // Direct deploy mode: upload to Supabase storage directly
@@ -1539,6 +1707,67 @@ function deployedAppVersion(app) {
1539
1707
  return Number.isFinite(parsed) ? parsed : 0;
1540
1708
  }
1541
1709
 
1710
+ async function appsDuplicateHandler(ctx) {
1711
+ const projectDir = resolveProjectDir(ctx.args.dir || '.');
1712
+ // Either target an app explicitly, or duplicate whatever this project is
1713
+ // linked to, so `notis apps duplicate` works from inside a project.
1714
+ const appId = requireLinkedAppId(projectDir, ctx.options.appId);
1715
+ const idempotencyKey = nextIdempotencyKey(ctx.globalOptions);
1716
+
1717
+ const copyDocuments = ctx.options.copyDocuments || 'declared';
1718
+ if (!['declared', 'all', 'none'].includes(copyDocuments)) {
1719
+ throw usageError("--copy-documents must be one of: declared, all, none.");
1720
+ }
1721
+
1722
+ const result = await runToolCommand({
1723
+ runtime: ctx.runtime,
1724
+ toolName: DUPLICATE_APP_TOOL,
1725
+ arguments_: {
1726
+ app_id: appId,
1727
+ ...(ctx.options.name ? { name: ctx.options.name } : {}),
1728
+ copy_documents: copyDocuments,
1729
+ },
1730
+ mutating: true,
1731
+ idempotencyKey,
1732
+ });
1733
+
1734
+ const payload = result.payload || {};
1735
+ if (payload.status === 'error') {
1736
+ throw usageError(`Could not duplicate app ${appId}: ${payload.message || 'unknown error'}`);
1737
+ }
1738
+
1739
+ const duplicated = payload.app || {};
1740
+ if (!duplicated.id) {
1741
+ throw usageError(`Could not duplicate app ${appId}: the backend did not return an app id.`);
1742
+ }
1743
+
1744
+ const data = {
1745
+ app_id: duplicated.id,
1746
+ name: duplicated.name,
1747
+ slug: duplicated.slug,
1748
+ duplicated_from_app_id: payload.duplicated_from_app_id || appId,
1749
+ copied_document_count: payload.copied_document_count ?? 0,
1750
+ portal_url: payload.portal_url,
1751
+ idempotency_key: idempotencyKey,
1752
+ // The duplicate owns brand new databases; nothing is shared with the source.
1753
+ databases: (payload.databases || []).map((database) => ({
1754
+ id: database.id,
1755
+ slug: database.slug,
1756
+ name: database.name,
1757
+ })),
1758
+ };
1759
+
1760
+ return ctx.output.emitSuccess({
1761
+ command: ctx.spec.command_path.join(' '),
1762
+ data,
1763
+ humanSummary: `Duplicated app ${appId} as ${duplicated.name || duplicated.id}`,
1764
+ hints: payload.portal_url
1765
+ ? [{ command: payload.portal_url, reason: 'Open the duplicated app in Portal' }]
1766
+ : [],
1767
+ meta: { mutating: true, idempotency_key: idempotencyKey },
1768
+ });
1769
+ }
1770
+
1542
1771
  async function appsPublishHandler(ctx) {
1543
1772
  if (ctx.options.confirmReady !== true) {
1544
1773
  throw usageError(
@@ -1652,10 +1881,20 @@ async function appsDoctorHandler(ctx) {
1652
1881
  ? `Found ${problems.length} problems:\n${problems.map((p) => ` - ${p}`).join('\n')}`
1653
1882
  : warnings.length
1654
1883
  ? `Healthy with ${warnings.length} warnings:\n${warnings.map((w) => ` - ${w}`).join('\n')}`
1655
- : `Project is healthy.${linkedState ? ` Linked to ${linkedState.app_id}.` : ' Not linked.'}`,
1884
+ : `Project is healthy.${doctorLinkSummary(linkedState)}`,
1656
1885
  });
1657
1886
  }
1658
1887
 
1888
+ export function doctorLinkSummary(linkedState) {
1889
+ if (linkedState?.app_id) {
1890
+ return ` Linked to app ${linkedState.app_id}.`;
1891
+ }
1892
+ if (linkedState?.dev_app_id) {
1893
+ return ` Local development app ${linkedState.dev_app_id} is active.`;
1894
+ }
1895
+ return ' Not linked.';
1896
+ }
1897
+
1659
1898
  // ---------------------------------------------------------------------------
1660
1899
  // Command specs
1661
1900
  // ---------------------------------------------------------------------------
@@ -1919,6 +2158,35 @@ export const appsCommandSpecs = [
1919
2158
  backend_call: { type: 'http', name: 'portal_apps/publish' },
1920
2159
  handler: appsPublishHandler,
1921
2160
  },
2161
+ {
2162
+ command_path: ['apps', 'duplicate'],
2163
+ summary: 'Duplicate an app into an independent copy with its own databases.',
2164
+ when_to_use:
2165
+ 'When the same app should run for a second purpose - a notes app for blog drafts alongside one for bookmarks. The copy shares no data with the source.',
2166
+ args_schema: {
2167
+ arguments: [
2168
+ { token: '[dir]', key: 'dir', description: 'Project directory (default: current dir).' },
2169
+ ],
2170
+ options: [
2171
+ { flags: '--app-id <id>', description: 'App to duplicate. Defaults to the app this project is linked to.' },
2172
+ { flags: '--name <name>', description: 'Name for the duplicate (default: the source name followed by "copy").' },
2173
+ {
2174
+ flags: '--copy-documents <mode>',
2175
+ description:
2176
+ "Which rows to copy: 'declared' (default, the starter content a fresh install would have), 'all', or 'none'.",
2177
+ },
2178
+ ],
2179
+ },
2180
+ examples: [
2181
+ 'notis apps duplicate --name "Blog"',
2182
+ 'notis apps duplicate --app-id abc123 --name "Bookmarks" --copy-documents none',
2183
+ ],
2184
+ mutates: true,
2185
+ idempotent: false,
2186
+ require_auth: true,
2187
+ backend_call: { type: 'tool', name: DUPLICATE_APP_TOOL },
2188
+ handler: appsDuplicateHandler,
2189
+ },
1922
2190
  {
1923
2191
  command_path: ['apps', 'doctor'],
1924
2192
  summary: 'Check project health and readiness.',