Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion client/packages/cli/__tests__/e2e/helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand Down
15 changes: 12 additions & 3 deletions client/packages/cli/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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:*",
Expand All @@ -32,13 +36,16 @@
"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",
"json5": "^2.2.3",
"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",
Expand All @@ -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"
Expand Down
99 changes: 15 additions & 84 deletions client/packages/cli/src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -393,7 +393,7 @@ program
.option('--title <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.')
Expand Down Expand Up @@ -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 =
Expand Down Expand Up @@ -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') {
Expand Down Expand Up @@ -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',
Expand Down Expand Up @@ -1353,7 +1283,7 @@ async function getOrPromptPackageAndAuthInfoWithErrorLogging(opts) {
return { pkgDir, projectType, instantModuleName, authToken };
}

async function pullSchema(
async function oldPullSchema(
appId,
{ pkgDir, instantModuleName, experimentalTypePreservation },
) {
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -2050,7 +1981,7 @@ function prettyPrintJSONErr(data) {
}
}

async function readLocalPermsFile() {
export async function readLocalPermsFile() {
const readCandidates = getPermsReadCandidates();
const res = await loadConfig({
sources: readCandidates,
Expand All @@ -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,
Expand All @@ -2082,7 +2013,7 @@ async function readLocalSchemaFile() {
return { path: relativePath, schema: res.config };
}

async function readInstantConfigFile() {
export async function readInstantConfigFile() {
return (
await loadConfig({
sources: [
Expand Down Expand Up @@ -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;
}

Expand Down
40 changes: 40 additions & 0 deletions client/packages/cli/src/new/commands/claim.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
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!'));
}).pipe(
Effect.provide(
WithAppLayer({
coerce: false,
allowAdminToken: false,
applyEnv: false,
}),
),
);
29 changes: 29 additions & 0 deletions client/packages/cli/src/new/commands/explorer.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { Effect } from 'effect';
import openInBrowser from 'open';
import { explorerDef, OptsFromCommand } from '../index.js';
import { CurrentApp } from '../context/currentApp.js';
import { WithAppLayer } from '../layer.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}`,
),
),
);
}).pipe(
Effect.provide(
WithAppLayer({
coerce: true,
coerceAuth: true,
appId: opts.app,
}),
),
);
42 changes: 42 additions & 0 deletions client/packages/cli/src/new/commands/info.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
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.');
}
}).pipe(
Effect.provide(
AuthLayerLive({
coerce: false,
allowAdminToken: false,
}).pipe(Layer.catchAll((e) => Layer.empty)),
),
);
Loading
Loading