From 0b51c641598771ae0c847ed65c36d53d068cb1e7 Mon Sep 17 00:00:00 2001 From: yaowenc2 Date: Wed, 15 Jul 2026 22:43:15 +0800 Subject: [PATCH] fix(project create): guide to name it instead of inventing a junk-named project MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Follow-up to the no-prompt fix. Bare `insta project create` in a generic dir (~/projects, /tmp, the home dir) previously auto-generated a random name and provisioned real postgres/storage/compute under it — surprising for a bare command. Now it guides instead: name your project: insta project create (or just ask your coding agent — it has the insta skill and will do this for you) Still never prompts/hangs, and still uses the dir basename when it's a real project-dir name (~/my-app → my-app). Agents always pass a name via the skill, so the guide path is only the manual bare-command case. resolveProjectName now returns string|null (null = guide); removed the name generator. 77/77 green. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01GMbAe5K1RfwaAcge5inX7P --- src/commands/project.ts | 39 +++++++++++++++++---------------------- test/create-name.test.ts | 38 ++++++++++++++++++-------------------- 2 files changed, 35 insertions(+), 42 deletions(-) diff --git a/src/commands/project.ts b/src/commands/project.ts index 4f1cbc2..ad262c7 100644 --- a/src/commands/project.ts +++ b/src/commands/project.ts @@ -6,22 +6,13 @@ import { installObserve } from '../observe/install.js' import { installSkills } from '../ensure-skills.js' // Generic directory names that make a useless project name ("projects", "~", "tmp", …). When the -// cwd basename is one of these we auto-generate a friendly name instead of using it. +// cwd basename is one of these we DON'T invent a name — we guide the user to name it (or let their +// skill-equipped agent do it), rather than provisioning real resources under a junk name. const GENERIC_DIRS = new Set([ 'projects', 'project', 'home', 'tmp', 'temp', 'desktop', 'documents', 'downloads', 'src', 'source', 'code', 'dev', 'work', 'workspace', 'repos', 'repo', 'git', 'app', 'apps', 'users', 'user', 'bin', 'new', 'test', 'tests', ]) -const ADJ = ['swift', 'brave', 'calm', 'bright', 'bold', 'quiet', 'warm', 'keen', 'wise', - 'lucky', 'sunny', 'cosmic', 'gentle', 'rapid', 'vivid', 'amber', 'crisp', 'noble'] -const NOUN = ['otter', 'falcon', 'maple', 'river', 'harbor', 'meadow', 'comet', 'cedar', 'lark', - 'delta', 'summit', 'ember', 'willow', 'pixel', 'forge', 'harbor', 'atlas', 'quartz'] - -/** A friendly auto-generated name like `swift-meadow-482` (Vercel/Render style). */ -export function generateProjectName(rand: () => number = Math.random): string { - const pick = (a: string[]) => a[Math.floor(rand() * a.length)] - return `${pick(ADJ)}-${pick(NOUN)}-${100 + Math.floor(rand() * 900)}` -} // Best-effort: wire the credential-audit hook into the project (no-op if assets aren't built). function tryInstallObserve(): void { @@ -43,26 +34,30 @@ export function slugifyName(raw: string): string { return raw.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, '').slice(0, 40) } -/** Name resolution so `insta project create` (no arg) NEVER blocks on a prompt — the whole point - * of a paste-and-run one-liner. Explicit arg wins; else use the cwd basename when it's a real - * project-dir name; else auto-generate a friendly name (e.g. in ~/projects, where the basename - * "projects" is useless). You can always pass a name or rename later. */ -export async function resolveProjectName( - nameArg: string | undefined, - cwd = process.cwd(), - generate: () => string = generateProjectName, -): Promise { +/** Name resolution — NEVER prompts (a paste-and-run one-liner must not block on input). Returns + * the name to create, or `null` meaning "no sensible name; guide the user instead of inventing + * one". Explicit arg wins; else the cwd basename when it's a real project-dir name; else null + * (generic dir like ~/projects or /tmp, or the home dir itself). Agents pass a name via the skill, + * so null is only reached when a human runs bare `insta project create` somewhere generic. */ +export function resolveProjectName(nameArg: string | undefined, cwd = process.cwd()): string | null { if (nameArg) return slugifyName(nameArg) const base = slugifyName(cwd.split('/').filter(Boolean).pop() ?? '') const home = slugifyName(homedir().split('/').filter(Boolean).pop() ?? '') if (base && base !== home && !GENERIC_DIRS.has(base)) return base - return generate() + return null } export async function projectCreate(name: string | undefined, opts: { org?: string }): Promise { + const resolved = resolveProjectName(name, process.cwd()) + if (!resolved) { + // No name given and the cwd name is generic — don't provision resources under a junk name. + // Guide instead (no hang, no error): name it explicitly, or just ask the skill-equipped agent. + info('name your project: insta project create ') + info(' (or just ask your coding agent — it has the insta skill and will do this for you)') + return + } const api = await ApiClient.load() const orgId = await resolveOrg(api, opts.org) - const resolved = await resolveProjectName(name, process.cwd()) const out = await api.request('POST', `/orgs/${orgId}/projects`, { name: resolved }) await writeProject({ projectId: out.project.id, orgId, branch: out.defaultBranch.name }) info(`created project ${out.project.id} (${resolved})`) diff --git a/test/create-name.test.ts b/test/create-name.test.ts index 823437e..f247737 100644 --- a/test/create-name.test.ts +++ b/test/create-name.test.ts @@ -1,36 +1,34 @@ import { test, expect } from 'vitest' -import { slugifyName, resolveProjectName, generateProjectName } from '../src/commands/project.js' +import { slugifyName, resolveProjectName } from '../src/commands/project.js' test('slugify makes a valid project name', () => { expect(slugifyName('My Cool App!')).toBe('my-cool-app') expect(slugifyName('linkbox')).toBe('linkbox') }) -test('explicit arg wins (slugified)', async () => { - expect(await resolveProjectName('LinkBox', '/x/whatever')).toBe('linkbox') +test('explicit arg wins (slugified)', () => { + expect(resolveProjectName('LinkBox', '/x/whatever')).toBe('linkbox') }) -test('no arg, real project dir → the dir basename (intuitive, no prompt)', async () => { - expect(await resolveProjectName(undefined, '/Users/me/my-project', () => 'gen-name-1')).toBe('my-project') +test('no arg, real project dir → the dir basename (intuitive, no prompt)', () => { + expect(resolveProjectName(undefined, '/Users/me/my-project')).toBe('my-project') }) -test('no arg, GENERIC dir → auto-generated name, never the useless basename', async () => { - // ~/projects, /tmp, etc. must NOT become a project literally named "projects"/"tmp". - expect(await resolveProjectName(undefined, '/Users/gary/projects', () => 'swift-otter-482')).toBe('swift-otter-482') - expect(await resolveProjectName(undefined, '/tmp', () => 'swift-otter-482')).toBe('swift-otter-482') +test('no arg, GENERIC dir → null (caller guides; no junk-named project)', () => { + // ~/projects, /tmp, etc. must NOT become a project named "projects"/"tmp", and must NOT + // auto-invent one either — return null so the command guides the user to name it. + expect(resolveProjectName(undefined, '/Users/gary/projects')).toBeNull() + expect(resolveProjectName(undefined, '/tmp')).toBeNull() + expect(resolveProjectName(undefined, '/Users/me/src')).toBeNull() }) -test('no arg NEVER blocks on a prompt (no stdin/TTY dependency)', async () => { - // Regression: create must resolve a name with zero interaction so `curl|sh && insta project create` - // and `insta project create` in ~/projects never hang waiting for input. - const name = await resolveProjectName(undefined, '/Users/gary/projects', () => 'auto-name-1') - expect(name).toBe('auto-name-1') +test('no arg, the HOME dir itself → null (not the username)', () => { + const home = process.env.HOME || '/Users/me' + expect(resolveProjectName(undefined, home)).toBeNull() }) -test('generated names are valid slugs shaped adjective-noun-NNN', () => { - // deterministic rand → first adjective, first noun, floor(0*900)=0 → "swift-otter-100" - const n = generateProjectName(() => 0) - expect(n).toBe('swift-otter-100') - expect(slugifyName(n)).toBe(n) // already a valid project name - expect(n).toMatch(/^[a-z]+-[a-z]+-\d{3}$/) +test('resolution NEVER blocks on a prompt (pure, no stdin/TTY dependency)', () => { + // Regression: `curl|sh && insta project create` and bare create in ~/projects must never hang. + expect(resolveProjectName(undefined, '/Users/gary/projects')).toBeNull() + expect(resolveProjectName('demo', '/Users/gary/projects')).toBe('demo') })