Skip to content

Commit 7507d32

Browse files
tonychang04claude
andauthored
fix: insta project create name is optional — recommended one-liner is paste-run (#34)
The console's empty-state onboarding recommended 'curl … | sh && insta project create <app-name>', but the <app-name> placeholder is unrunnable: zsh reads <…> as redirection → 'parse error near \n'. The RECOMMENDED command couldn't be pasted. Name is now optional: explicit arg wins; else prompt with the cwd dir-name default (TTY); else use the dir name (non-TTY/CI). So 'insta project create' runs clean. TDD: slugify + resolver, 4 cases. Claude-Session: https://claude.ai/code/session_01GMbAe5K1RfwaAcge5inX7P Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
1 parent 4e2b6cd commit 7507d32

3 files changed

Lines changed: 52 additions & 4 deletions

File tree

src/commands/project.ts

Lines changed: 29 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,16 @@
1+
import { createInterface } from 'node:readline/promises'
12
import { ApiClient, requireProject } from '../api.js'
23
import { writeProject } from '../config.js'
34
import { info, die, printJson, handleApproval, renderNextActions } from '../util.js'
45
import { installObserve } from '../observe/install.js'
56
import { installSkills } from '../ensure-skills.js'
67

8+
// Interactive name prompt (stderr, so piped stdout stays clean). Enter accepts the default.
9+
async function promptName(question: string, def: string): Promise<string> {
10+
const rl = createInterface({ input: process.stdin, output: process.stderr })
11+
try { return (await rl.question(`${question} [${def}]: `)).trim() } finally { rl.close() }
12+
}
13+
714
// Best-effort: wire the credential-audit hook into the project (no-op if assets aren't built).
815
function tryInstallObserve(): void {
916
try {
@@ -19,12 +26,31 @@ async function resolveOrg(api: ApiClient, given?: string): Promise<string> {
1926
return orgs[0].id
2027
}
2128

22-
export async function projectCreate(name: string, opts: { org?: string }): Promise<void> {
29+
/** A valid project name from a raw string: lowercase, non-alnum → hyphen, trimmed. */
30+
export function slugifyName(raw: string): string {
31+
return raw.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, '').slice(0, 40)
32+
}
33+
34+
/** Name resolution so the recommended one-liner (`insta project create`, no arg) is paste-and-run:
35+
* explicit arg wins; else prompt with the cwd basename as default (TTY); else use the basename. */
36+
export async function resolveProjectName(
37+
nameArg: string | undefined,
38+
cwd = process.cwd(),
39+
prompt?: (question: string, def: string) => Promise<string>,
40+
): Promise<string> {
41+
const fromDir = slugifyName(cwd.split('/').filter(Boolean).pop() ?? 'app') || 'app'
42+
if (nameArg) return slugifyName(nameArg)
43+
if (prompt && process.stdin.isTTY) return slugifyName((await prompt('project name', fromDir)) || fromDir) || fromDir
44+
return fromDir
45+
}
46+
47+
export async function projectCreate(name: string | undefined, opts: { org?: string }): Promise<void> {
2348
const api = await ApiClient.load()
2449
const orgId = await resolveOrg(api, opts.org)
25-
const out = await api.request('POST', `/orgs/${orgId}/projects`, { name })
50+
const resolved = await resolveProjectName(name, process.cwd(), promptName)
51+
const out = await api.request('POST', `/orgs/${orgId}/projects`, { name: resolved })
2652
await writeProject({ projectId: out.project.id, orgId, branch: out.defaultBranch.name })
27-
info(`created project ${out.project.id} (${name})`)
53+
info(`created project ${out.project.id} (${resolved})`)
2854
info(` resources: ${out.resources.map((r: any) => r.kind).join(', ')}`)
2955
info(` linked ./.insta/project.json (branch ${out.defaultBranch.name})`)
3056
renderNextActions(out.nextActions)

src/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,7 @@ orgCmd.command('create <name>').action(guard((name) => org.orgCreate(name)))
7777

7878
// ---- project ----
7979
const pj = program.command('project').description('Manage projects')
80-
pj.command('create <name>').option('--org <id>', 'org to create under (default: personal)').action(guard((name, o) => project.projectCreate(name, o)))
80+
pj.command('create [name]').option('--org <id>', 'org to create under (default: personal)').action(guard((name, o) => project.projectCreate(name, o)))
8181
pj.command('list').option('--org <id>').option('--json').action(guard((o) => project.projectList(o)))
8282
pj.command('link <id>').description('Link a project to this directory').action(guard((id) => project.projectLink(id)))
8383
pj.command('delete').option('--project <id>').action(guard((o) => project.projectDelete(o)))

test/create-name.test.ts

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
import { test, expect, afterEach } from 'vitest'
2+
import { slugifyName, resolveProjectName } from '../src/commands/project.js'
3+
4+
const realTTY = process.stdin.isTTY
5+
afterEach(() => { Object.defineProperty(process.stdin, 'isTTY', { value: realTTY, configurable: true }) })
6+
7+
test('slugify makes a valid project name', () => {
8+
expect(slugifyName('My Cool App!')).toBe('my-cool-app')
9+
expect(slugifyName('linkbox')).toBe('linkbox')
10+
})
11+
test('explicit arg wins (slugified)', async () => {
12+
expect(await resolveProjectName('LinkBox', '/x/whatever')).toBe('linkbox')
13+
})
14+
test('no arg, non-TTY → directory basename (so the pasted one-liner runs unedited)', async () => {
15+
Object.defineProperty(process.stdin, 'isTTY', { value: false, configurable: true })
16+
expect(await resolveProjectName(undefined, '/Users/me/my-project')).toBe('my-project')
17+
})
18+
test('no arg, TTY → prompt, Enter accepts the dir-name default', async () => {
19+
Object.defineProperty(process.stdin, 'isTTY', { value: true, configurable: true })
20+
expect(await resolveProjectName(undefined, '/Users/me/cool-thing', async () => '')).toBe('cool-thing')
21+
expect(await resolveProjectName(undefined, '/Users/me/cool-thing', async () => 'chosen name')).toBe('chosen-name')
22+
})

0 commit comments

Comments
 (0)