Skip to content
Merged
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
9 changes: 2 additions & 7 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -88,10 +88,5 @@ jobs:
- name: Install dependencies
run: pnpm install --frozen-lockfile

- name: Pack CLI
run: pnpm --filter devmap pack --pack-destination artifacts

- name: Smoke test packed CLI
run: |
npm exec --yes --package ./artifacts/devmap-0.1.0.tgz -- devmap --version
npm exec --yes --package ./artifacts/devmap-0.1.0.tgz -- devmap --help
- name: Test packed CLI end to end
run: pnpm test:package-e2e
38 changes: 37 additions & 1 deletion docs/for-me-personal/DEBUG.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Catatan Debugging DevMap

Terakhir diperbarui: 2026-06-11
Terakhir diperbarui: 2026-06-13

Dokumen ini menyimpan masalah teknis yang pernah ditemukan selama development
DevMap. Tujuannya supaya penyebab, solusi, dan cara verifikasinya tidak perlu
Expand Down Expand Up @@ -672,3 +672,39 @@ Gunakan format:
### Verifikasi
### Pelajaran
```
## 12. Packed E2E Mengasumsikan Layout Internal npm

**Tanggal:** 2026-06-13
**Status:** Selesai

### Gejala

Package smoke test gagal pada GitHub Actions Ubuntu dengan `MODULE_NOT_FOUND`
untuk path `node_modules/npm/bin/npm-cli.js`.

### Akar Masalah

Harness menyusun path internal npm relatif terhadap `process.execPath`.
Layout tersebut tersedia pada instalasi Node lokal tertentu, tetapi bukan
kontrak lintas platform dan tidak dipakai oleh image Node GitHub Actions.

### Solusi

Jalankan executable npm resmi yang berada di samping binary Node:

- `npm.cmd` pada Windows;
- `npm` pada Linux dan macOS.

Windows menjalankan `npm.cmd` melalui shell, sedangkan Node, pnpm, dan binary
Linux/macOS tetap dijalankan langsung. pnpm tetap memakai `npm_execpath` yang
disediakan oleh pnpm.

### Verifikasi

- `pnpm test:package-e2e`
- package smoke test GitHub Actions

### Pelajaran

Jangan bergantung pada struktur internal instalasi package manager. Gunakan
executable publiknya ketika menguji perilaku CLI lintas platform.
15 changes: 15 additions & 0 deletions docs/for-me-personal/PROGRESS.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,21 @@

Terakhir diperbarui: 2026-06-13

## Update 2026-06-13

### Packed CLI End-to-End

- Manual tarball test pada project eksternal dengan Groq API key sudah berhasil
dijalankan oleh maintainer.
- Script `pnpm test:package-e2e` membuat tarball CLI dari source terbaru.
- Tarball dipasang sebagai dependency pada project Next.js dan Express
sementara, bukan dijalankan langsung dari workspace.
- E2E memverifikasi `--version`, `--help`, `analyze --fresh`, `ask`, `doctor`,
dan isi snapshot.
- Home directory diisolasi agar config Groq pribadi tidak terbaca dan tes tidak
memakai quota AI.
- Package smoke test di CI sekarang menjalankan alur distribusi end-to-end ini.

## Update 2026-06-12

### Analyzer Resilience
Expand Down
17 changes: 17 additions & 0 deletions docs/for-me-personal/TEST.md
Original file line number Diff line number Diff line change
Expand Up @@ -251,6 +251,23 @@ Gunakan build mode untuk menemukan:

Ini adalah tes distribusi paling realistis sebelum package dipublish ke npm.

Automated E2E untuk membuat tarball, memasangnya pada project Next.js dan
Express sementara, lalu menjalankan binary hasil install:

```powershell
pnpm test:package-e2e
```

Tes ini memakai home directory sementara agar config Groq pribadi tidak
terbaca dan tidak melakukan request AI live.

Status manual terakhir:

- install tarball pada project eksternal berhasil;
- validasi API key Groq berhasil;
- command AI live berhasil dijalankan;
- nilai API key tidak dicatat dalam repository.

### A. Buat Tarball Dari Repository DevMap

Dari root repository DevMap:
Expand Down
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,8 @@
"preview:web": "pnpm --filter @devmap/web preview",
"dev:cli": "pnpm --filter devmap dev",
"build:cli": "pnpm --filter devmap build",
"test:cli": "pnpm --filter devmap test"
"test:cli": "pnpm --filter devmap test",
"test:package-e2e": "node packages/cli/test/package-e2e.mjs"
},
"keywords": [
"codebase",
Expand Down
155 changes: 155 additions & 0 deletions packages/cli/test/package-e2e.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,155 @@
import assert from "node:assert/strict";
import { exec, execFile } from "node:child_process";
import {
cp,
mkdtemp,
mkdir,
readFile,
readdir,
rm,
writeFile
} from "node:fs/promises";
import { tmpdir } from "node:os";
import { dirname, join, resolve } from "node:path";
import { promisify } from "node:util";
import { fileURLToPath } from "node:url";

const execute = promisify(execFile);
const executeShell = promisify(exec);
const testDirectory = dirname(fileURLToPath(import.meta.url));
const packageRoot = resolve(testDirectory, "..");
const workspaceRoot = resolve(packageRoot, "../..");
const pnpmCli = process.env.npm_execpath;
const npmExecutable = join(
dirname(process.execPath),
process.platform === "win32" ? "npm.cmd" : "npm"
);

if (!pnpmCli) {
throw new Error("Run this test through pnpm so npm_execpath is available.");
}

const temporaryRoot = await mkdtemp(join(tmpdir(), "devmap-package-e2e-"));
const artifactsDirectory = join(temporaryRoot, "artifacts");
const isolatedHome = join(temporaryRoot, "home");

try {
await mkdir(artifactsDirectory, { recursive: true });
await mkdir(isolatedHome, { recursive: true });

await runNodeCli(pnpmCli, [
"--filter",
"devmap",
"pack",
"--pack-destination",
artifactsDirectory
], workspaceRoot);

const tarballName = (await readdir(artifactsDirectory))
.find((file) => file.endsWith(".tgz"));
assert.ok(tarballName, "Expected pnpm pack to create a tarball.");

const tarballPath = join(artifactsDirectory, tarballName);
await verifyProject("nextjs-project", "nextjs");
await verifyProject("express-project", "express");

console.log("Packed CLI E2E passed for Next.js and Express fixtures.");

async function verifyProject(fixtureName, expectedFramework) {
const projectRoot = join(temporaryRoot, fixtureName);
await cp(join(testDirectory, "fixtures", fixtureName), projectRoot, {
recursive: true
});

const packageJsonPath = join(projectRoot, "package.json");
const fixturePackageJson = await readFile(packageJsonPath, "utf8");
await writeFile(packageJsonPath, JSON.stringify({
name: `devmap-e2e-${fixtureName}`,
private: true
}, null, 2), "utf8");

await runNpm([
"install",
"--no-package-lock",
"--ignore-scripts",
"--save-dev",
tarballPath
], projectRoot);

await writeFile(packageJsonPath, fixturePackageJson, "utf8");

const version = await runDevmap(projectRoot, ["--version"]);
assert.match(version.stdout, /^0\.1\.0/m);

const help = await runDevmap(projectRoot, ["--help"]);
assert.match(stripAnsi(help.stdout), /analyze\s+Analyze project structure/);

const analyze = await runDevmap(projectRoot, ["analyze", "--fresh"]);
assert.match(
stripAnsi(analyze.stdout),
new RegExp(`Framework\\s+${expectedFramework}`, "i")
);

const snapshotPath = join(projectRoot, ".devmap", "snapshot.json");
const snapshot = JSON.parse(await readFile(snapshotPath, "utf8"));
assert.equal(snapshot.project.framework, expectedFramework);

const ask = await runDevmap(projectRoot, [
"ask",
"Where is the main application entry point?"
]);
assert.match(stripAnsi(ask.stdout), /Relevant Files/);

const doctor = await runDevmap(projectRoot, ["doctor"]);
const doctorOutput = stripAnsi(doctor.stdout);
assert.match(doctorOutput, /DevMap Doctor/);
assert.match(doctorOutput, /Config\s+missing/i);
}
} finally {
await rm(temporaryRoot, { recursive: true, force: true });
}

async function runDevmap(cwd, args) {
return runNpm(["exec", "--", "devmap", ...args], cwd);
}

async function runNodeCli(cliPath, args, cwd) {
return runExecutable(process.execPath, [cliPath, ...args], cwd);
}

async function runNpm(args, cwd) {
if (process.platform !== "win32") {
return runExecutable(npmExecutable, args, cwd);
}

const command = [npmExecutable, ...args]
.map(quoteWindowsArgument)
.join(" ");
return executeShell(command, commandOptions(cwd));
}

async function runExecutable(executable, args, cwd) {
return execute(executable, args, commandOptions(cwd));
}

function commandOptions(cwd) {
return {
cwd,
env: {
...process.env,
HOME: isolatedHome,
USERPROFILE: isolatedHome,
NO_COLOR: "1"
},
maxBuffer: 10 * 1024 * 1024,
windowsHide: true
};
}

function quoteWindowsArgument(value) {
return `"${value.replaceAll('"', '""')}"`;
}

function stripAnsi(value) {
return value.replace(/\u001B\[[0-?]*[ -/]*[@-~]/g, "");
}
Loading