Skip to content

Commit 5293743

Browse files
Stuff
1 parent 6dfebc0 commit 5293743

17 files changed

Lines changed: 440 additions & 327 deletions

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,3 +51,5 @@ src/Nap.Zed/grammars/nap.wasm
5151
src/Nap.Zed/grammars/napenv.wasm
5252

5353
*.wasm
54+
55+
scripts/logs/

specs/CLI-PLAN.md

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -77,9 +77,10 @@ nap/
7777

7878
### Phase 4 — Polish & Distribution
7979

80-
- Standalone native binary (NativeAOT or single-file publish)
81-
- NuGet package for `dotnet tool install`
80+
- **NuGet package for `dotnet tool install` (PRIMARY channel)** — set `<PackAsTool>true</PackAsTool>` and `<ToolCommandName>napper</ToolCommandName>` in `Nap.Cli.fsproj`, publish to nuget.org. This is the primary distribution method — no code signing needed, no SmartScreen warnings on Windows, immediate availability. The VSIX extension auto-installs via `dotnet tool install -g napper --version X.X.X`.
81+
- Standalone native binary (NativeAOT or single-file publish) — secondary channel for users without .NET SDK
8282
- Homebrew formula
83+
- Winget / Chocolatey / Scoop packages (future)
8384
- `nap new` scaffolding commands
8485
- Language-extensible script runner plugin model
8586

@@ -119,8 +120,10 @@ nap/
119120
- [ ] `ctx.Set` for cross-step variable passing
120121

121122
### Phase 4 — Polish & Distribution
122-
- [ ] Standalone native binary (NativeAOT or single-file publish)
123-
- [ ] NuGet package for `dotnet tool install`
123+
- [ ] `dotnet tool install` — set `PackAsTool` in fsproj, publish to nuget.org (PRIMARY)
124+
- [ ] VSIX auto-installs CLI via `dotnet tool install -g napper --version X.X.X`
125+
- [ ] Standalone native binary (NativeAOT or single-file publish) — secondary
124126
- [ ] Homebrew formula
127+
- [ ] Winget / Chocolatey / Scoop packages
125128
- [ ] `nap new` scaffolding commands
126129
- [ ] Language-extensible script runner plugin model

specs/CLI-SPEC.md

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,30 @@ Nap is a developer-first HTTP testing tool. It is as simple as curl for one-off
2020

2121
---
2222

23+
## Installation
24+
25+
The Napper CLI is distributed as a **dotnet tool** via NuGet. This is the primary distribution channel — it avoids code-signing requirements (no Windows SmartScreen warnings), works cross-platform, and integrates with existing .NET toolchains.
26+
27+
```sh
28+
# Install globally
29+
dotnet tool install -g napper
30+
31+
# Install a specific version
32+
dotnet tool install -g napper --version 0.6.0
33+
34+
# Update to latest
35+
dotnet tool update -g napper
36+
```
37+
38+
The VSIX extension installs the CLI automatically via `dotnet tool install` on activation, using the extension's own version to determine which CLI version to install. Users with the CLI already on PATH (or configured via `nap.cliPath`) skip the auto-install.
39+
40+
**Future channels** (not yet implemented):
41+
- Homebrew formula (`brew install napper`)
42+
- Winget / Chocolatey / Scoop packages
43+
- Standalone native binary (NativeAOT single-file publish)
44+
45+
---
46+
2347
## Usage
2448

2549
### `cli-run` — Run Command

specs/IDE-EXTENSION-PLAN.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ This phase **deletes duplicated TypeScript parsing code** and replaces it with L
3838

3939
### Phase 4 — Polish & Distribution
4040

41+
- **CLI installation via `dotnet tool install`** — replace raw binary download with `dotnet tool install -g napper --version X.X.X`. Version is read from the extension's own `package.json`. Eliminates Windows SmartScreen warnings and custom HTTP download code.
4142
- Split editor layout (request panel webview)
4243
- New request guided flow
4344
- OpenAPI generation command
@@ -72,6 +73,8 @@ This phase **deletes duplicated TypeScript parsing code** and replaces it with L
7273
- [ ] Run ALL existing VSIX e2e tests — must pass
7374

7475
### Phase 4 — Polish & Distribution
76+
- [ ] Replace raw binary download with `dotnet tool install -g napper --version X.X.X`
77+
- [ ] Delete custom HTTP download code (`cliInstaller.ts` download/redirect logic)
7578
- [ ] Split editor layout (request panel webview)
7679
- [ ] New request guided flow
7780
- [ ] OpenAPI generation command

specs/IDE-EXTENSION-SPEC.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -354,6 +354,7 @@ These settings apply across all IDEs where the extension supports configuration.
354354
- Built in **TypeScript** using the VSCode Extension API.
355355
- The response panel webview uses a minimal framework (Lit or vanilla TS + CSS) — no heavy UI library.
356356
- The extension shells out to the **Nap CLI** (`nap run --output json`) for all HTTP execution.
357+
- **CLI acquisition:** The VSIX installs the CLI via `dotnet tool install -g napper --version X.X.X` on activation, where `X.X.X` is the extension's own `package.json` version. This avoids raw binary downloads (which trigger Windows SmartScreen warnings on unsigned binaries) and leverages NuGet as a trusted distribution channel. If the CLI is already on PATH at the correct version, installation is skipped.
357358
- File watching via `vscode.workspace.createFileSystemWatcher` keeps the panel tree up to date without polling.
358359
- The `.nap` language grammar (TextMate `.tmLanguage.json`) is generated from the ANTLR grammar to avoid drift.
359360
- Published to the **VS Code Marketplace** and the **Open VSX Registry** (for VSCodium / Cursor / Windsurf users).

src/Nap.VsCode/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
"name": "napper",
33
"displayName": "Napper",
44
"description": "CLI-first, test-oriented HTTP API testing tool. Send requests, run assertions, manage environments.",
5-
"version": "0.1.0",
5+
"version": "0.6.0",
66
"publisher": "nimblesite",
77
"license": "MIT",
88
"repository": {

src/Nap.VsCode/src/cliInstaller.ts

Lines changed: 15 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,6 @@ import {
1616
CLI_BIN_DIR,
1717
CLI_DOWNLOAD_ERROR_PREFIX,
1818
CLI_DOWNLOAD_HOST,
19-
CLI_DOWNLOAD_PATH_PREFIX,
2019
CLI_FILE_MODE_EXECUTABLE,
2120
CLI_MAX_REDIRECTS,
2221
CLI_PLATFORM_DARWIN,
@@ -36,6 +35,7 @@ import {
3635
HTTP_STATUS_CLIENT_ERROR_MIN,
3736
HTTP_STATUS_OK,
3837
HTTP_STATUS_REDIRECT_MIN,
38+
cliDownloadPath,
3939
} from './constants';
4040

4141
const PLATFORM_RID_MAP: ReadonlyMap<string, string> = new Map([
@@ -167,9 +167,10 @@ async function followRedirect(
167167
export const downloadBinary = async (
168168
rid: string,
169169
destPath: string,
170+
version: string,
170171
): Promise<Result<void, string>> => {
171172
const asset = assetName(rid),
172-
url = `https://${CLI_DOWNLOAD_HOST}${CLI_DOWNLOAD_PATH_PREFIX}${asset}`,
173+
url = `https://${CLI_DOWNLOAD_HOST}${cliDownloadPath(version)}${asset}`,
173174
dir = path.dirname(destPath);
174175

175176
if (!fs.existsSync(dir)) {
@@ -189,22 +190,27 @@ export interface InstallResult {
189190
readonly cliPath: string;
190191
}
191192

193+
export interface InstallCliParams {
194+
readonly storageDir: string;
195+
readonly platform: string;
196+
readonly arch: string;
197+
readonly version: string;
198+
}
199+
192200
export const installCli = async (
193-
storageDir: string,
194-
platform: string,
195-
arch: string,
201+
params: InstallCliParams,
196202
): Promise<Result<InstallResult, string>> => {
197-
const ridResult = platformToRid(platform, arch);
203+
const ridResult = platformToRid(params.platform, params.arch);
198204
if (!ridResult.ok) {
199205
return err(ridResult.error);
200206
}
201207

202-
const destPath = installedCliPath(storageDir, platform),
203-
downloadResult = await downloadBinary(ridResult.value, destPath);
208+
const destPath = installedCliPath(params.storageDir, params.platform),
209+
downloadResult = await downloadBinary(ridResult.value, destPath, params.version);
204210
if (!downloadResult.ok) {
205211
return err(downloadResult.error);
206212
}
207213

208-
makeExecutable(destPath, platform);
214+
makeExecutable(destPath, params.platform);
209215
return ok({ cliPath: destPath });
210216
};

src/Nap.VsCode/src/cliRunner.ts

Lines changed: 21 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -158,25 +158,31 @@ const attachDataListeners = (ctx: StreamListenerContext): void => {
158158
ctx.state.stderrOutput += chunk.toString();
159159
});
160160
},
161+
handleClose = (ctx: StreamListenerContext): void => {
162+
if (ctx.state.finished) {
163+
return;
164+
}
165+
ctx.state.finished = true;
166+
flushAndFinish({
167+
buffer: ctx.state.buffer,
168+
onResult: ctx.options.onResult,
169+
stderrOutput: ctx.state.stderrOutput,
170+
onDone: ctx.options.onDone,
171+
});
172+
},
173+
handleError = (ctx: StreamListenerContext, error: Error): void => {
174+
if (ctx.state.finished) {
175+
return;
176+
}
177+
ctx.state.finished = true;
178+
ctx.options.onDone(`${CLI_SPAWN_FAILED_PREFIX}${ctx.cliPath}${error.message}`);
179+
},
161180
attachLifecycleListeners = (ctx: StreamListenerContext): void => {
162181
ctx.child.on('close', () => {
163-
if (ctx.state.finished) {
164-
return;
165-
}
166-
ctx.state.finished = true;
167-
flushAndFinish({
168-
buffer: ctx.state.buffer,
169-
onResult: ctx.options.onResult,
170-
stderrOutput: ctx.state.stderrOutput,
171-
onDone: ctx.options.onDone,
172-
});
182+
handleClose(ctx);
173183
});
174184
ctx.child.on('error', (error) => {
175-
if (ctx.state.finished) {
176-
return;
177-
}
178-
ctx.state.finished = true;
179-
ctx.options.onDone(`${CLI_SPAWN_FAILED_PREFIX}${ctx.cliPath}${error.message}`);
185+
handleError(ctx, error);
180186
});
181187
};
182188

src/Nap.VsCode/src/constants.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -144,8 +144,9 @@ export const CLI_REPO_NAME = 'napper';
144144
export const CLI_BINARY_NAME = 'napper';
145145
export const CLI_BIN_DIR = 'bin';
146146
export const CLI_DOWNLOAD_HOST = 'github.com';
147-
export const CLI_REQUIRED_VERSION = '0.1.0';
148-
export const CLI_DOWNLOAD_PATH_PREFIX = `/MelbourneDeveloper/napper/releases/download/v${CLI_REQUIRED_VERSION}/`;
147+
export const CLI_DOWNLOAD_PATH_BASE = '/MelbourneDeveloper/napper/releases/download/v';
148+
export const cliDownloadPath = (version: string): string =>
149+
`${CLI_DOWNLOAD_PATH_BASE}${version}/`;
149150
export const CLI_ASSET_PREFIX = 'napper-';
150151
export const CLI_WIN_EXE_SUFFIX = '.exe';
151152
export const CLI_MAX_REDIRECTS = 5;

src/Nap.VsCode/src/extension.ts

Lines changed: 31 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,6 @@ import {
3636
CLI_INSTALL_COMPLETE_MSG,
3737
CLI_INSTALL_FAILED_MSG,
3838
CLI_INSTALL_MSG,
39-
CLI_REQUIRED_VERSION,
4039
CLI_VERSION_MISMATCH_MSG,
4140
CMD_COPY_CURL,
4241
CMD_ENRICH_AI,
@@ -83,6 +82,7 @@ import {
8382

8483
let bundledCliPath: string | undefined,
8584
envStatusBar: EnvironmentStatusBar,
85+
extensionVersion: string,
8686
explorerProvider: ExplorerAdapter,
8787
installedPath: string | undefined,
8888
lastPlaylistReport: (() => void) | undefined,
@@ -117,34 +117,42 @@ const getCliPath = (): string => {
117117
},
118118
isVersionMatch = async (candidate: string): Promise<boolean> => {
119119
const versionResult = await getCliVersion(candidate);
120-
if (versionResult.ok && versionResult.value === CLI_REQUIRED_VERSION) {
120+
if (versionResult.ok && versionResult.value === extensionVersion) {
121121
installedPath = candidate;
122122
return true;
123123
}
124124
logger.info(CLI_VERSION_MISMATCH_MSG);
125125
return false;
126126
},
127-
ensureCliInstalled = async (storageUri: vscode.Uri | undefined): Promise<void> => {
128-
if (storageUri === undefined) {
129-
return;
130-
}
131-
const storagePath = storageUri.fsPath,
132-
candidate = installedCliPath(storagePath, process.platform);
133-
if (isCliInstalled(candidate) && (await isVersionMatch(candidate))) {
134-
return;
135-
}
127+
performInstall = async (storagePath: string): Promise<void> => {
136128
await vscode.window.withProgress(
137129
{
138130
location: vscode.ProgressLocation.Notification,
139131
title: CLI_INSTALL_MSG,
140132
cancellable: false,
141133
},
142134
async () => {
143-
const result = await installCli(storagePath, process.platform, process.arch);
135+
const result = await installCli({
136+
storageDir: storagePath,
137+
platform: process.platform,
138+
arch: process.arch,
139+
version: extensionVersion,
140+
});
144141
handleInstallResult(result);
145142
},
146143
);
147144
},
145+
ensureCliInstalled = async (storageUri: vscode.Uri | undefined): Promise<void> => {
146+
if (storageUri === undefined) {
147+
return;
148+
}
149+
const storagePath = storageUri.fsPath,
150+
candidate = installedCliPath(storagePath, process.platform);
151+
if (isCliInstalled(candidate) && (await isVersionMatch(candidate))) {
152+
return;
153+
}
154+
await performInstall(storagePath);
155+
},
148156
getWorkspacePath = (): string | undefined => vscode.workspace.workspaceFolders?.[0]?.uri.fsPath,
149157
getResponseColumn = (): vscode.ViewColumn => {
150158
const config = vscode.workspace.getConfiguration(CONFIG_SECTION),
@@ -345,6 +353,14 @@ const collectResult = (state: StreamState, result: RunResult): void => {
345353
}),
346354
);
347355
},
356+
handleEnrichAi = async (arg?: { readonly filePath?: string }): Promise<void> => {
357+
const fp = arg?.filePath;
358+
if (fp === undefined) {
359+
return;
360+
}
361+
await runAiEnrichment(path.dirname(fp), logger);
362+
explorerProvider.refresh();
363+
},
348364
registerOpenApiCommands = (context: vscode.ExtensionContext): void => {
349365
context.subscriptions.push(
350366
vscode.commands.registerCommand(CMD_IMPORT_OPENAPI_URL, async () => {
@@ -353,17 +369,7 @@ const collectResult = (state: StreamState, result: RunResult): void => {
353369
vscode.commands.registerCommand(CMD_IMPORT_OPENAPI_FILE, async () => {
354370
await importOpenApiFromFile(explorerProvider, logger);
355371
}),
356-
vscode.commands.registerCommand(
357-
CMD_ENRICH_AI,
358-
async (arg?: { readonly filePath?: string }) => {
359-
const fp = arg?.filePath;
360-
if (fp === undefined) {
361-
return;
362-
}
363-
await runAiEnrichment(path.dirname(fp), logger);
364-
explorerProvider.refresh();
365-
},
366-
),
372+
vscode.commands.registerCommand(CMD_ENRICH_AI, handleEnrichAi),
367373
);
368374
},
369375
initProviders = (): void => {
@@ -393,6 +399,8 @@ const collectResult = (state: StreamState, result: RunResult): void => {
393399
outputChannel.appendLine(msg);
394400
});
395401
logger.info(LOG_MSG_ACTIVATED);
402+
extensionVersion = (context.extension.packageJSON as { version: string }).version;
403+
logger.info(`Extension version: ${extensionVersion}`);
396404
bundledCliPath = path.join(
397405
context.extensionPath,
398406
CLI_BIN_DIR,

0 commit comments

Comments
 (0)