diff --git a/client/packages/cli/__tests__/e2e/helpers.ts b/client/packages/cli/__tests__/e2e/helpers.ts index fda69259c8..e6f64a50cd 100644 --- a/client/packages/cli/__tests__/e2e/helpers.ts +++ b/client/packages/cli/__tests__/e2e/helpers.ts @@ -5,7 +5,7 @@ import { tmpdir } from 'os'; import { fileURLToPath } from 'url'; const __dirname = dirname(fileURLToPath(import.meta.url)); -const CLI_BIN = join(__dirname, '../../bin/index.js'); +const CLI_BIN = join(__dirname, '../../dist/new/index.js'); const apiUrl = process.env.INSTANT_CLI_API_URI || 'https://api.instantdb.com'; diff --git a/client/packages/cli/package.json b/client/packages/cli/package.json index c4fe96d6a2..0a1c605be4 100644 --- a/client/packages/cli/package.json +++ b/client/packages/cli/package.json @@ -18,9 +18,13 @@ } }, "bin": { - "instant-cli": "bin/index.js" + "instant-cli": "bin/index.js", + "new-instant-cli": "dist/new/index.js" }, "dependencies": { + "@commander-js/extra-typings": "^14.0.0", + "@effect/platform": "^0.94.1", + "@effect/platform-node": "^0.104.0", "@instantdb/core": "workspace:*", "@instantdb/platform": "workspace:*", "@instantdb/version": "workspace:*", @@ -32,6 +36,7 @@ "commander": "^12.1.0", "dotenv": "^16.3.1", "dotenv-flow": "^4.1.0", + "effect": "^3.19.14", "env-paths": "^3.0.0", "find-up-simple": "^1.0.0", "json-diff": "^1.0.6", @@ -39,6 +44,8 @@ "lodash.throttle": "^4.1.1", "open": "^10.1.0", "package-directory": "^8.1.0", + "package-manager-detector": "^1.6.0", + "pkg-types": "^2.3.0", "prettier": "^3.3.3", "sisteransi": "^1.0.5", "string-width": "^8.1.0", @@ -52,14 +59,16 @@ "test": "vitest --project unit", "test:ci": "vitest run", "publish-package": "pnpm pack && npm publish *.tgz --access public", - "clean": "rm -rf dist" + "clean": "rm -rf dist", + "prepare": "effect-language-service patch" }, "devDependencies": { "@babel/core": "^7.17.9", "@babel/preset-env": "^7.16.11", + "@effect/language-service": "^0.64.1", "@types/json-diff": "^1.0.3", "@types/lodash.throttle": "^4.1.9", - "@types/node": "^22.6.1", + "@types/node": "^22.13.0", "npm-run-all": "^4.1.5", "typescript": "^5.9.3", "vitest": "^3.2.4" diff --git a/client/packages/cli/src/index.js b/client/packages/cli/src/index.js index 8031fc40a4..6ffea0c851 100644 --- a/client/packages/cli/src/index.js +++ b/client/packages/cli/src/index.js @@ -393,7 +393,7 @@ program .option('--title ', 'Title for the created app') .action(handleInit); -program +const initWithoutFilesDef = program .command('init-without-files') .description('Generate a new app id and admin token pair without any files.') .option('--title <title>', 'Title for the created app.') @@ -601,7 +601,11 @@ program await handleQuery(queryArg, opts); }); -program.parse(process.argv); +// Only parse the old version of the cli if the command is "instant-cli" +// because this file gets imported places +if (process.argv[1].split('/').pop() === 'instant-cli') { + program.parse(process.argv); +} async function handleInit(opts) { const pkgAndAuthInfo = @@ -884,7 +888,7 @@ async function getOrCreateAppAndWriteToEnv(pkgAndAuthInfo, opts) { async function pull(bag, appId, pkgAndAuthInfo) { if (bag === 'schema' || bag === 'all') { - const { ok } = await pullSchema(appId, pkgAndAuthInfo); + const { ok } = await oldPullSchema(appId, pkgAndAuthInfo); if (!ok) return process.exit(1); } if (bag === 'perms' || bag === 'all') { @@ -1043,80 +1047,6 @@ async function getOrInstallInstantModuleWithErrorLogging(pkgDir, opts) { return moduleName; } -async function promptCreateApp(opts) { - const id = randomUUID(); - const token = randomUUID(); - - let _title; - if (opts?.title) { - _title = opts.title; - } else { - _title = await renderUnwrap( - new UI.TextInput({ - prompt: 'What would you like to call it?', - placeholder: 'My cool app', - }), - ).catch(() => null); - } - - const title = _title?.trim(); - - if (!title) { - error('No name provided.'); - return { ok: false }; - } - - const res = await fetchJson({ - debugName: 'Fetching orgs', - method: 'GET', - path: '/dash', - errorMessage: 'Failed to fetch apps.', - command: 'init', - }); - if (!res.ok) { - return { ok: false }; - } - - const allowedOrgs = res.data.orgs.filter((org) => org.role !== 'app-member'); - - let org_id = opts.org; - - if (!org_id && allowedOrgs.length) { - const choices = [{ label: '(No organization)', value: null }]; - for (const org of allowedOrgs) { - choices.push({ label: org.title, value: org.id }); - } - const choice = await renderUnwrap( - new UI.Select({ - promptText: 'Would you like to create the app in an organization?', - options: choices, - }), - ); - if (choice) { - org_id = choice; - } - } - - const app = { id, title, admin_token: token, org_id }; - const appRes = await fetchJson({ - method: 'POST', - path: '/dash/apps', - debugName: 'App create', - errorMessage: 'Failed to create app.', - body: app, - command: 'init', - }); - - if (!appRes.ok) return { ok: false }; - return { - ok: true, - appId: id, - appTitle: title, - appToken: token, - source: 'created', - }; -} - async function promptImportAppOrCreateApp() { const res = await fetchJson({ debugName: 'Fetching apps', @@ -1353,7 +1283,7 @@ async function getOrPromptPackageAndAuthInfoWithErrorLogging(opts) { return { pkgDir, projectType, instantModuleName, authToken }; } -async function pullSchema( +async function oldPullSchema( appId, { pkgDir, instantModuleName, experimentalTypePreservation }, ) { @@ -1608,7 +1538,8 @@ function jobGroupDescription(jobs) { return joinInSentence([...actions].sort()) || 'updating schema'; } -async function waitForIndexingJobsToFinish(appId, data) { +// TODO: rewrite in effect +export async function waitForIndexingJobsToFinish(appId, data) { const spinnerDefferedPromise = deferred(); const spinner = new UI.Spinner({ promise: spinnerDefferedPromise.promise, @@ -1694,7 +1625,7 @@ async function waitForIndexingJobsToFinish(appId, data) { } } -const resolveRenames = async (created, promptData, extraInfo) => { +export const resolveRenames = async (created, promptData, extraInfo) => { const answer = await renderUnwrap( new ResolveRenamePrompt( created, @@ -2050,7 +1981,7 @@ function prettyPrintJSONErr(data) { } } -async function readLocalPermsFile() { +export async function readLocalPermsFile() { const readCandidates = getPermsReadCandidates(); const res = await loadConfig({ sources: readCandidates, @@ -2071,7 +2002,7 @@ async function readLocalPermsFileWithErrorLogging() { return res; } -async function readLocalSchemaFile() { +export async function readLocalSchemaFile() { const readCandidates = getSchemaReadCandidates(); const res = await loadConfig({ sources: readCandidates, @@ -2082,7 +2013,7 @@ async function readLocalSchemaFile() { return { path: relativePath, schema: res.config }; } -async function readInstantConfigFile() { +export async function readInstantConfigFile() { return ( await loadConfig({ sources: [ @@ -2189,7 +2120,7 @@ function sleep(ms) { return new Promise((resolve) => setTimeout(resolve, ms)); } -function countEntities(o) { +export function countEntities(o) { return Object.keys(o).length; } diff --git a/client/packages/cli/src/new/commands/claim.ts b/client/packages/cli/src/new/commands/claim.ts new file mode 100644 index 0000000000..d3d9983a5c --- /dev/null +++ b/client/packages/cli/src/new/commands/claim.ts @@ -0,0 +1,32 @@ +import { HttpClient, HttpClientRequest } from '@effect/platform'; +import chalk from 'chalk'; +import { Effect } from 'effect'; +import { CurrentApp } from '../context/currentApp.js'; +import { BadArgsError } from '../errors.js'; +import { WithAppLayer } from '../layer.js'; +import { InstantHttpAuthed } from '../lib/http.js'; + +export const claimCommand = Effect.gen(function* () { + const { appId, adminToken } = yield* CurrentApp; + + yield* Effect.log(`Found app: ${appId}`); + + const http = yield* InstantHttpAuthed; + + if (!adminToken) { + return yield* BadArgsError.make({ message: 'Missing app admin token' }); + } + + yield* http + .pipe( + HttpClient.mapRequestInputEffect( + HttpClientRequest.bodyJson({ + app_id: appId, + token: adminToken, + }), + ), + ) + .post(`/dash/apps/ephemeral/${appId}/claim`); + + yield* Effect.log(chalk.green('App claimed!')); +}); diff --git a/client/packages/cli/src/new/commands/explorer.ts b/client/packages/cli/src/new/commands/explorer.ts new file mode 100644 index 0000000000..c8536111d6 --- /dev/null +++ b/client/packages/cli/src/new/commands/explorer.ts @@ -0,0 +1,20 @@ +import { Effect } from 'effect'; +import openInBrowser from 'open'; +import { explorerDef, OptsFromCommand } from '../index.js'; +import { CurrentApp } from '../context/currentApp.js'; +import { getDashUrl } from '../lib/http.js'; + +export const explorerCmd = (opts: OptsFromCommand<typeof explorerDef>) => + Effect.gen(function* () { + const { appId } = yield* CurrentApp; + const dashUrl = yield* getDashUrl; + yield* Effect.log('Opening Explorer...'); + const url = `${dashUrl}/dash?s=main&app=${appId}&t=explorer`; + yield* Effect.tryPromise(() => openInBrowser(url)).pipe( + Effect.catchAll(() => + Effect.log( + `Failed to open Explorer in browser\nOpen Explorer manually:\n${url}`, + ), + ), + ); + }); diff --git a/client/packages/cli/src/new/commands/info.ts b/client/packages/cli/src/new/commands/info.ts new file mode 100644 index 0000000000..372f807b22 --- /dev/null +++ b/client/packages/cli/src/new/commands/info.ts @@ -0,0 +1,35 @@ +import { HttpClientResponse } from '@effect/platform'; +import { Effect, Layer, pipe, Schema, Option } from 'effect'; +import { AuthLayerLive } from '../layer.js'; +import { InstantHttpAuthed } from '../lib/http.js'; +import { version } from '@instantdb/version'; + +const DashMeResponse = Schema.Struct({ + user: Schema.Struct({ + id: Schema.String, + email: Schema.String, + created_at: Schema.String, + }), +}); + +export const infoCommand = () => + Effect.gen(function* () { + const http = yield* Effect.serviceOption(InstantHttpAuthed).pipe( + Effect.map(Option.getOrNull), + ); + + yield* Effect.log('CLI Version:', version); + // If logged in.. + if (http) { + const meData = yield* http.get('/dash/me').pipe( + Effect.flatMap(HttpClientResponse.schemaBodyJson(DashMeResponse)), + Effect.mapError( + (e) => new Error("Couldn't get user information.", { cause: e }), + ), + ); + + yield* Effect.log(`Logged in as ${meData.user.email}`); + } else { + yield* Effect.log('Not logged in.'); + } + }); diff --git a/client/packages/cli/src/new/commands/init.ts b/client/packages/cli/src/new/commands/init.ts new file mode 100644 index 0000000000..935333f700 --- /dev/null +++ b/client/packages/cli/src/new/commands/init.ts @@ -0,0 +1,58 @@ +import { Effect, Option } from 'effect'; +import { OptsFromCommand, initDef } from '../index.js'; +import { WithAppLayer } from '../layer.js'; +import { readLocalPermsFile, readLocalSchemaFile } from '../../index.js'; +import { pullSchema } from '../lib/pullSchema.js'; +import { pullPerms } from '../lib/pullPerms.js'; +import { promptOk } from '../lib/ui.js'; +import { pushSchema } from '../lib/pushSchema.js'; +import { pushPerms } from '../lib/pushPerms.js'; + +export const initCommand = (options: OptsFromCommand<typeof initDef>) => + Effect.gen(function* () { + yield* Effect.matchEffect( + Effect.tryPromise(readLocalSchemaFile).pipe( + // Throws NoSuchElementException if no file found + Effect.flatMap(Option.fromNullable), + ), + { + onFailure: () => pullSchema({ experimentalTypePreservation: false }), + onSuccess: () => + Effect.gen(function* () { + const doSchemaPush = yield* promptOk( + { + promptText: 'Found local schema. Push it to the new app?', + inline: true, + }, + true, + ); + if (doSchemaPush) { + yield* pushSchema(); + } + }), + }, + ); + + yield* Effect.matchEffect( + Effect.tryPromise(readLocalPermsFile).pipe( + // Throws NoSuchElementException if no file found + Effect.flatMap(Option.fromNullable), + ), + { + onFailure: () => pullPerms, + onSuccess: () => + Effect.gen(function* () { + const doPermsPush = yield* promptOk( + { + promptText: 'Found local perms. Push it to the new app?', + inline: true, + }, + true, + ); + if (doPermsPush) { + yield* pushPerms; + } + }), + }, + ); + }); diff --git a/client/packages/cli/src/new/commands/initWithoutFiles.ts b/client/packages/cli/src/new/commands/initWithoutFiles.ts new file mode 100644 index 0000000000..96302d4505 --- /dev/null +++ b/client/packages/cli/src/new/commands/initWithoutFiles.ts @@ -0,0 +1,103 @@ +import { Effect } from 'effect'; +import { PlatformApi } from '../context/platformApi.js'; +import { BadArgsError } from '../errors.js'; +import { OptsFromCommand, initWithoutFilesDef } from '../index.js'; +import { createApp } from '../lib/createApp.js'; +import { AuthLayerLive } from '../layer.js'; +import chalk from 'chalk'; +import { NotAuthedError } from '../context/authToken.js'; + +export const initWithoutFilesCommand = ( + opts: OptsFromCommand<typeof initWithoutFilesDef>, +) => + Effect.gen(function* () { + if (!opts?.title) { + return yield* BadArgsError.make({ + message: + 'Title is required for creating a new app without local files.', + }); + } + + if (opts.title.startsWith('-')) { + return yield* BadArgsError.make({ + message: `Invalid title: "${opts.title}". Title cannot be a flag.`, + }); + } + + if (opts?.temp && opts?.orgId) { + return yield* BadArgsError.make({ + message: 'Cannot use --temp and --org-id flags together.', + }); + } + + if (!opts.temp) { + const app = yield* createApp(opts.title, opts.orgId).pipe( + Effect.provide( + AuthLayerLive({ + allowAdminToken: false, + coerce: false, + }), + ), + ); + console.error(`${chalk.green('Successfully created new app!')}\n`); + yield* Effect.log( + JSON.stringify( + { + app: { + appId: app.app.id, + adminToken: app.app['admin-token'], + }, + error: null, + }, + null, + 2, + ), + ); + } else { + const platform = yield* PlatformApi; + const app = yield* platform.use((api) => + api.createTemporaryApp({ + title: opts.title!, + }), + ); + + console.error(`${chalk.green('Successfully created new app!')}\n`); + yield* Effect.log( + JSON.stringify( + { + app: { + appId: app.app.id, + adminToken: app.app.adminToken, + }, + error: null, + }, + null, + 2, + ), + ); + } + }).pipe( + Effect.catchTag('NotAuthedError', (e) => + NotAuthedError.make({ + message: + 'Please log in first with `instant-cli login` before running this command.', + }), + ), + Effect.catchAll((e) => + Effect.gen(function* () { + yield* Effect.log( + JSON.stringify( + { + app: null, + error: { + message: e.message, + }, + }, + null, + 2, + ), + ); + process.exit(1); + }), + ), + ); diff --git a/client/packages/cli/src/new/commands/login.ts b/client/packages/cli/src/new/commands/login.ts new file mode 100644 index 0000000000..93bde3fdc9 --- /dev/null +++ b/client/packages/cli/src/new/commands/login.ts @@ -0,0 +1,75 @@ +import chalk from 'chalk'; +import { Effect } from 'effect'; +import openInBrowser from 'open'; +import { OptsFromCommand, loginDef } from '../index.js'; +import { getDashUrl } from '../lib/http.js'; +import { + getLoginTicketAndSecret, + saveConfigAuthToken, + waitForAuthToken, +} from '../lib/login.js'; +import { promptOk } from '../lib/ui.js'; + +const isHeadLessEnvironment = (opts: OptsFromCommand<typeof loginDef>) => { + const noBrowserMode = Boolean( + process.env.INSTANT_CLI_NO_BROWSER || process.env.CI || opts?.headless, + ); + + // Check for common headless environment indicators + return ( + noBrowserMode || + process.env.TERM === 'dumb' || + process.env.SSH_CONNECTION !== undefined || + process.env.SSH_CLIENT !== undefined || + (!process.env.DISPLAY && process.platform === 'linux') || + process.env.WSL_DISTRO_NAME !== undefined + ); +}; + +export const loginCommand = Effect.fn(function* ( + opts: OptsFromCommand<typeof loginDef>, +) { + yield* Effect.log("Let's log you in!"); + + const loginInfo = yield* getLoginTicketAndSecret; + const { secret, ticket } = loginInfo; + const dashOrigin = yield* getDashUrl; + yield* Effect.log(); + // TODO: flip these so rejecting the prompt prints url + if (isHeadLessEnvironment(opts)) { + yield* Effect.log( + `Open this URL in a browser to log in:\n ${dashOrigin}/dash?ticket=${ticket}\n`, + ); + } else { + const ok = yield* promptOk( + { + promptText: + 'This will open instantdb.com in your browser, OK to proceed?', + }, + true, + ); + if (!ok) { + process.exit(0); + } + yield* Effect.tryPromise(() => + openInBrowser(`${dashOrigin}/dash?ticket=${ticket}`), + ); + } + + yield* Effect.log('Waiting for authentication...'); + + const result = yield* waitForAuthToken(secret); + const { token, email } = result; + if (opts.print) { + yield* Effect.log( + chalk.red('[Do not share] Your Instant auth token:', token), + ); + } else { + yield* saveConfigAuthToken(token); + yield* Effect.log(chalk.green(`Successfully logged in as ${email}!`)); + } + return { + authToken: token, + source: 'file' as const, + }; +}); diff --git a/client/packages/cli/src/new/commands/logout.ts b/client/packages/cli/src/new/commands/logout.ts new file mode 100644 index 0000000000..760ad44b6b --- /dev/null +++ b/client/packages/cli/src/new/commands/logout.ts @@ -0,0 +1,23 @@ +import { Effect } from 'effect'; +import { getAuthPaths } from '../../util/getAuthPaths.js'; +import { FileSystem } from '@effect/platform'; +import chalk from 'chalk'; +import { SystemError } from '@effect/platform/Error'; + +export const logoutCommand = Effect.fn(function* () { + const { authConfigFilePath } = getAuthPaths(); + const fs = yield* FileSystem.FileSystem; + + yield* Effect.matchEffect(fs.remove(authConfigFilePath), { + onFailure: (e) => + Effect.gen(function* () { + if (e instanceof SystemError && e.reason === 'NotFound') { + yield* Effect.log(chalk.green('You were already logged out!')); + } else { + yield* Effect.logError(chalk.red('Failed to logout: ' + e.message)); + } + }), + onSuccess: () => + Effect.log(chalk.green('Successfully logged out from Instant!')), + }); +}); diff --git a/client/packages/cli/src/new/commands/pull.ts b/client/packages/cli/src/new/commands/pull.ts new file mode 100644 index 0000000000..90a2aaf808 --- /dev/null +++ b/client/packages/cli/src/new/commands/pull.ts @@ -0,0 +1,22 @@ +import { Effect } from 'effect'; +import { OptsFromCommand, pullDef } from '../index.js'; +import { pullSchema } from '../lib/pullSchema.js'; +import { pullPerms } from '../lib/pullPerms.js'; + +export type SchemaPermsOrBoth = 'schema' | 'perms' | 'all'; + +export const pullCommand = ( + arg: SchemaPermsOrBoth, + opts: OptsFromCommand<typeof pullDef>, +) => + Effect.gen(function* () { + arg ||= 'all'; + if (arg === 'schema' || arg === 'all') { + yield* pullSchema({ + experimentalTypePreservation: opts.experimentalTypePreservation, + }); + } + if (arg === 'perms' || arg === 'all') { + yield* pullPerms; + } + }); diff --git a/client/packages/cli/src/new/commands/push.ts b/client/packages/cli/src/new/commands/push.ts new file mode 100644 index 0000000000..04935d2a1b --- /dev/null +++ b/client/packages/cli/src/new/commands/push.ts @@ -0,0 +1,26 @@ +import { Effect } from 'effect'; +import { OptsFromCommand, pushDef } from '../index.js'; +import { WithAppLayer } from '../layer.js'; +import { PACKAGE_ALIAS_AND_FULL_NAMES } from '../context/projectInfo.js'; +import { pushPerms } from '../lib/pushPerms.js'; +import { pushSchema } from '../lib/pushSchema.js'; +import { BadArgsError } from '../errors.js'; + +export const pushCommand = ( + arg: string | undefined, + opts: OptsFromCommand<typeof pushDef>, +) => + Effect.gen(function* () { + arg ||= 'all'; + if (arg !== 'schema' && arg !== 'perms' && arg !== 'all') { + return yield* new BadArgsError({ + message: `Invalid argument: ${arg}. Expected one of schema, perms, or all`, + }); + } + if (arg === 'schema' || arg === 'all') { + yield* pushSchema(opts.rename); + } + if (arg === 'perms' || arg === 'all') { + yield* pushPerms; + } + }); diff --git a/client/packages/cli/src/new/commands/query.ts b/client/packages/cli/src/new/commands/query.ts new file mode 100644 index 0000000000..e2524eddb4 --- /dev/null +++ b/client/packages/cli/src/new/commands/query.ts @@ -0,0 +1,57 @@ +import { Effect } from 'effect'; +import JSON5 from 'json5'; +import { OptsFromCommand, queryDef } from '../index.js'; +import { CurrentApp } from '../context/currentApp.js'; +import { BadArgsError } from '../errors.js'; +import { InstantHttpAuthed, withCommand } from '../lib/http.js'; +import { HttpBody } from '@effect/platform'; + +export const queryCmd = (arg: string, opts: OptsFromCommand<typeof queryDef>) => + Effect.gen(function* () { + const { appId } = yield* CurrentApp; + const contexts = [ + opts.admin, + opts.asEmail, + opts.asGuest, + opts.asToken, + ].filter(Boolean); + if (contexts.length === 0) { + return yield* BadArgsError.make({ + message: + 'Please specify a context: --admin, --as-email <email>, or --as-guest', + }); + } + if (contexts.length > 1) { + return yield* BadArgsError.make({ + message: + 'Please specify only one context: --admin, --as-email <email>, or --as-guest', + }); + } + + const query = yield* Effect.try(() => JSON5.parse(arg)).pipe( + Effect.mapError((e) => + BadArgsError.make({ + message: `Invalid query: ${e.cause}`, + }), + ), + ); + + const headers = { 'app-id': appId }; + if (opts.asEmail) { + headers['as-email'] = opts.asEmail; + } else if (opts.asGuest) { + headers['as-guest'] = 'true'; + } else if (opts.asToken) { + headers['as-token'] = opts.asToken; + } + + const http = (yield* InstantHttpAuthed).pipe(withCommand('query')); + const response = yield* http.post('/admin/query', { + headers, + body: HttpBody.unsafeJson({ + query, + }), + }); + const body = yield* response.json; + yield* Effect.log(JSON.stringify(body, null, 2)); + }); diff --git a/client/packages/cli/src/new/context/authToken.ts b/client/packages/cli/src/new/context/authToken.ts new file mode 100644 index 0000000000..380ba32272 --- /dev/null +++ b/client/packages/cli/src/new/context/authToken.ts @@ -0,0 +1,104 @@ +import { FileSystem } from '@effect/platform'; +import { Config, Context, Effect, Layer, Option, Schema } from 'effect'; +import envPaths from 'env-paths'; +import { join } from 'path'; +import { loginCommand } from '../commands/login.js'; +import { program } from '../program.js'; + +export class AuthToken extends Context.Tag('instant-cli/new/context/authToken')< + AuthToken, + { + authToken: string; + source: 'admin' | 'env' | 'opt' | 'file'; + } +>() {} + +export class NotAuthedError extends Schema.TaggedError<NotAuthedError>( + 'NotAuthedError', +)('NotAuthedError', { + message: Schema.String, +}) {} + +export const authTokenGetEffect = (allowAdminToken: boolean = true) => + Effect.gen(function* () { + const options = program.opts() as Record<string, any>; + if (typeof options.token === 'string') { + return { + authToken: options.token, + source: 'opt' as 'env' | 'opt' | 'file', + }; + } + + const env = yield* Config.string('INSTANT_CLI_AUTH_TOKEN').pipe( + Config.option, + Config.map(Option.getOrNull), + ); + + if (env) { + return { + authToken: env, + source: 'env' as 'env' | 'opt' | 'file', + }; + } + + const secondaryEnv = yield* Config.string('INSTANT_APP_ADMIN_TOKEN').pipe( + Config.orElse(() => Config.string('INSTANT_ADMIN_TOKEN')), + Config.option, + Config.map(Option.getOrNull), + ); + if (secondaryEnv && allowAdminToken) { + return { + authToken: secondaryEnv, + source: 'admin' as 'admin', + }; + } + + const authPaths = yield* getAuthPaths; + const fs = yield* FileSystem.FileSystem; + const file = yield* fs + .readFileString(authPaths.authConfigFilePath, 'utf8') + .pipe( + // will usually fail if file not found, return null instead + Effect.orElseSucceed(() => null), + ); + if (file) { + return { + authToken: file, + source: 'file' as 'env' | 'opt' | 'file', + }; + } + + return yield* NotAuthedError.make({ message: 'You are not logged in' }); + }); + +export const AuthTokenLive = ({ + coerce, + allowAdminToken = true, +}: { + coerce: boolean; + allowAdminToken: boolean; +}) => + Layer.effect( + AuthToken, + authTokenGetEffect(allowAdminToken).pipe( + Effect.catchTag('NotAuthedError', (e) => + Effect.gen(function* () { + if (coerce) { + return yield* loginCommand({}); + } else { + return yield* e; + } + }), + ), + ), + ); + +const getAuthPaths = Effect.gen(function* () { + const dev = yield* Config.boolean('INSTANT_CLI_DEV').pipe( + Config.withDefault(false), + ); + const key = `instantdb-${dev ? 'dev' : 'prod'}`; + const { config: appConfigDirPath } = envPaths(key); + const authConfigFilePath = join(appConfigDirPath, 'a'); + return { authConfigFilePath, appConfigDirPath }; +}); diff --git a/client/packages/cli/src/new/context/currentApp.ts b/client/packages/cli/src/new/context/currentApp.ts new file mode 100644 index 0000000000..f4cb1e23bf --- /dev/null +++ b/client/packages/cli/src/new/context/currentApp.ts @@ -0,0 +1,265 @@ +import { HttpClientRequest, HttpClientResponse } from '@effect/platform'; +import { randomUUID } from 'crypto'; +import { Context, Data, Effect, Layer, Runtime, Schema, Option } from 'effect'; +import { UI } from '../../ui/index.js'; +import { handleEnv } from '../lib/handleEnv.js'; +import { getBaseUrl, InstantHttpAuthed, withCommand } from '../lib/http.js'; +import { runUIEffect } from '../lib/ui.js'; +import { AuthToken } from './authToken.js'; +import { GlobalOpts } from './globalOpts.js'; +import { PlatformApi } from './platformApi.js'; +import { readInstantConfigFile } from '../../index.js'; +import { BadArgsError } from '../errors.js'; + +export type CurrentAppInfo = { + appId: string; + adminToken?: string; + source: 'create' | 'import' | 'env' | 'flag' | 'ephemeral'; +}; + +export class CurrentApp extends Context.Tag( + 'instant-cli/new/context/currentApp', +)<CurrentApp, CurrentAppInfo>() {} + +function isUUID(uuid: string) { + const uuidRegex = + /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i; + return uuidRegex.test(uuid); +} + +export class CurrentAppContextError extends Data.TaggedError( + 'CurrentAppContextError', +)<{ + message: string; +}> {} + +export class AppNotFoundError extends Data.TaggedError('AppNotFoundError')<{ + message: string; +}> {} + +export const potentialEnvs: Record<string, string> = { + catchall: 'INSTANT_APP_ID', + next: 'NEXT_PUBLIC_INSTANT_APP_ID', + svelte: 'PUBLIC_INSTANT_APP_ID', + vite: 'VITE_INSTANT_APP_ID', + expo: 'EXPO_PUBLIC_INSTANT_APP_ID', + nuxt: 'NUXT_PUBLIC_INSTANT_APP_ID', + bun: 'BUN_PUBLIC_INSTANT_APP_ID', +}; + +const potentialAdminTokenEnvs = { + default: 'INSTANT_APP_ADMIN_TOKEN', + short: 'INSTANT_ADMIN_TOKEN', +} as const; + +// TODO: add instant.config.ts support +export const CurrentAppLive = (args: { + appId?: string; + coerce?: boolean; + title?: string; + applyEnv?: boolean; +}) => + Layer.effect( + CurrentApp, + Effect.gen(function* () { + if (args.appId) { + if (isUUID(args.appId)) { + return { + appId: args.appId, + source: 'flag' as const, + }; + } else { + // Check for instant.config.ts + const config = yield* Effect.tryPromise(() => + readInstantConfigFile(), + ); + if (!config) { + return yield* new BadArgsError({ + message: `App ID provided (${args.appId}) is not valid UUID`, + }); + } + const nameMatch = config?.apps?.[args.appId]; + if (!nameMatch?.id) { + return yield* BadArgsError.make({ + message: 'App ID not found in app inside instant.config.ts', + }); + } + if (nameMatch) { + return { + appId: nameMatch.id, + source: 'flag' as const, + }; + } + } + } + + // Detect from ENV + const found = Object.keys(potentialEnvs) + .map((type) => { + const envName = potentialEnvs[type]; + const value = process.env[envName]; + return { type, envName, value }; + }) + .find(({ value }) => !!value); + + if (found?.value && !isUUID(found.value)) { + return yield* new CurrentAppContextError({ + message: `Invalid UUID: ${found.value}`, + }); + } else if (found?.value) { + // try to get admin token + const adminToken = yield* getAdminToken; + return { + adminToken: Option.getOrUndefined(adminToken), + appId: found?.value, + source: 'env' as const, + }; + } + return yield* new AppNotFoundError({ + message: 'No app found', + }); + }).pipe( + // coerce into new app if app not found + Effect.catchTag('AppNotFoundError', () => + Effect.gen(function* () { + if (!args.coerce) + return yield* new AppNotFoundError({ message: 'No app found' }); + + // coerce into a new app + const globalOpts = yield* GlobalOpts; + if (globalOpts.yes) { + if (!args.title) { + return yield* new CurrentAppContextError({ + message: `Title is required when using --yes and no app is linked`, + }); + } else { + yield* Effect.log(`Creating app with title: ${args.title}`); + return yield* createApp(args.title); + } + } + + return yield* promptImportOrCreateApp; + }), + ), + + // Handle save env + Effect.tap((app) => + Effect.gen(function* () { + if ( + args.applyEnv && + (app.source === 'import' || app.source == 'create') + ) { + yield* handleEnv(app); + } + }), + ), + ), + ); + +const createApp = Effect.fn(function* (title: string, orgId?: string) { + const id = randomUUID(); + const token = randomUUID(); + const app = { id, title, admin_token: token, org_id: orgId }; + + const http = yield* InstantHttpAuthed; + yield* HttpClientRequest.post('/dash/apps').pipe( + HttpClientRequest.bodyJson(app), + Effect.flatMap(http.execute), + ); + return { + appId: id, + source: 'create', + adminToken: token, + } satisfies CurrentAppInfo; +}); + +const promptImportOrCreateApp = Effect.gen(function* () { + const api = yield* getSimpleApi; + const result = yield* runUIEffect( + new UI.AppSelector({ + startingMenuIndex: 0, + allowEphemeral: true, + allowCreate: true, + api, + }), + ); + + if (result.approach === 'import') { + yield* Effect.fork(trackAppImport(result.appId)); + } + + return { + appId: result.appId, + source: result.approach, + adminToken: result.adminToken, + } satisfies CurrentAppInfo; +}); + +const getSimpleApi = Effect.gen(function* () { + const effectRuntime = yield* Effect.runtime<never>(); + + const http = yield* InstantHttpAuthed; + const dashData = yield* http + .get('/dash') + .pipe(Effect.flatMap(HttpClientResponse.schemaBodyJson(Schema.Any))); + const platform = yield* PlatformApi; + + const baseUrl = yield* getBaseUrl; + const { authToken } = yield* AuthToken; + + return { + getDash: () => dashData, + createApp: async (title, orgId) => { + return Runtime.runPromise( + effectRuntime, + createApp(title, orgId).pipe( + Effect.provideService(InstantHttpAuthed, http), + ), + ); + }, + + createEphemeralApp: async (title) => { + return await Runtime.runPromise( + effectRuntime, + Effect.gen(function* () { + const platform = yield* PlatformApi; + const response = yield* platform.use( + (p) => p.createTemporaryApp({ title: title }), + 'Error creating temporary app', + ); + return { + appId: response.app.id, + adminToken: response.app.adminToken, + }; + }).pipe(Effect.provideService(PlatformApi, platform)), + ); + }, + + async getAppsForOrg(orgId) { + const response = await fetch(baseUrl + '/dash/orgs/' + orgId, { + headers: { + Authorization: `Bearer ${authToken}`, + }, + }); + const data = await response.json(); + return { apps: data.apps }; + }, + } satisfies UI.AppSelectorApi; +}); + +const getAdminToken = Effect.gen(function* () { + const found = Object.keys(potentialAdminTokenEnvs) + .map((type) => { + const envName = potentialAdminTokenEnvs[type]; + const value = process.env[envName]; + return { type, envName, value }; + }) + .find(({ value }) => !!value); + return Option.fromNullable(found?.value); +}); + +const trackAppImport = (appId: string) => + Effect.gen(function* () { + const http = (yield* InstantHttpAuthed).pipe(withCommand('init')); + yield* http.post(`/dash/apps/${appId}/track-import`); + }); diff --git a/client/packages/cli/src/new/context/globalOpts.ts b/client/packages/cli/src/new/context/globalOpts.ts new file mode 100644 index 0000000000..bba2836d65 --- /dev/null +++ b/client/packages/cli/src/new/context/globalOpts.ts @@ -0,0 +1,22 @@ +import { Context, Effect, Layer } from 'effect'; +import { program } from '../program.js'; + +export class GlobalOpts extends Context.Tag( + 'instant-cli/new/context/globalOpts', +)< + GlobalOpts, + { + token?: string; + yes: boolean; + env?: string; + } +>() {} + +export const GlobalOptsLive = Layer.sync(GlobalOpts, () => { + const opts = program.optsWithGlobals() as Record<string, any>; + return { + yes: opts?.yes || false, + token: opts?.token, + env: opts?.env, + }; +}); diff --git a/client/packages/cli/src/new/context/platformApi.ts b/client/packages/cli/src/new/context/platformApi.ts new file mode 100644 index 0000000000..28509748e7 --- /dev/null +++ b/client/packages/cli/src/new/context/platformApi.ts @@ -0,0 +1,35 @@ +import { PlatformApi as InstantPlatformApi } from '@instantdb/platform'; +import { Data, Effect } from 'effect'; +import { getBaseUrl } from '../lib/http.js'; + +export class PlatformApiError extends Data.TaggedError('PlatformApiError')<{ + message: string; + cause: unknown; +}> {} + +export class PlatformApi extends Effect.Service<PlatformApi>()( + 'instant-cli/new/context/platformApi', + { + effect: Effect.gen(function* () { + const origin = yield* getBaseUrl; + const apiClient = new InstantPlatformApi({ + apiURI: origin, + }); + + return { + use: <R>( + fun: (api: typeof apiClient) => Promise<R>, + errorMessage?: string, + ) => + Effect.tryPromise({ + try: (_signal) => fun(apiClient), + catch: (e) => + new PlatformApiError({ + message: errorMessage || 'Error using platform api', + cause: e, + }), + }), + }; + }), + }, +) {} diff --git a/client/packages/cli/src/new/context/projectInfo.ts b/client/packages/cli/src/new/context/projectInfo.ts new file mode 100644 index 0000000000..201d110c9a --- /dev/null +++ b/client/packages/cli/src/new/context/projectInfo.ts @@ -0,0 +1,193 @@ +import { Context, Data, Effect, Layer } from 'effect'; +import { detect } from 'package-manager-detector/detect'; +import { PackageJson, readPackage } from 'pkg-types'; + +import { exec } from 'child_process'; +import { promisify } from 'util'; +import { UI } from '../../ui/index.js'; +import { findProjectDir } from '../../util/projectDir.js'; +import { runUIEffect } from '../lib/ui.js'; +import chalk from 'chalk'; +import { GlobalOpts } from './globalOpts.js'; +import { BadArgsError } from '../errors.js'; + +export class ProjectInfo extends Context.Tag( + 'instant-cli/new/context/projectInfo', +)< + ProjectInfo, + { + pkgDir: string; + projectType: 'node' | 'deno'; + instantModuleName: string; + } +>() {} + +const execAsync = promisify(exec); + +export const PACKAGE_ALIAS_AND_FULL_NAMES = { + react: '@instantdb/react', + 'react-native': '@instantdb/react-native', + core: '@instantdb/core', + admin: '@instantdb/admin', + solid: '@instantdb/solidjs', + svelte: '@instantdb/svelte', +}; + +export class ProjectInfoError extends Data.TaggedError('ProjectInfoError')<{ + message: string; + cause?: unknown; +}> {} + +const getProjectInfo = ( + coerce: boolean = true, + packageName?: keyof typeof PACKAGE_ALIAS_AND_FULL_NAMES, +) => + Effect.gen(function* () { + const projectDir = yield* Effect.tryPromise({ + try: () => findProjectDir(), + catch: (e) => + new ProjectInfoError({ message: "Couldn't get project dir" }), + }); + + if (!projectDir) { + return yield* new ProjectInfoError({ + message: + "Couldn't find a project directory (package.json). Is there a package.json or deno.json file", + }); + } + + if (projectDir.type === 'deno') { + return { + pkgDir: projectDir.dir, + projectType: projectDir.type, + instantModuleName: '@instantdb/core', + }; + } + + const pkgJson = yield* Effect.tryPromise({ + try: () => readPackage(), + catch: () => + new ProjectInfoError({ message: "Couldn't read package.json" }), + }); + + yield* Effect.log('Checking for an Instant SDK...'); + let moduleName = getInstantModuleName(pkgJson); + if (!moduleName && !coerce) { + return yield* new ProjectInfoError({ + message: 'No instant client library installed', + }); + } + yield* Effect.log( + `Found ${chalk.green(moduleName)} in your package.json.\n`, + ); + + // TODO: Clean up with option + const packageManager = yield* Effect.tryPromise(() => detect()).pipe( + Effect.flatMap(Effect.fromNullable), + Effect.mapError( + () => + new ProjectInfoError({ + message: 'Failed to detect package manager', + }), + ), + ); + + const { yes } = yield* GlobalOpts; + if (!moduleName && coerce) { + // install the packages + if (packageName) { + moduleName = PACKAGE_ALIAS_AND_FULL_NAMES[packageName]; + } else { + if (yes) { + return yield* BadArgsError.make({ + message: + '--yes was provided without a package specificaion and no Instant SDK was found', + }); + } + moduleName = yield* runUIEffect( + new UI.Select({ + promptText: 'Which package would you like to use?', + options: [ + { label: '@instantdb/react', value: '@instantdb/react' }, + { + label: '@instantdb/react-native', + value: '@instantdb/react-native', + }, + { label: '@instantdb/core', value: '@instantdb/core' }, + { label: '@instantdb/admin', value: '@instantdb/admin' }, + { label: '@instantdb/solidjs', value: '@instantdb/solidjs' }, + { label: '@instantdb/svelte', value: '@instantdb/svelte' }, + ], + }), + ).pipe( + Effect.flatMap(Effect.fromNullable), + Effect.mapError( + () => + new ProjectInfoError({ + message: 'Failed to select package', + }), + ), + ); + } + const packagesToInstall = [moduleName]; + if (moduleName === '@instantdb/react-native') { + packagesToInstall.push( + 'react-native-get-random-values', + '@react-native-async-storage/async-storage', + ); + } + const installCommand = getInstallCommand( + packageManager.agent, + packagesToInstall.join(' '), + ); + yield* Effect.log(installCommand); + yield* runUIEffect( + new UI.Spinner({ + promise: execAsync(installCommand, { + cwd: projectDir.dir, + }), + errorText: 'Failed to install packages', + workingText: `Installing ${packagesToInstall.join(', ')} using ${packageManager.agent}...`, + doneText: `Installed ${packagesToInstall.join(', ')} using ${packageManager.agent}.`, + }), + ); + return { + pkgDir: projectDir.dir, + projectType: projectDir.type, + instantModuleName: moduleName, + }; + } else { + return { + pkgDir: projectDir.dir, + projectType: projectDir.type, + instantModuleName: moduleName!, + }; + } + }); + +export const ProjectInfoLive = ( + coerce: boolean = true, + packageName?: keyof typeof PACKAGE_ALIAS_AND_FULL_NAMES, +) => Layer.effect(ProjectInfo, getProjectInfo(coerce, packageName)); + +function getInstantModuleName(pkgJson: PackageJson) { + const deps = pkgJson.dependencies || {}; + const devDeps = pkgJson.devDependencies || {}; + const instantModuleName = [ + '@instantdb/react', + '@instantdb/react-native', + '@instantdb/core', + '@instantdb/admin', + '@instantdb/solidjs', + '@instantdb/svelte', + ].find((name) => deps[name] || devDeps[name]); + return instantModuleName; +} + +function getInstallCommand(packageManager: string, moduleName: string) { + if (packageManager === 'npm') { + return `npm install ${moduleName}`; + } else { + return `${packageManager} add ${moduleName}`; + } +} diff --git a/client/packages/cli/src/new/errors.ts b/client/packages/cli/src/new/errors.ts new file mode 100644 index 0000000000..c8c6719db4 --- /dev/null +++ b/client/packages/cli/src/new/errors.ts @@ -0,0 +1,7 @@ +import { Schema } from 'effect'; + +export class BadArgsError extends Schema.TaggedError<BadArgsError>( + 'BadArgsError', +)('BadArgsError', { + message: Schema.String, +}) {} diff --git a/client/packages/cli/src/new/index.ts b/client/packages/cli/src/new/index.ts new file mode 100644 index 0000000000..8f696d280e --- /dev/null +++ b/client/packages/cli/src/new/index.ts @@ -0,0 +1,421 @@ +import { Command, Option } from '@commander-js/extra-typings'; +import chalk from 'chalk'; +import { Effect, Layer } from 'effect'; +import version from '../version.js'; +import { initCommand } from './commands/init.js'; +import { initWithoutFilesCommand } from './commands/initWithoutFiles.js'; +import { loginCommand } from './commands/login.js'; +import { logoutCommand } from './commands/logout.js'; +import { loadEnv } from '../util/loadEnv.js'; +import { + AuthLayerLive, + BaseLayerLive, + printRedErrors, + runCommandEffect, + WithAppLayer, +} from './layer.js'; +import { infoCommand } from './commands/info.js'; +import { pullCommand, SchemaPermsOrBoth } from './commands/pull.js'; +import { claimCommand } from './commands/claim.js'; +import { pushCommand } from './commands/push.js'; +import { explorerCmd } from './commands/explorer.js'; +import { queryCmd } from './commands/query.js'; +import { program } from './program.js'; +import { PACKAGE_ALIAS_AND_FULL_NAMES } from './context/projectInfo.js'; + +loadEnv(); + +export type OptsFromCommand<C> = + C extends Command<any, infer R, any> ? R : never; + +program + .name('instant-cli') + .addOption(globalOption('-t --token <token>', 'Auth token override')) + .addOption(globalOption('-y --yes', "Answer 'yes' to all prompts")) + .addOption(globalOption('--env <file>', 'Use a specific .env file')) + .addOption( + globalOption('-v --version', 'Print the version number', () => { + console.log(version); + process.exit(0); + }), + ) + .addHelpOption(globalOption('-h --help', 'Print the help text for a command')) + .usage(`<command> ${chalk.dim('[options] [args]')}`); + +// Command List +export const initDef = program + .command('init') + .description('Set up a new project.') + .option( + '-a --app <app-id>', + 'If you have an existing app ID, we can pull schema and perms from there.', + ) + .option( + '-p --package <react|react-native|core|admin>', + 'Which package to automatically install if there is not one installed already.', + ) + .option('--title <title>', 'Title for the created app') + .action((options) => { + return runCommandEffect( + initCommand(options).pipe( + Effect.provide( + WithAppLayer({ + coerce: true, + coerceAuth: true, + title: options.title, + appId: options.app, + packageName: options.package as any, + applyEnv: true, + }), + ), + ), + ); + }); + +export const initWithoutFilesDef = program + .command('init-without-files') + .description('Generate a new app id and admin token pair without any files.') + .option('--title <title>', 'Title for the created app.') + .option( + '--org-id <org-id>', + 'Organization id for app. Cannot be used with --temp flag.', + ) + .option( + '--temp', + 'Create a temporary app which will automatically delete itself after >24 hours.', + ) + .action((opts) => { + return runCommandEffect( + initWithoutFilesCommand(opts).pipe(Effect.provide(BaseLayerLive)), + ); + }); + +export const loginDef = program + .command('login') + .description('Log into your account') + .option('-p --print', 'Prints the auth token into the console.') + .option( + '--headless', + 'Print the login URL instead of trying to open the browser', + ) + .action(async (opts) => { + runCommandEffect(loginCommand(opts).pipe(Effect.provide(BaseLayerLive))); + }); + +const _logoutDef = program + .command('logout') + .description('Log out of your Instant account') + .action(async () => { + runCommandEffect(logoutCommand().pipe(Effect.provide(BaseLayerLive))); + }); + +export const infoDef = program + .command('info') + .description('Display CLI version and login status') + .action(async () => { + runCommandEffect( + infoCommand().pipe( + Effect.provide( + AuthLayerLive({ + coerce: false, + allowAdminToken: false, + }).pipe(Layer.catchAll((e) => Layer.empty)), + ), + ), + ); + }); + +export const explorerDef = program + .command('explorer') + .description('Opens the Explorer in your browser') + .option( + '-a --app <app-id>', + 'App ID to open the explorer to. Defaults to *_INSTANT_APP_ID in .env', + ) + .action(async (opts) => { + return runCommandEffect( + explorerCmd(opts).pipe( + Effect.provide( + WithAppLayer({ + coerce: true, + coerceAuth: true, + appId: opts.app, + }), + ), + ), + ); + }); + +export const queryDef = program + .command('query') + .argument('<query>', 'InstaQL query as JSON/JSON5') + .option( + '-a --app <app-id>', + 'App ID to query. Defaults to *_INSTANT_APP_ID in .env', + ) + .option('--admin', 'Run the query as admin (bypasses permissions)') + .option('--as-email <email>', 'Run the query as a specific user by email') + .option('--as-guest', 'Run the query as an unauthenticated guest') + .option( + '--as-token <refresh-token>', + 'Run the query as a user identified by refresh token', + ) + .description('Run an InstaQL query against your app.') + .action(async function (queryArg, opts) { + runCommandEffect( + queryCmd(queryArg, opts).pipe( + Effect.provide( + WithAppLayer({ + coerce: false, + appId: opts.app, + }), + ), + ), + ); + }); + +export const pullDef = program + .command('pull') + .argument( + '[schema|perms|all]', + 'Which configuration to push. Defaults to `all`', + ) + .option( + '-a --app <app-id>', + 'App ID to push to. Defaults to *_INSTANT_APP_ID in .env', + ) + .option( + '-p --package <react|react-native|core|admin|solid|svelte>', + 'Which package to automatically install if there is not one installed already.', + ) + .option( + '--experimental-type-preservation', + "[Experimental] Preserve manual type changes like `status: i.json<'online' | 'offline'>()` when doing `instant-cli pull schema`", + ) + .description('Pull schema and perm files from production.') + .addHelpText( + 'after', + ` +Environment Variables: + INSTANT_SCHEMA_FILE_PATH Override schema file location (default: instant.schema.ts) + INSTANT_PERMS_FILE_PATH Override perms file location (default: instant.perms.ts) +`, + ) + .action(async function (arg, inputOpts) { + return runCommandEffect( + pullCommand(arg as SchemaPermsOrBoth, inputOpts).pipe( + Effect.provide( + WithAppLayer({ + coerce: true, + packageName: inputOpts.package as + | 'react' + | 'react-native' + | 'core' + | 'admin' + | undefined, + appId: inputOpts.app, + }), + ), + ), + ); + }); + +export const pushDef = program + .command('push') + .argument( + '[schema|perms|all]', + 'Which configuration to push. Defaults to `all`', + ) + .option( + '-a --app <app-id>', + 'App ID to push to. Defaults to *_INSTANT_APP_ID in .env', + ) + .option( + '--skip-check-types', + "Don't check types on the server when pushing schema", + ) + .option( + '--rename [renames...]', + 'List of full attribute names separated by a ":"\n Example:`push --rename posts.author:posts.creator stores.owner:stores.manager`', + ) + .option( + '-p --package <react|react-native|core|admin|solid|svelte>', + 'Which package to automatically install if there is not one installed already.', + ) + .description('Push schema and perm files to production.') + .addHelpText( + 'after', + ` +Environment Variables: + INSTANT_SCHEMA_FILE_PATH Override schema file location (default: instant.schema.ts) + INSTANT_PERMS_FILE_PATH Override perms file location (default: instant.perms.ts) +`, + ) + .action(async function (arg, inputOpts) { + runCommandEffect( + pushCommand(arg, inputOpts).pipe( + Effect.provide( + WithAppLayer({ + coerce: true, + appId: inputOpts.app, + applyEnv: true, + packageName: + inputOpts.package as keyof typeof PACKAGE_ALIAS_AND_FULL_NAMES, + }), + ), + ), + ); + }); + +const claimDef = program + .command('claim') + .description('Transfer a tempoary app into your Instant account') + .action(async function () { + runCommandEffect( + claimCommand.pipe( + Effect.provide( + WithAppLayer({ + coerce: false, + allowAdminToken: false, + applyEnv: false, + }), + ), + ), + ); + }); +//// Program setup ///// + +function globalOption( + flags: string, + description?: string, + argParser?: (value: string, prev?: unknown) => unknown, +) { + const opt = new Option(flags, description); + if (argParser) { + opt.argParser(argParser); + } + // @ts-ignore + // __global does not exist on `Option`, + // but we use it in `getLocalAndGlobalOptions`, to produce + // our own custom list of local and global options. + // For more info, see the original PR: + // https://github.com/instantdb/instant/pull/505 + opt.__global = true; + return opt; +} + +function getLocalAndGlobalOptions(cmd, helper) { + const mixOfLocalAndGlobal = helper.visibleOptions(cmd); + const localOptionsFromMix = mixOfLocalAndGlobal.filter( + (option) => !option.__global, + ); + const globalOptionsFromMix = mixOfLocalAndGlobal.filter( + (option) => option.__global, + ); + const globalOptions = helper.visibleGlobalOptions(cmd); + + return [localOptionsFromMix, globalOptionsFromMix.concat(globalOptions)]; +} + +function formatHelp(cmd, helper) { + const termWidth = helper.padWidth(cmd, helper); + const helpWidth = helper.helpWidth || 80; + const itemIndentWidth = 2; + const itemSeparatorWidth = 2; // between term and description + function formatItem(term, description) { + if (description) { + const fullText = `${term.padEnd(termWidth + itemSeparatorWidth)}${description}`; + return helper.wrap( + fullText, + helpWidth - itemIndentWidth, + termWidth + itemSeparatorWidth, + ); + } + return term; + } + function formatList(textArray) { + return textArray.join('\n').replace(/^/gm, ' '.repeat(itemIndentWidth)); + } + + // Usage + let output = [`${helper.commandUsage(cmd)}`, '']; + + // Description + const commandDescription = helper.commandDescription(cmd); + if (commandDescription.length > 0) { + output = output.concat([helper.wrap(commandDescription, helpWidth, 0), '']); + } + + // Arguments + const argumentList = helper.visibleArguments(cmd).map((argument) => { + return formatItem( + helper.argumentTerm(argument), + helper.argumentDescription(argument), + ); + }); + if (argumentList.length > 0) { + output = output.concat([ + chalk.dim.bold('Arguments'), + formatList(argumentList), + '', + ]); + } + const [visibleOptions, visibleGlobalOptions] = getLocalAndGlobalOptions( + cmd, + helper, + ); + + // Options + const optionList = visibleOptions.map((option) => { + return formatItem( + helper.optionTerm(option), + helper.optionDescription(option), + ); + }); + if (optionList.length > 0) { + output = output.concat([ + chalk.dim.bold('Options'), + formatList(optionList), + '', + ]); + } + // Commands + const commandList = helper.visibleCommands(cmd).map((cmd) => { + return formatItem( + helper.subcommandTerm(cmd), + helper.subcommandDescription(cmd), + ); + }); + if (commandList.length > 0) { + output = output.concat([ + chalk.dim.bold('Commands'), + formatList(commandList), + '', + ]); + } + + if (this.showGlobalOptions) { + const globalOptionList = visibleGlobalOptions.map((option) => { + return formatItem( + helper.optionTerm(option), + helper.optionDescription(option), + ); + }); + if (globalOptionList.length > 0) { + output = output.concat([ + chalk.dim.bold('Global Options'), + formatList(globalOptionList), + '', + ]); + } + } + + return output.join('\n'); +} + +program.configureHelp({ + showGlobalOptions: true, + formatHelp, +}); + +program.parse(process.argv); diff --git a/client/packages/cli/src/new/layer.ts b/client/packages/cli/src/new/layer.ts new file mode 100644 index 0000000000..b3e317ac69 --- /dev/null +++ b/client/packages/cli/src/new/layer.ts @@ -0,0 +1,140 @@ +import { NodeContext, NodeHttpClient } from '@effect/platform-node'; +import { Cause, Effect, Layer, ManagedRuntime } from 'effect'; +import { AuthTokenLive } from './context/authToken.js'; +import { CurrentAppLive } from './context/currentApp.js'; +import { GlobalOptsLive } from './context/globalOpts.js'; +import { PlatformApi } from './context/platformApi.js'; +import { + PACKAGE_ALIAS_AND_FULL_NAMES, + ProjectInfoLive, +} from './context/projectInfo.js'; +import { + InstantHttpAuthedLive, + InstantHttpError, + InstantHttpLive, +} from './lib/http.js'; +import { SimpleLogLayer } from './logging.js'; + +const runtime = ManagedRuntime.make(SimpleLogLayer); + +export const runCommandEffect = <A, E, R extends never>( + effect: Effect.Effect<A, E, R>, +): Promise<any> => runtime.runPromise(effect.pipe(printRedErrors) as any); + +export const printRedErrors = Effect.catchAllCause((cause) => + Effect.gen(function* () { + const failure = Cause.failureOption(cause); + + // This should never happen because the catchAllCause should only fire when there IS a failure + if (failure._tag !== 'Some') { + return; + } + + const theError = failure.value; + + // Special error handling for specific error types + if (theError instanceof InstantHttpError) { + return yield* Effect.logError( + `Error making request to Instant API: ${theError.message}`, + ); + } + + // Print just the message if the error has a message attribute and no cause + if ( + typeof failure.value === 'object' && + failure.value !== null && + 'message' in failure.value && + !('cause' in failure.value) + ) { + return yield* Effect.logError( + (failure.value as { message: string }).message, + ).pipe( + Effect.tap(() => { + process.exit(1); + }), + ); + } + + return yield* Effect.logError( + Cause.pretty(cause, { renderErrorCause: true }), + ).pipe( + Effect.tap(() => { + process.exit(1); + }), + ); + }), +); + +/** + * Note: + Avoid Duplicate Layer Creation + + Layers are memoized using reference equality. Therefore, if you have a layer that is created by calling a function like f(), you should only call that f once and re-use the resulting layer so that you are always using the same instance. + */ + +// TODO: make coerce param work for auth too + +// Base layers +const AuthTokenLayer = ({ + allowAdminToken = true, + coerce = false, +}: { + allowAdminToken: boolean; + coerce: boolean; +}) => + Layer.provide(AuthTokenLive({ allowAdminToken, coerce }), NodeContext.layer); + +const InstantHttpLayer = Layer.provide(InstantHttpLive, NodeHttpClient.layer); + +// Unauthenticated layer with InstantHttp + PlatformApi + GlobalOpts + NodeContext +export const BaseLayerLive = Layer.provideMerge( + Layer.mergeAll(InstantHttpLayer, PlatformApi.Default, GlobalOptsLive), + NodeContext.layer, +); + +// Authenticated layer extends BaseLayerLive with InstantHttpAuthed +export const AuthLayerLive = ({ + allowAdminToken = true, + coerce = false, +}: { + allowAdminToken: boolean; + coerce: boolean; +}) => + Layer.provideMerge( + Layer.provideMerge( + InstantHttpAuthedLive, + Layer.merge( + AuthTokenLayer({ allowAdminToken, coerce }), + InstantHttpLayer, + ), + ), + BaseLayerLive, + ); + +export const WithAppLayer = (args: { + appId?: string; + title?: string; + coerce: boolean; + coerceAuth?: boolean; + packageName?: keyof typeof PACKAGE_ALIAS_AND_FULL_NAMES; + allowAdminToken?: boolean; + applyEnv?: boolean; +}) => + Layer.mergeAll( + CurrentAppLive({ + coerce: args.coerce, + appId: args.appId, + title: args.title, + applyEnv: args.applyEnv, + }), + ).pipe( + Layer.provideMerge(GlobalOptsLive), + Layer.provideMerge( + AuthLayerLive({ + allowAdminToken: args.allowAdminToken || true, + coerce: args.coerceAuth || false, + }), + ), + Layer.provideMerge(ProjectInfoLive(args.coerce, args.packageName)), + Layer.provideMerge(BaseLayerLive), + ); diff --git a/client/packages/cli/src/new/lib/createApp.ts b/client/packages/cli/src/new/lib/createApp.ts new file mode 100644 index 0000000000..d35fa7d364 --- /dev/null +++ b/client/packages/cli/src/new/lib/createApp.ts @@ -0,0 +1,28 @@ +import { randomUUID } from 'crypto'; +import { Data, Effect, Schema } from 'effect'; +import { InstantHttpAuthed } from './http.js'; +import { HttpClientRequest, HttpClientResponse } from '@effect/platform'; + +export class CreateAppError extends Data.TaggedError('CreateAppError')<{ + message: string; +}> {} + +export const createApp = Effect.fn( + function* (title: string, orgId?: string) { + const http = yield* InstantHttpAuthed; + const id = randomUUID(); + const token = randomUUID(); + const app = { id, title, admin_token: token, org_id: orgId }; + + const res = yield* HttpClientRequest.post('/dash/apps').pipe( + HttpClientRequest.bodyJson(app), + Effect.flatMap(http.execute), + Effect.flatMap(HttpClientResponse.schemaBodyJson(Schema.Any)), + ); + return res; + }, + Effect.catchTag( + 'HttpBodyError', + (e) => new CreateAppError({ message: 'Error constructing http body' }), + ), +); diff --git a/client/packages/cli/src/new/lib/handleEnv.ts b/client/packages/cli/src/new/lib/handleEnv.ts new file mode 100644 index 0000000000..506303b3fa --- /dev/null +++ b/client/packages/cli/src/new/lib/handleEnv.ts @@ -0,0 +1,110 @@ +import { Effect } from 'effect'; +import { CurrentAppInfo, potentialEnvs } from '../context/currentApp.js'; +import { ProjectInfo, ProjectInfoError } from '../context/projectInfo.js'; +import { readPackage } from 'pkg-types'; +import { GlobalOpts } from '../context/globalOpts.js'; +import { FileSystem, Path } from '@effect/platform'; +import chalk from 'chalk'; +import terminalLink from 'terminal-link'; +import { getDashUrl } from './http.js'; +import { promptOk } from './ui.js'; + +export const handleEnv = Effect.fn(function* (app: CurrentAppInfo) { + const opts = yield* GlobalOpts; + const { pkgDir } = yield* ProjectInfo; + const envType = yield* detectEnvType; + const envName = potentialEnvs[envType]; + const envFile = opts.env ?? '.env'; + const fs = yield* FileSystem.FileSystem; + const path = yield* Path.Path; + const hasEnvFile = yield* fs.exists(path.join(pkgDir, envFile)); + const dashOrigin = yield* getDashUrl; + if (hasEnvFile) { + return printDotEnvInfo(envType, app.appId, dashOrigin); + } + yield* Effect.log( + `\nLooks like you don't have a ${chalk.green(`\`${envFile}\``)} file yet.`, + ); + yield* Effect.log( + `If we set ${chalk.green(envName)} & ${chalk.green('INSTANT_APP_ADMIN_TOKEN')}, we can remember the app that you chose for all future commands.`, + ); + const saveExtraInfo = + envFile !== '.env' ? chalk.green(' (will create `' + envFile + '`)') : ''; + + const ok = yield* promptOk( + { + inline: true, + promptText: 'Want us to create this env file for you?' + saveExtraInfo, + modifyOutput: (a) => a, + }, + true, + ); + if (!ok) { + yield* Effect.log( + `No .env file created. You can always set ${chalk.green('`' + envName + '`')} later. \n`, + ); + return; + } + const content = + [ + [envName, app.appId], + ['INSTANT_APP_ADMIN_TOKEN', app.adminToken], + ] + .map(([k, v]) => `${k}=${v}`) + .join('\n') + '\n'; + + yield* fs.writeFileString(path.join(pkgDir, envFile), content); + + if (envFile !== '.env') { + yield* Effect.log(`Created ${chalk.green(envFile)}!`); + } else { + yield* Effect.log(`Created ${chalk.green('.env')} file!`); + } +}); + +const detectEnvType = Effect.gen(function* () { + const pkgJson = yield* Effect.tryPromise({ + try: () => readPackage(), + catch: () => + new ProjectInfoError({ message: "Couldn't read package.json" }), + }); + if (pkgJson.dependencies?.next) { + return 'next'; + } + if (pkgJson.devDependencies?.svelte) { + return 'svelte'; + } + if (pkgJson.devDependencies?.vite) { + return 'vite'; + } + if (pkgJson.dependencies?.expo) { + return 'expo'; + } + if (pkgJson.dependencies?.nuxt) { + return 'nuxt'; + } + if (pkgJson.dependencies?.['@types/bun']) { + return 'bun'; + } + return 'catchall'; +}).pipe(Effect.catchTag('ProjectInfoError', () => Effect.succeed('catchall'))); + +function printDotEnvInfo(envType, appId, dashOrigin: string) { + console.log(`\nPicked app ${chalk.green(appId)}!\n`); + console.log( + `To use this app automatically from now on, update your ${chalk.green('`.env`')} file:`, + ); + const picked = potentialEnvs[envType]; + const rest = { ...potentialEnvs }; + delete rest[envType]; + console.log(` ${chalk.green(picked)}=${appId}`); + const otherEnvs = Object.values(rest); + otherEnvs.sort(); + const otherEnvStr = otherEnvs.map((x) => ' ' + chalk.green(x)).join('\n'); + console.log(`Alternative names: \n${otherEnvStr} \n`); + console.log(terminalLink('Dashboard:', appDashUrl(appId, dashOrigin)) + '\n'); +} + +function appDashUrl(id, instantOrigin: string) { + return `${instantOrigin}/dash?s=main&t=home&app=${id}`; +} diff --git a/client/packages/cli/src/new/lib/http.ts b/client/packages/cli/src/new/lib/http.ts new file mode 100644 index 0000000000..e03bf89039 --- /dev/null +++ b/client/packages/cli/src/new/lib/http.ts @@ -0,0 +1,130 @@ +import { HttpClient, HttpClientRequest } from '@effect/platform'; +import { version } from '@instantdb/version'; +import { Config, Context, Data, Effect, Layer, Option, Schema } from 'effect'; +import { AuthToken } from '../context/authToken.js'; +import { TimeoutException } from 'effect/Cause'; +import { RequestError } from '@effect/platform/HttpClientError'; + +export class InstantHttp extends Context.Tag( + 'instant-cli/new/lib/http/InstantHttp', +)< + InstantHttp, + HttpClient.HttpClient.With<InstantHttpError | TimeoutException | RequestError> +>() {} + +export class InstantHttpAuthed extends Context.Tag( + 'instant-cli/new/lib/http/InstantHttpAuthed', +)< + InstantHttpAuthed, + HttpClient.HttpClient.With<InstantHttpError | TimeoutException | RequestError> +>() {} + +export class InstantHttpError extends Data.TaggedError('InstantHttpError')<{ + message: string; + type: string; + methodAndUrl: string; + hint?: Record<string, any>; +}> {} + +// Pipe on a client to set command header +export const withCommand = (command: string) => { + return (client: HttpClient.HttpClient.With<InstantHttpError>) => + client.pipe( + HttpClient.mapRequest((r) => + r.pipe(HttpClientRequest.setHeader(`X-Instant-Command`, command)), + ), + ); +}; + +class InstantTypicalHttpErrorResponse extends Schema.Struct({ + message: Schema.String, + type: Schema.String.pipe(Schema.optional), + hint: Schema.Record({ key: Schema.String, value: Schema.Any }).pipe( + Schema.optional, + ), +}) {} + +export const InstantHttpLive = Layer.effect( + InstantHttp, + Effect.gen(function* () { + const client = yield* HttpClient.HttpClient; + const baseUrl = yield* getBaseUrl; + return client.pipe( + HttpClient.mapRequest((r) => + r.pipe( + HttpClientRequest.prependUrl(baseUrl), + HttpClientRequest.setHeader('X-Instant-Source', 'instant-cli'), + HttpClientRequest.setHeader('X-Instant-Version', version), + ), + ), + HttpClient.transformResponse((r) => r.pipe(Effect.timeout('5 minutes'))), + HttpClient.filterStatusOk, // makes non 2xx http codes error + HttpClient.transformResponse((r) => + r.pipe( + Effect.catchTag('ResponseError', (requestError) => + Effect.gen(function* () { + const jsonBody = yield* requestError.response.json.pipe( + Effect.andThen( + Schema.decodeUnknown(InstantTypicalHttpErrorResponse), + ), + Effect.mapError( + (e) => + new InstantHttpError({ + message: + 'Error making request to ' + requestError.methodAndUrl, + type: e._tag, + methodAndUrl: requestError.methodAndUrl, + }), + ), + ); + return yield* new InstantHttpError({ + message: jsonBody.message, + methodAndUrl: requestError.methodAndUrl, + hint: jsonBody.hint, + type: jsonBody.type || 'Unknown type', + }); + }), + ), + ), + ), + ); + }), +); + +export const InstantHttpAuthedLive = Layer.effect( + InstantHttpAuthed, + Effect.gen(function* () { + const http = yield* InstantHttp; + const { authToken } = yield* AuthToken; + return http.pipe( + HttpClient.mapRequest((r) => + r.pipe( + HttpClientRequest.setHeader('Authorization', `Bearer ${authToken}`), + ), + ), + ); + }), +); + +export const getBaseUrl = Effect.gen(function* () { + const setEnv = yield* Config.string('INSTANT_CLI_API_URI').pipe( + Config.option, + ); + const dev = yield* Config.boolean('INSTANT_CLI_DEV').pipe( + Config.withDefault(false), + ); + + return Option.match(setEnv, { + onSome: (url) => url, + onNone: () => { + return dev ? 'http://localhost:8888' : 'https://api.instantdb.com'; + }, + }); +}); + +export const getDashUrl = Effect.gen(function* () { + const dev = Option.getOrNull( + yield* Config.boolean('INSTANT_CLI_DEV').pipe(Config.option), + ); + return dev ? 'http://localhost:3000' : 'https://instantdb.com'; +}); diff --git a/client/packages/cli/src/new/lib/login.ts b/client/packages/cli/src/new/lib/login.ts new file mode 100644 index 0000000000..29d555ffb7 --- /dev/null +++ b/client/packages/cli/src/new/lib/login.ts @@ -0,0 +1,54 @@ +import { Effect, Schedule, Schema } from 'effect'; +import { InstantHttp, withCommand } from './http.js'; +import { + HttpClientRequest, + HttpClientResponse, + FileSystem, +} from '@effect/platform'; +import { getAuthPaths } from '../../util/getAuthPaths.js'; + +const LoginInfo = Schema.Struct({ + secret: Schema.String, + ticket: Schema.String, +}); + +const TokenResult = Schema.Struct({ + token: Schema.String, + email: Schema.String, +}); + +export const getLoginTicketAndSecret = Effect.gen(function* () { + const http = yield* InstantHttp; + const res = yield* http + .pipe(withCommand('login')) + .post('/dash/cli/auth/register') + .pipe(Effect.flatMap(HttpClientResponse.schemaBodyJson(LoginInfo))); + return res; +}); + +export const waitForAuthToken = Effect.fn(function* (secret: string) { + const http = (yield* InstantHttp).pipe(withCommand('login')); + const res = yield* HttpClientRequest.post('/dash/cli/auth/check').pipe( + HttpClientRequest.bodyUnsafeJson({ + secret, + }), + http.execute, + Effect.flatMap(HttpClientResponse.schemaBodyJson(TokenResult)), + Effect.retry({ + while: (e) => + e._tag === 'InstantHttpError' && + e.hint?.errors?.at(0).issue === 'waiting-for-user', + schedule: Schedule.fixed('1 seconds'), + times: 120, + }), + ); + return res; +}); + +export const saveConfigAuthToken = Effect.fn(function* (token: string) { + const authPaths = getAuthPaths(); + + const fs = yield* FileSystem.FileSystem; + yield* fs.makeDirectory(authPaths.appConfigDirPath, { recursive: true }); + yield* fs.writeFileString(authPaths.authConfigFilePath, token); +}); diff --git a/client/packages/cli/src/new/lib/pullPerms.ts b/client/packages/cli/src/new/lib/pullPerms.ts new file mode 100644 index 0000000000..97d371d5b2 --- /dev/null +++ b/client/packages/cli/src/new/lib/pullPerms.ts @@ -0,0 +1,44 @@ +import { Effect, Schema } from 'effect'; +import { CurrentApp } from '../context/currentApp.js'; +import { InstantHttpAuthed, withCommand } from './http.js'; +import { HttpClientResponse } from '@effect/platform'; +import { readLocalPermsFile } from '../../index.js'; +import { getPermsPathToWrite } from '../../util/findConfigCandidates.js'; +import { promptOk } from './ui.js'; +import { UI } from '../../ui/index.js'; +import { writeTypescript } from './pullSchema.js'; +import { generatePermsTypescriptFile } from '@instantdb/platform'; +import { ProjectInfo } from '../context/projectInfo.js'; +import { Path } from '@effect/platform'; + +export const pullPerms = Effect.gen(function* () { + yield* Effect.log('Pulling perms...'); + const { appId } = yield* CurrentApp; + const http = yield* InstantHttpAuthed; + const permsResponse = yield* http + .pipe(withCommand('pull')) + .get(`/dash/apps/${appId}/perms/pull`) + .pipe(Effect.flatMap(HttpClientResponse.schemaBodyJson(Schema.Any))); // parse result body into "any" + + const prevPermsFile = yield* Effect.tryPromise(readLocalPermsFile); + const shortPermsPath = getPermsPathToWrite(prevPermsFile?.path); + + if (prevPermsFile) { + const shouldContinue = yield* promptOk({ + promptText: `This will overwrite your local ${shortPermsPath} file, OK to proceed?`, + modifyOutput: UI.modifiers.yPadding, + inline: true, + }); + if (!shouldContinue) return; + } + const { instantModuleName, pkgDir } = yield* ProjectInfo; + const fileContent = generatePermsTypescriptFile( + permsResponse.perms || {}, + instantModuleName, + ); + + const path = yield* Path.Path; + + yield* writeTypescript(path.join(pkgDir, shortPermsPath), fileContent); + yield* Effect.log('✅ Wrote permissions to ' + shortPermsPath); +}); diff --git a/client/packages/cli/src/new/lib/pullSchema.ts b/client/packages/cli/src/new/lib/pullSchema.ts new file mode 100644 index 0000000000..d7f48a88fb --- /dev/null +++ b/client/packages/cli/src/new/lib/pullSchema.ts @@ -0,0 +1,95 @@ +import { FileSystem, HttpClientResponse, Path } from '@effect/platform'; +import { + apiSchemaToInstantSchemaDef, + generateSchemaTypescriptFile, +} from '@instantdb/platform'; +import { Effect, Schema } from 'effect'; +import prettier from 'prettier'; +import { countEntities, readLocalSchemaFile } from '../../index.js'; +import { UI } from '../../ui/index.js'; +import { getSchemaPathToWrite } from '../../util/findConfigCandidates.js'; +import { CurrentApp } from '../context/currentApp.js'; +import { ProjectInfo } from '../context/projectInfo.js'; +import { InstantHttpAuthed, withCommand } from './http.js'; +import { promptOk } from './ui.js'; +import { ReadSchemaFileError } from './pushSchema.js'; +import { mergeSchema, MergeSchemaError } from '../../util/mergeSchema.js'; + +export const pullSchema = ({ + experimentalTypePreservation, +}: { + experimentalTypePreservation?: boolean; +}) => + Effect.gen(function* () { + yield* Effect.log('Pulling schema...'); + const { appId } = yield* CurrentApp; + const http = yield* InstantHttpAuthed; + + const schemaResponse = yield* http + .pipe(withCommand('pull')) + .get(`/dash/apps/${appId}/schema/pull`) + .pipe(Effect.flatMap(HttpClientResponse.schemaBodyJson(Schema.Any))); // parse result body into "any" + + if ( + !countEntities(schemaResponse.schema.refs) && + !countEntities(schemaResponse.schema.blobs) + ) { + yield* Effect.log('Schema is empty. Skipping.'); + return; + } + + const prevSchemaFile = yield* Effect.tryPromise(readLocalSchemaFile).pipe( + Effect.mapError((err) => + ReadSchemaFileError.make({ + message: `Error reading local schema file: ${err}`, + cause: err, + }), + ), + ); + const shortSchemaPath = getSchemaPathToWrite(prevSchemaFile?.path); + const path = yield* Path.Path; + const { pkgDir, instantModuleName } = yield* ProjectInfo; + const schemaPath = path.join(pkgDir, shortSchemaPath); + + if (prevSchemaFile) { + const shouldContinue = yield* promptOk({ + promptText: `This will overwrite your local ${shortSchemaPath} file, OK to proceed?`, + modifyOutput: UI.modifiers.yPadding, + inline: true, + }); + if (!shouldContinue) return; + } + + let newSchemaContent = generateSchemaTypescriptFile( + prevSchemaFile?.schema, + apiSchemaToInstantSchemaDef(schemaResponse.schema), + instantModuleName, + ); + + if (prevSchemaFile && experimentalTypePreservation) { + const fs = yield* FileSystem.FileSystem; + const oldSchemaContent = yield* fs.readFileString(prevSchemaFile.path); + newSchemaContent = yield* Effect.try(() => + mergeSchema(oldSchemaContent, newSchemaContent), + ).pipe( + Effect.mapError((e) => new MergeSchemaError({ message: e.message })), + ); + } + yield* writeTypescript(schemaPath, newSchemaContent); + yield* Effect.log('✅ Wrote schema to ' + shortSchemaPath); + }); + +export const writeTypescript = (path: string, content: string) => + Effect.gen(function* () { + const prettierConfig = yield* Effect.tryPromise(() => + prettier.resolveConfig(path), + ); + const formattedCode = yield* Effect.tryPromise(() => + prettier.format(content, { + ...prettierConfig, + parser: 'typescript', + }), + ); + const fs = yield* FileSystem.FileSystem; + yield* fs.writeFileString(path, formattedCode); + }); diff --git a/client/packages/cli/src/new/lib/pushPerms.ts b/client/packages/cli/src/new/lib/pushPerms.ts new file mode 100644 index 0000000000..c0edc8e7ab --- /dev/null +++ b/client/packages/cli/src/new/lib/pushPerms.ts @@ -0,0 +1,78 @@ +import { Effect, Option, Schema } from 'effect'; +import jsonDiff from 'json-diff'; +import { readLocalPermsFile } from '../../index.js'; +import { InstantHttpAuthed, withCommand } from './http.js'; +import { + HttpClient, + HttpClientRequest, + HttpClientResponse, +} from '@effect/platform'; +import { CurrentApp } from '../context/currentApp.js'; +import { promptOk } from './ui.js'; +import boxen from 'boxen'; +import chalk from 'chalk'; + +export class NoPermsFileError extends Schema.TaggedError<NoPermsFileError>( + 'NoPermsFileError', +)('NoPermsFileError', { + message: Schema.String, +}) {} + +const PullPermsResponse = Schema.Struct({ + perms: Schema.Any.pipe(Schema.optional), +}); + +export const pushPerms = Effect.gen(function* () { + yield* Effect.log('Planning perms...'); + const { appId } = yield* CurrentApp; + const http = yield* InstantHttpAuthed; + + const permsFile = yield* Effect.tryPromise(readLocalPermsFile).pipe( + Effect.flatMap(Option.fromNullable), + Effect.mapError(() => + NoPermsFileError.make({ message: 'No permissions file found' }), + ), + ); + + const prodPerms = yield* http + .pipe(withCommand('push')) + .get(`/dash/apps/${appId}/perms/pull`) + .pipe(Effect.flatMap(HttpClientResponse.schemaBodyJson(PullPermsResponse))); // parse result body into "any" + + const diffedStr = jsonDiff.diffString( + prodPerms.perms || {}, + permsFile.perms || {}, + ); + + if (!diffedStr.length) { + yield* Effect.log('No perms changes detected. Skipping.'); + return; + } + + const okPush = yield* promptOk({ + promptText: 'Push these changes to your perms?', + modifyOutput: (output) => { + let both = diffedStr + '\n' + output; + return boxen(both, { + dimBorder: true, + padding: { + left: 1, + right: 1, + }, + }); + }, + }); + if (!okPush) return; + + yield* http + .pipe( + withCommand('push'), + HttpClient.mapRequestInputEffect( + HttpClientRequest.bodyJson({ code: permsFile.perms }), + ), + ) + .post(`/dash/apps/${appId}/rules`) + .pipe(Effect.flatMap(HttpClientResponse.schemaBodyJson(Schema.Any))); + + yield* Effect.log(chalk.green('Permissions updated!')); +}); diff --git a/client/packages/cli/src/new/lib/pushSchema.ts b/client/packages/cli/src/new/lib/pushSchema.ts new file mode 100644 index 0000000000..2e5175265a --- /dev/null +++ b/client/packages/cli/src/new/lib/pushSchema.ts @@ -0,0 +1,225 @@ +import { + HttpClient, + HttpClientRequest, + HttpClientResponse, +} from '@effect/platform'; +import chalk from 'chalk'; +import { Data, Effect, Schema } from 'effect'; +import { + readLocalSchemaFile, + resolveRenames, + waitForIndexingJobsToFinish, +} from '../../index.js'; +import { CurrentApp } from '../context/currentApp.js'; +import { error } from '../logging.js'; +import { InstantHttpAuthed, withCommand } from './http.js'; +import { + apiSchemaToInstantSchemaDef, + buildAutoRenameSelector, + collectSystemCatalogIdentNames, + convertTxSteps, + diffSchemas, + SchemaValidationError as PlatformSchemaError, + validateSchema, +} from '@instantdb/platform'; + +import { OptsFromCommand, pushDef } from '../index.js'; +import { GlobalOpts } from '../context/globalOpts.js'; +import { + groupSteps, + renderSchemaPlan, + SuperMigrationTx, +} from '../../renderSchemaPlan.js'; +import { promptOk } from './ui.js'; +import boxen from 'boxen'; + +const FetchSchemaResponse = Schema.Struct({ + schema: Schema.Struct({ + refs: Schema.Any, + blobs: Schema.Any, + }), + attrs: Schema.Array(Schema.Any).pipe(Schema.mutable), + 'app-title': Schema.String, +}).pipe(Schema.mutable); + +export class ReadSchemaFileError extends Schema.TaggedError<ReadSchemaFileError>( + 'ReadSchemaFileError', +)('ReadSchemaFileError', { + message: Schema.String, + cause: Schema.Any.pipe(Schema.optional), +}) {} + +export class SchemaDiffError extends Schema.TaggedError<SchemaDiffError>( + 'SchemaDiffError', +)('SchemaDiffError', { + message: Schema.String, +}) {} + +export class GetSchemaError extends Schema.TaggedError<GetSchemaError>( + 'GetSchemaError', +)('GetSchemaError', { + message: Schema.String, +}) {} + +export class SchemaValidationError extends Schema.TaggedError<SchemaValidationError>( + 'SchemaValidationError', +)('SchemaValidationError', { + message: Schema.String, +}) {} + +export const pushSchema = ( + rename?: OptsFromCommand<typeof pushDef>['rename'], +) => + Effect.gen(function* () { + const localSchemaFile = yield* Effect.tryPromise({ + try: readLocalSchemaFile, + catch: (e) => + e instanceof Error + ? ReadSchemaFileError.make({ message: e.message }) + : ReadSchemaFileError.make({ message: String(e) }), + }); + if (!localSchemaFile || !localSchemaFile?.schema) { + error( + `We couldn't find your ${chalk.yellow('`instant.schema.ts`')} file. Make sure it's in the root directory. (Hint: You can use an INSTANT_SCHEMA_FILE_PATH environment variable to specify it.)`, + ); + return; + } + if (localSchemaFile.schema?.constructor?.name !== 'InstantSchemaDef') { + error("We couldn't find your schema export."); + error( + 'In your ' + + chalk.green('`instant.schema.ts`') + + ' file, make sure you ' + + chalk.green('`export default schema`'), + ); + return; + } + + const http = yield* InstantHttpAuthed; + const { appId } = yield* CurrentApp; + const res = yield* http + .pipe(withCommand('push')) + .get(`/dash/apps/${appId}/schema/pull`) + .pipe( + Effect.andThen(HttpClientResponse.schemaBodyJson(FetchSchemaResponse)), + Effect.mapError((e) => GetSchemaError.make({ message: e.message })), + ); + + const currentAttrs = res['attrs']; + const currentApiSchema = res['schema']; + const oldSchema = apiSchemaToInstantSchemaDef(currentApiSchema, { + disableTypeInference: true, + }); + const systemCatalogIdentNames = + collectSystemCatalogIdentNames(currentAttrs); + + yield* Effect.tryPromise({ + try: async () => + validateSchema(localSchemaFile.schema, systemCatalogIdentNames), + catch: (e) => { + if (e instanceof PlatformSchemaError) { + return SchemaValidationError.make({ + message: 'Invalid Schema: ' + e.message, + }); + } + return SchemaValidationError.make({ + message: 'Failed to validate schema' + e, + }); + }, + }); + const renames = rename && Array.isArray(rename) ? rename : []; + const globalOpts = yield* GlobalOpts; + const renameSelector = globalOpts.yes + ? buildAutoRenameSelector(renames as any) + : resolveRenames; + + const diffResult = yield* Effect.tryPromise(() => + diffSchemas( + oldSchema, + localSchemaFile.schema, + renameSelector, + systemCatalogIdentNames, + ), + ).pipe( + Effect.mapError((e) => { + if (e.cause instanceof Error) { + return SchemaDiffError.make({ message: e.cause.message }); + } + return e.message; + }), + ); + + const txSteps = convertTxSteps(diffResult, currentAttrs); + if (txSteps.length === 0) { + yield* Effect.log(chalk.bgGray('No schema changes to apply!')); + return; + } + + const groupedSteps = groupSteps(diffResult); + yield* confirmSchemaChanges(groupedSteps, currentAttrs); + + const pushRes = yield* http + .pipe( + HttpClient.mapRequestInputEffect( + HttpClientRequest.bodyJson({ + steps: txSteps, + }), + ), + ) + .post(`/dash/apps/${appId}/schema/steps/apply`) + .pipe(Effect.flatMap(HttpClientResponse.schemaBodyJson(Schema.Any))); + + if (pushRes?.['indexing-jobs']) { + // TODO: rewrite in effect + yield* Effect.tryPromise({ + try: () => + waitForIndexingJobsToFinish(appId, pushRes?.['indexing-jobs'] || []), + catch: (e: any) => + WaitForJobsError.make({ + message: + e?.message || 'Error waiting for schema push jobs to finish', + }), + }); + } + + yield* Effect.log(chalk.green('Schema updated!')); + }); + +export class WaitForJobsError extends Schema.TaggedError<WaitForJobsError>( + 'WaitForJobsError', +)('WaitForJobsError', { + message: Schema.String, +}) {} + +export class CancelSchemaError extends Data.TaggedError('CancelSchemaError')<{ + message: string; +}> {} + +const confirmSchemaChanges = (steps: SuperMigrationTx[], currentAttrs: any[]) => + Effect.gen(function* () { + const lines = renderSchemaPlan(steps, currentAttrs); + const globalOpts = yield* GlobalOpts; + if (globalOpts.yes) { + return; + } + const wantsToPush = yield* promptOk({ + promptText: 'Push these changes?', + yesText: 'Push', + noText: 'Cancel', + modifyOutput: (output) => { + let both = lines.join('\n') + '\n\n' + output; + return boxen(both, { + dimBorder: true, + padding: { + left: 1, + right: 1, + }, + }); + }, + }); + if (!wantsToPush) { + return yield* new CancelSchemaError({ + message: 'Schema Migration Cancelled', + }); + } + }); diff --git a/client/packages/cli/src/new/lib/ui.ts b/client/packages/cli/src/new/lib/ui.ts new file mode 100644 index 0000000000..7add4c7aea --- /dev/null +++ b/client/packages/cli/src/new/lib/ui.ts @@ -0,0 +1,36 @@ +import { Data, Effect } from 'effect'; +import { GlobalOpts } from '../context/globalOpts.js'; +import { Prompt, renderUnwrap } from '../../ui/lib.js'; +import { UI } from '../../ui/index.js'; + +export class UIError extends Data.TaggedError('UIError')<{ + message: string; + cause?: unknown; +}> {} + +export const promptOk = Effect.fn('promptOk')(function* ( + props: UI.ConfirmationProps, + defaultValue: boolean = true, +) { + const opts = yield* GlobalOpts; + if (opts.yes) { + return defaultValue; + } + + const ok = yield* Effect.tryPromise(() => + renderUnwrap( + new UI.Confirmation({ + ...props, + defaultValue, + }), + ), + ).pipe(Effect.orElseSucceed(() => defaultValue)); + + return ok; +}); + +export const runUIEffect = <P>(prompt: Prompt<P>) => + Effect.tryPromise({ + try: () => renderUnwrap(prompt), + catch: (error) => new UIError({ message: 'UI Error', cause: error }), + }); diff --git a/client/packages/cli/src/new/logging.ts b/client/packages/cli/src/new/logging.ts new file mode 100644 index 0000000000..2089dd4ba5 --- /dev/null +++ b/client/packages/cli/src/new/logging.ts @@ -0,0 +1,32 @@ +import chalk from 'chalk'; +import { HashMap, Logger, Match, Option } from 'effect'; + +export function warn(firstArg, ...rest) { + console.warn(chalk.yellow('[warning]') + ' ' + firstArg, ...rest); +} + +export function error(firstArg, ...rest) { + console.error(chalk.red('[error]') + ' ' + firstArg, ...rest); +} + +const simpleLogger = Logger.make(({ logLevel, message, annotations }) => { + const isSilent = HashMap.get(annotations, 'silent').pipe(Option.getOrNull); + if (isSilent) return; + const formattedMessage = Array.isArray(message) ? message.join(' ') : message; + Match.value(logLevel).pipe( + Match.tag('Info', () => console.log(formattedMessage)), + Match.tag('Warning', () => console.warn(formattedMessage)), + Match.tag('Error', () => + console.error(chalk.red('[error]') + ' ' + formattedMessage), + ), + Match.tag('Debug', () => console.debug(formattedMessage)), + Match.tag('Fatal', () => + console.error(chalk.red('[error]') + ' ' + formattedMessage), + ), + ); +}); + +export const SimpleLogLayer = Logger.replace( + Logger.defaultLogger, + simpleLogger, +); diff --git a/client/packages/cli/src/new/program.ts b/client/packages/cli/src/new/program.ts new file mode 100644 index 0000000000..c88b800e84 --- /dev/null +++ b/client/packages/cli/src/new/program.ts @@ -0,0 +1,3 @@ +import { Command } from '@commander-js/extra-typings'; + +export const program = new Command(); diff --git a/client/packages/cli/src/renderSchemaPlan.ts b/client/packages/cli/src/renderSchemaPlan.ts index ff28408564..7baf817555 100644 --- a/client/packages/cli/src/renderSchemaPlan.ts +++ b/client/packages/cli/src/renderSchemaPlan.ts @@ -98,7 +98,7 @@ type AddOrDeleteAttr = | MigrationTxSpecific<'add-attr'> | MigrationTxSpecific<'delete-attr'>; -type SuperMigrationTx = +export type SuperMigrationTx = | MigrationTx | { type: 'create-namespace'; namespace: string; innerSteps: MigrationTx[] } | { type: 'delete-namespace'; namespace: string; innerSteps: MigrationTx[] }; diff --git a/client/packages/cli/src/ui/index.ts b/client/packages/cli/src/ui/index.ts index 5209609b66..2b93964780 100644 --- a/client/packages/cli/src/ui/index.ts +++ b/client/packages/cli/src/ui/index.ts @@ -864,7 +864,7 @@ ${inputDisplay}`; } } - interface AppSelectorApi { + export interface AppSelectorApi { getDash: () => { apps: App[]; orgs: Org[] }; createEphemeralApp: (title: string) => Promise<{ appId: string; diff --git a/client/packages/cli/src/util/getAuthPaths.ts b/client/packages/cli/src/util/getAuthPaths.ts index 0af3ae571d..5a8e8a3c25 100644 --- a/client/packages/cli/src/util/getAuthPaths.ts +++ b/client/packages/cli/src/util/getAuthPaths.ts @@ -2,6 +2,7 @@ import envPaths from 'env-paths'; import { join } from 'path'; const dev = Boolean(process.env.INSTANT_CLI_DEV); + export function getAuthPaths() { const key = `instantdb-${dev ? 'dev' : 'prod'}`; const { config: appConfigDirPath } = envPaths(key); diff --git a/client/packages/cli/src/util/mergeSchema.js b/client/packages/cli/src/util/mergeSchema.ts similarity index 96% rename from client/packages/cli/src/util/mergeSchema.js rename to client/packages/cli/src/util/mergeSchema.ts index afbf3346c4..de5df89957 100644 --- a/client/packages/cli/src/util/mergeSchema.js +++ b/client/packages/cli/src/util/mergeSchema.ts @@ -1,6 +1,12 @@ import * as acorn from 'acorn'; import tsPlugin from 'acorn-typescript'; +import { Data } from 'effect'; +export class MergeSchemaError extends Data.TaggedError('MergeSchemaError')<{ + message: string; +}> {} + +// @ts-ignore const node = acorn.Parser.extend(tsPlugin({ dts: false })); // --- Import Handling Helpers --- @@ -102,7 +108,7 @@ function getPropName(prop) { return null; } -function analyzeChain(node) { +function analyzeChain(node): any { let curr = node; let typeParams = null; let baseCall = null; @@ -192,7 +198,7 @@ function findSchemaObject(ast) { return schemaObj; } -export function mergeSchema(oldFile, newFile) { +export function mergeSchema(oldFile, newFile): string { const oldParsed = node.parse(oldFile, { sourceType: 'module', ecmaVersion: 'latest', @@ -225,7 +231,7 @@ export function mergeSchema(oldFile, newFile) { const neededIdentifiers = new Set(); // 3. Apply to new file & Collect needed identifiers - const edits = []; + const edits: any[] = []; const newSchemaObj = findSchemaObject(newParsed); if (newSchemaObj) { @@ -290,7 +296,7 @@ export function mergeSchema(oldFile, newFile) { } } - const importBlocks = []; + const importBlocks: string[] = []; for (const [source, info] of importsToAdd) { // Check if source exists in new file to merge? @@ -299,7 +305,7 @@ export function mergeSchema(oldFile, newFile) { // If we have named imports if (info.named.size > 0) { - const namedImports = Array.from(info.named.values()); + const namedImports: any[] = Array.from(info.named.values()); const allTypes = namedImports.every((x) => x.isType); if (allTypes) { diff --git a/client/packages/cli/tsconfig.json b/client/packages/cli/tsconfig.json index 06246f01f3..4d49715cda 100644 --- a/client/packages/cli/tsconfig.json +++ b/client/packages/cli/tsconfig.json @@ -5,6 +5,11 @@ "rootDir": "src", "sourceMap": true, "skipLibCheck": true, // `unconfig` currently imports a broken type from `@antfu/utils`, + "plugins": [ + { + "name": "@effect/language-service" + } + ], "strictNullChecks": true // remove once fixed }, diff --git a/client/pnpm-lock.yaml b/client/pnpm-lock.yaml index ad155eb331..24163130f0 100644 --- a/client/pnpm-lock.yaml +++ b/client/pnpm-lock.yaml @@ -69,6 +69,15 @@ importers: packages/cli: dependencies: + '@commander-js/extra-typings': + specifier: ^14.0.0 + version: 14.0.0(commander@12.1.0) + '@effect/platform': + specifier: ^0.94.1 + version: 0.94.1(effect@3.19.14) + '@effect/platform-node': + specifier: ^0.104.0 + version: 0.104.0(@effect/cluster@0.56.1(@effect/platform@0.94.1(effect@3.19.14))(@effect/rpc@0.73.0(@effect/platform@0.94.1(effect@3.19.14))(effect@3.19.14))(@effect/sql@0.49.0(@effect/experimental@0.58.0(@effect/platform@0.94.1(effect@3.19.14))(effect@3.19.14)(ioredis@5.6.1))(@effect/platform@0.94.1(effect@3.19.14))(effect@3.19.14))(@effect/workflow@0.16.0(@effect/experimental@0.58.0(@effect/platform@0.94.1(effect@3.19.14))(effect@3.19.14)(ioredis@5.6.1))(@effect/platform@0.94.1(effect@3.19.14))(@effect/rpc@0.73.0(@effect/platform@0.94.1(effect@3.19.14))(effect@3.19.14))(effect@3.19.14))(effect@3.19.14))(@effect/platform@0.94.1(effect@3.19.14))(@effect/rpc@0.73.0(@effect/platform@0.94.1(effect@3.19.14))(effect@3.19.14))(@effect/sql@0.49.0(@effect/experimental@0.58.0(@effect/platform@0.94.1(effect@3.19.14))(effect@3.19.14)(ioredis@5.6.1))(@effect/platform@0.94.1(effect@3.19.14))(effect@3.19.14))(effect@3.19.14) '@instantdb/core': specifier: workspace:* version: link:../core @@ -102,6 +111,9 @@ importers: dotenv-flow: specifier: ^4.1.0 version: 4.1.0 + effect: + specifier: ^3.19.14 + version: 3.19.14 env-paths: specifier: ^3.0.0 version: 3.0.0 @@ -123,6 +135,12 @@ importers: package-directory: specifier: ^8.1.0 version: 8.1.0 + package-manager-detector: + specifier: ^1.6.0 + version: 1.6.0 + pkg-types: + specifier: ^2.3.0 + version: 2.3.0 prettier: specifier: ^3.3.3 version: 3.4.2 @@ -148,6 +166,9 @@ importers: '@babel/preset-env': specifier: ^7.16.11 version: 7.26.7(@babel/core@7.26.7) + '@effect/language-service': + specifier: ^0.64.1 + version: 0.64.1 '@types/json-diff': specifier: ^1.0.3 version: 1.0.3 @@ -155,7 +176,7 @@ importers: specifier: ^4.1.9 version: 4.1.9 '@types/node': - specifier: ^22.6.1 + specifier: ^22.13.0 version: 22.13.0 npm-run-all: specifier: ^4.1.5 @@ -2831,6 +2852,11 @@ packages: resolution: {integrity: sha512-Ir+AOibqzrIsL6ajt3Rz3LskB7OiMVHqltZmspbW/TJuTVuyOMirVqAkjfY6JISiLHgyNqicAC8AyHHGzNd/dA==} engines: {node: '>=0.1.90'} + '@commander-js/extra-typings@14.0.0': + resolution: {integrity: sha512-hIn0ncNaJRLkZrxBIp5AsW/eXEHNKYQBh0aPdoUqNgD+Io3NIykQqpKFyKcuasZhicGaEZJX/JBSIkZ4e5x8Dg==} + peerDependencies: + commander: ~14.0.0 + '@corex/deepmerge@4.0.43': resolution: {integrity: sha512-N8uEMrMPL0cu/bdboEWpQYb/0i2K5Qn8eCsxzOmxSggJbbQte7ljMRoXm917AbntqTGOzdTu+vP3KOOzoC70HQ==} @@ -2921,6 +2947,76 @@ packages: search-insights: optional: true + '@effect/cluster@0.56.1': + resolution: {integrity: sha512-gnrsH6kfrUjn+82j/bw1IR4yFqJqV8tc7xZvrbJPRgzANycc6K1hu3LMg548uYbUkTzD8YYyqrSatMO1mkQpzw==} + peerDependencies: + '@effect/platform': ^0.94.1 + '@effect/rpc': ^0.73.0 + '@effect/sql': ^0.49.0 + '@effect/workflow': ^0.16.0 + effect: ^3.19.14 + + '@effect/experimental@0.58.0': + resolution: {integrity: sha512-IEP9sapjF6rFy5TkoqDPc86st/fnqUfjT7Xa3pWJrFGr1hzaMXHo+mWsYOZS9LAOVKnpHuVziDK97EP5qsCHVA==} + peerDependencies: + '@effect/platform': ^0.94.0 + effect: ^3.19.13 + ioredis: ^5 + lmdb: ^3 + peerDependenciesMeta: + ioredis: + optional: true + lmdb: + optional: true + + '@effect/language-service@0.64.1': + resolution: {integrity: sha512-1S7Xr2t9mygR6QyiIdCQUvPwSvcf5EBpnWmKa/8rC4P0btFfw/RiWRI8bvFI2AW+U4KStMUHYxPu4QEsk4y8bg==} + hasBin: true + + '@effect/platform-node-shared@0.57.0': + resolution: {integrity: sha512-QXuvmLNlABCQLcTl+lN1YPhKosR6KqArPYjC2reU0fb5lroCo3YRb/aGpXIgLthHzQL8cLU5XMGA3Cu5hKY2Tw==} + peerDependencies: + '@effect/cluster': ^0.56.0 + '@effect/platform': ^0.94.0 + '@effect/rpc': ^0.73.0 + '@effect/sql': ^0.49.0 + effect: ^3.19.13 + + '@effect/platform-node@0.104.0': + resolution: {integrity: sha512-2ZkUDDTxLD95ARdYIKBx4tdIIgqA3cwb3jlnVVBxmHUf0Pg5N2HdMuD0Q+CXQ7Q94FDwnLW3ZvaSfxDh6FvrNw==} + peerDependencies: + '@effect/cluster': ^0.56.0 + '@effect/platform': ^0.94.0 + '@effect/rpc': ^0.73.0 + '@effect/sql': ^0.49.0 + effect: ^3.19.13 + + '@effect/platform@0.94.1': + resolution: {integrity: sha512-SlL8OMTogHmMNnFLnPAHHo3ua1yrB1LNQOVQMiZsqYu9g3216xjr0gn5WoDgCxUyOdZcseegMjWJ7dhm/2vnfg==} + peerDependencies: + effect: ^3.19.14 + + '@effect/rpc@0.73.0': + resolution: {integrity: sha512-iMPf6tTriz8sK0l5x4koFId8Hz5nFptHYg8WqyjHGIIVLTpZxuiSqhmXZG7FnAs5N2n6uCEws4wWGcIgXNUrFg==} + peerDependencies: + '@effect/platform': ^0.94.0 + effect: ^3.19.13 + + '@effect/sql@0.49.0': + resolution: {integrity: sha512-9UEKR+z+MrI/qMAmSvb/RiD9KlgIazjZUCDSpwNgm0lEK9/Q6ExEyfziiYFVCPiptp52cBw8uBHRic8hHnwqXA==} + peerDependencies: + '@effect/experimental': ^0.58.0 + '@effect/platform': ^0.94.0 + effect: ^3.19.13 + + '@effect/workflow@0.16.0': + resolution: {integrity: sha512-MiAdlxx3TixkgHdbw+Yf1Z3tHAAE0rOQga12kIydJqj05Fnod+W/I+kQGRMY/XWRg+QUsVxhmh1qTr7Ype6lrw==} + peerDependencies: + '@effect/experimental': ^0.58.0 + '@effect/platform': ^0.94.0 + '@effect/rpc': ^0.73.0 + effect: ^3.19.13 + '@emnapi/core@1.4.3': resolution: {integrity: sha512-4m62DuCE07lw01soJwPiBGC0nAww0Q+RY70VZ+n49yDIO13yyinhbWCeNnaob0lakDtWQzSdtNWzJeOJt2ma+g==} @@ -4708,6 +4804,36 @@ packages: react: ^16.8.0 || ^17.0.0 || ^18.0.0 react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 + '@msgpackr-extract/msgpackr-extract-darwin-arm64@3.0.3': + resolution: {integrity: sha512-QZHtlVgbAdy2zAqNA9Gu1UpIuI8Xvsd1v8ic6B2pZmeFnFcMWiPLfWXh7TVw4eGEZ/C9TH281KwhVoeQUKbyjw==} + cpu: [arm64] + os: [darwin] + + '@msgpackr-extract/msgpackr-extract-darwin-x64@3.0.3': + resolution: {integrity: sha512-mdzd3AVzYKuUmiWOQ8GNhl64/IoFGol569zNRdkLReh6LRLHOXxU4U8eq0JwaD8iFHdVGqSy4IjFL4reoWCDFw==} + cpu: [x64] + os: [darwin] + + '@msgpackr-extract/msgpackr-extract-linux-arm64@3.0.3': + resolution: {integrity: sha512-YxQL+ax0XqBJDZiKimS2XQaf+2wDGVa1enVRGzEvLLVFeqa5kx2bWbtcSXgsxjQB7nRqqIGFIcLteF/sHeVtQg==} + cpu: [arm64] + os: [linux] + + '@msgpackr-extract/msgpackr-extract-linux-arm@3.0.3': + resolution: {integrity: sha512-fg0uy/dG/nZEXfYilKoRe7yALaNmHoYeIoJuJ7KJ+YyU2bvY8vPv27f7UKhGRpY6euFYqEVhxCFZgAUNQBM3nw==} + cpu: [arm] + os: [linux] + + '@msgpackr-extract/msgpackr-extract-linux-x64@3.0.3': + resolution: {integrity: sha512-cvwNfbP07pKUfq1uH+S6KJ7dT9K8WOE4ZiAcsrSes+UY55E/0jLYc+vq+DO7jlmqRb5zAggExKm0H7O/CBaesg==} + cpu: [x64] + os: [linux] + + '@msgpackr-extract/msgpackr-extract-win32-x64@3.0.3': + resolution: {integrity: sha512-x0fWaQtYp4E6sktbsdAqnehxDgEc/VwM7uLsRCYWaiGu0ykYdZPiS8zCWdnjHwyiumousxfBm4SO31eXqwEZhQ==} + cpu: [x64] + os: [win32] + '@mux/mux-data-google-ima@0.2.8': resolution: {integrity: sha512-0ZEkHdcZ6bS8QtcjFcoJeZxJTpX7qRIledf4q1trMWPznugvtajCjCM2kieK/pzkZj1JM6liDRFs1PJSfVUs2A==} @@ -9878,6 +10004,9 @@ packages: ee-first@1.1.1: resolution: {integrity: sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==} + effect@3.19.14: + resolution: {integrity: sha512-3vwdq0zlvQOxXzXNKRIPKTqZNMyGCdaFUBfMPqpsyzZDre67kgC1EEHDV4EoQTovJ4w5fmJW756f86kkuz7WFA==} + electron-to-chromium@1.5.150: resolution: {integrity: sha512-rOOkP2ZUMx1yL4fCxXQKDHQ8ZXwisb2OycOQVKHgvB3ZI4CvehOd4y2tfnnLDieJ3Zs1RL1Dlp3cMkyIn7nnXA==} @@ -10477,6 +10606,9 @@ packages: exsolve@1.0.5: resolution: {integrity: sha512-pz5dvkYYKQ1AHVrgOzBKWeP4u4FRb3a6DNK2ucr0OoNwYIU4QWsJ+NM36LLzORT+z845MzKHHhpXiUF5nvQoJg==} + exsolve@1.0.8: + resolution: {integrity: sha512-LmDxfWXwcTArk8fUEnOfSZpHOJ6zOMUJKOtFLFqJLoKJetuQG874Uc7/Kki7zFLzYybmZhp1M7+98pfMqeX8yA==} + extend-shallow@2.0.1: resolution: {integrity: sha512-zCnTtlxNoAiDc3gqY2aYAWFx7XWWiasuF2K8Me5WbN8otHKTUKBwjPtNpRs/rbUZm7KxWAaNj7P1a/p52GbVug==} engines: {node: '>=0.10.0'} @@ -10639,6 +10771,9 @@ packages: resolution: {integrity: sha512-Tq6PixE0w/VMFfCgbONnkiQIVol/JJL7nRMi20fqzA4NRs9AfeqMGeRdPi3wIhYkxjeBaWh2rxwapn5Tu3IqOQ==} engines: {node: '>=6'} + find-my-way-ts@0.1.6: + resolution: {integrity: sha512-a85L9ZoXtNAey3Y6Z+eBWW658kO/MwR7zIafkIUPUMf3isZG0NCs2pjW2wtjxAKuJPxMAsHUIP4ZPGv0o5gyTA==} + find-up-simple@1.0.0: resolution: {integrity: sha512-q7Us7kcjj2VMePAa02hDAF6d+MzsdsAWEwYyOpwUtlerRBkOEPBCRZrAV4XfcSN8fHAgaD0hP7miwoay6DCprw==} engines: {node: '>=18'} @@ -12023,6 +12158,9 @@ packages: kolorist@1.8.0: resolution: {integrity: sha512-Y+60/zizpJ3HRH8DCss+q95yr6145JXZo46OTpFvDZWLfRCE4qChOyk1b26nMaNpfHHgxagk9dXT5OP0Tfe+dQ==} + kubernetes-types@1.30.0: + resolution: {integrity: sha512-Dew1okvhM/SQcIa2rcgujNndZwU8VnSapDgdxlYoB84ZlpAD43U6KLAFqYo17ykSFGHNPrg0qry0bP+GJd9v7Q==} + kuler@2.0.0: resolution: {integrity: sha512-Xq9nH7KlWZmXAtodXDDRE7vs6DU1gTU8zYDHDiWLSip45Egwq3plLHzPn27NgvzL2r1LMPC1vdqh98sQxtqj4A==} @@ -13052,6 +13190,16 @@ packages: ms@2.1.3: resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} + msgpackr-extract@3.0.3: + resolution: {integrity: sha512-P0efT1C9jIdVRefqjzOQ9Xml57zpOXnIuS+csaB4MdZbTdmGDLo8XhzBG1N7aO11gKDDkJvBLULeFTo46wwreA==} + hasBin: true + + msgpackr@1.11.8: + resolution: {integrity: sha512-bC4UGzHhVvgDNS7kn9tV8fAucIYUBuGojcaLiz7v+P63Lmtm0Xeji8B/8tYKddALXxJLpwIeBmUN3u64C4YkRA==} + + multipasta@0.2.7: + resolution: {integrity: sha512-KPA58d68KgGil15oDqXjkUBEBYc00XvbPj5/X+dyzeo/lWm9Nc25pQRlf1D+gv4OpK7NM0J1odrbu9JNNGvynA==} + mutative@1.1.0: resolution: {integrity: sha512-2PJADREjOusk3iJkD3rXV2YjAxTuaLxdfqtqTEt6vcY07LtEBR1seHuBHXWEIuscqRDGvbauYPs+A4Rj/KTczQ==} engines: {node: '>=14.0'} @@ -13249,6 +13397,10 @@ packages: resolution: {integrity: sha512-dPEtOeMvF9VMcYV/1Wb8CPoVAXtp6MKMlcbAt4ddqmGqUJ6fQZFXkNZNkNlfevtNkGtaSoXf/vNNNSvgrdXwtA==} engines: {node: '>= 6.13.0'} + node-gyp-build-optional-packages@5.2.2: + resolution: {integrity: sha512-s+w+rBWnpTMwSFbaE0UXsRlg7hU4FjekKU4eyAih5T8nJuNZT1nNsskXpxmeqSK9UzkBl6UgRlnKc8hz8IEqOw==} + hasBin: true + node-gyp-build@4.8.4: resolution: {integrity: sha512-LA4ZjwlnUblHVgq0oBF3Jl/6h/Nvs5fzBLwdEF4nuxnFdsfajde4WfxtJr3CaiH+F6ewcIB/q4jQ4UzPyid+CQ==} hasBin: true @@ -13608,6 +13760,9 @@ packages: package-manager-detector@1.3.0: resolution: {integrity: sha512-ZsEbbZORsyHuO00lY1kV3/t72yp6Ysay6Pd17ZAlNGuGwmWDLCJxFpRs0IzfXfj1o4icJOkUEioexFHzyPurSQ==} + package-manager-detector@1.6.0: + resolution: {integrity: sha512-61A5ThoTiDG/C8s8UMZwSorAGwMJ0ERVGj2OjoW5pAalsNOg15+iQiPzrLJ4jhZ1HJzmC2PIHT2oEiH3R5fzNA==} + pako@0.2.9: resolution: {integrity: sha512-NUcwaKxUxWrZLpDG+z/xZaCgQITkA/Dv4V/T6bw7VON6l1Xz/VnrBqrYjZQ12TamKHzITTfOEIYUj48y2KXImA==} @@ -13833,6 +13988,9 @@ packages: pkg-types@2.1.0: resolution: {integrity: sha512-wmJwA+8ihJixSoHKxZJRBQG1oY8Yr9pGLzRmSsNms0iNWyHHAlZCa7mmKiFR10YPZuz/2k169JiS/inOjBCZ2A==} + pkg-types@2.3.0: + resolution: {integrity: sha512-SIqCzDRg0s9npO5XQ3tNZioRY1uK06lA41ynBC1YmFTmnY6FjUjVt6s4LoADmwoig1qqD0oK8h1p/8mlMx8Oig==} + player.style@0.2.0: resolution: {integrity: sha512-Ngoaz49TClptMr8HDA2IFmjT3Iq6R27QEUH/C+On33L59RSF3dCLefBYB1Au2RDZQJ6oVFpc1sXaPVpp7fEzzA==} @@ -16110,6 +16268,10 @@ packages: resolution: {integrity: sha512-q/1rj5D0/zayJB2FraXdaWxbhWiNKDvu8naDT2dl1yTlvJp4BLtOcp2a5BvgGNQpYYJzau7tf1WgKv3b+7mqpQ==} engines: {node: '>=18.17'} + undici@7.18.2: + resolution: {integrity: sha512-y+8YjDFzWdQlSE9N5nzKMT3g4a5UBX1HKowfdXh0uvAnTaqqwqB92Jt4UXBAeKekDs5IaDKyJFR4X1gYVCgXcw==} + engines: {node: '>=20.18.1'} + unenv@2.0.0-rc.15: resolution: {integrity: sha512-J/rEIZU8w6FOfLNz/hNKsnY+fFHWnu9MH4yRbSZF3xbbGHovcetXPs7sD+9p8L6CeNC//I9bhRYAOsBt2u7/OA==} @@ -19373,6 +19535,10 @@ snapshots: '@colors/colors@1.6.0': {} + '@commander-js/extra-typings@14.0.0(commander@12.1.0)': + dependencies: + commander: 12.1.0 + '@corex/deepmerge@4.0.43': {} '@cspotcode/source-map-support@0.8.1': @@ -19458,6 +19624,81 @@ snapshots: transitivePeerDependencies: - '@algolia/client-search' + '@effect/cluster@0.56.1(@effect/platform@0.94.1(effect@3.19.14))(@effect/rpc@0.73.0(@effect/platform@0.94.1(effect@3.19.14))(effect@3.19.14))(@effect/sql@0.49.0(@effect/experimental@0.58.0(@effect/platform@0.94.1(effect@3.19.14))(effect@3.19.14)(ioredis@5.6.1))(@effect/platform@0.94.1(effect@3.19.14))(effect@3.19.14))(@effect/workflow@0.16.0(@effect/experimental@0.58.0(@effect/platform@0.94.1(effect@3.19.14))(effect@3.19.14)(ioredis@5.6.1))(@effect/platform@0.94.1(effect@3.19.14))(@effect/rpc@0.73.0(@effect/platform@0.94.1(effect@3.19.14))(effect@3.19.14))(effect@3.19.14))(effect@3.19.14)': + dependencies: + '@effect/platform': 0.94.1(effect@3.19.14) + '@effect/rpc': 0.73.0(@effect/platform@0.94.1(effect@3.19.14))(effect@3.19.14) + '@effect/sql': 0.49.0(@effect/experimental@0.58.0(@effect/platform@0.94.1(effect@3.19.14))(effect@3.19.14)(ioredis@5.6.1))(@effect/platform@0.94.1(effect@3.19.14))(effect@3.19.14) + '@effect/workflow': 0.16.0(@effect/experimental@0.58.0(@effect/platform@0.94.1(effect@3.19.14))(effect@3.19.14)(ioredis@5.6.1))(@effect/platform@0.94.1(effect@3.19.14))(@effect/rpc@0.73.0(@effect/platform@0.94.1(effect@3.19.14))(effect@3.19.14))(effect@3.19.14) + effect: 3.19.14 + kubernetes-types: 1.30.0 + + '@effect/experimental@0.58.0(@effect/platform@0.94.1(effect@3.19.14))(effect@3.19.14)(ioredis@5.6.1)': + dependencies: + '@effect/platform': 0.94.1(effect@3.19.14) + effect: 3.19.14 + uuid: 11.1.0 + optionalDependencies: + ioredis: 5.6.1 + + '@effect/language-service@0.64.1': {} + + '@effect/platform-node-shared@0.57.0(@effect/cluster@0.56.1(@effect/platform@0.94.1(effect@3.19.14))(@effect/rpc@0.73.0(@effect/platform@0.94.1(effect@3.19.14))(effect@3.19.14))(@effect/sql@0.49.0(@effect/experimental@0.58.0(@effect/platform@0.94.1(effect@3.19.14))(effect@3.19.14)(ioredis@5.6.1))(@effect/platform@0.94.1(effect@3.19.14))(effect@3.19.14))(@effect/workflow@0.16.0(@effect/experimental@0.58.0(@effect/platform@0.94.1(effect@3.19.14))(effect@3.19.14)(ioredis@5.6.1))(@effect/platform@0.94.1(effect@3.19.14))(@effect/rpc@0.73.0(@effect/platform@0.94.1(effect@3.19.14))(effect@3.19.14))(effect@3.19.14))(effect@3.19.14))(@effect/platform@0.94.1(effect@3.19.14))(@effect/rpc@0.73.0(@effect/platform@0.94.1(effect@3.19.14))(effect@3.19.14))(@effect/sql@0.49.0(@effect/experimental@0.58.0(@effect/platform@0.94.1(effect@3.19.14))(effect@3.19.14)(ioredis@5.6.1))(@effect/platform@0.94.1(effect@3.19.14))(effect@3.19.14))(effect@3.19.14)': + dependencies: + '@effect/cluster': 0.56.1(@effect/platform@0.94.1(effect@3.19.14))(@effect/rpc@0.73.0(@effect/platform@0.94.1(effect@3.19.14))(effect@3.19.14))(@effect/sql@0.49.0(@effect/experimental@0.58.0(@effect/platform@0.94.1(effect@3.19.14))(effect@3.19.14)(ioredis@5.6.1))(@effect/platform@0.94.1(effect@3.19.14))(effect@3.19.14))(@effect/workflow@0.16.0(@effect/experimental@0.58.0(@effect/platform@0.94.1(effect@3.19.14))(effect@3.19.14)(ioredis@5.6.1))(@effect/platform@0.94.1(effect@3.19.14))(@effect/rpc@0.73.0(@effect/platform@0.94.1(effect@3.19.14))(effect@3.19.14))(effect@3.19.14))(effect@3.19.14) + '@effect/platform': 0.94.1(effect@3.19.14) + '@effect/rpc': 0.73.0(@effect/platform@0.94.1(effect@3.19.14))(effect@3.19.14) + '@effect/sql': 0.49.0(@effect/experimental@0.58.0(@effect/platform@0.94.1(effect@3.19.14))(effect@3.19.14)(ioredis@5.6.1))(@effect/platform@0.94.1(effect@3.19.14))(effect@3.19.14) + '@parcel/watcher': 2.5.1 + effect: 3.19.14 + multipasta: 0.2.7 + ws: 8.19.0 + transitivePeerDependencies: + - bufferutil + - utf-8-validate + + '@effect/platform-node@0.104.0(@effect/cluster@0.56.1(@effect/platform@0.94.1(effect@3.19.14))(@effect/rpc@0.73.0(@effect/platform@0.94.1(effect@3.19.14))(effect@3.19.14))(@effect/sql@0.49.0(@effect/experimental@0.58.0(@effect/platform@0.94.1(effect@3.19.14))(effect@3.19.14)(ioredis@5.6.1))(@effect/platform@0.94.1(effect@3.19.14))(effect@3.19.14))(@effect/workflow@0.16.0(@effect/experimental@0.58.0(@effect/platform@0.94.1(effect@3.19.14))(effect@3.19.14)(ioredis@5.6.1))(@effect/platform@0.94.1(effect@3.19.14))(@effect/rpc@0.73.0(@effect/platform@0.94.1(effect@3.19.14))(effect@3.19.14))(effect@3.19.14))(effect@3.19.14))(@effect/platform@0.94.1(effect@3.19.14))(@effect/rpc@0.73.0(@effect/platform@0.94.1(effect@3.19.14))(effect@3.19.14))(@effect/sql@0.49.0(@effect/experimental@0.58.0(@effect/platform@0.94.1(effect@3.19.14))(effect@3.19.14)(ioredis@5.6.1))(@effect/platform@0.94.1(effect@3.19.14))(effect@3.19.14))(effect@3.19.14)': + dependencies: + '@effect/cluster': 0.56.1(@effect/platform@0.94.1(effect@3.19.14))(@effect/rpc@0.73.0(@effect/platform@0.94.1(effect@3.19.14))(effect@3.19.14))(@effect/sql@0.49.0(@effect/experimental@0.58.0(@effect/platform@0.94.1(effect@3.19.14))(effect@3.19.14)(ioredis@5.6.1))(@effect/platform@0.94.1(effect@3.19.14))(effect@3.19.14))(@effect/workflow@0.16.0(@effect/experimental@0.58.0(@effect/platform@0.94.1(effect@3.19.14))(effect@3.19.14)(ioredis@5.6.1))(@effect/platform@0.94.1(effect@3.19.14))(@effect/rpc@0.73.0(@effect/platform@0.94.1(effect@3.19.14))(effect@3.19.14))(effect@3.19.14))(effect@3.19.14) + '@effect/platform': 0.94.1(effect@3.19.14) + '@effect/platform-node-shared': 0.57.0(@effect/cluster@0.56.1(@effect/platform@0.94.1(effect@3.19.14))(@effect/rpc@0.73.0(@effect/platform@0.94.1(effect@3.19.14))(effect@3.19.14))(@effect/sql@0.49.0(@effect/experimental@0.58.0(@effect/platform@0.94.1(effect@3.19.14))(effect@3.19.14)(ioredis@5.6.1))(@effect/platform@0.94.1(effect@3.19.14))(effect@3.19.14))(@effect/workflow@0.16.0(@effect/experimental@0.58.0(@effect/platform@0.94.1(effect@3.19.14))(effect@3.19.14)(ioredis@5.6.1))(@effect/platform@0.94.1(effect@3.19.14))(@effect/rpc@0.73.0(@effect/platform@0.94.1(effect@3.19.14))(effect@3.19.14))(effect@3.19.14))(effect@3.19.14))(@effect/platform@0.94.1(effect@3.19.14))(@effect/rpc@0.73.0(@effect/platform@0.94.1(effect@3.19.14))(effect@3.19.14))(@effect/sql@0.49.0(@effect/experimental@0.58.0(@effect/platform@0.94.1(effect@3.19.14))(effect@3.19.14)(ioredis@5.6.1))(@effect/platform@0.94.1(effect@3.19.14))(effect@3.19.14))(effect@3.19.14) + '@effect/rpc': 0.73.0(@effect/platform@0.94.1(effect@3.19.14))(effect@3.19.14) + '@effect/sql': 0.49.0(@effect/experimental@0.58.0(@effect/platform@0.94.1(effect@3.19.14))(effect@3.19.14)(ioredis@5.6.1))(@effect/platform@0.94.1(effect@3.19.14))(effect@3.19.14) + effect: 3.19.14 + mime: 3.0.0 + undici: 7.18.2 + ws: 8.19.0 + transitivePeerDependencies: + - bufferutil + - utf-8-validate + + '@effect/platform@0.94.1(effect@3.19.14)': + dependencies: + effect: 3.19.14 + find-my-way-ts: 0.1.6 + msgpackr: 1.11.8 + multipasta: 0.2.7 + + '@effect/rpc@0.73.0(@effect/platform@0.94.1(effect@3.19.14))(effect@3.19.14)': + dependencies: + '@effect/platform': 0.94.1(effect@3.19.14) + effect: 3.19.14 + msgpackr: 1.11.8 + + '@effect/sql@0.49.0(@effect/experimental@0.58.0(@effect/platform@0.94.1(effect@3.19.14))(effect@3.19.14)(ioredis@5.6.1))(@effect/platform@0.94.1(effect@3.19.14))(effect@3.19.14)': + dependencies: + '@effect/experimental': 0.58.0(@effect/platform@0.94.1(effect@3.19.14))(effect@3.19.14)(ioredis@5.6.1) + '@effect/platform': 0.94.1(effect@3.19.14) + effect: 3.19.14 + uuid: 11.1.0 + + '@effect/workflow@0.16.0(@effect/experimental@0.58.0(@effect/platform@0.94.1(effect@3.19.14))(effect@3.19.14)(ioredis@5.6.1))(@effect/platform@0.94.1(effect@3.19.14))(@effect/rpc@0.73.0(@effect/platform@0.94.1(effect@3.19.14))(effect@3.19.14))(effect@3.19.14)': + dependencies: + '@effect/experimental': 0.58.0(@effect/platform@0.94.1(effect@3.19.14))(effect@3.19.14)(ioredis@5.6.1) + '@effect/platform': 0.94.1(effect@3.19.14) + '@effect/rpc': 0.73.0(@effect/platform@0.94.1(effect@3.19.14))(effect@3.19.14) + effect: 3.19.14 + '@emnapi/core@1.4.3': dependencies: '@emnapi/wasi-threads': 1.0.2 @@ -21448,6 +21689,24 @@ snapshots: react: 18.3.1 react-dom: 18.3.1(react@18.3.1) + '@msgpackr-extract/msgpackr-extract-darwin-arm64@3.0.3': + optional: true + + '@msgpackr-extract/msgpackr-extract-darwin-x64@3.0.3': + optional: true + + '@msgpackr-extract/msgpackr-extract-linux-arm64@3.0.3': + optional: true + + '@msgpackr-extract/msgpackr-extract-linux-arm@3.0.3': + optional: true + + '@msgpackr-extract/msgpackr-extract-linux-x64@3.0.3': + optional: true + + '@msgpackr-extract/msgpackr-extract-win32-x64@3.0.3': + optional: true + '@mux/mux-data-google-ima@0.2.8': dependencies: mux-embed: 5.9.0 @@ -27398,6 +27657,11 @@ snapshots: ee-first@1.1.1: {} + effect@3.19.14: + dependencies: + '@standard-schema/spec': 1.0.0 + fast-check: 3.23.2 + electron-to-chromium@1.5.150: {} electron-to-chromium@1.5.90: {} @@ -28292,6 +28556,8 @@ snapshots: exsolve@1.0.5: {} + exsolve@1.0.8: {} + extend-shallow@2.0.1: dependencies: is-extendable: 0.1.1 @@ -28475,6 +28741,8 @@ snapshots: make-dir: 2.1.0 pkg-dir: 3.0.0 + find-my-way-ts@0.1.6: {} + find-up-simple@1.0.0: {} find-up@3.0.0: @@ -30231,6 +30499,8 @@ snapshots: kolorist@1.8.0: {} + kubernetes-types@1.30.0: {} + kuler@2.0.0: {} lambda-local@2.2.0: @@ -31628,6 +31898,24 @@ snapshots: ms@2.1.3: {} + msgpackr-extract@3.0.3: + dependencies: + node-gyp-build-optional-packages: 5.2.2 + optionalDependencies: + '@msgpackr-extract/msgpackr-extract-darwin-arm64': 3.0.3 + '@msgpackr-extract/msgpackr-extract-darwin-x64': 3.0.3 + '@msgpackr-extract/msgpackr-extract-linux-arm': 3.0.3 + '@msgpackr-extract/msgpackr-extract-linux-arm64': 3.0.3 + '@msgpackr-extract/msgpackr-extract-linux-x64': 3.0.3 + '@msgpackr-extract/msgpackr-extract-win32-x64': 3.0.3 + optional: true + + msgpackr@1.11.8: + optionalDependencies: + msgpackr-extract: 3.0.3 + + multipasta@0.2.7: {} + mutative@1.1.0: {} mute-stream@1.0.0: {} @@ -31966,6 +32254,11 @@ snapshots: node-forge@1.3.1: {} + node-gyp-build-optional-packages@5.2.2: + dependencies: + detect-libc: 2.1.2 + optional: true + node-gyp-build@4.8.4: {} node-int64@0.4.0: {} @@ -32449,6 +32742,8 @@ snapshots: package-manager-detector@1.3.0: {} + package-manager-detector@1.6.0: {} + pako@0.2.9: {} parent-module@1.0.1: @@ -32669,6 +32964,12 @@ snapshots: exsolve: 1.0.5 pathe: 2.0.3 + pkg-types@2.3.0: + dependencies: + confbox: 0.2.2 + exsolve: 1.0.8 + pathe: 2.0.3 + player.style@0.2.0(react@18.3.1): dependencies: media-chrome: 4.13.1(react@18.3.1) @@ -35411,6 +35712,8 @@ snapshots: undici@6.21.1: {} + undici@7.18.2: {} + unenv@2.0.0-rc.15: dependencies: defu: 6.1.4