@notis_ai/cli 0.2.10 → 0.2.11
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 +73 -9
- package/dist/scaffolds.json +1 -1
- package/package.json +1 -1
- package/skills/notis-apps/SKILL.md +1 -1
- package/skills/notis-apps/cli.md +1 -1
- package/skills/notis-cli/SKILL.md +56 -16
- package/skills/notis-onboarding/BRIEF.md +24 -5
- package/skills/notis-query/cli.md +1 -1
- package/src/cli.js +27 -2
- package/src/command-specs/apps.js +7 -29
- package/src/command-specs/auth.js +15 -20
- package/src/command-specs/index.js +3 -0
- package/src/command-specs/meta.js +29 -19
- package/src/command-specs/onboarding.js +122 -200
- package/src/command-specs/profile.js +358 -0
- package/src/runtime/app-platform.js +5 -5
- package/src/runtime/auth-recovery.js +100 -0
- package/src/runtime/oauth.js +111 -36
- package/src/runtime/profiles.js +395 -221
- package/src/runtime/transport.js +84 -29
- package/src/runtime/desktop-auth.js +0 -162
|
@@ -0,0 +1,358 @@
|
|
|
1
|
+
import { CliError, EXIT_CODES } from '../runtime/errors.js';
|
|
2
|
+
import { quoteShellArgument } from '../runtime/auth-recovery.js';
|
|
3
|
+
import {
|
|
4
|
+
DEFAULT_PROFILE,
|
|
5
|
+
getProfile,
|
|
6
|
+
listProfiles,
|
|
7
|
+
loadConfig,
|
|
8
|
+
normalizeConfig,
|
|
9
|
+
profileHasCredential,
|
|
10
|
+
profileExists,
|
|
11
|
+
resolveWorktreeRuntime,
|
|
12
|
+
updateConfig,
|
|
13
|
+
} from '../runtime/profiles.js';
|
|
14
|
+
|
|
15
|
+
function describeProfile(config, name, worktreeRuntime) {
|
|
16
|
+
const entry = listProfiles(config).find((candidate) => candidate.name === name);
|
|
17
|
+
if (worktreeRuntime?.profile === name) {
|
|
18
|
+
return {
|
|
19
|
+
name,
|
|
20
|
+
active: false,
|
|
21
|
+
api_base: worktreeRuntime.api_base,
|
|
22
|
+
label: './dev.sh worktree',
|
|
23
|
+
credential_kind: 'dev',
|
|
24
|
+
user_id: worktreeRuntime.expected_user_id || null,
|
|
25
|
+
authenticated: true,
|
|
26
|
+
dev_runtime_live: true,
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
if (!entry) return null;
|
|
30
|
+
return { ...entry, dev_runtime_live: false };
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function unknownProfileError(name, config) {
|
|
34
|
+
return new CliError({
|
|
35
|
+
code: 'profile_unknown',
|
|
36
|
+
message: `No CLI profile named "${name}"`,
|
|
37
|
+
exitCode: EXIT_CODES.usage,
|
|
38
|
+
details: { known_profiles: Object.keys(normalizeConfig(config).profiles) },
|
|
39
|
+
hints: [
|
|
40
|
+
{ command: 'notis profile list', reason: 'See which profiles this machine has' },
|
|
41
|
+
{ command: `notis login --profile ${quoteShellArgument(name)}`, reason: 'Authorize a new account under this name' },
|
|
42
|
+
],
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function currentWorktreeState() {
|
|
47
|
+
const resolved = resolveWorktreeRuntime();
|
|
48
|
+
return resolved?.unavailable
|
|
49
|
+
? { runtime: null, unavailable: resolved.unavailable }
|
|
50
|
+
: { runtime: resolved, unavailable: null };
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function unavailableForEffectiveRoute(ctx, unavailable) {
|
|
54
|
+
return ctx.runtime.profileSource === 'explicit' ? null : unavailable;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
async function listHandler(ctx) {
|
|
58
|
+
const config = loadConfig();
|
|
59
|
+
const { runtime: worktreeRuntime, unavailable: worktreeUnavailable } = currentWorktreeState();
|
|
60
|
+
const effectiveWorktreeUnavailable = unavailableForEffectiveRoute(ctx, worktreeUnavailable);
|
|
61
|
+
const profiles = listProfiles(config).map((entry) =>
|
|
62
|
+
describeProfile(config, entry.name, worktreeRuntime));
|
|
63
|
+
if (
|
|
64
|
+
worktreeRuntime
|
|
65
|
+
&& !profiles.some((entry) => entry.name === worktreeRuntime.profile)
|
|
66
|
+
) {
|
|
67
|
+
profiles.unshift(describeProfile(config, worktreeRuntime.profile, worktreeRuntime));
|
|
68
|
+
}
|
|
69
|
+
// In a hosted shell every profile reads as signed out while commands work
|
|
70
|
+
// fine, because NOTIS_JWT overrides all of them. Say so rather than letting
|
|
71
|
+
// an agent conclude it needs to authorize something.
|
|
72
|
+
const envOverride = ctx.runtime.credentialKind === 'env';
|
|
73
|
+
|
|
74
|
+
return ctx.output.emitSuccess({
|
|
75
|
+
command: ctx.spec.command_path.join(' '),
|
|
76
|
+
data: {
|
|
77
|
+
active_profile: normalizeConfig(config).current_profile,
|
|
78
|
+
effective_profile: effectiveWorktreeUnavailable ? null : ctx.runtime.profileName,
|
|
79
|
+
effective_profile_source: effectiveWorktreeUnavailable
|
|
80
|
+
? 'worktree-unavailable'
|
|
81
|
+
: ctx.runtime.profileSource,
|
|
82
|
+
worktree_runtime_unavailable: Boolean(effectiveWorktreeUnavailable),
|
|
83
|
+
effective_credential_kind: ctx.runtime.credentialKind || null,
|
|
84
|
+
env_credential_override: envOverride,
|
|
85
|
+
profiles,
|
|
86
|
+
},
|
|
87
|
+
humanSummary: effectiveWorktreeUnavailable
|
|
88
|
+
? `${profiles.length} stored CLI profile${profiles.length === 1 ? '' : 's'}; this local-only worktree is stopped`
|
|
89
|
+
: envOverride
|
|
90
|
+
? `NOTIS_JWT overrides all ${profiles.length} stored profile${profiles.length === 1 ? '' : 's'}`
|
|
91
|
+
: `${profiles.length} CLI profile${profiles.length === 1 ? '' : 's'} on this machine`,
|
|
92
|
+
renderHuman: () =>
|
|
93
|
+
[
|
|
94
|
+
...(envOverride
|
|
95
|
+
? ['NOTIS_JWT is set and takes precedence over every profile below.', '']
|
|
96
|
+
: []),
|
|
97
|
+
...profiles.map((entry) => {
|
|
98
|
+
const marker = !envOverride && entry.name === ctx.runtime.profileName ? '*' : ' ';
|
|
99
|
+
const auth = entry.authenticated ? entry.credential_kind : 'signed out';
|
|
100
|
+
const live = entry.dev_runtime_live ? ' (dev.sh running)' : '';
|
|
101
|
+
return `${marker} ${entry.name.padEnd(16)} ${String(entry.api_base).padEnd(32)} ${auth}${live}`;
|
|
102
|
+
}),
|
|
103
|
+
].join('\n'),
|
|
104
|
+
hints: [
|
|
105
|
+
...(effectiveWorktreeUnavailable
|
|
106
|
+
? [{
|
|
107
|
+
command: 'notis --profile <name> <command>',
|
|
108
|
+
reason: 'Explicitly escape the stopped local-only worktree for one command',
|
|
109
|
+
}]
|
|
110
|
+
: []),
|
|
111
|
+
{ command: 'notis profile use <name>', reason: 'Switch the default account outside this worktree' },
|
|
112
|
+
{ command: 'notis login --profile <name>', reason: 'Add another account without signing this one out' },
|
|
113
|
+
],
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
async function useHandler(ctx) {
|
|
118
|
+
const requested = ctx.args.name;
|
|
119
|
+
const config = loadConfig();
|
|
120
|
+
if (!profileExists(config, requested)) {
|
|
121
|
+
throw unknownProfileError(requested, config);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// Switching only moves the pointer. Every profile keeps its own credential so
|
|
125
|
+
// going back is another `profile use`, never another browser authorization.
|
|
126
|
+
const next = updateConfig((latest) => {
|
|
127
|
+
latest.current_profile = requested;
|
|
128
|
+
return latest;
|
|
129
|
+
});
|
|
130
|
+
const { runtime: worktreeRuntime, unavailable: worktreeUnavailable } = currentWorktreeState();
|
|
131
|
+
const profile = getProfile(next, requested);
|
|
132
|
+
const worktreeOverride = Boolean(worktreeRuntime);
|
|
133
|
+
const worktreeBlocked = Boolean(worktreeUnavailable);
|
|
134
|
+
const authenticated = profileHasCredential(profile);
|
|
135
|
+
|
|
136
|
+
return ctx.output.emitSuccess({
|
|
137
|
+
command: ctx.spec.command_path.join(' '),
|
|
138
|
+
data: {
|
|
139
|
+
active_profile: requested,
|
|
140
|
+
...describeProfile(next, requested, worktreeRuntime),
|
|
141
|
+
effective_profile: worktreeBlocked
|
|
142
|
+
? null
|
|
143
|
+
: worktreeOverride ? worktreeRuntime.profile : requested,
|
|
144
|
+
effective_profile_source: worktreeBlocked
|
|
145
|
+
? 'worktree-unavailable'
|
|
146
|
+
: worktreeOverride ? 'worktree' : 'current',
|
|
147
|
+
worktree_runtime_unavailable: worktreeBlocked,
|
|
148
|
+
},
|
|
149
|
+
humanSummary: worktreeBlocked
|
|
150
|
+
? `Saved profile "${requested}" as the default outside this worktree; this local-only checkout is stopped, so use --profile explicitly or restart ./dev.sh.`
|
|
151
|
+
: worktreeOverride
|
|
152
|
+
? `Saved profile "${requested}" as the default outside this worktree; this checkout still uses "${worktreeRuntime.profile}" unless --profile is explicit.`
|
|
153
|
+
: authenticated
|
|
154
|
+
? `Switched to profile "${requested}".`
|
|
155
|
+
: `Switched to profile "${requested}", which has no credential yet.`,
|
|
156
|
+
hints: worktreeBlocked
|
|
157
|
+
? [
|
|
158
|
+
{
|
|
159
|
+
command: `notis --profile ${quoteShellArgument(requested)} whoami`,
|
|
160
|
+
reason: 'Explicitly use and confirm this account while the local worktree is stopped',
|
|
161
|
+
},
|
|
162
|
+
{ message: 'Restart ./dev.sh to restore the worktree test identity.' },
|
|
163
|
+
]
|
|
164
|
+
: worktreeOverride
|
|
165
|
+
? [
|
|
166
|
+
{
|
|
167
|
+
command: `notis --profile ${quoteShellArgument(requested)} whoami`,
|
|
168
|
+
reason: 'Use and confirm this account explicitly inside the active worktree',
|
|
169
|
+
},
|
|
170
|
+
...(!authenticated
|
|
171
|
+
? [{
|
|
172
|
+
command: `notis login --profile ${quoteShellArgument(requested)}`,
|
|
173
|
+
reason: 'Authorize an account for this profile',
|
|
174
|
+
}]
|
|
175
|
+
: []),
|
|
176
|
+
]
|
|
177
|
+
: authenticated
|
|
178
|
+
? [{ command: 'notis whoami', reason: 'Confirm the account and API this profile targets' }]
|
|
179
|
+
: [{ command: `notis login --profile ${quoteShellArgument(requested)}`, reason: 'Authorize an account for this profile' }],
|
|
180
|
+
});
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
async function showHandler(ctx) {
|
|
184
|
+
const config = loadConfig();
|
|
185
|
+
const name = ctx.args.name || ctx.runtime.profileName;
|
|
186
|
+
const { runtime: worktreeRuntime, unavailable: worktreeUnavailable } = currentWorktreeState();
|
|
187
|
+
const effectiveWorktreeUnavailable = unavailableForEffectiveRoute(ctx, worktreeUnavailable);
|
|
188
|
+
const described = describeProfile(config, name, worktreeRuntime);
|
|
189
|
+
if (!described) {
|
|
190
|
+
throw unknownProfileError(name, config);
|
|
191
|
+
}
|
|
192
|
+
const profile = getProfile(config, name);
|
|
193
|
+
const envOverride = ctx.runtime.credentialKind === 'env';
|
|
194
|
+
|
|
195
|
+
return ctx.output.emitSuccess({
|
|
196
|
+
command: ctx.spec.command_path.join(' '),
|
|
197
|
+
data: {
|
|
198
|
+
...described,
|
|
199
|
+
env_credential_override: envOverride,
|
|
200
|
+
worktree_runtime_unavailable: Boolean(effectiveWorktreeUnavailable),
|
|
201
|
+
oauth_scopes: profile.oauth_scopes || [],
|
|
202
|
+
oauth_access_expires_at: profile.oauth_access_expires_at || null,
|
|
203
|
+
oauth_refresh_expires_at: profile.oauth_refresh_expires_at || null,
|
|
204
|
+
dev_workspace_root:
|
|
205
|
+
profile.dev_workspace_root || (
|
|
206
|
+
worktreeRuntime?.profile === name
|
|
207
|
+
? worktreeRuntime.workspace_root || null
|
|
208
|
+
: null
|
|
209
|
+
),
|
|
210
|
+
},
|
|
211
|
+
humanSummary: `Profile "${name}" targets ${described.api_base}`,
|
|
212
|
+
renderHuman: () =>
|
|
213
|
+
[
|
|
214
|
+
`Profile: ${name}`,
|
|
215
|
+
`API: ${described.api_base}`,
|
|
216
|
+
`User: ${described.user_id || 'unknown'}`,
|
|
217
|
+
`Credential:${described.credential_kind ? ` ${described.credential_kind}` : ' none'}`,
|
|
218
|
+
`Active: ${described.active ? 'yes' : 'no'}`,
|
|
219
|
+
...(envOverride
|
|
220
|
+
? ['', 'NOTIS_JWT is set and overrides this profile\'s credential.']
|
|
221
|
+
: []),
|
|
222
|
+
].join('\n'),
|
|
223
|
+
hints: effectiveWorktreeUnavailable
|
|
224
|
+
? [{
|
|
225
|
+
command: `notis --profile ${quoteShellArgument(name)} whoami`,
|
|
226
|
+
reason: 'Explicitly use this profile while the local-only worktree is stopped',
|
|
227
|
+
}]
|
|
228
|
+
: [],
|
|
229
|
+
});
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
async function removeHandler(ctx) {
|
|
233
|
+
const requested = ctx.args.name;
|
|
234
|
+
const config = loadConfig();
|
|
235
|
+
if (!profileExists(config, requested)) {
|
|
236
|
+
throw unknownProfileError(requested, config);
|
|
237
|
+
}
|
|
238
|
+
if (requested === DEFAULT_PROFILE) {
|
|
239
|
+
throw new CliError({
|
|
240
|
+
code: 'profile_not_removable',
|
|
241
|
+
message: 'The "default" profile cannot be removed',
|
|
242
|
+
exitCode: EXIT_CODES.usage,
|
|
243
|
+
hints: [{ command: 'notis logout', reason: 'Clear its credential instead of removing the profile' }],
|
|
244
|
+
});
|
|
245
|
+
}
|
|
246
|
+
if (profileHasCredential(getProfile(config, requested)) && !ctx.options.force) {
|
|
247
|
+
throw new CliError({
|
|
248
|
+
code: 'profile_still_authorized',
|
|
249
|
+
message: `Profile "${requested}" still holds a credential`,
|
|
250
|
+
exitCode: EXIT_CODES.usage,
|
|
251
|
+
hints: [
|
|
252
|
+
{
|
|
253
|
+
command: `notis logout --profile ${quoteShellArgument(requested)}`,
|
|
254
|
+
reason: 'Revoke the grant server-side before discarding it locally',
|
|
255
|
+
},
|
|
256
|
+
{ command: `notis profile remove ${quoteShellArgument(requested)} --force`, reason: 'Discard the local credential without revoking it' },
|
|
257
|
+
],
|
|
258
|
+
});
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
const next = updateConfig((latest) => {
|
|
262
|
+
delete latest.profiles[requested];
|
|
263
|
+
if (latest.current_profile === requested) {
|
|
264
|
+
latest.current_profile = DEFAULT_PROFILE;
|
|
265
|
+
}
|
|
266
|
+
return latest;
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
return ctx.output.emitSuccess({
|
|
270
|
+
command: ctx.spec.command_path.join(' '),
|
|
271
|
+
data: {
|
|
272
|
+
removed_profile: requested,
|
|
273
|
+
active_profile: normalizeConfig(next).current_profile,
|
|
274
|
+
},
|
|
275
|
+
humanSummary: `Removed profile "${requested}".`,
|
|
276
|
+
});
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
export const profileCommandSpecs = [
|
|
280
|
+
{
|
|
281
|
+
command_path: ['profile', 'list'],
|
|
282
|
+
summary: 'List every CLI profile with its account, API endpoint, and credential state.',
|
|
283
|
+
when_to_use:
|
|
284
|
+
'Use this to see which accounts and environments this machine can reach before choosing one.',
|
|
285
|
+
args_schema: { arguments: [], options: [] },
|
|
286
|
+
examples: ['notis profile list', 'notis profile list --json'],
|
|
287
|
+
output_schema:
|
|
288
|
+
'Returns active_profile, effective_profile, and a profiles array of {name, api_base, credential_kind, user_id, authenticated, dev_runtime_live}.',
|
|
289
|
+
mutates: false,
|
|
290
|
+
idempotent: true,
|
|
291
|
+
require_auth: false,
|
|
292
|
+
allow_unknown_profile: true,
|
|
293
|
+
related_commands: ['notis profile use', 'notis login', 'notis whoami'],
|
|
294
|
+
backend_call: { type: 'local_config' },
|
|
295
|
+
handler: listHandler,
|
|
296
|
+
},
|
|
297
|
+
{
|
|
298
|
+
command_path: ['profile', 'use'],
|
|
299
|
+
summary: 'Switch the default profile without signing any profile out.',
|
|
300
|
+
when_to_use:
|
|
301
|
+
'Use this to change which account and API subsequent commands target. Every other profile keeps its credential.',
|
|
302
|
+
args_schema: {
|
|
303
|
+
arguments: [{ token: '<name>', key: 'name', description: 'Profile to make active.' }],
|
|
304
|
+
options: [],
|
|
305
|
+
},
|
|
306
|
+
examples: ['notis profile use work', 'notis profile use default'],
|
|
307
|
+
output_schema: 'Returns the newly active profile with its api_base, user_id, and credential kind.',
|
|
308
|
+
mutates: true,
|
|
309
|
+
idempotent: true,
|
|
310
|
+
require_auth: false,
|
|
311
|
+
allow_unknown_profile: true,
|
|
312
|
+
related_commands: ['notis profile list', 'notis login'],
|
|
313
|
+
backend_call: { type: 'local_config' },
|
|
314
|
+
handler: useHandler,
|
|
315
|
+
},
|
|
316
|
+
{
|
|
317
|
+
command_path: ['profile', 'show'],
|
|
318
|
+
summary: 'Show one profile in detail, including scopes and credential expiry.',
|
|
319
|
+
when_to_use: 'Use this to inspect exactly which account and endpoint a profile resolves to.',
|
|
320
|
+
args_schema: {
|
|
321
|
+
arguments: [
|
|
322
|
+
{ token: '[name]', key: 'name', description: 'Profile to inspect; defaults to the active one.' },
|
|
323
|
+
],
|
|
324
|
+
options: [],
|
|
325
|
+
},
|
|
326
|
+
examples: ['notis profile show', 'notis profile show work --json'],
|
|
327
|
+
output_schema:
|
|
328
|
+
'Returns name, api_base, user_id, credential_kind, oauth scopes and expiries, and dev runtime state.',
|
|
329
|
+
mutates: false,
|
|
330
|
+
idempotent: true,
|
|
331
|
+
require_auth: false,
|
|
332
|
+
allow_unknown_profile: true,
|
|
333
|
+
related_commands: ['notis profile list', 'notis whoami'],
|
|
334
|
+
backend_call: { type: 'local_config' },
|
|
335
|
+
handler: showHandler,
|
|
336
|
+
},
|
|
337
|
+
{
|
|
338
|
+
command_path: ['profile', 'remove'],
|
|
339
|
+
summary: 'Delete a CLI profile from this machine.',
|
|
340
|
+
when_to_use:
|
|
341
|
+
'Use this after logging a profile out. Removing a still-authorized profile requires --force and leaves the grant live server-side.',
|
|
342
|
+
args_schema: {
|
|
343
|
+
arguments: [{ token: '<name>', key: 'name', description: 'Profile to delete.' }],
|
|
344
|
+
options: [
|
|
345
|
+
{ flags: '--force', description: 'Discard a profile that still holds a credential.' },
|
|
346
|
+
],
|
|
347
|
+
},
|
|
348
|
+
examples: ['notis profile remove old-work', 'notis profile remove old-work --force'],
|
|
349
|
+
output_schema: 'Returns removed_profile and the resulting active_profile.',
|
|
350
|
+
mutates: true,
|
|
351
|
+
idempotent: true,
|
|
352
|
+
require_auth: false,
|
|
353
|
+
allow_unknown_profile: true,
|
|
354
|
+
related_commands: ['notis logout', 'notis profile list'],
|
|
355
|
+
backend_call: { type: 'local_config' },
|
|
356
|
+
handler: removeHandler,
|
|
357
|
+
},
|
|
358
|
+
];
|
|
@@ -24,7 +24,7 @@ const BUNDLE_DIR = join(OUTPUT_DIR, 'bundle');
|
|
|
24
24
|
const MANIFEST_FILE = join(OUTPUT_DIR, 'manifest.json');
|
|
25
25
|
const METADATA_DIR = 'metadata';
|
|
26
26
|
const CLI_ROOT = resolve(dirname(fileURLToPath(import.meta.url)), '../..');
|
|
27
|
-
const
|
|
27
|
+
const MONOREPO_SCAFFOLDS_DIR = resolve(CLI_ROOT, '../..', 'scaffolds');
|
|
28
28
|
const DIST_DIR = join(CLI_ROOT, 'dist');
|
|
29
29
|
const SCAFFOLD_CATALOG_FILE = join(DIST_DIR, 'scaffolds.json');
|
|
30
30
|
const SCAFFOLD_SOURCE_DIR = join(DIST_DIR, 'scaffolds');
|
|
@@ -1075,7 +1075,7 @@ function resolveScaffoldSourceDir(fromSlug) {
|
|
|
1075
1075
|
if (existsSync(bundledDir)) {
|
|
1076
1076
|
return bundledDir;
|
|
1077
1077
|
}
|
|
1078
|
-
const monorepoDir = join(
|
|
1078
|
+
const monorepoDir = join(MONOREPO_SCAFFOLDS_DIR, fromSlug);
|
|
1079
1079
|
if (existsSync(join(monorepoDir, 'notis.config.ts'))) {
|
|
1080
1080
|
return monorepoDir;
|
|
1081
1081
|
}
|
|
@@ -1083,15 +1083,15 @@ function resolveScaffoldSourceDir(fromSlug) {
|
|
|
1083
1083
|
}
|
|
1084
1084
|
|
|
1085
1085
|
function loadMonorepoScaffoldCatalog() {
|
|
1086
|
-
if (!existsSync(
|
|
1086
|
+
if (!existsSync(MONOREPO_SCAFFOLDS_DIR)) {
|
|
1087
1087
|
return [];
|
|
1088
1088
|
}
|
|
1089
1089
|
const scaffolds = [];
|
|
1090
|
-
for (const entry of readdirSync(
|
|
1090
|
+
for (const entry of readdirSync(MONOREPO_SCAFFOLDS_DIR, { withFileTypes: true })) {
|
|
1091
1091
|
if (!entry.isDirectory() || entry.name.startsWith('.') || entry.name.startsWith('_')) {
|
|
1092
1092
|
continue;
|
|
1093
1093
|
}
|
|
1094
|
-
const configPath = join(
|
|
1094
|
+
const configPath = join(MONOREPO_SCAFFOLDS_DIR, entry.name, 'notis.config.ts');
|
|
1095
1095
|
if (!existsSync(configPath)) {
|
|
1096
1096
|
continue;
|
|
1097
1097
|
}
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import { CliError, EXIT_CODES } from './errors.js';
|
|
2
|
+
|
|
3
|
+
const CLI_NPX = 'npx --package @notis_ai/cli@latest -- notis';
|
|
4
|
+
|
|
5
|
+
export function quoteShellArgument(value) {
|
|
6
|
+
return `'${String(value).replace(/'/g, `'"'"'`)}'`;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
function profileSuffix(profileName) {
|
|
10
|
+
return profileName && profileName !== 'default'
|
|
11
|
+
? ` --profile ${quoteShellArgument(profileName)}`
|
|
12
|
+
: '';
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* How to get an unusable profile back to a working credential.
|
|
17
|
+
*
|
|
18
|
+
* `mode` distinguishes the two cases a caller has to act on differently:
|
|
19
|
+
* "expired" means the profile holds a grant the browser can renew, while
|
|
20
|
+
* "missing" means this profile has never been authorized at all.
|
|
21
|
+
*/
|
|
22
|
+
export function getAuthRecovery({ profileName } = {}, { mode = 'expired' } = {}) {
|
|
23
|
+
const suffix = profileSuffix(profileName);
|
|
24
|
+
const hints = [
|
|
25
|
+
{
|
|
26
|
+
command: `${CLI_NPX} login${suffix}`,
|
|
27
|
+
reason: mode === 'missing'
|
|
28
|
+
? 'Sign in or create an account in the browser and authorize this machine'
|
|
29
|
+
: 'Authorize a fresh scoped CLI credential for this profile',
|
|
30
|
+
},
|
|
31
|
+
{
|
|
32
|
+
command: `${CLI_NPX} profile list`,
|
|
33
|
+
reason: 'Check whether another profile on this machine is already signed in',
|
|
34
|
+
},
|
|
35
|
+
{
|
|
36
|
+
command: `${CLI_NPX} doctor${suffix}`,
|
|
37
|
+
reason: 'Retry the auth and API checks once authorization completes',
|
|
38
|
+
},
|
|
39
|
+
];
|
|
40
|
+
return { hints };
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function createExpiredAuthError(runtime) {
|
|
44
|
+
if (runtime?.credentialKind === 'worktree') {
|
|
45
|
+
return new CliError({
|
|
46
|
+
code: 'auth_expired',
|
|
47
|
+
message: `The ./dev.sh credential for profile "${runtime.profileName}" has expired`,
|
|
48
|
+
exitCode: EXIT_CODES.auth,
|
|
49
|
+
details: { credential_source: 'worktree' },
|
|
50
|
+
hints: [
|
|
51
|
+
{ message: 'Restart ./dev.sh in this worktree to mint a fresh dev credential.' },
|
|
52
|
+
{
|
|
53
|
+
command: `${CLI_NPX} profile list`,
|
|
54
|
+
reason: 'Switch to a live account profile instead of the dev one',
|
|
55
|
+
},
|
|
56
|
+
],
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
if (runtime?.credentialSource === 'env') {
|
|
60
|
+
return new CliError({
|
|
61
|
+
code: 'auth_expired',
|
|
62
|
+
message: 'NOTIS_JWT is expired',
|
|
63
|
+
exitCode: EXIT_CODES.auth,
|
|
64
|
+
details: { credential_source: 'env' },
|
|
65
|
+
hints: [
|
|
66
|
+
{
|
|
67
|
+
command: 'Set NOTIS_JWT to a fresh token',
|
|
68
|
+
reason: 'The explicit environment credential overrides every stored profile',
|
|
69
|
+
},
|
|
70
|
+
],
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
return new CliError({
|
|
75
|
+
code: 'auth_expired',
|
|
76
|
+
message: `Notis CLI authorization for profile "${runtime?.profileName || 'default'}" has expired`,
|
|
77
|
+
exitCode: EXIT_CODES.auth,
|
|
78
|
+
details: { credential_source: 'oauth' },
|
|
79
|
+
hints: getAuthRecovery(runtime).hints,
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export function createInvalidAuthHints(runtime) {
|
|
84
|
+
if (runtime?.credentialKind === 'worktree') {
|
|
85
|
+
return [
|
|
86
|
+
{
|
|
87
|
+
message: 'Restart ./dev.sh in this worktree to mint a fresh dev credential.',
|
|
88
|
+
},
|
|
89
|
+
];
|
|
90
|
+
}
|
|
91
|
+
if (runtime?.credentialSource === 'env') {
|
|
92
|
+
return [
|
|
93
|
+
{
|
|
94
|
+
command: 'Set NOTIS_JWT to a fresh token',
|
|
95
|
+
reason: 'The explicit environment credential was rejected',
|
|
96
|
+
},
|
|
97
|
+
];
|
|
98
|
+
}
|
|
99
|
+
return getAuthRecovery(runtime || {}).hints;
|
|
100
|
+
}
|