@notis_ai/cli 0.2.0-beta.113.1 → 0.2.0-beta.116.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +15 -0
- 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 +15 -0
- package/skills/notis-cli/SKILL.md +8 -4
- package/skills/notis-onboarding/BRIEF.md +93 -0
- package/src/cli.js +16 -0
- package/src/command-specs/apps.js +290 -41
- package/src/command-specs/index.js +2 -0
- package/src/command-specs/onboarding.js +216 -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 +66 -17
- package/src/runtime/errors.js +1 -0
- package/src/runtime/profiles.js +27 -6
- package/src/runtime/telemetry.js +92 -0
- package/src/runtime/transport.js +12 -0
- 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
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
import { readFileSync } from 'node:fs';
|
|
2
|
+
import { dirname, join } from 'node:path';
|
|
3
|
+
import { fileURLToPath } from 'node:url';
|
|
4
|
+
|
|
5
|
+
import { CliError, EXIT_CODES } from '../runtime/errors.js';
|
|
6
|
+
import { getDesktopAuthRecovery, waitForDesktopAuth } from '../runtime/desktop-auth.js';
|
|
7
|
+
import { getJwt, isJwtExpired, loadConfig } from '../runtime/profiles.js';
|
|
8
|
+
|
|
9
|
+
const HERE = dirname(fileURLToPath(import.meta.url));
|
|
10
|
+
const BUNDLED_BRIEF_PATH = join(HERE, '..', '..', 'skills', 'notis-onboarding', 'BRIEF.md');
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* The brief is served rather than bundled so it tracks the deployed server. The
|
|
14
|
+
* bundled copy is a fallback for an offline or unreachable API, and the payload
|
|
15
|
+
* says which one the caller got so a stale brief is diagnosable.
|
|
16
|
+
*/
|
|
17
|
+
async function fetchBrief(apiBase, timeoutMs) {
|
|
18
|
+
try {
|
|
19
|
+
const controller = new AbortController();
|
|
20
|
+
const timer = setTimeout(() => controller.abort(), Math.min(timeoutMs || 30_000, 30_000));
|
|
21
|
+
try {
|
|
22
|
+
const response = await fetch(`${apiBase.replace(/\/$/, '')}/signup/onboarding-brief`, {
|
|
23
|
+
signal: controller.signal,
|
|
24
|
+
});
|
|
25
|
+
if (response.ok) {
|
|
26
|
+
const payload = await response.json();
|
|
27
|
+
if (payload?.markdown) {
|
|
28
|
+
return { markdown: payload.markdown, source: 'server' };
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
} finally {
|
|
32
|
+
clearTimeout(timer);
|
|
33
|
+
}
|
|
34
|
+
} catch {
|
|
35
|
+
// fall through to the bundled copy
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
try {
|
|
39
|
+
return { markdown: readFileSync(BUNDLED_BRIEF_PATH, 'utf-8'), source: 'bundled' };
|
|
40
|
+
} catch {
|
|
41
|
+
return { markdown: null, source: null };
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
async function requestSignupLink(apiBase, { email, useCases }) {
|
|
46
|
+
const response = await fetch(`${apiBase.replace(/\/$/, '')}/signup/agent`, {
|
|
47
|
+
method: 'POST',
|
|
48
|
+
headers: { 'Content-Type': 'application/json' },
|
|
49
|
+
body: JSON.stringify({
|
|
50
|
+
email,
|
|
51
|
+
...(useCases && useCases.length ? { preferred_use_cases: useCases } : {}),
|
|
52
|
+
}),
|
|
53
|
+
});
|
|
54
|
+
const payload = await response.json().catch(() => null);
|
|
55
|
+
|
|
56
|
+
if (response.status === 429) {
|
|
57
|
+
// Requesting another link invalidates the previous one, so retrying is
|
|
58
|
+
// actively harmful. Surface the cooldown and stop.
|
|
59
|
+
throw new CliError({
|
|
60
|
+
code: 'signup_throttled',
|
|
61
|
+
message: payload?.message || 'A sign-in link was just sent to this address.',
|
|
62
|
+
exitCode: EXIT_CODES.usage,
|
|
63
|
+
details: payload || {},
|
|
64
|
+
hints: [
|
|
65
|
+
{
|
|
66
|
+
command: 'Open the newest Notis email on this machine',
|
|
67
|
+
reason: 'Requesting another link makes earlier links stop working',
|
|
68
|
+
},
|
|
69
|
+
],
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
if (!response.ok) {
|
|
74
|
+
throw new CliError({
|
|
75
|
+
code: 'signup_failed',
|
|
76
|
+
message: payload?.message || `Signup failed with status ${response.status}`,
|
|
77
|
+
exitCode: EXIT_CODES.backend,
|
|
78
|
+
details: payload || {},
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
return payload || {};
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
async function startHandler(ctx) {
|
|
86
|
+
const { runtime, options, output } = ctx;
|
|
87
|
+
const apiBase = runtime.apiBase;
|
|
88
|
+
const authenticated = Boolean(runtime.jwt) && !isJwtExpired(runtime.jwt);
|
|
89
|
+
|
|
90
|
+
// Safe to re-run: an already-authenticated machine skips straight to the brief.
|
|
91
|
+
// Agents retry commands, and a second signup would invalidate the first email.
|
|
92
|
+
if (authenticated || options.briefOnly) {
|
|
93
|
+
if (!authenticated) {
|
|
94
|
+
const recovery = getDesktopAuthRecovery(runtime, { mode: 'missing' });
|
|
95
|
+
throw new CliError({
|
|
96
|
+
code: 'auth_missing',
|
|
97
|
+
message: 'This machine is not signed in to Notis yet.',
|
|
98
|
+
exitCode: EXIT_CODES.auth,
|
|
99
|
+
hints: recovery.hints,
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
const brief = await fetchBrief(apiBase, runtime.timeoutMs);
|
|
103
|
+
return output.emitSuccess({
|
|
104
|
+
command: 'start',
|
|
105
|
+
data: { authenticated: true, brief: brief.markdown, brief_source: brief.source },
|
|
106
|
+
humanSummary: 'Notis CLI is authenticated on this machine.',
|
|
107
|
+
renderHuman: () => brief.markdown || 'Notis CLI is authenticated.',
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
if (!options.email) {
|
|
112
|
+
// The one point in the flow that has to stop and talk to the human.
|
|
113
|
+
throw new CliError({
|
|
114
|
+
code: 'signup_email_required',
|
|
115
|
+
message: 'An email address is required to create or access a Notis account.',
|
|
116
|
+
exitCode: EXIT_CODES.usage,
|
|
117
|
+
hints: [
|
|
118
|
+
{
|
|
119
|
+
command: 'Ask the user for their email address, then rerun with --email <address>',
|
|
120
|
+
reason: 'Notis sends a sign-in link there to prove they own the address',
|
|
121
|
+
},
|
|
122
|
+
],
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
const signup = await requestSignupLink(apiBase, {
|
|
127
|
+
email: options.email,
|
|
128
|
+
useCases: options.useCase,
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
// Commander stores the negatable `--no-wait` flag as `options.wait === false`
|
|
132
|
+
// and never sets `options.noWait` — the same footgun already fixed for
|
|
133
|
+
// `--no-open` in apps.js. Reading `noWait` made the flag a no-op, so an agent
|
|
134
|
+
// that asked not to wait blocked for the full timeout instead.
|
|
135
|
+
if (options.wait === false) {
|
|
136
|
+
return output.emitSuccess({
|
|
137
|
+
command: 'start',
|
|
138
|
+
data: { authenticated: false, next_action: 'open_email_link', ...signup },
|
|
139
|
+
humanSummary: `A sign-in link is on its way to ${options.email}.`,
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
const timeoutMs = Number(options.waitTimeoutMs) > 0 ? Number(options.waitTimeoutMs) : 300_000;
|
|
144
|
+
const jwt = await waitForDesktopAuth({
|
|
145
|
+
loadConfig,
|
|
146
|
+
getJwt,
|
|
147
|
+
isJwtExpired,
|
|
148
|
+
profileName: runtime.profileName,
|
|
149
|
+
timeoutMs,
|
|
150
|
+
onTick: (remaining) => {
|
|
151
|
+
if (runtime.outputMode !== 'json' && remaining % 15_000 < 2_000) {
|
|
152
|
+
output.note?.(`Waiting for Notis Desktop to sign in (${Math.round(remaining / 1000)}s left)...`);
|
|
153
|
+
}
|
|
154
|
+
},
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
if (!jwt) {
|
|
158
|
+
throw new CliError({
|
|
159
|
+
code: 'auth_timeout',
|
|
160
|
+
message: 'Timed out waiting for Notis Desktop to authenticate the CLI.',
|
|
161
|
+
exitCode: EXIT_CODES.auth,
|
|
162
|
+
details: { desktop_download_url: signup.desktop_download_url || null },
|
|
163
|
+
hints: [
|
|
164
|
+
{
|
|
165
|
+
command: `Install Notis Desktop: ${signup.desktop_download_url || 'https://notis.ai/channels/desktop-app'}`,
|
|
166
|
+
reason: 'The desktop app is what writes the CLI credential',
|
|
167
|
+
},
|
|
168
|
+
{ command: 'notis start --brief-only', reason: 'Resume once the desktop app is signed in' },
|
|
169
|
+
{ command: 'notis doctor', reason: 'Re-check config, auth, and API reachability' },
|
|
170
|
+
],
|
|
171
|
+
});
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
const brief = await fetchBrief(apiBase, runtime.timeoutMs);
|
|
175
|
+
return output.emitSuccess({
|
|
176
|
+
command: 'start',
|
|
177
|
+
data: { authenticated: true, brief: brief.markdown, brief_source: brief.source },
|
|
178
|
+
humanSummary: 'Notis CLI is authenticated. Follow the onboarding brief below.',
|
|
179
|
+
renderHuman: () => brief.markdown || 'Notis CLI is authenticated.',
|
|
180
|
+
});
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
export const onboardingCommandSpecs = [
|
|
184
|
+
{
|
|
185
|
+
command_path: ['start'],
|
|
186
|
+
summary: 'Create or access a Notis account and wait for the desktop app to sign the CLI in.',
|
|
187
|
+
when_to_use:
|
|
188
|
+
'Run this first on a new machine, before anything that needs auth. Safe to re-run: an already-signed-in machine just reprints the onboarding brief.',
|
|
189
|
+
args_schema: {
|
|
190
|
+
arguments: [],
|
|
191
|
+
options: [
|
|
192
|
+
{ flags: '--email <email>', description: 'Email to create or sign in the Notis account with.' },
|
|
193
|
+
{ flags: '--use-case <slug>', description: 'Primary use case (repeatable).', collect: true },
|
|
194
|
+
{
|
|
195
|
+
flags: '--wait-timeout-ms <n>',
|
|
196
|
+
description: 'How long to wait for the desktop sign-in (default 300000).',
|
|
197
|
+
},
|
|
198
|
+
{ flags: '--no-wait', description: 'Send the link and exit without waiting.' },
|
|
199
|
+
{ flags: '--brief-only', description: 'Print the onboarding brief for an already-signed-in profile.' },
|
|
200
|
+
],
|
|
201
|
+
},
|
|
202
|
+
examples: [
|
|
203
|
+
'notis start --email you@example.com',
|
|
204
|
+
'notis start --brief-only',
|
|
205
|
+
'notis start --email you@example.com --json',
|
|
206
|
+
],
|
|
207
|
+
output_schema:
|
|
208
|
+
'Returns {authenticated, brief, brief_source} once signed in, or {next_action, cooldown_seconds, desktop_download_url} while waiting.',
|
|
209
|
+
mutates: true,
|
|
210
|
+
idempotent: true,
|
|
211
|
+
require_auth: false,
|
|
212
|
+
related_commands: ['notis doctor', 'notis tools link'],
|
|
213
|
+
backend_call: { type: 'http', name: 'POST /signup/agent' },
|
|
214
|
+
handler: startHandler,
|
|
215
|
+
},
|
|
216
|
+
];
|
|
@@ -453,8 +453,12 @@ async function toolsExecHandler(ctx) {
|
|
|
453
453
|
arguments_: {
|
|
454
454
|
tools: [{ tool_slug: requestedToolName, arguments: args }],
|
|
455
455
|
},
|
|
456
|
-
|
|
457
|
-
|
|
456
|
+
// The server is authoritative about provider/MCP effects. A tool name that
|
|
457
|
+
// looks like a read is not proof of read-only behavior, so generic
|
|
458
|
+
// executions always carry a key and use mutation-safe transport retries.
|
|
459
|
+
// Proven reads are still executed in the read lane by hosted MCP.
|
|
460
|
+
mutating: true,
|
|
461
|
+
idempotencyKey,
|
|
458
462
|
fileBindings,
|
|
459
463
|
});
|
|
460
464
|
ctx.output.emitProgress({
|
|
@@ -470,8 +474,8 @@ async function toolsExecHandler(ctx) {
|
|
|
470
474
|
requestId: result.requestId,
|
|
471
475
|
meta: {
|
|
472
476
|
mutating: targetMutating,
|
|
473
|
-
mutation_classification: targetMutating === null ? 'unknown' : '
|
|
474
|
-
idempotency_key:
|
|
477
|
+
mutation_classification: targetMutating === null ? 'unknown' : 'name_hint_only',
|
|
478
|
+
idempotency_key: idempotencyKey,
|
|
475
479
|
},
|
|
476
480
|
renderHuman: () => JSON.stringify(result.payload, null, 2),
|
|
477
481
|
});
|
|
@@ -288,12 +288,13 @@ function renderHarnessHtml({ state, manifest, appConfig, route, harnessOptions,
|
|
|
288
288
|
/**
|
|
289
289
|
* Start the dev server for one or more apps.
|
|
290
290
|
*
|
|
291
|
-
* @param {{apps: Array<{slug: string, projectDir: string, appId?: string, targetAppId?: string, userId?: string}>, port: number, watch?: boolean, harness?: { mode?: string, apiBase?: string, jwt?: string }, log?: (m: string) => void, logError?: (m: string) => void}} options
|
|
291
|
+
* @param {{apps: Array<{slug: string, projectDir: string, appId?: string, targetAppId?: string, userId?: string}>, port: number, watch?: boolean, sessionsFilePath?: string, harness?: { mode?: string, apiBase?: string, jwt?: string }, log?: (m: string) => void, logError?: (m: string) => void}} options
|
|
292
292
|
*/
|
|
293
293
|
export async function startAppDevServer({
|
|
294
294
|
apps,
|
|
295
295
|
port,
|
|
296
296
|
watch = true,
|
|
297
|
+
sessionsFilePath,
|
|
297
298
|
harness = {},
|
|
298
299
|
log = (msg) => process.stdout.write(`${msg}\n`),
|
|
299
300
|
logError = (msg) => process.stderr.write(`${msg}\n`),
|
|
@@ -466,7 +467,7 @@ export async function startAppDevServer({
|
|
|
466
467
|
devSlug: state.slug,
|
|
467
468
|
targetAppId: appId,
|
|
468
469
|
lastHeartbeatAt: now,
|
|
469
|
-
});
|
|
470
|
+
}, sessionsFilePath);
|
|
470
471
|
const response = {
|
|
471
472
|
ok: true,
|
|
472
473
|
app_id: appId,
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import { randomUUID } from 'node:crypto';
|
|
2
2
|
import { existsSync, mkdirSync, readFileSync, renameSync, writeFileSync } from 'node:fs';
|
|
3
|
-
import { homedir } from 'node:os';
|
|
4
3
|
import { dirname, join, parse as parsePath } from 'node:path';
|
|
5
4
|
import { CONFIG_DIR } from './profiles.js';
|
|
6
5
|
|
|
@@ -9,37 +8,6 @@ export const APP_DEV_SESSIONS_FILE = DEFAULT_APP_DEV_SESSIONS_FILE;
|
|
|
9
8
|
export const APP_DEV_SESSIONS_VERSION = 1;
|
|
10
9
|
export const APP_DEV_SESSION_MOUNT_ACKS_VERSION = 1;
|
|
11
10
|
|
|
12
|
-
/**
|
|
13
|
-
* Walk up from `startDir` looking for a workspace-scoped dev-sessions registry.
|
|
14
|
-
*
|
|
15
|
-
* The Notis desktop (and Conductor) reads a PER-WORKSPACE registry at
|
|
16
|
-
* `<workspace>/.context/app-dev-sessions.json` (it points its own `apps dev`
|
|
17
|
-
* at it via `NOTIS_APP_DEV_SESSIONS_FILE`). When a coding agent runs
|
|
18
|
-
* `apps dev` manually it does not inherit that env var, so its session would
|
|
19
|
-
* otherwise land in the global `~/.notis` registry and never appear in the
|
|
20
|
-
* desktop's Local development sidebar. Resolving the nearest `.context`
|
|
21
|
-
* registry keeps agent-run and desktop-run sessions in the same file.
|
|
22
|
-
*
|
|
23
|
-
* Returns the registry path if a `.context/` directory is found in `startDir`
|
|
24
|
-
* or an ancestor (stopping at the home directory / filesystem root), else null.
|
|
25
|
-
*/
|
|
26
|
-
export function findWorkspaceAppDevSessionsFile(startDir = process.cwd()) {
|
|
27
|
-
let dir = startDir;
|
|
28
|
-
const home = homedir();
|
|
29
|
-
const { root } = parsePath(dir);
|
|
30
|
-
// Bounded walk; `.context` lives at the workspace root, never above $HOME.
|
|
31
|
-
for (let depth = 0; depth < 64; depth += 1) {
|
|
32
|
-
if (existsSync(join(dir, '.context'))) {
|
|
33
|
-
return join(dir, '.context', 'app-dev-sessions.json');
|
|
34
|
-
}
|
|
35
|
-
if (dir === home || dir === root) break;
|
|
36
|
-
const parent = dirname(dir);
|
|
37
|
-
if (parent === dir) break;
|
|
38
|
-
dir = parent;
|
|
39
|
-
}
|
|
40
|
-
return null;
|
|
41
|
-
}
|
|
42
|
-
|
|
43
11
|
export function getAppDevSessionsFile(filePath) {
|
|
44
12
|
if (filePath) {
|
|
45
13
|
return filePath;
|
|
@@ -48,10 +16,6 @@ export function getAppDevSessionsFile(filePath) {
|
|
|
48
16
|
if (typeof envPath === 'string' && envPath.trim()) {
|
|
49
17
|
return envPath.trim();
|
|
50
18
|
}
|
|
51
|
-
const workspaceFile = findWorkspaceAppDevSessionsFile();
|
|
52
|
-
if (workspaceFile) {
|
|
53
|
-
return workspaceFile;
|
|
54
|
-
}
|
|
55
19
|
return DEFAULT_APP_DEV_SESSIONS_FILE;
|
|
56
20
|
}
|
|
57
21
|
|
|
@@ -173,7 +137,7 @@ function normalizeMountAcknowledgements(raw) {
|
|
|
173
137
|
typeof acknowledgement.devSlug === 'string' &&
|
|
174
138
|
typeof acknowledgement.mountNonce === 'string' &&
|
|
175
139
|
typeof acknowledgement.acknowledgedAt === 'string' &&
|
|
176
|
-
|
|
140
|
+
['listed', 'rendered'].includes(acknowledgement.stage),
|
|
177
141
|
);
|
|
178
142
|
}
|
|
179
143
|
|
|
@@ -218,12 +182,13 @@ export function removeAppDevSessionMountAcknowledgements(sessionId, sessionsFile
|
|
|
218
182
|
return writeAppDevSessionMountAcknowledgements(registry, sessionsFilePath);
|
|
219
183
|
}
|
|
220
184
|
|
|
221
|
-
function mountAcknowledgementKey(value) {
|
|
222
|
-
return `${value.sessionId}:${value.appId}:${value.devSlug}:${value.mountNonce}`;
|
|
185
|
+
function mountAcknowledgementKey(value, fallbackStage = 'listed') {
|
|
186
|
+
return `${value.sessionId}:${value.appId}:${value.devSlug}:${value.mountNonce}:${value.stage || fallbackStage}`;
|
|
223
187
|
}
|
|
224
188
|
|
|
225
189
|
export async function waitForAppDevSessionMountAcknowledgements(expectedSessions, options = {}) {
|
|
226
190
|
const expected = Array.isArray(expectedSessions) ? expectedSessions : [expectedSessions];
|
|
191
|
+
const stage = options.stage === 'rendered' ? 'rendered' : 'listed';
|
|
227
192
|
const timeoutMs = Number.isFinite(options.timeoutMs) ? Math.max(0, options.timeoutMs) : 15_000;
|
|
228
193
|
const pollIntervalMs = Number.isFinite(options.pollIntervalMs)
|
|
229
194
|
? Math.max(1, options.pollIntervalMs)
|
|
@@ -237,7 +202,9 @@ export async function waitForAppDevSessionMountAcknowledgements(expectedSessions
|
|
|
237
202
|
while (true) {
|
|
238
203
|
const registry = readAppDevSessionMountAcknowledgements(options.sessionsFilePath);
|
|
239
204
|
const acknowledgedKeys = new Set(registry.acknowledgements.map(mountAcknowledgementKey));
|
|
240
|
-
const missing = expected.filter(
|
|
205
|
+
const missing = expected.filter(
|
|
206
|
+
(session) => !acknowledgedKeys.has(mountAcknowledgementKey(session, stage)),
|
|
207
|
+
);
|
|
241
208
|
if (missing.length === 0) {
|
|
242
209
|
return { mounted: true, missing: [], acknowledgements: registry.acknowledgements };
|
|
243
210
|
}
|
|
@@ -249,3 +216,10 @@ export async function waitForAppDevSessionMountAcknowledgements(expectedSessions
|
|
|
249
216
|
await sleep(Math.min(pollIntervalMs, timeoutMs - elapsedMs));
|
|
250
217
|
}
|
|
251
218
|
}
|
|
219
|
+
|
|
220
|
+
export function waitForAppDevSessionRenderAcknowledgements(expectedSessions, options = {}) {
|
|
221
|
+
return waitForAppDevSessionMountAcknowledgements(expectedSessions, {
|
|
222
|
+
...options,
|
|
223
|
+
stage: 'rendered',
|
|
224
|
+
});
|
|
225
|
+
}
|
|
@@ -231,7 +231,13 @@ export function detectProjectWarnings(projectDir, appConfig = null) {
|
|
|
231
231
|
warnings.push('Missing Tailwind config.');
|
|
232
232
|
}
|
|
233
233
|
|
|
234
|
-
|
|
234
|
+
const readsWorkspaceDatabases =
|
|
235
|
+
appConfig?.capabilities?.workspaceDatabases === 'read';
|
|
236
|
+
if (
|
|
237
|
+
appConfig
|
|
238
|
+
&& !readsWorkspaceDatabases
|
|
239
|
+
&& (!Array.isArray(appConfig.databases) || appConfig.databases.length === 0)
|
|
240
|
+
) {
|
|
235
241
|
warnings.push('No database references declared in notis.config.ts.');
|
|
236
242
|
}
|
|
237
243
|
|
|
@@ -490,13 +496,24 @@ export async function runProjectScript({ projectDir, scriptName, env = {}, stdio
|
|
|
490
496
|
stdio,
|
|
491
497
|
env: { ...process.env, ...env },
|
|
492
498
|
});
|
|
499
|
+
let capturedOutput = '';
|
|
500
|
+
for (const stream of [child.stdout, child.stderr]) {
|
|
501
|
+
stream?.on('data', (chunk) => {
|
|
502
|
+
capturedOutput += chunk.toString();
|
|
503
|
+
});
|
|
504
|
+
}
|
|
493
505
|
child.on('error', rejectPromise);
|
|
494
506
|
child.on('exit', (code) => {
|
|
495
507
|
if (code === 0) {
|
|
496
508
|
resolvePromise();
|
|
497
509
|
return;
|
|
498
510
|
}
|
|
499
|
-
|
|
511
|
+
const detail = capturedOutput.trim();
|
|
512
|
+
rejectPromise(
|
|
513
|
+
new Error(
|
|
514
|
+
`npm run ${scriptName} failed with exit code ${code}${detail ? `:\n${detail}` : ''}`,
|
|
515
|
+
),
|
|
516
|
+
);
|
|
500
517
|
});
|
|
501
518
|
});
|
|
502
519
|
}
|
|
@@ -707,7 +724,14 @@ export function generateManifest(appConfig, projectDir) {
|
|
|
707
724
|
return entry;
|
|
708
725
|
});
|
|
709
726
|
|
|
710
|
-
|
|
727
|
+
// Database entries may be a bare slug (structure only) or an object opting
|
|
728
|
+
// into shipping rows to installers. Normalize to the manifest's snake_case.
|
|
729
|
+
const databases = (appConfig.databases || []).map((entry) => {
|
|
730
|
+
if (typeof entry === 'string') return entry;
|
|
731
|
+
if (!entry || typeof entry !== 'object' || !entry.slug) return entry;
|
|
732
|
+
const seedDocuments = entry.seedDocuments ?? entry.seed_documents;
|
|
733
|
+
return seedDocuments === true ? { slug: entry.slug, seed_documents: true } : entry.slug;
|
|
734
|
+
});
|
|
711
735
|
const categories = normalizeCategories(appConfig.categories || []);
|
|
712
736
|
const metadata = { screenshots: resolveListingScreenshots(projectDir, appConfig) };
|
|
713
737
|
const appSlug = safeKebab(appConfig.name);
|
|
@@ -786,12 +810,29 @@ export function generateManifest(appConfig, projectDir) {
|
|
|
786
810
|
css: 'bundle/app.css',
|
|
787
811
|
},
|
|
788
812
|
databases,
|
|
813
|
+
capabilities: normalizeAppCapabilities(appConfig.capabilities),
|
|
789
814
|
tools: appConfig.tools || [],
|
|
790
815
|
skills,
|
|
791
816
|
onboarding: appConfig.onboarding || null,
|
|
792
817
|
};
|
|
793
818
|
}
|
|
794
819
|
|
|
820
|
+
/**
|
|
821
|
+
* Keeps only capabilities the platform actually understands, at the exact
|
|
822
|
+
* values it accepts. An unknown key or value is dropped rather than passed
|
|
823
|
+
* through, so a typo can never reach the server as a permission grant.
|
|
824
|
+
*/
|
|
825
|
+
export function normalizeAppCapabilities(capabilities) {
|
|
826
|
+
if (!capabilities || typeof capabilities !== 'object') {
|
|
827
|
+
return {};
|
|
828
|
+
}
|
|
829
|
+
const normalized = {};
|
|
830
|
+
if (capabilities.workspaceDatabases === 'read') {
|
|
831
|
+
normalized.workspaceDatabases = 'read';
|
|
832
|
+
}
|
|
833
|
+
return normalized;
|
|
834
|
+
}
|
|
835
|
+
|
|
795
836
|
export function resolveConfiguredAppSkills(appConfig, projectDir) {
|
|
796
837
|
const configured = Array.isArray(appConfig.skills) ? appConfig.skills : [];
|
|
797
838
|
const projectRoot = resolve(projectDir);
|
|
@@ -831,13 +872,14 @@ export function resolveConfiguredAppSkills(appConfig, projectDir) {
|
|
|
831
872
|
/**
|
|
832
873
|
* Build the app bundle: generate entry file, run `vite build`, package into .notis/output/.
|
|
833
874
|
*/
|
|
834
|
-
export async function buildArtifact(projectDir) {
|
|
875
|
+
export async function buildArtifact(projectDir, { stdio = 'inherit' } = {}) {
|
|
835
876
|
await prepareArtifactBuild(projectDir);
|
|
836
877
|
|
|
837
878
|
// Run Vite build
|
|
838
879
|
await runProjectScript({
|
|
839
880
|
projectDir,
|
|
840
881
|
scriptName: 'build',
|
|
882
|
+
stdio,
|
|
841
883
|
});
|
|
842
884
|
|
|
843
885
|
// Verify the canonical `.notis/output/bundle` packaging contract.
|
|
@@ -948,8 +990,13 @@ export function scaffoldProject({ projectDir, appName, fromSlug = null }) {
|
|
|
948
990
|
if (existsSync(pkgPath)) {
|
|
949
991
|
const pkg = JSON.parse(readFileSync(pkgPath, 'utf-8'));
|
|
950
992
|
pkg.name = appName.toLowerCase().replace(/[^a-z0-9-]/g, '-').replace(/-+/g, '-');
|
|
993
|
+
// A scaffold creates a new app identity, even when its source comes from a
|
|
994
|
+
// versioned Store example. New apps must therefore start their own release
|
|
995
|
+
// history instead of inheriting the example app's registry version.
|
|
996
|
+
pkg.notisAppVersion = '0.1.0';
|
|
951
997
|
ensureScaffoldLocalSdk(projectDir, pkg);
|
|
952
998
|
writeFileSync(pkgPath, JSON.stringify(pkg, null, 2) + '\n');
|
|
999
|
+
normalizeScaffoldLockfile(projectDir, pkg);
|
|
953
1000
|
}
|
|
954
1001
|
|
|
955
1002
|
// Update notis.config.ts with the app name
|
|
@@ -983,6 +1030,20 @@ export function scaffoldProject({ projectDir, appName, fromSlug = null }) {
|
|
|
983
1030
|
return { projectDir };
|
|
984
1031
|
}
|
|
985
1032
|
|
|
1033
|
+
function normalizeScaffoldLockfile(projectDir, pkg) {
|
|
1034
|
+
const lockPath = join(projectDir, 'package-lock.json');
|
|
1035
|
+
if (!existsSync(lockPath)) {
|
|
1036
|
+
return;
|
|
1037
|
+
}
|
|
1038
|
+
|
|
1039
|
+
const lockfile = JSON.parse(readFileSync(lockPath, 'utf-8'));
|
|
1040
|
+
lockfile.name = pkg.name;
|
|
1041
|
+
if (lockfile.packages?.['']) {
|
|
1042
|
+
lockfile.packages[''].name = pkg.name;
|
|
1043
|
+
}
|
|
1044
|
+
writeFileSync(lockPath, JSON.stringify(lockfile, null, 2) + '\n');
|
|
1045
|
+
}
|
|
1046
|
+
|
|
986
1047
|
function ensureScaffoldLocalSdk(projectDir, pkg) {
|
|
987
1048
|
let shouldInstallLocalSdk = false;
|
|
988
1049
|
for (const dependencyGroup of ['dependencies', 'devDependencies']) {
|
|
@@ -24,7 +24,15 @@ function defaultDesktopAppName(apiBase) {
|
|
|
24
24
|
}
|
|
25
25
|
}
|
|
26
26
|
|
|
27
|
-
|
|
27
|
+
/**
|
|
28
|
+
* Recovery hints for an unusable desktop-managed credential.
|
|
29
|
+
*
|
|
30
|
+
* `mode` distinguishes the two cases an agent has to act on differently:
|
|
31
|
+
* "expired" means a session existed and the desktop app can renew it, while
|
|
32
|
+
* "missing" means this machine was never signed in — telling that caller to
|
|
33
|
+
* renew something is misleading.
|
|
34
|
+
*/
|
|
35
|
+
export function getDesktopAuthRecovery(runtime, { mode = 'expired' } = {}) {
|
|
28
36
|
const desktopRunning = isPidRunning(runtime.desktopPid);
|
|
29
37
|
const appName = runtime.desktopAppName || defaultDesktopAppName(runtime.apiBase);
|
|
30
38
|
let command = 'Start the Notis desktop app';
|
|
@@ -32,22 +40,63 @@ export function getDesktopAuthRecovery(runtime) {
|
|
|
32
40
|
command = `open -a ${quoteShellArgument(appName)}`;
|
|
33
41
|
}
|
|
34
42
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
{
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
43
|
+
let reason;
|
|
44
|
+
if (mode === 'missing') {
|
|
45
|
+
reason = desktopRunning
|
|
46
|
+
? `Sign in to ${appName} to authenticate the CLI on this machine, then retry`
|
|
47
|
+
: `Install and sign in to ${appName} to authenticate the CLI on this machine, then retry`;
|
|
48
|
+
} else {
|
|
49
|
+
reason = desktopRunning
|
|
50
|
+
? `Bring ${appName} forward so it can renew CLI authentication, then retry`
|
|
51
|
+
: `Start ${appName} to renew expired CLI authentication, then retry`;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const hints = [{ command, reason }];
|
|
55
|
+
if (mode === 'missing') {
|
|
56
|
+
// Leads the list: a machine that was never signed in may not even have an
|
|
57
|
+
// account yet, and `notis start` covers both cases.
|
|
58
|
+
hints.unshift({
|
|
59
|
+
command: 'notis start --email <address>',
|
|
60
|
+
reason: 'Create or access a Notis account and sign this machine in',
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
hints.push({
|
|
64
|
+
command: 'notis doctor',
|
|
65
|
+
reason: 'Retry the auth and API checks after the desktop app is ready',
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
return { desktopRunning, appName, hints };
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Block until the desktop app writes an unexpired JWT into the CLI profile.
|
|
73
|
+
*
|
|
74
|
+
* Polls the local config file only — never the server. The desktop app is what
|
|
75
|
+
* mints the credential, and `transport.js` re-reads the profile before every
|
|
76
|
+
* request, so this loop exists purely so the caller's process can wait rather
|
|
77
|
+
* than fail and make the user re-run the command.
|
|
78
|
+
*/
|
|
79
|
+
export async function waitForDesktopAuth({
|
|
80
|
+
loadConfig,
|
|
81
|
+
getJwt,
|
|
82
|
+
isJwtExpired,
|
|
83
|
+
profileName = 'default',
|
|
84
|
+
timeoutMs = 300_000,
|
|
85
|
+
intervalMs = 2_000,
|
|
86
|
+
onTick,
|
|
87
|
+
} = {}) {
|
|
88
|
+
const deadline = Date.now() + timeoutMs;
|
|
89
|
+
for (;;) {
|
|
90
|
+
const jwt = getJwt(loadConfig(), profileName);
|
|
91
|
+
if (jwt && !isJwtExpired(jwt)) {
|
|
92
|
+
return jwt;
|
|
93
|
+
}
|
|
94
|
+
if (Date.now() >= deadline) {
|
|
95
|
+
return null;
|
|
96
|
+
}
|
|
97
|
+
if (onTick) onTick(Math.max(0, deadline - Date.now()));
|
|
98
|
+
await new Promise((resolve) => setTimeout(resolve, intervalMs));
|
|
99
|
+
}
|
|
51
100
|
}
|
|
52
101
|
|
|
53
102
|
export function createExpiredAuthError(runtime) {
|
package/src/runtime/errors.js
CHANGED
package/src/runtime/profiles.js
CHANGED
|
@@ -143,6 +143,14 @@ export function resolveWorktreeRuntime(startDir = process.cwd()) {
|
|
|
143
143
|
const runtime = readJsonFile(runtimePath);
|
|
144
144
|
const apiBase = typeof runtime?.api_base === 'string' ? runtime.api_base.replace(/\/+$/, '') : '';
|
|
145
145
|
const configFile = typeof runtime?.config_file === 'string' ? runtime.config_file : '';
|
|
146
|
+
const appDevSessionsFile =
|
|
147
|
+
typeof runtime?.app_dev_sessions_file === 'string' && runtime.app_dev_sessions_file.trim()
|
|
148
|
+
? runtime.app_dev_sessions_file.trim()
|
|
149
|
+
: join(dirname(runtimePath), 'app-dev-sessions.json');
|
|
150
|
+
const desktopDeepLinkScheme =
|
|
151
|
+
typeof runtime?.desktop_deep_link_scheme === 'string'
|
|
152
|
+
? runtime.desktop_deep_link_scheme.trim()
|
|
153
|
+
: '';
|
|
146
154
|
const pid = Number(runtime?.dev_pid);
|
|
147
155
|
if (
|
|
148
156
|
runtime?.mode !== 'local-only' ||
|
|
@@ -165,6 +173,8 @@ export function resolveWorktreeRuntime(startDir = process.cwd()) {
|
|
|
165
173
|
...runtime,
|
|
166
174
|
api_base: apiBase,
|
|
167
175
|
config_file: resolve(dirname(runtimePath), configFile),
|
|
176
|
+
app_dev_sessions_file: resolve(dirname(runtimePath), appDevSessionsFile),
|
|
177
|
+
desktop_deep_link_scheme: desktopDeepLinkScheme || undefined,
|
|
168
178
|
runtime_path: runtimePath,
|
|
169
179
|
routing_path: routingPath,
|
|
170
180
|
};
|
|
@@ -187,7 +197,15 @@ export function loadConfig(runtime = null) {
|
|
|
187
197
|
return normalizeConfig({});
|
|
188
198
|
}
|
|
189
199
|
|
|
190
|
-
|
|
200
|
+
// The desktop app rewrites this file while the CLI may be reading it, so a
|
|
201
|
+
// torn or corrupt read is expected rather than exceptional. Degrade to
|
|
202
|
+
// "unauthenticated", which surfaces the actionable auth_missing error instead
|
|
203
|
+
// of a raw SyntaxError from deep inside the runtime.
|
|
204
|
+
try {
|
|
205
|
+
return normalizeConfig(JSON.parse(readFileSync(configFile, 'utf-8')));
|
|
206
|
+
} catch {
|
|
207
|
+
return normalizeConfig({});
|
|
208
|
+
}
|
|
191
209
|
}
|
|
192
210
|
|
|
193
211
|
export function saveConfig(config, runtime = null) {
|
|
@@ -361,11 +379,14 @@ export function resolveRuntimeProfile(
|
|
|
361
379
|
: null;
|
|
362
380
|
|
|
363
381
|
if (requireAuth && !jwt) {
|
|
364
|
-
const recovery = getDesktopAuthRecovery(
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
382
|
+
const recovery = getDesktopAuthRecovery(
|
|
383
|
+
{
|
|
384
|
+
apiBase,
|
|
385
|
+
desktopAppName: profile.desktop_app_name,
|
|
386
|
+
desktopPid: profile.desktop_pid,
|
|
387
|
+
},
|
|
388
|
+
{ mode: 'missing' },
|
|
389
|
+
);
|
|
369
390
|
throw new CliError({
|
|
370
391
|
code: 'auth_missing',
|
|
371
392
|
message: `No JWT configured for profile ${profileName}`,
|