From 9f272015254a0700bb51681d9525da0bb7a28675 Mon Sep 17 00:00:00 2001 From: yaowenc2 Date: Fri, 10 Apr 2026 22:37:08 -0700 Subject: [PATCH 1/8] =?UTF-8?q?feat(create):=20improve=20UX=20=E2=80=94=20?= =?UTF-8?q?auto-select=20single=20org,=20project=20subdirectory,=20next=20?= =?UTF-8?q?steps?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Skip org selection prompt when user has only one organization - Prompt for directory name and create project files in a subdirectory - Show next steps (cd + npm run dev) and suggested prompts for coding agents - Template projects suggest feature additions; blank projects suggest app ideas Co-Authored-By: Claude Opus 4.6 (1M context) --- src/commands/create.ts | 90 ++++++++++++++++++++++++++++++++++-------- 1 file changed, 74 insertions(+), 16 deletions(-) diff --git a/src/commands/create.ts b/src/commands/create.ts index f6ba783..a6a472e 100644 --- a/src/commands/create.ts +++ b/src/commands/create.ts @@ -167,7 +167,7 @@ export function registerCreateCommand(program: Command): void { clack.intro("Let's build something great"); } - // 1. Select organization + // 1. Select organization (auto-select if only one) let orgId = opts.orgId; if (!orgId) { const orgs = await listOrganizations(apiUrl); @@ -177,15 +177,20 @@ export function registerCreateCommand(program: Command): void { if (json) { throw new CLIError('Specify --org-id in JSON mode.'); } - const selected = await clack.select({ - message: 'Select an organization:', - options: orgs.map((o) => ({ - value: o.id, - label: o.name, - })), - }); - if (clack.isCancel(selected)) process.exit(0); - orgId = selected as string; + if (orgs.length === 1) { + orgId = orgs[0].id; + clack.log.info(`Using organization: ${orgs[0].name}`); + } else { + const selected = await clack.select({ + message: 'Select an organization:', + options: orgs.map((o) => ({ + value: o.id, + label: o.name, + })), + }); + if (clack.isCancel(selected)) process.exit(0); + orgId = selected as string; + } } // Save default org @@ -260,7 +265,28 @@ export function registerCreateCommand(program: Command): void { approach: template === 'empty' ? 'blank' : 'template', }); - // 4. Create project via Platform API + // 4. Choose directory name for the project + let dirName = projectName; + if (!json) { + const inputDir = await clack.text({ + message: 'Directory name:', + initialValue: projectName, + validate: (v) => { + if (v.length < 1) return 'Directory name is required'; + if (v === '.' || v === '..') return 'Invalid directory name'; + return undefined; + }, + }); + if (clack.isCancel(inputDir)) process.exit(0); + dirName = path.basename(inputDir as string).replace(/[^a-zA-Z0-9._-]/g, '-'); + } + + // Create the project directory and switch into it + const projectDir = path.resolve(process.cwd(), dirName); + await fs.mkdir(projectDir, { recursive: true }); + process.chdir(projectDir); + + // 5. Create project via Platform API const s = !json ? clack.spinner() : null; s?.start('Creating project...'); @@ -269,7 +295,7 @@ export function registerCreateCommand(program: Command): void { s?.message('Waiting for project to become active...'); await waitForProjectActive(project.id, apiUrl); - // 5. Fetch API key and link project + // 6. Fetch API key and link project const apiKey = await getProjectApiKey(project.id, apiUrl); const projectConfig: ProjectConfig = { project_id: project.id, @@ -284,7 +310,7 @@ export function registerCreateCommand(program: Command): void { s?.stop(`Project "${project.name}" created and linked`); - // 6. Download template or seed env for blank projects + // 7. Download template or seed env for blank projects const hasTemplate = template !== 'empty'; const githubTemplates = ['chatbot', 'crm', 'e-commerce', 'nextjs', 'react']; if (githubTemplates.includes(template!)) { @@ -327,7 +353,7 @@ export function registerCreateCommand(program: Command): void { trackCommand('create', orgId); await reportCliUsage('cli.create', true, 6); - // 7. Install npm dependencies (template projects only) + // 8. Install npm dependencies (template projects only) if (hasTemplate) { const installSpinner = !json ? clack.spinner() : null; installSpinner?.start('Installing dependencies...'); @@ -343,7 +369,7 @@ export function registerCreateCommand(program: Command): void { } } - // 8. Offer to deploy (template projects, interactive mode only) + // 9. Offer to deploy (template projects, interactive mode only) let liveUrl: string | null = null; if (hasTemplate && !json) { const shouldDeploy = await clack.confirm({ @@ -381,7 +407,7 @@ export function registerCreateCommand(program: Command): void { } } - // 9. Show links + // 10. Show links and next steps const dashboardUrl = `${getFrontendUrl()}/dashboard/project/${project.id}`; if (json) { @@ -389,6 +415,7 @@ export function registerCreateCommand(program: Command): void { success: true, project: { id: project.id, name: project.name, appkey: project.appkey, region: project.region }, template, + directory: dirName, urls: { dashboard: dashboardUrl, ...(liveUrl ? { liveSite: liveUrl } : {}), @@ -399,6 +426,37 @@ export function registerCreateCommand(program: Command): void { if (liveUrl) { clack.log.success(`Live site: ${liveUrl}`); } + + // Next steps + if (hasTemplate) { + const steps = [ + `cd ${dirName}`, + 'npm run dev', + ]; + clack.note(steps.join('\n'), 'Next steps'); + + const prompts = [ + 'Add an admin dashboard with analytics charts', + 'Add a Stripe checkout flow for premium plans', + 'Add real-time notifications with InsForge Realtime', + ]; + clack.note( + `Open your agent (Claude Code, Codex, Cursor, etc.) and try:\n\n${prompts.map((p) => `• "${p}"`).join('\n')}`, + 'Keep building', + ); + } else { + clack.note(`cd ${dirName}`, 'Next steps'); + + const prompts = [ + 'Build a todo app with Google OAuth sign-in', + 'Build an Instagram clone where users can upload photos, like, and comment', + 'Build an AI chatbot with conversation history and streaming responses', + ]; + clack.note( + `Open your agent (Claude Code, Codex, Cursor, etc.) and try:\n\n${prompts.map((p) => `• "${p}"`).join('\n')}`, + 'Start building', + ); + } clack.outro('Done!'); } } catch (err) { From 4a6c588536779e3e35a851fbbb4a9928d1165e75 Mon Sep 17 00:00:00 2001 From: yaowenc2 Date: Fri, 10 Apr 2026 22:46:48 -0700 Subject: [PATCH 2/8] =?UTF-8?q?fix(create):=20address=20PR=20review=20?= =?UTF-8?q?=E2=80=94=20org=20auto-select=20in=20JSON=20mode,=20directory?= =?UTF-8?q?=20validation?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Move JSON-mode org error after single-org check so auto-select works in both interactive and JSON modes - Validate directory name after normalization (rejects /, ./, etc.) - Reject existing directories instead of silently reusing them - Add unit tests for org selection logic and directory validation Co-Authored-By: Claude Opus 4.6 (1M context) --- src/commands/create.test.ts | 142 ++++++++++++++++++++++++++++++++++++ src/commands/create.ts | 22 ++++-- 2 files changed, 158 insertions(+), 6 deletions(-) create mode 100644 src/commands/create.test.ts diff --git a/src/commands/create.test.ts b/src/commands/create.test.ts new file mode 100644 index 0000000..f48e0ea --- /dev/null +++ b/src/commands/create.test.ts @@ -0,0 +1,142 @@ +import { describe, expect, it } from 'vitest'; +import * as path from 'node:path'; + +/** + * Unit tests for create command logic extracted into pure functions. + * These test the validation and normalization rules without needing + * to mock the full CLI command handler. + */ + +// Mirrors the sanitization logic in create.ts line ~281 +function sanitizeDirName(input: string): string { + return path.basename(input).replace(/[^a-zA-Z0-9._-]/g, '-'); +} + +// Mirrors the validation logic in create.ts lines ~274-278 +function validateDirInput(v: string): string | undefined { + if (v.length < 1) return 'Directory name is required'; + const normalized = path.basename(v).replace(/[^a-zA-Z0-9._-]/g, '-'); + if (!normalized || normalized === '.' || normalized === '..') return 'Invalid directory name'; + return undefined; +} + +// Mirrors the post-normalization check in create.ts lines ~283-285 +function isValidNormalizedDir(dirName: string): boolean { + return !!dirName && dirName !== '.' && dirName !== '..'; +} + +describe('create command: directory name validation', () => { + it('accepts a simple name', () => { + expect(validateDirInput('my-app')).toBeUndefined(); + expect(sanitizeDirName('my-app')).toBe('my-app'); + }); + + it('accepts names with dots and underscores', () => { + expect(validateDirInput('my_app.v2')).toBeUndefined(); + expect(sanitizeDirName('my_app.v2')).toBe('my_app.v2'); + }); + + it('rejects empty input', () => { + expect(validateDirInput('')).toBe('Directory name is required'); + }); + + it('rejects . and ..', () => { + expect(validateDirInput('.')).toBe('Invalid directory name'); + expect(validateDirInput('..')).toBe('Invalid directory name'); + }); + + it('rejects / which normalizes to empty string', () => { + // path.basename('/') returns '' which becomes '' after sanitization + expect(validateDirInput('/')).toBe('Invalid directory name'); + }); + + it('rejects ./ which normalizes to .', () => { + expect(validateDirInput('./')).toBe('Invalid directory name'); + }); + + it('sanitizes special characters to hyphens', () => { + expect(sanitizeDirName('my app!')).toBe('my-app-'); + expect(sanitizeDirName('hello@world')).toBe('hello-world'); + }); + + it('extracts basename from path input', () => { + expect(sanitizeDirName('/some/path/my-app')).toBe('my-app'); + expect(sanitizeDirName('../../my-app')).toBe('my-app'); + }); + + it('post-normalization check rejects empty and dot names', () => { + expect(isValidNormalizedDir('')).toBe(false); + expect(isValidNormalizedDir('.')).toBe(false); + expect(isValidNormalizedDir('..')).toBe(false); + }); + + it('post-normalization check accepts valid names', () => { + expect(isValidNormalizedDir('my-app')).toBe(true); + expect(isValidNormalizedDir('test.project')).toBe(true); + }); +}); + +describe('create command: org auto-select logic', () => { + // Mirrors the org selection logic in create.ts + function selectOrg( + orgs: Array<{ id: string; name: string }>, + json: boolean, + providedOrgId?: string, + ): { orgId: string | null; error: string | null; autoSelected: boolean } { + if (providedOrgId) { + return { orgId: providedOrgId, error: null, autoSelected: false }; + } + if (orgs.length === 0) { + return { orgId: null, error: 'No organizations found.', autoSelected: false }; + } + if (orgs.length === 1) { + return { orgId: orgs[0].id, error: null, autoSelected: true }; + } + // Multiple orgs + if (json) { + return { orgId: null, error: 'Multiple organizations found. Specify --org-id.', autoSelected: false }; + } + // Would prompt interactively + return { orgId: null, error: null, autoSelected: false }; + } + + it('uses provided org-id directly', () => { + const result = selectOrg([{ id: 'org1', name: 'Org 1' }], false, 'org-provided'); + expect(result.orgId).toBe('org-provided'); + expect(result.autoSelected).toBe(false); + }); + + it('errors when no orgs exist', () => { + const result = selectOrg([], false); + expect(result.error).toBe('No organizations found.'); + }); + + it('auto-selects single org in interactive mode', () => { + const result = selectOrg([{ id: 'org1', name: 'My Org' }], false); + expect(result.orgId).toBe('org1'); + expect(result.autoSelected).toBe(true); + }); + + it('auto-selects single org in JSON mode', () => { + const result = selectOrg([{ id: 'org1', name: 'My Org' }], true); + expect(result.orgId).toBe('org1'); + expect(result.autoSelected).toBe(true); + }); + + it('errors in JSON mode with multiple orgs', () => { + const result = selectOrg( + [{ id: 'org1', name: 'Org 1' }, { id: 'org2', name: 'Org 2' }], + true, + ); + expect(result.error).toBe('Multiple organizations found. Specify --org-id.'); + }); + + it('returns null orgId for multiple orgs in interactive mode (would prompt)', () => { + const result = selectOrg( + [{ id: 'org1', name: 'Org 1' }, { id: 'org2', name: 'Org 2' }], + false, + ); + expect(result.orgId).toBeNull(); + expect(result.error).toBeNull(); + }); +}); diff --git a/src/commands/create.ts b/src/commands/create.ts index a6a472e..0f6b623 100644 --- a/src/commands/create.ts +++ b/src/commands/create.ts @@ -174,13 +174,13 @@ export function registerCreateCommand(program: Command): void { if (orgs.length === 0) { throw new CLIError('No organizations found.'); } - if (json) { - throw new CLIError('Specify --org-id in JSON mode.'); - } if (orgs.length === 1) { orgId = orgs[0].id; - clack.log.info(`Using organization: ${orgs[0].name}`); + if (!json) clack.log.info(`Using organization: ${orgs[0].name}`); } else { + if (json) { + throw new CLIError('Multiple organizations found. Specify --org-id.'); + } const selected = await clack.select({ message: 'Select an organization:', options: orgs.map((o) => ({ @@ -273,7 +273,8 @@ export function registerCreateCommand(program: Command): void { initialValue: projectName, validate: (v) => { if (v.length < 1) return 'Directory name is required'; - if (v === '.' || v === '..') return 'Invalid directory name'; + const normalized = path.basename(v).replace(/[^a-zA-Z0-9._-]/g, '-'); + if (!normalized || normalized === '.' || normalized === '..') return 'Invalid directory name'; return undefined; }, }); @@ -281,9 +282,18 @@ export function registerCreateCommand(program: Command): void { dirName = path.basename(inputDir as string).replace(/[^a-zA-Z0-9._-]/g, '-'); } + // Validate normalized dirName + if (!dirName || dirName === '.' || dirName === '..') { + throw new CLIError('Invalid directory name.'); + } + // Create the project directory and switch into it const projectDir = path.resolve(process.cwd(), dirName); - await fs.mkdir(projectDir, { recursive: true }); + const dirExists = await fs.stat(projectDir).catch(() => null); + if (dirExists) { + throw new CLIError(`Directory "${dirName}" already exists.`); + } + await fs.mkdir(projectDir); process.chdir(projectDir); // 5. Create project via Platform API From d020bf30396742e80371a4f97445ed3088467a74 Mon Sep 17 00:00:00 2001 From: yaowenc2 Date: Fri, 10 Apr 2026 22:50:34 -0700 Subject: [PATCH 3/8] =?UTF-8?q?fix(create):=20refine=20post-create=20messa?= =?UTF-8?q?ges=20=E2=80=94=20prompts=20for=20blank,=20nudge=20for=20templa?= =?UTF-8?q?tes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Template projects: just point users to their coding agent - Blank projects: suggest app ideas (todo, Instagram clone, AI chatbot) Co-Authored-By: Claude Opus 4.6 (1M context) --- src/commands/create.ts | 15 +++------------ 1 file changed, 3 insertions(+), 12 deletions(-) diff --git a/src/commands/create.ts b/src/commands/create.ts index 0f6b623..d768022 100644 --- a/src/commands/create.ts +++ b/src/commands/create.ts @@ -444,26 +444,17 @@ export function registerCreateCommand(program: Command): void { 'npm run dev', ]; clack.note(steps.join('\n'), 'Next steps'); - - const prompts = [ - 'Add an admin dashboard with analytics charts', - 'Add a Stripe checkout flow for premium plans', - 'Add real-time notifications with InsForge Realtime', - ]; - clack.note( - `Open your agent (Claude Code, Codex, Cursor, etc.) and try:\n\n${prompts.map((p) => `• "${p}"`).join('\n')}`, - 'Keep building', - ); + clack.note('Open your coding agent (Claude Code, Codex, Cursor, etc.) to add new features.', 'Keep building'); } else { clack.note(`cd ${dirName}`, 'Next steps'); const prompts = [ 'Build a todo app with Google OAuth sign-in', 'Build an Instagram clone where users can upload photos, like, and comment', - 'Build an AI chatbot with conversation history and streaming responses', + 'Build an AI chatbot with conversation history', ]; clack.note( - `Open your agent (Claude Code, Codex, Cursor, etc.) and try:\n\n${prompts.map((p) => `• "${p}"`).join('\n')}`, + `Open your coding agent (Claude Code, Codex, Cursor, etc.) and try:\n\n${prompts.map((p) => `• "${p}"`).join('\n')}`, 'Start building', ); } From 8096892b445ccd78b6851e797b7859794a157cba Mon Sep 17 00:00:00 2001 From: yaowenc2 Date: Sat, 11 Apr 2026 09:09:33 -0700 Subject: [PATCH 4/8] fix(create): clean up project directory on setup failure If project creation or linking fails after the directory was created, roll back by removing the directory so retries don't hit "already exists". Co-Authored-By: Claude Opus 4.6 (1M context) --- src/commands/create.ts | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/src/commands/create.ts b/src/commands/create.ts index d768022..7edfc32 100644 --- a/src/commands/create.ts +++ b/src/commands/create.ts @@ -288,7 +288,8 @@ export function registerCreateCommand(program: Command): void { } // Create the project directory and switch into it - const projectDir = path.resolve(process.cwd(), dirName); + const originalCwd = process.cwd(); + const projectDir = path.resolve(originalCwd, dirName); const dirExists = await fs.stat(projectDir).catch(() => null); if (dirExists) { throw new CLIError(`Directory "${dirName}" already exists.`); @@ -297,8 +298,10 @@ export function registerCreateCommand(program: Command): void { process.chdir(projectDir); // 5. Create project via Platform API + let projectLinked = false; const s = !json ? clack.spinner() : null; - s?.start('Creating project...'); + try { + s?.start('Creating project...'); const project = await createProject(orgId, projectName, opts.region, apiUrl); @@ -317,6 +320,7 @@ export function registerCreateCommand(program: Command): void { oss_host: buildOssHost(project.appkey, project.region), }; saveProjectConfig(projectConfig); + projectLinked = true; s?.stop(`Project "${project.name}" created and linked`); @@ -460,6 +464,14 @@ export function registerCreateCommand(program: Command): void { } clack.outro('Done!'); } + } catch (err) { + // Clean up the project directory if it was created but linking failed + if (!projectLinked) { + process.chdir(originalCwd); + await fs.rm(projectDir, { recursive: true, force: true }).catch(() => {}); + } + throw err; + } } catch (err) { handleError(err, json); } finally { From 831983fd96ab76e831ced258c0a01cdbf4b6f3b1 Mon Sep 17 00:00:00 2001 From: yaowenc2 Date: Sat, 11 Apr 2026 09:45:51 -0700 Subject: [PATCH 5/8] chore: bump version to 0.1.44 Co-Authored-By: Claude Opus 4.6 (1M context) --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 0aad093..2807a32 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "@insforge/cli", - "version": "0.1.43", + "version": "0.1.44", "description": "InsForge CLI - Command line tool for InsForge platform", "type": "module", "bin": { From 1f9c5719290ff62e0a8ada7ca3ce217f22720337 Mon Sep 17 00:00:00 2001 From: yaowenc2 Date: Sat, 11 Apr 2026 09:56:50 -0700 Subject: [PATCH 6/8] fix(link): use --template flag instead of auto-detecting empty dir - `insforge link` now just links the project in current directory - `insforge link --template ` downloads template into a subdirectory - Auto-select org when only one exists (same as create) - Remove isDirEmpty auto-detection and interactive template selection - Show next steps and coding agent nudge Also in create: only prompt for directory when template is selected, blank projects use current directory. Co-Authored-By: Claude Opus 4.6 (1M context) --- src/commands/create.ts | 69 +++++----- src/commands/projects/link.ts | 236 ++++++++++++---------------------- 2 files changed, 120 insertions(+), 185 deletions(-) diff --git a/src/commands/create.ts b/src/commands/create.ts index 7edfc32..9326d86 100644 --- a/src/commands/create.ts +++ b/src/commands/create.ts @@ -265,37 +265,43 @@ export function registerCreateCommand(program: Command): void { approach: template === 'empty' ? 'blank' : 'template', }); - // 4. Choose directory name for the project - let dirName = projectName; - if (!json) { - const inputDir = await clack.text({ - message: 'Directory name:', - initialValue: projectName, - validate: (v) => { - if (v.length < 1) return 'Directory name is required'; - const normalized = path.basename(v).replace(/[^a-zA-Z0-9._-]/g, '-'); - if (!normalized || normalized === '.' || normalized === '..') return 'Invalid directory name'; - return undefined; - }, - }); - if (clack.isCancel(inputDir)) process.exit(0); - dirName = path.basename(inputDir as string).replace(/[^a-zA-Z0-9._-]/g, '-'); - } + // 4. Choose directory (templates need a subdirectory, blank uses cwd) + const hasTemplate = template !== 'empty'; + let dirName: string | null = null; + const originalCwd = process.cwd(); + let projectDir = originalCwd; - // Validate normalized dirName - if (!dirName || dirName === '.' || dirName === '..') { - throw new CLIError('Invalid directory name.'); - } + if (hasTemplate) { + dirName = projectName; + if (!json) { + const inputDir = await clack.text({ + message: 'Directory name:', + initialValue: projectName, + validate: (v) => { + if (v.length < 1) return 'Directory name is required'; + const normalized = path.basename(v).replace(/[^a-zA-Z0-9._-]/g, '-'); + if (!normalized || normalized === '.' || normalized === '..') return 'Invalid directory name'; + return undefined; + }, + }); + if (clack.isCancel(inputDir)) process.exit(0); + dirName = path.basename(inputDir as string).replace(/[^a-zA-Z0-9._-]/g, '-'); + } - // Create the project directory and switch into it - const originalCwd = process.cwd(); - const projectDir = path.resolve(originalCwd, dirName); - const dirExists = await fs.stat(projectDir).catch(() => null); - if (dirExists) { - throw new CLIError(`Directory "${dirName}" already exists.`); + // Validate normalized dirName + if (!dirName || dirName === '.' || dirName === '..') { + throw new CLIError('Invalid directory name.'); + } + + // Create the project directory and switch into it + projectDir = path.resolve(originalCwd, dirName); + const dirExists = await fs.stat(projectDir).catch(() => null); + if (dirExists) { + throw new CLIError(`Directory "${dirName}" already exists.`); + } + await fs.mkdir(projectDir); + process.chdir(projectDir); } - await fs.mkdir(projectDir); - process.chdir(projectDir); // 5. Create project via Platform API let projectLinked = false; @@ -325,7 +331,6 @@ export function registerCreateCommand(program: Command): void { s?.stop(`Project "${project.name}" created and linked`); // 7. Download template or seed env for blank projects - const hasTemplate = template !== 'empty'; const githubTemplates = ['chatbot', 'crm', 'e-commerce', 'nextjs', 'react']; if (githubTemplates.includes(template!)) { await downloadGitHubTemplate(template!, projectConfig, json); @@ -429,7 +434,7 @@ export function registerCreateCommand(program: Command): void { success: true, project: { id: project.id, name: project.name, appkey: project.appkey, region: project.region }, template, - directory: dirName, + ...(dirName ? { directory: dirName } : {}), urls: { dashboard: dashboardUrl, ...(liveUrl ? { liveSite: liveUrl } : {}), @@ -450,8 +455,6 @@ export function registerCreateCommand(program: Command): void { clack.note(steps.join('\n'), 'Next steps'); clack.note('Open your coding agent (Claude Code, Codex, Cursor, etc.) to add new features.', 'Keep building'); } else { - clack.note(`cd ${dirName}`, 'Next steps'); - const prompts = [ 'Build a todo app with Google OAuth sign-in', 'Build an Instagram clone where users can upload photos, like, and comment', @@ -466,7 +469,7 @@ export function registerCreateCommand(program: Command): void { } } catch (err) { // Clean up the project directory if it was created but linking failed - if (!projectLinked) { + if (!projectLinked && hasTemplate && projectDir !== originalCwd) { process.chdir(originalCwd); await fs.rm(projectDir, { recursive: true, force: true }).catch(() => {}); } diff --git a/src/commands/projects/link.ts b/src/commands/projects/link.ts index 5fa2726..2e52aee 100644 --- a/src/commands/projects/link.ts +++ b/src/commands/projects/link.ts @@ -11,41 +11,17 @@ import { getProjectApiKey, reportAgentConnected, } from '../../lib/api/platform.js'; -import { getAnonKey } from '../../lib/api/oss.js'; import { getGlobalConfig, saveGlobalConfig, saveProjectConfig, getFrontendUrl } from '../../lib/config.js'; import { requireAuth } from '../../lib/credentials.js'; import { handleError, getRootOpts, CLIError } from '../../lib/errors.js'; import { outputJson, outputSuccess } from '../../lib/output.js'; -import { readEnvFile } from '../../lib/env.js'; import { installSkills, reportCliUsage } from '../../lib/skills.js'; import { captureEvent, trackCommand, shutdownAnalytics } from '../../lib/analytics.js'; -import { deployProject } from '../deployments/deploy.js'; import { downloadGitHubTemplate, downloadTemplate, type Framework } from '../create.js'; import type { ProjectConfig } from '../../types.js'; const execAsync = promisify(exec); -/** Files that indicate real project content exists */ -const PROJECT_MARKERS = new Set([ - 'package.json', - 'index.html', - 'tsconfig.json', - 'next.config.js', - 'next.config.ts', - 'next.config.mjs', - 'vite.config.ts', - 'vite.config.js', - 'src', - 'app', - 'pages', - 'public', -]); - -async function isDirEmpty(dir: string): Promise { - const entries = await fs.readdir(dir); - return !entries.some((e) => PROJECT_MARKERS.has(e)); -} - function buildOssHost(appkey: string, region: string): string { return `https://${appkey}.${region}.insforge.app`; } @@ -56,6 +32,7 @@ export function registerProjectLinkCommand(program: Command): void { .description('Link current directory to an InsForge project') .option('--project-id ', 'Project ID to link') .option('--org-id ', 'Organization ID') + .option('--template