@notis_ai/cli 0.2.0-beta.139.1 → 0.2.0-beta.144.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +44 -20
- package/dist/agent-hooks/notis-agent-hook.mjs +4579 -1449
- package/dist/base-skills/notis-apps/SKILL.md +83 -55
- package/dist/base-skills/notis-cli/SKILL.md +8 -8
- package/package.json +2 -2
- package/skills/notis-apps/cli.md +38 -17
- package/src/command-specs/apps.js +871 -387
- package/src/command-specs/auth.js +47 -10
- package/src/command-specs/onboarding.js +11 -4
- package/src/runtime/app-dev-consumers.js +154 -0
- package/src/runtime/app-dev-host-lock.js +80 -0
- package/src/runtime/app-dev-roots.js +284 -0
- package/src/runtime/app-dev-server.js +212 -22
- package/src/runtime/app-dev-sessions.js +105 -145
- package/src/runtime/app-platform.js +1291 -165
- package/src/runtime/app-registry-scaffolds.js +367 -0
- package/src/runtime/login-listener.js +15 -0
- package/src/runtime/oauth.js +1506 -141
- package/src/runtime/output.js +7 -0
- package/src/runtime/ports.js +16 -0
- package/src/runtime/profiles.js +0 -5
- package/template/app/layout.tsx +5 -1
- package/template/notis.config.ts +1 -0
- package/template/packages/sdk/package.json +2 -1
- package/template/packages/sdk/src/components/MarkdownEditor.tsx +121 -0
- package/template/packages/sdk/src/components/MultiSelectActionBar.tsx +37 -119
- package/template/packages/sdk/src/components/MultiSelectCheckbox.tsx +5 -1
- package/template/packages/sdk/src/components/MultiSelectDragOverlay.tsx +1 -1
- package/template/packages/sdk/src/components/NotisSelectionBoundary.tsx +55 -0
- package/template/packages/sdk/src/components/ShortcutHints.tsx +56 -0
- package/template/packages/sdk/src/config.ts +19 -0
- package/template/packages/sdk/src/hooks/useActiveResource.ts +19 -0
- package/template/packages/sdk/src/hooks/useCollectionInteractions.ts +709 -0
- package/template/packages/sdk/src/hooks/useHandover.ts +3 -0
- package/template/packages/sdk/src/hooks/useMultiSelect.ts +44 -482
- package/template/packages/sdk/src/hooks/useTool.ts +5 -4
- package/template/packages/sdk/src/index.ts +47 -2
- package/template/packages/sdk/src/interactions/actions.ts +46 -0
- package/template/packages/sdk/src/interactions/shortcuts.tsx +628 -0
- package/template/packages/sdk/src/interactions.ts +41 -0
- package/template/packages/sdk/src/provider.tsx +2 -1
- package/template/packages/sdk/src/runtime.ts +85 -3
- package/dist/scaffolds/notis-database/CHANGELOG.md +0 -5
- package/dist/scaffolds/notis-database/app/globals.css +0 -44
- package/dist/scaffolds/notis-database/app/layout.tsx +0 -6
- package/dist/scaffolds/notis-database/app/page.tsx +0 -1088
- package/dist/scaffolds/notis-database/components/ui/badge.tsx +0 -28
- package/dist/scaffolds/notis-database/components/ui/button.tsx +0 -53
- package/dist/scaffolds/notis-database/components/ui/card.tsx +0 -56
- package/dist/scaffolds/notis-database/components/ui/table.tsx +0 -120
- package/dist/scaffolds/notis-database/components.json +0 -20
- package/dist/scaffolds/notis-database/index.html +0 -12
- package/dist/scaffolds/notis-database/lib/types.ts +0 -132
- package/dist/scaffolds/notis-database/lib/utils.ts +0 -6
- 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 +0 -1839
- package/dist/scaffolds/notis-database/notis.config.ts +0 -73
- package/dist/scaffolds/notis-database/package-lock.json +0 -3935
- package/dist/scaffolds/notis-database/package.json +0 -32
- package/dist/scaffolds/notis-database/packages/sdk/package.json +0 -36
- package/dist/scaffolds/notis-database/packages/sdk/src/components/DocumentEditor.tsx +0 -93
- package/dist/scaffolds/notis-database/packages/sdk/src/components/Markdown.tsx +0 -60
- package/dist/scaffolds/notis-database/packages/sdk/src/components/MultiSelectActionBar.tsx +0 -278
- package/dist/scaffolds/notis-database/packages/sdk/src/components/MultiSelectCheckbox.tsx +0 -91
- package/dist/scaffolds/notis-database/packages/sdk/src/components/MultiSelectDragOverlay.tsx +0 -39
- package/dist/scaffolds/notis-database/packages/sdk/src/config.ts +0 -234
- package/dist/scaffolds/notis-database/packages/sdk/src/documents.ts +0 -250
- package/dist/scaffolds/notis-database/packages/sdk/src/hooks/useBackend.ts +0 -41
- package/dist/scaffolds/notis-database/packages/sdk/src/hooks/useCloudComputer.ts +0 -97
- package/dist/scaffolds/notis-database/packages/sdk/src/hooks/useDatabaseSchema.ts +0 -85
- package/dist/scaffolds/notis-database/packages/sdk/src/hooks/useDatabaseSubscription.ts +0 -76
- package/dist/scaffolds/notis-database/packages/sdk/src/hooks/useDocument.ts +0 -78
- package/dist/scaffolds/notis-database/packages/sdk/src/hooks/useDocuments.ts +0 -121
- package/dist/scaffolds/notis-database/packages/sdk/src/hooks/useHandover.ts +0 -75
- package/dist/scaffolds/notis-database/packages/sdk/src/hooks/useMultiSelect.ts +0 -539
- package/dist/scaffolds/notis-database/packages/sdk/src/hooks/useNotis.ts +0 -34
- package/dist/scaffolds/notis-database/packages/sdk/src/hooks/useNotisNavigation.ts +0 -49
- package/dist/scaffolds/notis-database/packages/sdk/src/hooks/useTool.ts +0 -64
- package/dist/scaffolds/notis-database/packages/sdk/src/hooks/useTools.ts +0 -56
- package/dist/scaffolds/notis-database/packages/sdk/src/hooks/useTopBarSearch.ts +0 -73
- package/dist/scaffolds/notis-database/packages/sdk/src/hooks/useUpsertDocument.ts +0 -95
- package/dist/scaffolds/notis-database/packages/sdk/src/index.ts +0 -100
- package/dist/scaffolds/notis-database/packages/sdk/src/provider.tsx +0 -43
- package/dist/scaffolds/notis-database/packages/sdk/src/runtime.ts +0 -351
- package/dist/scaffolds/notis-database/packages/sdk/src/styles.css +0 -186
- package/dist/scaffolds/notis-database/packages/sdk/src/ui.ts +0 -15
- package/dist/scaffolds/notis-database/packages/sdk/src/vite.ts +0 -56
- package/dist/scaffolds/notis-database/packages/sdk/tsconfig.json +0 -15
- package/dist/scaffolds/notis-database/postcss.config.mjs +0 -8
- package/dist/scaffolds/notis-database/src/dev-main.tsx +0 -23
- package/dist/scaffolds/notis-database/src/mock-runtime.ts +0 -561
- package/dist/scaffolds/notis-database/tailwind.config.ts +0 -59
- package/dist/scaffolds/notis-database/tsconfig.json +0 -23
- package/dist/scaffolds/notis-database/vite.config.ts +0 -22
- package/dist/scaffolds/notis-journal/CHANGELOG.md +0 -29
- package/dist/scaffolds/notis-journal/app/globals.css +0 -37
- package/dist/scaffolds/notis-journal/app/insights/page.tsx +0 -513
- package/dist/scaffolds/notis-journal/app/journal-core.tsx +0 -362
- package/dist/scaffolds/notis-journal/app/journal-ui.tsx +0 -337
- package/dist/scaffolds/notis-journal/app/layout.tsx +0 -6
- package/dist/scaffolds/notis-journal/app/page.tsx +0 -486
- package/dist/scaffolds/notis-journal/components/ui/badge.tsx +0 -28
- package/dist/scaffolds/notis-journal/components/ui/button.tsx +0 -53
- package/dist/scaffolds/notis-journal/components/ui/card.tsx +0 -56
- package/dist/scaffolds/notis-journal/components.json +0 -20
- package/dist/scaffolds/notis-journal/index.html +0 -12
- package/dist/scaffolds/notis-journal/lib/utils.ts +0 -6
- package/dist/scaffolds/notis-journal/metadata/screenshot-1.png +0 -0
- package/dist/scaffolds/notis-journal/metadata/screenshot-2.png +0 -0
- package/dist/scaffolds/notis-journal/metadata/screenshot-3.png +0 -0
- package/dist/scaffolds/notis-journal/metadata/screenshot-4.png +0 -0
- package/dist/scaffolds/notis-journal/metadata/screenshot-5.png +0 -0
- package/dist/scaffolds/notis-journal/metadata/screenshot-6.png +0 -0
- package/dist/scaffolds/notis-journal/metadata/screenshot-fixtures.json +0 -132
- package/dist/scaffolds/notis-journal/notis.config.ts +0 -93
- package/dist/scaffolds/notis-journal/package-lock.json +0 -4615
- package/dist/scaffolds/notis-journal/package.json +0 -34
- package/dist/scaffolds/notis-journal/packages/sdk/package.json +0 -36
- package/dist/scaffolds/notis-journal/packages/sdk/src/components/DocumentEditor.tsx +0 -93
- package/dist/scaffolds/notis-journal/packages/sdk/src/components/Markdown.tsx +0 -60
- package/dist/scaffolds/notis-journal/packages/sdk/src/components/MultiSelectActionBar.tsx +0 -278
- package/dist/scaffolds/notis-journal/packages/sdk/src/components/MultiSelectCheckbox.tsx +0 -91
- package/dist/scaffolds/notis-journal/packages/sdk/src/components/MultiSelectDragOverlay.tsx +0 -39
- package/dist/scaffolds/notis-journal/packages/sdk/src/config.ts +0 -234
- package/dist/scaffolds/notis-journal/packages/sdk/src/documents.ts +0 -250
- package/dist/scaffolds/notis-journal/packages/sdk/src/hooks/useBackend.ts +0 -41
- package/dist/scaffolds/notis-journal/packages/sdk/src/hooks/useCloudComputer.ts +0 -97
- package/dist/scaffolds/notis-journal/packages/sdk/src/hooks/useDatabaseSchema.ts +0 -85
- package/dist/scaffolds/notis-journal/packages/sdk/src/hooks/useDatabaseSubscription.ts +0 -76
- package/dist/scaffolds/notis-journal/packages/sdk/src/hooks/useDocument.ts +0 -78
- package/dist/scaffolds/notis-journal/packages/sdk/src/hooks/useDocuments.ts +0 -121
- package/dist/scaffolds/notis-journal/packages/sdk/src/hooks/useHandover.ts +0 -75
- package/dist/scaffolds/notis-journal/packages/sdk/src/hooks/useMultiSelect.ts +0 -539
- package/dist/scaffolds/notis-journal/packages/sdk/src/hooks/useNotis.ts +0 -34
- package/dist/scaffolds/notis-journal/packages/sdk/src/hooks/useNotisNavigation.ts +0 -49
- package/dist/scaffolds/notis-journal/packages/sdk/src/hooks/useTool.ts +0 -64
- package/dist/scaffolds/notis-journal/packages/sdk/src/hooks/useTools.ts +0 -56
- package/dist/scaffolds/notis-journal/packages/sdk/src/hooks/useTopBarSearch.ts +0 -73
- package/dist/scaffolds/notis-journal/packages/sdk/src/hooks/useUpsertDocument.ts +0 -95
- package/dist/scaffolds/notis-journal/packages/sdk/src/index.ts +0 -100
- package/dist/scaffolds/notis-journal/packages/sdk/src/provider.tsx +0 -43
- package/dist/scaffolds/notis-journal/packages/sdk/src/runtime.ts +0 -351
- package/dist/scaffolds/notis-journal/packages/sdk/src/styles.css +0 -186
- package/dist/scaffolds/notis-journal/packages/sdk/src/ui.ts +0 -15
- package/dist/scaffolds/notis-journal/packages/sdk/src/vite.ts +0 -56
- package/dist/scaffolds/notis-journal/packages/sdk/tsconfig.json +0 -15
- package/dist/scaffolds/notis-journal/postcss.config.mjs +0 -8
- package/dist/scaffolds/notis-journal/skills/journal-onboarding/SKILL.md +0 -120
- package/dist/scaffolds/notis-journal/src/dev-main.tsx +0 -58
- package/dist/scaffolds/notis-journal/src/mock-runtime.ts +0 -199
- package/dist/scaffolds/notis-journal/tailwind.config.ts +0 -58
- package/dist/scaffolds/notis-journal/tsconfig.json +0 -23
- package/dist/scaffolds/notis-journal/vite.config.ts +0 -10
- package/dist/scaffolds/notis-notes/CHANGELOG.md +0 -5
- package/dist/scaffolds/notis-notes/app/globals.css +0 -3
- package/dist/scaffolds/notis-notes/app/layout.tsx +0 -6
- package/dist/scaffolds/notis-notes/app/page.tsx +0 -1943
- package/dist/scaffolds/notis-notes/app/phosphor-icons.ts +0 -596
- package/dist/scaffolds/notis-notes/components/ui/badge.tsx +0 -28
- package/dist/scaffolds/notis-notes/components/ui/button.tsx +0 -53
- package/dist/scaffolds/notis-notes/components/ui/card.tsx +0 -56
- package/dist/scaffolds/notis-notes/components.json +0 -20
- package/dist/scaffolds/notis-notes/lib/utils.ts +0 -6
- package/dist/scaffolds/notis-notes/lib/visible-properties.ts +0 -144
- 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 +0 -752
- package/dist/scaffolds/notis-notes/notis.config.ts +0 -80
- package/dist/scaffolds/notis-notes/package-lock.json +0 -4636
- package/dist/scaffolds/notis-notes/package.json +0 -35
- package/dist/scaffolds/notis-notes/packages/sdk/package.json +0 -36
- package/dist/scaffolds/notis-notes/packages/sdk/src/components/DocumentEditor.tsx +0 -93
- package/dist/scaffolds/notis-notes/packages/sdk/src/components/Markdown.tsx +0 -60
- package/dist/scaffolds/notis-notes/packages/sdk/src/components/MultiSelectActionBar.tsx +0 -278
- package/dist/scaffolds/notis-notes/packages/sdk/src/components/MultiSelectCheckbox.tsx +0 -91
- package/dist/scaffolds/notis-notes/packages/sdk/src/components/MultiSelectDragOverlay.tsx +0 -39
- package/dist/scaffolds/notis-notes/packages/sdk/src/config.ts +0 -234
- package/dist/scaffolds/notis-notes/packages/sdk/src/documents.ts +0 -250
- package/dist/scaffolds/notis-notes/packages/sdk/src/hooks/useBackend.ts +0 -41
- package/dist/scaffolds/notis-notes/packages/sdk/src/hooks/useCloudComputer.ts +0 -97
- package/dist/scaffolds/notis-notes/packages/sdk/src/hooks/useDatabaseSchema.ts +0 -85
- package/dist/scaffolds/notis-notes/packages/sdk/src/hooks/useDatabaseSubscription.ts +0 -76
- package/dist/scaffolds/notis-notes/packages/sdk/src/hooks/useDocument.ts +0 -78
- package/dist/scaffolds/notis-notes/packages/sdk/src/hooks/useDocuments.ts +0 -121
- package/dist/scaffolds/notis-notes/packages/sdk/src/hooks/useHandover.ts +0 -75
- package/dist/scaffolds/notis-notes/packages/sdk/src/hooks/useMultiSelect.ts +0 -539
- package/dist/scaffolds/notis-notes/packages/sdk/src/hooks/useNotis.ts +0 -34
- package/dist/scaffolds/notis-notes/packages/sdk/src/hooks/useNotisNavigation.ts +0 -49
- package/dist/scaffolds/notis-notes/packages/sdk/src/hooks/useTool.ts +0 -64
- package/dist/scaffolds/notis-notes/packages/sdk/src/hooks/useTools.ts +0 -56
- package/dist/scaffolds/notis-notes/packages/sdk/src/hooks/useTopBarSearch.ts +0 -73
- package/dist/scaffolds/notis-notes/packages/sdk/src/hooks/useUpsertDocument.ts +0 -95
- package/dist/scaffolds/notis-notes/packages/sdk/src/index.ts +0 -100
- package/dist/scaffolds/notis-notes/packages/sdk/src/provider.tsx +0 -43
- package/dist/scaffolds/notis-notes/packages/sdk/src/runtime.ts +0 -351
- package/dist/scaffolds/notis-notes/packages/sdk/src/styles.css +0 -186
- package/dist/scaffolds/notis-notes/packages/sdk/src/ui.ts +0 -15
- package/dist/scaffolds/notis-notes/packages/sdk/src/vite.ts +0 -56
- package/dist/scaffolds/notis-notes/packages/sdk/tsconfig.json +0 -15
- package/dist/scaffolds/notis-notes/postcss.config.mjs +0 -8
- package/dist/scaffolds/notis-notes/tailwind.config.ts +0 -58
- package/dist/scaffolds/notis-notes/tsconfig.json +0 -23
- package/dist/scaffolds/notis-notes/vite.config.ts +0 -10
- package/dist/scaffolds/notis-random/CHANGELOG.md +0 -15
- package/dist/scaffolds/notis-random/README.md +0 -33
- package/dist/scaffolds/notis-random/app/globals.css +0 -11
- package/dist/scaffolds/notis-random/app/history/page.tsx +0 -67
- package/dist/scaffolds/notis-random/app/layout.tsx +0 -7
- package/dist/scaffolds/notis-random/app/page.tsx +0 -289
- package/dist/scaffolds/notis-random/components/ui/button.tsx +0 -50
- package/dist/scaffolds/notis-random/components/ui/card.tsx +0 -16
- package/dist/scaffolds/notis-random/components/ui/input.tsx +0 -23
- package/dist/scaffolds/notis-random/components.json +0 -20
- package/dist/scaffolds/notis-random/index.html +0 -12
- package/dist/scaffolds/notis-random/lib/notis-tools.ts +0 -128
- package/dist/scaffolds/notis-random/lib/rng.ts +0 -202
- package/dist/scaffolds/notis-random/lib/roll-record.ts +0 -189
- package/dist/scaffolds/notis-random/lib/utils.ts +0 -25
- 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 +0 -753
- package/dist/scaffolds/notis-random/notis.config.ts +0 -86
- package/dist/scaffolds/notis-random/package-lock.json +0 -4513
- package/dist/scaffolds/notis-random/package.json +0 -36
- package/dist/scaffolds/notis-random/packages/sdk/package.json +0 -36
- package/dist/scaffolds/notis-random/packages/sdk/src/components/DocumentEditor.tsx +0 -93
- package/dist/scaffolds/notis-random/packages/sdk/src/components/Markdown.tsx +0 -60
- package/dist/scaffolds/notis-random/packages/sdk/src/components/MultiSelectActionBar.tsx +0 -278
- package/dist/scaffolds/notis-random/packages/sdk/src/components/MultiSelectCheckbox.tsx +0 -91
- package/dist/scaffolds/notis-random/packages/sdk/src/components/MultiSelectDragOverlay.tsx +0 -39
- package/dist/scaffolds/notis-random/packages/sdk/src/config.ts +0 -234
- package/dist/scaffolds/notis-random/packages/sdk/src/documents.ts +0 -250
- package/dist/scaffolds/notis-random/packages/sdk/src/hooks/useBackend.ts +0 -41
- package/dist/scaffolds/notis-random/packages/sdk/src/hooks/useCloudComputer.ts +0 -97
- package/dist/scaffolds/notis-random/packages/sdk/src/hooks/useDatabaseSchema.ts +0 -85
- package/dist/scaffolds/notis-random/packages/sdk/src/hooks/useDatabaseSubscription.ts +0 -76
- package/dist/scaffolds/notis-random/packages/sdk/src/hooks/useDocument.ts +0 -78
- package/dist/scaffolds/notis-random/packages/sdk/src/hooks/useDocuments.ts +0 -121
- package/dist/scaffolds/notis-random/packages/sdk/src/hooks/useHandover.ts +0 -75
- package/dist/scaffolds/notis-random/packages/sdk/src/hooks/useMultiSelect.ts +0 -539
- package/dist/scaffolds/notis-random/packages/sdk/src/hooks/useNotis.ts +0 -34
- package/dist/scaffolds/notis-random/packages/sdk/src/hooks/useNotisNavigation.ts +0 -49
- package/dist/scaffolds/notis-random/packages/sdk/src/hooks/useTool.ts +0 -64
- package/dist/scaffolds/notis-random/packages/sdk/src/hooks/useTools.ts +0 -56
- package/dist/scaffolds/notis-random/packages/sdk/src/hooks/useTopBarSearch.ts +0 -73
- package/dist/scaffolds/notis-random/packages/sdk/src/hooks/useUpsertDocument.ts +0 -95
- package/dist/scaffolds/notis-random/packages/sdk/src/index.ts +0 -100
- package/dist/scaffolds/notis-random/packages/sdk/src/provider.tsx +0 -43
- package/dist/scaffolds/notis-random/packages/sdk/src/runtime.ts +0 -351
- package/dist/scaffolds/notis-random/packages/sdk/src/styles.css +0 -186
- package/dist/scaffolds/notis-random/packages/sdk/src/ui.ts +0 -15
- package/dist/scaffolds/notis-random/packages/sdk/src/vite.ts +0 -56
- package/dist/scaffolds/notis-random/packages/sdk/tsconfig.json +0 -15
- package/dist/scaffolds/notis-random/postcss.config.mjs +0 -6
- package/dist/scaffolds/notis-random/src/dev-main.tsx +0 -70
- package/dist/scaffolds/notis-random/src/mock-runtime.ts +0 -129
- package/dist/scaffolds/notis-random/tailwind.config.ts +0 -50
- package/dist/scaffolds/notis-random/tsconfig.json +0 -23
- package/dist/scaffolds/notis-random/vite.config.ts +0 -11
- package/dist/scaffolds.json +0 -49
|
@@ -4,10 +4,11 @@ import { installLocalAgentContext } from './agents.js';
|
|
|
4
4
|
async function loginHandler(ctx) {
|
|
5
5
|
const result = await loginWithOAuth(ctx.runtime, ctx.options, ctx.output);
|
|
6
6
|
if (result.agentAuthorization) {
|
|
7
|
+
const presentation = loginAgentAuthorizationPresentation(result.agentAuthorization);
|
|
7
8
|
return ctx.output.emitSuccess({
|
|
8
9
|
command: 'login',
|
|
9
10
|
data: result.agentAuthorization,
|
|
10
|
-
|
|
11
|
+
...presentation,
|
|
11
12
|
});
|
|
12
13
|
}
|
|
13
14
|
let agentSetup = [];
|
|
@@ -44,6 +45,27 @@ async function loginHandler(ctx) {
|
|
|
44
45
|
});
|
|
45
46
|
}
|
|
46
47
|
|
|
48
|
+
export function loginAgentAuthorizationPresentation(authorization) {
|
|
49
|
+
const browserHandOff = authorization?.hand_off === 'browser_callback';
|
|
50
|
+
const nextCommand = browserHandOff
|
|
51
|
+
? authorization?.confirm_command
|
|
52
|
+
: authorization?.redeem_command;
|
|
53
|
+
return {
|
|
54
|
+
humanSummary: browserHandOff
|
|
55
|
+
? 'Give the user the authorization URL. Signing in there completes it — there is no code to ask them for.'
|
|
56
|
+
: 'Give the user the authorization URL, then ask them for the code it shows.',
|
|
57
|
+
hints: nextCommand
|
|
58
|
+
? [{
|
|
59
|
+
command: nextCommand,
|
|
60
|
+
reason: browserHandOff
|
|
61
|
+
? 'Confirm the account once they have finished in the browser'
|
|
62
|
+
: 'Redeem the code shown in the browser',
|
|
63
|
+
}]
|
|
64
|
+
: [],
|
|
65
|
+
renderHuman: () => `Authorize Notis CLI: ${authorization.authorize_url}`,
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
|
|
47
69
|
async function logoutHandler(ctx) {
|
|
48
70
|
const result = await logoutOAuth(ctx.runtime, {
|
|
49
71
|
allProfiles: Boolean(ctx.options.allProfiles),
|
|
@@ -54,12 +76,24 @@ async function logoutHandler(ctx) {
|
|
|
54
76
|
oauth_connected: false,
|
|
55
77
|
cleared_profiles: result.profiles,
|
|
56
78
|
},
|
|
57
|
-
humanSummary:
|
|
58
|
-
|
|
59
|
-
:
|
|
79
|
+
humanSummary: logoutHumanSummary(result, {
|
|
80
|
+
allProfiles: Boolean(ctx.options.allProfiles),
|
|
81
|
+
profileName: ctx.runtime.profileName,
|
|
82
|
+
}),
|
|
60
83
|
});
|
|
61
84
|
}
|
|
62
85
|
|
|
86
|
+
export function logoutHumanSummary(result, { allProfiles, profileName }) {
|
|
87
|
+
if (allProfiles) {
|
|
88
|
+
return result.profiles.length > 0
|
|
89
|
+
? 'OAuth state was cleared from all connected or pending CLI profiles.'
|
|
90
|
+
: 'No OAuth credentials were connected in any CLI profile.';
|
|
91
|
+
}
|
|
92
|
+
return result.profiles.includes(profileName)
|
|
93
|
+
? `OAuth state was cleared for profile "${profileName}", including any pending authorization.`
|
|
94
|
+
: `No OAuth credential was connected for profile "${profileName}".`;
|
|
95
|
+
}
|
|
96
|
+
|
|
63
97
|
export const authCommandSpecs = [
|
|
64
98
|
{
|
|
65
99
|
command_path: ['login'],
|
|
@@ -71,8 +105,9 @@ export const authCommandSpecs = [
|
|
|
71
105
|
options: [
|
|
72
106
|
{ flags: '--no-browser', description: 'Print the authorization URL without opening a browser.' },
|
|
73
107
|
{ flags: '--print-url', description: 'Print the authorization URL even when opening a browser.' },
|
|
74
|
-
{ flags: '--
|
|
75
|
-
{ flags: '--
|
|
108
|
+
{ flags: '--mode <mode>', description: 'auto (default) hands the browser callback to a background listener when this command cannot wait; browser waits in-process; code shows a one-time code to copy.' },
|
|
109
|
+
{ flags: '--paste-code', description: 'Alias for --mode code.' },
|
|
110
|
+
{ flags: '--timeout-seconds <n>', description: 'Authorization lifetime in seconds (default 300 while waiting in a terminal; 1800 for detached or code hand-offs).' },
|
|
76
111
|
{ flags: '--scope <scope>', description: 'OAuth permission to request (repeatable).', collect: true },
|
|
77
112
|
{ flags: '--code <code>', description: 'Redeem the code shown in the browser after a non-interactive login.' },
|
|
78
113
|
],
|
|
@@ -82,11 +117,12 @@ export const authCommandSpecs = [
|
|
|
82
117
|
'notis login --profile work',
|
|
83
118
|
'notis login --profile beta --api-base https://api-beta.notis.ai',
|
|
84
119
|
'notis login --no-browser --print-url',
|
|
85
|
-
'notis login --
|
|
120
|
+
'notis login --mode browser',
|
|
121
|
+
'notis login --mode code',
|
|
86
122
|
'notis login --code 4f3c2b1a',
|
|
87
123
|
],
|
|
88
124
|
output_schema:
|
|
89
|
-
'Returns credential_source, profile, api_base, user_id, scopes, and credential expiries; agent
|
|
125
|
+
'Returns credential_source, profile, api_base, user_id, scopes, and credential expiries; non-blocking agent, machine-output, or non-interactive modes return authorize_url, expires_in, and hand_off ("browser_callback" needs no code, "code" must be redeemed with redeem_command).',
|
|
90
126
|
mutates: true,
|
|
91
127
|
idempotent: true,
|
|
92
128
|
require_auth: false,
|
|
@@ -103,14 +139,15 @@ export const authCommandSpecs = [
|
|
|
103
139
|
args_schema: {
|
|
104
140
|
arguments: [],
|
|
105
141
|
options: [
|
|
106
|
-
{ flags: '--all-profiles', description: '
|
|
142
|
+
{ flags: '--all-profiles', description: 'Clear OAuth credentials and pending authorizations from every CLI profile.' },
|
|
107
143
|
],
|
|
108
144
|
},
|
|
109
145
|
examples: ['notis logout', 'notis logout --profile work', 'notis logout --all-profiles'],
|
|
110
|
-
output_schema: 'Returns oauth_connected=false and the profiles whose OAuth credentials were
|
|
146
|
+
output_schema: 'Returns oauth_connected=false and the profiles whose OAuth credentials or pending authorizations were cleared.',
|
|
111
147
|
mutates: true,
|
|
112
148
|
idempotent: true,
|
|
113
149
|
require_auth: false,
|
|
150
|
+
allow_unknown_profile: true,
|
|
114
151
|
related_commands: ['notis login', 'notis profile list'],
|
|
115
152
|
backend_call: { type: 'oauth', name: 'revocation' },
|
|
116
153
|
handler: logoutHandler,
|
|
@@ -11,6 +11,7 @@ import {
|
|
|
11
11
|
} from '../runtime/profiles.js';
|
|
12
12
|
import { ensureFreshOAuthCredential, loginWithOAuth } from '../runtime/oauth.js';
|
|
13
13
|
import { runToolCommand } from './helpers.js';
|
|
14
|
+
import { loginAgentAuthorizationPresentation } from './auth.js';
|
|
14
15
|
import { installLocalAgentContext } from './agents.js';
|
|
15
16
|
|
|
16
17
|
const HERE = dirname(fileURLToPath(import.meta.url));
|
|
@@ -223,8 +224,13 @@ async function startHandler(ctx) {
|
|
|
223
224
|
// Browser authorization is the whole signup path: it creates the account when
|
|
224
225
|
// the address is new and authorizes this machine either way. There is nothing
|
|
225
226
|
// for the CLI to collect up front, and nothing to wait on afterwards.
|
|
226
|
-
const authorization = await loginWithOAuth(
|
|
227
|
+
const authorization = await loginWithOAuth(
|
|
228
|
+
runtime,
|
|
229
|
+
{ browser: true, mode: options.mode, reusePersistedCredential: true },
|
|
230
|
+
output,
|
|
231
|
+
);
|
|
227
232
|
if (authorization?.agentAuthorization) {
|
|
233
|
+
const presentation = loginAgentAuthorizationPresentation(authorization.agentAuthorization);
|
|
228
234
|
return output.emitSuccess({
|
|
229
235
|
command: 'start',
|
|
230
236
|
data: {
|
|
@@ -232,8 +238,7 @@ async function startHandler(ctx) {
|
|
|
232
238
|
profile: runtime.profileName,
|
|
233
239
|
...authorization.agentAuthorization,
|
|
234
240
|
},
|
|
235
|
-
|
|
236
|
-
renderHuman: () => `Authorize Notis CLI: ${authorization.agentAuthorization.authorize_url}`,
|
|
241
|
+
...presentation,
|
|
237
242
|
});
|
|
238
243
|
}
|
|
239
244
|
|
|
@@ -262,16 +267,18 @@ export const onboardingCommandSpecs = [
|
|
|
262
267
|
arguments: [],
|
|
263
268
|
options: [
|
|
264
269
|
{ flags: '--brief-only', description: 'Print the onboarding brief for an already-signed-in profile.' },
|
|
270
|
+
{ flags: '--mode <mode>', description: 'auto (default) hands the browser callback to a background listener when this command cannot wait; browser waits in-process; code shows a one-time code to copy.' },
|
|
265
271
|
],
|
|
266
272
|
},
|
|
267
273
|
examples: [
|
|
268
274
|
'notis start',
|
|
269
275
|
'notis start --profile work',
|
|
276
|
+
'notis start --mode browser',
|
|
270
277
|
'notis start --brief-only',
|
|
271
278
|
'notis start --json',
|
|
272
279
|
],
|
|
273
280
|
output_schema:
|
|
274
|
-
'Returns {authenticated, profile, api_base, onboarding_complete, known_settings, missing_settings, agent_setup, brief, brief_source} once signed in — brief is null when onboarding_complete is true — or {authorize_url, expires_in,
|
|
281
|
+
'Returns {authenticated, profile, api_base, onboarding_complete, known_settings, missing_settings, agent_setup, brief, brief_source} once signed in — brief is null when onboarding_complete is true — or {authorize_url, expires_in, hand_off, ...} while waiting for browser authorization. hand_off is "browser_callback" when signing in finishes the login by itself (re-run confirm_command to see it land) or "code" when the user must be asked for the code the page shows and it redeemed with redeem_command.',
|
|
275
282
|
mutates: true,
|
|
276
283
|
idempotent: true,
|
|
277
284
|
require_auth: false,
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
import { randomUUID } from 'node:crypto';
|
|
2
|
+
import {
|
|
3
|
+
existsSync,
|
|
4
|
+
lstatSync,
|
|
5
|
+
mkdirSync,
|
|
6
|
+
readFileSync,
|
|
7
|
+
renameSync,
|
|
8
|
+
rmSync,
|
|
9
|
+
writeFileSync,
|
|
10
|
+
} from 'node:fs';
|
|
11
|
+
import { homedir } from 'node:os';
|
|
12
|
+
import { dirname, join } from 'node:path';
|
|
13
|
+
|
|
14
|
+
export const DEFAULT_APP_DEV_CONSUMERS_FILE = join(homedir(), '.notis', 'app-dev-consumers.json');
|
|
15
|
+
export const APP_DEV_CONSUMER_EXPIRES_AFTER_MS = 10_000;
|
|
16
|
+
const waitArray = new Int32Array(new SharedArrayBuffer(4));
|
|
17
|
+
const CONSUMER_LOCK_STALE_AFTER_MS = 30_000;
|
|
18
|
+
|
|
19
|
+
function processIsRunning(pid) {
|
|
20
|
+
try {
|
|
21
|
+
process.kill(pid, 0);
|
|
22
|
+
return true;
|
|
23
|
+
} catch (error) {
|
|
24
|
+
return error?.code !== 'ESRCH';
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function reclaimConsumerLock(lockPath) {
|
|
29
|
+
const stat = lstatSync(lockPath);
|
|
30
|
+
if (!stat.isDirectory() || stat.isSymbolicLink()) {
|
|
31
|
+
throw new Error(`Refusing unsafe app development consumer lock: ${lockPath}`);
|
|
32
|
+
}
|
|
33
|
+
let ownerPid = null;
|
|
34
|
+
try {
|
|
35
|
+
const owner = readFileSync(join(lockPath, 'owner'), 'utf8').trim();
|
|
36
|
+
const parsed = Number.parseInt(owner.split('.')[0] || '', 10);
|
|
37
|
+
ownerPid = Number.isSafeInteger(parsed) && parsed > 0 ? parsed : null;
|
|
38
|
+
} catch {
|
|
39
|
+
// An interrupted owner write is reclaimed after the bounded stale window.
|
|
40
|
+
}
|
|
41
|
+
if (ownerPid !== null) return !processIsRunning(ownerPid);
|
|
42
|
+
return Date.now() - stat.mtimeMs >= CONSUMER_LOCK_STALE_AFTER_MS;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function withLock(filePath, callback) {
|
|
46
|
+
mkdirSync(dirname(filePath), { recursive: true, mode: 0o700 });
|
|
47
|
+
const lockPath = `${filePath}.lock`;
|
|
48
|
+
const owner = `${process.pid}.${randomUUID()}`;
|
|
49
|
+
const startedAt = Date.now();
|
|
50
|
+
while (true) {
|
|
51
|
+
try {
|
|
52
|
+
mkdirSync(lockPath, { mode: 0o700 });
|
|
53
|
+
writeFileSync(join(lockPath, 'owner'), owner, { mode: 0o600 });
|
|
54
|
+
break;
|
|
55
|
+
} catch (error) {
|
|
56
|
+
if (error?.code !== 'EEXIST') throw error;
|
|
57
|
+
try {
|
|
58
|
+
if (reclaimConsumerLock(lockPath)) {
|
|
59
|
+
rmSync(lockPath, { recursive: true, force: true });
|
|
60
|
+
continue;
|
|
61
|
+
}
|
|
62
|
+
} catch (lockError) {
|
|
63
|
+
if (lockError?.code === 'ENOENT') continue;
|
|
64
|
+
throw lockError;
|
|
65
|
+
}
|
|
66
|
+
if (Date.now() - startedAt > 2_000) {
|
|
67
|
+
throw new Error('App development consumer registry is busy.');
|
|
68
|
+
}
|
|
69
|
+
Atomics.wait(waitArray, 0, 0, 10);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
try {
|
|
73
|
+
return callback();
|
|
74
|
+
} finally {
|
|
75
|
+
try {
|
|
76
|
+
if (readFileSync(join(lockPath, 'owner'), 'utf8').trim() === owner) {
|
|
77
|
+
rmSync(lockPath, { recursive: true, force: true });
|
|
78
|
+
}
|
|
79
|
+
} catch {
|
|
80
|
+
// A replaced lock belongs to another writer.
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function normalize(value, now) {
|
|
86
|
+
const raw = value && typeof value === 'object' && Array.isArray(value.consumers)
|
|
87
|
+
? value.consumers
|
|
88
|
+
: [];
|
|
89
|
+
return raw.filter((lease) => {
|
|
90
|
+
const heartbeat = Date.parse(String(lease?.lastHeartbeatAt || ''));
|
|
91
|
+
return typeof lease?.instanceId === 'string'
|
|
92
|
+
&& typeof lease?.userId === 'string'
|
|
93
|
+
&& typeof lease?.apiBase === 'string'
|
|
94
|
+
&& Number.isSafeInteger(lease?.pid)
|
|
95
|
+
&& Number.isFinite(heartbeat)
|
|
96
|
+
&& now - heartbeat <= APP_DEV_CONSUMER_EXPIRES_AFTER_MS;
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export function readAppDevConsumers(filePath = DEFAULT_APP_DEV_CONSUMERS_FILE, now = Date.now()) {
|
|
101
|
+
if (!existsSync(filePath)) return [];
|
|
102
|
+
try {
|
|
103
|
+
return normalize(JSON.parse(readFileSync(filePath, 'utf8')), now);
|
|
104
|
+
} catch {
|
|
105
|
+
return [];
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function writeAppDevConsumers(filePath, consumers) {
|
|
110
|
+
mkdirSync(dirname(filePath), { recursive: true, mode: 0o700 });
|
|
111
|
+
const temporary = `${filePath}.${process.pid}.${randomUUID()}.tmp`;
|
|
112
|
+
writeFileSync(temporary, JSON.stringify({ version: 1, consumers }, null, 2), { mode: 0o600 });
|
|
113
|
+
renameSync(temporary, filePath);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
export function heartbeatAppDevConsumer(
|
|
117
|
+
lease,
|
|
118
|
+
filePath = DEFAULT_APP_DEV_CONSUMERS_FILE,
|
|
119
|
+
) {
|
|
120
|
+
return withLock(filePath, () => {
|
|
121
|
+
const now = Date.now();
|
|
122
|
+
const next = readAppDevConsumers(filePath, now)
|
|
123
|
+
.filter((entry) => entry.instanceId !== lease.instanceId);
|
|
124
|
+
next.push({
|
|
125
|
+
...lease,
|
|
126
|
+
apiBase: lease.apiBase.replace(/\/$/, ''),
|
|
127
|
+
lastHeartbeatAt: new Date(now).toISOString(),
|
|
128
|
+
});
|
|
129
|
+
writeAppDevConsumers(filePath, next);
|
|
130
|
+
return next;
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
export function removeAppDevConsumer(
|
|
135
|
+
instanceId,
|
|
136
|
+
filePath = DEFAULT_APP_DEV_CONSUMERS_FILE,
|
|
137
|
+
) {
|
|
138
|
+
return withLock(filePath, () => {
|
|
139
|
+
const next = readAppDevConsumers(filePath)
|
|
140
|
+
.filter((entry) => entry.instanceId !== instanceId);
|
|
141
|
+
writeAppDevConsumers(filePath, next);
|
|
142
|
+
return next;
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
export function hasAppDevConsumer(consumers, { mode, userId, apiBase }) {
|
|
147
|
+
if (mode === 'machine') return consumers.length > 0;
|
|
148
|
+
if (mode !== 'environment') return true;
|
|
149
|
+
const normalizedApiBase = String(apiBase || '').replace(/\/$/, '');
|
|
150
|
+
return consumers.some((consumer) => (
|
|
151
|
+
consumer.userId === userId
|
|
152
|
+
&& consumer.apiBase.replace(/\/$/, '') === normalizedApiBase
|
|
153
|
+
));
|
|
154
|
+
}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { createHash, randomUUID } from 'node:crypto';
|
|
2
|
+
import { lstatSync, mkdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
|
|
3
|
+
import { homedir } from 'node:os';
|
|
4
|
+
import { join } from 'node:path';
|
|
5
|
+
|
|
6
|
+
const DEFAULT_LOCK_ROOT = join(homedir(), '.notis', 'app-dev-host-locks');
|
|
7
|
+
const OWNER_FILE = 'owner.json';
|
|
8
|
+
|
|
9
|
+
function processIsRunning(pid) {
|
|
10
|
+
try {
|
|
11
|
+
process.kill(pid, 0);
|
|
12
|
+
return true;
|
|
13
|
+
} catch (error) {
|
|
14
|
+
return error?.code !== 'ESRCH';
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function lockName(identity, apiBase, projectDir) {
|
|
19
|
+
return createHash('sha256').update(`${identity}\0${apiBase}\0${projectDir}`).digest('hex');
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function readOwner(path) {
|
|
23
|
+
try {
|
|
24
|
+
const value = JSON.parse(readFileSync(join(path, OWNER_FILE), 'utf8'));
|
|
25
|
+
return Number.isSafeInteger(value?.pid) && typeof value?.ownerId === 'string'
|
|
26
|
+
? { pid: value.pid, ownerId: value.ownerId }
|
|
27
|
+
: null;
|
|
28
|
+
} catch {
|
|
29
|
+
return null;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function reclaimable(path, now, staleAfterMs) {
|
|
34
|
+
const stat = lstatSync(path);
|
|
35
|
+
if (!stat.isDirectory() || stat.isSymbolicLink()) {
|
|
36
|
+
throw new Error(`Refusing unsafe app development host lock: ${path}`);
|
|
37
|
+
}
|
|
38
|
+
const owner = readOwner(path);
|
|
39
|
+
if (owner) return !processIsRunning(owner.pid);
|
|
40
|
+
return now - stat.mtimeMs >= staleAfterMs;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function tryAcquireAppDevHostLock(options) {
|
|
44
|
+
const lockRoot = options.lockRoot || DEFAULT_LOCK_ROOT;
|
|
45
|
+
mkdirSync(lockRoot, { recursive: true, mode: 0o700 });
|
|
46
|
+
const path = join(lockRoot, lockName(options.identity, options.apiBase, options.projectDir));
|
|
47
|
+
const ownerId = `${process.pid}.${randomUUID()}`;
|
|
48
|
+
for (let attempt = 0; attempt < 2; attempt += 1) {
|
|
49
|
+
try {
|
|
50
|
+
mkdirSync(path, { mode: 0o700 });
|
|
51
|
+
try {
|
|
52
|
+
writeFileSync(join(path, OWNER_FILE), JSON.stringify({ pid: process.pid, ownerId }), {
|
|
53
|
+
mode: 0o600,
|
|
54
|
+
});
|
|
55
|
+
} catch (error) {
|
|
56
|
+
rmSync(path, { recursive: true, force: true });
|
|
57
|
+
throw error;
|
|
58
|
+
}
|
|
59
|
+
return { path, ownerId };
|
|
60
|
+
} catch (error) {
|
|
61
|
+
if (error?.code !== 'EEXIST') throw error;
|
|
62
|
+
if (reclaimable(path, options.now ?? Date.now(), options.staleAfterMs ?? 60_000)) {
|
|
63
|
+
rmSync(path, { recursive: true, force: true });
|
|
64
|
+
continue;
|
|
65
|
+
}
|
|
66
|
+
return null;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
return null;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export function releaseAppDevHostLock(lock) {
|
|
73
|
+
try {
|
|
74
|
+
if (readOwner(lock.path)?.ownerId === lock.ownerId) {
|
|
75
|
+
rmSync(lock.path, { recursive: true, force: true });
|
|
76
|
+
}
|
|
77
|
+
} catch {
|
|
78
|
+
// A replaced lock belongs to another process.
|
|
79
|
+
}
|
|
80
|
+
}
|
|
@@ -0,0 +1,284 @@
|
|
|
1
|
+
import { randomUUID } from 'node:crypto';
|
|
2
|
+
import {
|
|
3
|
+
existsSync,
|
|
4
|
+
lstatSync,
|
|
5
|
+
mkdirSync,
|
|
6
|
+
readFileSync,
|
|
7
|
+
readdirSync,
|
|
8
|
+
realpathSync,
|
|
9
|
+
renameSync,
|
|
10
|
+
rmSync,
|
|
11
|
+
statSync,
|
|
12
|
+
writeFileSync,
|
|
13
|
+
} from 'node:fs';
|
|
14
|
+
import { homedir } from 'node:os';
|
|
15
|
+
import { dirname, join, parse as parsePath, resolve } from 'node:path';
|
|
16
|
+
|
|
17
|
+
import { usageError } from './errors.js';
|
|
18
|
+
import { CONFIG_DIR } from './profiles.js';
|
|
19
|
+
|
|
20
|
+
export const APP_DEV_ROOTS_VERSION = 1;
|
|
21
|
+
export const DEFAULT_APP_DEV_ROOT = join(homedir(), '.notis', 'apps');
|
|
22
|
+
export const DEFAULT_APP_DEV_ROOTS_FILE = join(CONFIG_DIR, 'app-dev-roots.json');
|
|
23
|
+
const CONFIG_FILENAMES = ['notis.config.ts', 'notis.config.js', 'notis.config.mjs'];
|
|
24
|
+
const LOCK_TIMEOUT_MS = 5_000;
|
|
25
|
+
const lockWait = new Int32Array(new SharedArrayBuffer(4));
|
|
26
|
+
|
|
27
|
+
function rootsFile(filePath) {
|
|
28
|
+
if (filePath) return resolve(filePath);
|
|
29
|
+
const envPath = process.env.NOTIS_APP_DEV_ROOTS_FILE;
|
|
30
|
+
return typeof envPath === 'string' && envPath.trim()
|
|
31
|
+
? resolve(envPath.trim())
|
|
32
|
+
: DEFAULT_APP_DEV_ROOTS_FILE;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function legacyProjectsFile(filePath) {
|
|
36
|
+
const sessionsFile = filePath
|
|
37
|
+
|| join(CONFIG_DIR, 'app-dev-sessions.json');
|
|
38
|
+
const parsed = parsePath(resolve(sessionsFile));
|
|
39
|
+
return join(parsed.dir, `${parsed.name}-projects${parsed.ext || '.json'}`);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function readLockOwner(lockPath) {
|
|
43
|
+
try {
|
|
44
|
+
return readFileSync(join(lockPath, 'owner'), 'utf8').trim();
|
|
45
|
+
} catch {
|
|
46
|
+
return null;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function withLock(filePath, callback) {
|
|
51
|
+
mkdirSync(dirname(filePath), { recursive: true, mode: 0o700 });
|
|
52
|
+
const lockPath = `${filePath}.lock`;
|
|
53
|
+
const owner = `${process.pid}.${randomUUID()}`;
|
|
54
|
+
const startedAt = Date.now();
|
|
55
|
+
while (true) {
|
|
56
|
+
try {
|
|
57
|
+
mkdirSync(lockPath, { mode: 0o700 });
|
|
58
|
+
writeFileSync(join(lockPath, 'owner'), owner, { mode: 0o600 });
|
|
59
|
+
break;
|
|
60
|
+
} catch (error) {
|
|
61
|
+
if (error?.code !== 'EEXIST') throw error;
|
|
62
|
+
try {
|
|
63
|
+
const stat = lstatSync(lockPath);
|
|
64
|
+
if (!stat.isDirectory() || stat.isSymbolicLink()) {
|
|
65
|
+
throw new Error(`Refusing unsafe app development roots lock: ${lockPath}`);
|
|
66
|
+
}
|
|
67
|
+
const existingOwner = readLockOwner(lockPath);
|
|
68
|
+
const pid = Number.parseInt(String(existingOwner || '').split('.')[0] || '', 10);
|
|
69
|
+
if (Number.isInteger(pid) && pid > 0) {
|
|
70
|
+
try {
|
|
71
|
+
process.kill(pid, 0);
|
|
72
|
+
} catch (ownerError) {
|
|
73
|
+
if (ownerError?.code === 'ESRCH') {
|
|
74
|
+
rmSync(lockPath, { recursive: true, force: true });
|
|
75
|
+
continue;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
} catch (statError) {
|
|
80
|
+
if (statError?.code === 'ENOENT') continue;
|
|
81
|
+
throw statError;
|
|
82
|
+
}
|
|
83
|
+
if (Date.now() - startedAt >= LOCK_TIMEOUT_MS) {
|
|
84
|
+
throw new Error(`Timed out waiting for app development roots lock: ${filePath}`);
|
|
85
|
+
}
|
|
86
|
+
Atomics.wait(lockWait, 0, 0, 10);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
try {
|
|
90
|
+
return callback();
|
|
91
|
+
} finally {
|
|
92
|
+
if (readLockOwner(lockPath) === owner) {
|
|
93
|
+
rmSync(lockPath, { recursive: true, force: true });
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function canonicalExistingDirectory(inputPath) {
|
|
99
|
+
const absolute = resolve(String(inputPath || ''));
|
|
100
|
+
let stat;
|
|
101
|
+
try {
|
|
102
|
+
stat = statSync(absolute);
|
|
103
|
+
} catch {
|
|
104
|
+
throw usageError(`App development root does not exist: ${absolute}`);
|
|
105
|
+
}
|
|
106
|
+
if (!stat.isDirectory()) {
|
|
107
|
+
throw usageError(`App development root is not a directory: ${absolute}`);
|
|
108
|
+
}
|
|
109
|
+
return realpathSync(absolute);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function normalizeRegistry(raw) {
|
|
113
|
+
const roots = Array.isArray(raw?.roots) ? raw.roots : [];
|
|
114
|
+
const seen = new Set();
|
|
115
|
+
return roots
|
|
116
|
+
.map((entry) => typeof entry === 'string' ? { path: entry } : entry)
|
|
117
|
+
.filter((entry) => entry && typeof entry.path === 'string' && entry.path.trim())
|
|
118
|
+
.map((entry) => ({
|
|
119
|
+
path: resolve(entry.path),
|
|
120
|
+
registeredAt: typeof entry.registeredAt === 'string'
|
|
121
|
+
? entry.registeredAt
|
|
122
|
+
: new Date(0).toISOString(),
|
|
123
|
+
}))
|
|
124
|
+
.filter((entry) => {
|
|
125
|
+
if (seen.has(entry.path)) return false;
|
|
126
|
+
seen.add(entry.path);
|
|
127
|
+
return true;
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function readRaw(filePath) {
|
|
132
|
+
if (!existsSync(filePath)) return { version: APP_DEV_ROOTS_VERSION, roots: [] };
|
|
133
|
+
try {
|
|
134
|
+
return {
|
|
135
|
+
version: APP_DEV_ROOTS_VERSION,
|
|
136
|
+
roots: normalizeRegistry(JSON.parse(readFileSync(filePath, 'utf8'))),
|
|
137
|
+
};
|
|
138
|
+
} catch {
|
|
139
|
+
return { version: APP_DEV_ROOTS_VERSION, roots: [] };
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function writeRaw(registry, filePath) {
|
|
144
|
+
mkdirSync(dirname(filePath), { recursive: true, mode: 0o700 });
|
|
145
|
+
const normalized = {
|
|
146
|
+
version: APP_DEV_ROOTS_VERSION,
|
|
147
|
+
roots: normalizeRegistry(registry),
|
|
148
|
+
};
|
|
149
|
+
const temporary = `${filePath}.${process.pid}.${randomUUID()}.tmp`;
|
|
150
|
+
writeFileSync(temporary, JSON.stringify(normalized, null, 2), { mode: 0o600 });
|
|
151
|
+
renameSync(temporary, filePath);
|
|
152
|
+
return normalized;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function migrateLegacyProjectsUnlocked(registry, options) {
|
|
156
|
+
const path = legacyProjectsFile(options.legacySessionsFilePath);
|
|
157
|
+
if (!existsSync(path)) return registry;
|
|
158
|
+
let projects = [];
|
|
159
|
+
try {
|
|
160
|
+
const raw = JSON.parse(readFileSync(path, 'utf8'));
|
|
161
|
+
projects = Array.isArray(raw?.projects) ? raw.projects : [];
|
|
162
|
+
} catch {
|
|
163
|
+
return registry;
|
|
164
|
+
}
|
|
165
|
+
const byPath = new Map(registry.roots.map((entry) => [entry.path, entry]));
|
|
166
|
+
for (const project of projects) {
|
|
167
|
+
if (typeof project?.projectDir !== 'string' || !project.projectDir.trim()) continue;
|
|
168
|
+
try {
|
|
169
|
+
const path = canonicalExistingDirectory(project.projectDir);
|
|
170
|
+
if (path === realpathIfExists(DEFAULT_APP_DEV_ROOT)) continue;
|
|
171
|
+
if (!byPath.has(path)) {
|
|
172
|
+
byPath.set(path, {
|
|
173
|
+
path,
|
|
174
|
+
registeredAt: typeof project.lastMountedAt === 'string'
|
|
175
|
+
? project.lastMountedAt
|
|
176
|
+
: new Date().toISOString(),
|
|
177
|
+
});
|
|
178
|
+
}
|
|
179
|
+
} catch {
|
|
180
|
+
// Deleted or inaccessible legacy projects are intentionally not migrated.
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
rmSync(path, { force: true });
|
|
184
|
+
return { version: APP_DEV_ROOTS_VERSION, roots: [...byPath.values()] };
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
function realpathIfExists(path) {
|
|
188
|
+
try {
|
|
189
|
+
return realpathSync(path);
|
|
190
|
+
} catch {
|
|
191
|
+
return resolve(path);
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
export function readAppDevRoots(options = {}) {
|
|
196
|
+
const filePath = rootsFile(options.filePath);
|
|
197
|
+
let registry = readRaw(filePath);
|
|
198
|
+
if (options.migrateLegacy !== false) {
|
|
199
|
+
registry = withLock(filePath, () => {
|
|
200
|
+
const before = readRaw(filePath);
|
|
201
|
+
const after = migrateLegacyProjectsUnlocked(before, options);
|
|
202
|
+
const changed = JSON.stringify(before.roots) !== JSON.stringify(after.roots);
|
|
203
|
+
return changed ? writeRaw(after, filePath) : before;
|
|
204
|
+
});
|
|
205
|
+
}
|
|
206
|
+
const implicit = realpathIfExists(options.defaultRoot || DEFAULT_APP_DEV_ROOT);
|
|
207
|
+
const roots = [
|
|
208
|
+
{ path: implicit, registeredAt: null, implicit: true },
|
|
209
|
+
...registry.roots
|
|
210
|
+
.filter((entry) => entry.path !== implicit)
|
|
211
|
+
.map((entry) => ({ ...entry, implicit: false })),
|
|
212
|
+
];
|
|
213
|
+
return { version: APP_DEV_ROOTS_VERSION, roots };
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
export function registerAppDevRoot(inputPath, options = {}) {
|
|
217
|
+
const path = canonicalExistingDirectory(inputPath);
|
|
218
|
+
const implicit = realpathIfExists(options.defaultRoot || DEFAULT_APP_DEV_ROOT);
|
|
219
|
+
if (path === implicit) return readAppDevRoots(options);
|
|
220
|
+
const filePath = rootsFile(options.filePath);
|
|
221
|
+
withLock(filePath, () => {
|
|
222
|
+
const registry = readRaw(filePath);
|
|
223
|
+
if (!registry.roots.some((entry) => entry.path === path)) {
|
|
224
|
+
registry.roots.push({ path, registeredAt: new Date().toISOString() });
|
|
225
|
+
writeRaw(registry, filePath);
|
|
226
|
+
}
|
|
227
|
+
});
|
|
228
|
+
return readAppDevRoots({ ...options, migrateLegacy: false });
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
export function removeAppDevRoot(inputPath, options = {}) {
|
|
232
|
+
const path = realpathIfExists(resolve(String(inputPath || '')));
|
|
233
|
+
const implicit = realpathIfExists(options.defaultRoot || DEFAULT_APP_DEV_ROOT);
|
|
234
|
+
if (path === implicit) {
|
|
235
|
+
throw usageError(`The default app development root cannot be removed: ${implicit}`);
|
|
236
|
+
}
|
|
237
|
+
const filePath = rootsFile(options.filePath);
|
|
238
|
+
let removed = false;
|
|
239
|
+
withLock(filePath, () => {
|
|
240
|
+
const registry = readRaw(filePath);
|
|
241
|
+
const next = registry.roots.filter((entry) => entry.path !== path);
|
|
242
|
+
removed = next.length !== registry.roots.length;
|
|
243
|
+
if (removed) writeRaw({ ...registry, roots: next }, filePath);
|
|
244
|
+
});
|
|
245
|
+
return { removed, path, registry: readAppDevRoots({ ...options, migrateLegacy: false }) };
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
function hasConfig(path) {
|
|
249
|
+
return CONFIG_FILENAMES.some((name) => existsSync(join(path, name)));
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
function childAppDirs(parent) {
|
|
253
|
+
if (!existsSync(parent)) return [];
|
|
254
|
+
let entries = [];
|
|
255
|
+
try {
|
|
256
|
+
entries = readdirSync(parent, { withFileTypes: true });
|
|
257
|
+
} catch {
|
|
258
|
+
return [];
|
|
259
|
+
}
|
|
260
|
+
return entries
|
|
261
|
+
.filter((entry) => entry.isDirectory())
|
|
262
|
+
.filter((entry) => !entry.name.startsWith('.') && !entry.name.startsWith('_'))
|
|
263
|
+
.filter((entry) => entry.name !== 'node_modules')
|
|
264
|
+
.map((entry) => join(parent, entry.name))
|
|
265
|
+
.filter(hasConfig);
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
export function discoverAppProjectsInRoot(inputRoot) {
|
|
269
|
+
const root = realpathIfExists(resolve(inputRoot));
|
|
270
|
+
const candidates = [];
|
|
271
|
+
if (hasConfig(root)) candidates.push(root);
|
|
272
|
+
candidates.push(...childAppDirs(root));
|
|
273
|
+
candidates.push(...childAppDirs(join(root, 'apps')));
|
|
274
|
+
return [...new Set(candidates.map(realpathIfExists))].sort();
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
export function discoverRegisteredAppProjects(options = {}) {
|
|
278
|
+
const roots = readAppDevRoots(options).roots;
|
|
279
|
+
return [...new Set(roots.flatMap((entry) => discoverAppProjectsInRoot(entry.path)))].sort();
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
export function getAppDevRootsFile(filePath) {
|
|
283
|
+
return rootsFile(filePath);
|
|
284
|
+
}
|