@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.
- package/README.md +56 -2
- package/dist/scaffolds/notis-database/app/page.tsx +38 -41
- package/dist/scaffolds/notis-database/lib/types.ts +5 -7
- package/dist/scaffolds/notis-database/metadata/screenshot-1.png +0 -0
- package/dist/scaffolds/notis-database/metadata/screenshot-2.png +0 -0
- package/dist/scaffolds/notis-database/metadata/screenshot-3.png +0 -0
- package/dist/scaffolds/notis-database/metadata/screenshot-4.png +0 -0
- package/dist/scaffolds/notis-database/metadata/screenshot-5.png +0 -0
- package/dist/scaffolds/notis-database/metadata/screenshot-fixtures.json +1839 -0
- package/dist/scaffolds/notis-database/notis.config.ts +48 -1
- package/dist/scaffolds/notis-database/package-lock.json +3935 -0
- package/dist/scaffolds/notis-database/package.json +2 -1
- package/dist/scaffolds/{notis-affiliate-prospects → notis-database}/packages/sdk/package.json +4 -0
- package/dist/scaffolds/notis-database/packages/sdk/src/components/DocumentEditor.tsx +93 -0
- package/dist/scaffolds/notis-database/packages/sdk/src/components/Markdown.tsx +60 -0
- package/dist/scaffolds/{notis-affiliate-prospects → notis-database}/packages/sdk/src/components/MultiSelectActionBar.tsx +7 -2
- package/dist/scaffolds/notis-database/packages/sdk/src/config.ts +196 -0
- package/dist/scaffolds/notis-database/packages/sdk/src/documents.ts +229 -0
- package/dist/scaffolds/notis-database/packages/sdk/src/hooks/useDatabaseSchema.ts +85 -0
- package/dist/scaffolds/notis-database/packages/sdk/src/hooks/useDocument.ts +78 -0
- package/dist/scaffolds/notis-database/packages/sdk/src/hooks/useDocuments.ts +121 -0
- package/dist/scaffolds/{notis-affiliate-prospects → notis-database}/packages/sdk/src/hooks/useMultiSelect.ts +38 -2
- package/dist/scaffolds/notis-database/packages/sdk/src/hooks/useUpsertDocument.ts +95 -0
- package/dist/scaffolds/{notis-affiliate-prospects → notis-database}/packages/sdk/src/index.ts +30 -1
- package/dist/scaffolds/{notis-affiliate-prospects → notis-database}/packages/sdk/src/runtime.ts +76 -17
- package/dist/scaffolds/notis-database/packages/sdk/src/styles.css +186 -0
- package/dist/scaffolds/notis-database/src/mock-runtime.ts +4 -1
- package/dist/scaffolds/notis-journal/CHANGELOG.md +4 -0
- package/dist/scaffolds/notis-journal/app/page.tsx +1 -0
- package/dist/scaffolds/notis-journal/package-lock.json +4615 -0
- package/dist/scaffolds/notis-journal/package.json +2 -2
- package/dist/scaffolds/notis-journal/packages/sdk/src/config.ts +52 -1
- package/dist/scaffolds/notis-notes/app/page.tsx +39 -46
- package/dist/scaffolds/notis-notes/app/phosphor-icons.ts +596 -0
- package/dist/scaffolds/notis-notes/lib/visible-properties.ts +144 -0
- package/dist/scaffolds/notis-notes/metadata/screenshot-1.png +0 -0
- package/dist/scaffolds/notis-notes/metadata/screenshot-2.png +0 -0
- package/dist/scaffolds/notis-notes/metadata/screenshot-3.png +0 -0
- package/dist/scaffolds/notis-notes/metadata/screenshot-4.png +0 -0
- package/dist/scaffolds/notis-notes/metadata/screenshot-5.png +0 -0
- package/dist/scaffolds/notis-notes/metadata/screenshot-fixtures.json +752 -0
- package/dist/scaffolds/notis-notes/notis.config.ts +48 -3
- package/dist/scaffolds/notis-notes/package-lock.json +4636 -0
- package/dist/scaffolds/notis-notes/package.json +7 -3
- package/dist/scaffolds/notis-notes/packages/sdk/package.json +36 -0
- package/dist/scaffolds/notis-notes/packages/sdk/src/components/DocumentEditor.tsx +93 -0
- package/dist/scaffolds/notis-notes/packages/sdk/src/components/Markdown.tsx +60 -0
- package/dist/scaffolds/notis-notes/packages/sdk/src/components/MultiSelectActionBar.tsx +278 -0
- package/dist/scaffolds/notis-notes/packages/sdk/src/components/MultiSelectCheckbox.tsx +91 -0
- package/dist/scaffolds/notis-notes/packages/sdk/src/components/MultiSelectDragOverlay.tsx +39 -0
- package/dist/scaffolds/notis-notes/packages/sdk/src/config.ts +196 -0
- package/dist/scaffolds/notis-notes/packages/sdk/src/documents.ts +229 -0
- package/dist/scaffolds/notis-notes/packages/sdk/src/hooks/useBackend.ts +41 -0
- package/dist/scaffolds/notis-notes/packages/sdk/src/hooks/useDatabaseSchema.ts +85 -0
- package/dist/scaffolds/notis-notes/packages/sdk/src/hooks/useDocument.ts +78 -0
- package/dist/scaffolds/notis-notes/packages/sdk/src/hooks/useDocuments.ts +121 -0
- package/dist/scaffolds/notis-notes/packages/sdk/src/hooks/useMultiSelect.ts +539 -0
- package/dist/scaffolds/notis-notes/packages/sdk/src/hooks/useNotis.ts +34 -0
- package/dist/scaffolds/notis-notes/packages/sdk/src/hooks/useNotisNavigation.ts +49 -0
- package/dist/scaffolds/notis-notes/packages/sdk/src/hooks/useTool.ts +64 -0
- package/dist/scaffolds/notis-notes/packages/sdk/src/hooks/useTools.ts +56 -0
- package/dist/scaffolds/notis-notes/packages/sdk/src/hooks/useTopBarSearch.ts +73 -0
- package/dist/scaffolds/notis-notes/packages/sdk/src/hooks/useUpsertDocument.ts +95 -0
- package/dist/scaffolds/notis-notes/packages/sdk/src/index.ts +83 -0
- package/dist/scaffolds/notis-notes/packages/sdk/src/provider.tsx +43 -0
- package/dist/scaffolds/notis-notes/packages/sdk/src/runtime.ts +220 -0
- package/dist/scaffolds/notis-notes/packages/sdk/src/styles.css +186 -0
- package/dist/scaffolds/notis-notes/packages/sdk/src/ui.ts +15 -0
- package/dist/scaffolds/notis-notes/packages/sdk/src/vite.ts +56 -0
- package/dist/scaffolds/notis-notes/packages/sdk/tsconfig.json +15 -0
- package/dist/scaffolds/notis-random/CHANGELOG.md +12 -2
- package/dist/scaffolds/notis-random/app/history/page.tsx +5 -4
- package/dist/scaffolds/notis-random/app/page.tsx +141 -74
- package/dist/scaffolds/notis-random/components/ui/button.tsx +50 -0
- package/dist/scaffolds/notis-random/components/ui/card.tsx +16 -0
- package/dist/scaffolds/notis-random/components/ui/input.tsx +23 -0
- package/dist/scaffolds/notis-random/lib/notis-tools.ts +26 -7
- package/dist/scaffolds/notis-random/lib/rng.ts +179 -19
- package/dist/scaffolds/notis-random/lib/roll-record.ts +103 -16
- package/dist/scaffolds/notis-random/metadata/screenshot-1.png +0 -0
- package/dist/scaffolds/notis-random/metadata/screenshot-2.png +0 -0
- package/dist/scaffolds/notis-random/metadata/screenshot-3.png +0 -0
- package/dist/scaffolds/notis-random/metadata/screenshot-4.png +0 -0
- package/dist/scaffolds/notis-random/metadata/screenshot-5.png +0 -0
- package/dist/scaffolds/notis-random/metadata/screenshot-fixtures.json +753 -0
- package/dist/scaffolds/notis-random/notis.config.ts +47 -3
- package/dist/scaffolds/notis-random/package-lock.json +4513 -0
- package/dist/scaffolds/notis-random/package.json +6 -2
- package/dist/scaffolds/notis-random/packages/sdk/package.json +36 -0
- package/dist/scaffolds/notis-random/packages/sdk/src/components/DocumentEditor.tsx +93 -0
- package/dist/scaffolds/notis-random/packages/sdk/src/components/Markdown.tsx +60 -0
- package/dist/scaffolds/notis-random/packages/sdk/src/components/MultiSelectActionBar.tsx +278 -0
- package/dist/scaffolds/notis-random/packages/sdk/src/components/MultiSelectCheckbox.tsx +91 -0
- package/dist/scaffolds/notis-random/packages/sdk/src/components/MultiSelectDragOverlay.tsx +39 -0
- package/dist/scaffolds/notis-random/packages/sdk/src/config.ts +196 -0
- package/dist/scaffolds/notis-random/packages/sdk/src/documents.ts +229 -0
- package/dist/scaffolds/notis-random/packages/sdk/src/hooks/useBackend.ts +41 -0
- package/dist/scaffolds/notis-random/packages/sdk/src/hooks/useDatabaseSchema.ts +85 -0
- package/dist/scaffolds/notis-random/packages/sdk/src/hooks/useDocument.ts +78 -0
- package/dist/scaffolds/notis-random/packages/sdk/src/hooks/useDocuments.ts +121 -0
- package/dist/scaffolds/notis-random/packages/sdk/src/hooks/useMultiSelect.ts +539 -0
- package/dist/scaffolds/notis-random/packages/sdk/src/hooks/useNotis.ts +34 -0
- package/dist/scaffolds/notis-random/packages/sdk/src/hooks/useNotisNavigation.ts +49 -0
- package/dist/scaffolds/notis-random/packages/sdk/src/hooks/useTool.ts +64 -0
- package/dist/scaffolds/notis-random/packages/sdk/src/hooks/useTools.ts +56 -0
- package/dist/scaffolds/notis-random/packages/sdk/src/hooks/useTopBarSearch.ts +73 -0
- package/dist/scaffolds/notis-random/packages/sdk/src/hooks/useUpsertDocument.ts +95 -0
- package/dist/scaffolds/notis-random/packages/sdk/src/index.ts +83 -0
- package/dist/scaffolds/notis-random/packages/sdk/src/provider.tsx +43 -0
- package/dist/scaffolds/notis-random/packages/sdk/src/runtime.ts +220 -0
- package/dist/scaffolds/notis-random/packages/sdk/src/styles.css +186 -0
- package/dist/scaffolds/notis-random/packages/sdk/src/ui.ts +15 -0
- package/dist/scaffolds/notis-random/packages/sdk/src/vite.ts +56 -0
- package/dist/scaffolds/notis-random/packages/sdk/tsconfig.json +15 -0
- package/dist/scaffolds/notis-random/tailwind.config.ts +8 -1
- package/dist/scaffolds.json +8 -17
- package/package.json +2 -1
- package/skills/notis-apps/SKILL.md +438 -144
- package/skills/notis-apps/cli.md +16 -1
- package/skills/notis-cli/SKILL.md +11 -4
- package/skills/notis-onboarding/BRIEF.md +93 -0
- package/skills/notis-query/cli.md +1 -1
- package/src/cli.js +50 -12
- package/src/command-specs/apps.js +309 -41
- package/src/command-specs/auth.js +107 -0
- package/src/command-specs/index.js +4 -0
- package/src/command-specs/meta.js +68 -6
- package/src/command-specs/onboarding.js +304 -0
- package/src/command-specs/tools.js +8 -4
- package/src/runtime/app-dev-server.js +3 -2
- package/src/runtime/app-dev-sessions.js +14 -40
- package/src/runtime/app-platform.js +65 -4
- package/src/runtime/desktop-auth.js +86 -17
- package/src/runtime/errors.js +1 -0
- package/src/runtime/oauth.js +1105 -0
- package/src/runtime/output.js +16 -2
- package/src/runtime/profiles.js +542 -18
- package/src/runtime/telemetry.js +92 -0
- package/src/runtime/transport.js +66 -11
- package/template/.harness/index.html.tmpl +30 -2
- package/template/packages/sdk/src/config.ts +38 -1
- package/dist/scaffolds/notis-affiliate-prospects/CHANGELOG.md +0 -7
- package/dist/scaffolds/notis-affiliate-prospects/README.md +0 -20
- package/dist/scaffolds/notis-affiliate-prospects/app/globals.css +0 -42
- package/dist/scaffolds/notis-affiliate-prospects/app/layout.tsx +0 -7
- package/dist/scaffolds/notis-affiliate-prospects/app/page.tsx +0 -248
- package/dist/scaffolds/notis-affiliate-prospects/app/prospects/page.tsx +0 -134
- package/dist/scaffolds/notis-affiliate-prospects/app/segments/page.tsx +0 -108
- package/dist/scaffolds/notis-affiliate-prospects/components/ui/card.tsx +0 -35
- package/dist/scaffolds/notis-affiliate-prospects/index.html +0 -12
- package/dist/scaffolds/notis-affiliate-prospects/lib/affiliate-data.ts +0 -218
- package/dist/scaffolds/notis-affiliate-prospects/lib/demo-prospects.ts +0 -52
- package/dist/scaffolds/notis-affiliate-prospects/lib/notis-tools.ts +0 -100
- package/dist/scaffolds/notis-affiliate-prospects/lib/utils.ts +0 -25
- package/dist/scaffolds/notis-affiliate-prospects/metadata/screenshot-1.png +0 -0
- package/dist/scaffolds/notis-affiliate-prospects/metadata/screenshot-2.png +0 -0
- package/dist/scaffolds/notis-affiliate-prospects/metadata/screenshot-3.png +0 -0
- package/dist/scaffolds/notis-affiliate-prospects/metadata/screenshot-fixtures.json +0 -187
- package/dist/scaffolds/notis-affiliate-prospects/notis.config.ts +0 -65
- package/dist/scaffolds/notis-affiliate-prospects/package.json +0 -32
- package/dist/scaffolds/notis-affiliate-prospects/packages/sdk/src/config.ts +0 -90
- package/dist/scaffolds/notis-affiliate-prospects/packages/sdk/src/hooks/useDatabase.ts +0 -76
- package/dist/scaffolds/notis-affiliate-prospects/packages/sdk/src/hooks/useUpsertDocument.ts +0 -50
- package/dist/scaffolds/notis-affiliate-prospects/packages/sdk/src/styles.css +0 -38
- package/dist/scaffolds/notis-affiliate-prospects/postcss.config.mjs +0 -6
- package/dist/scaffolds/notis-affiliate-prospects/src/dev-main.tsx +0 -72
- package/dist/scaffolds/notis-affiliate-prospects/src/mock-runtime.ts +0 -242
- package/dist/scaffolds/notis-affiliate-prospects/tailwind.config.ts +0 -51
- package/dist/scaffolds/notis-affiliate-prospects/tsconfig.json +0 -23
- package/dist/scaffolds/notis-affiliate-prospects/vite.config.ts +0 -11
- /package/dist/scaffolds/{notis-affiliate-prospects → notis-database}/packages/sdk/src/components/MultiSelectCheckbox.tsx +0 -0
- /package/dist/scaffolds/{notis-affiliate-prospects → notis-database}/packages/sdk/src/components/MultiSelectDragOverlay.tsx +0 -0
- /package/dist/scaffolds/{notis-affiliate-prospects → notis-database}/packages/sdk/src/hooks/useBackend.ts +0 -0
- /package/dist/scaffolds/{notis-affiliate-prospects → notis-database}/packages/sdk/src/hooks/useNotis.ts +0 -0
- /package/dist/scaffolds/{notis-affiliate-prospects → notis-database}/packages/sdk/src/hooks/useNotisNavigation.ts +0 -0
- /package/dist/scaffolds/{notis-affiliate-prospects → notis-database}/packages/sdk/src/hooks/useTool.ts +0 -0
- /package/dist/scaffolds/{notis-affiliate-prospects → notis-database}/packages/sdk/src/hooks/useTools.ts +0 -0
- /package/dist/scaffolds/{notis-affiliate-prospects → notis-database}/packages/sdk/src/hooks/useTopBarSearch.ts +0 -0
- /package/dist/scaffolds/{notis-affiliate-prospects → notis-database}/packages/sdk/src/provider.tsx +0 -0
- /package/dist/scaffolds/{notis-affiliate-prospects → notis-database}/packages/sdk/src/ui.ts +0 -0
- /package/dist/scaffolds/{notis-affiliate-prospects → notis-database}/packages/sdk/src/vite.ts +0 -0
- /package/dist/scaffolds/{notis-affiliate-prospects → notis-database}/packages/sdk/tsconfig.json +0 -0
- /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
|
|
142
|
-
|
|
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 =
|
|
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'
|
|
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 = (
|
|
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
|
|
232
|
-
|
|
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
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
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: '
|
|
391
|
-
message: `Route "${route.slug}"
|
|
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
|
-
|
|
488
|
-
if (
|
|
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 ||
|
|
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:
|
|
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
|
-
|
|
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
|
|
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(
|
|
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
|
|
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]
|
|
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(
|
|
1374
|
-
|
|
1375
|
-
|
|
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
|
|
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.',
|