@notis_ai/cli 0.2.0-beta.113.1 → 0.2.0-beta.116.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (178) hide show
  1. package/README.md +15 -0
  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 +15 -0
  120. package/skills/notis-cli/SKILL.md +8 -4
  121. package/skills/notis-onboarding/BRIEF.md +93 -0
  122. package/src/cli.js +16 -0
  123. package/src/command-specs/apps.js +290 -41
  124. package/src/command-specs/index.js +2 -0
  125. package/src/command-specs/onboarding.js +216 -0
  126. package/src/command-specs/tools.js +8 -4
  127. package/src/runtime/app-dev-server.js +3 -2
  128. package/src/runtime/app-dev-sessions.js +14 -40
  129. package/src/runtime/app-platform.js +65 -4
  130. package/src/runtime/desktop-auth.js +66 -17
  131. package/src/runtime/errors.js +1 -0
  132. package/src/runtime/profiles.js +27 -6
  133. package/src/runtime/telemetry.js +92 -0
  134. package/src/runtime/transport.js +12 -0
  135. package/template/.harness/index.html.tmpl +30 -2
  136. package/template/packages/sdk/src/config.ts +38 -1
  137. package/dist/scaffolds/notis-affiliate-prospects/CHANGELOG.md +0 -7
  138. package/dist/scaffolds/notis-affiliate-prospects/README.md +0 -20
  139. package/dist/scaffolds/notis-affiliate-prospects/app/globals.css +0 -42
  140. package/dist/scaffolds/notis-affiliate-prospects/app/layout.tsx +0 -7
  141. package/dist/scaffolds/notis-affiliate-prospects/app/page.tsx +0 -248
  142. package/dist/scaffolds/notis-affiliate-prospects/app/prospects/page.tsx +0 -134
  143. package/dist/scaffolds/notis-affiliate-prospects/app/segments/page.tsx +0 -108
  144. package/dist/scaffolds/notis-affiliate-prospects/components/ui/card.tsx +0 -35
  145. package/dist/scaffolds/notis-affiliate-prospects/index.html +0 -12
  146. package/dist/scaffolds/notis-affiliate-prospects/lib/affiliate-data.ts +0 -218
  147. package/dist/scaffolds/notis-affiliate-prospects/lib/demo-prospects.ts +0 -52
  148. package/dist/scaffolds/notis-affiliate-prospects/lib/notis-tools.ts +0 -100
  149. package/dist/scaffolds/notis-affiliate-prospects/lib/utils.ts +0 -25
  150. package/dist/scaffolds/notis-affiliate-prospects/metadata/screenshot-1.png +0 -0
  151. package/dist/scaffolds/notis-affiliate-prospects/metadata/screenshot-2.png +0 -0
  152. package/dist/scaffolds/notis-affiliate-prospects/metadata/screenshot-3.png +0 -0
  153. package/dist/scaffolds/notis-affiliate-prospects/metadata/screenshot-fixtures.json +0 -187
  154. package/dist/scaffolds/notis-affiliate-prospects/notis.config.ts +0 -65
  155. package/dist/scaffolds/notis-affiliate-prospects/package.json +0 -32
  156. package/dist/scaffolds/notis-affiliate-prospects/packages/sdk/src/config.ts +0 -90
  157. package/dist/scaffolds/notis-affiliate-prospects/packages/sdk/src/hooks/useDatabase.ts +0 -76
  158. package/dist/scaffolds/notis-affiliate-prospects/packages/sdk/src/hooks/useUpsertDocument.ts +0 -50
  159. package/dist/scaffolds/notis-affiliate-prospects/packages/sdk/src/styles.css +0 -38
  160. package/dist/scaffolds/notis-affiliate-prospects/postcss.config.mjs +0 -6
  161. package/dist/scaffolds/notis-affiliate-prospects/src/dev-main.tsx +0 -72
  162. package/dist/scaffolds/notis-affiliate-prospects/src/mock-runtime.ts +0 -242
  163. package/dist/scaffolds/notis-affiliate-prospects/tailwind.config.ts +0 -51
  164. package/dist/scaffolds/notis-affiliate-prospects/tsconfig.json +0 -23
  165. package/dist/scaffolds/notis-affiliate-prospects/vite.config.ts +0 -11
  166. /package/dist/scaffolds/{notis-affiliate-prospects → notis-database}/packages/sdk/src/components/MultiSelectCheckbox.tsx +0 -0
  167. /package/dist/scaffolds/{notis-affiliate-prospects → notis-database}/packages/sdk/src/components/MultiSelectDragOverlay.tsx +0 -0
  168. /package/dist/scaffolds/{notis-affiliate-prospects → notis-database}/packages/sdk/src/hooks/useBackend.ts +0 -0
  169. /package/dist/scaffolds/{notis-affiliate-prospects → notis-database}/packages/sdk/src/hooks/useNotis.ts +0 -0
  170. /package/dist/scaffolds/{notis-affiliate-prospects → notis-database}/packages/sdk/src/hooks/useNotisNavigation.ts +0 -0
  171. /package/dist/scaffolds/{notis-affiliate-prospects → notis-database}/packages/sdk/src/hooks/useTool.ts +0 -0
  172. /package/dist/scaffolds/{notis-affiliate-prospects → notis-database}/packages/sdk/src/hooks/useTools.ts +0 -0
  173. /package/dist/scaffolds/{notis-affiliate-prospects → notis-database}/packages/sdk/src/hooks/useTopBarSearch.ts +0 -0
  174. /package/dist/scaffolds/{notis-affiliate-prospects → notis-database}/packages/sdk/src/provider.tsx +0 -0
  175. /package/dist/scaffolds/{notis-affiliate-prospects → notis-database}/packages/sdk/src/ui.ts +0 -0
  176. /package/dist/scaffolds/{notis-affiliate-prospects → notis-database}/packages/sdk/src/vite.ts +0 -0
  177. /package/dist/scaffolds/{notis-affiliate-prospects → notis-database}/packages/sdk/tsconfig.json +0 -0
  178. /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,10 +44,12 @@ 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';
@@ -66,6 +69,7 @@ const ENSURE_DEV_APP_INSTALLATION_TOOL = 'LOCAL_NOTIS_ENSURE_DEV_APP_INSTALLATIO
66
69
  const GET_APP_TOOL = 'LOCAL_NOTIS_GET_APP';
67
70
  const LIST_APPS_TOOL = 'LOCAL_NOTIS_LIST_APPS';
68
71
  const CREATE_APP_TOOL = 'LOCAL_NOTIS_CREATE_APP';
72
+ const DUPLICATE_APP_TOOL = 'LOCAL_NOTIS_DUPLICATE_APP';
69
73
  const SAVE_APP_FILES_TOOL = 'LOCAL_NOTIS_SAVE_APP_FILES';
70
74
 
71
75
  // ---------------------------------------------------------------------------
@@ -138,13 +142,24 @@ function decodeJwtSub(jwt) {
138
142
  }
139
143
  }
140
144
 
141
- function openInBrowser(url) {
142
- const platform = process.platform;
145
+ export function developmentDesktopOpenCommand(
146
+ url,
147
+ {
148
+ platform = process.platform,
149
+ appName = null,
150
+ bundleId = null,
151
+ scheme = 'notis',
152
+ } = {},
153
+ ) {
143
154
  let command;
144
155
  let args;
145
156
  if (platform === 'darwin') {
146
157
  command = 'open';
147
- args = [url];
158
+ args = bundleId && scheme === 'notis'
159
+ ? ['-b', bundleId, url]
160
+ : appName && scheme === 'notis'
161
+ ? ['-a', appName, url]
162
+ : [url];
148
163
  } else if (platform === 'win32') {
149
164
  command = 'cmd';
150
165
  args = ['/c', 'start', '', url];
@@ -152,10 +167,14 @@ function openInBrowser(url) {
152
167
  command = 'xdg-open';
153
168
  args = [url];
154
169
  }
170
+ return { command, args };
171
+ }
172
+
173
+ function openInBrowser(url, options = {}) {
174
+ const { command, args } = developmentDesktopOpenCommand(url, options);
155
175
  try {
156
- const child = spawn(command, args, { stdio: 'ignore', detached: true });
176
+ const child = spawn(command, args, { stdio: 'ignore' });
157
177
  child.on('error', () => {});
158
- child.unref();
159
178
  } catch {
160
179
  // Non-fatal. The URL is printed in the CLI output.
161
180
  }
@@ -205,21 +224,55 @@ function pickDefaultRouteSlug(manifest) {
205
224
 
206
225
  const DESKTOP_DEEP_LINK_SCHEME_PATTERN = /^[a-z][a-z0-9-]*$/;
207
226
 
208
- export function resolveDevelopmentDesktopScheme(env = process.env) {
227
+ export function resolveDevelopmentDesktopScheme(env = process.env, worktreeRuntime = null) {
209
228
  // Mirror the desktop app's own scheme resolution (electron main + forge config):
210
229
  // local dev launches register `notis-dev`, while installed prod/beta builds claim
211
230
  // `notis`. Hardcoding `notis` here is what made `apps dev` open the installed
212
231
  // prod/beta app instead of the local dev app.
213
- const scheme = (env.NOTIS_DESKTOP_DEEP_LINK_SCHEME || '').trim();
232
+ const scheme = (
233
+ worktreeRuntime?.desktop_deep_link_scheme
234
+ || env.NOTIS_DESKTOP_DEEP_LINK_SCHEME
235
+ || ''
236
+ ).trim();
214
237
  return DESKTOP_DEEP_LINK_SCHEME_PATTERN.test(scheme) ? scheme : 'notis';
215
238
  }
216
239
 
240
+ export function resolveDevelopmentDesktopAppName(runtime = {}) {
241
+ const explicit = String(
242
+ runtime.desktopAppName
243
+ || runtime.worktreeRuntime?.desktop_app_name
244
+ || '',
245
+ ).trim();
246
+ if (explicit) {
247
+ return explicit;
248
+ }
249
+ try {
250
+ return new URL(runtime.apiBase).hostname === 'api-beta.notis.ai'
251
+ ? 'Notis Beta'
252
+ : 'Notis';
253
+ } catch {
254
+ return 'Notis';
255
+ }
256
+ }
257
+
258
+ export function resolveDevelopmentDesktopBundleId(runtime = {}) {
259
+ return resolveDevelopmentDesktopAppName(runtime) === 'Notis Beta'
260
+ ? 'ai.notis.desktop.beta'
261
+ : 'ai.notis.desktop';
262
+ }
263
+
217
264
  export function buildDevelopmentDesktopUrl(appHref = null, scheme = 'notis') {
218
265
  const route = String(appHref || '/store').replace(/^\/+/, '');
219
266
  const normalizedScheme = DESKTOP_DEEP_LINK_SCHEME_PATTERN.test(scheme) ? scheme : 'notis';
220
267
  return `${normalizedScheme}://${route || 'store'}`;
221
268
  }
222
269
 
270
+ export function buildMountedDevelopmentDesktopUrl(appHref, scheme, sessionId) {
271
+ const url = new URL(buildDevelopmentDesktopUrl(appHref, scheme));
272
+ url.searchParams.set('notis_dev_session', sessionId);
273
+ return url.toString();
274
+ }
275
+
223
276
  export function shouldOpenDevelopmentTab(options = {}) {
224
277
  // Commander stores the negatable `--no-open` flag as `options.open === false`;
225
278
  // it never sets `options.noOpen`. Reading the non-existent `noOpen` key meant
@@ -228,8 +281,17 @@ export function shouldOpenDevelopmentTab(options = {}) {
228
281
  return options.open !== false;
229
282
  }
230
283
 
231
- function buildAppHref({ appSlug, appId, manifest }) {
232
- const originlessBase = `/apps/${appSlug}-${appId}`;
284
+ export function buildDevelopmentAppHref({
285
+ appSlug,
286
+ appId,
287
+ devSlug,
288
+ targetAppId = null,
289
+ targetAppSlug = null,
290
+ manifest,
291
+ }) {
292
+ const routeAppId = `${targetAppId || appId}__local_dev__${devSlug}`;
293
+ const routeAppSlug = targetAppSlug || devSlug || appSlug;
294
+ const originlessBase = `/apps/${routeAppSlug}-${routeAppId}`;
233
295
  const routeSlug = pickDefaultRouteSlug(manifest);
234
296
  return routeSlug ? `${originlessBase}/${routeSlug}` : originlessBase;
235
297
  }
@@ -377,22 +439,35 @@ function assertHarnessResult(result, route, databaseSlugs) {
377
439
  details: error,
378
440
  });
379
441
  }
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) {
442
+ const declaredDatabaseSet = new Set(databaseSlugs);
443
+ const databaseQueries = (result.runtimeCalls || []).filter(
444
+ (call) =>
445
+ call?.op === 'callTool'
446
+ && localNotisToolSlug(call?.args?.name) === 'LOCAL_NOTIS_DATABASE_QUERY',
447
+ );
448
+ for (const call of databaseQueries) {
449
+ const databaseSlug = call?.args?.arguments?.database_slug;
450
+ if (databaseSlug && !declaredDatabaseSet.has(databaseSlug)) {
388
451
  assertions.push({
389
452
  ok: false,
390
- code: 'missing_database_query',
391
- message: `Route "${route.slug}" did not call LOCAL_NOTIS_DATABASE_QUERY for "${databaseSlug}".`,
453
+ code: 'undeclared_database_query',
454
+ message: `Route "${route.slug}" queried undeclared database "${databaseSlug}".`,
392
455
  details: { databaseSlug },
393
456
  });
394
457
  }
395
458
  }
459
+ const collectionDatabase = route?.collection?.database;
460
+ if (
461
+ collectionDatabase
462
+ && !databaseQueries.some((call) => call?.args?.arguments?.database_slug === collectionDatabase)
463
+ ) {
464
+ assertions.push({
465
+ ok: false,
466
+ code: 'missing_collection_database_query',
467
+ message: `Collection route "${route.slug}" did not query "${collectionDatabase}".`,
468
+ details: { databaseSlug: collectionDatabase },
469
+ });
470
+ }
396
471
  return assertions;
397
472
  }
398
473
 
@@ -440,6 +515,7 @@ function buildManifestForDev(appConfig) {
440
515
  css: 'bundle/app.css',
441
516
  },
442
517
  databases: appConfig.databases || [],
518
+ capabilities: normalizeAppCapabilities(appConfig.capabilities),
443
519
  tools: appConfig.tools || [],
444
520
  skills: (appConfig.skills || []).map((skill) => ({
445
521
  key: skill.key,
@@ -483,11 +559,12 @@ export async function ensureDevInstall({
483
559
  const manifest = buildManifestForDev(appConfig);
484
560
  const skills = resolveConfiguredAppSkills(appConfig, projectDir);
485
561
  let linkedState = readLinkedState(projectDir);
562
+ let linkedApp = null;
486
563
  if (linkedState?.app_id) {
487
- const linkedApp = await getAccessibleApp(ctx.runtime, linkedState.app_id, runTool);
488
- if (!linkedApp || linkedApp.manifest?.is_dev === true) {
564
+ linkedApp = await getAccessibleApp(ctx.runtime, linkedState.app_id, runTool);
565
+ if (linkedApp?.manifest?.is_dev === true) {
489
566
  const { app_id: legacyDevAppId, linked_at: _linkedAt, deployed_at: _deployedAt, version: _version, ...rest } = linkedState;
490
- const devAppId = linkedState.dev_app_id || (linkedApp ? legacyDevAppId : null);
567
+ const devAppId = linkedState.dev_app_id || legacyDevAppId;
491
568
  linkedState = {
492
569
  ...rest,
493
570
  ...(devAppId ? { dev_app_id: devAppId } : {}),
@@ -496,6 +573,7 @@ export async function ensureDevInstall({
496
573
  } : {}),
497
574
  };
498
575
  writeLinkedState(projectDir, linkedState);
576
+ linkedApp = null;
499
577
  }
500
578
  }
501
579
  const ensureArguments = buildEnsureDevInstallArguments({ appConfig, manifest, linkedState, skills });
@@ -528,6 +606,7 @@ export async function ensureDevInstall({
528
606
  created: ensureResult.payload.created || false,
529
607
  linkedAppId: linkedState?.app_id || null,
530
608
  targetAppId: linkedState?.app_id || null,
609
+ targetAppSlug: linkedApp?.slug || null,
531
610
  databaseMaterialization: ensureResult.payload.database_materialization || { created: [], unresolved: [] },
532
611
  };
533
612
  }
@@ -709,6 +788,15 @@ async function appsDevHandler(ctx) {
709
788
  throw usageError('Could not determine the current user from the CLI auth token. Open the Notis desktop app, sign in, and retry.');
710
789
  }
711
790
  const apiBase = String(ctx.runtime.apiBase || '').replace(/\/$/, '');
791
+ const sessionsFilePath = getAppDevSessionsFile(
792
+ ctx.runtime.worktreeRuntime?.app_dev_sessions_file,
793
+ );
794
+ const desktopScheme = resolveDevelopmentDesktopScheme(
795
+ process.env,
796
+ ctx.runtime.worktreeRuntime,
797
+ );
798
+ const desktopAppName = resolveDevelopmentDesktopAppName(ctx.runtime);
799
+ const desktopBundleId = resolveDevelopmentDesktopBundleId(ctx.runtime);
712
800
  const sessionId = randomUUID();
713
801
 
714
802
  const candidates = [];
@@ -764,16 +852,25 @@ async function appsDevHandler(ctx) {
764
852
  ...app,
765
853
  bundleBaseUrl,
766
854
  mountNonce: randomUUID(),
767
- appHref: buildAppHref({
855
+ appHref: buildDevelopmentAppHref({
768
856
  appSlug: app.slug,
769
857
  appId: app.appId,
858
+ devSlug: app.devSlug,
859
+ targetAppId: app.targetAppId,
860
+ targetAppSlug: app.targetAppSlug,
770
861
  manifest: app.manifest,
771
862
  }),
772
863
  };
773
864
  });
774
865
  const developmentTabUrl = buildDevelopmentDesktopUrl(
775
866
  apps[0]?.appHref,
776
- resolveDevelopmentDesktopScheme(),
867
+ desktopScheme,
868
+ );
869
+ const desktopWakeUrl = buildDevelopmentDesktopUrl('/manage', desktopScheme);
870
+ const mountedDevelopmentTabUrl = buildMountedDevelopmentDesktopUrl(
871
+ apps[0]?.appHref,
872
+ desktopScheme,
873
+ sessionId,
777
874
  );
778
875
  const warnings = databaseMaterializationWarnings(apps);
779
876
 
@@ -786,6 +883,7 @@ async function appsDevHandler(ctx) {
786
883
  userId: identity,
787
884
  })),
788
885
  port,
886
+ sessionsFilePath,
789
887
  });
790
888
 
791
889
  try {
@@ -802,7 +900,8 @@ async function appsDevHandler(ctx) {
802
900
  projectDir: app.projectDir,
803
901
  startedAt: now,
804
902
  lastHeartbeatAt: now,
805
- })));
903
+ desktopAppName,
904
+ })), sessionsFilePath);
806
905
  } catch (error) {
807
906
  try {
808
907
  await devServer.close();
@@ -814,7 +913,7 @@ async function appsDevHandler(ctx) {
814
913
 
815
914
  let heartbeatTimer = setInterval(() => {
816
915
  try {
817
- heartbeatAppDevSession(sessionId, new Date().toISOString());
916
+ heartbeatAppDevSession(sessionId, new Date().toISOString(), sessionsFilePath);
818
917
  } catch (error) {
819
918
  const message = error instanceof Error ? error.message : String(error);
820
919
  process.stderr.write(`[notis apps dev] heartbeat failed: ${message}\n`);
@@ -836,6 +935,8 @@ async function appsDevHandler(ctx) {
836
935
  development_url: developmentTabUrl,
837
936
  session_id: sessionId,
838
937
  mount_status: 'serving',
938
+ render_status: 'waiting_for_route',
939
+ desktop_target: desktopAppName,
839
940
  identity,
840
941
  apps: apps.map((app) => ({
841
942
  slug: app.devSlug,
@@ -853,36 +954,67 @@ async function appsDevHandler(ctx) {
853
954
  warnings,
854
955
  humanSummary: [
855
956
  `Running apps dev against ${apiBase} as ${identity} (mode: ${mode})`,
957
+ `Target desktop: ${desktopAppName}`,
856
958
  '',
857
959
  `Open in desktop: ${developmentTabUrl}`,
858
960
  '',
859
961
  ...apps.map((app) => ` ${app.name.padEnd(24)} ${app.bundleBaseUrl} -> ${app.appHref}`),
860
962
  '',
861
- `Serving locally; waiting for Notis to mount ${apps.length === 1 ? 'the app' : `${apps.length} apps`}.`,
963
+ `Serving locally; waiting for ${desktopAppName} to mount ${apps.length === 1 ? 'the app' : `${apps.length} apps`}.`,
862
964
  '',
863
965
  'Press Ctrl-C to stop.',
864
966
  ].join('\n'),
865
967
  });
866
968
 
867
969
  if (shouldOpenDevelopmentTab(ctx.options)) {
868
- openInBrowser(developmentTabUrl);
970
+ openInBrowser(desktopWakeUrl, {
971
+ appName: desktopAppName,
972
+ bundleId: desktopBundleId,
973
+ scheme: desktopScheme,
974
+ });
869
975
  }
870
976
 
871
977
  void (async () => {
872
- let result = await waitForAppDevSessionMountAcknowledgements(expectedMountAcknowledgements);
978
+ let result = await waitForAppDevSessionMountAcknowledgements(expectedMountAcknowledgements, {
979
+ sessionsFilePath,
980
+ });
873
981
  if (!result.mounted) {
874
982
  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`,
983
+ `[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
984
  );
877
985
  }
878
986
  while (!result.mounted) {
879
987
  result = await waitForAppDevSessionMountAcknowledgements(expectedMountAcknowledgements, {
988
+ sessionsFilePath,
880
989
  timeoutMs: 60_000,
881
990
  pollIntervalMs: 250,
882
991
  });
883
992
  }
884
993
  process.stderr.write(
885
- `[notis apps dev] Mounted in Notis: ${apps.map((app) => app.name).join(', ')}.\n`,
994
+ `[notis apps dev] Mounted in ${desktopAppName}: ${apps.map((app) => app.name).join(', ')}.\n`,
995
+ );
996
+ if (shouldOpenDevelopmentTab(ctx.options)) {
997
+ openInBrowser(mountedDevelopmentTabUrl, {
998
+ appName: desktopAppName,
999
+ bundleId: desktopBundleId,
1000
+ scheme: desktopScheme,
1001
+ });
1002
+ }
1003
+
1004
+ const firstAppRender = expectedMountAcknowledgements.slice(0, 1);
1005
+ let renderResult = await waitForAppDevSessionRenderAcknowledgements(firstAppRender, {
1006
+ sessionsFilePath,
1007
+ timeoutMs: 0,
1008
+ });
1009
+ while (!renderResult.mounted) {
1010
+ renderResult = await waitForAppDevSessionRenderAcknowledgements(firstAppRender, {
1011
+ sessionsFilePath,
1012
+ timeoutMs: 60_000,
1013
+ pollIntervalMs: 250,
1014
+ });
1015
+ }
1016
+ process.stderr.write(
1017
+ `[notis apps dev] Rendered in ${desktopAppName}: ${apps[0].name}.\n`,
886
1018
  );
887
1019
  })().catch((error) => {
888
1020
  const message = error instanceof Error ? error.message : String(error);
@@ -899,7 +1031,7 @@ async function appsDevHandler(ctx) {
899
1031
  heartbeatTimer = null;
900
1032
  }
901
1033
  try {
902
- removeAppDevSession(sessionId);
1034
+ removeAppDevSession(sessionId, sessionsFilePath);
903
1035
  } catch {
904
1036
  // ignore cleanup failures during shutdown
905
1037
  }
@@ -930,7 +1062,9 @@ async function appsBuildHandler(ctx) {
930
1062
  throw usageError(`Project has problems:\n${problems.map((p) => ` - ${p}`).join('\n')}`);
931
1063
  }
932
1064
 
933
- const { manifest } = await buildArtifact(projectDir);
1065
+ const { manifest } = await buildArtifact(projectDir, {
1066
+ stdio: ctx.output.isMachineMode() ? 'pipe' : 'inherit',
1067
+ });
934
1068
 
935
1069
  return ctx.output.emitSuccess({
936
1070
  command: ctx.spec.command_path.join(' '),
@@ -963,7 +1097,9 @@ async function appsVerifyHandler(ctx) {
963
1097
  }
964
1098
 
965
1099
  if (!ctx.options.skipBuild) {
966
- await buildArtifact(projectDir);
1100
+ await buildArtifact(projectDir, {
1101
+ stdio: ctx.output.isMachineMode() ? 'pipe' : 'inherit',
1102
+ });
967
1103
  }
968
1104
 
969
1105
  const manifest = readManifest(projectDir);
@@ -1175,7 +1311,9 @@ async function appsScreenshotHandler(ctx) {
1175
1311
  }
1176
1312
 
1177
1313
  if (!ctx.options.skipBuild) {
1178
- await buildArtifact(projectDir);
1314
+ await buildArtifact(projectDir, {
1315
+ stdio: ctx.output.isMachineMode() ? 'pipe' : 'inherit',
1316
+ });
1179
1317
  }
1180
1318
 
1181
1319
  const manifest = readManifest(projectDir);
@@ -1364,16 +1502,25 @@ async function appsScreenshotHandler(ctx) {
1364
1502
  }
1365
1503
  }
1366
1504
 
1505
+ export function buildLinkedAppState(existingState, appId, linkedAt = new Date().toISOString()) {
1506
+ return {
1507
+ ...(existingState?.dev_app_id ? { dev_app_id: existingState.dev_app_id } : {}),
1508
+ ...(existingState?.dev_linked_at ? { dev_linked_at: existingState.dev_linked_at } : {}),
1509
+ app_id: appId,
1510
+ linked_at: linkedAt,
1511
+ };
1512
+ }
1513
+
1367
1514
  async function appsLinkHandler(ctx) {
1368
1515
  const projectDir = resolveProjectDir(ctx.args.dir || '.');
1369
1516
  const appId = ctx.args.appId;
1370
1517
 
1371
1518
  await assertLinkTarget(ctx.runtime, appId);
1372
1519
 
1373
- writeLinkedState(projectDir, {
1374
- app_id: appId,
1375
- linked_at: new Date().toISOString(),
1376
- });
1520
+ writeLinkedState(
1521
+ projectDir,
1522
+ buildLinkedAppState(readLinkedState(projectDir), appId),
1523
+ );
1377
1524
 
1378
1525
  return ctx.output.emitSuccess({
1379
1526
  command: ctx.spec.command_path.join(' '),
@@ -1442,7 +1589,9 @@ async function appsDeployHandler(ctx) {
1442
1589
 
1443
1590
  // Build if needed
1444
1591
  if (!ctx.options.skipBuild) {
1445
- await buildArtifact(projectDir);
1592
+ await buildArtifact(projectDir, {
1593
+ stdio: ctx.output.isMachineMode() ? 'pipe' : 'inherit',
1594
+ });
1446
1595
  }
1447
1596
 
1448
1597
  // Direct deploy mode: upload to Supabase storage directly
@@ -1539,6 +1688,67 @@ function deployedAppVersion(app) {
1539
1688
  return Number.isFinite(parsed) ? parsed : 0;
1540
1689
  }
1541
1690
 
1691
+ async function appsDuplicateHandler(ctx) {
1692
+ const projectDir = resolveProjectDir(ctx.args.dir || '.');
1693
+ // Either target an app explicitly, or duplicate whatever this project is
1694
+ // linked to, so `notis apps duplicate` works from inside a project.
1695
+ const appId = requireLinkedAppId(projectDir, ctx.options.appId);
1696
+ const idempotencyKey = nextIdempotencyKey(ctx.globalOptions);
1697
+
1698
+ const copyDocuments = ctx.options.copyDocuments || 'declared';
1699
+ if (!['declared', 'all', 'none'].includes(copyDocuments)) {
1700
+ throw usageError("--copy-documents must be one of: declared, all, none.");
1701
+ }
1702
+
1703
+ const result = await runToolCommand({
1704
+ runtime: ctx.runtime,
1705
+ toolName: DUPLICATE_APP_TOOL,
1706
+ arguments_: {
1707
+ app_id: appId,
1708
+ ...(ctx.options.name ? { name: ctx.options.name } : {}),
1709
+ copy_documents: copyDocuments,
1710
+ },
1711
+ mutating: true,
1712
+ idempotencyKey,
1713
+ });
1714
+
1715
+ const payload = result.payload || {};
1716
+ if (payload.status === 'error') {
1717
+ throw usageError(`Could not duplicate app ${appId}: ${payload.message || 'unknown error'}`);
1718
+ }
1719
+
1720
+ const duplicated = payload.app || {};
1721
+ if (!duplicated.id) {
1722
+ throw usageError(`Could not duplicate app ${appId}: the backend did not return an app id.`);
1723
+ }
1724
+
1725
+ const data = {
1726
+ app_id: duplicated.id,
1727
+ name: duplicated.name,
1728
+ slug: duplicated.slug,
1729
+ duplicated_from_app_id: payload.duplicated_from_app_id || appId,
1730
+ copied_document_count: payload.copied_document_count ?? 0,
1731
+ portal_url: payload.portal_url,
1732
+ idempotency_key: idempotencyKey,
1733
+ // The duplicate owns brand new databases; nothing is shared with the source.
1734
+ databases: (payload.databases || []).map((database) => ({
1735
+ id: database.id,
1736
+ slug: database.slug,
1737
+ name: database.name,
1738
+ })),
1739
+ };
1740
+
1741
+ return ctx.output.emitSuccess({
1742
+ command: ctx.spec.command_path.join(' '),
1743
+ data,
1744
+ humanSummary: `Duplicated app ${appId} as ${duplicated.name || duplicated.id}`,
1745
+ hints: payload.portal_url
1746
+ ? [{ command: payload.portal_url, reason: 'Open the duplicated app in Portal' }]
1747
+ : [],
1748
+ meta: { mutating: true, idempotency_key: idempotencyKey },
1749
+ });
1750
+ }
1751
+
1542
1752
  async function appsPublishHandler(ctx) {
1543
1753
  if (ctx.options.confirmReady !== true) {
1544
1754
  throw usageError(
@@ -1652,10 +1862,20 @@ async function appsDoctorHandler(ctx) {
1652
1862
  ? `Found ${problems.length} problems:\n${problems.map((p) => ` - ${p}`).join('\n')}`
1653
1863
  : warnings.length
1654
1864
  ? `Healthy with ${warnings.length} warnings:\n${warnings.map((w) => ` - ${w}`).join('\n')}`
1655
- : `Project is healthy.${linkedState ? ` Linked to ${linkedState.app_id}.` : ' Not linked.'}`,
1865
+ : `Project is healthy.${doctorLinkSummary(linkedState)}`,
1656
1866
  });
1657
1867
  }
1658
1868
 
1869
+ export function doctorLinkSummary(linkedState) {
1870
+ if (linkedState?.app_id) {
1871
+ return ` Linked to app ${linkedState.app_id}.`;
1872
+ }
1873
+ if (linkedState?.dev_app_id) {
1874
+ return ` Local development app ${linkedState.dev_app_id} is active.`;
1875
+ }
1876
+ return ' Not linked.';
1877
+ }
1878
+
1659
1879
  // ---------------------------------------------------------------------------
1660
1880
  // Command specs
1661
1881
  // ---------------------------------------------------------------------------
@@ -1919,6 +2139,35 @@ export const appsCommandSpecs = [
1919
2139
  backend_call: { type: 'http', name: 'portal_apps/publish' },
1920
2140
  handler: appsPublishHandler,
1921
2141
  },
2142
+ {
2143
+ command_path: ['apps', 'duplicate'],
2144
+ summary: 'Duplicate an app into an independent copy with its own databases.',
2145
+ when_to_use:
2146
+ '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.',
2147
+ args_schema: {
2148
+ arguments: [
2149
+ { token: '[dir]', key: 'dir', description: 'Project directory (default: current dir).' },
2150
+ ],
2151
+ options: [
2152
+ { flags: '--app-id <id>', description: 'App to duplicate. Defaults to the app this project is linked to.' },
2153
+ { flags: '--name <name>', description: 'Name for the duplicate (default: the source name followed by "copy").' },
2154
+ {
2155
+ flags: '--copy-documents <mode>',
2156
+ description:
2157
+ "Which rows to copy: 'declared' (default, the starter content a fresh install would have), 'all', or 'none'.",
2158
+ },
2159
+ ],
2160
+ },
2161
+ examples: [
2162
+ 'notis apps duplicate --name "Blog"',
2163
+ 'notis apps duplicate --app-id abc123 --name "Bookmarks" --copy-documents none',
2164
+ ],
2165
+ mutates: true,
2166
+ idempotent: false,
2167
+ require_auth: true,
2168
+ backend_call: { type: 'tool', name: DUPLICATE_APP_TOOL },
2169
+ handler: appsDuplicateHandler,
2170
+ },
1922
2171
  {
1923
2172
  command_path: ['apps', 'doctor'],
1924
2173
  summary: 'Check project health and readiness.',
@@ -1,6 +1,7 @@
1
1
  import { appsCommandSpecs } from './apps.js';
2
2
  import { toolsCommandSpecs } from './tools.js';
3
3
  import { metaCommandSpecs } from './meta.js';
4
+ import { onboardingCommandSpecs } from './onboarding.js';
4
5
  import { diagnosticCommandSpecs } from './diagnostics.js';
5
6
  import { smokeCommandSpecs } from './smoke.js';
6
7
 
@@ -12,6 +13,7 @@ export const GROUP_SUMMARIES = {
12
13
  };
13
14
 
14
15
  export const COMMAND_SPECS = [
16
+ ...onboardingCommandSpecs,
15
17
  ...appsCommandSpecs,
16
18
  ...toolsCommandSpecs,
17
19
  ...diagnosticCommandSpecs,