Skip to content
Closed
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
35 changes: 34 additions & 1 deletion packages/cli/src/commands/build.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { mkdtempSync, writeFileSync, mkdirSync, rmSync, existsSync } from 'node:
import { join } from 'node:path';
import { tmpdir } from 'node:os';
import { execSync } from 'node:child_process';
import { detectStack, cloneAndDetect } from './build.js';
import { detectStack, cloneAndDetect, detectLocalPath } from './build.js';
import type { ResolvedInput } from '../input.js';

describe('detectStack', () => {
Expand Down Expand Up @@ -103,6 +103,39 @@ describe('detectStack', () => {
expect(result).toBeUndefined();
});

it('detects a local --from path project', () => {
const dir = makeTempDir();
writeFileSync(join(dir, 'package.json'), JSON.stringify({
name: 'local-path-app',
packageManager: 'pnpm@9.12.0',
}));

const result = detectLocalPath({
kind: 'path',
raw: dir,
value: dir,
inferredName: 'fallback-name',
exists: true,
});

expect(result.path).toBe(dir);
expect(result.projectName).toBe('local-path-app');
expect(result.stack!.runtime).toBe('node');
expect(result.stack!.packageManager).toBe('pnpm');
});

it('throws when a local --from path is missing', () => {
const dir = join(makeTempDir(), 'missing');

expect(() => detectLocalPath({
kind: 'path',
raw: dir,
value: dir,
inferredName: 'missing',
exists: false,
})).toThrow('local path not found');
});
Comment on lines +127 to +137
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Missing test for "path is a file, not a directory" branch

detectLocalPath throws "local path must be a directory" when statSync shows the path is a regular file, but this error path has no corresponding test.


it('prefers package.json over other manifests when multiple exist', () => {
const dir = makeTempDir();
writeFileSync(join(dir, 'package.json'), JSON.stringify({ name: 'multi' }));
Expand Down
70 changes: 58 additions & 12 deletions packages/cli/src/commands/build.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { Command } from 'commander';
import { spawnSync } from 'node:child_process';
import { readFileSync, rmSync, existsSync } from 'node:fs';
import { readFileSync, rmSync, existsSync, statSync } from 'node:fs';
import { join } from 'node:path';
import { tmpdir } from 'node:os';
import { randomBytes } from 'node:crypto';
Expand Down Expand Up @@ -36,16 +36,6 @@ export interface DetectedStack {
* Inspect a directory root and return detected stack info based on manifest
* files. Returns undefined if nothing recognizable is found.
*/
/**
* Extract a human-readable project name from a Go module path.
* Strips major-version suffixes so `github.com/user/my-go-app/v2`
* returns `my-go-app` rather than `v2`.
*/
function goProjectName(module: string): string {
const stripped = module.replace(/\/v\d+$/, '');
return stripped.split('/').pop() ?? stripped;
}

export function detectStack(dir: string): DetectedStack | undefined {
// Node (package.json)
const pkgPath = join(dir, 'package.json');
Expand Down Expand Up @@ -103,7 +93,7 @@ export function detectStack(dir: string): DetectedStack | undefined {
return {
runtime: 'go',
packageManager: 'go',
projectName: modName ? goProjectName(modName) : undefined,
projectName: modName ? modName.split('/').pop() : undefined,
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Go versioned-module name regression

The deleted goProjectName helper explicitly stripped major-version path segments before calling .split('/').pop(). The new inline expression skips that step, so a Go module declared as module github.com/user/my-go-app/v2 now returns "v2" as the project name instead of "my-go-app". Any Go monorepo or library that follows the standard vN major-version suffix convention will produce a misleading project name in the build summary.

};
} catch { /* skip */ }
}
Expand All @@ -119,6 +109,12 @@ export interface CloneResult {
projectName: string;
}

export interface LocalPathResult {
path: string;
stack: DetectedStack | undefined;
projectName: string;
}

/**
* Shallow-clone a git repo into a temp directory and detect the stack.
* Throws on clone failure.
Expand All @@ -144,6 +140,41 @@ export function cloneAndDetect(input: ResolvedInput): CloneResult {
return { cloneDir, stack, projectName };
}

/**
* Inspect a local project directory and detect the same stack metadata used
* by git inputs. Throws when the path is missing or points at a file.
*/
export function detectLocalPath(input: ResolvedInput): LocalPathResult {
if (!input.exists || !existsSync(input.value)) {
throw new Error(`local path not found: ${input.value}`);
}
if (!statSync(input.value).isDirectory()) {
throw new Error(`local path must be a directory: ${input.value}`);
}

const stack = detectStack(input.value);
const projectName = stack?.projectName ?? input.inferredName ?? 'project';

return { path: input.value, stack, projectName };
}

function printBuildSummary(args: {
projectName: string;
stack: DetectedStack | undefined;
channel: string;
where: string;
sourceLabel: string;
sourceValue: string;
}): void {
console.log();
console.log(kleur.bold('Build summary'));
console.log(` project: ${args.projectName}`);
console.log(` stack: ${args.stack ? `${args.stack.runtime} (${args.stack.packageManager ?? 'unknown'})` : 'unknown'}`);
console.log(` channel: ${args.channel}`);
console.log(` target: ${args.where}`);
console.log(` ${args.sourceLabel}: ${args.sourceValue}`);
}

// --- Command -----------------------------------------------------------------

export const buildCmd = new Command('build')
Expand Down Expand Up @@ -178,6 +209,21 @@ export const buildCmd = new Command('build')
return;
}

if (input.kind === 'path') {
const { path, stack, projectName } = detectLocalPath(input);

console.log(kleur.green('Local path inspected'));
printBuildSummary({
projectName,
stack,
channel: opts.channel,
where,
sourceLabel: 'path',
sourceValue: path,
});
return;
}

// Other kinds remain stubs for now.
console.log(kleur.cyan(`[stub] build (${where}) · channel=${opts.channel} · from=${describeInput(input)}`));
// TODO: kind==='path' → load manifest; kind==='doc' → parse manifest;
Expand Down
Loading