Big News: Socket raises $60M Series C at a $1B valuation to secure software supply chains for AI-driven development.Announcement
Sign In

@freestyle-sh/fdev-cli

Package Overview
Dependencies
Maintainers
3
Versions
4
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

@freestyle-sh/fdev-cli - npm Package Compare versions

Comparing version
0.1.5
to
0.1.6
+2
-2
package.json
{
"name": "@freestyle-sh/fdev-cli",
"version": "0.1.5",
"version": "0.1.6",
"type": "module",

@@ -24,3 +24,3 @@ "repository": {

"commander": "^14.0.3",
"@freestyle-sh/fdev-engine": "0.1.5"
"@freestyle-sh/fdev-engine": "0.1.6"
},

@@ -27,0 +27,0 @@ "devDependencies": {

@@ -11,4 +11,4 @@ # @freestyle-sh/fdev-cli

`fdev init` asks for a project name, Freestyle API key, and package manager. It creates `fdev.config.ts`, `.env`, `.env.example`, `package.json`, and local ignore rules.
`fdev init` asks for a project name, Freestyle API key, and package manager. It creates a project folder containing `fdev.config.ts`, `.env`, `.env.example`, `package.json`, and local ignore rules.
Projects should install matching `@freestyle-sh/fdev-sdk` versions locally.
+158
-12
#!/usr/bin/env bun
import { existsSync, mkdirSync } from "node:fs";
import { dirname, join } from "node:path";
import { existsSync } from "node:fs";
import { dirname, join, relative, resolve } from "node:path";
import { createInterface } from "node:readline/promises";

@@ -17,3 +17,3 @@ import chalk from "chalk";

import { FDEV_CLI_VERSION } from "./version.ts";
import { defaultProjectName, initProject, normalizeMachineName, type InitProjectResult } from "./init.ts";
import { initProject, normalizeMachineName, type InitProjectResult } from "./init.ts";

@@ -251,4 +251,4 @@ type GlobalOptions = {

async function runInit(command: Command, options: InitOptions): Promise<void> {
const paths = resolveCommandConfigPaths(command);
mkdirSync(paths.projectDir, { recursive: true });
const answers = await resolveInitAnswers(options, wantsJson(command));
const paths = resolveInitProjectPaths(command, answers.name);

@@ -259,3 +259,2 @@ if (existsSync(paths.configPath) && !options.force) {

const answers = await resolveInitAnswers(paths.projectDir, options, wantsJson(command));
const result = initProject({

@@ -279,3 +278,2 @@ projectDir: paths.projectDir,

async function resolveInitAnswers(
projectDir: string,
options: InitOptions,

@@ -298,3 +296,3 @@ jsonMode: boolean,

console.log(chalk.bold("Initialize fdev"));
console.log(chalk.dim("This creates fdev.config.ts, .env, package.json, and local ignore rules."));
console.log(chalk.dim("This creates a project folder with fdev.config.ts, .env, package.json, and local ignore rules."));
console.log("");

@@ -305,3 +303,3 @@ }

? normalizeMachineName(options.name)
: await promptName(defaultProjectName(projectDir));
: await promptName();
const apiKey = options.apiKey?.trim() || await promptRequiredSecret("Freestyle API key");

@@ -326,3 +324,17 @@ const packageManager = options.packageManager ?? (jsonMode || !canPrompt() ? "skip" : await promptPackageManager("skip"));

async function promptName(defaultName: string): Promise<string> {
function resolveInitProjectPaths(command: Command, name: string): { projectDir: string; configPath: string } {
const options = command.optsWithGlobals() as GlobalOptions;
if (options.config) {
throw new Error(`fdev init does not support --config. Use -C/--project to choose the parent directory.`);
}
const parentDir = resolve(process.cwd(), options.project ?? ".");
const projectDir = resolve(parentDir, name);
return {
projectDir,
configPath: join(projectDir, DEFAULT_CONFIG_FILE),
};
}
async function promptName(): Promise<string> {
const rl = createInterface({ input: process.stdin, output: process.stdout });

@@ -332,6 +344,6 @@

for (;;) {
const prompt = `${chalk.cyan("?")} What do you want to call it? ${chalk.dim(`(${defaultName})`)} `;
const prompt = `${chalk.cyan("?")} Project name: `;
const answer = await rl.question(prompt);
try {
return normalizeMachineName(answer || defaultName);
return normalizeMachineName(answer);
} catch (error) {

@@ -355,2 +367,17 @@ console.log(chalk.red(error instanceof Error ? error.message : String(error)));

async function promptPackageManager(defaultValue: PackageManager): Promise<PackageManager> {
const choices: Array<{ value: PackageManager; label: string; hint: string }> = [
{ value: "npm", label: "npm", hint: "npm install" },
{ value: "bun", label: "bun", hint: "bun install" },
{ value: "pnpm", label: "pnpm", hint: "pnpm install" },
{ value: "skip", label: "skip", hint: "do not install now" },
];
const stdin = process.stdin;
if (stdin.isTTY && process.stdout.isTTY) {
return await promptSelect("Install dependencies?", choices, defaultValue);
}
return await promptPackageManagerText(defaultValue);
}
async function promptPackageManagerText(defaultValue: PackageManager): Promise<PackageManager> {
const rl = createInterface({ input: process.stdin, output: process.stdout });

@@ -372,2 +399,115 @@ const choices = "npm, bun, pnpm, skip";

async function promptSelect<T extends string>(
label: string,
choices: Array<{ value: T; label: string; hint?: string }>,
defaultValue: T,
): Promise<T> {
const stdin = process.stdin;
const stdout = process.stdout;
const defaultIndex = choices.findIndex((choice) => choice.value === defaultValue);
let index = defaultIndex >= 0 ? defaultIndex : 0;
let rendered = false;
const lineCount = choices.length + 1;
return new Promise<T>((resolvePromise, reject) => {
const wasRaw = stdin.isRaw;
const render = () => {
if (rendered) {
stdout.write(`\x1b[${lineCount}A\x1b[J`);
}
rendered = true;
stdout.write(`${chalk.cyan("?")} ${label}\n`);
for (const [choiceIndex, choice] of choices.entries()) {
const selected = choiceIndex === index;
const pointer = selected ? chalk.cyan("›") : " ";
const name = selected ? chalk.cyan(choice.label) : choice.label;
const hint = choice.hint ? chalk.dim(` ${choice.hint}`) : "";
stdout.write(`${pointer} ${name}${hint}\n`);
}
};
const cleanup = () => {
stdin.off("data", onData);
stdin.setRawMode(wasRaw);
stdin.pause();
stdout.write("\x1b[?25h");
};
const finish = () => {
const selected = choices[index]!;
if (rendered) {
stdout.write(`\x1b[${lineCount}A\x1b[J`);
}
cleanup();
stdout.write(`${chalk.cyan("?")} ${label} ${chalk.green(selected.label)}\n`);
resolvePromise(selected.value);
};
const cancel = () => {
cleanup();
stdout.write("\n");
reject(new Error("Init cancelled."));
};
const move = (delta: number) => {
index = (index + delta + choices.length) % choices.length;
render();
};
const onData = (chunk: Buffer | string) => {
const key = String(chunk);
if (key.includes("\u0003")) {
cancel();
return;
}
for (let offset = 0; offset < key.length;) {
if (key.startsWith("\u001b[A", offset)) {
move(-1);
offset += 3;
continue;
}
if (key.startsWith("\u001b[B", offset)) {
move(1);
offset += 3;
continue;
}
const char = key[offset]!;
if (char === "\r" || char === "\n" || char === " ") {
finish();
return;
}
if (char === "k") {
move(-1);
offset += 1;
continue;
}
if (char === "j") {
move(1);
offset += 1;
continue;
}
const numericChoice = Number(char);
if (Number.isInteger(numericChoice) && numericChoice >= 1 && numericChoice <= choices.length) {
index = numericChoice - 1;
finish();
return;
}
offset += 1;
}
};
stdout.write("\x1b[?25l");
stdin.resume();
stdin.setRawMode(true);
stdin.setEncoding("utf8");
stdin.on("data", onData);
render();
});
}
async function promptSecret(label: string): Promise<string> {

@@ -495,2 +635,3 @@ const stdin = process.stdin;

console.log(chalk.bold("Next steps"));
console.log(` cd ${displayProjectDir(result.projectDir)}`);
if (install.skipped) {

@@ -502,2 +643,7 @@ console.log(` ${detectInstallCommand(result.packageJsonPath)}`);

function displayProjectDir(projectDir: string): string {
const path = relative(process.cwd(), projectDir);
return path && !path.startsWith("..") ? path : projectDir;
}
function printInitLine(status: "created" | "updated" | "kept", path: string): void {

@@ -504,0 +650,0 @@ const color = status === "kept" ? chalk.dim : status === "updated" ? chalk.yellow : chalk.green;

import { describe, expect, test } from "bun:test";
import { mkdirSync, mkdtempSync, readFileSync, writeFileSync } from "node:fs";
import { existsSync, mkdirSync, mkdtempSync, readFileSync, writeFileSync } from "node:fs";
import { tmpdir } from "node:os";

@@ -11,3 +11,4 @@ import { join } from "node:path";

test("creates a full fdev project", () => {
const projectDir = mkdtempSync(join(tmpdir(), "fdev-init-"));
const parentDir = mkdtempSync(join(tmpdir(), "fdev-init-"));
const projectDir = join(parentDir, "platform-api");
const result = initProject({

@@ -21,2 +22,4 @@ projectDir,

expect(result.name).toBe("platform-api");
expect(result.projectDir).toBe(projectDir);
expect(existsSync(projectDir)).toBe(true);
expect(result.created).toEqual({

@@ -76,6 +79,6 @@ config: true,

test("defaults empty names to fdev", () => {
expect(normalizeMachineName(" ")).toBe("fdev");
expect(normalizeMachineName("!!!")).toBe("fdev");
test("rejects empty names", () => {
expect(() => normalizeMachineName(" ")).toThrow("Project name is required.");
expect(() => normalizeMachineName("!!!")).toThrow("Project name is required.");
});
});

@@ -1,8 +0,6 @@

import { existsSync, readFileSync, writeFileSync } from "node:fs";
import { basename, join } from "node:path";
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
import { join } from "node:path";
import { FDEV_CLI_VERSION } from "./version.ts";
import { SDK_PACKAGE_NAME } from "./project.ts";
export const DEFAULT_MACHINE_NAME = "fdev";
export type InitProjectInput = {

@@ -18,2 +16,3 @@ projectDir: string;

name: string;
projectDir: string;
configPath: string;

@@ -41,2 +40,3 @@ envPath: string;

const name = normalizeMachineName(input.name);
mkdirSync(input.projectDir, { recursive: true });

@@ -66,2 +66,3 @@ if (existsSync(input.configPath) && !input.force) {

name,
projectDir: input.projectDir,
configPath: input.configPath,

@@ -88,9 +89,8 @@ envPath,

export function defaultProjectName(projectDir: string): string {
return normalizeMachineName(packageNameFromDir(projectDir));
}
export function normalizeMachineName(value: string): string {
const name = value.trim().toLowerCase().replace(/[^a-z0-9._-]+/g, "-").replace(/^-+|-+$/g, "");
return name || DEFAULT_MACHINE_NAME;
if (!name) {
throw new Error("Project name is required.");
}
return name;
}

@@ -215,6 +215,2 @@

function packageNameFromDir(projectDir: string): string {
return basename(projectDir).toLowerCase().replace(/[^a-z0-9._-]+/g, "-") || "fdev-project";
}
function isRecord(value: unknown): value is Record<string, any> {

@@ -221,0 +217,0 @@ return Boolean(value && typeof value === "object" && !Array.isArray(value));

@@ -1,1 +0,1 @@

export const FDEV_CLI_VERSION = "0.1.5";
export const FDEV_CLI_VERSION = "0.1.6";