@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
package/src/runtime/output.js
CHANGED
|
@@ -22,6 +22,13 @@ function formatTable(rows, columns) {
|
|
|
22
22
|
return [header, divider, ...body].join('\n');
|
|
23
23
|
}
|
|
24
24
|
|
|
25
|
+
function formatHint(hint) {
|
|
26
|
+
if (typeof hint?.message === 'string' && hint.message) {
|
|
27
|
+
return ` ${hint.message}`;
|
|
28
|
+
}
|
|
29
|
+
return ` ${hint?.command || ''} ${hint?.reason || ''}`.trimEnd();
|
|
30
|
+
}
|
|
31
|
+
|
|
25
32
|
function yamlScalar(value) {
|
|
26
33
|
if (value === null || value === undefined) {
|
|
27
34
|
return 'null';
|
|
@@ -94,6 +101,13 @@ export class OutputManager {
|
|
|
94
101
|
process.stderr.write(`${message}\n`);
|
|
95
102
|
}
|
|
96
103
|
|
|
104
|
+
note(message) {
|
|
105
|
+
if (this.isMachineMode()) {
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
process.stderr.write(`${message}\n`);
|
|
109
|
+
}
|
|
110
|
+
|
|
97
111
|
emitProgress({ phase, message, requestId = null }) {
|
|
98
112
|
if (this.runtime.quiet) {
|
|
99
113
|
return;
|
|
@@ -153,7 +167,7 @@ export class OutputManager {
|
|
|
153
167
|
}
|
|
154
168
|
|
|
155
169
|
if (hints.length) {
|
|
156
|
-
const lines = hints.map(
|
|
170
|
+
const lines = hints.map(formatHint);
|
|
157
171
|
process.stdout.write(`\nNext:\n${lines.join('\n')}\n`);
|
|
158
172
|
}
|
|
159
173
|
|
|
@@ -182,7 +196,7 @@ export class OutputManager {
|
|
|
182
196
|
|
|
183
197
|
process.stderr.write(`Error: ${error.message}\n`);
|
|
184
198
|
for (const hint of error.hints || []) {
|
|
185
|
-
process.stderr.write(
|
|
199
|
+
process.stderr.write(`${formatHint(hint)}\n`);
|
|
186
200
|
}
|
|
187
201
|
return error.exitCode || EXIT_CODES.unexpected;
|
|
188
202
|
}
|
package/src/runtime/profiles.js
CHANGED
|
@@ -1,6 +1,15 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { randomUUID } from 'node:crypto';
|
|
2
|
+
import {
|
|
3
|
+
existsSync,
|
|
4
|
+
mkdirSync,
|
|
5
|
+
readFileSync,
|
|
6
|
+
renameSync,
|
|
7
|
+
rmSync,
|
|
8
|
+
statSync,
|
|
9
|
+
writeFileSync,
|
|
10
|
+
} from 'node:fs';
|
|
2
11
|
import { homedir } from 'node:os';
|
|
3
|
-
import { join } from 'node:path';
|
|
12
|
+
import { dirname, join, parse, resolve } from 'node:path';
|
|
4
13
|
import { CliError, EXIT_CODES } from './errors.js';
|
|
5
14
|
import { getDesktopAuthRecovery } from './desktop-auth.js';
|
|
6
15
|
|
|
@@ -9,11 +18,22 @@ export const CONFIG_FILE = join(CONFIG_DIR, 'config.json');
|
|
|
9
18
|
export const WORKSPACE_DIR = join(CONFIG_DIR, 'workspace');
|
|
10
19
|
export const DEFAULT_API_BASE = 'https://api.notis.ai';
|
|
11
20
|
export const DEFAULT_PROFILE = 'default';
|
|
21
|
+
const WORKTREE_RUNTIME_FILENAME = join('.context', 'notis-runtime.json');
|
|
22
|
+
const WORKTREE_ROUTING_FILENAME = join('.context', 'notis-routing.json');
|
|
12
23
|
const LOCAL_DEFAULT_API_BASES = new Set([
|
|
13
24
|
'http://localhost:3001',
|
|
14
25
|
'http://127.0.0.1:3001',
|
|
15
26
|
]);
|
|
16
27
|
const LOCAL_API_HOSTS = new Set(['localhost', '127.0.0.1', '::1']);
|
|
28
|
+
// Cross-process write lock over ~/.notis/config.json. Notis Desktop implements
|
|
29
|
+
// the same protocol independently in electron/src/cli-auth.ts (updateConfig);
|
|
30
|
+
// the `${configFile}.write-lock` directory name and these three timings must
|
|
31
|
+
// stay identical on both sides or `notis login` and desktop syncAuth stop
|
|
32
|
+
// excluding each other and clobber stored OAuth tokens. Guarded by the drift
|
|
33
|
+
// test in packages/cli/test/runtime-auth.test.js.
|
|
34
|
+
const CONFIG_WRITE_LOCK_TIMEOUT_MS = 5_000;
|
|
35
|
+
const CONFIG_WRITE_LOCK_STALE_MS = 2_000;
|
|
36
|
+
const CONFIG_WRITE_LOCK_POLL_MS = 10;
|
|
17
37
|
|
|
18
38
|
function clone(value) {
|
|
19
39
|
return JSON.parse(JSON.stringify(value));
|
|
@@ -41,6 +61,28 @@ export function normalizeConfig(rawConfig = {}) {
|
|
|
41
61
|
desktop_app_name:
|
|
42
62
|
typeof profile.desktop_app_name === 'string' ? profile.desktop_app_name : undefined,
|
|
43
63
|
desktop_pid: typeof profile.desktop_pid === 'number' ? profile.desktop_pid : undefined,
|
|
64
|
+
oauth_access_token:
|
|
65
|
+
typeof profile.oauth_access_token === 'string' ? profile.oauth_access_token : undefined,
|
|
66
|
+
oauth_refresh_token:
|
|
67
|
+
typeof profile.oauth_refresh_token === 'string' ? profile.oauth_refresh_token : undefined,
|
|
68
|
+
oauth_access_expires_at:
|
|
69
|
+
typeof profile.oauth_access_expires_at === 'number' ? profile.oauth_access_expires_at : undefined,
|
|
70
|
+
oauth_refresh_expires_at:
|
|
71
|
+
typeof profile.oauth_refresh_expires_at === 'number' ? profile.oauth_refresh_expires_at : undefined,
|
|
72
|
+
oauth_client_id:
|
|
73
|
+
typeof profile.oauth_client_id === 'string' ? profile.oauth_client_id : undefined,
|
|
74
|
+
oauth_issuer:
|
|
75
|
+
typeof profile.oauth_issuer === 'string' ? profile.oauth_issuer : undefined,
|
|
76
|
+
oauth_api_base:
|
|
77
|
+
typeof profile.oauth_api_base === 'string' ? profile.oauth_api_base : undefined,
|
|
78
|
+
oauth_resource:
|
|
79
|
+
typeof profile.oauth_resource === 'string' ? profile.oauth_resource : undefined,
|
|
80
|
+
oauth_scopes:
|
|
81
|
+
Array.isArray(profile.oauth_scopes)
|
|
82
|
+
? profile.oauth_scopes.filter((scope) => typeof scope === 'string')
|
|
83
|
+
: undefined,
|
|
84
|
+
oauth_user_id:
|
|
85
|
+
typeof profile.oauth_user_id === 'string' ? profile.oauth_user_id : undefined,
|
|
44
86
|
};
|
|
45
87
|
}
|
|
46
88
|
|
|
@@ -72,22 +114,282 @@ export function normalizeConfig(rawConfig = {}) {
|
|
|
72
114
|
desktop_app_name:
|
|
73
115
|
typeof raw.desktop_app_name === 'string' ? raw.desktop_app_name : undefined,
|
|
74
116
|
desktop_pid: typeof raw.desktop_pid === 'number' ? raw.desktop_pid : undefined,
|
|
117
|
+
oauth_access_token:
|
|
118
|
+
typeof raw.oauth_access_token === 'string' ? raw.oauth_access_token : undefined,
|
|
119
|
+
oauth_refresh_token:
|
|
120
|
+
typeof raw.oauth_refresh_token === 'string' ? raw.oauth_refresh_token : undefined,
|
|
121
|
+
oauth_access_expires_at:
|
|
122
|
+
typeof raw.oauth_access_expires_at === 'number' ? raw.oauth_access_expires_at : undefined,
|
|
123
|
+
oauth_refresh_expires_at:
|
|
124
|
+
typeof raw.oauth_refresh_expires_at === 'number' ? raw.oauth_refresh_expires_at : undefined,
|
|
125
|
+
oauth_client_id:
|
|
126
|
+
typeof raw.oauth_client_id === 'string' ? raw.oauth_client_id : undefined,
|
|
127
|
+
oauth_issuer:
|
|
128
|
+
typeof raw.oauth_issuer === 'string' ? raw.oauth_issuer : undefined,
|
|
129
|
+
oauth_api_base:
|
|
130
|
+
typeof raw.oauth_api_base === 'string' ? raw.oauth_api_base : undefined,
|
|
131
|
+
oauth_resource:
|
|
132
|
+
typeof raw.oauth_resource === 'string' ? raw.oauth_resource : undefined,
|
|
133
|
+
oauth_scopes:
|
|
134
|
+
Array.isArray(raw.oauth_scopes)
|
|
135
|
+
? raw.oauth_scopes.filter((scope) => typeof scope === 'string')
|
|
136
|
+
: undefined,
|
|
137
|
+
oauth_user_id:
|
|
138
|
+
typeof raw.oauth_user_id === 'string' ? raw.oauth_user_id : undefined,
|
|
75
139
|
},
|
|
76
140
|
},
|
|
77
141
|
};
|
|
78
142
|
}
|
|
79
143
|
|
|
80
|
-
|
|
81
|
-
|
|
144
|
+
function readJsonFile(path) {
|
|
145
|
+
try {
|
|
146
|
+
return JSON.parse(readFileSync(path, 'utf-8'));
|
|
147
|
+
} catch {
|
|
148
|
+
return null;
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function findUp(filename, startDir = process.cwd()) {
|
|
153
|
+
let current = resolve(startDir);
|
|
154
|
+
const root = parse(current).root;
|
|
155
|
+
while (true) {
|
|
156
|
+
const candidate = join(current, filename);
|
|
157
|
+
if (existsSync(candidate)) {
|
|
158
|
+
return candidate;
|
|
159
|
+
}
|
|
160
|
+
if (current === root) {
|
|
161
|
+
return null;
|
|
162
|
+
}
|
|
163
|
+
current = dirname(current);
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function processIsAlive(pid) {
|
|
168
|
+
if (!Number.isInteger(pid) || pid <= 0) {
|
|
169
|
+
return false;
|
|
170
|
+
}
|
|
171
|
+
try {
|
|
172
|
+
process.kill(pid, 0);
|
|
173
|
+
return true;
|
|
174
|
+
} catch {
|
|
175
|
+
return false;
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
export function resolveWorktreeRuntime(startDir = process.cwd()) {
|
|
180
|
+
if (
|
|
181
|
+
process.env.NODE_ENV === 'test' &&
|
|
182
|
+
process.env.NOTIS_TEST_DISABLE_WORKTREE_ROUTING === '1'
|
|
183
|
+
) {
|
|
184
|
+
return null;
|
|
185
|
+
}
|
|
186
|
+
const runtimePath = findUp(WORKTREE_RUNTIME_FILENAME, startDir);
|
|
187
|
+
const routingPath = findUp(WORKTREE_ROUTING_FILENAME, startDir);
|
|
188
|
+
const routing = routingPath ? readJsonFile(routingPath) : null;
|
|
189
|
+
|
|
190
|
+
if (!runtimePath) {
|
|
191
|
+
if (routing?.mode === 'local-only') {
|
|
192
|
+
throw new CliError({
|
|
193
|
+
code: 'dev_runtime_unavailable',
|
|
194
|
+
message: `This worktree is local-only, but its dev.sh runtime is not active`,
|
|
195
|
+
exitCode: EXIT_CODES.network,
|
|
196
|
+
hints: [
|
|
197
|
+
{ message: 'Start ./dev.sh in this worktree, then retry the command.' },
|
|
198
|
+
{ message: `Routing policy: ${routingPath}` },
|
|
199
|
+
],
|
|
200
|
+
});
|
|
201
|
+
}
|
|
202
|
+
return null;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
const runtime = readJsonFile(runtimePath);
|
|
206
|
+
const apiBase = typeof runtime?.api_base === 'string' ? runtime.api_base.replace(/\/+$/, '') : '';
|
|
207
|
+
const configFile = typeof runtime?.config_file === 'string' ? runtime.config_file : '';
|
|
208
|
+
const appDevSessionsFile =
|
|
209
|
+
typeof runtime?.app_dev_sessions_file === 'string' && runtime.app_dev_sessions_file.trim()
|
|
210
|
+
? runtime.app_dev_sessions_file.trim()
|
|
211
|
+
: join(dirname(runtimePath), 'app-dev-sessions.json');
|
|
212
|
+
const desktopDeepLinkScheme =
|
|
213
|
+
typeof runtime?.desktop_deep_link_scheme === 'string'
|
|
214
|
+
? runtime.desktop_deep_link_scheme.trim()
|
|
215
|
+
: '';
|
|
216
|
+
const pid = Number(runtime?.dev_pid);
|
|
217
|
+
if (
|
|
218
|
+
runtime?.mode !== 'local-only' ||
|
|
219
|
+
!isLocalApiBase(apiBase) ||
|
|
220
|
+
!configFile ||
|
|
221
|
+
!processIsAlive(pid)
|
|
222
|
+
) {
|
|
223
|
+
throw new CliError({
|
|
224
|
+
code: 'dev_runtime_unavailable',
|
|
225
|
+
message: 'The local-only worktree runtime is stale or invalid',
|
|
226
|
+
exitCode: EXIT_CODES.network,
|
|
227
|
+
hints: [
|
|
228
|
+
{ message: 'Restart ./dev.sh in this worktree, then retry the command.' },
|
|
229
|
+
{ message: `Runtime lease: ${runtimePath}` },
|
|
230
|
+
],
|
|
231
|
+
});
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
return {
|
|
235
|
+
...runtime,
|
|
236
|
+
api_base: apiBase,
|
|
237
|
+
config_file: resolve(dirname(runtimePath), configFile),
|
|
238
|
+
app_dev_sessions_file: resolve(dirname(runtimePath), appDevSessionsFile),
|
|
239
|
+
desktop_deep_link_scheme: desktopDeepLinkScheme || undefined,
|
|
240
|
+
runtime_path: runtimePath,
|
|
241
|
+
routing_path: routingPath,
|
|
242
|
+
};
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
export function resolveConfigFile(runtime = null) {
|
|
246
|
+
if (runtime?.config_file) {
|
|
247
|
+
return runtime.config_file;
|
|
248
|
+
}
|
|
249
|
+
const envConfigFile = process.env.NOTIS_CLI_CONFIG_FILE;
|
|
250
|
+
if (envConfigFile) {
|
|
251
|
+
return resolve(envConfigFile);
|
|
252
|
+
}
|
|
253
|
+
return CONFIG_FILE;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
export function loadConfig(runtime = null) {
|
|
257
|
+
const configFile = resolveConfigFile(runtime);
|
|
258
|
+
if (!existsSync(configFile)) {
|
|
259
|
+
return normalizeConfig({});
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
// The desktop app rewrites this file while the CLI may be reading it, so a
|
|
263
|
+
// torn or corrupt read is expected rather than exceptional. Degrade to
|
|
264
|
+
// "unauthenticated", which surfaces the actionable auth_missing error instead
|
|
265
|
+
// of a raw SyntaxError from deep inside the runtime.
|
|
266
|
+
try {
|
|
267
|
+
return normalizeConfig(JSON.parse(readFileSync(configFile, 'utf-8')));
|
|
268
|
+
} catch {
|
|
82
269
|
return normalizeConfig({});
|
|
83
270
|
}
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
function writeConfig(configFile, config) {
|
|
274
|
+
mkdirSync(dirname(configFile), { recursive: true });
|
|
275
|
+
const temporaryFile = `${configFile}.${process.pid}.${Date.now()}.tmp`;
|
|
276
|
+
writeFileSync(temporaryFile, JSON.stringify(normalizeConfig(config), null, 2), { mode: 0o600 });
|
|
277
|
+
renameSync(temporaryFile, configFile);
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
// The lock records who holds it and when they took it. Reclaiming a stale lock
|
|
281
|
+
// by mtime alone is not safe: the reclaimer cannot tell an abandoned lock from
|
|
282
|
+
// one it is about to steal from a live writer, and the victim's own release
|
|
283
|
+
// would then delete the thief's lock and admit a third writer. Both sides are
|
|
284
|
+
// on the same host, so the stored timestamp and Date.now() share a clock.
|
|
285
|
+
function readLockOwner(lockDirectory) {
|
|
286
|
+
try {
|
|
287
|
+
return JSON.parse(readFileSync(join(lockDirectory, 'owner'), 'utf-8'));
|
|
288
|
+
} catch {
|
|
289
|
+
return null;
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
function lockDirectoryMtime(lockDirectory) {
|
|
294
|
+
try {
|
|
295
|
+
return statSync(lockDirectory).mtimeMs;
|
|
296
|
+
} catch {
|
|
297
|
+
return null;
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
function publishConfigWriteLock(lockDirectory, ownerId) {
|
|
302
|
+
const candidateDirectory = `${lockDirectory}.candidate-${ownerId}`;
|
|
303
|
+
try {
|
|
304
|
+
mkdirSync(candidateDirectory);
|
|
305
|
+
writeFileSync(
|
|
306
|
+
join(candidateDirectory, 'owner'),
|
|
307
|
+
JSON.stringify({ id: ownerId, at: Date.now() }),
|
|
308
|
+
{ mode: 0o600 },
|
|
309
|
+
);
|
|
310
|
+
try {
|
|
311
|
+
renameSync(candidateDirectory, lockDirectory);
|
|
312
|
+
return true;
|
|
313
|
+
} catch (error) {
|
|
314
|
+
if (!existsSync(lockDirectory)) throw error;
|
|
315
|
+
return false;
|
|
316
|
+
}
|
|
317
|
+
} finally {
|
|
318
|
+
rmSync(candidateDirectory, { recursive: true, force: true });
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
function withConfigWriteLock(runtime, callback) {
|
|
323
|
+
const configFile = resolveConfigFile(runtime);
|
|
324
|
+
const lockDirectory = `${configFile}.write-lock`;
|
|
325
|
+
const ownerId = `${process.pid}.${randomUUID()}`;
|
|
326
|
+
const deadline = Date.now() + CONFIG_WRITE_LOCK_TIMEOUT_MS;
|
|
327
|
+
mkdirSync(dirname(configFile), { recursive: true });
|
|
328
|
+
for (;;) {
|
|
329
|
+
if (!existsSync(lockDirectory) && publishConfigWriteLock(lockDirectory, ownerId)) {
|
|
330
|
+
break;
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
const owner = readLockOwner(lockDirectory);
|
|
334
|
+
const observedMtime = lockDirectoryMtime(lockDirectory);
|
|
335
|
+
const ownerIsStale =
|
|
336
|
+
owner?.id && Date.now() - Number(owner.at) > CONFIG_WRITE_LOCK_STALE_MS;
|
|
337
|
+
const ownerlessIsStale =
|
|
338
|
+
!owner
|
|
339
|
+
&& observedMtime !== null
|
|
340
|
+
&& Date.now() - observedMtime > CONFIG_WRITE_LOCK_STALE_MS;
|
|
341
|
+
if (ownerIsStale || ownerlessIsStale) {
|
|
342
|
+
try {
|
|
343
|
+
const currentOwner = readLockOwner(lockDirectory);
|
|
344
|
+
const ownerUnchanged = owner?.id
|
|
345
|
+
? currentOwner?.id === owner.id
|
|
346
|
+
: !currentOwner && lockDirectoryMtime(lockDirectory) === observedMtime;
|
|
347
|
+
if (ownerUnchanged) {
|
|
348
|
+
rmSync(lockDirectory, { recursive: true, force: true });
|
|
349
|
+
continue;
|
|
350
|
+
}
|
|
351
|
+
} catch {
|
|
352
|
+
// Losing the reclaim race is normal; fall through and keep waiting.
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
if (Date.now() >= deadline) {
|
|
356
|
+
throw new Error(`Timed out waiting to update ${configFile}`);
|
|
357
|
+
}
|
|
358
|
+
Atomics.wait(
|
|
359
|
+
new Int32Array(new SharedArrayBuffer(4)),
|
|
360
|
+
0,
|
|
361
|
+
0,
|
|
362
|
+
CONFIG_WRITE_LOCK_POLL_MS,
|
|
363
|
+
);
|
|
364
|
+
}
|
|
365
|
+
try {
|
|
366
|
+
return callback(configFile);
|
|
367
|
+
} finally {
|
|
368
|
+
try {
|
|
369
|
+
// Never release a lock that was reclaimed from us while we worked.
|
|
370
|
+
if (readLockOwner(lockDirectory)?.id === ownerId) {
|
|
371
|
+
rmSync(lockDirectory, { recursive: true, force: true });
|
|
372
|
+
}
|
|
373
|
+
} catch {
|
|
374
|
+
// The lock may already have been removed after an interrupted write.
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
}
|
|
84
378
|
|
|
85
|
-
|
|
379
|
+
export function saveConfig(config, runtime = null) {
|
|
380
|
+
return withConfigWriteLock(runtime, (configFile) => {
|
|
381
|
+
writeConfig(configFile, config);
|
|
382
|
+
});
|
|
86
383
|
}
|
|
87
384
|
|
|
88
|
-
export function
|
|
89
|
-
|
|
90
|
-
|
|
385
|
+
export function updateConfig(updater, runtime = null) {
|
|
386
|
+
return withConfigWriteLock(runtime, (configFile) => {
|
|
387
|
+
const current = loadConfig(runtime);
|
|
388
|
+
const updated = updater(normalizeConfig(current));
|
|
389
|
+
const next = normalizeConfig(updated ?? current);
|
|
390
|
+
writeConfig(configFile, next);
|
|
391
|
+
return next;
|
|
392
|
+
});
|
|
91
393
|
}
|
|
92
394
|
|
|
93
395
|
export function getProfile(config, profileName) {
|
|
@@ -221,15 +523,132 @@ export function parseDebugEntitlementOverride(value = process.env.NOTIS_DEBUG_EN
|
|
|
221
523
|
});
|
|
222
524
|
}
|
|
223
525
|
|
|
526
|
+
export function getOAuthResource(profile = {}) {
|
|
527
|
+
if (typeof profile.oauth_resource === 'string' && profile.oauth_resource) {
|
|
528
|
+
return profile.oauth_resource.replace(/\/+$/, '');
|
|
529
|
+
}
|
|
530
|
+
if (typeof profile.oauth_access_token === 'string' && profile.oauth_access_token) {
|
|
531
|
+
try {
|
|
532
|
+
const parts = profile.oauth_access_token.split('.');
|
|
533
|
+
if (parts.length === 3) {
|
|
534
|
+
const payload = JSON.parse(Buffer.from(parts[1], 'base64url').toString());
|
|
535
|
+
if (typeof payload.aud === 'string' && payload.aud) {
|
|
536
|
+
return payload.aud.replace(/\/+$/, '');
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
} catch {
|
|
540
|
+
// Legacy tokens without a readable audience fall back below.
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
return null;
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
export function getOAuthApiBase(profile = {}) {
|
|
547
|
+
if (typeof profile.oauth_api_base === 'string' && profile.oauth_api_base) {
|
|
548
|
+
return profile.oauth_api_base.replace(/\/+$/, '');
|
|
549
|
+
}
|
|
550
|
+
const resource = getOAuthResource(profile);
|
|
551
|
+
if (resource?.endsWith('/cli')) {
|
|
552
|
+
return resource.slice(0, -'/cli'.length);
|
|
553
|
+
}
|
|
554
|
+
return null;
|
|
555
|
+
}
|
|
556
|
+
|
|
224
557
|
export function resolveRuntimeProfile(
|
|
225
558
|
globalOptions = {},
|
|
226
559
|
{ requireAuth = true, includeDebugEntitlementOverride = true } = {},
|
|
227
560
|
) {
|
|
228
|
-
const
|
|
561
|
+
const worktreeRuntime = resolveWorktreeRuntime();
|
|
562
|
+
const config = loadConfig(worktreeRuntime);
|
|
229
563
|
const profileName = getCurrentProfileName(config, globalOptions.profile);
|
|
230
|
-
const
|
|
231
|
-
|
|
564
|
+
const requestedApiBase = globalOptions.apiBase;
|
|
565
|
+
if (
|
|
566
|
+
worktreeRuntime &&
|
|
567
|
+
requestedApiBase &&
|
|
568
|
+
requestedApiBase.replace(/\/+$/, '') !== worktreeRuntime.api_base
|
|
569
|
+
) {
|
|
570
|
+
throw new CliError({
|
|
571
|
+
code: 'dev_runtime_route_mismatch',
|
|
572
|
+
message: `This local-only worktree cannot route to ${requestedApiBase}`,
|
|
573
|
+
exitCode: EXIT_CODES.usage,
|
|
574
|
+
hints: [{ message: `Expected local API: ${worktreeRuntime.api_base}` }],
|
|
575
|
+
});
|
|
576
|
+
}
|
|
577
|
+
let apiBase = worktreeRuntime
|
|
578
|
+
? worktreeRuntime.api_base
|
|
579
|
+
: getApiBase(config, profileName, globalOptions.apiBase);
|
|
232
580
|
const profile = getProfile(config, profileName);
|
|
581
|
+
const envJwt = !worktreeRuntime ? process.env.NOTIS_JWT : undefined;
|
|
582
|
+
const desktopJwt = profile.jwt;
|
|
583
|
+
const oauthJwt = profile.oauth_access_token;
|
|
584
|
+
let jwt;
|
|
585
|
+
let credentialKind;
|
|
586
|
+
|
|
587
|
+
if (worktreeRuntime && desktopJwt) {
|
|
588
|
+
jwt = desktopJwt;
|
|
589
|
+
credentialKind = 'worktree';
|
|
590
|
+
} else if (envJwt) {
|
|
591
|
+
jwt = envJwt;
|
|
592
|
+
credentialKind = 'env';
|
|
593
|
+
} else if (
|
|
594
|
+
desktopJwt
|
|
595
|
+
&& !credentialIsExpired({ credentialKind: 'desktop', jwt: desktopJwt }, profile)
|
|
596
|
+
) {
|
|
597
|
+
jwt = desktopJwt;
|
|
598
|
+
credentialKind = 'desktop';
|
|
599
|
+
} else if (
|
|
600
|
+
oauthJwt
|
|
601
|
+
&& !credentialIsExpired({ credentialKind: 'oauth', jwt: oauthJwt }, profile)
|
|
602
|
+
) {
|
|
603
|
+
jwt = oauthJwt;
|
|
604
|
+
credentialKind = 'oauth';
|
|
605
|
+
} else if (oauthJwt && profile.oauth_refresh_token) {
|
|
606
|
+
// A lapsed OAuth access token remains usable through its rotating refresh
|
|
607
|
+
// token and must outrank an abandoned, expired Desktop credential.
|
|
608
|
+
jwt = oauthJwt;
|
|
609
|
+
credentialKind = 'oauth';
|
|
610
|
+
} else if (desktopJwt) {
|
|
611
|
+
jwt = desktopJwt;
|
|
612
|
+
credentialKind = 'desktop';
|
|
613
|
+
} else if (oauthJwt) {
|
|
614
|
+
// Preserve the OAuth credential so transport can refresh it before use.
|
|
615
|
+
jwt = oauthJwt;
|
|
616
|
+
credentialKind = 'oauth';
|
|
617
|
+
}
|
|
618
|
+
const oauthApiBase = getOAuthApiBase(profile);
|
|
619
|
+
const oauthResource = getOAuthResource(profile);
|
|
620
|
+
const normalizedRequestedApiBase = requestedApiBase
|
|
621
|
+
? requestedApiBase.replace(/\/+$/, '')
|
|
622
|
+
: null;
|
|
623
|
+
if (
|
|
624
|
+
requireAuth
|
|
625
|
+
&& credentialKind === 'oauth'
|
|
626
|
+
&& normalizedRequestedApiBase
|
|
627
|
+
&& oauthApiBase
|
|
628
|
+
&& normalizedRequestedApiBase !== oauthApiBase
|
|
629
|
+
) {
|
|
630
|
+
const quoteShellArgument = (value) => `'${String(value).replace(/'/g, `'"'"'`)}'`;
|
|
631
|
+
throw new CliError({
|
|
632
|
+
code: 'oauth_api_target_mismatch',
|
|
633
|
+
message: (
|
|
634
|
+
`The OAuth credential for profile ${profileName} belongs to ${oauthApiBase}, `
|
|
635
|
+
+ `not ${normalizedRequestedApiBase}`
|
|
636
|
+
),
|
|
637
|
+
exitCode: EXIT_CODES.auth,
|
|
638
|
+
hints: [{
|
|
639
|
+
command: [
|
|
640
|
+
'npx --package @notis_ai/cli@latest -- notis',
|
|
641
|
+
`--profile ${quoteShellArgument(profileName)}`,
|
|
642
|
+
`--api-base ${quoteShellArgument(normalizedRequestedApiBase)}`,
|
|
643
|
+
'login --force',
|
|
644
|
+
].join(' '),
|
|
645
|
+
reason: 'Authorize a separate OAuth grant for the requested Notis environment',
|
|
646
|
+
}],
|
|
647
|
+
});
|
|
648
|
+
}
|
|
649
|
+
if (credentialKind === 'oauth' && oauthApiBase && !requestedApiBase) {
|
|
650
|
+
apiBase = oauthApiBase;
|
|
651
|
+
}
|
|
233
652
|
const agentMode = isAgentMode(globalOptions);
|
|
234
653
|
const nonInteractive = isNonInteractive(globalOptions);
|
|
235
654
|
const outputMode = resolveOutputMode(globalOptions);
|
|
@@ -239,11 +658,14 @@ export function resolveRuntimeProfile(
|
|
|
239
658
|
: null;
|
|
240
659
|
|
|
241
660
|
if (requireAuth && !jwt) {
|
|
242
|
-
const recovery = getDesktopAuthRecovery(
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
661
|
+
const recovery = getDesktopAuthRecovery(
|
|
662
|
+
{
|
|
663
|
+
apiBase,
|
|
664
|
+
desktopAppName: profile.desktop_app_name,
|
|
665
|
+
desktopPid: profile.desktop_pid,
|
|
666
|
+
},
|
|
667
|
+
{ mode: 'missing' },
|
|
668
|
+
);
|
|
247
669
|
throw new CliError({
|
|
248
670
|
code: 'auth_missing',
|
|
249
671
|
message: `No JWT configured for profile ${profileName}`,
|
|
@@ -251,16 +673,46 @@ export function resolveRuntimeProfile(
|
|
|
251
673
|
hints: recovery.hints,
|
|
252
674
|
});
|
|
253
675
|
}
|
|
676
|
+
if (
|
|
677
|
+
worktreeRuntime?.expected_user_id &&
|
|
678
|
+
(
|
|
679
|
+
credentialKind === 'oauth'
|
|
680
|
+
? profile.oauth_user_id
|
|
681
|
+
: getJwtSubject(jwt)
|
|
682
|
+
) !== worktreeRuntime.expected_user_id
|
|
683
|
+
) {
|
|
684
|
+
throw new CliError({
|
|
685
|
+
code: 'dev_runtime_identity_mismatch',
|
|
686
|
+
message: 'The scoped dev credential does not belong to this worktree test user',
|
|
687
|
+
exitCode: EXIT_CODES.auth,
|
|
688
|
+
hints: [
|
|
689
|
+
{ message: 'Restart ./dev.sh to restore the approved worktree identity.' },
|
|
690
|
+
{ message: `Expected user: ${worktreeRuntime.expected_user_id}` },
|
|
691
|
+
],
|
|
692
|
+
});
|
|
693
|
+
}
|
|
254
694
|
|
|
255
695
|
// An explicit NOTIS_JWT is a complete credential override. Use it verbatim
|
|
256
696
|
// and never replace it with a token later synced by the desktop profile.
|
|
257
|
-
const usingEnvJwt =
|
|
697
|
+
const usingEnvJwt = credentialKind === 'env';
|
|
258
698
|
return {
|
|
259
699
|
config,
|
|
260
700
|
profileName,
|
|
261
701
|
apiBase,
|
|
702
|
+
requestedApiBase: normalizedRequestedApiBase,
|
|
262
703
|
jwt,
|
|
263
|
-
|
|
704
|
+
credentialKind,
|
|
705
|
+
credentialSource: credentialKind === 'desktop' ? 'profile' : credentialKind,
|
|
706
|
+
oauthAccessToken: profile.oauth_access_token,
|
|
707
|
+
oauthRefreshToken: profile.oauth_refresh_token,
|
|
708
|
+
oauthAccessExpiresAt: profile.oauth_access_expires_at,
|
|
709
|
+
oauthRefreshExpiresAt: profile.oauth_refresh_expires_at,
|
|
710
|
+
oauthClientId: profile.oauth_client_id,
|
|
711
|
+
oauthIssuer: profile.oauth_issuer,
|
|
712
|
+
oauthApiBase,
|
|
713
|
+
oauthResource,
|
|
714
|
+
oauthScopes: profile.oauth_scopes || [],
|
|
715
|
+
oauthUserId: profile.oauth_user_id,
|
|
264
716
|
desktopAppName: usingEnvJwt ? undefined : profile.desktop_app_name,
|
|
265
717
|
desktopPid: usingEnvJwt ? undefined : profile.desktop_pid,
|
|
266
718
|
agentMode,
|
|
@@ -268,6 +720,7 @@ export function resolveRuntimeProfile(
|
|
|
268
720
|
outputMode,
|
|
269
721
|
timeoutMs,
|
|
270
722
|
debugEntitlementOverride,
|
|
723
|
+
worktreeRuntime,
|
|
271
724
|
};
|
|
272
725
|
}
|
|
273
726
|
|
|
@@ -285,11 +738,82 @@ export function getJwtExpiration(jwt) {
|
|
|
285
738
|
}
|
|
286
739
|
}
|
|
287
740
|
|
|
741
|
+
export function getJwtSubject(jwt) {
|
|
742
|
+
if (typeof jwt !== 'string' || !jwt) {
|
|
743
|
+
return null;
|
|
744
|
+
}
|
|
745
|
+
try {
|
|
746
|
+
const parts = jwt.split('.');
|
|
747
|
+
if (parts.length !== 3) return null;
|
|
748
|
+
const payload = JSON.parse(Buffer.from(parts[1], 'base64url').toString());
|
|
749
|
+
return typeof payload.sub === 'string' && payload.sub ? payload.sub : null;
|
|
750
|
+
} catch {
|
|
751
|
+
return null;
|
|
752
|
+
}
|
|
753
|
+
}
|
|
754
|
+
|
|
755
|
+
export function getJwtCanonicalUserId(jwt) {
|
|
756
|
+
if (typeof jwt !== 'string' || !jwt) {
|
|
757
|
+
return null;
|
|
758
|
+
}
|
|
759
|
+
try {
|
|
760
|
+
const parts = jwt.split('.');
|
|
761
|
+
if (parts.length !== 3) return null;
|
|
762
|
+
const payload = JSON.parse(Buffer.from(parts[1], 'base64url').toString());
|
|
763
|
+
const candidate = payload?.app_metadata?.app_user_id
|
|
764
|
+
|| payload?.user_metadata?.notis_user_id
|
|
765
|
+
|| payload?.notis_user_id;
|
|
766
|
+
return typeof candidate === 'string' && candidate ? candidate : null;
|
|
767
|
+
} catch {
|
|
768
|
+
return null;
|
|
769
|
+
}
|
|
770
|
+
}
|
|
771
|
+
|
|
288
772
|
export function isJwtExpired(jwt, nowSeconds = Math.floor(Date.now() / 1000)) {
|
|
289
773
|
const expiration = getJwtExpiration(jwt);
|
|
290
774
|
return expiration !== null && expiration <= nowSeconds;
|
|
291
775
|
}
|
|
292
776
|
|
|
777
|
+
export function credentialIsExpired(
|
|
778
|
+
runtime,
|
|
779
|
+
profile = {},
|
|
780
|
+
nowSeconds = Math.floor(Date.now() / 1000),
|
|
781
|
+
) {
|
|
782
|
+
// Older callers and focused transport tests predate `credentialKind`.
|
|
783
|
+
// Infer only the legacy desktop/env shapes; OAuth must always opt in
|
|
784
|
+
// explicitly so a scoped token can never be mistaken for a Supabase JWT.
|
|
785
|
+
const credentialKind = runtime?.credentialKind
|
|
786
|
+
|| (runtime?.credentialSource === 'env' ? 'env' : 'desktop');
|
|
787
|
+
switch (credentialKind) {
|
|
788
|
+
case 'oauth': {
|
|
789
|
+
const expiration = Number(profile.oauth_access_expires_at);
|
|
790
|
+
return !Number.isFinite(expiration) || expiration <= nowSeconds;
|
|
791
|
+
}
|
|
792
|
+
case 'env':
|
|
793
|
+
// NOTIS_JWT is a complete override. Never combine it with expiry
|
|
794
|
+
// metadata left behind by a desktop credential in the same profile.
|
|
795
|
+
// A token with no readable `exp` is a personal API key, which never
|
|
796
|
+
// expires, so let the server rather than the CLI reject it.
|
|
797
|
+
{
|
|
798
|
+
const expiration = getJwtExpiration(runtime?.jwt);
|
|
799
|
+
return expiration !== null && expiration <= nowSeconds;
|
|
800
|
+
}
|
|
801
|
+
case 'worktree':
|
|
802
|
+
case 'desktop': {
|
|
803
|
+
const rawExpiration = profile.access_expires_at ?? getJwtExpiration(runtime?.jwt);
|
|
804
|
+
// Every selected credential must carry an independently verifiable
|
|
805
|
+
// expiry. Missing or malformed expiry metadata fails closed.
|
|
806
|
+
if (rawExpiration === null || rawExpiration === undefined || rawExpiration === '') {
|
|
807
|
+
return true;
|
|
808
|
+
}
|
|
809
|
+
const expiration = Number(rawExpiration);
|
|
810
|
+
return !Number.isFinite(expiration) || expiration <= nowSeconds;
|
|
811
|
+
}
|
|
812
|
+
default:
|
|
813
|
+
return true;
|
|
814
|
+
}
|
|
815
|
+
}
|
|
816
|
+
|
|
293
817
|
export function workspacePath(appId) {
|
|
294
818
|
return join(WORKSPACE_DIR, appId);
|
|
295
819
|
}
|