@notis_ai/cli 0.2.8 → 0.2.10
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 +41 -2
- package/package.json +1 -1
- package/skills/notis-apps/SKILL.md +10 -21
- package/skills/notis-apps/cli.md +1 -1
- package/skills/notis-cli/SKILL.md +11 -5
- package/skills/notis-query/cli.md +1 -1
- package/src/command-specs/apps.js +188 -23
- package/src/command-specs/auth.js +107 -0
- package/src/command-specs/diagnostics.js +6 -1
- package/src/command-specs/helpers.js +7 -1
- package/src/command-specs/index.js +2 -0
- package/src/command-specs/meta.js +68 -6
- package/src/command-specs/onboarding.js +94 -6
- package/src/runtime/app-dev-server.js +3 -2
- package/src/runtime/app-dev-sessions.js +14 -40
- package/src/runtime/cli-mode.generated.js +4 -3
- package/src/runtime/cli-mode.js +13 -8
- package/src/runtime/desktop-auth.js +22 -2
- package/src/runtime/oauth.js +1121 -0
- package/src/runtime/output.js +7 -0
- package/src/runtime/profiles.js +423 -23
- package/src/runtime/transport.js +41 -11
|
@@ -1,19 +1,52 @@
|
|
|
1
1
|
import { COMPOSIO_SEARCH_TOOLS, healthCheck, probeAuth } from './helpers.js';
|
|
2
2
|
import { findCommandSpec, formatDescribe } from '../runtime/help.js';
|
|
3
|
-
import { createExpiredAuthError } from '../runtime/desktop-auth.js';
|
|
4
|
-
import {
|
|
3
|
+
import { createExpiredAuthError, getDesktopAuthRecovery } from '../runtime/desktop-auth.js';
|
|
4
|
+
import {
|
|
5
|
+
credentialIsExpired,
|
|
6
|
+
getJwtCanonicalUserId,
|
|
7
|
+
getProfile,
|
|
8
|
+
loadConfig,
|
|
9
|
+
} from '../runtime/profiles.js';
|
|
10
|
+
import { ensureFreshOAuthCredential } from '../runtime/oauth.js';
|
|
11
|
+
|
|
12
|
+
export const DOCTOR_TOOL_ROUNDTRIP_TIMEOUT_MS = 90_000;
|
|
13
|
+
|
|
14
|
+
export function doctorToolRoundtripRuntime(runtime) {
|
|
15
|
+
return {
|
|
16
|
+
...runtime,
|
|
17
|
+
timeoutMs: Math.max(runtime.timeoutMs || 0, DOCTOR_TOOL_ROUNDTRIP_TIMEOUT_MS),
|
|
18
|
+
};
|
|
19
|
+
}
|
|
5
20
|
|
|
6
21
|
async function doctorHandler(ctx) {
|
|
7
22
|
const checks = {
|
|
8
23
|
config: 'ok',
|
|
9
24
|
auth: 'missing',
|
|
25
|
+
identity: 'ok',
|
|
10
26
|
health: 'unknown',
|
|
11
27
|
tool_roundtrip: 'unknown',
|
|
12
28
|
};
|
|
13
29
|
|
|
30
|
+
if (ctx.runtime.credentialKind === 'oauth') {
|
|
31
|
+
try {
|
|
32
|
+
await ensureFreshOAuthCredential(ctx.runtime);
|
|
33
|
+
} catch {
|
|
34
|
+
// Doctor still reports the remaining health and recovery checks when a
|
|
35
|
+
// refresh endpoint is unavailable or rejects the stored credential.
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
let profile = getProfile(loadConfig(ctx.runtime.worktreeRuntime), ctx.runtime.profileName);
|
|
14
39
|
checks.auth = ctx.runtime.jwt
|
|
15
|
-
? (
|
|
40
|
+
? (credentialIsExpired(ctx.runtime, profile) ? 'expired' : 'configured')
|
|
16
41
|
: 'missing';
|
|
42
|
+
const desktopUserId = getJwtCanonicalUserId(profile.jwt);
|
|
43
|
+
if (
|
|
44
|
+
desktopUserId
|
|
45
|
+
&& profile.oauth_user_id
|
|
46
|
+
&& desktopUserId !== profile.oauth_user_id
|
|
47
|
+
) {
|
|
48
|
+
checks.identity = 'error';
|
|
49
|
+
}
|
|
17
50
|
|
|
18
51
|
try {
|
|
19
52
|
await healthCheck(ctx.runtime);
|
|
@@ -24,8 +57,15 @@ async function doctorHandler(ctx) {
|
|
|
24
57
|
|
|
25
58
|
if (ctx.runtime.jwt) {
|
|
26
59
|
try {
|
|
27
|
-
|
|
60
|
+
// Tool discovery may have to query several connected MCP servers on a
|
|
61
|
+
// cold local backend. A diagnostic must not report a false failure just
|
|
62
|
+
// because that legitimate roundtrip exceeds the general 30s default.
|
|
63
|
+
const payload = await probeAuth(doctorToolRoundtripRuntime(ctx.runtime));
|
|
28
64
|
checks.tool_roundtrip = Array.isArray(payload.toolkit_connection_statuses) ? 'ok' : 'error';
|
|
65
|
+
profile = getProfile(loadConfig(ctx.runtime.worktreeRuntime), ctx.runtime.profileName);
|
|
66
|
+
checks.auth = ctx.runtime.jwt
|
|
67
|
+
? (credentialIsExpired(ctx.runtime, profile) ? 'expired' : 'configured')
|
|
68
|
+
: 'missing';
|
|
29
69
|
} catch {
|
|
30
70
|
checks.tool_roundtrip = 'error';
|
|
31
71
|
}
|
|
@@ -33,7 +73,7 @@ async function doctorHandler(ctx) {
|
|
|
33
73
|
|
|
34
74
|
const hints = [];
|
|
35
75
|
if (checks.auth === 'missing') {
|
|
36
|
-
hints.push({
|
|
76
|
+
hints.push(...getDesktopAuthRecovery(ctx.runtime, { mode: 'missing' }).hints);
|
|
37
77
|
} else if (checks.auth === 'expired') {
|
|
38
78
|
hints.push(...createExpiredAuthError(ctx.runtime).hints);
|
|
39
79
|
}
|
|
@@ -43,12 +83,27 @@ async function doctorHandler(ctx) {
|
|
|
43
83
|
if (checks.tool_roundtrip === 'error') {
|
|
44
84
|
hints.push({ command: 'notis whoami', reason: 'Verify your account and permissions' });
|
|
45
85
|
}
|
|
86
|
+
if (checks.identity === 'error') {
|
|
87
|
+
hints.push({
|
|
88
|
+
command: 'notis logout',
|
|
89
|
+
reason: 'Desktop and OAuth credentials identify different accounts; remove the independent OAuth grant or sign Desktop back into the same account',
|
|
90
|
+
});
|
|
91
|
+
}
|
|
46
92
|
|
|
47
93
|
return ctx.output.emitSuccess({
|
|
48
94
|
command: ctx.spec.command_path.join(' '),
|
|
49
95
|
data: {
|
|
50
96
|
profile: ctx.runtime.profileName,
|
|
51
97
|
api_base: ctx.runtime.apiBase,
|
|
98
|
+
credential_source: ctx.runtime.credentialKind || null,
|
|
99
|
+
...(ctx.runtime.credentialKind === 'oauth'
|
|
100
|
+
? {
|
|
101
|
+
oauth_client_id: profile.oauth_client_id || null,
|
|
102
|
+
oauth_scopes: profile.oauth_scopes || [],
|
|
103
|
+
oauth_access_expires_at: profile.oauth_access_expires_at || null,
|
|
104
|
+
oauth_refresh_expires_at: profile.oauth_refresh_expires_at || null,
|
|
105
|
+
}
|
|
106
|
+
: {}),
|
|
52
107
|
checks,
|
|
53
108
|
},
|
|
54
109
|
humanSummary: `Doctor checks completed for profile ${ctx.runtime.profileName}`,
|
|
@@ -72,16 +127,23 @@ function decodeJwtUserId(jwt) {
|
|
|
72
127
|
}
|
|
73
128
|
}
|
|
74
129
|
|
|
130
|
+
export function activeRuntimeUserId(runtime) {
|
|
131
|
+
return runtime.credentialKind === 'oauth'
|
|
132
|
+
? runtime.oauthUserId
|
|
133
|
+
: decodeJwtUserId(runtime.jwt);
|
|
134
|
+
}
|
|
135
|
+
|
|
75
136
|
async function whoamiHandler(ctx) {
|
|
76
137
|
const payload = await probeAuth(ctx.runtime);
|
|
77
138
|
const toolkits = payload.toolkit_connection_statuses || [];
|
|
78
|
-
const userId =
|
|
139
|
+
const userId = activeRuntimeUserId(ctx.runtime);
|
|
79
140
|
|
|
80
141
|
return ctx.output.emitSuccess({
|
|
81
142
|
command: ctx.spec.command_path.join(' '),
|
|
82
143
|
data: {
|
|
83
144
|
profile: ctx.runtime.profileName,
|
|
84
145
|
api_base: ctx.runtime.apiBase,
|
|
146
|
+
credential_source: ctx.runtime.credentialKind || null,
|
|
85
147
|
user_id: userId,
|
|
86
148
|
toolkit_count: toolkits.length,
|
|
87
149
|
toolkits: toolkits.map((t) => t.toolkit),
|
|
@@ -4,7 +4,12 @@ import { fileURLToPath } from 'node:url';
|
|
|
4
4
|
|
|
5
5
|
import { CliError, EXIT_CODES } from '../runtime/errors.js';
|
|
6
6
|
import { getDesktopAuthRecovery, waitForDesktopAuth } from '../runtime/desktop-auth.js';
|
|
7
|
-
import {
|
|
7
|
+
import {
|
|
8
|
+
credentialIsExpired,
|
|
9
|
+
getProfile,
|
|
10
|
+
loadConfig,
|
|
11
|
+
} from '../runtime/profiles.js';
|
|
12
|
+
import { ensureFreshOAuthCredential, loginWithOAuth } from '../runtime/oauth.js';
|
|
8
13
|
|
|
9
14
|
const HERE = dirname(fileURLToPath(import.meta.url));
|
|
10
15
|
const BUNDLED_BRIEF_PATH = join(HERE, '..', '..', 'skills', 'notis-onboarding', 'BRIEF.md');
|
|
@@ -85,7 +90,20 @@ async function requestSignupLink(apiBase, { email, useCases }) {
|
|
|
85
90
|
async function startHandler(ctx) {
|
|
86
91
|
const { runtime, options, output } = ctx;
|
|
87
92
|
const apiBase = runtime.apiBase;
|
|
88
|
-
|
|
93
|
+
if (runtime.credentialKind === 'oauth') {
|
|
94
|
+
try {
|
|
95
|
+
await ensureFreshOAuthCredential(runtime);
|
|
96
|
+
} catch {
|
|
97
|
+
// A failed refresh is equivalent to no usable session here. Interactive
|
|
98
|
+
// starts may still authorize again; brief-only and agent runs surface
|
|
99
|
+
// the normal authentication recovery below.
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
let authenticated = Boolean(runtime.jwt)
|
|
103
|
+
&& !credentialIsExpired(
|
|
104
|
+
runtime,
|
|
105
|
+
getProfile(loadConfig(runtime.worktreeRuntime), runtime.profileName),
|
|
106
|
+
);
|
|
89
107
|
|
|
90
108
|
// Safe to re-run: an already-authenticated machine skips straight to the brief.
|
|
91
109
|
// Agents retry commands, and a second signup would invalidate the first email.
|
|
@@ -108,6 +126,54 @@ async function startHandler(ctx) {
|
|
|
108
126
|
});
|
|
109
127
|
}
|
|
110
128
|
|
|
129
|
+
if (
|
|
130
|
+
!options.email
|
|
131
|
+
&& !runtime.agentMode
|
|
132
|
+
&& !runtime.nonInteractive
|
|
133
|
+
&& options.wait !== false
|
|
134
|
+
) {
|
|
135
|
+
let authorization;
|
|
136
|
+
let authorizationError;
|
|
137
|
+
try {
|
|
138
|
+
authorization = await loginWithOAuth(runtime, { browser: true }, output);
|
|
139
|
+
} catch (error) {
|
|
140
|
+
// Keep the original OAuth failure while checking whether another process
|
|
141
|
+
// completed authentication before this attempt failed.
|
|
142
|
+
authorizationError = error;
|
|
143
|
+
authorization = null;
|
|
144
|
+
}
|
|
145
|
+
if (authorization?.agentAuthorization) {
|
|
146
|
+
return output.emitSuccess({
|
|
147
|
+
command: 'start',
|
|
148
|
+
data: { authenticated: false, ...authorization.agentAuthorization },
|
|
149
|
+
humanSummary: 'Open the authorization URL to finish signing in.',
|
|
150
|
+
renderHuman: () => `Authorize Notis CLI: ${authorization.agentAuthorization.authorize_url}`,
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
// A failed authorization leaves the stale credential that got us here in
|
|
154
|
+
// place, so re-apply the same expiry test used above rather than reporting
|
|
155
|
+
// a machine as signed in on the strength of a dead token.
|
|
156
|
+
authenticated = Boolean(runtime.jwt)
|
|
157
|
+
&& !credentialIsExpired(runtime, getProfile(loadConfig(runtime.worktreeRuntime), runtime.profileName));
|
|
158
|
+
if (authenticated) {
|
|
159
|
+
const brief = await fetchBrief(apiBase, runtime.timeoutMs);
|
|
160
|
+
return output.emitSuccess({
|
|
161
|
+
command: 'start',
|
|
162
|
+
data: {
|
|
163
|
+
authenticated: true,
|
|
164
|
+
credential_source: runtime.credentialKind,
|
|
165
|
+
brief: brief.markdown,
|
|
166
|
+
brief_source: brief.source,
|
|
167
|
+
},
|
|
168
|
+
humanSummary: 'Notis CLI is authenticated. Follow the onboarding brief below.',
|
|
169
|
+
renderHuman: () => brief.markdown || 'Notis CLI is authenticated.',
|
|
170
|
+
});
|
|
171
|
+
}
|
|
172
|
+
if (authorizationError) {
|
|
173
|
+
throw authorizationError;
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
111
177
|
if (!options.email) {
|
|
112
178
|
// The one point in the flow that has to stop and talk to the human.
|
|
113
179
|
throw new CliError({
|
|
@@ -143,8 +209,29 @@ async function startHandler(ctx) {
|
|
|
143
209
|
const timeoutMs = Number(options.waitTimeoutMs) > 0 ? Number(options.waitTimeoutMs) : 300_000;
|
|
144
210
|
const jwt = await waitForDesktopAuth({
|
|
145
211
|
loadConfig,
|
|
146
|
-
getJwt,
|
|
147
|
-
|
|
212
|
+
getJwt: (config, profileName) => {
|
|
213
|
+
const profile = getProfile(config, profileName);
|
|
214
|
+
if (
|
|
215
|
+
profile.jwt
|
|
216
|
+
&& !credentialIsExpired(
|
|
217
|
+
{ credentialKind: 'desktop', jwt: profile.jwt },
|
|
218
|
+
profile,
|
|
219
|
+
)
|
|
220
|
+
) {
|
|
221
|
+
return profile.jwt;
|
|
222
|
+
}
|
|
223
|
+
if (
|
|
224
|
+
profile.oauth_access_token
|
|
225
|
+
&& !credentialIsExpired(
|
|
226
|
+
{ credentialKind: 'oauth', jwt: profile.oauth_access_token },
|
|
227
|
+
profile,
|
|
228
|
+
)
|
|
229
|
+
) {
|
|
230
|
+
return profile.oauth_access_token;
|
|
231
|
+
}
|
|
232
|
+
return undefined;
|
|
233
|
+
},
|
|
234
|
+
isJwtExpired: () => false,
|
|
148
235
|
profileName: runtime.profileName,
|
|
149
236
|
timeoutMs,
|
|
150
237
|
onTick: (remaining) => {
|
|
@@ -165,7 +252,8 @@ async function startHandler(ctx) {
|
|
|
165
252
|
command: `Install Notis Desktop: ${signup.desktop_download_url || 'https://notis.ai/channels/desktop-app'}`,
|
|
166
253
|
reason: 'The desktop app is what writes the CLI credential',
|
|
167
254
|
},
|
|
168
|
-
{ command: 'notis
|
|
255
|
+
{ command: 'notis login', reason: 'Authorize this machine directly in a browser' },
|
|
256
|
+
{ command: 'notis start --brief-only', reason: 'Resume once authentication is complete' },
|
|
169
257
|
{ command: 'notis doctor', reason: 'Re-check config, auth, and API reachability' },
|
|
170
258
|
],
|
|
171
259
|
});
|
|
@@ -183,7 +271,7 @@ async function startHandler(ctx) {
|
|
|
183
271
|
export const onboardingCommandSpecs = [
|
|
184
272
|
{
|
|
185
273
|
command_path: ['start'],
|
|
186
|
-
summary: 'Create or access a Notis account and
|
|
274
|
+
summary: 'Create or access a Notis account and authorize the CLI.',
|
|
187
275
|
when_to_use:
|
|
188
276
|
'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
277
|
args_schema: {
|
|
@@ -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
|
+
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
// Auto-generated on npm publish.
|
|
2
|
-
// Committed value is '
|
|
3
|
-
//
|
|
4
|
-
|
|
2
|
+
// Committed value is 'published' so the CLI defaults to the live Notis API.
|
|
3
|
+
// Localhost overrides come only from the worktree test lease (`./dev.sh`).
|
|
4
|
+
// scripts/set-cli-mode.js can rewrite this for publish/labeling experiments.
|
|
5
|
+
export const MODE = 'published';
|
package/src/runtime/cli-mode.js
CHANGED
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* CLI mode detection for `notis apps dev`.
|
|
3
3
|
*
|
|
4
|
-
* The repo-local
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
* that isn't running).
|
|
4
|
+
* The published npm CLI and the repo-local checkout both default to the live
|
|
5
|
+
* Notis API (`api.notis.ai` / `api-beta.notis.ai`). Localhost is not a CLI
|
|
6
|
+
* default — the `/notis-tests` worktree lease (`./dev.sh`) is the only
|
|
7
|
+
* supported loopback override for Notis developers.
|
|
9
8
|
*
|
|
10
|
-
*
|
|
9
|
+
* Mode is baked into `cli-mode.generated.js` at publish time. The
|
|
10
|
+
* `NOTIS_CLI_MODE` env var is honored for internal labeling only (e.g. the
|
|
11
|
+
* `apps` auto-dev loop inside `./dev.sh`).
|
|
11
12
|
*/
|
|
12
13
|
|
|
13
14
|
import { MODE as BAKED_MODE } from './cli-mode.generated.js';
|
|
@@ -20,8 +21,12 @@ export function getCliMode() {
|
|
|
20
21
|
return BAKED_MODE === 'published' ? 'published' : 'local';
|
|
21
22
|
}
|
|
22
23
|
|
|
23
|
-
export function getDefaultApiBase(mode = getCliMode()) {
|
|
24
|
-
|
|
24
|
+
export function getDefaultApiBase(mode = getCliMode(), { beta = false } = {}) {
|
|
25
|
+
// Mode no longer switches the default API to localhost. Local loopback is
|
|
26
|
+
// reserved for the worktree test lease; `mode` only affects portal origin
|
|
27
|
+
// labeling for the in-repo apps-dev helper.
|
|
28
|
+
void mode;
|
|
29
|
+
return beta ? 'https://api-beta.notis.ai' : 'https://api.notis.ai';
|
|
25
30
|
}
|
|
26
31
|
|
|
27
32
|
export function getDefaultPortalOrigin(mode = getCliMode()) {
|
|
@@ -56,8 +56,8 @@ export function getDesktopAuthRecovery(runtime, { mode = 'expired' } = {}) {
|
|
|
56
56
|
// Leads the list: a machine that was never signed in may not even have an
|
|
57
57
|
// account yet, and `notis start` covers both cases.
|
|
58
58
|
hints.unshift({
|
|
59
|
-
command: 'notis
|
|
60
|
-
reason: '
|
|
59
|
+
command: 'notis login',
|
|
60
|
+
reason: 'Sign in or create an account in the browser and authorize this machine',
|
|
61
61
|
});
|
|
62
62
|
}
|
|
63
63
|
hints.push({
|
|
@@ -100,6 +100,18 @@ export async function waitForDesktopAuth({
|
|
|
100
100
|
}
|
|
101
101
|
|
|
102
102
|
export function createExpiredAuthError(runtime) {
|
|
103
|
+
if (runtime.credentialKind === 'oauth') {
|
|
104
|
+
return new CliError({
|
|
105
|
+
code: 'auth_expired',
|
|
106
|
+
message: 'Notis CLI OAuth authentication has expired',
|
|
107
|
+
exitCode: EXIT_CODES.auth,
|
|
108
|
+
details: { credential_source: 'oauth' },
|
|
109
|
+
hints: [
|
|
110
|
+
{ command: 'notis login', reason: 'Authorize a new scoped CLI credential' },
|
|
111
|
+
{ command: 'notis doctor', reason: 'Inspect the active credential state' },
|
|
112
|
+
],
|
|
113
|
+
});
|
|
114
|
+
}
|
|
103
115
|
if (runtime.credentialSource === 'env') {
|
|
104
116
|
return new CliError({
|
|
105
117
|
code: 'auth_expired',
|
|
@@ -130,6 +142,14 @@ export function createExpiredAuthError(runtime) {
|
|
|
130
142
|
}
|
|
131
143
|
|
|
132
144
|
export function createInvalidAuthHints(runtime) {
|
|
145
|
+
if (runtime?.credentialKind === 'oauth') {
|
|
146
|
+
return [
|
|
147
|
+
{
|
|
148
|
+
command: 'notis login',
|
|
149
|
+
reason: 'Authorize a new scoped CLI credential',
|
|
150
|
+
},
|
|
151
|
+
];
|
|
152
|
+
}
|
|
133
153
|
if (runtime?.credentialSource === 'env') {
|
|
134
154
|
return [
|
|
135
155
|
{
|