@notis_ai/cli 0.2.12 → 0.2.14
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +56 -3
- package/dist/scaffolds/notis-database/packages/sdk/src/config.ts +40 -2
- package/dist/scaffolds/notis-database/packages/sdk/src/documents.ts +21 -0
- package/dist/scaffolds/notis-database/packages/sdk/src/hooks/useCloudComputer.ts +97 -0
- package/dist/scaffolds/notis-database/packages/sdk/src/hooks/useDatabaseSubscription.ts +76 -0
- package/dist/scaffolds/notis-database/packages/sdk/src/hooks/useHandover.ts +75 -0
- package/dist/scaffolds/notis-database/packages/sdk/src/index.ts +17 -0
- package/dist/scaffolds/notis-database/packages/sdk/src/runtime.ts +132 -1
- package/dist/scaffolds/notis-journal/packages/sdk/src/config.ts +40 -2
- package/dist/scaffolds/notis-journal/packages/sdk/src/documents.ts +21 -0
- package/dist/scaffolds/notis-journal/packages/sdk/src/hooks/useCloudComputer.ts +97 -0
- package/dist/scaffolds/notis-journal/packages/sdk/src/hooks/useDatabaseSubscription.ts +76 -0
- package/dist/scaffolds/notis-journal/packages/sdk/src/hooks/useHandover.ts +75 -0
- package/dist/scaffolds/notis-journal/packages/sdk/src/index.ts +17 -0
- package/dist/scaffolds/notis-journal/packages/sdk/src/runtime.ts +132 -1
- package/dist/scaffolds/notis-journal/src/mock-runtime.ts +2 -0
- package/dist/scaffolds/notis-notes/packages/sdk/src/config.ts +40 -2
- package/dist/scaffolds/notis-notes/packages/sdk/src/documents.ts +21 -0
- package/dist/scaffolds/notis-notes/packages/sdk/src/hooks/useCloudComputer.ts +97 -0
- package/dist/scaffolds/notis-notes/packages/sdk/src/hooks/useDatabaseSubscription.ts +76 -0
- package/dist/scaffolds/notis-notes/packages/sdk/src/hooks/useHandover.ts +75 -0
- package/dist/scaffolds/notis-notes/packages/sdk/src/index.ts +17 -0
- package/dist/scaffolds/notis-notes/packages/sdk/src/runtime.ts +132 -1
- package/dist/scaffolds/notis-random/packages/sdk/src/config.ts +40 -2
- package/dist/scaffolds/notis-random/packages/sdk/src/documents.ts +21 -0
- package/dist/scaffolds/notis-random/packages/sdk/src/hooks/useCloudComputer.ts +97 -0
- package/dist/scaffolds/notis-random/packages/sdk/src/hooks/useDatabaseSubscription.ts +76 -0
- package/dist/scaffolds/notis-random/packages/sdk/src/hooks/useHandover.ts +75 -0
- package/dist/scaffolds/notis-random/packages/sdk/src/index.ts +17 -0
- package/dist/scaffolds/notis-random/packages/sdk/src/runtime.ts +132 -1
- package/package.json +1 -1
- package/skills/notis-apps/SKILL.md +11 -7
- package/skills/notis-apps/cli.md +8 -3
- package/skills/notis-cli/SKILL.md +2 -0
- package/src/cli.js +158 -0
- package/src/command-specs/apps.js +238 -50
- package/src/command-specs/handover.js +374 -0
- package/src/command-specs/index.js +3 -0
- package/src/command-specs/meta.js +53 -0
- package/src/command-specs/tools.js +6 -0
- package/src/runtime/app-dev-server.js +17 -8
- package/src/runtime/app-platform.js +218 -6
- package/src/runtime/auth-recovery.js +13 -3
- package/src/runtime/channel.js +133 -0
- package/src/runtime/delegated-context.js +68 -0
- package/src/runtime/git.js +233 -0
- package/src/runtime/oauth.js +36 -4
- package/src/runtime/profiles.js +17 -1
- package/src/runtime/transport.js +19 -2
- package/template/.harness/index.html.tmpl +116 -47
- package/template/packages/sdk/src/config.ts +52 -0
- package/template/packages/sdk/src/documents.ts +21 -0
- package/template/packages/sdk/src/hooks/useCloudComputer.ts +97 -0
- package/template/packages/sdk/src/hooks/useDatabaseSubscription.ts +76 -0
- package/template/packages/sdk/src/hooks/useHandover.ts +75 -0
- package/template/packages/sdk/src/index.ts +17 -0
- package/template/packages/sdk/src/runtime.ts +132 -1
- package/template/metadata/screenshot-1.png +0 -0
- package/template/metadata/screenshot-2.png +0 -0
- package/template/metadata/screenshot-3.png +0 -0
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useEffect, useRef, useState } from 'react';
|
|
4
|
+
import { useNotisRuntime } from '../provider';
|
|
5
|
+
import { useDocuments, type UseDocumentsOptions, type UseDocumentsResult } from './useDocuments';
|
|
6
|
+
import type { DocumentRecord } from '../runtime';
|
|
7
|
+
|
|
8
|
+
export interface UseDatabaseSubscriptionOptions extends UseDocumentsOptions {
|
|
9
|
+
/** Set to false to keep the query but skip the change feed. */
|
|
10
|
+
subscribe?: boolean;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export interface UseDatabaseSubscriptionResult extends UseDocumentsResult {
|
|
14
|
+
/** Alias of `documents`, for views that think in rows. */
|
|
15
|
+
rows: DocumentRecord[];
|
|
16
|
+
/** True while a live change feed is attached to this database. */
|
|
17
|
+
live: boolean;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Query a Notis database and keep it fresh without polling.
|
|
22
|
+
*
|
|
23
|
+
* ```tsx
|
|
24
|
+
* const { rows, live, refetch } = useDatabaseSubscription('workspaces');
|
|
25
|
+
* ```
|
|
26
|
+
*
|
|
27
|
+
* A change on the database wakes the hook, which then refetches through the
|
|
28
|
+
* usual `LOCAL_NOTIS_DATABASE_QUERY` path — the change feed is a signal only
|
|
29
|
+
* and never carries row data. Hosts without a change feed (the dev harness,
|
|
30
|
+
* the screenshot stub, the vite preview) still return rows; `live` is false
|
|
31
|
+
* there and the app should keep offering its manual refresh.
|
|
32
|
+
*/
|
|
33
|
+
export function useDatabaseSubscription(
|
|
34
|
+
databaseSlug: string,
|
|
35
|
+
options: UseDatabaseSubscriptionOptions = {},
|
|
36
|
+
): UseDatabaseSubscriptionResult {
|
|
37
|
+
const runtime = useNotisRuntime();
|
|
38
|
+
const { subscribe = true, ...documentOptions } = options;
|
|
39
|
+
const { documents, loading, error, refetch } = useDocuments(databaseSlug, documentOptions);
|
|
40
|
+
const [live, setLive] = useState(false);
|
|
41
|
+
|
|
42
|
+
const refetchRef = useRef(refetch);
|
|
43
|
+
useEffect(() => {
|
|
44
|
+
refetchRef.current = refetch;
|
|
45
|
+
}, [refetch]);
|
|
46
|
+
|
|
47
|
+
const enabled = options.enabled !== false && subscribe;
|
|
48
|
+
|
|
49
|
+
useEffect(() => {
|
|
50
|
+
if (!runtime?.subscribeDatabase || !enabled || !databaseSlug) {
|
|
51
|
+
setLive(false);
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
let cancelled = false;
|
|
56
|
+
const unsubscribe = runtime.subscribeDatabase(
|
|
57
|
+
databaseSlug,
|
|
58
|
+
() => {
|
|
59
|
+
refetchRef.current();
|
|
60
|
+
},
|
|
61
|
+
{
|
|
62
|
+
onStatusChange: (isLive) => {
|
|
63
|
+
if (!cancelled) setLive(isLive);
|
|
64
|
+
},
|
|
65
|
+
},
|
|
66
|
+
);
|
|
67
|
+
|
|
68
|
+
return () => {
|
|
69
|
+
cancelled = true;
|
|
70
|
+
setLive(false);
|
|
71
|
+
unsubscribe?.();
|
|
72
|
+
};
|
|
73
|
+
}, [runtime, databaseSlug, enabled]);
|
|
74
|
+
|
|
75
|
+
return { documents, rows: documents, loading, error, refetch, live };
|
|
76
|
+
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useCallback, useState } from 'react';
|
|
4
|
+
import { useNotisRuntime } from '../provider';
|
|
5
|
+
import type { HandoverPayload, HandoverResult } from '../runtime';
|
|
6
|
+
|
|
7
|
+
export interface UseHandoverResult {
|
|
8
|
+
/** Hand the work over. Rejects when the host has no manager chat. */
|
|
9
|
+
handover: (payload: HandoverPayload) => Promise<HandoverResult>;
|
|
10
|
+
/** True while the manager chat is being prepared. */
|
|
11
|
+
pending: boolean;
|
|
12
|
+
error: Error | null;
|
|
13
|
+
/**
|
|
14
|
+
* False when the host cannot hand work over (dev harness, vite preview).
|
|
15
|
+
* Render the app's own fallback — a copyable prompt, say — when it is false.
|
|
16
|
+
*/
|
|
17
|
+
available: boolean;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Hand a piece of work from app code to the Notis manager chat.
|
|
22
|
+
*
|
|
23
|
+
* An app displays work; the manager runs it. `handover` puts the message in
|
|
24
|
+
* the chat surface that already owns streaming progress, billing, cancellation
|
|
25
|
+
* and the transcript, and the app watches its own databases for the result.
|
|
26
|
+
*
|
|
27
|
+
* ```tsx
|
|
28
|
+
* const { handover, pending, available } = useHandover();
|
|
29
|
+
*
|
|
30
|
+
* return available ? (
|
|
31
|
+
* <Button
|
|
32
|
+
* disabled={pending}
|
|
33
|
+
* onClick={() => { void handover({ prompt: 'Create a workspace on notis to ...' }); }}
|
|
34
|
+
* >
|
|
35
|
+
* Send to Notis
|
|
36
|
+
* </Button>
|
|
37
|
+
* ) : (
|
|
38
|
+
* <CopyablePrompt prompt="Create a workspace on notis to ..." />
|
|
39
|
+
* );
|
|
40
|
+
* ```
|
|
41
|
+
*
|
|
42
|
+
* Pass `skill` to bind the work to a skill declared in `notis.config.ts`; the
|
|
43
|
+
* host rejects a key the app does not declare. `autoSend` is accepted for
|
|
44
|
+
* forward compatibility; today's hosts always return `drafted` and let the
|
|
45
|
+
* user press send.
|
|
46
|
+
*/
|
|
47
|
+
export function useHandover(): UseHandoverResult {
|
|
48
|
+
const runtime = useNotisRuntime();
|
|
49
|
+
const [pending, setPending] = useState(false);
|
|
50
|
+
const [error, setError] = useState<Error | null>(null);
|
|
51
|
+
|
|
52
|
+
const handover = useCallback(
|
|
53
|
+
async (payload: HandoverPayload): Promise<HandoverResult> => {
|
|
54
|
+
if (!runtime?.handover) {
|
|
55
|
+
throw new Error('This Notis host cannot hand work to the manager chat.');
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
setPending(true);
|
|
59
|
+
setError(null);
|
|
60
|
+
|
|
61
|
+
try {
|
|
62
|
+
return await runtime.handover(payload);
|
|
63
|
+
} catch (err) {
|
|
64
|
+
const e = err instanceof Error ? err : new Error(String(err));
|
|
65
|
+
setError(e);
|
|
66
|
+
throw e;
|
|
67
|
+
} finally {
|
|
68
|
+
setPending(false);
|
|
69
|
+
}
|
|
70
|
+
},
|
|
71
|
+
[runtime],
|
|
72
|
+
);
|
|
73
|
+
|
|
74
|
+
return { handover, pending, error, available: Boolean(runtime?.handover) };
|
|
75
|
+
}
|
|
@@ -13,6 +13,11 @@ export { NotisProvider, useNotisRuntime } from './provider';
|
|
|
13
13
|
export { useNotis } from './hooks/useNotis';
|
|
14
14
|
export { useDocuments } from './hooks/useDocuments';
|
|
15
15
|
export type { UseDocumentsOptions, UseDocumentsResult } from './hooks/useDocuments';
|
|
16
|
+
export { useDatabaseSubscription } from './hooks/useDatabaseSubscription';
|
|
17
|
+
export type {
|
|
18
|
+
UseDatabaseSubscriptionOptions,
|
|
19
|
+
UseDatabaseSubscriptionResult,
|
|
20
|
+
} from './hooks/useDatabaseSubscription';
|
|
16
21
|
export { useDocument } from './hooks/useDocument';
|
|
17
22
|
export type { UseDocumentOptions, UseDocumentResult } from './hooks/useDocument';
|
|
18
23
|
export { useUpsertDocument } from './hooks/useUpsertDocument';
|
|
@@ -22,6 +27,10 @@ export type { UseDatabaseSchemaResult } from './hooks/useDatabaseSchema';
|
|
|
22
27
|
export { useTool } from './hooks/useTool';
|
|
23
28
|
export type { ToolCallState, UseToolResult } from './hooks/useTool';
|
|
24
29
|
export { useTools } from './hooks/useTools';
|
|
30
|
+
export { useHandover } from './hooks/useHandover';
|
|
31
|
+
export type { UseHandoverResult } from './hooks/useHandover';
|
|
32
|
+
export { useCloudComputer } from './hooks/useCloudComputer';
|
|
33
|
+
export type { UseCloudComputerResult } from './hooks/useCloudComputer';
|
|
25
34
|
export { useNotisNavigation } from './hooks/useNotisNavigation';
|
|
26
35
|
export { useTopBarSearch } from './hooks/useTopBarSearch';
|
|
27
36
|
export { useBackend } from './hooks/useBackend';
|
|
@@ -37,6 +46,7 @@ export {
|
|
|
37
46
|
extractRichText,
|
|
38
47
|
getDocumentPreview,
|
|
39
48
|
getRelationIds,
|
|
49
|
+
getSecretValue,
|
|
40
50
|
isPresentString,
|
|
41
51
|
markdownToPlainText,
|
|
42
52
|
normalizeDatabaseProperty,
|
|
@@ -64,6 +74,9 @@ export type { MultiSelectDragOverlayProps } from './components/MultiSelectDragOv
|
|
|
64
74
|
// Types (re-exported for convenience)
|
|
65
75
|
export type {
|
|
66
76
|
AppDescriptor,
|
|
77
|
+
CloudComputerCliAuthFacts,
|
|
78
|
+
CloudComputerFacts,
|
|
79
|
+
CloudComputerSandboxFacts,
|
|
67
80
|
CollectionItem,
|
|
68
81
|
CollectionItemDetail,
|
|
69
82
|
DatabaseDescriptor,
|
|
@@ -72,12 +85,16 @@ export type {
|
|
|
72
85
|
DatabasePropertyType,
|
|
73
86
|
DocumentContentType,
|
|
74
87
|
DocumentRecord,
|
|
88
|
+
HandoverPayload,
|
|
89
|
+
HandoverResult,
|
|
75
90
|
NotisDocumentEditorProps,
|
|
76
91
|
NotisRuntime,
|
|
77
92
|
NotisRuntimeContext,
|
|
78
93
|
NotisRuntimeUI,
|
|
79
94
|
QueryFilter,
|
|
80
95
|
RouteDescriptor,
|
|
96
|
+
SecretPropertyValue,
|
|
97
|
+
SubscribeDatabaseOptions,
|
|
81
98
|
ToolDescriptor,
|
|
82
99
|
ToolInputSchema,
|
|
83
100
|
} from './runtime';
|
|
@@ -25,7 +25,21 @@ export type DatabasePropertyType =
|
|
|
25
25
|
| 'status'
|
|
26
26
|
| 'relation'
|
|
27
27
|
| 'formula'
|
|
28
|
-
| 'files'
|
|
28
|
+
| 'files'
|
|
29
|
+
| 'secret';
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* The value of a `secret` property. The platform stores a pointer to a
|
|
33
|
+
* credential held elsewhere and never the credential itself, so there is
|
|
34
|
+
* deliberately nothing here to read the secret material from — only whether
|
|
35
|
+
* one is attached, which credential it is, and its lifecycle state.
|
|
36
|
+
*/
|
|
37
|
+
export interface SecretPropertyValue {
|
|
38
|
+
present: boolean;
|
|
39
|
+
reference: string | null;
|
|
40
|
+
status: string | null;
|
|
41
|
+
metadata: Record<string, unknown> | null;
|
|
42
|
+
}
|
|
29
43
|
|
|
30
44
|
export interface DatabasePropertyOption {
|
|
31
45
|
id?: string | null;
|
|
@@ -188,6 +202,84 @@ export interface NotisRuntimeUI {
|
|
|
188
202
|
// NotisRuntime
|
|
189
203
|
// ---------------------------------------------------------------------------
|
|
190
204
|
|
|
205
|
+
/**
|
|
206
|
+
* Options for `NotisRuntime.subscribeDatabase`.
|
|
207
|
+
*/
|
|
208
|
+
export interface SubscribeDatabaseOptions {
|
|
209
|
+
/**
|
|
210
|
+
* Called with `true` once a live change feed is attached, and with `false`
|
|
211
|
+
* when it drops or is torn down. Hosts without a change feed (dev harness,
|
|
212
|
+
* screenshot stub, vite preview) never call it, so `live` stays false there.
|
|
213
|
+
*/
|
|
214
|
+
onStatusChange?: (live: boolean) => void;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
* Work an app hands to the Notis manager chat through `NotisRuntime.handover`.
|
|
219
|
+
*/
|
|
220
|
+
export interface HandoverPayload {
|
|
221
|
+
/** The message the manager should act on. */
|
|
222
|
+
prompt: string;
|
|
223
|
+
/**
|
|
224
|
+
* Key of a skill declared in `notis.config.ts` -> `skills[].key`. The host
|
|
225
|
+
* rejects a key this app does not declare. Omit to hand over plain work.
|
|
226
|
+
*/
|
|
227
|
+
skill?: string;
|
|
228
|
+
/**
|
|
229
|
+
* Accepted for forward compatibility. The portal never submits a composer on
|
|
230
|
+
* the user's behalf today, so every handover resolves `drafted`; `sent` is
|
|
231
|
+
* reserved for a host that can genuinely dispatch the run.
|
|
232
|
+
*/
|
|
233
|
+
autoSend?: boolean;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
export interface HandoverResult {
|
|
237
|
+
/** `drafted` when the user still has to press send, `sent` when it went straight through. */
|
|
238
|
+
status: 'drafted' | 'sent';
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
/**
|
|
242
|
+
* The user's cloud computer, as far as an app may see it.
|
|
243
|
+
*
|
|
244
|
+
* `exists` is false when the user has never had a sandbox provisioned. A
|
|
245
|
+
* `status` of anything other than `'running'` means the VM is asleep; reading
|
|
246
|
+
* these facts never wakes it.
|
|
247
|
+
*/
|
|
248
|
+
export interface CloudComputerSandboxFacts {
|
|
249
|
+
exists: boolean;
|
|
250
|
+
status: string | null;
|
|
251
|
+
provider: string | null;
|
|
252
|
+
created_at: string | null;
|
|
253
|
+
updated_at: string | null;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
/**
|
|
257
|
+
* Whether a CLI inside the cloud computer is signed in.
|
|
258
|
+
*
|
|
259
|
+
* `authenticated: null` means *unknown*, never *signed out*: the sandbox was
|
|
260
|
+
* not running, or the probe could not answer. `reason` says which
|
|
261
|
+
* (`'sandbox_not_running'`, `'no_sandbox'`, `'sandbox_status_unknown'`,
|
|
262
|
+
* `'probe_failed'`, `'not_signed_in'`).
|
|
263
|
+
*/
|
|
264
|
+
export interface CloudComputerCliAuthFacts {
|
|
265
|
+
authenticated: boolean | null;
|
|
266
|
+
account: string | null;
|
|
267
|
+
checked_at: string | null;
|
|
268
|
+
reason: string | null;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
export interface CloudComputerFacts {
|
|
272
|
+
/**
|
|
273
|
+
* False when this host cannot answer at all — no cloud computer on the user's
|
|
274
|
+
* plan, or the platform could not resolve the facts. Render whatever the app
|
|
275
|
+
* did before rather than an error.
|
|
276
|
+
*/
|
|
277
|
+
available: boolean;
|
|
278
|
+
reason?: string | null;
|
|
279
|
+
sandbox: CloudComputerSandboxFacts | null;
|
|
280
|
+
cli_auth: { gh: CloudComputerCliAuthFacts };
|
|
281
|
+
}
|
|
282
|
+
|
|
191
283
|
export interface NotisRuntime {
|
|
192
284
|
app: AppDescriptor;
|
|
193
285
|
route: RouteDescriptor;
|
|
@@ -195,6 +287,45 @@ export interface NotisRuntime {
|
|
|
195
287
|
context: NotisRuntimeContext;
|
|
196
288
|
ui?: NotisRuntimeUI;
|
|
197
289
|
|
|
290
|
+
/**
|
|
291
|
+
* Subscribe to changes on an app-owned database. Returns an unsubscribe.
|
|
292
|
+
*
|
|
293
|
+
* The change notification is only a signal — it carries no rows. Consumers
|
|
294
|
+
* react by refetching through the normal tool path, so app scoping,
|
|
295
|
+
* permissions and billing are unchanged. Use the `useDatabaseSubscription`
|
|
296
|
+
* hook rather than calling this directly.
|
|
297
|
+
*/
|
|
298
|
+
subscribeDatabase?(
|
|
299
|
+
slug: string,
|
|
300
|
+
onChange: () => void,
|
|
301
|
+
options?: SubscribeDatabaseOptions,
|
|
302
|
+
): () => void;
|
|
303
|
+
|
|
304
|
+
/**
|
|
305
|
+
* Hand a piece of work to the Notis manager chat. The app cannot run an
|
|
306
|
+
* agent itself: it describes the job, and the manager surface owns progress,
|
|
307
|
+
* billing, cancellation and the transcript. Results come back to the app
|
|
308
|
+
* through its own databases (see `useDatabaseSubscription`).
|
|
309
|
+
*
|
|
310
|
+
* Use the `useHandover` hook rather than calling this directly. Hosts
|
|
311
|
+
* without a manager chat (the dev harness, the vite preview) leave it
|
|
312
|
+
* undefined, so keep whatever fallback the app already offers.
|
|
313
|
+
*/
|
|
314
|
+
handover?(payload: HandoverPayload): Promise<HandoverResult>;
|
|
315
|
+
|
|
316
|
+
/**
|
|
317
|
+
* Read-only facts about the user's cloud computer. Requires
|
|
318
|
+
* `capabilities.cloudComputer: 'read'` in `notis.config.ts` plus the user's
|
|
319
|
+
* approval; resolving it never creates, resumes or commands a sandbox.
|
|
320
|
+
*
|
|
321
|
+
* Use the `useCloudComputer` hook rather than calling this directly. Hosts
|
|
322
|
+
* without a cloud computer (the dev harness, the vite preview) answer
|
|
323
|
+
* `{ available: false }`, so keep whatever fallback the app already has.
|
|
324
|
+
* `{ refresh: true }` bypasses the host's short answer cache — the hook's
|
|
325
|
+
* refresh() sends it so a just-completed sign-in becomes visible.
|
|
326
|
+
*/
|
|
327
|
+
cloudComputerFacts?(options?: { refresh?: boolean }): Promise<CloudComputerFacts>;
|
|
328
|
+
|
|
198
329
|
navigate?: (payload: { kind: string; [key: string]: unknown }) => void;
|
|
199
330
|
|
|
200
331
|
registerTopBarSearch?: (
|
package/package.json
CHANGED
|
@@ -31,7 +31,8 @@ All Notis apps are built using the Notis CLI, either locally in a repo workspace
|
|
|
31
31
|
- Before installing, inspect the listing's `required_capabilities`. Explain each
|
|
32
32
|
requested capability and obtain explicit approval; only then pass the matching
|
|
33
33
|
token in `approved_capabilities`. Never infer capability approval. The current
|
|
34
|
-
workspace-wide read token is `workspace_databases_read
|
|
34
|
+
workspace-wide read token is `workspace_databases_read`; the read-only
|
|
35
|
+
cloud computer token is `cloud_computer_read`.
|
|
35
36
|
|
|
36
37
|
## Architecture
|
|
37
38
|
|
|
@@ -76,7 +77,7 @@ App code never accesses the runtime directly -- it uses SDK hooks (`useTool`, `u
|
|
|
76
77
|
3. **Component rendering** -- Apps render as React components directly in the portal. No iframes.
|
|
77
78
|
The portal owns the `ShadowRoot`, theme tokens, and runtime provider.
|
|
78
79
|
4. **HTTP bridge** -- Runtime calls use fetch to `/portal_views/runtime_query`
|
|
79
|
-
5. **Declarative tools** -- Tool access declared in `notis.config.ts
|
|
80
|
+
5. **Declarative tools** -- Tool access is declared in `notis.config.ts` by the final names returned by tool discovery and enforced server-side. Views can call native Notis, connected integrations, PostForMe, and MCP tools directly; metered calls use the same credit-cap and usage-billing path as the CLI.
|
|
80
81
|
6. **shadcn + Notis theme** -- Apps must use shadcn components with the live Notis theme provided by the portal
|
|
81
82
|
7. **Phosphor icons only** -- Always `phosphor:` prefix. Never emojis.
|
|
82
83
|
8. **Database refs only** -- `notis.config.ts` references existing databases by slug. The schema source of truth lives in the `databases` table, not in the manifest. Every native database is owned by exactly one app (`databases.owner_app_id`): creating one through `LOCAL_NOTIS_DATABASE_UPSERT_DATABASE` requires the owning app's slug or id in the `app` argument, install/dev materialization stamps ownership automatically, and deleting an app deletes its databases and their documents.
|
|
@@ -124,9 +125,9 @@ These are the most common mistakes agents make. Each one wastes time and produce
|
|
|
124
125
|
2. **Pull only installed apps.** If the user explicitly wants to fork an app they already installed, run `npx --package @notis_ai/cli@latest -- notis apps list`, then `npx --package @notis_ai/cli@latest -- notis apps pull <app-id> ./<dir>`. To fork a Store app that is not installed, tell the user to install it from `/store` first.
|
|
125
126
|
3. **Edit the listing source.** Update `name` (slug), `title`, description, icon, accent, author, categories, tagline, databases, routes, and tools in `notis.config.ts`. Declare a database as a string for schema-only Store packaging; use `{ slug: 'templates', seedDocuments: true }` only when its rows are deliberate starter content for every installer. Keep the complete Store release history in the root `CHANGELOG.md`, newest entry first, using `## [Release title] - YYYY-MM-DD` (or `{PR_MERGE_DATE}` before publication). The first entry powers **What’s New** and the same file powers **Version History**. `icon` is a `phosphor:<name>` value or `metadata/icon.png`; when unset the app shows its **two-letter initials** everywhere (store, sidebar, app details). `accent` optionally pins the avatar color to one of `blue|violet|emerald|amber|rose|sky|fuchsia|teal` (default derived from the app id). Icon/accent flow through deploy onto the app row + listing and can also be set later via the `update_app` tool.
|
|
126
127
|
4. **Build pages in `app/`.** Reuse scaffold code wherever it fits.
|
|
127
|
-
5. **Iterate live.** Run `npx --package @notis_ai/cli@latest -- notis apps dev` so the target desktop's **Local development** sidebar group discovers the app and renders the local bundle. Keep this command running for as long as the user is testing; stopping it removes the temporary Local development entry. Read the command's `Target desktop` line instead of guessing between Notis, Notis Beta, or a source-workspace desktop.
|
|
128
|
-
6. **Capture listing screenshots.** Declare 3–6 screenshots in `notis.config.ts`, each with a stable `path`, descriptive `alt`, and optional `route`/`scenario`/`focus`/`theme`, then run `npx --package @notis_ai/cli@latest -- notis apps screenshot`. Use `focus` to frame a real app root without empty browser canvas; use `theme: 'light'` or `theme: 'dark'` to match both the Portal render and Store backdrop, and pair both modes when that best represents the app. It renders the configured states in a headless harness and writes exact 2000x1250 PNGs under `metadata/`, using the deterministic Store presentation by default (`--raw` is diagnostic only). Apps are icon-led like Raycast — the icon set in `notis.config.ts` represents the app, so there is no cover image, only these screenshots. Never hand-author the PNGs; regenerate them when routes or UI change.
|
|
129
|
-
7. **Verify locally.** Run `npm install`, then `npx --package @notis_ai/cli@latest -- notis apps build` and `npx --package @notis_ai/cli@latest -- notis apps verify`. Surface the verify report and fix failures.
|
|
128
|
+
5. **Iterate live.** Run `npx --package @notis_ai/cli@latest -- notis apps dev` so the target desktop's **Local development** sidebar group discovers the app and renders the local bundle. Keep this command running for as long as the user is testing; stopping it removes the temporary Local development entry. Read the command's `Target desktop` line instead of guessing between Notis, Notis Beta, or a source-workspace desktop. Add `--live-data` to point the session at the installed app's real databases instead of its own empty dev copies -- it applies to that session only, and warns and falls back when the app has not been deployed yet.
|
|
129
|
+
6. **Capture listing screenshots.** Declare 3–6 screenshots in `notis.config.ts`, each with a stable `path`, descriptive `alt`, and optional `route`/`scenario`/`focus`/`theme`, then run `npx --package @notis_ai/cli@latest -- notis apps screenshot`. Use `focus` to frame a real app root without empty browser canvas; use `theme: 'light'` or `theme: 'dark'` to match both the Portal render and Store backdrop, and pair both modes when that best represents the app. It renders the configured states in a headless harness and writes exact 2000x1250 PNGs under `metadata/`, using the deterministic Store presentation by default (`--raw` is diagnostic only). Apps are icon-led like Raycast — the icon set in `notis.config.ts` represents the app, so there is no cover image, only these screenshots. Never hand-author the PNGs; regenerate them when routes or UI change. A `scenario` names an entry in `metadata/screenshot-fixtures.json`; besides `actions` it may carry its own `tools` and `requests`, shallow-merged per key over the file-level ones for that capture, which is how the same route is shown both populated and in its first-run empty state.
|
|
130
|
+
7. **Verify locally.** Run `npm install`, then `npx --package @notis_ai/cli@latest -- notis apps build` and `npx --package @notis_ai/cli@latest -- notis apps verify`. Surface the verify report and fix failures. Incomplete listing media is only a `Store readiness:` warning there; run `notis apps verify --listing` before publish to make it a failure.
|
|
130
131
|
8. **Local-development-first handoff — STOP HERE.** Keep `apps dev` running and hand off to the user: tell them the app is live in the target desktop's **Local development** sidebar group (green `DEV` badge) and ask them to test it there. Building a new app to this point, without deploying, is a **complete and expected** result. Do NOT proceed to `apps create` / `apps deploy` yet — wait for the user to test and explicitly ask to deploy. (`apps dev` is what puts the app in Local development; without a running session the app never appears there.) **Before handing off, complete all three acceptance checks:**
|
|
131
132
|
1. Target: capture the CLI's `Target desktop: <name>` line and make sure that exact desktop app is running and signed in.
|
|
132
133
|
2. Bundle: the reported loopback `/snapshot` URL responds successfully and contains the expected manifest/routes.
|
|
@@ -194,7 +195,7 @@ Create `notis.config.ts` with:
|
|
|
194
195
|
- **name** -- Display name
|
|
195
196
|
- **databases** -- Slug references to existing Notis databases
|
|
196
197
|
- **routes** -- Route-first sidebar entries with explicit `slug`, optional `parentSlug`, and optional `collection.sidebar` tree config
|
|
197
|
-
- **tools** --
|
|
198
|
+
- **tools** -- Final tool names the app can call at runtime. Use the shared discovery flow (`COMPOSIO_SEARCH_TOOLS`, then `COMPOSIO_GET_TOOL_SCHEMAS`) while building the app, and copy the returned final names into this list. Examples include `LOCAL_NOTIS_DATABASE_QUERY`, `LOCAL_NOTIS_MONID_RUN`, `GMAIL_SEND_EMAIL`, `LOCAL_POSTFORME_CREATE_POST`, and `LOCAL_MCP_<SERVER>_<TOOL>`. App code calls each declared name directly through `useTool`; it does not wrap provider or MCP calls in `COMPOSIO_MULTI_EXECUTE_TOOL`. Access stays scoped to the signed-in user's own connections, native database tools stay scoped to the app's databases unless `capabilities.workspaceDatabases: 'read'` is granted, and metered tools use the CLI-equivalent credit-cap and fail-closed usage-billing path.
|
|
198
199
|
|
|
199
200
|
For collection-backed sidebars, use the route schema directly:
|
|
200
201
|
|
|
@@ -436,6 +437,9 @@ All hooks are imported from `@notis/sdk`:
|
|
|
436
437
|
| `useNotisNavigation()` | `() => { toRoute, toDocument, toApp }` | Navigate between routes, documents, or the app root |
|
|
437
438
|
| `useTopBarSearch(opts)` | `({ value, onChange, placeholder?, onSubmit? }) => { setLoading }` | Bind the current view to the Portal-owned top-bar search input |
|
|
438
439
|
| `useBackend()` | `() => { request }` | Raw backend request proxy with JWT auth |
|
|
440
|
+
| `useDatabaseSubscription(slug, opts?)` | `(slug: string, opts?) => { rows, documents, loading, error, refetch, live }` | Query a database and refetch it when its rows change. `live` is false on hosts without a change feed (dev harness, vite preview) -- keep a manual refresh for those |
|
|
441
|
+
| `useHandover()` | `() => { handover, pending, error, available }` | Hand a prompt (optionally bound to a declared skill) to the Notis manager chat, which owns progress, billing and cancellation. `available` is false on hosts with no chat -- fall back to a copyable prompt |
|
|
442
|
+
| `useCloudComputer()` | `() => { facts, loading, error, refresh }` | Read-only cloud computer facts: sandbox existence/status and whether the GitHub CLI is signed in. Requires `capabilities.cloudComputer: 'read'` plus the user's approval; `facts.available === false` means answer from the app's own fallback |
|
|
439
443
|
|
|
440
444
|
### Typed tool calls
|
|
441
445
|
|
|
@@ -487,7 +491,7 @@ This uploads the bundle and editable source snapshot directly to Supabase storag
|
|
|
487
491
|
|
|
488
492
|
### Headless harness verification
|
|
489
493
|
|
|
490
|
-
Run `npx --package @notis_ai/cli@latest -- notis apps verify` after `npx --package @notis_ai/cli@latest -- notis apps build`. Use `--mode live` after deploy to exercise the real `/portal_views/runtime_query` with the CLI JWT instead of stub data. If `agent-browser` is unavailable, pass `--no-browser` to print URLs and use `--keep-open` for interactive triage with `notis-browser-control`.
|
|
494
|
+
Run `npx --package @notis_ai/cli@latest -- notis apps verify` after `npx --package @notis_ai/cli@latest -- notis apps build`. Use `--mode live` after deploy to exercise the real `/portal_views/runtime_query` with the CLI JWT instead of stub data; live mode also fails a route whose runtime calls all errored, which a well-behaved error state would otherwise hide. If `agent-browser` is unavailable, pass `--no-browser` to print URLs and use `--keep-open` for interactive triage with `notis-browser-control`.
|
|
491
495
|
|
|
492
496
|
#### What the harness catches that `npx --package @notis_ai/cli@latest -- notis apps build` does not
|
|
493
497
|
|
package/skills/notis-apps/cli.md
CHANGED
|
@@ -119,11 +119,14 @@ When to use: Run this inside a single app or a monorepo root with apps/<name>/no
|
|
|
119
119
|
Options:
|
|
120
120
|
- `--port <number>` — Local bundle server port (default: 5173).
|
|
121
121
|
- `--no-open` — Do not auto-open the desktop Portal local development app.
|
|
122
|
+
- `--live-data` — Read and write the installed app's real databases instead of empty dev copies. Applies to this session only; warns and falls back when the app is not installed yet.
|
|
123
|
+
- `--grant-cloud-shell` — Approve a cloudComputer: 'shell' declaration without the interactive prompt. The grant persists for this dev app; authorship alone never grants it.
|
|
122
124
|
|
|
123
125
|
Examples:
|
|
124
126
|
- `npx --package @notis_ai/cli@latest -- notis apps dev`
|
|
125
127
|
- `npx --package @notis_ai/cli@latest -- notis apps dev ./my-app`
|
|
126
128
|
- `npx --package @notis_ai/cli@latest -- notis apps dev ./workspace --port 5200`
|
|
129
|
+
- `npx --package @notis_ai/cli@latest -- notis apps dev --live-data # iterate on a view over the installed app's real rows`
|
|
127
130
|
|
|
128
131
|
### `npx --package @notis_ai/cli@latest -- notis apps build [dir]`
|
|
129
132
|
|
|
@@ -137,15 +140,16 @@ Examples:
|
|
|
137
140
|
|
|
138
141
|
### `npx --package @notis_ai/cli@latest -- notis apps verify [dir]`
|
|
139
142
|
|
|
140
|
-
Validate every route and
|
|
143
|
+
Validate that every route renders and reports Store listing readiness.
|
|
141
144
|
|
|
142
|
-
When to use:
|
|
145
|
+
When to use: Any time after notis apps build, and before deploy. Catches render-time crashes and missing runtime calls. Incomplete listing media is reported as a warning; pass --listing to fail on it instead.
|
|
143
146
|
|
|
144
147
|
Options:
|
|
145
148
|
- `--routes <slugs>` — Comma-separated route slugs. Default: every route in manifest.
|
|
146
149
|
- `--port <n>` — Loopback port. Default: auto-pick.
|
|
147
150
|
- `--skip-build` — Skip notis apps build; reuse existing .notis/output/.
|
|
148
|
-
- `--mode <mode>` — stub | live. Default stub. Live posts to /portal_views/runtime_query with the CLI JWT.
|
|
151
|
+
- `--mode <mode>` — stub | live. Default stub. Live posts to /portal_views/runtime_query with the CLI JWT and fails routes whose runtime calls all errored.
|
|
152
|
+
- `--listing` — Fail instead of warn when the Store listing (tagline, categories, screenshots, changelog) is incomplete.
|
|
149
153
|
- `--no-browser` — Start the harness server and print URLs; do not drive agent-browser.
|
|
150
154
|
- `--keep-open` — Leave server + browser session running after report (for manual triage).
|
|
151
155
|
|
|
@@ -153,6 +157,7 @@ Examples:
|
|
|
153
157
|
- `npx --package @notis_ai/cli@latest -- notis apps verify`
|
|
154
158
|
- `npx --package @notis_ai/cli@latest -- notis apps verify --routes notes`
|
|
155
159
|
- `npx --package @notis_ai/cli@latest -- notis apps verify --mode live`
|
|
160
|
+
- `npx --package @notis_ai/cli@latest -- notis apps verify --listing # gate on Store listing readiness before publish`
|
|
156
161
|
- `npx --package @notis_ai/cli@latest -- notis apps verify --no-browser # start the harness, drive agent-browser yourself`
|
|
157
162
|
|
|
158
163
|
### `npx --package @notis_ai/cli@latest -- notis apps screenshot [dir]`
|
|
@@ -31,6 +31,8 @@ Use the registry-resolved published npm package everywhere:
|
|
|
31
31
|
|
|
32
32
|
Always use this NPX command form so the agent runs the current published CLI. In hosted shells, the CLI is pre-authenticated through `NOTIS_JWT`. On a local machine the CLI holds its own OAuth grant: `notis login` authorizes one in the browser, and signing in to the Notis desktop app authorizes one automatically for that account. Either way the grant belongs to the CLI, which refreshes it without the desktop app running.
|
|
33
33
|
|
|
34
|
+
`@latest` is correct for every account, including beta ones. Each deployment reports which published build belongs to it, `notis login` pins that on the profile, and a later run that finds itself on the wrong build hands the invocation to the right one before doing anything. Never substitute a channel by hand: pinning `@beta` on a production profile is how a machine ends up running a build its API does not expect. `notis doctor` reports the active channel, and `NOTIS_CLI_AUTO_CHANNEL=0` turns the hand-off off for a run.
|
|
35
|
+
|
|
34
36
|
This `notis-cli` skill is delivered through normal Notis skill sync for the signed-in user, alongside other curated skills.
|
|
35
37
|
|
|
36
38
|
## Profiles: accounts and endpoints
|
package/src/cli.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { spawnSync } from 'node:child_process';
|
|
1
2
|
import { readFileSync } from 'node:fs';
|
|
2
3
|
import { dirname, join } from 'node:path';
|
|
3
4
|
import { fileURLToPath } from 'node:url';
|
|
@@ -6,10 +7,17 @@ import { COMMAND_SPECS, GROUP_SUMMARIES } from './command-specs/index.js';
|
|
|
6
7
|
import { OutputManager } from './runtime/output.js';
|
|
7
8
|
import { asCliError } from './runtime/errors.js';
|
|
8
9
|
import { reportCliCommand } from './runtime/telemetry.js';
|
|
10
|
+
import {
|
|
11
|
+
CHANNEL_SWITCH_ENV,
|
|
12
|
+
resolveChannelSwitch,
|
|
13
|
+
} from './runtime/channel.js';
|
|
9
14
|
import {
|
|
10
15
|
DEFAULT_PROFILE,
|
|
16
|
+
getProfile,
|
|
17
|
+
loadConfig,
|
|
11
18
|
resolveOutputMode,
|
|
12
19
|
resolveRuntimeProfile,
|
|
20
|
+
resolveWorktreeRuntime,
|
|
13
21
|
workspacePath,
|
|
14
22
|
} from './runtime/profiles.js';
|
|
15
23
|
|
|
@@ -232,7 +240,157 @@ export function createProgram() {
|
|
|
232
240
|
return program;
|
|
233
241
|
}
|
|
234
242
|
|
|
243
|
+
/**
|
|
244
|
+
* Read the two global flags that decide which build should serve this run.
|
|
245
|
+
*
|
|
246
|
+
* Commander cannot help here: the decision has to be made before the program
|
|
247
|
+
* parses, because the answer may be to hand the whole invocation to a
|
|
248
|
+
* different process. Only `--profile` and `--api-base` matter, and both are
|
|
249
|
+
* plain `--flag value` pairs.
|
|
250
|
+
*/
|
|
251
|
+
export function readChannelRelevantFlags(args = []) {
|
|
252
|
+
const flags = {};
|
|
253
|
+
for (const [index, token] of args.entries()) {
|
|
254
|
+
for (const [flag, key] of [['--profile', 'profile'], ['--api-base', 'apiBase']]) {
|
|
255
|
+
if (token === flag) flags[key] = args[index + 1];
|
|
256
|
+
else if (token.startsWith(`${flag}=`)) flags[key] = token.slice(flag.length + 1);
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
return flags;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
export function channelProfileForArgs(
|
|
263
|
+
args = [],
|
|
264
|
+
env = process.env,
|
|
265
|
+
{ config: suppliedConfig, worktreeRuntime: suppliedWorktreeRuntime } = {},
|
|
266
|
+
) {
|
|
267
|
+
// An explicit endpoint overrides the stored profile for this run, so it also
|
|
268
|
+
// decides the build: `--api-base https://api-beta.notis.ai` on a production
|
|
269
|
+
// profile is a deliberate one-off beta call.
|
|
270
|
+
const { profile: profileName, apiBase } = readChannelRelevantFlags(args);
|
|
271
|
+
if (apiBase) {
|
|
272
|
+
return { api_base: apiBase };
|
|
273
|
+
}
|
|
274
|
+
if (env.NOTIS_API_BASE) {
|
|
275
|
+
return { api_base: env.NOTIS_API_BASE };
|
|
276
|
+
}
|
|
277
|
+
const config = suppliedConfig || loadConfig();
|
|
278
|
+
const explicitProfile = profileName || env.NOTIS_PROFILE;
|
|
279
|
+
if (explicitProfile) {
|
|
280
|
+
return getProfile(config, explicitProfile);
|
|
281
|
+
}
|
|
282
|
+
const resolvedWorktree = suppliedWorktreeRuntime === undefined
|
|
283
|
+
? resolveWorktreeRuntime()
|
|
284
|
+
: suppliedWorktreeRuntime;
|
|
285
|
+
// A stopped local-only worktree must reach the normal routing error on the
|
|
286
|
+
// current build. Switching based on an unrelated shared profile would escape
|
|
287
|
+
// the worktree boundary before that fail-closed check runs.
|
|
288
|
+
if (resolvedWorktree?.unavailable) {
|
|
289
|
+
return {};
|
|
290
|
+
}
|
|
291
|
+
if (resolvedWorktree?.profile) {
|
|
292
|
+
return resolvedWorktree;
|
|
293
|
+
}
|
|
294
|
+
// NOTIS_JWT replaces the credential, not the route. Unless NOTIS_API_BASE
|
|
295
|
+
// was explicit above, the selected/current profile still owns the channel.
|
|
296
|
+
return getProfile(config, config.current_profile || DEFAULT_PROFILE);
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
function interruptedExitCode(signal) {
|
|
300
|
+
if (signal === 'SIGINT') return 130;
|
|
301
|
+
if (signal === 'SIGTERM') return 143;
|
|
302
|
+
return 1;
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
export function executeChannelSwitch(
|
|
306
|
+
decision,
|
|
307
|
+
args,
|
|
308
|
+
env,
|
|
309
|
+
{ spawn = spawnSync } = {},
|
|
310
|
+
) {
|
|
311
|
+
const childEnv = { ...env, [CHANNEL_SWITCH_ENV]: '1' };
|
|
312
|
+
// First make npm resolve and boot the target package without executing the
|
|
313
|
+
// requested command. That distinguishes an unavailable tag/network from a
|
|
314
|
+
// legitimate non-zero exit of the handed-off CLI, which must be propagated
|
|
315
|
+
// rather than retried locally after a possible mutation.
|
|
316
|
+
const probe = spawn(decision.command, [...decision.args, '--version'], {
|
|
317
|
+
stdio: 'ignore',
|
|
318
|
+
env: childEnv,
|
|
319
|
+
});
|
|
320
|
+
if (probe.signal) {
|
|
321
|
+
return {
|
|
322
|
+
...decision,
|
|
323
|
+
exitCode: interruptedExitCode(probe.signal),
|
|
324
|
+
reason: 'switch_interrupted',
|
|
325
|
+
};
|
|
326
|
+
}
|
|
327
|
+
if (probe.error || probe.status !== 0) {
|
|
328
|
+
return { ...decision, switch: false, reason: 'switch_unavailable' };
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
const result = spawn(decision.command, [...decision.args, ...args], {
|
|
332
|
+
stdio: 'inherit',
|
|
333
|
+
env: childEnv,
|
|
334
|
+
});
|
|
335
|
+
if (result.signal) {
|
|
336
|
+
return {
|
|
337
|
+
...decision,
|
|
338
|
+
exitCode: interruptedExitCode(result.signal),
|
|
339
|
+
reason: 'switch_interrupted',
|
|
340
|
+
};
|
|
341
|
+
}
|
|
342
|
+
if (result.error || typeof result.status !== 'number') {
|
|
343
|
+
return { ...decision, switch: false, reason: 'switch_failed' };
|
|
344
|
+
}
|
|
345
|
+
return { ...decision, exitCode: result.status };
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
/**
|
|
349
|
+
* Hand this invocation to the published build the profile is pinned to.
|
|
350
|
+
*
|
|
351
|
+
* Returns the decision rather than exiting so tests can assert on it. Any
|
|
352
|
+
* failure to reach the other build is non-fatal: running the wrong channel is
|
|
353
|
+
* a much smaller problem than refusing to run at all.
|
|
354
|
+
*/
|
|
355
|
+
export function switchChannelIfNeeded(
|
|
356
|
+
argv = process.argv,
|
|
357
|
+
env = process.env,
|
|
358
|
+
{ spawn = spawnSync, platform = process.platform, moduleDirectory } = {},
|
|
359
|
+
) {
|
|
360
|
+
const args = argv.slice(2);
|
|
361
|
+
let profile;
|
|
362
|
+
try {
|
|
363
|
+
profile = channelProfileForArgs(args, env);
|
|
364
|
+
} catch {
|
|
365
|
+
return { switch: false, reason: 'profile_unreadable' };
|
|
366
|
+
}
|
|
367
|
+
const decision = resolveChannelSwitch({
|
|
368
|
+
runningVersion: CLI_VERSION,
|
|
369
|
+
profile,
|
|
370
|
+
moduleDirectory: moduleDirectory || dirname(fileURLToPath(import.meta.url)),
|
|
371
|
+
env,
|
|
372
|
+
platform,
|
|
373
|
+
});
|
|
374
|
+
if (!decision.switch) {
|
|
375
|
+
return decision;
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
const result = executeChannelSwitch(decision, args, env, { spawn });
|
|
379
|
+
if (!result.switch) {
|
|
380
|
+
process.stderr.write(
|
|
381
|
+
`Notis CLI could not start the ${decision.targetChannel} build for this profile; `
|
|
382
|
+
+ `continuing on ${decision.runningChannel}.\n`,
|
|
383
|
+
);
|
|
384
|
+
}
|
|
385
|
+
return result;
|
|
386
|
+
}
|
|
387
|
+
|
|
235
388
|
export async function run(argv = process.argv) {
|
|
389
|
+
const switched = switchChannelIfNeeded(argv);
|
|
390
|
+
if (switched.switch) {
|
|
391
|
+
process.exitCode = switched.exitCode;
|
|
392
|
+
return;
|
|
393
|
+
}
|
|
236
394
|
const program = createProgram();
|
|
237
395
|
await program.parseAsync(argv);
|
|
238
396
|
}
|