@notis_ai/cli 0.2.0-beta.16.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 +335 -0
- package/bin/notis.js +2 -0
- package/package.json +38 -0
- package/src/cli.js +147 -0
- package/src/command-specs/apps.js +496 -0
- package/src/command-specs/auth.js +178 -0
- package/src/command-specs/db.js +163 -0
- package/src/command-specs/helpers.js +193 -0
- package/src/command-specs/index.js +20 -0
- package/src/command-specs/meta.js +154 -0
- package/src/command-specs/tools.js +391 -0
- package/src/runtime/app-platform.js +624 -0
- package/src/runtime/app-preview-server.js +312 -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/profiles.js +202 -0
- package/src/runtime/transport.js +198 -0
- package/template/app/globals.css +3 -0
- package/template/app/layout.tsx +7 -0
- package/template/app/page.tsx +55 -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/notis.config.ts +18 -0
- package/template/package.json +32 -0
- package/template/packages/notis-sdk/package.json +26 -0
- package/template/packages/notis-sdk/src/config.ts +48 -0
- package/template/packages/notis-sdk/src/helpers.ts +131 -0
- package/template/packages/notis-sdk/src/hooks/useAppState.ts +50 -0
- package/template/packages/notis-sdk/src/hooks/useBackend.ts +41 -0
- package/template/packages/notis-sdk/src/hooks/useCollectionItem.ts +58 -0
- package/template/packages/notis-sdk/src/hooks/useDatabase.ts +87 -0
- package/template/packages/notis-sdk/src/hooks/useDocument.ts +61 -0
- package/template/packages/notis-sdk/src/hooks/useNotis.ts +31 -0
- package/template/packages/notis-sdk/src/hooks/useNotisNavigation.ts +49 -0
- package/template/packages/notis-sdk/src/hooks/useTool.ts +49 -0
- package/template/packages/notis-sdk/src/hooks/useTools.ts +56 -0
- package/template/packages/notis-sdk/src/hooks/useUpsertDocument.ts +57 -0
- package/template/packages/notis-sdk/src/index.ts +47 -0
- package/template/packages/notis-sdk/src/provider.tsx +44 -0
- package/template/packages/notis-sdk/src/runtime.ts +159 -0
- package/template/packages/notis-sdk/src/styles.css +123 -0
- package/template/packages/notis-sdk/src/ui.ts +15 -0
- package/template/packages/notis-sdk/src/vite.ts +54 -0
- package/template/packages/notis-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
|
@@ -0,0 +1,496 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Notis apps CLI commands.
|
|
3
|
+
*
|
|
4
|
+
* Clean command set for the Vercel-like Notis app workflow:
|
|
5
|
+
* init -> dev -> build -> preview -> deploy
|
|
6
|
+
*
|
|
7
|
+
* Supporting commands: list, link, doctor.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { EXIT_CODES, usageError } from '../runtime/errors.js';
|
|
11
|
+
import { formatTable } from '../runtime/output.js';
|
|
12
|
+
import {
|
|
13
|
+
resolveProjectDir,
|
|
14
|
+
loadAppConfig,
|
|
15
|
+
detectProjectProblems,
|
|
16
|
+
detectProjectWarnings,
|
|
17
|
+
buildArtifact,
|
|
18
|
+
readManifest,
|
|
19
|
+
readLinkedState,
|
|
20
|
+
writeLinkedState,
|
|
21
|
+
requireLinkedAppId,
|
|
22
|
+
scaffoldProject,
|
|
23
|
+
collectArtifactFiles,
|
|
24
|
+
runProjectScript,
|
|
25
|
+
directDeploy,
|
|
26
|
+
} from '../runtime/app-platform.js';
|
|
27
|
+
import { startPreviewServer } from '../runtime/app-preview-server.js';
|
|
28
|
+
import {
|
|
29
|
+
nextIdempotencyKey,
|
|
30
|
+
runToolCommand,
|
|
31
|
+
toolConflictToError,
|
|
32
|
+
} from './helpers.js';
|
|
33
|
+
|
|
34
|
+
// ---------------------------------------------------------------------------
|
|
35
|
+
// Formatters
|
|
36
|
+
// ---------------------------------------------------------------------------
|
|
37
|
+
|
|
38
|
+
function appsTable(apps) {
|
|
39
|
+
return formatTable(apps, [
|
|
40
|
+
{ label: 'ID', value: (app) => app.app_id || app.id || '' },
|
|
41
|
+
{ label: 'Name', value: (app) => app.name || 'Untitled' },
|
|
42
|
+
{ label: 'Version', value: (app) => app.current_version || app.manifest?.version || 0 },
|
|
43
|
+
{ label: 'Status', value: (app) => app.status || '' },
|
|
44
|
+
]);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
async function assertDirectDeployAccess(runtime, appId) {
|
|
48
|
+
const result = await runToolCommand({
|
|
49
|
+
runtime,
|
|
50
|
+
toolName: 'notis_list_apps',
|
|
51
|
+
});
|
|
52
|
+
const apps = result.payload.apps || [];
|
|
53
|
+
const hasAccess = apps.some((app) => (app.app_id || app.id) === appId);
|
|
54
|
+
if (!hasAccess) {
|
|
55
|
+
throw usageError(`Direct deploy requires access to app ${appId}.`);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// ---------------------------------------------------------------------------
|
|
60
|
+
// Handlers
|
|
61
|
+
// ---------------------------------------------------------------------------
|
|
62
|
+
|
|
63
|
+
async function appsListHandler(ctx) {
|
|
64
|
+
const result = await runToolCommand({
|
|
65
|
+
runtime: ctx.runtime,
|
|
66
|
+
toolName: 'notis_list_apps',
|
|
67
|
+
});
|
|
68
|
+
const apps = result.payload.apps || [];
|
|
69
|
+
return ctx.output.emitSuccess({
|
|
70
|
+
command: ctx.spec.command_path.join(' '),
|
|
71
|
+
data: { apps },
|
|
72
|
+
humanSummary: apps.length ? `Found ${apps.length} apps` : 'No apps found.',
|
|
73
|
+
renderHuman: () => (apps.length ? appsTable(apps) : 'No apps found.'),
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
async function appsInitHandler(ctx) {
|
|
78
|
+
const projectDir = resolveProjectDir(ctx.args.dir || ctx.args.name.toLowerCase().replace(/[^a-z0-9]+/g, '-'));
|
|
79
|
+
|
|
80
|
+
scaffoldProject({ projectDir, appName: ctx.args.name });
|
|
81
|
+
|
|
82
|
+
return ctx.output.emitSuccess({
|
|
83
|
+
command: ctx.spec.command_path.join(' '),
|
|
84
|
+
data: { project_dir: projectDir, app_name: ctx.args.name },
|
|
85
|
+
humanSummary: `Scaffolded "${ctx.args.name}" in ${projectDir}`,
|
|
86
|
+
hints: [
|
|
87
|
+
{ command: `cd ${projectDir} && npm install`, reason: 'Install dependencies' },
|
|
88
|
+
{ command: 'npm run dev', reason: 'Start the Vite dev server' },
|
|
89
|
+
],
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
async function appsCreateHandler(ctx) {
|
|
94
|
+
const projectDir = ctx.args.dir ? resolveProjectDir(ctx.args.dir) : null;
|
|
95
|
+
const idempotencyKey = nextIdempotencyKey(ctx.globalOptions);
|
|
96
|
+
const icon = ctx.options.icon
|
|
97
|
+
? (ctx.options.icon.startsWith('lucide:') ? ctx.options.icon : `lucide:${ctx.options.icon}`)
|
|
98
|
+
: undefined;
|
|
99
|
+
const result = await runToolCommand({
|
|
100
|
+
runtime: ctx.runtime,
|
|
101
|
+
toolName: 'notis_create_app',
|
|
102
|
+
arguments_: {
|
|
103
|
+
name: ctx.args.name,
|
|
104
|
+
description: ctx.options.description || undefined,
|
|
105
|
+
icon,
|
|
106
|
+
},
|
|
107
|
+
mutating: true,
|
|
108
|
+
idempotencyKey,
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
const app = result.payload.app || result.payload;
|
|
112
|
+
if (!app?.id) {
|
|
113
|
+
throw usageError('Create app did not return an app id.');
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
if (projectDir) {
|
|
117
|
+
writeLinkedState(projectDir, {
|
|
118
|
+
app_id: app.id,
|
|
119
|
+
linked_at: new Date().toISOString(),
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
return ctx.output.emitSuccess({
|
|
124
|
+
command: ctx.spec.command_path.join(' '),
|
|
125
|
+
data: {
|
|
126
|
+
app,
|
|
127
|
+
project_dir: projectDir,
|
|
128
|
+
linked: Boolean(projectDir),
|
|
129
|
+
idempotency_key: idempotencyKey,
|
|
130
|
+
},
|
|
131
|
+
humanSummary: projectDir
|
|
132
|
+
? `Created app ${app.name || ctx.args.name} (${app.id}) and linked ${projectDir}`
|
|
133
|
+
: `Created app ${app.name || ctx.args.name} (${app.id})`,
|
|
134
|
+
hints: projectDir
|
|
135
|
+
? [{ command: `cd ${projectDir} && notis apps deploy .`, reason: 'Deploy the linked project' }]
|
|
136
|
+
: [{ command: `notis apps link ${app.id} .`, reason: 'Link a local project before deploying' }],
|
|
137
|
+
meta: { mutating: true, idempotency_key: idempotencyKey },
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
async function appsDevHandler(ctx) {
|
|
142
|
+
const projectDir = resolveProjectDir(ctx.args.dir || '.');
|
|
143
|
+
const problems = detectProjectProblems(projectDir);
|
|
144
|
+
if (problems.length) {
|
|
145
|
+
throw usageError(`Project has problems:\n${problems.map((p) => ` - ${p}`).join('\n')}`);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
await runProjectScript({
|
|
149
|
+
projectDir,
|
|
150
|
+
scriptName: 'dev',
|
|
151
|
+
env: { NOTIS_DEV: '1' },
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
return EXIT_CODES.ok;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
async function appsBuildHandler(ctx) {
|
|
158
|
+
const projectDir = resolveProjectDir(ctx.args.dir || '.');
|
|
159
|
+
const problems = detectProjectProblems(projectDir);
|
|
160
|
+
if (problems.length) {
|
|
161
|
+
throw usageError(`Project has problems:\n${problems.map((p) => ` - ${p}`).join('\n')}`);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
const { manifest } = await buildArtifact(projectDir);
|
|
165
|
+
|
|
166
|
+
return ctx.output.emitSuccess({
|
|
167
|
+
command: ctx.spec.command_path.join(' '),
|
|
168
|
+
data: { manifest },
|
|
169
|
+
humanSummary: `Built ${manifest.routes.length} routes into .notis/output/`,
|
|
170
|
+
});
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
async function appsPreviewHandler(ctx) {
|
|
174
|
+
const projectDir = resolveProjectDir(ctx.args.dir || '.');
|
|
175
|
+
const port = ctx.options.port ? Number.parseInt(ctx.options.port, 10) : 8787;
|
|
176
|
+
if (!Number.isInteger(port) || port <= 0 || port > 65535) {
|
|
177
|
+
throw usageError('Port must be between 1 and 65535.');
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
const manifest = readManifest(projectDir);
|
|
181
|
+
const defaultRoute = (manifest.routes || []).find((r) => r.default) || manifest.routes?.[0];
|
|
182
|
+
const url = `http://localhost:${port}${defaultRoute?.path || '/'}`;
|
|
183
|
+
|
|
184
|
+
ctx.output.emitSuccess({
|
|
185
|
+
command: ctx.spec.command_path.join(' '),
|
|
186
|
+
data: { port, url, routes: manifest.routes.length },
|
|
187
|
+
humanSummary: `Preview at ${url} -- press Ctrl+C to stop.`,
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
await startPreviewServer({ projectDir, port });
|
|
191
|
+
return EXIT_CODES.ok;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
async function appsLinkHandler(ctx) {
|
|
195
|
+
const projectDir = resolveProjectDir(ctx.args.dir || '.');
|
|
196
|
+
const appId = ctx.args.appId;
|
|
197
|
+
|
|
198
|
+
writeLinkedState(projectDir, {
|
|
199
|
+
app_id: appId,
|
|
200
|
+
linked_at: new Date().toISOString(),
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
return ctx.output.emitSuccess({
|
|
204
|
+
command: ctx.spec.command_path.join(' '),
|
|
205
|
+
data: { app_id: appId, project_dir: projectDir },
|
|
206
|
+
humanSummary: `Linked to app ${appId}`,
|
|
207
|
+
hints: [
|
|
208
|
+
{ command: 'notis apps deploy .', reason: 'Deploy the app' },
|
|
209
|
+
],
|
|
210
|
+
});
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
async function appsDeployHandler(ctx) {
|
|
214
|
+
const projectDir = resolveProjectDir(ctx.args.dir || '.');
|
|
215
|
+
const appId = requireLinkedAppId(projectDir, ctx.options.appId);
|
|
216
|
+
const idempotencyKey = nextIdempotencyKey(ctx.globalOptions);
|
|
217
|
+
|
|
218
|
+
// Build if needed
|
|
219
|
+
if (!ctx.options.skipBuild) {
|
|
220
|
+
await buildArtifact(projectDir);
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
// Direct deploy mode: upload to Supabase storage directly
|
|
224
|
+
if (ctx.options.direct) {
|
|
225
|
+
await assertDirectDeployAccess(ctx.runtime, appId);
|
|
226
|
+
const { version } = await directDeploy(projectDir, appId);
|
|
227
|
+
return ctx.output.emitSuccess({
|
|
228
|
+
command: ctx.spec.command_path.join(' '),
|
|
229
|
+
data: { app_id: appId, version, mode: 'direct' },
|
|
230
|
+
humanSummary: `Deployed to app ${appId} (version ${version}) via direct upload`,
|
|
231
|
+
meta: { mutating: true },
|
|
232
|
+
});
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
// Standard deploy via backend server, with auto-fallback to direct
|
|
236
|
+
const files = collectArtifactFiles(projectDir);
|
|
237
|
+
const manifest = readManifest(projectDir);
|
|
238
|
+
|
|
239
|
+
let result;
|
|
240
|
+
try {
|
|
241
|
+
result = await runToolCommand({
|
|
242
|
+
runtime: ctx.runtime,
|
|
243
|
+
toolName: 'notis_save_app_files',
|
|
244
|
+
arguments_: {
|
|
245
|
+
app_id: appId,
|
|
246
|
+
files,
|
|
247
|
+
manifest,
|
|
248
|
+
},
|
|
249
|
+
mutating: true,
|
|
250
|
+
idempotencyKey,
|
|
251
|
+
});
|
|
252
|
+
} catch (error) {
|
|
253
|
+
if (error.code === 'conflict') {
|
|
254
|
+
throw toolConflictToError(error.details, 'Deploy conflict');
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
// Auto-fallback to direct deploy on network errors
|
|
258
|
+
const isNetworkError = error.code === 'network_error'
|
|
259
|
+
|| error.code === 'network_timeout'
|
|
260
|
+
|| (error.message && /fetch failed|ECONNREFUSED|network/i.test(error.message));
|
|
261
|
+
|
|
262
|
+
if (isNetworkError) {
|
|
263
|
+
try {
|
|
264
|
+
await assertDirectDeployAccess(ctx.runtime, appId);
|
|
265
|
+
} catch (accessError) {
|
|
266
|
+
throw usageError(
|
|
267
|
+
`Backend deploy failed (${error.message}) and direct fallback was blocked because app access ` +
|
|
268
|
+
`could not be verified (${accessError.message}).`,
|
|
269
|
+
);
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
try {
|
|
273
|
+
const { version } = await directDeploy(projectDir, appId);
|
|
274
|
+
return ctx.output.emitSuccess({
|
|
275
|
+
command: ctx.spec.command_path.join(' '),
|
|
276
|
+
data: { app_id: appId, version, mode: 'direct-fallback' },
|
|
277
|
+
humanSummary: `Backend unavailable -- deployed to app ${appId} (version ${version}) via direct upload`,
|
|
278
|
+
warnings: ['Backend server was unreachable. Used direct Supabase upload as fallback.'],
|
|
279
|
+
meta: { mutating: true },
|
|
280
|
+
});
|
|
281
|
+
} catch (directError) {
|
|
282
|
+
throw usageError(
|
|
283
|
+
`Backend deploy failed (${error.message}) and direct fallback also failed (${directError.message}). ` +
|
|
284
|
+
'Check server/.env for Supabase credentials or start the backend server.',
|
|
285
|
+
);
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
throw error;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
return ctx.output.emitSuccess({
|
|
293
|
+
command: ctx.spec.command_path.join(' '),
|
|
294
|
+
data: {
|
|
295
|
+
app_id: appId,
|
|
296
|
+
version: result.payload.version,
|
|
297
|
+
idempotency_key: idempotencyKey,
|
|
298
|
+
},
|
|
299
|
+
humanSummary: `Deployed to app ${appId} (version ${result.payload.version})`,
|
|
300
|
+
meta: { mutating: true, idempotency_key: idempotencyKey },
|
|
301
|
+
});
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
async function appsDoctorHandler(ctx) {
|
|
305
|
+
const projectDir = resolveProjectDir(ctx.args.dir || '.');
|
|
306
|
+
const problems = detectProjectProblems(projectDir);
|
|
307
|
+
|
|
308
|
+
let appConfig = null;
|
|
309
|
+
try {
|
|
310
|
+
appConfig = await loadAppConfig(projectDir);
|
|
311
|
+
} catch {
|
|
312
|
+
problems.push('Failed to load notis.config.ts');
|
|
313
|
+
}
|
|
314
|
+
const warnings = detectProjectWarnings(projectDir, appConfig);
|
|
315
|
+
|
|
316
|
+
const linkedState = readLinkedState(projectDir);
|
|
317
|
+
const status = problems.length ? 'unhealthy' : warnings.length ? 'warnings' : 'healthy';
|
|
318
|
+
|
|
319
|
+
return ctx.output.emitSuccess({
|
|
320
|
+
command: ctx.spec.command_path.join(' '),
|
|
321
|
+
data: { status, problems, warnings, linked: linkedState, config: appConfig },
|
|
322
|
+
humanSummary: problems.length
|
|
323
|
+
? `Found ${problems.length} problems:\n${problems.map((p) => ` - ${p}`).join('\n')}`
|
|
324
|
+
: warnings.length
|
|
325
|
+
? `Healthy with ${warnings.length} warnings:\n${warnings.map((w) => ` - ${w}`).join('\n')}`
|
|
326
|
+
: `Project is healthy.${linkedState ? ` Linked to ${linkedState.app_id}.` : ' Not linked.'}`,
|
|
327
|
+
});
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
// ---------------------------------------------------------------------------
|
|
331
|
+
// Command specs
|
|
332
|
+
// ---------------------------------------------------------------------------
|
|
333
|
+
|
|
334
|
+
export const appsCommandSpecs = [
|
|
335
|
+
{
|
|
336
|
+
command_path: ['apps', 'list'],
|
|
337
|
+
summary: 'List apps the current profile can access.',
|
|
338
|
+
when_to_use: 'Discover existing apps before linking or deploying.',
|
|
339
|
+
args_schema: { arguments: [], options: [] },
|
|
340
|
+
examples: ['notis apps list', 'notis apps list --json'],
|
|
341
|
+
mutates: false,
|
|
342
|
+
idempotent: true,
|
|
343
|
+
backend_call: { type: 'tool', name: 'notis_list_apps' },
|
|
344
|
+
handler: appsListHandler,
|
|
345
|
+
},
|
|
346
|
+
{
|
|
347
|
+
command_path: ['apps', 'init'],
|
|
348
|
+
summary: 'Scaffold a new Notis app project.',
|
|
349
|
+
when_to_use: 'Start a new Notis app. Creates a Vite + React project with @notis/sdk pre-configured.',
|
|
350
|
+
args_schema: {
|
|
351
|
+
arguments: [
|
|
352
|
+
{ token: '<name>', description: 'Display name for the app.' },
|
|
353
|
+
{ token: '[dir]', key: 'dir', description: 'Target directory (defaults to kebab-case of name).' },
|
|
354
|
+
],
|
|
355
|
+
options: [],
|
|
356
|
+
},
|
|
357
|
+
examples: ['notis apps init "Mind the Flo"', 'notis apps init "My App" ./my-app'],
|
|
358
|
+
mutates: true,
|
|
359
|
+
idempotent: false,
|
|
360
|
+
require_auth: false,
|
|
361
|
+
backend_call: { type: 'local', name: 'scaffold_project' },
|
|
362
|
+
handler: appsInitHandler,
|
|
363
|
+
},
|
|
364
|
+
{
|
|
365
|
+
command_path: ['apps', 'create'],
|
|
366
|
+
summary: 'Create a new remote Notis app and optionally link a local project to it.',
|
|
367
|
+
when_to_use: 'Provision a fresh remote app before the first deploy. Pass a project directory to link it immediately.',
|
|
368
|
+
args_schema: {
|
|
369
|
+
arguments: [
|
|
370
|
+
{ token: '<name>', description: 'Display name for the remote app.' },
|
|
371
|
+
{ token: '[dir]', key: 'dir', description: 'Project directory to link after creation (default: do not link).' },
|
|
372
|
+
],
|
|
373
|
+
options: [
|
|
374
|
+
{ flags: '--description <text>', description: 'Optional app description.' },
|
|
375
|
+
{ flags: '--icon <lucide:icon>', description: 'Optional Lucide icon, for example lucide:dices.' },
|
|
376
|
+
],
|
|
377
|
+
},
|
|
378
|
+
examples: [
|
|
379
|
+
'notis apps create "My App"',
|
|
380
|
+
'notis apps create "My App" . --description "Internal tool" --icon lucide:layout-dashboard',
|
|
381
|
+
],
|
|
382
|
+
mutates: true,
|
|
383
|
+
idempotent: false,
|
|
384
|
+
backend_call: { type: 'tool', name: 'notis_create_app' },
|
|
385
|
+
handler: appsCreateHandler,
|
|
386
|
+
},
|
|
387
|
+
{
|
|
388
|
+
command_path: ['apps', 'dev'],
|
|
389
|
+
summary: 'Run the Vite dev server for local development.',
|
|
390
|
+
when_to_use: 'Iterate on app UI with hot reload. SDK hooks return mock data.',
|
|
391
|
+
args_schema: {
|
|
392
|
+
arguments: [
|
|
393
|
+
{ token: '[dir]', key: 'dir', description: 'Project directory (default: current dir).' },
|
|
394
|
+
],
|
|
395
|
+
options: [],
|
|
396
|
+
},
|
|
397
|
+
examples: ['notis apps dev', 'notis apps dev ./my-app'],
|
|
398
|
+
mutates: false,
|
|
399
|
+
idempotent: true,
|
|
400
|
+
require_auth: false,
|
|
401
|
+
backend_call: { type: 'local', name: 'next_dev' },
|
|
402
|
+
handler: appsDevHandler,
|
|
403
|
+
},
|
|
404
|
+
{
|
|
405
|
+
command_path: ['apps', 'build'],
|
|
406
|
+
summary: 'Build and package the app into .notis/output/.',
|
|
407
|
+
when_to_use: 'Prepare the app for preview or deployment.',
|
|
408
|
+
args_schema: {
|
|
409
|
+
arguments: [
|
|
410
|
+
{ token: '[dir]', key: 'dir', description: 'Project directory (default: current dir).' },
|
|
411
|
+
],
|
|
412
|
+
options: [],
|
|
413
|
+
},
|
|
414
|
+
examples: ['notis apps build', 'notis apps build ./my-app'],
|
|
415
|
+
mutates: true,
|
|
416
|
+
idempotent: true,
|
|
417
|
+
require_auth: false,
|
|
418
|
+
backend_call: { type: 'local', name: 'next_build_and_package' },
|
|
419
|
+
handler: appsBuildHandler,
|
|
420
|
+
},
|
|
421
|
+
{
|
|
422
|
+
command_path: ['apps', 'preview'],
|
|
423
|
+
summary: 'Serve the built bundle locally for testing.',
|
|
424
|
+
when_to_use: 'Smoke-test the exact bundle that will be deployed. Databases use seed data.',
|
|
425
|
+
args_schema: {
|
|
426
|
+
arguments: [
|
|
427
|
+
{ token: '[dir]', key: 'dir', description: 'Project directory (default: current dir).' },
|
|
428
|
+
],
|
|
429
|
+
options: [
|
|
430
|
+
{ flags: '--port <number>', description: 'Server port (default: 8787).' },
|
|
431
|
+
],
|
|
432
|
+
},
|
|
433
|
+
examples: ['notis apps preview', 'notis apps preview --port 3000'],
|
|
434
|
+
mutates: false,
|
|
435
|
+
idempotent: true,
|
|
436
|
+
require_auth: false,
|
|
437
|
+
backend_call: { type: 'local', name: 'preview_server' },
|
|
438
|
+
handler: appsPreviewHandler,
|
|
439
|
+
},
|
|
440
|
+
{
|
|
441
|
+
command_path: ['apps', 'link'],
|
|
442
|
+
summary: 'Link a local project to a remote Notis app.',
|
|
443
|
+
when_to_use: 'Connect a local project to an existing app for deployment.',
|
|
444
|
+
args_schema: {
|
|
445
|
+
arguments: [
|
|
446
|
+
{ token: '<app-id>', description: 'Remote app ID to link to.' },
|
|
447
|
+
{ token: '[dir]', key: 'dir', description: 'Project directory (default: current dir).' },
|
|
448
|
+
],
|
|
449
|
+
options: [],
|
|
450
|
+
},
|
|
451
|
+
examples: ['notis apps link abc123', 'notis apps link abc123 ./my-app'],
|
|
452
|
+
mutates: true,
|
|
453
|
+
idempotent: true,
|
|
454
|
+
require_auth: false,
|
|
455
|
+
backend_call: { type: 'local', name: 'write_link_state' },
|
|
456
|
+
handler: appsLinkHandler,
|
|
457
|
+
},
|
|
458
|
+
{
|
|
459
|
+
command_path: ['apps', 'deploy'],
|
|
460
|
+
summary: 'Build and upload the app to the linked Notis app.',
|
|
461
|
+
when_to_use:
|
|
462
|
+
'Ship the installed app to production for the linked user/team app. Requires a linked app (notis apps link). This command does not publish to the app store.',
|
|
463
|
+
args_schema: {
|
|
464
|
+
arguments: [
|
|
465
|
+
{ token: '[dir]', key: 'dir', description: 'Project directory (default: current dir).' },
|
|
466
|
+
],
|
|
467
|
+
options: [
|
|
468
|
+
{ flags: '--app-id <id>', description: 'Override linked app ID.' },
|
|
469
|
+
{ flags: '--skip-build', description: 'Skip the build step (use existing .notis/output/).' },
|
|
470
|
+
{ flags: '--direct', description: 'Upload directly to Supabase storage, bypassing the backend server. Auto-fallback on network errors.' },
|
|
471
|
+
],
|
|
472
|
+
},
|
|
473
|
+
examples: ['notis apps deploy', 'notis apps deploy --skip-build', 'notis apps deploy --app-id abc123', 'notis apps deploy --direct'],
|
|
474
|
+
mutates: true,
|
|
475
|
+
idempotent: true,
|
|
476
|
+
backend_call: { type: 'tool', name: 'notis_save_app_files' },
|
|
477
|
+
handler: appsDeployHandler,
|
|
478
|
+
},
|
|
479
|
+
{
|
|
480
|
+
command_path: ['apps', 'doctor'],
|
|
481
|
+
summary: 'Check project health and readiness.',
|
|
482
|
+
when_to_use: 'Diagnose issues with a Notis app project.',
|
|
483
|
+
args_schema: {
|
|
484
|
+
arguments: [
|
|
485
|
+
{ token: '[dir]', key: 'dir', description: 'Project directory (default: current dir).' },
|
|
486
|
+
],
|
|
487
|
+
options: [],
|
|
488
|
+
},
|
|
489
|
+
examples: ['notis apps doctor', 'notis apps doctor ./my-app'],
|
|
490
|
+
mutates: false,
|
|
491
|
+
idempotent: true,
|
|
492
|
+
require_auth: false,
|
|
493
|
+
backend_call: { type: 'local', name: 'project_health_check' },
|
|
494
|
+
handler: appsDoctorHandler,
|
|
495
|
+
},
|
|
496
|
+
];
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
import { CliError, EXIT_CODES } from '../runtime/errors.js';
|
|
2
|
+
import {
|
|
3
|
+
clearStoredJwt,
|
|
4
|
+
probeAuth,
|
|
5
|
+
promptForJwt,
|
|
6
|
+
updateStoredProfile,
|
|
7
|
+
} from './helpers.js';
|
|
8
|
+
|
|
9
|
+
async function authLoginHandler(ctx) {
|
|
10
|
+
let jwt = ctx.options.jwt || process.env.NOTIS_JWT;
|
|
11
|
+
if (!jwt) {
|
|
12
|
+
if (ctx.runtime.nonInteractive) {
|
|
13
|
+
throw new CliError({
|
|
14
|
+
code: 'auth_missing',
|
|
15
|
+
message: `No JWT configured for profile ${ctx.runtime.profileName}`,
|
|
16
|
+
exitCode: EXIT_CODES.auth,
|
|
17
|
+
hints: [
|
|
18
|
+
{
|
|
19
|
+
command: 'notis auth login --jwt <token>',
|
|
20
|
+
reason: 'Provide a token explicitly in non-interactive mode',
|
|
21
|
+
},
|
|
22
|
+
],
|
|
23
|
+
});
|
|
24
|
+
}
|
|
25
|
+
jwt = await promptForJwt();
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
updateStoredProfile({
|
|
29
|
+
profileName: ctx.runtime.profileName,
|
|
30
|
+
jwt,
|
|
31
|
+
apiBase: ctx.globalOptions.apiBase,
|
|
32
|
+
setCurrent: true,
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
return ctx.output.emitSuccess({
|
|
36
|
+
command: ctx.spec.command_path.join(' '),
|
|
37
|
+
data: {
|
|
38
|
+
profile: ctx.runtime.profileName,
|
|
39
|
+
api_base: ctx.globalOptions.apiBase || ctx.runtime.apiBase,
|
|
40
|
+
},
|
|
41
|
+
humanSummary: `Stored credentials for profile ${ctx.runtime.profileName}`,
|
|
42
|
+
hints: [
|
|
43
|
+
{ command: 'notis whoami', reason: 'Verify your identity and available toolkits' },
|
|
44
|
+
],
|
|
45
|
+
meta: { mutating: true },
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
async function authLogoutHandler(ctx) {
|
|
50
|
+
clearStoredJwt(ctx.runtime.profileName);
|
|
51
|
+
return ctx.output.emitSuccess({
|
|
52
|
+
command: ctx.spec.command_path.join(' '),
|
|
53
|
+
data: { profile: ctx.runtime.profileName },
|
|
54
|
+
humanSummary: `Removed stored JWT for profile ${ctx.runtime.profileName}`,
|
|
55
|
+
meta: { mutating: true },
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
async function authStatusHandler(ctx) {
|
|
60
|
+
const hasJwt = Boolean(ctx.runtime.jwt);
|
|
61
|
+
let verified = false;
|
|
62
|
+
let toolkits = [];
|
|
63
|
+
if (hasJwt && ctx.options.verify) {
|
|
64
|
+
const payload = await probeAuth(ctx.runtime);
|
|
65
|
+
toolkits = payload.toolkits || [];
|
|
66
|
+
verified = true;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
return ctx.output.emitSuccess({
|
|
70
|
+
command: ctx.spec.command_path.join(' '),
|
|
71
|
+
data: {
|
|
72
|
+
profile: ctx.runtime.profileName,
|
|
73
|
+
api_base: ctx.runtime.apiBase,
|
|
74
|
+
has_jwt: hasJwt,
|
|
75
|
+
verified,
|
|
76
|
+
toolkits,
|
|
77
|
+
},
|
|
78
|
+
humanSummary: hasJwt
|
|
79
|
+
? `Profile ${ctx.runtime.profileName} is configured`
|
|
80
|
+
: `Profile ${ctx.runtime.profileName} is missing a JWT`,
|
|
81
|
+
hints: hasJwt
|
|
82
|
+
? []
|
|
83
|
+
: [{ command: 'notis auth login --jwt <token>', reason: 'Configure credentials for this profile' }],
|
|
84
|
+
meta: { mutating: false },
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export const authCommandSpecs = [
|
|
89
|
+
{
|
|
90
|
+
command_path: ['auth', 'login'],
|
|
91
|
+
summary: 'Store credentials for a named CLI profile.',
|
|
92
|
+
when_to_use: 'Use this before authenticated commands, especially in fresh environments or CI profiles.',
|
|
93
|
+
args_schema: {
|
|
94
|
+
arguments: [],
|
|
95
|
+
options: [
|
|
96
|
+
{ flags: '--jwt <token>', description: 'JWT token to store for the profile.' },
|
|
97
|
+
],
|
|
98
|
+
},
|
|
99
|
+
examples: ['notis auth login --jwt <token>', 'notis auth login --profile staging --api-base http://localhost:3001'],
|
|
100
|
+
output_schema: 'Returns the active profile name and API base.',
|
|
101
|
+
mutates: true,
|
|
102
|
+
idempotent: true,
|
|
103
|
+
require_auth: false,
|
|
104
|
+
related_commands: ['notis auth status --verify', 'notis auth logout'],
|
|
105
|
+
backend_call: { type: 'local' },
|
|
106
|
+
handler: authLoginHandler,
|
|
107
|
+
},
|
|
108
|
+
{
|
|
109
|
+
command_path: ['auth', 'logout'],
|
|
110
|
+
summary: 'Remove the stored JWT for the active profile.',
|
|
111
|
+
when_to_use: 'Use this to clear local credentials without touching other profiles.',
|
|
112
|
+
args_schema: { arguments: [], options: [] },
|
|
113
|
+
examples: ['notis auth logout', 'notis auth logout --profile staging'],
|
|
114
|
+
output_schema: 'Returns the profile that was updated.',
|
|
115
|
+
mutates: true,
|
|
116
|
+
idempotent: true,
|
|
117
|
+
require_auth: false,
|
|
118
|
+
related_commands: ['notis auth login --jwt <token>', 'notis auth status'],
|
|
119
|
+
backend_call: { type: 'local' },
|
|
120
|
+
handler: authLogoutHandler,
|
|
121
|
+
},
|
|
122
|
+
{
|
|
123
|
+
command_path: ['auth', 'status'],
|
|
124
|
+
summary: 'Inspect local auth configuration and optionally verify it against the API.',
|
|
125
|
+
when_to_use: 'Use this before automating commands to confirm the active profile and token health.',
|
|
126
|
+
args_schema: {
|
|
127
|
+
arguments: [],
|
|
128
|
+
options: [
|
|
129
|
+
{ flags: '--verify', description: 'Perform a live authenticated roundtrip to the API.' },
|
|
130
|
+
],
|
|
131
|
+
},
|
|
132
|
+
examples: ['notis auth status', 'notis auth status --verify --json'],
|
|
133
|
+
output_schema: 'Returns profile, api_base, local auth presence, and optional verification data.',
|
|
134
|
+
mutates: false,
|
|
135
|
+
idempotent: true,
|
|
136
|
+
require_auth: false,
|
|
137
|
+
related_commands: ['notis doctor', 'notis auth login --jwt <token>'],
|
|
138
|
+
backend_call: { type: 'notis_find_toolkits' },
|
|
139
|
+
handler: authStatusHandler,
|
|
140
|
+
},
|
|
141
|
+
{
|
|
142
|
+
command_path: ['login'],
|
|
143
|
+
display_name: 'login',
|
|
144
|
+
summary: 'Deprecated alias for `notis auth login`.',
|
|
145
|
+
when_to_use: 'Use only for backward compatibility with older scripts.',
|
|
146
|
+
args_schema: {
|
|
147
|
+
arguments: [],
|
|
148
|
+
options: [
|
|
149
|
+
{ flags: '--jwt <token>', description: 'JWT token to store for the profile.' },
|
|
150
|
+
],
|
|
151
|
+
},
|
|
152
|
+
examples: ['notis login --jwt <token>'],
|
|
153
|
+
output_schema: 'Same output as `notis auth login`.',
|
|
154
|
+
mutates: true,
|
|
155
|
+
idempotent: true,
|
|
156
|
+
require_auth: false,
|
|
157
|
+
related_commands: ['notis auth login --jwt <token>'],
|
|
158
|
+
backend_call: { type: 'local', alias_for: 'auth login' },
|
|
159
|
+
deprecated_alias_for: 'notis auth login',
|
|
160
|
+
handler: authLoginHandler,
|
|
161
|
+
},
|
|
162
|
+
{
|
|
163
|
+
command_path: ['logout'],
|
|
164
|
+
display_name: 'logout',
|
|
165
|
+
summary: 'Deprecated alias for `notis auth logout`.',
|
|
166
|
+
when_to_use: 'Use only for backward compatibility with older scripts.',
|
|
167
|
+
args_schema: { arguments: [], options: [] },
|
|
168
|
+
examples: ['notis logout'],
|
|
169
|
+
output_schema: 'Same output as `notis auth logout`.',
|
|
170
|
+
mutates: true,
|
|
171
|
+
idempotent: true,
|
|
172
|
+
require_auth: false,
|
|
173
|
+
related_commands: ['notis auth logout'],
|
|
174
|
+
backend_call: { type: 'local', alias_for: 'auth logout' },
|
|
175
|
+
deprecated_alias_for: 'notis auth logout',
|
|
176
|
+
handler: authLogoutHandler,
|
|
177
|
+
},
|
|
178
|
+
];
|