@notis_ai/cli 0.2.6 → 0.2.8

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 (151) hide show
  1. package/README.md +17 -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-database/packages/sdk/package.json +36 -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-database/packages/sdk/src/components/MultiSelectActionBar.tsx +278 -0
  17. package/dist/scaffolds/notis-database/packages/sdk/src/components/MultiSelectCheckbox.tsx +91 -0
  18. package/dist/scaffolds/notis-database/packages/sdk/src/components/MultiSelectDragOverlay.tsx +39 -0
  19. package/dist/scaffolds/notis-database/packages/sdk/src/config.ts +196 -0
  20. package/dist/scaffolds/notis-database/packages/sdk/src/documents.ts +229 -0
  21. package/dist/scaffolds/notis-database/packages/sdk/src/hooks/useBackend.ts +41 -0
  22. package/dist/scaffolds/notis-database/packages/sdk/src/hooks/useDatabaseSchema.ts +85 -0
  23. package/dist/scaffolds/notis-database/packages/sdk/src/hooks/useDocument.ts +78 -0
  24. package/dist/scaffolds/notis-database/packages/sdk/src/hooks/useDocuments.ts +121 -0
  25. package/dist/scaffolds/notis-database/packages/sdk/src/hooks/useMultiSelect.ts +539 -0
  26. package/dist/scaffolds/notis-database/packages/sdk/src/hooks/useNotis.ts +34 -0
  27. package/dist/scaffolds/notis-database/packages/sdk/src/hooks/useNotisNavigation.ts +49 -0
  28. package/dist/scaffolds/notis-database/packages/sdk/src/hooks/useTool.ts +64 -0
  29. package/dist/scaffolds/notis-database/packages/sdk/src/hooks/useTools.ts +56 -0
  30. package/dist/scaffolds/notis-database/packages/sdk/src/hooks/useTopBarSearch.ts +73 -0
  31. package/dist/scaffolds/notis-database/packages/sdk/src/hooks/useUpsertDocument.ts +95 -0
  32. package/dist/scaffolds/notis-database/packages/sdk/src/index.ts +83 -0
  33. package/dist/scaffolds/notis-database/packages/sdk/src/provider.tsx +43 -0
  34. package/dist/scaffolds/notis-database/packages/sdk/src/runtime.ts +220 -0
  35. package/dist/scaffolds/notis-database/packages/sdk/src/styles.css +186 -0
  36. package/dist/scaffolds/notis-database/packages/sdk/src/ui.ts +15 -0
  37. package/dist/scaffolds/notis-database/packages/sdk/src/vite.ts +56 -0
  38. package/dist/scaffolds/notis-database/packages/sdk/tsconfig.json +15 -0
  39. package/dist/scaffolds/notis-database/src/mock-runtime.ts +4 -1
  40. package/dist/scaffolds/notis-journal/CHANGELOG.md +4 -0
  41. package/dist/scaffolds/notis-journal/app/page.tsx +1 -0
  42. package/dist/scaffolds/notis-journal/package-lock.json +4615 -0
  43. package/dist/scaffolds/notis-journal/package.json +2 -2
  44. package/dist/scaffolds/notis-journal/packages/sdk/src/config.ts +52 -1
  45. package/dist/scaffolds/notis-notes/app/page.tsx +39 -46
  46. package/dist/scaffolds/notis-notes/app/phosphor-icons.ts +596 -0
  47. package/dist/scaffolds/notis-notes/lib/visible-properties.ts +144 -0
  48. package/dist/scaffolds/notis-notes/metadata/screenshot-1.png +0 -0
  49. package/dist/scaffolds/notis-notes/metadata/screenshot-2.png +0 -0
  50. package/dist/scaffolds/notis-notes/metadata/screenshot-3.png +0 -0
  51. package/dist/scaffolds/notis-notes/metadata/screenshot-4.png +0 -0
  52. package/dist/scaffolds/notis-notes/metadata/screenshot-5.png +0 -0
  53. package/dist/scaffolds/notis-notes/metadata/screenshot-fixtures.json +752 -0
  54. package/dist/scaffolds/notis-notes/notis.config.ts +48 -3
  55. package/dist/scaffolds/notis-notes/package-lock.json +4636 -0
  56. package/dist/scaffolds/notis-notes/package.json +7 -3
  57. package/dist/scaffolds/notis-notes/packages/sdk/package.json +36 -0
  58. package/dist/scaffolds/notis-notes/packages/sdk/src/components/DocumentEditor.tsx +93 -0
  59. package/dist/scaffolds/notis-notes/packages/sdk/src/components/Markdown.tsx +60 -0
  60. package/dist/scaffolds/notis-notes/packages/sdk/src/components/MultiSelectActionBar.tsx +278 -0
  61. package/dist/scaffolds/notis-notes/packages/sdk/src/components/MultiSelectCheckbox.tsx +91 -0
  62. package/dist/scaffolds/notis-notes/packages/sdk/src/components/MultiSelectDragOverlay.tsx +39 -0
  63. package/dist/scaffolds/notis-notes/packages/sdk/src/config.ts +196 -0
  64. package/dist/scaffolds/notis-notes/packages/sdk/src/documents.ts +229 -0
  65. package/dist/scaffolds/notis-notes/packages/sdk/src/hooks/useBackend.ts +41 -0
  66. package/dist/scaffolds/notis-notes/packages/sdk/src/hooks/useDatabaseSchema.ts +85 -0
  67. package/dist/scaffolds/notis-notes/packages/sdk/src/hooks/useDocument.ts +78 -0
  68. package/dist/scaffolds/notis-notes/packages/sdk/src/hooks/useDocuments.ts +121 -0
  69. package/dist/scaffolds/notis-notes/packages/sdk/src/hooks/useMultiSelect.ts +539 -0
  70. package/dist/scaffolds/notis-notes/packages/sdk/src/hooks/useNotis.ts +34 -0
  71. package/dist/scaffolds/notis-notes/packages/sdk/src/hooks/useNotisNavigation.ts +49 -0
  72. package/dist/scaffolds/notis-notes/packages/sdk/src/hooks/useTool.ts +64 -0
  73. package/dist/scaffolds/notis-notes/packages/sdk/src/hooks/useTools.ts +56 -0
  74. package/dist/scaffolds/notis-notes/packages/sdk/src/hooks/useTopBarSearch.ts +73 -0
  75. package/dist/scaffolds/notis-notes/packages/sdk/src/hooks/useUpsertDocument.ts +95 -0
  76. package/dist/scaffolds/notis-notes/packages/sdk/src/index.ts +83 -0
  77. package/dist/scaffolds/notis-notes/packages/sdk/src/provider.tsx +43 -0
  78. package/dist/scaffolds/notis-notes/packages/sdk/src/runtime.ts +220 -0
  79. package/dist/scaffolds/notis-notes/packages/sdk/src/styles.css +186 -0
  80. package/dist/scaffolds/notis-notes/packages/sdk/src/ui.ts +15 -0
  81. package/dist/scaffolds/notis-notes/packages/sdk/src/vite.ts +56 -0
  82. package/dist/scaffolds/notis-notes/packages/sdk/tsconfig.json +15 -0
  83. package/dist/scaffolds/notis-random/CHANGELOG.md +12 -2
  84. package/dist/scaffolds/notis-random/app/history/page.tsx +5 -4
  85. package/dist/scaffolds/notis-random/app/page.tsx +141 -74
  86. package/dist/scaffolds/notis-random/components/ui/button.tsx +50 -0
  87. package/dist/scaffolds/notis-random/components/ui/card.tsx +16 -0
  88. package/dist/scaffolds/notis-random/components/ui/input.tsx +23 -0
  89. package/dist/scaffolds/notis-random/components.json +20 -0
  90. package/dist/scaffolds/notis-random/lib/notis-tools.ts +26 -7
  91. package/dist/scaffolds/notis-random/lib/rng.ts +179 -19
  92. package/dist/scaffolds/notis-random/lib/roll-record.ts +103 -16
  93. package/dist/scaffolds/notis-random/metadata/screenshot-1.png +0 -0
  94. package/dist/scaffolds/notis-random/metadata/screenshot-2.png +0 -0
  95. package/dist/scaffolds/notis-random/metadata/screenshot-3.png +0 -0
  96. package/dist/scaffolds/notis-random/metadata/screenshot-4.png +0 -0
  97. package/dist/scaffolds/notis-random/metadata/screenshot-5.png +0 -0
  98. package/dist/scaffolds/notis-random/metadata/screenshot-fixtures.json +753 -0
  99. package/dist/scaffolds/notis-random/notis.config.ts +47 -3
  100. package/dist/scaffolds/notis-random/package-lock.json +4513 -0
  101. package/dist/scaffolds/notis-random/package.json +6 -2
  102. package/dist/scaffolds/notis-random/packages/sdk/package.json +36 -0
  103. package/dist/scaffolds/notis-random/packages/sdk/src/components/DocumentEditor.tsx +93 -0
  104. package/dist/scaffolds/notis-random/packages/sdk/src/components/Markdown.tsx +60 -0
  105. package/dist/scaffolds/notis-random/packages/sdk/src/components/MultiSelectActionBar.tsx +278 -0
  106. package/dist/scaffolds/notis-random/packages/sdk/src/components/MultiSelectCheckbox.tsx +91 -0
  107. package/dist/scaffolds/notis-random/packages/sdk/src/components/MultiSelectDragOverlay.tsx +39 -0
  108. package/dist/scaffolds/notis-random/packages/sdk/src/config.ts +196 -0
  109. package/dist/scaffolds/notis-random/packages/sdk/src/documents.ts +229 -0
  110. package/dist/scaffolds/notis-random/packages/sdk/src/hooks/useBackend.ts +41 -0
  111. package/dist/scaffolds/notis-random/packages/sdk/src/hooks/useDatabaseSchema.ts +85 -0
  112. package/dist/scaffolds/notis-random/packages/sdk/src/hooks/useDocument.ts +78 -0
  113. package/dist/scaffolds/notis-random/packages/sdk/src/hooks/useDocuments.ts +121 -0
  114. package/dist/scaffolds/notis-random/packages/sdk/src/hooks/useMultiSelect.ts +539 -0
  115. package/dist/scaffolds/notis-random/packages/sdk/src/hooks/useNotis.ts +34 -0
  116. package/dist/scaffolds/notis-random/packages/sdk/src/hooks/useNotisNavigation.ts +49 -0
  117. package/dist/scaffolds/notis-random/packages/sdk/src/hooks/useTool.ts +64 -0
  118. package/dist/scaffolds/notis-random/packages/sdk/src/hooks/useTools.ts +56 -0
  119. package/dist/scaffolds/notis-random/packages/sdk/src/hooks/useTopBarSearch.ts +73 -0
  120. package/dist/scaffolds/notis-random/packages/sdk/src/hooks/useUpsertDocument.ts +95 -0
  121. package/dist/scaffolds/notis-random/packages/sdk/src/index.ts +83 -0
  122. package/dist/scaffolds/notis-random/packages/sdk/src/provider.tsx +43 -0
  123. package/dist/scaffolds/notis-random/packages/sdk/src/runtime.ts +220 -0
  124. package/dist/scaffolds/notis-random/packages/sdk/src/styles.css +186 -0
  125. package/dist/scaffolds/notis-random/packages/sdk/src/ui.ts +15 -0
  126. package/dist/scaffolds/notis-random/packages/sdk/src/vite.ts +56 -0
  127. package/dist/scaffolds/notis-random/packages/sdk/tsconfig.json +15 -0
  128. package/dist/scaffolds/notis-random/tailwind.config.ts +8 -1
  129. package/dist/scaffolds.json +8 -6
  130. package/package.json +2 -1
  131. package/skills/notis-apps/SKILL.md +449 -144
  132. package/skills/notis-apps/cli.md +15 -0
  133. package/skills/notis-cli/SKILL.md +8 -4
  134. package/skills/notis-onboarding/BRIEF.md +93 -0
  135. package/src/cli.js +52 -7
  136. package/src/command-specs/apps.js +154 -21
  137. package/src/command-specs/diagnostics.js +674 -0
  138. package/src/command-specs/helpers.js +4 -1
  139. package/src/command-specs/index.js +8 -0
  140. package/src/command-specs/onboarding.js +216 -0
  141. package/src/command-specs/smoke.js +386 -0
  142. package/src/command-specs/tools.js +189 -5
  143. package/src/runtime/app-platform.js +65 -4
  144. package/src/runtime/desktop-auth.js +66 -17
  145. package/src/runtime/errors.js +1 -0
  146. package/src/runtime/output.js +21 -3
  147. package/src/runtime/profiles.js +214 -18
  148. package/src/runtime/telemetry.js +92 -0
  149. package/src/runtime/transport.js +29 -1
  150. package/template/.harness/index.html.tmpl +30 -2
  151. package/template/packages/sdk/src/config.ts +38 -1
@@ -232,6 +232,21 @@ Examples:
232
232
  - `npx --package @notis_ai/cli@latest -- notis apps publish --confirm-ready`
233
233
  - `npx --package @notis_ai/cli@latest -- notis apps publish ./my-app --confirm-ready`
234
234
 
235
+ ### `npx --package @notis_ai/cli@latest -- notis apps duplicate [dir]`
236
+
237
+ Duplicate an app into an independent copy with its own databases.
238
+
239
+ When to use: 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.
240
+
241
+ Options:
242
+ - `--app-id <id>` — App to duplicate. Defaults to the app this project is linked to.
243
+ - `--name <name>` — Name for the duplicate (default: the source name followed by "copy").
244
+ - `--copy-documents <mode>` — Which rows to copy: 'declared' (default, the starter content a fresh install would have), 'all', or 'none'.
245
+
246
+ Examples:
247
+ - `npx --package @notis_ai/cli@latest -- notis apps duplicate --name "Blog"`
248
+ - `npx --package @notis_ai/cli@latest -- notis apps duplicate --app-id abc123 --name "Bookmarks" --copy-documents none`
249
+
235
250
  ### `npx --package @notis_ai/cli@latest -- notis apps doctor [dir]`
236
251
 
237
252
  Check project health and readiness.
@@ -49,9 +49,9 @@ Treat the Notis CLI the same way you would treat a Composio-style tool router fl
49
49
 
50
50
  Use this section when the goal is to create or update a Notis app from a local workspace.
51
51
 
52
- Notis apps are Vite + React projects using `@notis/sdk`. The workflow is init or pull, dev, build, verify, create/link, deploy.
52
+ Notis apps are Vite + React projects using `@notis/sdk`. The workflow is init or pull, dev, build, verify, create/link, deploy, then an explicitly confirmed Store submission when requested.
53
53
 
54
- Important: `deploy` means updating the installed app artifact for the current user or team. To list an app in the public Notis App Store, the owner opens the app details page in the Notis Portal, sets visibility to Team or Public, then uses Publish/Update. The CLI does not publish to the store; the backend opens the registry PR using a service token.
54
+ Important: `deploy` only updates the installed app artifact for the current user or team. Store submission is a separate, user-gated step. After the user explicitly confirms that the current App Details page and Store listing are ready, `apps publish --confirm-ready` submits the matching deployed version through the backend review flow.
55
55
 
56
56
  ### App development workflow
57
57
 
@@ -74,6 +74,8 @@ Important: `deploy` means updating the installed app artifact for the current us
74
74
  - `npx --package @notis_ai/cli@latest -- notis apps deploy`
75
75
  9. Check project health:
76
76
  - `npx --package @notis_ai/cli@latest -- notis apps doctor`
77
+ 10. Only after the user explicitly approves the current Store preview, submit the deployed version:
78
+ - `npx --package @notis_ai/cli@latest -- notis apps publish --confirm-ready`
77
79
 
78
80
  ### App development rules
79
81
 
@@ -82,7 +84,8 @@ Important: `deploy` means updating the installed app artifact for the current us
82
84
  - Link before `deploy`, or pass `--app-id <id>` when intentionally deploying without writing local link state.
83
85
  - Use `npx --package @notis_ai/cli@latest -- notis apps doctor` to diagnose configuration or dependency issues.
84
86
  - Use `npx --package @notis_ai/cli@latest -- notis apps list` to discover existing app IDs before linking.
85
- - For App Store publishing, do not use the CLI. The user opens the app details page in the Notis Portal, sets visibility to Team or Public, then uses Publish/Update. The backend opens the registry PR on their behalf using a service token. Do not invent any CLI store-publish command.
87
+ - Never treat deploy approval as Store approval. Set visibility to Team or Public first, then run `apps publish --confirm-ready` only after the user explicitly confirms the current App Details page and Store listing.
88
+ - `apps publish --confirm-ready` submits the deployed snapshot through the same backend review flow as App Details. It must reject missing confirmation, incomplete listing media, a local/deployed version mismatch, private visibility, or an existing pending review.
86
89
 
87
90
  ### App development command reference
88
91
 
@@ -96,9 +99,10 @@ Important: `deploy` means updating the installed app artifact for the current us
96
99
  - `npx --package @notis_ai/cli@latest -- notis apps link` -- associate the project with a remote app
97
100
  - `npx --package @notis_ai/cli@latest -- notis apps deploy` -- upload the artifact and editable source snapshot to the linked installed app in Notis
98
101
  - `npx --package @notis_ai/cli@latest -- notis apps deploy --direct` -- deploy directly to Supabase storage, bypassing the backend server (auto-fallback when server is down)
102
+ - `npx --package @notis_ai/cli@latest -- notis apps publish --confirm-ready` -- submit the matching deployed version for Team or Public Store review after explicit user confirmation
99
103
  - `npx --package @notis_ai/cli@latest -- notis apps doctor` -- run project diagnostics
100
104
 
101
- App Store publishing is portal-only there is no CLI publish command.
105
+ App Details remains the visual review surface and offers the same Publish/Update action. The CLI command is for agents completing an already approved submission; it does not weaken the separate confirmation gate.
102
106
 
103
107
  If the task is specifically about app structure, runtime behavior, or database/view packaging, pair this skill with the `notis-apps` skill. Use `notis-cli` for the command workflow and `notis-apps` for the product/runtime contract.
104
108
 
@@ -0,0 +1,93 @@
1
+ You are onboarding someone to Notis from inside their coding agent. You already
2
+ have a shell and an authenticated Notis CLI, so you do the setup for them and
3
+ report what you did.
4
+
5
+ Run every Notis operation as:
6
+
7
+ ```bash
8
+ npx --package @notis_ai/cli@latest -- notis tools exec <TOOL> --arguments '<json>'
9
+ ```
10
+
11
+ Never invent tool names. `notis tools search "<what you need>"` finds them.
12
+
13
+ ## 1. Collect the basics
14
+
15
+ The fields onboarding collects, wherever it runs. Both the conversational
16
+ onboarding assistant and the CLI brief reference this partial so the two paths
17
+ can never quietly collect different things.
18
+
19
+ All of these land through `LOCAL_NOTIS_SAVE_USER_SETTINGS`: `full_name` on the
20
+ user row, the rest merged into the `settings` blob.
21
+
22
+ | Field | Setting key | What it is for |
23
+ |---|---|---|
24
+ | First name | `full_name` | How Notis addresses the user. Deduce from the email before asking. |
25
+ | Occupation / role | `position` | Tailors examples and suggestions. |
26
+ | Language | `language` | The language Notis replies in. |
27
+ | Time zone | `timezone` | Anchors every scheduled and time-relative request. Ask; never guess silently. |
28
+ | Attribution | `attribution` | Where they heard about Notis. Asked once, never again. |
29
+
30
+ Rules that hold on every surface:
31
+
32
+ * Ask one question at a time. A wall of questions reads as a form, and people
33
+ abandon forms.
34
+ * Skip anything already known or confidently deducible, and say what you deduced
35
+ rather than asking the user to confirm a blank.
36
+ * Save as soon as you have the basics rather than batching to the end — a user who
37
+ drops out halfway should not lose what they already told you.
38
+
39
+
40
+ You are in a terminal, so there is no phone number to infer a country from. Take
41
+ the language and time zone from the shell environment if you can read them, state
42
+ what you inferred, and let the user correct you. Then save:
43
+
44
+ ```bash
45
+ npx --package @notis_ai/cli@latest -- notis tools exec LOCAL_NOTIS_SAVE_USER_SETTINGS \
46
+ --arguments '{"full_name":"...","position":"...","language":"...","timezone":"..."}'
47
+ ```
48
+
49
+ ## 2. Connect one app, then immediately read from it
50
+
51
+ Ask what they use day to day. Gmail, Google Calendar, Notion, Slack, Linear, and
52
+ their CRM are the common answers. For each:
53
+
54
+ ```bash
55
+ npx --package @notis_ai/cli@latest -- notis tools link <toolkit>
56
+ ```
57
+
58
+ Give the user the returned URL and wait. Confirm with `notis tools toolkits`.
59
+
60
+ Then — and this is the step that makes onboarding land — run one read-only action
61
+ through the connection and show them the actual result: today's calendar events,
62
+ their most recent email threads, the databases in their Notion. A connected
63
+ integration is a claim; their own data on screen is proof.
64
+
65
+ If they connect nothing, fall back to installing a public app:
66
+ `LOCAL_NOTIS_LIST_PUBLIC_APP_STORE`, then `LOCAL_NOTIS_INSTALL_APP` with
67
+ `destination_type="personal"`.
68
+
69
+ ## 3. Finish onboarding
70
+
71
+ **Do this. It is not optional.**
72
+
73
+ ```bash
74
+ npx --package @notis_ai/cli@latest -- notis tools exec LOCAL_NOTIS_COMPLETE_TUTORIAL --arguments '{}'
75
+ ```
76
+
77
+ Until this runs, every message the user sends on every channel is routed to the
78
+ onboarding assistant — so an agent that collects everything and skips this leaves
79
+ them permanently stuck talking to an onboarding bot.
80
+
81
+ Verify with `LOCAL_NOTIS_GET_INTEGRATIONS_STATUS` and tell them what is connected.
82
+
83
+ ## What this plan does not include
84
+
85
+ Do **not** call `LOCAL_NOTIS_INSERT_REMINDER` or any automation tool during
86
+ onboarding. Reminders, automations, voice, and the messaging channels are paid
87
+ features; calling them here fails at the worst possible moment. If the user asks
88
+ for a reminder or a recurring task, say plainly that it needs an upgrade and offer
89
+ `LOCAL_NOTIS_GET_SUBSCRIPTION_STATUS` so they can see their plan.
90
+
91
+ What they *do* have: 1,000+ integrations through this CLI, skills that sync to
92
+ their coding agents, long-term memory, notes and databases, and the ability to
93
+ build Notis apps from this machine.
package/src/cli.js CHANGED
@@ -5,7 +5,13 @@ import { Command } from 'commander';
5
5
  import { COMMAND_SPECS, GROUP_SUMMARIES } from './command-specs/index.js';
6
6
  import { OutputManager } from './runtime/output.js';
7
7
  import { asCliError } from './runtime/errors.js';
8
- import { resolveRuntimeProfile, workspacePath } from './runtime/profiles.js';
8
+ import { reportCliCommand } from './runtime/telemetry.js';
9
+ import {
10
+ DEFAULT_PROFILE,
11
+ resolveOutputMode,
12
+ resolveRuntimeProfile,
13
+ workspacePath,
14
+ } from './runtime/profiles.js';
9
15
 
10
16
  // Read the version from package.json: the publish pipeline bumps the manifest
11
17
  // (scripts/release-utils.js applyVersion), so a hardcoded string here goes
@@ -68,10 +74,39 @@ function buildRuntime(globalOptions, spec) {
68
74
  ...runtime,
69
75
  cliVersion: CLI_VERSION,
70
76
  color: globalOptions.color !== false,
77
+ quiet: Boolean(globalOptions.quiet),
78
+ verbose: Boolean(globalOptions.verbose),
71
79
  workspacePath,
72
80
  };
73
81
  }
74
82
 
83
+ function buildErrorRuntime(globalOptions) {
84
+ try {
85
+ return {
86
+ ...resolveRuntimeProfile(globalOptions, {
87
+ requireAuth: false,
88
+ includeDebugEntitlementOverride: false,
89
+ }),
90
+ cliVersion: CLI_VERSION,
91
+ color: globalOptions.color !== false,
92
+ quiet: Boolean(globalOptions.quiet),
93
+ verbose: Boolean(globalOptions.verbose),
94
+ workspacePath,
95
+ };
96
+ } catch {
97
+ return {
98
+ profileName: globalOptions.profile || DEFAULT_PROFILE,
99
+ apiBase: null,
100
+ outputMode: resolveOutputMode(globalOptions),
101
+ cliVersion: CLI_VERSION,
102
+ color: globalOptions.color !== false,
103
+ quiet: Boolean(globalOptions.quiet),
104
+ verbose: Boolean(globalOptions.verbose),
105
+ workspacePath,
106
+ };
107
+ }
108
+ }
109
+
75
110
  function attachSpec(program, parentMap, spec, specs) {
76
111
  const parent = ensureParentCommand(program, parentMap, spec.command_path.slice(0, -1));
77
112
  const leaf = spec.command_path[spec.command_path.length - 1];
@@ -96,6 +131,7 @@ function attachSpec(program, parentMap, spec, specs) {
96
131
  }
97
132
 
98
133
  command.action(async (...raw) => {
134
+ const startedAt = Date.now();
99
135
  const commanderCommand = raw[raw.length - 1];
100
136
  const argumentValues = raw.slice(0, -1);
101
137
  const globalOptions = commanderCommand.optsWithGlobals();
@@ -118,15 +154,24 @@ function attachSpec(program, parentMap, spec, specs) {
118
154
  output,
119
155
  });
120
156
  process.exitCode = typeof exitCode === 'number' ? exitCode : 0;
157
+ await reportCliCommand({
158
+ spec,
159
+ runtime,
160
+ result: process.exitCode === 0 ? 'success' : 'failed',
161
+ durationMs: Date.now() - startedAt,
162
+ error: process.exitCode === 0 ? null : { exitCode: process.exitCode },
163
+ });
121
164
  } catch (error) {
122
- const runtime = {
123
- ...resolveRuntimeProfile(globalOptions, { requireAuth: false }),
124
- cliVersion: CLI_VERSION,
125
- color: globalOptions.color !== false,
126
- workspacePath,
127
- };
165
+ const runtime = buildErrorRuntime(globalOptions);
128
166
  const output = new OutputManager(runtime);
129
167
  const cliError = asCliError(error);
168
+ await reportCliCommand({
169
+ spec,
170
+ runtime,
171
+ result: 'failed',
172
+ durationMs: Date.now() - startedAt,
173
+ error: cliError,
174
+ });
130
175
  process.exitCode = output.emitError({
131
176
  command: spec.command_path.join(' '),
132
177
  error: cliError,
@@ -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';
@@ -66,6 +67,7 @@ const ENSURE_DEV_APP_INSTALLATION_TOOL = 'LOCAL_NOTIS_ENSURE_DEV_APP_INSTALLATIO
66
67
  const GET_APP_TOOL = 'LOCAL_NOTIS_GET_APP';
67
68
  const LIST_APPS_TOOL = 'LOCAL_NOTIS_LIST_APPS';
68
69
  const CREATE_APP_TOOL = 'LOCAL_NOTIS_CREATE_APP';
70
+ const DUPLICATE_APP_TOOL = 'LOCAL_NOTIS_DUPLICATE_APP';
69
71
  const SAVE_APP_FILES_TOOL = 'LOCAL_NOTIS_SAVE_APP_FILES';
70
72
 
71
73
  // ---------------------------------------------------------------------------
@@ -377,22 +379,35 @@ function assertHarnessResult(result, route, databaseSlugs) {
377
379
  details: error,
378
380
  });
379
381
  }
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) {
382
+ const declaredDatabaseSet = new Set(databaseSlugs);
383
+ const databaseQueries = (result.runtimeCalls || []).filter(
384
+ (call) =>
385
+ call?.op === 'callTool'
386
+ && localNotisToolSlug(call?.args?.name) === 'LOCAL_NOTIS_DATABASE_QUERY',
387
+ );
388
+ for (const call of databaseQueries) {
389
+ const databaseSlug = call?.args?.arguments?.database_slug;
390
+ if (databaseSlug && !declaredDatabaseSet.has(databaseSlug)) {
388
391
  assertions.push({
389
392
  ok: false,
390
- code: 'missing_database_query',
391
- message: `Route "${route.slug}" did not call LOCAL_NOTIS_DATABASE_QUERY for "${databaseSlug}".`,
393
+ code: 'undeclared_database_query',
394
+ message: `Route "${route.slug}" queried undeclared database "${databaseSlug}".`,
392
395
  details: { databaseSlug },
393
396
  });
394
397
  }
395
398
  }
399
+ const collectionDatabase = route?.collection?.database;
400
+ if (
401
+ collectionDatabase
402
+ && !databaseQueries.some((call) => call?.args?.arguments?.database_slug === collectionDatabase)
403
+ ) {
404
+ assertions.push({
405
+ ok: false,
406
+ code: 'missing_collection_database_query',
407
+ message: `Collection route "${route.slug}" did not query "${collectionDatabase}".`,
408
+ details: { databaseSlug: collectionDatabase },
409
+ });
410
+ }
396
411
  return assertions;
397
412
  }
398
413
 
@@ -440,6 +455,7 @@ function buildManifestForDev(appConfig) {
440
455
  css: 'bundle/app.css',
441
456
  },
442
457
  databases: appConfig.databases || [],
458
+ capabilities: normalizeAppCapabilities(appConfig.capabilities),
443
459
  tools: appConfig.tools || [],
444
460
  skills: (appConfig.skills || []).map((skill) => ({
445
461
  key: skill.key,
@@ -485,9 +501,9 @@ export async function ensureDevInstall({
485
501
  let linkedState = readLinkedState(projectDir);
486
502
  if (linkedState?.app_id) {
487
503
  const linkedApp = await getAccessibleApp(ctx.runtime, linkedState.app_id, runTool);
488
- if (!linkedApp || linkedApp.manifest?.is_dev === true) {
504
+ if (linkedApp?.manifest?.is_dev === true) {
489
505
  const { app_id: legacyDevAppId, linked_at: _linkedAt, deployed_at: _deployedAt, version: _version, ...rest } = linkedState;
490
- const devAppId = linkedState.dev_app_id || (linkedApp ? legacyDevAppId : null);
506
+ const devAppId = linkedState.dev_app_id || legacyDevAppId;
491
507
  linkedState = {
492
508
  ...rest,
493
509
  ...(devAppId ? { dev_app_id: devAppId } : {}),
@@ -930,7 +946,9 @@ async function appsBuildHandler(ctx) {
930
946
  throw usageError(`Project has problems:\n${problems.map((p) => ` - ${p}`).join('\n')}`);
931
947
  }
932
948
 
933
- const { manifest } = await buildArtifact(projectDir);
949
+ const { manifest } = await buildArtifact(projectDir, {
950
+ stdio: ctx.output.isMachineMode() ? 'pipe' : 'inherit',
951
+ });
934
952
 
935
953
  return ctx.output.emitSuccess({
936
954
  command: ctx.spec.command_path.join(' '),
@@ -963,7 +981,9 @@ async function appsVerifyHandler(ctx) {
963
981
  }
964
982
 
965
983
  if (!ctx.options.skipBuild) {
966
- await buildArtifact(projectDir);
984
+ await buildArtifact(projectDir, {
985
+ stdio: ctx.output.isMachineMode() ? 'pipe' : 'inherit',
986
+ });
967
987
  }
968
988
 
969
989
  const manifest = readManifest(projectDir);
@@ -1175,7 +1195,9 @@ async function appsScreenshotHandler(ctx) {
1175
1195
  }
1176
1196
 
1177
1197
  if (!ctx.options.skipBuild) {
1178
- await buildArtifact(projectDir);
1198
+ await buildArtifact(projectDir, {
1199
+ stdio: ctx.output.isMachineMode() ? 'pipe' : 'inherit',
1200
+ });
1179
1201
  }
1180
1202
 
1181
1203
  const manifest = readManifest(projectDir);
@@ -1364,16 +1386,25 @@ async function appsScreenshotHandler(ctx) {
1364
1386
  }
1365
1387
  }
1366
1388
 
1389
+ export function buildLinkedAppState(existingState, appId, linkedAt = new Date().toISOString()) {
1390
+ return {
1391
+ ...(existingState?.dev_app_id ? { dev_app_id: existingState.dev_app_id } : {}),
1392
+ ...(existingState?.dev_linked_at ? { dev_linked_at: existingState.dev_linked_at } : {}),
1393
+ app_id: appId,
1394
+ linked_at: linkedAt,
1395
+ };
1396
+ }
1397
+
1367
1398
  async function appsLinkHandler(ctx) {
1368
1399
  const projectDir = resolveProjectDir(ctx.args.dir || '.');
1369
1400
  const appId = ctx.args.appId;
1370
1401
 
1371
1402
  await assertLinkTarget(ctx.runtime, appId);
1372
1403
 
1373
- writeLinkedState(projectDir, {
1374
- app_id: appId,
1375
- linked_at: new Date().toISOString(),
1376
- });
1404
+ writeLinkedState(
1405
+ projectDir,
1406
+ buildLinkedAppState(readLinkedState(projectDir), appId),
1407
+ );
1377
1408
 
1378
1409
  return ctx.output.emitSuccess({
1379
1410
  command: ctx.spec.command_path.join(' '),
@@ -1442,7 +1473,9 @@ async function appsDeployHandler(ctx) {
1442
1473
 
1443
1474
  // Build if needed
1444
1475
  if (!ctx.options.skipBuild) {
1445
- await buildArtifact(projectDir);
1476
+ await buildArtifact(projectDir, {
1477
+ stdio: ctx.output.isMachineMode() ? 'pipe' : 'inherit',
1478
+ });
1446
1479
  }
1447
1480
 
1448
1481
  // Direct deploy mode: upload to Supabase storage directly
@@ -1539,6 +1572,67 @@ function deployedAppVersion(app) {
1539
1572
  return Number.isFinite(parsed) ? parsed : 0;
1540
1573
  }
1541
1574
 
1575
+ async function appsDuplicateHandler(ctx) {
1576
+ const projectDir = resolveProjectDir(ctx.args.dir || '.');
1577
+ // Either target an app explicitly, or duplicate whatever this project is
1578
+ // linked to, so `notis apps duplicate` works from inside a project.
1579
+ const appId = requireLinkedAppId(projectDir, ctx.options.appId);
1580
+ const idempotencyKey = nextIdempotencyKey(ctx.globalOptions);
1581
+
1582
+ const copyDocuments = ctx.options.copyDocuments || 'declared';
1583
+ if (!['declared', 'all', 'none'].includes(copyDocuments)) {
1584
+ throw usageError("--copy-documents must be one of: declared, all, none.");
1585
+ }
1586
+
1587
+ const result = await runToolCommand({
1588
+ runtime: ctx.runtime,
1589
+ toolName: DUPLICATE_APP_TOOL,
1590
+ arguments_: {
1591
+ app_id: appId,
1592
+ ...(ctx.options.name ? { name: ctx.options.name } : {}),
1593
+ copy_documents: copyDocuments,
1594
+ },
1595
+ mutating: true,
1596
+ idempotencyKey,
1597
+ });
1598
+
1599
+ const payload = result.payload || {};
1600
+ if (payload.status === 'error') {
1601
+ throw usageError(`Could not duplicate app ${appId}: ${payload.message || 'unknown error'}`);
1602
+ }
1603
+
1604
+ const duplicated = payload.app || {};
1605
+ if (!duplicated.id) {
1606
+ throw usageError(`Could not duplicate app ${appId}: the backend did not return an app id.`);
1607
+ }
1608
+
1609
+ const data = {
1610
+ app_id: duplicated.id,
1611
+ name: duplicated.name,
1612
+ slug: duplicated.slug,
1613
+ duplicated_from_app_id: payload.duplicated_from_app_id || appId,
1614
+ copied_document_count: payload.copied_document_count ?? 0,
1615
+ portal_url: payload.portal_url,
1616
+ idempotency_key: idempotencyKey,
1617
+ // The duplicate owns brand new databases; nothing is shared with the source.
1618
+ databases: (payload.databases || []).map((database) => ({
1619
+ id: database.id,
1620
+ slug: database.slug,
1621
+ name: database.name,
1622
+ })),
1623
+ };
1624
+
1625
+ return ctx.output.emitSuccess({
1626
+ command: ctx.spec.command_path.join(' '),
1627
+ data,
1628
+ humanSummary: `Duplicated app ${appId} as ${duplicated.name || duplicated.id}`,
1629
+ hints: payload.portal_url
1630
+ ? [{ command: payload.portal_url, reason: 'Open the duplicated app in Portal' }]
1631
+ : [],
1632
+ meta: { mutating: true, idempotency_key: idempotencyKey },
1633
+ });
1634
+ }
1635
+
1542
1636
  async function appsPublishHandler(ctx) {
1543
1637
  if (ctx.options.confirmReady !== true) {
1544
1638
  throw usageError(
@@ -1652,10 +1746,20 @@ async function appsDoctorHandler(ctx) {
1652
1746
  ? `Found ${problems.length} problems:\n${problems.map((p) => ` - ${p}`).join('\n')}`
1653
1747
  : warnings.length
1654
1748
  ? `Healthy with ${warnings.length} warnings:\n${warnings.map((w) => ` - ${w}`).join('\n')}`
1655
- : `Project is healthy.${linkedState ? ` Linked to ${linkedState.app_id}.` : ' Not linked.'}`,
1749
+ : `Project is healthy.${doctorLinkSummary(linkedState)}`,
1656
1750
  });
1657
1751
  }
1658
1752
 
1753
+ export function doctorLinkSummary(linkedState) {
1754
+ if (linkedState?.app_id) {
1755
+ return ` Linked to app ${linkedState.app_id}.`;
1756
+ }
1757
+ if (linkedState?.dev_app_id) {
1758
+ return ` Local development app ${linkedState.dev_app_id} is active.`;
1759
+ }
1760
+ return ' Not linked.';
1761
+ }
1762
+
1659
1763
  // ---------------------------------------------------------------------------
1660
1764
  // Command specs
1661
1765
  // ---------------------------------------------------------------------------
@@ -1919,6 +2023,35 @@ export const appsCommandSpecs = [
1919
2023
  backend_call: { type: 'http', name: 'portal_apps/publish' },
1920
2024
  handler: appsPublishHandler,
1921
2025
  },
2026
+ {
2027
+ command_path: ['apps', 'duplicate'],
2028
+ summary: 'Duplicate an app into an independent copy with its own databases.',
2029
+ when_to_use:
2030
+ '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.',
2031
+ args_schema: {
2032
+ arguments: [
2033
+ { token: '[dir]', key: 'dir', description: 'Project directory (default: current dir).' },
2034
+ ],
2035
+ options: [
2036
+ { flags: '--app-id <id>', description: 'App to duplicate. Defaults to the app this project is linked to.' },
2037
+ { flags: '--name <name>', description: 'Name for the duplicate (default: the source name followed by "copy").' },
2038
+ {
2039
+ flags: '--copy-documents <mode>',
2040
+ description:
2041
+ "Which rows to copy: 'declared' (default, the starter content a fresh install would have), 'all', or 'none'.",
2042
+ },
2043
+ ],
2044
+ },
2045
+ examples: [
2046
+ 'notis apps duplicate --name "Blog"',
2047
+ 'notis apps duplicate --app-id abc123 --name "Bookmarks" --copy-documents none',
2048
+ ],
2049
+ mutates: true,
2050
+ idempotent: false,
2051
+ require_auth: true,
2052
+ backend_call: { type: 'tool', name: DUPLICATE_APP_TOOL },
2053
+ handler: appsDuplicateHandler,
2054
+ },
1922
2055
  {
1923
2056
  command_path: ['apps', 'doctor'],
1924
2057
  summary: 'Check project health and readiness.',