@notis_ai/cli 0.2.0-beta.102.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +334 -0
- package/bin/notis.js +2 -0
- package/config/notis_app_boundary_rules.json +50 -0
- package/dist/scaffolds/notis-database/CHANGELOG.md +5 -0
- package/dist/scaffolds/notis-database/app/globals.css +44 -0
- package/dist/scaffolds/notis-database/app/layout.tsx +6 -0
- package/dist/scaffolds/notis-database/app/page.tsx +1091 -0
- package/dist/scaffolds/notis-database/components/ui/badge.tsx +28 -0
- package/dist/scaffolds/notis-database/components/ui/button.tsx +53 -0
- package/dist/scaffolds/notis-database/components/ui/card.tsx +56 -0
- package/dist/scaffolds/notis-database/components/ui/table.tsx +120 -0
- package/dist/scaffolds/notis-database/components.json +20 -0
- package/dist/scaffolds/notis-database/index.html +12 -0
- package/dist/scaffolds/notis-database/lib/types.ts +134 -0
- package/dist/scaffolds/notis-database/lib/utils.ts +6 -0
- package/dist/scaffolds/notis-database/metadata/screenshot-1.png +0 -0
- package/dist/scaffolds/notis-database/notis.config.ts +26 -0
- package/dist/scaffolds/notis-database/package.json +31 -0
- package/dist/scaffolds/notis-database/postcss.config.mjs +8 -0
- package/dist/scaffolds/notis-database/src/dev-main.tsx +23 -0
- package/dist/scaffolds/notis-database/src/mock-runtime.ts +558 -0
- package/dist/scaffolds/notis-database/tailwind.config.ts +59 -0
- package/dist/scaffolds/notis-database/tsconfig.json +23 -0
- package/dist/scaffolds/notis-database/vite.config.ts +22 -0
- package/dist/scaffolds/notis-journal/CHANGELOG.md +25 -0
- package/dist/scaffolds/notis-journal/app/globals.css +37 -0
- package/dist/scaffolds/notis-journal/app/insights/page.tsx +513 -0
- package/dist/scaffolds/notis-journal/app/journal-core.tsx +362 -0
- package/dist/scaffolds/notis-journal/app/journal-ui.tsx +337 -0
- package/dist/scaffolds/notis-journal/app/layout.tsx +6 -0
- package/dist/scaffolds/notis-journal/app/page.tsx +485 -0
- package/dist/scaffolds/notis-journal/components/ui/badge.tsx +28 -0
- package/dist/scaffolds/notis-journal/components/ui/button.tsx +53 -0
- package/dist/scaffolds/notis-journal/components/ui/card.tsx +56 -0
- package/dist/scaffolds/notis-journal/components.json +20 -0
- package/dist/scaffolds/notis-journal/index.html +12 -0
- package/dist/scaffolds/notis-journal/lib/utils.ts +6 -0
- package/dist/scaffolds/notis-journal/metadata/screenshot-1.png +0 -0
- package/dist/scaffolds/notis-journal/metadata/screenshot-2.png +0 -0
- package/dist/scaffolds/notis-journal/metadata/screenshot-3.png +0 -0
- package/dist/scaffolds/notis-journal/metadata/screenshot-4.png +0 -0
- package/dist/scaffolds/notis-journal/metadata/screenshot-5.png +0 -0
- package/dist/scaffolds/notis-journal/metadata/screenshot-6.png +0 -0
- package/dist/scaffolds/notis-journal/metadata/screenshot-fixtures.json +132 -0
- package/dist/scaffolds/notis-journal/notis.config.ts +93 -0
- package/dist/scaffolds/notis-journal/package.json +34 -0
- package/dist/scaffolds/notis-journal/packages/sdk/package.json +36 -0
- package/dist/scaffolds/notis-journal/packages/sdk/src/components/DocumentEditor.tsx +93 -0
- package/dist/scaffolds/notis-journal/packages/sdk/src/components/Markdown.tsx +60 -0
- package/dist/scaffolds/notis-journal/packages/sdk/src/components/MultiSelectActionBar.tsx +278 -0
- package/dist/scaffolds/notis-journal/packages/sdk/src/components/MultiSelectCheckbox.tsx +91 -0
- package/dist/scaffolds/notis-journal/packages/sdk/src/components/MultiSelectDragOverlay.tsx +39 -0
- package/dist/scaffolds/notis-journal/packages/sdk/src/config.ts +145 -0
- package/dist/scaffolds/notis-journal/packages/sdk/src/documents.ts +229 -0
- package/dist/scaffolds/notis-journal/packages/sdk/src/hooks/useBackend.ts +41 -0
- package/dist/scaffolds/notis-journal/packages/sdk/src/hooks/useDatabaseSchema.ts +85 -0
- package/dist/scaffolds/notis-journal/packages/sdk/src/hooks/useDocument.ts +78 -0
- package/dist/scaffolds/notis-journal/packages/sdk/src/hooks/useDocuments.ts +121 -0
- package/dist/scaffolds/notis-journal/packages/sdk/src/hooks/useMultiSelect.ts +539 -0
- package/dist/scaffolds/notis-journal/packages/sdk/src/hooks/useNotis.ts +34 -0
- package/dist/scaffolds/notis-journal/packages/sdk/src/hooks/useNotisNavigation.ts +49 -0
- package/dist/scaffolds/notis-journal/packages/sdk/src/hooks/useTool.ts +64 -0
- package/dist/scaffolds/notis-journal/packages/sdk/src/hooks/useTools.ts +56 -0
- package/dist/scaffolds/notis-journal/packages/sdk/src/hooks/useTopBarSearch.ts +73 -0
- package/dist/scaffolds/notis-journal/packages/sdk/src/hooks/useUpsertDocument.ts +95 -0
- package/dist/scaffolds/notis-journal/packages/sdk/src/index.ts +83 -0
- package/dist/scaffolds/notis-journal/packages/sdk/src/provider.tsx +43 -0
- package/dist/scaffolds/notis-journal/packages/sdk/src/runtime.ts +220 -0
- package/dist/scaffolds/notis-journal/packages/sdk/src/styles.css +186 -0
- package/dist/scaffolds/notis-journal/packages/sdk/src/ui.ts +15 -0
- package/dist/scaffolds/notis-journal/packages/sdk/src/vite.ts +56 -0
- package/dist/scaffolds/notis-journal/packages/sdk/tsconfig.json +15 -0
- package/dist/scaffolds/notis-journal/postcss.config.mjs +8 -0
- package/dist/scaffolds/notis-journal/skills/journal-onboarding/SKILL.md +120 -0
- package/dist/scaffolds/notis-journal/src/dev-main.tsx +58 -0
- package/dist/scaffolds/notis-journal/src/mock-runtime.ts +197 -0
- package/dist/scaffolds/notis-journal/tailwind.config.ts +58 -0
- package/dist/scaffolds/notis-journal/tsconfig.json +23 -0
- package/dist/scaffolds/notis-journal/vite.config.ts +10 -0
- package/dist/scaffolds/notis-notes/CHANGELOG.md +5 -0
- package/dist/scaffolds/notis-notes/app/globals.css +3 -0
- package/dist/scaffolds/notis-notes/app/layout.tsx +6 -0
- package/dist/scaffolds/notis-notes/app/page.tsx +1950 -0
- package/dist/scaffolds/notis-notes/components/ui/badge.tsx +28 -0
- package/dist/scaffolds/notis-notes/components/ui/button.tsx +53 -0
- package/dist/scaffolds/notis-notes/components/ui/card.tsx +56 -0
- package/dist/scaffolds/notis-notes/components.json +20 -0
- package/dist/scaffolds/notis-notes/lib/utils.ts +6 -0
- package/dist/scaffolds/notis-notes/metadata/screenshot-1.png +0 -0
- package/dist/scaffolds/notis-notes/notis.config.ts +35 -0
- package/dist/scaffolds/notis-notes/package.json +31 -0
- package/dist/scaffolds/notis-notes/postcss.config.mjs +8 -0
- package/dist/scaffolds/notis-notes/tailwind.config.ts +58 -0
- package/dist/scaffolds/notis-notes/tsconfig.json +23 -0
- package/dist/scaffolds/notis-notes/vite.config.ts +10 -0
- package/dist/scaffolds/notis-random/CHANGELOG.md +5 -0
- package/dist/scaffolds/notis-random/README.md +33 -0
- package/dist/scaffolds/notis-random/app/globals.css +11 -0
- package/dist/scaffolds/notis-random/app/history/page.tsx +66 -0
- package/dist/scaffolds/notis-random/app/layout.tsx +7 -0
- package/dist/scaffolds/notis-random/app/page.tsx +222 -0
- package/dist/scaffolds/notis-random/index.html +12 -0
- package/dist/scaffolds/notis-random/lib/notis-tools.ts +109 -0
- package/dist/scaffolds/notis-random/lib/rng.ts +42 -0
- package/dist/scaffolds/notis-random/lib/roll-record.ts +102 -0
- package/dist/scaffolds/notis-random/lib/utils.ts +25 -0
- package/dist/scaffolds/notis-random/metadata/screenshot-1.png +0 -0
- package/dist/scaffolds/notis-random/metadata/screenshot-2.png +0 -0
- package/dist/scaffolds/notis-random/notis.config.ts +42 -0
- package/dist/scaffolds/notis-random/package.json +32 -0
- package/dist/scaffolds/notis-random/postcss.config.mjs +6 -0
- package/dist/scaffolds/notis-random/src/dev-main.tsx +70 -0
- package/dist/scaffolds/notis-random/src/mock-runtime.ts +129 -0
- package/dist/scaffolds/notis-random/tailwind.config.ts +43 -0
- package/dist/scaffolds/notis-random/tsconfig.json +23 -0
- package/dist/scaffolds/notis-random/vite.config.ts +11 -0
- package/dist/scaffolds.json +47 -0
- package/package.json +45 -0
- package/skills/notis-apps/SKILL.md +219 -0
- package/skills/notis-apps/cli.md +244 -0
- package/skills/notis-cli/SKILL.md +269 -0
- package/skills/notis-query/cli.md +39 -0
- package/src/cli.js +175 -0
- package/src/command-specs/apps.js +1939 -0
- package/src/command-specs/helpers.js +186 -0
- package/src/command-specs/index.js +14 -0
- package/src/command-specs/meta.js +161 -0
- package/src/command-specs/tools.js +499 -0
- package/src/runtime/agent-browser.js +464 -0
- package/src/runtime/app-boundary-validator.js +183 -0
- package/src/runtime/app-changelog.js +79 -0
- package/src/runtime/app-dev-server.js +712 -0
- package/src/runtime/app-dev-sessions.js +251 -0
- package/src/runtime/app-platform.js +1500 -0
- package/src/runtime/assets/store-screenshot-dark.png +0 -0
- package/src/runtime/cli-mode.generated.js +4 -0
- package/src/runtime/cli-mode.js +29 -0
- package/src/runtime/desktop-auth.js +93 -0
- package/src/runtime/errors.js +55 -0
- package/src/runtime/help.js +60 -0
- package/src/runtime/output.js +180 -0
- package/src/runtime/ports.js +15 -0
- package/src/runtime/profiles.js +261 -0
- package/src/runtime/store-screenshot.js +138 -0
- package/src/runtime/transport.js +347 -0
- package/template/.harness/index.html.tmpl +333 -0
- package/template/CHANGELOG.md +5 -0
- package/template/app/globals.css +3 -0
- package/template/app/layout.tsx +6 -0
- package/template/app/page.tsx +60 -0
- package/template/components/ui/badge.tsx +28 -0
- package/template/components/ui/button.tsx +53 -0
- package/template/components/ui/card.tsx +56 -0
- package/template/components.json +20 -0
- package/template/lib/utils.ts +6 -0
- package/template/metadata/screenshot-1.png +0 -0
- package/template/metadata/screenshot-2.png +0 -0
- package/template/metadata/screenshot-3.png +0 -0
- package/template/notis.config.ts +36 -0
- package/template/package-lock.json +4137 -0
- package/template/package.json +32 -0
- package/template/packages/sdk/package.json +36 -0
- package/template/packages/sdk/src/components/DocumentEditor.tsx +93 -0
- package/template/packages/sdk/src/components/Markdown.tsx +60 -0
- package/template/packages/sdk/src/components/MultiSelectActionBar.tsx +278 -0
- package/template/packages/sdk/src/components/MultiSelectCheckbox.tsx +91 -0
- package/template/packages/sdk/src/components/MultiSelectDragOverlay.tsx +39 -0
- package/template/packages/sdk/src/config.ts +145 -0
- package/template/packages/sdk/src/documents.ts +229 -0
- package/template/packages/sdk/src/hooks/useBackend.ts +41 -0
- package/template/packages/sdk/src/hooks/useDatabaseSchema.ts +85 -0
- package/template/packages/sdk/src/hooks/useDocument.ts +78 -0
- package/template/packages/sdk/src/hooks/useDocuments.ts +121 -0
- package/template/packages/sdk/src/hooks/useMultiSelect.ts +539 -0
- package/template/packages/sdk/src/hooks/useNotis.ts +34 -0
- package/template/packages/sdk/src/hooks/useNotisNavigation.ts +49 -0
- package/template/packages/sdk/src/hooks/useTool.ts +64 -0
- package/template/packages/sdk/src/hooks/useTools.ts +56 -0
- package/template/packages/sdk/src/hooks/useTopBarSearch.ts +73 -0
- package/template/packages/sdk/src/hooks/useUpsertDocument.ts +95 -0
- package/template/packages/sdk/src/index.ts +83 -0
- package/template/packages/sdk/src/provider.tsx +43 -0
- package/template/packages/sdk/src/runtime.ts +220 -0
- package/template/packages/sdk/src/styles.css +186 -0
- package/template/packages/sdk/src/ui.ts +15 -0
- package/template/packages/sdk/src/vite.ts +56 -0
- package/template/packages/sdk/tsconfig.json +15 -0
- package/template/postcss.config.mjs +8 -0
- package/template/tailwind.config.ts +58 -0
- package/template/tsconfig.json +22 -0
- package/template/vite.config.ts +10 -0
|
Binary file
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CLI mode detection for `notis apps dev`.
|
|
3
|
+
*
|
|
4
|
+
* The repo-local CLI package targets localhost; the published npm CLI package
|
|
5
|
+
* targets app.notis.ai. Mode is baked into `cli-mode.generated.js` at publish
|
|
6
|
+
* time — no runtime flag is needed (and none is exposed to end users, so a
|
|
7
|
+
* crafted env var can't trick a published CLI into opening a localhost portal
|
|
8
|
+
* that isn't running).
|
|
9
|
+
*
|
|
10
|
+
* The `NOTIS_CLI_MODE` env var is honored for internal testing only.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { MODE as BAKED_MODE } from './cli-mode.generated.js';
|
|
14
|
+
|
|
15
|
+
export function getCliMode() {
|
|
16
|
+
const override = process.env.NOTIS_CLI_MODE;
|
|
17
|
+
if (override === 'local' || override === 'published') {
|
|
18
|
+
return override;
|
|
19
|
+
}
|
|
20
|
+
return BAKED_MODE === 'published' ? 'published' : 'local';
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function getDefaultApiBase(mode = getCliMode()) {
|
|
24
|
+
return mode === 'local' ? 'http://localhost:3001' : 'https://api.notis.ai';
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function getDefaultPortalOrigin(mode = getCliMode()) {
|
|
28
|
+
return mode === 'local' ? 'http://localhost:3000' : 'https://app.notis.ai';
|
|
29
|
+
}
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import { CliError, EXIT_CODES } from './errors.js';
|
|
2
|
+
|
|
3
|
+
function isPidRunning(pid) {
|
|
4
|
+
if (!Number.isInteger(pid) || pid <= 0) {
|
|
5
|
+
return false;
|
|
6
|
+
}
|
|
7
|
+
try {
|
|
8
|
+
process.kill(pid, 0);
|
|
9
|
+
return true;
|
|
10
|
+
} catch (error) {
|
|
11
|
+
return error?.code === 'EPERM';
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function quoteShellArgument(value) {
|
|
16
|
+
return `'${String(value).replace(/'/g, `'"'"'`)}'`;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function defaultDesktopAppName(apiBase) {
|
|
20
|
+
try {
|
|
21
|
+
return new URL(apiBase).hostname === 'api-beta.notis.ai' ? 'Notis Beta' : 'Notis';
|
|
22
|
+
} catch {
|
|
23
|
+
return 'Notis';
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function getDesktopAuthRecovery(runtime) {
|
|
28
|
+
const desktopRunning = isPidRunning(runtime.desktopPid);
|
|
29
|
+
const appName = runtime.desktopAppName || defaultDesktopAppName(runtime.apiBase);
|
|
30
|
+
let command = 'Start the Notis desktop app';
|
|
31
|
+
if (process.platform === 'darwin') {
|
|
32
|
+
command = `open -a ${quoteShellArgument(appName)}`;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
return {
|
|
36
|
+
desktopRunning,
|
|
37
|
+
appName,
|
|
38
|
+
hints: [
|
|
39
|
+
{
|
|
40
|
+
command,
|
|
41
|
+
reason: desktopRunning
|
|
42
|
+
? `Bring ${appName} forward so it can renew CLI authentication, then retry`
|
|
43
|
+
: `Start ${appName} to renew expired CLI authentication, then retry`,
|
|
44
|
+
},
|
|
45
|
+
{
|
|
46
|
+
command: 'notis doctor',
|
|
47
|
+
reason: 'Retry the auth and API checks after the desktop app is ready',
|
|
48
|
+
},
|
|
49
|
+
],
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function createExpiredAuthError(runtime) {
|
|
54
|
+
if (runtime.credentialSource === 'env') {
|
|
55
|
+
return new CliError({
|
|
56
|
+
code: 'auth_expired',
|
|
57
|
+
message: 'NOTIS_JWT is expired',
|
|
58
|
+
exitCode: EXIT_CODES.auth,
|
|
59
|
+
details: { credential_source: 'env' },
|
|
60
|
+
hints: [
|
|
61
|
+
{
|
|
62
|
+
command: 'Set NOTIS_JWT to a fresh token',
|
|
63
|
+
reason: 'The explicit environment credential overrides desktop-managed auth',
|
|
64
|
+
},
|
|
65
|
+
],
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const recovery = getDesktopAuthRecovery(runtime);
|
|
70
|
+
return new CliError({
|
|
71
|
+
code: 'auth_expired',
|
|
72
|
+
message: 'Notis CLI authentication has expired',
|
|
73
|
+
exitCode: EXIT_CODES.auth,
|
|
74
|
+
details: {
|
|
75
|
+
credential_source: 'desktop',
|
|
76
|
+
desktop_running: recovery.desktopRunning,
|
|
77
|
+
desktop_app_name: recovery.appName,
|
|
78
|
+
},
|
|
79
|
+
hints: recovery.hints,
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export function createInvalidAuthHints(runtime) {
|
|
84
|
+
if (runtime?.credentialSource === 'env') {
|
|
85
|
+
return [
|
|
86
|
+
{
|
|
87
|
+
command: 'Set NOTIS_JWT to a fresh token',
|
|
88
|
+
reason: 'The explicit environment credential was rejected',
|
|
89
|
+
},
|
|
90
|
+
];
|
|
91
|
+
}
|
|
92
|
+
return getDesktopAuthRecovery(runtime || {}).hints;
|
|
93
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
export const EXIT_CODES = {
|
|
2
|
+
ok: 0,
|
|
3
|
+
usage: 2,
|
|
4
|
+
auth: 3,
|
|
5
|
+
network: 4,
|
|
6
|
+
conflict: 5,
|
|
7
|
+
backend: 6,
|
|
8
|
+
unexpected: 7,
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
export class CliError extends Error {
|
|
12
|
+
constructor({
|
|
13
|
+
code,
|
|
14
|
+
message,
|
|
15
|
+
exitCode = EXIT_CODES.unexpected,
|
|
16
|
+
retryable = false,
|
|
17
|
+
details = {},
|
|
18
|
+
hints = [],
|
|
19
|
+
warnings = [],
|
|
20
|
+
cause,
|
|
21
|
+
}) {
|
|
22
|
+
super(message);
|
|
23
|
+
this.name = 'CliError';
|
|
24
|
+
this.code = code;
|
|
25
|
+
this.exitCode = exitCode;
|
|
26
|
+
this.retryable = retryable;
|
|
27
|
+
this.details = details;
|
|
28
|
+
this.hints = hints;
|
|
29
|
+
this.warnings = warnings;
|
|
30
|
+
this.cause = cause;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function asCliError(error) {
|
|
35
|
+
if (error instanceof CliError) {
|
|
36
|
+
return error;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
return new CliError({
|
|
40
|
+
code: 'unexpected_error',
|
|
41
|
+
message: error instanceof Error ? error.message : String(error),
|
|
42
|
+
exitCode: EXIT_CODES.unexpected,
|
|
43
|
+
cause: error,
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function usageError(message, details = {}) {
|
|
48
|
+
return new CliError({
|
|
49
|
+
code: 'usage_error',
|
|
50
|
+
message,
|
|
51
|
+
details,
|
|
52
|
+
exitCode: EXIT_CODES.usage,
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { usageError } from './errors.js';
|
|
2
|
+
|
|
3
|
+
export function canonicalCommandName(spec) {
|
|
4
|
+
return spec.display_name || spec.command_path.join(' ');
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export function formatDescribe(spec) {
|
|
8
|
+
const lines = [
|
|
9
|
+
`${canonicalCommandName(spec)}`,
|
|
10
|
+
'',
|
|
11
|
+
spec.summary,
|
|
12
|
+
'',
|
|
13
|
+
'When to use:',
|
|
14
|
+
` ${spec.when_to_use}`,
|
|
15
|
+
'',
|
|
16
|
+
`Mutates state: ${spec.mutates ? 'yes' : 'no'}`,
|
|
17
|
+
`Idempotent: ${spec.idempotent ? 'yes' : 'no'}`,
|
|
18
|
+
'',
|
|
19
|
+
'Arguments and options:',
|
|
20
|
+
];
|
|
21
|
+
|
|
22
|
+
const args = spec.args_schema?.arguments || [];
|
|
23
|
+
const options = spec.args_schema?.options || [];
|
|
24
|
+
if (!args.length && !options.length) {
|
|
25
|
+
lines.push(' None');
|
|
26
|
+
} else {
|
|
27
|
+
for (const arg of args) {
|
|
28
|
+
lines.push(` ${arg.token} ${arg.description}`);
|
|
29
|
+
}
|
|
30
|
+
for (const option of options) {
|
|
31
|
+
lines.push(` ${option.flags} ${option.description}`);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
lines.push('', 'Examples:');
|
|
36
|
+
for (const example of spec.examples || []) {
|
|
37
|
+
lines.push(` ${example}`);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
lines.push('', 'Output shape:', ` ${spec.output_schema}`);
|
|
41
|
+
lines.push('', 'Backend call:', ` ${JSON.stringify(spec.backend_call)}`);
|
|
42
|
+
|
|
43
|
+
if (spec.related_commands?.length) {
|
|
44
|
+
lines.push('', 'Related commands:');
|
|
45
|
+
for (const related of spec.related_commands) {
|
|
46
|
+
lines.push(` ${related}`);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
return lines.join('\n');
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function findCommandSpec(specs, inputPath) {
|
|
54
|
+
const normalized = inputPath.join(' ').trim().toLowerCase();
|
|
55
|
+
const spec = specs.find((entry) => entry.command_path.join(' ').toLowerCase() === normalized);
|
|
56
|
+
if (!spec) {
|
|
57
|
+
throw usageError(`Unknown command path: ${inputPath.join(' ')}`);
|
|
58
|
+
}
|
|
59
|
+
return spec;
|
|
60
|
+
}
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
import { inspect } from 'node:util';
|
|
2
|
+
import { EXIT_CODES } from './errors.js';
|
|
3
|
+
|
|
4
|
+
function pad(value, width) {
|
|
5
|
+
return String(value).padEnd(width);
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
function formatTable(rows, columns) {
|
|
9
|
+
if (!rows.length) {
|
|
10
|
+
return '';
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const widths = columns.map((column) => {
|
|
14
|
+
const headerWidth = column.label.length;
|
|
15
|
+
const valueWidth = Math.max(...rows.map((row) => String(column.value(row) ?? '').length), 0);
|
|
16
|
+
return Math.max(headerWidth, valueWidth);
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
const header = columns.map((column, index) => pad(column.label, widths[index])).join(' ');
|
|
20
|
+
const divider = columns.map((_, index) => '-'.repeat(widths[index])).join(' ');
|
|
21
|
+
const body = rows.map((row) => columns.map((column, index) => pad(column.value(row) ?? '', widths[index])).join(' '));
|
|
22
|
+
return [header, divider, ...body].join('\n');
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function yamlScalar(value) {
|
|
26
|
+
if (value === null || value === undefined) {
|
|
27
|
+
return 'null';
|
|
28
|
+
}
|
|
29
|
+
if (typeof value === 'number' || typeof value === 'boolean') {
|
|
30
|
+
return String(value);
|
|
31
|
+
}
|
|
32
|
+
return JSON.stringify(String(value));
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function toYaml(value, indent = 0) {
|
|
36
|
+
const prefix = ' '.repeat(indent);
|
|
37
|
+
|
|
38
|
+
if (Array.isArray(value)) {
|
|
39
|
+
if (!value.length) {
|
|
40
|
+
return `${prefix}[]`;
|
|
41
|
+
}
|
|
42
|
+
return value
|
|
43
|
+
.map((item) => {
|
|
44
|
+
if (item && typeof item === 'object') {
|
|
45
|
+
const nested = toYaml(item, indent + 2);
|
|
46
|
+
return `${prefix}-\n${nested}`;
|
|
47
|
+
}
|
|
48
|
+
return `${prefix}- ${yamlScalar(item)}`;
|
|
49
|
+
})
|
|
50
|
+
.join('\n');
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
if (value && typeof value === 'object') {
|
|
54
|
+
const entries = Object.entries(value);
|
|
55
|
+
if (!entries.length) {
|
|
56
|
+
return `${prefix}{}`;
|
|
57
|
+
}
|
|
58
|
+
return entries
|
|
59
|
+
.map(([key, nestedValue]) => {
|
|
60
|
+
if (nestedValue && typeof nestedValue === 'object') {
|
|
61
|
+
return `${prefix}${key}:\n${toYaml(nestedValue, indent + 2)}`;
|
|
62
|
+
}
|
|
63
|
+
return `${prefix}${key}: ${yamlScalar(nestedValue)}`;
|
|
64
|
+
})
|
|
65
|
+
.join('\n');
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
return `${prefix}${yamlScalar(value)}`;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function serializeMachineEnvelope(envelope, outputMode) {
|
|
72
|
+
if (outputMode === 'yaml') {
|
|
73
|
+
return `${toYaml(envelope)}\n`;
|
|
74
|
+
}
|
|
75
|
+
if (outputMode === 'ndjson') {
|
|
76
|
+
return `${JSON.stringify(envelope)}\n`;
|
|
77
|
+
}
|
|
78
|
+
return `${JSON.stringify(envelope, null, 2)}\n`;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export class OutputManager {
|
|
82
|
+
constructor(runtime) {
|
|
83
|
+
this.runtime = runtime;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
isMachineMode() {
|
|
87
|
+
return this.runtime.outputMode !== 'table';
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
writeWarning(message) {
|
|
91
|
+
if (this.isMachineMode()) {
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
process.stderr.write(`${message}\n`);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
emitSuccess({
|
|
98
|
+
ok = true,
|
|
99
|
+
command,
|
|
100
|
+
data = {},
|
|
101
|
+
humanSummary,
|
|
102
|
+
hints = [],
|
|
103
|
+
warnings = [],
|
|
104
|
+
requestId = null,
|
|
105
|
+
meta = {},
|
|
106
|
+
renderHuman,
|
|
107
|
+
}) {
|
|
108
|
+
const envelope = {
|
|
109
|
+
ok: Boolean(ok),
|
|
110
|
+
command,
|
|
111
|
+
data,
|
|
112
|
+
human_summary: humanSummary,
|
|
113
|
+
hints,
|
|
114
|
+
warnings,
|
|
115
|
+
request_id: requestId,
|
|
116
|
+
meta: {
|
|
117
|
+
profile: this.runtime.profileName,
|
|
118
|
+
api_base: this.runtime.apiBase,
|
|
119
|
+
mutating: Boolean(meta.mutating),
|
|
120
|
+
...meta,
|
|
121
|
+
},
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
if (this.isMachineMode()) {
|
|
125
|
+
process.stdout.write(serializeMachineEnvelope(envelope, this.runtime.outputMode));
|
|
126
|
+
return EXIT_CODES.ok;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
for (const warning of warnings) {
|
|
130
|
+
this.writeWarning(`Warning: ${warning}`);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
if (typeof renderHuman === 'function') {
|
|
134
|
+
const rendered = renderHuman();
|
|
135
|
+
if (rendered) {
|
|
136
|
+
process.stdout.write(`${rendered}\n`);
|
|
137
|
+
}
|
|
138
|
+
} else if (humanSummary) {
|
|
139
|
+
process.stdout.write(`${humanSummary}\n`);
|
|
140
|
+
} else if (Object.keys(data).length) {
|
|
141
|
+
process.stdout.write(`${inspect(data, { depth: null, colors: this.runtime.color })}\n`);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
if (hints.length) {
|
|
145
|
+
const lines = hints.map((hint) => ` ${hint.command} ${hint.reason}`);
|
|
146
|
+
process.stdout.write(`\nNext:\n${lines.join('\n')}\n`);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
return EXIT_CODES.ok;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
emitError({ command, error, requestId = null }) {
|
|
153
|
+
const envelope = {
|
|
154
|
+
ok: false,
|
|
155
|
+
command,
|
|
156
|
+
error: {
|
|
157
|
+
code: error.code,
|
|
158
|
+
message: error.message,
|
|
159
|
+
retryable: Boolean(error.retryable),
|
|
160
|
+
details: error.details || {},
|
|
161
|
+
},
|
|
162
|
+
hints: error.hints || [],
|
|
163
|
+
warnings: error.warnings || [],
|
|
164
|
+
request_id: requestId,
|
|
165
|
+
};
|
|
166
|
+
|
|
167
|
+
if (this.isMachineMode()) {
|
|
168
|
+
process.stdout.write(serializeMachineEnvelope(envelope, this.runtime.outputMode));
|
|
169
|
+
return error.exitCode || EXIT_CODES.unexpected;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
process.stderr.write(`Error: ${error.message}\n`);
|
|
173
|
+
for (const hint of error.hints || []) {
|
|
174
|
+
process.stderr.write(` ${hint.command} ${hint.reason}\n`);
|
|
175
|
+
}
|
|
176
|
+
return error.exitCode || EXIT_CODES.unexpected;
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
export { formatTable };
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { createServer } from 'node:http';
|
|
2
|
+
|
|
3
|
+
export async function getAvailablePort() {
|
|
4
|
+
const server = createServer();
|
|
5
|
+
await new Promise((resolvePromise, rejectPromise) => {
|
|
6
|
+
server.once('error', rejectPromise);
|
|
7
|
+
server.listen(0, '127.0.0.1', () => {
|
|
8
|
+
server.off('error', rejectPromise);
|
|
9
|
+
resolvePromise();
|
|
10
|
+
});
|
|
11
|
+
});
|
|
12
|
+
const { port } = server.address();
|
|
13
|
+
await new Promise((resolvePromise) => server.close(resolvePromise));
|
|
14
|
+
return port;
|
|
15
|
+
}
|
|
@@ -0,0 +1,261 @@
|
|
|
1
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
2
|
+
import { homedir } from 'node:os';
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
import { CliError, EXIT_CODES } from './errors.js';
|
|
5
|
+
import { getDesktopAuthRecovery } from './desktop-auth.js';
|
|
6
|
+
|
|
7
|
+
export const CONFIG_DIR = join(homedir(), '.notis');
|
|
8
|
+
export const CONFIG_FILE = join(CONFIG_DIR, 'config.json');
|
|
9
|
+
export const WORKSPACE_DIR = join(CONFIG_DIR, 'workspace');
|
|
10
|
+
export const DEFAULT_API_BASE = 'https://api.notis.ai';
|
|
11
|
+
export const DEFAULT_PROFILE = 'default';
|
|
12
|
+
const LOCAL_DEFAULT_API_BASES = new Set([
|
|
13
|
+
'http://localhost:3001',
|
|
14
|
+
'http://127.0.0.1:3001',
|
|
15
|
+
]);
|
|
16
|
+
const LOCAL_API_HOSTS = new Set(['localhost', '127.0.0.1', '::1']);
|
|
17
|
+
|
|
18
|
+
function clone(value) {
|
|
19
|
+
return JSON.parse(JSON.stringify(value));
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function normalizeConfig(rawConfig = {}) {
|
|
23
|
+
const raw = rawConfig && typeof rawConfig === 'object' ? clone(rawConfig) : {};
|
|
24
|
+
|
|
25
|
+
if (raw.profiles && typeof raw.profiles === 'object') {
|
|
26
|
+
const profiles = {};
|
|
27
|
+
for (const [name, profile] of Object.entries(raw.profiles)) {
|
|
28
|
+
if (!profile || typeof profile !== 'object') {
|
|
29
|
+
continue;
|
|
30
|
+
}
|
|
31
|
+
profiles[name] = {
|
|
32
|
+
jwt: typeof profile.jwt === 'string' ? profile.jwt : undefined,
|
|
33
|
+
api_base: typeof profile.api_base === 'string' ? profile.api_base : undefined,
|
|
34
|
+
auth_mode: profile.auth_mode === 'dev_portal' ? profile.auth_mode : undefined,
|
|
35
|
+
refresh_token:
|
|
36
|
+
typeof profile.refresh_token === 'string' ? profile.refresh_token : undefined,
|
|
37
|
+
access_expires_at:
|
|
38
|
+
typeof profile.access_expires_at === 'number' ? profile.access_expires_at : undefined,
|
|
39
|
+
refresh_expires_at:
|
|
40
|
+
typeof profile.refresh_expires_at === 'number' ? profile.refresh_expires_at : undefined,
|
|
41
|
+
desktop_app_name:
|
|
42
|
+
typeof profile.desktop_app_name === 'string' ? profile.desktop_app_name : undefined,
|
|
43
|
+
desktop_pid: typeof profile.desktop_pid === 'number' ? profile.desktop_pid : undefined,
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
if (!profiles[DEFAULT_PROFILE]) {
|
|
48
|
+
profiles[DEFAULT_PROFILE] = {};
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
return {
|
|
52
|
+
current_profile:
|
|
53
|
+
typeof raw.current_profile === 'string' && raw.current_profile in profiles
|
|
54
|
+
? raw.current_profile
|
|
55
|
+
: DEFAULT_PROFILE,
|
|
56
|
+
profiles,
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
return {
|
|
61
|
+
current_profile: DEFAULT_PROFILE,
|
|
62
|
+
profiles: {
|
|
63
|
+
[DEFAULT_PROFILE]: {
|
|
64
|
+
jwt: typeof raw.jwt === 'string' ? raw.jwt : undefined,
|
|
65
|
+
api_base: typeof raw.api_base === 'string' ? raw.api_base : undefined,
|
|
66
|
+
auth_mode: raw.auth_mode === 'dev_portal' ? raw.auth_mode : undefined,
|
|
67
|
+
refresh_token: typeof raw.refresh_token === 'string' ? raw.refresh_token : undefined,
|
|
68
|
+
access_expires_at:
|
|
69
|
+
typeof raw.access_expires_at === 'number' ? raw.access_expires_at : undefined,
|
|
70
|
+
refresh_expires_at:
|
|
71
|
+
typeof raw.refresh_expires_at === 'number' ? raw.refresh_expires_at : undefined,
|
|
72
|
+
desktop_app_name:
|
|
73
|
+
typeof raw.desktop_app_name === 'string' ? raw.desktop_app_name : undefined,
|
|
74
|
+
desktop_pid: typeof raw.desktop_pid === 'number' ? raw.desktop_pid : undefined,
|
|
75
|
+
},
|
|
76
|
+
},
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export function loadConfig() {
|
|
81
|
+
if (!existsSync(CONFIG_FILE)) {
|
|
82
|
+
return normalizeConfig({});
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
return normalizeConfig(JSON.parse(readFileSync(CONFIG_FILE, 'utf-8')));
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export function saveConfig(config) {
|
|
89
|
+
mkdirSync(CONFIG_DIR, { recursive: true });
|
|
90
|
+
writeFileSync(CONFIG_FILE, JSON.stringify(normalizeConfig(config), null, 2));
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export function getProfile(config, profileName) {
|
|
94
|
+
const normalized = normalizeConfig(config);
|
|
95
|
+
return normalized.profiles[profileName] || {};
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export function getCurrentProfileName(config, preferredName) {
|
|
99
|
+
const normalized = normalizeConfig(config);
|
|
100
|
+
if (preferredName && normalized.profiles[preferredName]) {
|
|
101
|
+
return preferredName;
|
|
102
|
+
}
|
|
103
|
+
return normalized.current_profile || DEFAULT_PROFILE;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export function ensureProfile(config, profileName) {
|
|
107
|
+
const normalized = normalizeConfig(config);
|
|
108
|
+
if (!normalized.profiles[profileName]) {
|
|
109
|
+
normalized.profiles[profileName] = {};
|
|
110
|
+
}
|
|
111
|
+
return normalized;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function isLocalApiBase(value) {
|
|
115
|
+
if (typeof value !== 'string' || !value) {
|
|
116
|
+
return false;
|
|
117
|
+
}
|
|
118
|
+
try {
|
|
119
|
+
const parsed = new URL(value);
|
|
120
|
+
return ['http:', 'https:'].includes(parsed.protocol) && LOCAL_API_HOSTS.has(parsed.hostname);
|
|
121
|
+
} catch {
|
|
122
|
+
return false;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
export function getApiBase(config, profileName, override) {
|
|
127
|
+
if (override) {
|
|
128
|
+
return override;
|
|
129
|
+
}
|
|
130
|
+
const env = process.env.NOTIS_API_BASE;
|
|
131
|
+
if (env) {
|
|
132
|
+
return env;
|
|
133
|
+
}
|
|
134
|
+
const profile = getProfile(config, profileName);
|
|
135
|
+
const profileApiBase = profile.api_base;
|
|
136
|
+
const conductorPort = Number.parseInt(process.env.CONDUCTOR_PORT || '', 10);
|
|
137
|
+
|
|
138
|
+
// Conductor assigns dynamic local ports. When a workspace is running under
|
|
139
|
+
// Conductor, treat saved localhost profile values as stale and transparently
|
|
140
|
+
// retarget the CLI at the active backend port.
|
|
141
|
+
if (
|
|
142
|
+
Number.isInteger(conductorPort) &&
|
|
143
|
+
conductorPort > 0 &&
|
|
144
|
+
(!profileApiBase || LOCAL_DEFAULT_API_BASES.has(profileApiBase) || isLocalApiBase(profileApiBase))
|
|
145
|
+
) {
|
|
146
|
+
return `http://localhost:${conductorPort + 1}`;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
return profileApiBase || DEFAULT_API_BASE;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
export function getJwt(config, profileName) {
|
|
153
|
+
const env = process.env.NOTIS_JWT;
|
|
154
|
+
if (env) {
|
|
155
|
+
return env;
|
|
156
|
+
}
|
|
157
|
+
const profile = getProfile(config, profileName);
|
|
158
|
+
return profile.jwt;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
export function isAgentMode(globalOptions = {}) {
|
|
162
|
+
return process.env.NOTIS_AGENT === '1' || Boolean(globalOptions.agentMode);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
export function isNonInteractive(globalOptions = {}) {
|
|
166
|
+
if (process.env.NOTIS_NON_INTERACTIVE === '1') {
|
|
167
|
+
return true;
|
|
168
|
+
}
|
|
169
|
+
if (globalOptions.nonInteractive) {
|
|
170
|
+
return true;
|
|
171
|
+
}
|
|
172
|
+
return isAgentMode(globalOptions);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
export function resolveOutputMode(globalOptions = {}) {
|
|
176
|
+
if (globalOptions.json) {
|
|
177
|
+
return 'json';
|
|
178
|
+
}
|
|
179
|
+
if (globalOptions.output) {
|
|
180
|
+
return globalOptions.output;
|
|
181
|
+
}
|
|
182
|
+
if (process.env.NOTIS_OUTPUT) {
|
|
183
|
+
return process.env.NOTIS_OUTPUT;
|
|
184
|
+
}
|
|
185
|
+
return !process.stdout.isTTY || isAgentMode(globalOptions) ? 'json' : 'table';
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
export function resolveTimeoutMs(globalOptions = {}) {
|
|
189
|
+
const raw = globalOptions.timeoutMs || process.env.NOTIS_TIMEOUT_MS;
|
|
190
|
+
if (!raw) {
|
|
191
|
+
return 30000;
|
|
192
|
+
}
|
|
193
|
+
const parsed = Number.parseInt(raw, 10);
|
|
194
|
+
return Number.isFinite(parsed) && parsed > 0 ? parsed : 30000;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
export function resolveRuntimeProfile(globalOptions = {}, { requireAuth = true } = {}) {
|
|
198
|
+
const config = loadConfig();
|
|
199
|
+
const profileName = getCurrentProfileName(config, globalOptions.profile);
|
|
200
|
+
const apiBase = getApiBase(config, profileName, globalOptions.apiBase);
|
|
201
|
+
const jwt = getJwt(config, profileName);
|
|
202
|
+
const profile = getProfile(config, profileName);
|
|
203
|
+
const agentMode = isAgentMode(globalOptions);
|
|
204
|
+
const nonInteractive = isNonInteractive(globalOptions);
|
|
205
|
+
const outputMode = resolveOutputMode(globalOptions);
|
|
206
|
+
const timeoutMs = resolveTimeoutMs(globalOptions);
|
|
207
|
+
|
|
208
|
+
if (requireAuth && !jwt) {
|
|
209
|
+
const recovery = getDesktopAuthRecovery({
|
|
210
|
+
apiBase,
|
|
211
|
+
desktopAppName: profile.desktop_app_name,
|
|
212
|
+
desktopPid: profile.desktop_pid,
|
|
213
|
+
});
|
|
214
|
+
throw new CliError({
|
|
215
|
+
code: 'auth_missing',
|
|
216
|
+
message: `No JWT configured for profile ${profileName}`,
|
|
217
|
+
exitCode: EXIT_CODES.auth,
|
|
218
|
+
hints: recovery.hints,
|
|
219
|
+
});
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
// An explicit NOTIS_JWT is a complete credential override. Use it verbatim
|
|
223
|
+
// and never replace it with a token later synced by the desktop profile.
|
|
224
|
+
const usingEnvJwt = Boolean(process.env.NOTIS_JWT);
|
|
225
|
+
return {
|
|
226
|
+
config,
|
|
227
|
+
profileName,
|
|
228
|
+
apiBase,
|
|
229
|
+
jwt,
|
|
230
|
+
credentialSource: usingEnvJwt ? 'env' : 'profile',
|
|
231
|
+
desktopAppName: usingEnvJwt ? undefined : profile.desktop_app_name,
|
|
232
|
+
desktopPid: usingEnvJwt ? undefined : profile.desktop_pid,
|
|
233
|
+
agentMode,
|
|
234
|
+
nonInteractive,
|
|
235
|
+
outputMode,
|
|
236
|
+
timeoutMs,
|
|
237
|
+
};
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
export function getJwtExpiration(jwt) {
|
|
241
|
+
if (typeof jwt !== 'string' || !jwt) {
|
|
242
|
+
return null;
|
|
243
|
+
}
|
|
244
|
+
try {
|
|
245
|
+
const parts = jwt.split('.');
|
|
246
|
+
if (parts.length !== 3) return null;
|
|
247
|
+
const payload = JSON.parse(Buffer.from(parts[1], 'base64url').toString());
|
|
248
|
+
return typeof payload.exp === 'number' ? payload.exp : null;
|
|
249
|
+
} catch {
|
|
250
|
+
return null;
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
export function isJwtExpired(jwt, nowSeconds = Math.floor(Date.now() / 1000)) {
|
|
255
|
+
const expiration = getJwtExpiration(jwt);
|
|
256
|
+
return expiration !== null && expiration <= nowSeconds;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
export function workspacePath(appId) {
|
|
260
|
+
return join(WORKSPACE_DIR, appId);
|
|
261
|
+
}
|