├── .nvmrc ├── templates ├── ncs │ ├── prj.conf │ ├── CMakeLists.txt │ ├── west.yml │ └── src │ │ └── main.c ├── nfed │ ├── prj.conf │ ├── sysbuild.conf │ ├── sysbuild │ │ └── mcuboot.conf │ ├── boards │ │ ├── circuitdojo_feather_nrf9151_ns.conf │ │ └── circuitdojo_feather_nrf9151_ns.overlay │ ├── CMakeLists.txt │ ├── west.yml │ └── src │ │ └── main.c └── vanilla │ ├── prj.conf │ ├── CMakeLists.txt │ ├── west.yml │ └── src │ └── main.c ├── .gitignore ├── img └── bulb.png ├── src ├── tasks │ ├── index.ts │ └── task-manager.ts ├── build │ ├── index.ts │ └── build-assets-manager.ts ├── types │ ├── index.ts │ ├── project.ts │ ├── config.ts │ └── manifest.ts ├── hardware │ ├── index.ts │ ├── serial-port-manager.ts │ ├── newtmgr-manager.ts │ ├── board-detector.ts │ └── probe-manager.ts ├── utils │ ├── index.ts │ ├── platform-utils.ts │ ├── environment-utils.ts │ ├── path-utils.ts │ └── yaml-parser.ts ├── environment │ ├── index.ts │ ├── git-checker.ts │ ├── dependency-installer.ts │ ├── python-checker.ts │ └── path-manager.ts ├── files │ ├── index.ts │ ├── file-downloader.ts │ ├── file-validator.ts │ ├── archive-extractor.ts │ └── project-scanner.ts ├── ui │ ├── index.ts │ ├── output-channel.ts │ ├── quick-picks.ts │ ├── status-bar.ts │ └── dialogs.ts ├── config │ ├── index.ts │ ├── constants.ts │ ├── global-config.ts │ ├── project-config.ts │ ├── validation.ts │ └── settings-manager.ts ├── commands │ ├── index.ts │ ├── build-assets.ts │ ├── update.ts │ ├── terminal.ts │ ├── path-management.ts │ ├── clean.ts │ ├── create-project.ts │ ├── build.ts │ ├── monitor.ts │ ├── debug.ts │ └── project-management.ts └── test │ ├── runTest.ts │ └── suite │ ├── index.ts │ └── extension.test.ts ├── .prettierrc.json ├── .vscode ├── extensions.json ├── settings.json ├── launch.json └── tasks.json ├── .vscodeignore ├── media ├── reset.css └── vscode.css ├── .eslintrc.json ├── tsconfig.json ├── esbuild.js ├── AGENTS.md ├── debug-config-plan.md ├── README.md └── package.json /.nvmrc: -------------------------------------------------------------------------------- 1 | v16.13.1 -------------------------------------------------------------------------------- /templates/ncs/prj.conf: -------------------------------------------------------------------------------- 1 | CONFIG_GPIO=y 2 | -------------------------------------------------------------------------------- /templates/nfed/prj.conf: -------------------------------------------------------------------------------- 1 | CONFIG_GPIO=y 2 | -------------------------------------------------------------------------------- /templates/vanilla/prj.conf: -------------------------------------------------------------------------------- 1 | CONFIG_GPIO=y 2 | -------------------------------------------------------------------------------- /templates/nfed/sysbuild.conf: -------------------------------------------------------------------------------- 1 | SB_CONFIG_BOOTLOADER_MCUBOOT=y -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | out 2 | dist 3 | node_modules 4 | .vscode-test/ 5 | *.vsix 6 | extension.js -------------------------------------------------------------------------------- /img/bulb.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/circuitdojo/zephyr-tools/HEAD/img/bulb.png -------------------------------------------------------------------------------- /src/tasks/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @author Jared Wolff 3 | * @copyright Circuit Dojo LLC 4 | * @license Apache 2.0 5 | */ 6 | 7 | export * from './task-manager'; 8 | -------------------------------------------------------------------------------- /src/build/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @author Jared Wolff 3 | * @copyright Circuit Dojo LLC 4 | * @license Apache 2.0 5 | */ 6 | 7 | export * from './build-assets-manager'; 8 | -------------------------------------------------------------------------------- /templates/nfed/sysbuild/mcuboot.conf: -------------------------------------------------------------------------------- 1 | # Disable Zephyr console 2 | CONFIG_CONSOLE=n 3 | 4 | # Multithreading 5 | CONFIG_MULTITHREADING=y 6 | 7 | # MCUBoot settings 8 | CONFIG_BOOT_MAX_IMG_SECTORS=256 9 | -------------------------------------------------------------------------------- /templates/nfed/boards/circuitdojo_feather_nrf9151_ns.conf: -------------------------------------------------------------------------------- 1 | # Logging 2 | CONFIG_LOG=y 3 | CONFIG_LOG_PRINTK=y 4 | 5 | # Enable I2C 6 | CONFIG_I2C=y 7 | 8 | # Pmic LED driver 9 | CONFIG_LED=y 10 | CONFIG_LED_NPM1300=y 11 | -------------------------------------------------------------------------------- /.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "semi": true, 3 | "singleQuote": false, 4 | "printWidth": 120, 5 | "tabWidth": 2, 6 | "bracketSpacing": true, 7 | "arrowParens": "avoid", 8 | "proseWrap": "never", 9 | "endOfLine": "auto" 10 | } 11 | -------------------------------------------------------------------------------- /templates/ncs/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: Apache-2.0 2 | 3 | cmake_minimum_required(VERSION 3.20.0) 4 | find_package(Zephyr REQUIRED HINTS $ENV{ZEPHYR_BASE}) 5 | project(blinky) 6 | 7 | target_sources(app PRIVATE src/main.c) 8 | -------------------------------------------------------------------------------- /templates/nfed/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: Apache-2.0 2 | 3 | cmake_minimum_required(VERSION 3.20.0) 4 | find_package(Zephyr REQUIRED HINTS $ENV{ZEPHYR_BASE}) 5 | project(blinky) 6 | 7 | target_sources(app PRIVATE src/main.c) 8 | -------------------------------------------------------------------------------- /templates/vanilla/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: Apache-2.0 2 | 3 | cmake_minimum_required(VERSION 3.20.0) 4 | find_package(Zephyr REQUIRED HINTS $ENV{ZEPHYR_BASE}) 5 | project(blinky) 6 | 7 | target_sources(app PRIVATE src/main.c) 8 | -------------------------------------------------------------------------------- /src/types/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @author Jared Wolff 3 | * @copyright Circuit Dojo LLC 4 | * @license Apache 2.0 5 | */ 6 | 7 | export * from './manifest'; 8 | export * from './project'; 9 | export * from './config'; 10 | -------------------------------------------------------------------------------- /src/hardware/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @author Jared Wolff 3 | * @copyright Circuit Dojo LLC 4 | * @license Apache 2.0 5 | */ 6 | 7 | export * from './probe-manager'; 8 | export * from './serial-port-manager'; 9 | export * from './newtmgr-manager'; 10 | -------------------------------------------------------------------------------- /src/utils/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @author Jared Wolff 3 | * @copyright Circuit Dojo LLC 4 | * @license Apache 2.0 5 | */ 6 | 7 | export * from './environment-utils'; 8 | export * from './platform-utils'; 9 | export * from './path-utils'; 10 | export * from './yaml-parser'; 11 | -------------------------------------------------------------------------------- /src/environment/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @author Jared Wolff 3 | * @copyright Circuit Dojo LLC 4 | * @license Apache 2.0 5 | */ 6 | 7 | export * from './python-checker'; 8 | export * from './git-checker'; 9 | export * from './path-manager'; 10 | export * from './dependency-installer'; 11 | -------------------------------------------------------------------------------- /src/files/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @author Jared Wolff 3 | * @copyright Circuit Dojo LLC 4 | * @license Apache 2.0 5 | */ 6 | 7 | export * from './file-downloader'; 8 | export * from './archive-extractor'; 9 | export * from './file-validator'; 10 | export * from './project-scanner'; 11 | -------------------------------------------------------------------------------- /templates/nfed/west.yml: -------------------------------------------------------------------------------- 1 | manifest: 2 | projects: 3 | - name: nfed 4 | url: https://github.com/circuitdojo/nrf9160-feather-examples-and-drivers.git 5 | path: nfed 6 | revision: v3.0.x 7 | import: true 8 | self: 9 | # This repository should be cloned to 10 | path: app 11 | -------------------------------------------------------------------------------- /src/ui/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @author Jared Wolff 3 | * @copyright Circuit Dojo LLC 4 | * @license Apache 2.0 5 | */ 6 | 7 | export * from './output-channel'; 8 | export * from './status-bar'; 9 | export * from './quick-picks'; 10 | export * from './dialogs'; 11 | export * from './sidebar-webview'; 12 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | // See http://go.microsoft.com/fwlink/?LinkId=827846 3 | // for the documentation about the extensions.json format 4 | "recommendations": [ 5 | "dbaeumer.vscode-eslint", 6 | "ms-vscode.cpptools", 7 | "nordic-semiconductor.nrf-devicetree", 8 | "nordic-semiconductor.nrf-kconfig" 9 | ] 10 | } 11 | -------------------------------------------------------------------------------- /templates/nfed/boards/circuitdojo_feather_nrf9151_ns.overlay: -------------------------------------------------------------------------------- 1 | &i2c2 { 2 | npm1300_pmic: pmic@6b { 3 | compatible = "nordic,npm1300"; 4 | reg = <0x6b>; 5 | 6 | npm1300_leds: leds { 7 | compatible = "nordic,npm1300-led"; 8 | nordic,led0-mode = "host"; 9 | nordic,led1-mode = "host"; 10 | nordic,led2-mode = "host"; 11 | }; 12 | }; 13 | }; -------------------------------------------------------------------------------- /templates/vanilla/west.yml: -------------------------------------------------------------------------------- 1 | manifest: 2 | remotes: 3 | - name: zephyrproject 4 | url-base: https://github.com/zephyrproject-rtos 5 | projects: 6 | - name: zephyr 7 | repo-path: zephyr 8 | remote: zephyrproject 9 | revision: v4.2.0 10 | import: true 11 | self: 12 | # This repository should be cloned to 13 | path: app 14 | -------------------------------------------------------------------------------- /.vscodeignore: -------------------------------------------------------------------------------- 1 | **/*.map 2 | .vscode/** 3 | .vscode-test/** 4 | .github/** 5 | node_modules/** 6 | !node_modules/7zip-bin/** 7 | out/test/** 8 | src/** 9 | .gitignore 10 | .gitattributes 11 | tsconfig.json 12 | esbuild.js 13 | webpack.config.js 14 | .eslintrc.json 15 | **/*.vsix 16 | .git/** 17 | *.log 18 | test/** 19 | extension.js.map 20 | **/*.md 21 | !changelog.md 22 | !LICENSE.md 23 | !readme.md 24 | .nvmrc -------------------------------------------------------------------------------- /src/config/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @author Jared Wolff 3 | * @copyright Circuit Dojo LLC 4 | * @license Apache 2.0 5 | */ 6 | 7 | export { GlobalConfigManager } from './global-config'; 8 | export { ProjectConfigManager } from './project-config'; 9 | export { SettingsManager } from './settings-manager'; 10 | export { ConfigValidator } from './validation'; 11 | export { ManifestValidator } from './manifest-validator'; 12 | export * from './constants'; 13 | -------------------------------------------------------------------------------- /src/types/project.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @author Jared Wolff 3 | * @copyright Circuit Dojo LLC 4 | * @license Apache 2.0 5 | */ 6 | 7 | export interface ProjectConfig { 8 | board?: string; 9 | target?: string; 10 | isInit: boolean; 11 | isInitializing?: boolean; // Track when initialization is in progress 12 | runner?: string; 13 | runnerParams?: string; 14 | sysbuild?: boolean; 15 | } 16 | 17 | export interface ZephyrTask { 18 | name?: string; 19 | data?: any; 20 | } 21 | -------------------------------------------------------------------------------- /media/reset.css: -------------------------------------------------------------------------------- 1 | /* CSS Reset for consistent rendering across different environments */ 2 | * { 3 | box-sizing: border-box; 4 | } 5 | 6 | html, body, div, span, h1, h2, h3, h4, h5, h6, p, a, button, ul, li { 7 | margin: 0; 8 | padding: 0; 9 | border: 0; 10 | font: inherit; 11 | vertical-align: baseline; 12 | } 13 | 14 | body { 15 | line-height: 1; 16 | } 17 | 18 | ul { 19 | list-style: none; 20 | } 21 | 22 | button { 23 | cursor: pointer; 24 | background: none; 25 | outline: none; 26 | } 27 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "root": true, 3 | "parser": "@typescript-eslint/parser", 4 | "parserOptions": { 5 | "ecmaVersion": 6, 6 | "sourceType": "module" 7 | }, 8 | "plugins": [ 9 | "@typescript-eslint" 10 | ], 11 | "rules": { 12 | "@typescript-eslint/naming-convention": "warn", 13 | "@typescript-eslint/semi": "warn", 14 | "curly": "warn", 15 | "eqeqeq": "warn", 16 | "no-throw-literal": "warn", 17 | "semi": "off" 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | // Place your settings in this file to overwrite default and user settings. 2 | { 3 | "files.exclude": { 4 | "out": false // set this to true to hide the "out" folder with the compiled JS files 5 | }, 6 | "search.exclude": { 7 | "out": true // set this to false to include "out" folder in search results 8 | }, 9 | // Turn off tsc task auto detection since we have the necessary tasks as npm scripts 10 | "typescript.tsc.autoDetect": "off", 11 | "cmake.configureOnOpen": false 12 | } -------------------------------------------------------------------------------- /src/commands/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @author Jared Wolff 3 | * @copyright Circuit Dojo LLC 4 | * @license Apache 2.0 5 | */ 6 | 7 | export * from './setup'; 8 | export * from './build'; 9 | export * from './flash'; 10 | export * from './load'; 11 | export * from './monitor'; 12 | export * from './project-management'; 13 | export * from './board-management'; 14 | export * from './create-project'; 15 | export * from './clean'; 16 | export * from './update'; 17 | export * from './build-assets'; 18 | export * from './terminal'; 19 | export * from './debug'; 20 | -------------------------------------------------------------------------------- /src/types/config.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @author Jared Wolff 3 | * @copyright Circuit Dojo LLC 4 | * @license Apache 2.0 5 | */ 6 | 7 | export interface GlobalConfig { 8 | isSetup: boolean; 9 | isSetupInProgress: boolean; 10 | manifestVersion: Number; 11 | } 12 | 13 | export interface ProbeInfo { 14 | id: string; 15 | name: string; 16 | probeId?: string; // The actual probe identifier for --probe flag 17 | vidPid?: string; // VID:PID information 18 | serial?: string; // Serial number 19 | fullDescription?: string; // Full description from probe-rs 20 | } 21 | -------------------------------------------------------------------------------- /templates/ncs/west.yml: -------------------------------------------------------------------------------- 1 | manifest: 2 | remotes: 3 | - name: nrfconnect 4 | url-base: https://github.com/nrfconnect 5 | projects: 6 | - name: nrf 7 | repo-path: sdk-nrf 8 | remote: nrfconnect 9 | revision: v3.0.1 10 | import: 11 | name-blocklist: 12 | - matter 13 | - nrf-802154 14 | - cjson 15 | - openthread 16 | - cmock 17 | - unity 18 | - cddl-gen 19 | - homekit 20 | - loramac-node 21 | - lz4 22 | - lvgl 23 | - mipi-sys-t 24 | self: 25 | # This repository should be cloned to 26 | path: app 27 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "target": "es6", 5 | "outDir": "out", 6 | "lib": [ 7 | "es6" 8 | ], 9 | "sourceMap": true, 10 | "rootDir": "src", 11 | "strict": true /* enable all strict type-checking options */ 12 | /* Additional Checks */ 13 | // "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */ 14 | // "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */ 15 | // "noUnusedParameters": true, /* Report errors on unused parameters. */ 16 | }, 17 | "exclude": [ 18 | "node_modules", 19 | ".vscode-test" 20 | ] 21 | } 22 | -------------------------------------------------------------------------------- /src/test/runTest.ts: -------------------------------------------------------------------------------- 1 | import * as path from 'path'; 2 | 3 | import { runTests } from 'vscode-test'; 4 | 5 | async function main() { 6 | try { 7 | // The folder containing the Extension Manifest package.json 8 | // Passed to `--extensionDevelopmentPath` 9 | const extensionDevelopmentPath = path.resolve(__dirname, '../../'); 10 | 11 | // The path to test runner 12 | // Passed to --extensionTestsPath 13 | const extensionTestsPath = path.resolve(__dirname, './suite/index'); 14 | 15 | // Download VS Code, unzip it and run the integration test 16 | await runTests({ extensionDevelopmentPath, extensionTestsPath }); 17 | } catch (err) { 18 | console.error('Failed to run tests'); 19 | process.exit(1); 20 | } 21 | } 22 | 23 | main(); 24 | -------------------------------------------------------------------------------- /src/ui/output-channel.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @author Jared Wolff 3 | * @copyright Circuit Dojo LLC 4 | * @license Apache 2.0 5 | */ 6 | 7 | import * as vscode from "vscode"; 8 | 9 | export class OutputChannelManager { 10 | private static output: vscode.OutputChannel; 11 | private static readonly CHANNEL_NAME = "Zephyr Tools"; 12 | 13 | static getChannel(): vscode.OutputChannel { 14 | if (!this.output) { 15 | this.output = vscode.window.createOutputChannel(this.CHANNEL_NAME); 16 | } 17 | return this.output; 18 | } 19 | 20 | static appendLine(message: string): void { 21 | this.getChannel().appendLine(message); 22 | } 23 | 24 | static append(message: string): void { 25 | this.getChannel().append(message); 26 | } 27 | 28 | static show(): void { 29 | this.getChannel().show(); 30 | } 31 | 32 | static clear(): void { 33 | this.getChannel().clear(); 34 | } 35 | 36 | static dispose(): void { 37 | if (this.output) { 38 | this.output.dispose(); 39 | } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /templates/ncs/src/main.c: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2016 Intel Corporation 3 | * 4 | * SPDX-License-Identifier: Apache-2.0 5 | */ 6 | 7 | #include 8 | #include 9 | 10 | /* 1000 msec = 1 sec */ 11 | #define SLEEP_TIME_MS 1000 12 | 13 | /* The devicetree node identifier for the "led0" alias. */ 14 | #define LED0_NODE DT_ALIAS(led0) 15 | 16 | /* 17 | * A build error on this line means your board is unsupported. 18 | * See the sample documentation for information on how to fix this. 19 | */ 20 | static const struct gpio_dt_spec led = GPIO_DT_SPEC_GET(LED0_NODE, gpios); 21 | 22 | void main(void) 23 | { 24 | int ret; 25 | 26 | printk("Hello World! %s\n", CONFIG_BOARD); 27 | 28 | if (!gpio_is_ready_dt(&led)) 29 | { 30 | return; 31 | } 32 | 33 | ret = gpio_pin_configure_dt(&led, GPIO_OUTPUT_ACTIVE); 34 | if (ret < 0) 35 | { 36 | return; 37 | } 38 | 39 | while (1) 40 | { 41 | ret = gpio_pin_toggle_dt(&led); 42 | if (ret < 0) 43 | { 44 | return; 45 | } 46 | k_msleep(SLEEP_TIME_MS); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /templates/vanilla/src/main.c: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2016 Intel Corporation 3 | * 4 | * SPDX-License-Identifier: Apache-2.0 5 | */ 6 | 7 | #include 8 | #include 9 | 10 | /* 1000 msec = 1 sec */ 11 | #define SLEEP_TIME_MS 1000 12 | 13 | /* The devicetree node identifier for the "led0" alias. */ 14 | #define LED0_NODE DT_ALIAS(led0) 15 | 16 | /* 17 | * A build error on this line means your board is unsupported. 18 | * See the sample documentation for information on how to fix this. 19 | */ 20 | static const struct gpio_dt_spec led = GPIO_DT_SPEC_GET(LED0_NODE, gpios); 21 | 22 | void main(void) 23 | { 24 | int ret; 25 | 26 | printk("Hello World! %s\n", CONFIG_BOARD); 27 | 28 | if (!gpio_is_ready_dt(&led)) 29 | { 30 | return; 31 | } 32 | 33 | ret = gpio_pin_configure_dt(&led, GPIO_OUTPUT_ACTIVE); 34 | if (ret < 0) 35 | { 36 | return; 37 | } 38 | 39 | while (1) 40 | { 41 | ret = gpio_pin_toggle_dt(&led); 42 | if (ret < 0) 43 | { 44 | return; 45 | } 46 | k_msleep(SLEEP_TIME_MS); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/test/suite/index.ts: -------------------------------------------------------------------------------- 1 | import * as path from "path"; 2 | import * as Mocha from "mocha"; 3 | import { glob } from "glob"; 4 | 5 | export function run(): Promise { 6 | // Create the mocha test 7 | const mocha = new Mocha({ 8 | ui: "tdd", 9 | color: true, 10 | }); 11 | 12 | const testsRoot = path.resolve(__dirname, ".."); 13 | 14 | return new Promise((c, e) => { 15 | glob("**/**.test.js", { cwd: testsRoot }) 16 | .then((files: string[]) => { 17 | // Add files to the test suite 18 | files.forEach((f: string) => mocha.addFile(path.resolve(testsRoot, f))); 19 | 20 | try { 21 | // Run the mocha test 22 | mocha.run(failures => { 23 | if (failures > 0) { 24 | e(new Error(`${failures} tests failed.`)); 25 | } else { 26 | c(); 27 | } 28 | }); 29 | } catch (err) { 30 | console.error(err); 31 | e(err); 32 | } 33 | }) 34 | .catch((err: any) => { 35 | return e(err); 36 | }); 37 | }); 38 | } 39 | -------------------------------------------------------------------------------- /src/types/manifest.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @author Jared Wolff 3 | * @copyright Circuit Dojo LLC 4 | * @license Apache 2.0 5 | */ 6 | 7 | export interface ManifestEnvEntry { 8 | name: string; 9 | value?: string; 10 | usepath: boolean; 11 | append: boolean; 12 | suffix?: string; 13 | } 14 | 15 | export interface CmdEntry { 16 | cmd: string; 17 | usepath: boolean; 18 | } 19 | 20 | export interface ManifestToolchainEntry { 21 | name: string; 22 | downloads: ManifestDownloadEntry[]; 23 | } 24 | 25 | export interface ManifestDownloadEntry { 26 | name: string; 27 | url: string; 28 | md5: string; 29 | suffix?: string; 30 | env?: ManifestEnvEntry[]; 31 | cmd?: CmdEntry[]; 32 | filename: string; 33 | clear_target?: boolean; 34 | copy_to_subfolder?: string; 35 | } 36 | 37 | export interface ManifestEntry { 38 | arch: string; 39 | toolchains: ManifestToolchainEntry[]; 40 | downloads: ManifestDownloadEntry[]; 41 | } 42 | 43 | export interface Manifest { 44 | version: Number; 45 | win32: ManifestEntry[]; 46 | darwin: ManifestEntry[]; 47 | linux: ManifestEntry[]; 48 | } 49 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | // A launch configuration that compiles the extension and then opens it inside a new window 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | { 6 | "version": "0.2.0", 7 | "configurations": [ 8 | { 9 | "name": "Run Extension", 10 | "type": "extensionHost", 11 | "request": "launch", 12 | "args": ["--extensionDevelopmentPath=${workspaceFolder}"], 13 | "outFiles": ["${workspaceFolder}/out/**/*.js"], 14 | "preLaunchTask": "${defaultBuildTask}", 15 | "env": { 16 | "VSCODE_DEBUG": "true" 17 | } 18 | }, 19 | { 20 | "name": "Extension Tests", 21 | "type": "extensionHost", 22 | "request": "launch", 23 | "args": [ 24 | "--extensionDevelopmentPath=${workspaceFolder}", 25 | "--extensionTestsPath=${workspaceFolder}/out/test/suite/index" 26 | ], 27 | "outFiles": ["${workspaceFolder}/out/test/**/*.js"], 28 | "preLaunchTask": "${defaultBuildTask}" 29 | } 30 | ] 31 | } 32 | -------------------------------------------------------------------------------- /media/vscode.css: -------------------------------------------------------------------------------- 1 | /* VS Code CSS Variables for theme integration */ 2 | :root { 3 | --container-padding: 20px; 4 | --input-padding-vertical: 6px; 5 | --input-padding-horizontal: 4px; 6 | --input-margin-vertical: 4px; 7 | --input-margin-horizontal: 0; 8 | } 9 | 10 | body { 11 | padding: 0 var(--container-padding); 12 | color: var(--vscode-foreground); 13 | font-size: var(--vscode-font-size); 14 | font-weight: var(--vscode-font-weight); 15 | font-family: var(--vscode-font-family); 16 | background-color: var(--vscode-sidebar-background); 17 | } 18 | 19 | ol, 20 | ul { 21 | padding-left: var(--container-padding); 22 | } 23 | 24 | body > *, 25 | form > * { 26 | margin-block-start: var(--input-margin-vertical); 27 | margin-block-end: var(--input-margin-vertical); 28 | } 29 | 30 | *:focus { 31 | outline-color: var(--vscode-focusBorder) !important; 32 | } 33 | 34 | a { 35 | color: var(--vscode-textLink-foreground); 36 | } 37 | 38 | a:hover, 39 | a:active { 40 | color: var(--vscode-textLink-activeForeground); 41 | } 42 | 43 | code { 44 | font-size: var(--vscode-editor-font-size); 45 | font-family: var(--vscode-editor-font-family); 46 | } 47 | -------------------------------------------------------------------------------- /src/config/constants.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @author Jared Wolff 3 | * @copyright Circuit Dojo LLC 4 | * @license Apache 2.0 5 | */ 6 | 7 | import * as os from "os"; 8 | import * as path from "path"; 9 | 10 | // Platform 11 | export const platform: NodeJS.Platform = os.platform(); 12 | 13 | // Architecture 14 | export const arch: string = os.arch(); 15 | 16 | // Platform-dependent variables 17 | export const TOOLS_FOLDER_NAME = ".zephyrtools"; 18 | export const BAUD_LIST = ["1000000", "115200"]; 19 | 20 | // Platform-specific configurations 21 | export interface PlatformConfig { 22 | python: string; 23 | pathDivider: string; 24 | which: string; 25 | } 26 | 27 | export function getPlatformConfig(): PlatformConfig { 28 | switch (platform) { 29 | case "win32": 30 | return { 31 | python: "python", 32 | pathDivider: ";", 33 | which: "where" 34 | }; 35 | default: 36 | return { 37 | python: "python3", 38 | pathDivider: ":", 39 | which: "which" 40 | }; 41 | } 42 | } 43 | 44 | // Path divider for the current platform 45 | export const pathdivider = getPlatformConfig().pathDivider; 46 | -------------------------------------------------------------------------------- /src/commands/build-assets.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @author Jared Wolff 3 | * @copyright Circuit Dojo LLC 4 | * @license Apache 2.0 5 | */ 6 | 7 | import * as vscode from "vscode"; 8 | import { GlobalConfig } from "../types"; 9 | import { ProjectConfigManager } from "../config"; 10 | import { BuildAssetsManager } from "../build/build-assets-manager"; 11 | 12 | export async function openBuildFolderCommand( 13 | config: GlobalConfig, 14 | context: vscode.ExtensionContext 15 | ): Promise { 16 | const project = await ProjectConfigManager.load(context); 17 | 18 | if (!project.target || !project.board) { 19 | vscode.window.showErrorMessage("No project or board selected. Configure your project first."); 20 | return; 21 | } 22 | 23 | await BuildAssetsManager.openBuildFolder(project); 24 | } 25 | 26 | export async function revealBuildAssetCommand( 27 | config: GlobalConfig, 28 | context: vscode.ExtensionContext, 29 | filePath: string 30 | ): Promise { 31 | if (!filePath) { 32 | vscode.window.showErrorMessage("No file path provided."); 33 | return; 34 | } 35 | 36 | await BuildAssetsManager.revealBuildAsset(filePath); 37 | } 38 | -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | // See https://go.microsoft.com/fwlink/?LinkId=733558 2 | // for the documentation about the tasks.json format 3 | { 4 | "version": "2.0.0", 5 | "tasks": [ 6 | { 7 | "type": "npm", 8 | "script": "compile", 9 | "problemMatcher": ["$esbuild"], 10 | "group": { 11 | "kind": "build", 12 | "isDefault": true 13 | } 14 | }, 15 | { 16 | "type": "npm", 17 | "script": "compile:tsc", 18 | "problemMatcher": "$tsc", 19 | "group": { 20 | "kind": "build" 21 | } 22 | }, 23 | { 24 | "type": "npm", 25 | "script": "watch", 26 | "problemMatcher": ["$esbuild-watch"], 27 | "isBackground": true, 28 | "presentation": { 29 | "reveal": "never" 30 | }, 31 | "group": { 32 | "kind": "build" 33 | } 34 | }, 35 | { 36 | "label": "compile and watch", 37 | "type": "shell", 38 | "command": "npm run compile && npm run watch", 39 | "problemMatcher": "$tsc-watch", 40 | "isBackground": true, 41 | "presentation": { 42 | "reveal": "always" 43 | }, 44 | "group": { 45 | "kind": "build" 46 | } 47 | } 48 | ] 49 | } 50 | -------------------------------------------------------------------------------- /esbuild.js: -------------------------------------------------------------------------------- 1 | const esbuild = require('esbuild'); 2 | 3 | const production = process.argv.includes('--production'); 4 | const watch = process.argv.includes('--watch'); 5 | 6 | async function main() { 7 | const ctx = await esbuild.context({ 8 | entryPoints: ['src/extension.ts'], 9 | bundle: true, 10 | format: 'cjs', 11 | minify: production, 12 | sourcemap: !production, 13 | sourcesContent: false, 14 | platform: 'node', 15 | outfile: 'out/extension.js', 16 | external: ['vscode', '7zip-bin'], 17 | logLevel: 'silent', 18 | plugins: [ 19 | /* add to the end of plugins array */ 20 | esbuildProblemMatcherPlugin 21 | ] 22 | }); 23 | if (watch) { 24 | await ctx.watch(); 25 | } else { 26 | await ctx.rebuild(); 27 | await ctx.dispose(); 28 | } 29 | } 30 | 31 | /** 32 | * @type {import('esbuild').Plugin} 33 | */ 34 | const esbuildProblemMatcherPlugin = { 35 | name: 'esbuild-problem-matcher', 36 | 37 | setup(build) { 38 | build.onStart(() => { 39 | console.log('[watch] build started'); 40 | }); 41 | build.onEnd(result => { 42 | result.errors.forEach(({ text, location }) => { 43 | console.error(`✘ [ERROR] ${text}`); 44 | console.error(` ${location.file}:${location.line}:${location.column}:`); 45 | }); 46 | console.log('[watch] build finished'); 47 | }); 48 | } 49 | }; 50 | 51 | main().catch(e => { 52 | console.error(e); 53 | process.exit(1); 54 | }); -------------------------------------------------------------------------------- /src/environment/git-checker.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @author Jared Wolff 3 | * @copyright Circuit Dojo LLC 4 | * @license Apache 2.0 5 | */ 6 | 7 | import * as vscode from "vscode"; 8 | import * as util from "util"; 9 | import * as cp from "child_process"; 10 | import { platform } from "../config"; 11 | 12 | export async function validateGitInstallation(env: { [key: string]: string | undefined }, output: vscode.OutputChannel): Promise { 13 | const exec = util.promisify(cp.exec); 14 | 15 | try { 16 | const result = await exec("git --version", { env }); 17 | output.append(result.stdout); 18 | output.append(result.stderr); 19 | output.appendLine("[SETUP] git installed"); 20 | return true; 21 | } catch (error) { 22 | output.appendLine("[SETUP] git is not found"); 23 | output.append(String(error)); 24 | showGitInstallInstructions(output); 25 | return false; 26 | } 27 | } 28 | 29 | function showGitInstallInstructions(output: vscode.OutputChannel): void { 30 | switch (platform) { 31 | case "darwin": 32 | output.appendLine("[SETUP] use `brew` to install `git`"); 33 | output.appendLine("[SETUP] Install `brew` first: https://brew.sh"); 34 | output.appendLine("[SETUP] Then run `brew install git`"); 35 | break; 36 | case "linux": 37 | output.appendLine("[SETUP] refer to your distros preferred `git` install method."); 38 | break; 39 | default: 40 | break; 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/config/global-config.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @author Jared Wolff 3 | * @copyright Circuit Dojo LLC 4 | * @license Apache 2.0 5 | */ 6 | 7 | import * as vscode from "vscode"; 8 | import { GlobalConfig } from "../types"; 9 | import { EnvironmentUtils } from "../utils"; 10 | 11 | export class GlobalConfigManager { 12 | private static readonly CONFIG_KEY = "zephyr.env"; 13 | private static _onDidChangeConfig = new vscode.EventEmitter(); 14 | public static readonly onDidChangeConfig = GlobalConfigManager._onDidChangeConfig.event; 15 | 16 | static async load(context: vscode.ExtensionContext): Promise { 17 | return context.globalState.get(this.CONFIG_KEY) ?? { 18 | manifestVersion: 0, 19 | isSetup: false, 20 | isSetupInProgress: false, 21 | }; 22 | } 23 | 24 | static async save(context: vscode.ExtensionContext, config: GlobalConfig): Promise { 25 | await context.globalState.update(this.CONFIG_KEY, config); 26 | // Fire event to notify listeners of config changes 27 | this._onDidChangeConfig.fire(config); 28 | } 29 | 30 | static async reset(context: vscode.ExtensionContext): Promise { 31 | await context.globalState.update(this.CONFIG_KEY, undefined); 32 | // Load default config and fire event 33 | const defaultConfig: GlobalConfig = { 34 | manifestVersion: 0, 35 | isSetup: false, 36 | isSetupInProgress: false, 37 | }; 38 | this._onDidChangeConfig.fire(defaultConfig); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/utils/platform-utils.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @author Jared Wolff 3 | * @copyright Circuit Dojo LLC 4 | * @license Apache 2.0 5 | */ 6 | 7 | import { platform } from "../config"; 8 | 9 | export interface PlatformPaths { 10 | pythonExecutable: string; 11 | pathDivider: string; 12 | whichCommand: string; 13 | } 14 | 15 | export class PlatformUtils { 16 | static getPlatformSpecificPaths(): PlatformPaths { 17 | switch (platform) { 18 | case "win32": 19 | return { 20 | pythonExecutable: "python", 21 | pathDivider: ";", 22 | whichCommand: "where" 23 | }; 24 | default: 25 | return { 26 | pythonExecutable: "python3", 27 | pathDivider: ":", 28 | whichCommand: "which" 29 | }; 30 | } 31 | } 32 | 33 | static isWindows(): boolean { 34 | return platform === "win32"; 35 | } 36 | 37 | static getExecutableExtension(): string { 38 | return this.isWindows() ? ".exe" : ""; 39 | } 40 | 41 | static normalizePathForPlatform(inputPath: string): string { 42 | if (this.isWindows()) { 43 | return inputPath.replace(/\//g, "\\"); 44 | } else { 45 | return inputPath.replace(/\\/g, "/"); 46 | } 47 | } 48 | 49 | /** 50 | * Get platform-specific executable names for tools used by Zephyr Tools 51 | */ 52 | static getToolExecutables() { 53 | return { 54 | probeRs: `probe-rs${this.getExecutableExtension()}`, 55 | newtmgr: `newtmgr${this.getExecutableExtension()}`, 56 | zephyrTools: `zephyr-tools${this.getExecutableExtension()}` 57 | }; 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /templates/nfed/src/main.c: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2016 Intel Corporation 3 | * 4 | * SPDX-License-Identifier: Apache-2.0 5 | */ 6 | 7 | #include 8 | #include 9 | #include 10 | #include 11 | #include 12 | LOG_MODULE_REGISTER(main); 13 | 14 | /* 1000 msec = 1 sec */ 15 | #define SLEEP_TIME_MS 1000 16 | 17 | #if defined(CONFIG_BOARD_CIRCUITDOJO_FEATHER_NRF9151) 18 | 19 | static const struct device *leds = DEVICE_DT_GET(DT_NODELABEL(npm1300_leds)); 20 | 21 | int main(void) 22 | { 23 | LOG_INF("Blinky Sample"); 24 | 25 | while (1) 26 | { 27 | led_on(leds, 2U); 28 | k_sleep(K_MSEC(SLEEP_TIME_MS)); 29 | led_off(leds, 2U); 30 | k_sleep(K_MSEC(SLEEP_TIME_MS)); 31 | } 32 | 33 | return 0; 34 | } 35 | 36 | #else 37 | 38 | /* The devicetree node identifier for the "led0" alias. */ 39 | #define LED0_NODE DT_ALIAS(led0) 40 | 41 | /* 42 | * A build error on this line means your board is unsupported. 43 | * See the sample documentation for information on how to fix this. 44 | */ 45 | static const struct gpio_dt_spec led = GPIO_DT_SPEC_GET(LED0_NODE, gpios); 46 | 47 | void main(void) 48 | { 49 | int ret; 50 | 51 | printk("Hello World! %s\n", CONFIG_BOARD); 52 | 53 | if (!gpio_is_ready_dt(&led)) 54 | { 55 | return; 56 | } 57 | 58 | ret = gpio_pin_configure_dt(&led, GPIO_OUTPUT_ACTIVE); 59 | if (ret < 0) 60 | { 61 | return; 62 | } 63 | 64 | while (1) 65 | { 66 | ret = gpio_pin_toggle_dt(&led); 67 | if (ret < 0) 68 | { 69 | return; 70 | } 71 | k_msleep(SLEEP_TIME_MS); 72 | } 73 | } 74 | 75 | #endif -------------------------------------------------------------------------------- /src/config/project-config.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @author Jared Wolff 3 | * @copyright Circuit Dojo LLC 4 | * @license Apache 2.0 5 | */ 6 | 7 | import * as vscode from "vscode"; 8 | import { ProjectConfig, ZephyrTask } from "../types"; 9 | 10 | // Default project configuration 11 | export const DEFAULT_PROJECT_CONFIG: ProjectConfig = { 12 | isInit: false, 13 | sysbuild: true, 14 | }; 15 | 16 | export class ProjectConfigManager { 17 | // Event emitter for configuration change 18 | private static _onDidChangeConfig: vscode.EventEmitter = new vscode.EventEmitter(); 19 | static readonly onDidChangeConfig: vscode.Event = ProjectConfigManager._onDidChangeConfig.event; 20 | private static readonly PROJECT_CONFIG_KEY = "zephyr.project"; 21 | private static readonly TASK_CONFIG_KEY = "zephyr.task"; 22 | 23 | static async load(context: vscode.ExtensionContext): Promise { 24 | return context.workspaceState.get(this.PROJECT_CONFIG_KEY) ?? DEFAULT_PROJECT_CONFIG; 25 | } 26 | 27 | static async save(context: vscode.ExtensionContext, config: ProjectConfig): Promise { 28 | await context.workspaceState.update(this.PROJECT_CONFIG_KEY, config); 29 | ProjectConfigManager._onDidChangeConfig.fire(); // Notify listeners of changes 30 | } 31 | 32 | static async loadPendingTask(context: vscode.ExtensionContext): Promise { 33 | return context.globalState.get(this.TASK_CONFIG_KEY); 34 | } 35 | 36 | static async savePendingTask(context: vscode.ExtensionContext, task: ZephyrTask | undefined): Promise { 37 | await context.globalState.update(this.TASK_CONFIG_KEY, task); 38 | } 39 | 40 | static async clearPendingTask(context: vscode.ExtensionContext): Promise { 41 | await context.globalState.update(this.TASK_CONFIG_KEY, undefined); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/commands/update.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @author Jared Wolff 3 | * @copyright Circuit Dojo LLC 4 | * @license Apache 2.0 5 | */ 6 | 7 | import * as vscode from "vscode"; 8 | import { GlobalConfig } from "../types"; 9 | import { ProjectConfigManager, ConfigValidator } from "../config"; 10 | import { EnvironmentUtils } from "../utils"; 11 | import { SettingsManager } from "../config/settings-manager"; 12 | 13 | export async function updateCommand( 14 | config: GlobalConfig, 15 | context: vscode.ExtensionContext 16 | ): Promise { 17 | // Check manifest version and setup state 18 | const validationResult = await ConfigValidator.validateSetupState(config, context, false); 19 | if (!validationResult.isValid) { 20 | vscode.window.showErrorMessage(validationResult.error!); 21 | return; 22 | } 23 | 24 | const project = await ProjectConfigManager.load(context); 25 | const rootPaths = vscode.workspace.workspaceFolders; 26 | 27 | if (!rootPaths) { 28 | vscode.window.showErrorMessage('No workspace folder found.'); 29 | return; 30 | } 31 | 32 | const options: vscode.ShellExecutionOptions = { 33 | env: EnvironmentUtils.normalizeEnvironment(SettingsManager.buildEnvironmentForExecution()), 34 | cwd: rootPaths[0].uri.fsPath, 35 | }; 36 | 37 | const taskName = "Zephyr Tools: Update Dependencies"; 38 | const cmd = "west update"; 39 | const exec = new vscode.ShellExecution(cmd, options); 40 | 41 | const task = new vscode.Task( 42 | { type: "zephyr-tools", command: taskName }, 43 | vscode.TaskScope.Workspace, 44 | taskName, 45 | "zephyr-tools", 46 | exec, 47 | ); 48 | 49 | try { 50 | await vscode.tasks.executeTask(task); 51 | vscode.window.showInformationMessage('Updating dependencies for project.'); 52 | } catch (error) { 53 | vscode.window.showErrorMessage(`Update failed: ${error}`); 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/ui/quick-picks.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @file quick-picks.ts 3 | * Provides an interface to handle quickpick dialogs. 4 | * 5 | * @license Apache-2.0 6 | */ 7 | 8 | import * as vscode from 'vscode'; 9 | 10 | export class QuickPickManager { 11 | static async selectBoard(boards: string[]): Promise { 12 | // Add custom board option at the beginning 13 | const CUSTOM_BOARD_OPTION = "$(edit) Enter custom board..."; 14 | const boardOptions = [CUSTOM_BOARD_OPTION, ...boards]; 15 | 16 | const selected = await vscode.window.showQuickPick(boardOptions, { 17 | placeHolder: "Select a board or enter custom", 18 | ignoreFocusOut: true, 19 | }); 20 | 21 | if (selected === CUSTOM_BOARD_OPTION) { 22 | // Show input box for custom board 23 | return await vscode.window.showInputBox({ 24 | prompt: "Enter custom board identifier (e.g., stm32h747i_disco/stm32h747xx/m4)", 25 | placeHolder: "board/variant/core", 26 | ignoreFocusOut: true, 27 | validateInput: (value) => { 28 | if (!value || value.trim().length === 0) { 29 | return "Board identifier cannot be empty"; 30 | } 31 | return null; 32 | } 33 | }); 34 | } 35 | 36 | return selected; 37 | } 38 | 39 | static async selectProject(projects: string[]): Promise { 40 | return await vscode.window.showQuickPick(projects, { 41 | placeHolder: "Select a project", 42 | ignoreFocusOut: true, 43 | }); 44 | } 45 | 46 | static async selectToolchain(toolchains: string[]): Promise { 47 | return await vscode.window.showQuickPick(toolchains, { 48 | placeHolder: "Select a toolchain", 49 | ignoreFocusOut: true, 50 | }); 51 | } 52 | 53 | static async selectRunner(runners: string[]): Promise { 54 | return await vscode.window.showQuickPick(runners, { 55 | placeHolder: "Select a runner", 56 | ignoreFocusOut: true, 57 | }); 58 | } 59 | } 60 | 61 | -------------------------------------------------------------------------------- /src/utils/environment-utils.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @author Jared Wolff 3 | * @copyright Circuit Dojo LLC 4 | * @license Apache 2.0 5 | */ 6 | 7 | /** 8 | * Utility class for handling environment variable normalization across platforms 9 | */ 10 | export class EnvironmentUtils { 11 | /** 12 | * Normalizes environment variables for cross-platform compatibility. 13 | * Ensures PATH is properly set on Windows where it might be 'Path' or 'path'. 14 | * 15 | * @param env - The environment object to normalize 16 | * @returns Normalized environment with consistent PATH variable 17 | */ 18 | static normalizeEnvironment(env: NodeJS.ProcessEnv): { [key: string]: string } { 19 | const normalized = { ...env }; 20 | 21 | // Handle Windows PATH case sensitivity 22 | // Windows may use 'Path' while Unix systems use 'PATH' 23 | if (!normalized.PATH && (normalized.Path || normalized.path)) { 24 | console.log(`[ENV] Normalizing PATH from ${normalized.Path ? 'Path' : 'path'} to PATH`); 25 | normalized.PATH = normalized.Path || normalized.path || ""; 26 | 27 | // Clean up the old entries to avoid confusion 28 | delete normalized.Path; 29 | delete normalized.path; 30 | } 31 | 32 | return normalized as { [key: string]: string }; 33 | } 34 | 35 | /** 36 | * Gets normalized system environment with proper PATH handling. 37 | * This should be used when initializing configuration from system environment. 38 | * 39 | * @returns Normalized system environment variables 40 | */ 41 | static getSystemEnvironment(): { [key: string]: string } { 42 | return this.normalizeEnvironment(process.env); 43 | } 44 | 45 | /** 46 | * Creates shell execution options with normalized environment. 47 | * Convenience method for commands that need to execute shell processes. 48 | * 49 | * @param env - Environment to normalize 50 | * @param cwd - Working directory for the shell execution 51 | * @returns Shell execution options with normalized environment 52 | */ 53 | static createShellOptions(env: NodeJS.ProcessEnv, cwd?: string): { env: { [key: string]: string }, cwd?: string } { 54 | const normalizedEnv = this.normalizeEnvironment(env); 55 | const options: { env: { [key: string]: string }, cwd?: string } = { 56 | env: normalizedEnv 57 | }; 58 | 59 | if (cwd) { 60 | options.cwd = cwd; 61 | } 62 | 63 | return options; 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /src/hardware/serial-port-manager.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @author Jared Wolff 3 | * @copyright Circuit Dojo LLC 4 | * @license Apache 2.0 5 | */ 6 | 7 | import * as vscode from "vscode"; 8 | import * as util from "util"; 9 | import * as cp from "child_process"; 10 | import { BAUD_LIST } from "../config"; 11 | import { DialogManager } from "../ui"; 12 | import { GlobalConfig } from "../types"; 13 | import { SettingsManager } from "../config/settings-manager"; 14 | 15 | export class SerialPortManager { 16 | static async getAvailablePorts(config: GlobalConfig): Promise { 17 | const exec = util.promisify(cp.exec); 18 | 19 | try { 20 | // Get list of ports using zephyr-tools 21 | const cmd = "zephyr-tools -l"; 22 | const result = await exec(cmd, { env: SettingsManager.buildEnvironmentForExecution() }); 23 | 24 | if (result.stderr) { 25 | console.error("Error getting ports:", result.stderr); 26 | return []; 27 | } 28 | 29 | const ports = JSON.parse(result.stdout); 30 | return Array.isArray(ports) ? ports : []; 31 | } catch (error) { 32 | console.error("Failed to get available ports:", error); 33 | return []; 34 | } 35 | } 36 | 37 | static async selectPort(config: GlobalConfig): Promise { 38 | // Check if setup has been run 39 | if (!config.isSetup) { 40 | vscode.window.showErrorMessage("Please run 'Zephyr Tools: Setup' command before selecting a serial port."); 41 | return undefined; 42 | } 43 | 44 | const ports = await this.getAvailablePorts(config); 45 | 46 | if (ports.length === 0) { 47 | vscode.window.showErrorMessage("No serial ports found. Make sure the zephyr-tools CLI is properly installed and in your PATH."); 48 | return undefined; 49 | } 50 | 51 | const selectedPort = await DialogManager.selectSerialPort(ports); 52 | 53 | if (!selectedPort) { 54 | vscode.window.showErrorMessage("Invalid port choice."); 55 | return undefined; 56 | } 57 | 58 | return selectedPort; 59 | } 60 | 61 | static async selectBaudRate(defaultBaud?: string): Promise { 62 | const selectedBaud = await DialogManager.selectBaudRate(BAUD_LIST, defaultBaud); 63 | 64 | if (!selectedBaud || selectedBaud === "") { 65 | vscode.window.showErrorMessage("Invalid baud rate choice."); 66 | return undefined; 67 | } 68 | 69 | return selectedBaud; 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /AGENTS.md: -------------------------------------------------------------------------------- 1 | # Repository Guidelines 2 | 3 | ## Project Structure & Module Organization 4 | - `src/` — VS Code extension source (TypeScript). 5 | - `commands/`, `config/`, `hardware/`, `ui/`, `build/`, `files/`, `tasks/`, `utils/`. 6 | - `extension.ts` is the entrypoint; output bundles to `out/` via esbuild. 7 | - `src/test/` — Mocha tests using `vscode-test`. 8 | - `templates/` — Project templates (vanilla, nfed, ncs). 9 | - `media/`, `icons/`, `img/` — Webview assets and icons. 10 | - `manifest/` — Extension manifest resources. 11 | 12 | ## Build, Test, and Development Commands 13 | - `npm run watch` — Incremental build with esbuild (dev loop). 14 | - `npm run compile` — One-shot build to `out/extension.js`. 15 | - `npm run lint` — ESLint on `src/**/*.ts`. 16 | - `npm test` — Builds, then runs Mocha tests via `vscode-test`. 17 | - `npm run package` — Type-check, then production bundle (used for publishing). 18 | 19 | Tip: Open in VS Code and press F5 to launch the Extension Development Host. 20 | 21 | ## Coding Style & Naming Conventions 22 | - Language: TypeScript (ES6 target, CommonJS). `tsconfig` uses `strict: true`. 23 | - Indentation: 2 spaces. Prefer explicit types and early returns. 24 | - Names: `PascalCase` for classes, `camelCase` for functions/vars, `SCREAMING_SNAKE_CASE` for constants. 25 | - Linting: ESLint with `@typescript-eslint`. Fix issues or justify with minimal disables. 26 | 27 | ## Testing Guidelines 28 | - Framework: Mocha (`tdd` UI) with `vscode-test` harness. 29 | - Location: `src/test/suite/*.test.ts` (compiled to `.js`). 30 | - Run: `npm test`. Keep tests fast and deterministic; prefer unit-level coverage for commands and utilities. 31 | 32 | ## Commit & Pull Request Guidelines 33 | - Commits: Imperative subject, concise body with rationale. Reference issues (e.g., `Fixes #123`). 34 | - PRs: Include summary, testing steps, and screenshots for UI changes (webviews/status bar). Update docs (`README.md`, plans) when behavior changes. 35 | - CI hygiene: Ensure `npm run lint` and `npm test` pass before requesting review. 36 | 37 | ## Security & Configuration Tips 38 | - Node >= 16 and VS Code >= 1.101.0. 39 | - Extension settings live under `zephyr-tools.*` (e.g., `paths.zephyrBase`, `probeRs.*`). Avoid committing machine-specific paths. 40 | 41 | ## Agent-Specific Instructions 42 | - Respect this file’s scope for repository-wide conventions. 43 | - Prefer minimal, focused patches; avoid unrelated refactors. 44 | - When adding commands or settings, wire them in `package.json` and keep names consistent (`zephyr-tools.*`). 45 | -------------------------------------------------------------------------------- /src/commands/terminal.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @author Jared Wolff 3 | * @copyright Circuit Dojo LLC 4 | * @license Apache 2.0 5 | */ 6 | 7 | import * as vscode from "vscode"; 8 | import * as path from "path"; 9 | import { GlobalConfig } from "../types"; 10 | import { getPlatformConfig, SettingsManager } from "../config"; 11 | 12 | export async function openZephyrTerminalCommand( 13 | config: GlobalConfig, 14 | context: vscode.ExtensionContext 15 | ): Promise { 16 | if (!config.isSetup) { 17 | vscode.window.showErrorMessage("Run `Zephyr Tools: Setup` command first."); 18 | return; 19 | } 20 | 21 | const pythonenv = path.join(SettingsManager.getToolsDirectory(), "env"); 22 | const platformConfig = getPlatformConfig(); 23 | const pathDivider = platformConfig.pathDivider; 24 | 25 | // Start with system environment (including system PATH) 26 | const terminalEnv: { [key: string]: string } = {}; 27 | for (const [key, value] of Object.entries(process.env)) { 28 | if (value !== undefined) { 29 | terminalEnv[key] = value; 30 | } 31 | } 32 | 33 | // Add all configured environment variables from settings 34 | const envVars = SettingsManager.getEnvironmentVariables(); 35 | for (const [key, value] of Object.entries(envVars)) { 36 | if (value) { 37 | terminalEnv[key] = value; 38 | } 39 | } 40 | 41 | // Set VIRTUAL_ENV path 42 | terminalEnv["VIRTUAL_ENV"] = pythonenv; 43 | 44 | // Get all configured paths from settings 45 | const allPaths = SettingsManager.getAllPaths(); 46 | 47 | // Build the complete PATH by prepending all tool paths 48 | let pathComponents: string[] = []; 49 | 50 | // Add Python environment paths first 51 | pathComponents.push(path.join(pythonenv, "Scripts")); 52 | pathComponents.push(path.join(pythonenv, "bin")); 53 | 54 | // Add all saved tool paths 55 | pathComponents = pathComponents.concat(allPaths); 56 | 57 | // Add the existing PATH from environment 58 | if (terminalEnv["PATH"]) { 59 | pathComponents.push(terminalEnv["PATH"]); 60 | } 61 | 62 | // Join all path components 63 | terminalEnv["PATH"] = pathComponents.filter(p => p).join(pathDivider); 64 | 65 | // Create terminal with the configured environment 66 | const terminal = vscode.window.createTerminal({ 67 | name: "Zephyr Terminal", 68 | env: terminalEnv, 69 | cwd: vscode.workspace.workspaceFolders?.[0]?.uri.fsPath 70 | }); 71 | 72 | // Show the terminal 73 | terminal.show(); 74 | 75 | // Show success notification instead of echo 76 | vscode.window.showInformationMessage("Zephyr environment activated"); 77 | } -------------------------------------------------------------------------------- /src/ui/status-bar.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @author Jared Wolff 3 | * @copyright Circuit Dojo LLC 4 | * @license Apache 2.0 5 | */ 6 | 7 | import * as vscode from "vscode"; 8 | import * as path from "path"; 9 | 10 | export class StatusBarManager { 11 | private static boardStatusBarItem: vscode.StatusBarItem; 12 | private static projectStatusBarItem: vscode.StatusBarItem; 13 | 14 | static initializeStatusBarItems(context: vscode.ExtensionContext): void { 15 | // Create board status bar item with higher priority for more space 16 | this.boardStatusBarItem = vscode.window.createStatusBarItem(vscode.StatusBarAlignment.Right, 200); 17 | this.boardStatusBarItem.command = "zephyr-tools.change-board"; 18 | this.boardStatusBarItem.text = "$(circuit-board) No Board"; 19 | this.boardStatusBarItem.tooltip = "Click to change board"; 20 | this.boardStatusBarItem.show(); 21 | context.subscriptions.push(this.boardStatusBarItem); 22 | 23 | // Create project status bar item 24 | this.projectStatusBarItem = vscode.window.createStatusBarItem(vscode.StatusBarAlignment.Right, 99); 25 | this.projectStatusBarItem.command = "zephyr-tools.change-project"; 26 | this.projectStatusBarItem.text = "$(folder) No Project"; 27 | this.projectStatusBarItem.tooltip = "Click to change project"; 28 | this.projectStatusBarItem.show(); 29 | context.subscriptions.push(this.projectStatusBarItem); 30 | } 31 | 32 | static updateBoardStatusBar(board?: string): void { 33 | if (this.boardStatusBarItem) { 34 | const displayBoard = board ? this.truncateText(board, 40) : "No Board"; 35 | this.boardStatusBarItem.text = `$(circuit-board) ${displayBoard}`; 36 | this.boardStatusBarItem.tooltip = board 37 | ? `Board: ${board}\nClick to change board` 38 | : "Click to select a board"; 39 | } 40 | } 41 | 42 | static updateProjectStatusBar(project?: string): void { 43 | if (this.projectStatusBarItem) { 44 | let displayProject = "No Project"; 45 | 46 | if (project) { 47 | // Extract just the directory name from the full path for display 48 | displayProject = this.truncateText(path.basename(project), 25); 49 | } 50 | 51 | this.projectStatusBarItem.text = `$(folder) ${displayProject}`; 52 | this.projectStatusBarItem.tooltip = project 53 | ? `Project: ${project}\nClick to change project` 54 | : "Click to select a project"; 55 | } 56 | } 57 | 58 | private static truncateText(text: string, maxLength: number): string { 59 | if (text.length <= maxLength) { 60 | return text; 61 | } 62 | return text.substring(0, maxLength - 3) + "..."; 63 | } 64 | 65 | static dispose(): void { 66 | if (this.boardStatusBarItem) { 67 | this.boardStatusBarItem.dispose(); 68 | } 69 | if (this.projectStatusBarItem) { 70 | this.projectStatusBarItem.dispose(); 71 | } 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /src/commands/path-management.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @author Jared Wolff 3 | * @copyright Circuit Dojo LLC 4 | * @license Apache 2.0 5 | */ 6 | 7 | import * as vscode from "vscode"; 8 | import * as path from "path"; 9 | import * as fs from "fs-extra"; 10 | import { SettingsManager } from "../config"; 11 | import { GlobalConfig } from "../types"; 12 | 13 | export async function populateDetectedPaths(config: GlobalConfig): Promise { 14 | try { 15 | const toolsDir = SettingsManager.getToolsDirectory(); 16 | 17 | // Detect and save Python executable if not set 18 | if (!SettingsManager.getPythonExecutable()) { 19 | const envBinPath = path.join(toolsDir, "env", process.platform === "win32" ? "Scripts" : "bin"); 20 | const pythonPath = path.join(envBinPath, process.platform === "win32" ? "python.exe" : "python"); 21 | if (await fs.pathExists(pythonPath)) { 22 | await SettingsManager.setPythonExecutable(pythonPath); 23 | } 24 | } 25 | 26 | // Detect and save West executable if not set 27 | if (!SettingsManager.getWestExecutable()) { 28 | const envBinPath = path.join(toolsDir, "env", process.platform === "win32" ? "Scripts" : "bin"); 29 | const westPath = path.join(envBinPath, process.platform === "win32" ? "west.exe" : "west"); 30 | if (await fs.pathExists(westPath)) { 31 | await SettingsManager.setWestExecutable(westPath); 32 | } 33 | } 34 | 35 | // Detect and save ZEPHYR_BASE if not set 36 | if (!SettingsManager.getZephyrBase()) { 37 | // Try to detect from workspace 38 | const detectedBase = await SettingsManager.detectZephyrBase(); 39 | if (detectedBase) { 40 | await SettingsManager.setZephyrBase(detectedBase); 41 | await SettingsManager.setEnvironmentVariable("ZEPHYR_BASE", detectedBase); 42 | } 43 | } 44 | 45 | vscode.window.showInformationMessage("Path settings have been populated with detected values."); 46 | } catch (error) { 47 | vscode.window.showErrorMessage(`Failed to populate paths: ${error}`); 48 | } 49 | } 50 | 51 | export async function resetPaths(): Promise { 52 | const result = await vscode.window.showWarningMessage( 53 | "This will reset all custom path configurations to defaults. Continue?", 54 | { modal: true }, 55 | "Reset", 56 | "Cancel" 57 | ); 58 | 59 | if (result === "Reset") { 60 | try { 61 | // Clear all path settings 62 | await SettingsManager.setPythonExecutable(""); 63 | await SettingsManager.setWestExecutable(""); 64 | await SettingsManager.setZephyrBase(""); 65 | await SettingsManager.setAllPaths([]); 66 | await SettingsManager.setToolsDirectory(""); 67 | 68 | // Paths have been reset 69 | 70 | vscode.window.showInformationMessage("All path configurations have been reset to defaults."); 71 | } catch (error) { 72 | vscode.window.showErrorMessage(`Failed to reset paths: ${error}`); 73 | } 74 | } 75 | } -------------------------------------------------------------------------------- /src/test/suite/extension.test.ts: -------------------------------------------------------------------------------- 1 | import * as assert from "assert"; 2 | import * as path from "path"; 3 | import * as os from "os"; 4 | 5 | // You can import and use all API from the 'vscode' module 6 | // as well as import your extension to test it 7 | import * as vscode from "vscode"; 8 | // import * as myExtension from '../../extension'; 9 | 10 | suite("Extension Test Suite", () => { 11 | vscode.window.showInformationMessage("Start all tests."); 12 | 13 | test("Sample test", () => { 14 | assert.strictEqual(-1, [1, 2, 3].indexOf(5)); 15 | assert.strictEqual(-1, [1, 2, 3].indexOf(0)); 16 | }); 17 | 18 | test("PATH environment variable handling", () => { 19 | // Test path divider detection 20 | const isWindows = os.platform() === "win32"; 21 | const expectedDivider = isWindows ? ";" : ":"; 22 | 23 | // Mock system PATH 24 | const mockSystemPath = isWindows ? "C:\\Windows\\System32;C:\\Windows" : "/usr/bin:/bin"; 25 | 26 | // Test path extraction logic 27 | const testConfigPath = isWindows 28 | ? "C:\\tools\\python\\Scripts;C:\\tools\\python\\bin;C:\\Windows\\System32;C:\\Windows" 29 | : "/home/user/.local/bin:/usr/local/bin:/usr/bin:/bin"; 30 | 31 | // Extract added paths (simulate the logic from extension.ts) 32 | const configPath: string = testConfigPath; 33 | const systemPath: string = mockSystemPath; 34 | if (configPath !== systemPath && configPath.length > systemPath.length) { 35 | const pathDividerIndex = configPath.lastIndexOf(systemPath); 36 | if (pathDividerIndex > 0) { 37 | const addedPaths = configPath.substring(0, pathDividerIndex); 38 | const cleanAddedPaths = addedPaths.endsWith(expectedDivider) 39 | ? addedPaths.substring(0, addedPaths.length - expectedDivider.length) 40 | : addedPaths; 41 | 42 | const individualPaths = cleanAddedPaths.split(expectedDivider).filter(p => p.trim()); 43 | 44 | // Verify we extracted the correct paths 45 | if (isWindows) { 46 | assert.strictEqual(individualPaths.length, 2); 47 | assert.strictEqual(individualPaths[0], "C:\\tools\\python\\Scripts"); 48 | assert.strictEqual(individualPaths[1], "C:\\tools\\python\\bin"); 49 | } else { 50 | assert.strictEqual(individualPaths.length, 2); 51 | assert.strictEqual(individualPaths[0], "/home/user/.local/bin"); 52 | assert.strictEqual(individualPaths[1], "/usr/local/bin"); 53 | } 54 | } 55 | } 56 | }); 57 | 58 | test("PATH prepend order preservation", () => { 59 | // Test that when we prepend multiple paths, they maintain correct order 60 | const paths = ["path1", "path2", "path3"]; 61 | const divider = os.platform() === "win32" ? ";" : ":"; 62 | 63 | // When prepending in reverse order (as our code does), the final order should be correct 64 | const reversedPaths = [...paths].reverse(); 65 | let result = ""; 66 | 67 | for (const pathToAdd of reversedPaths) { 68 | result = pathToAdd + divider + result; 69 | } 70 | 71 | // Remove trailing divider 72 | result = result.endsWith(divider) ? result.slice(0, -1) : result; 73 | 74 | assert.strictEqual(result, paths.join(divider)); 75 | }); 76 | }); 77 | -------------------------------------------------------------------------------- /src/hardware/newtmgr-manager.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @author Jared Wolff 3 | * @copyright Circuit Dojo LLC 4 | * @license Apache 2.0 5 | */ 6 | 7 | import * as util from "util"; 8 | import * as cp from "child_process"; 9 | import { GlobalConfig } from "../types"; 10 | import { OutputChannelManager } from "../ui"; 11 | import { PlatformUtils } from "../utils"; 12 | import { SettingsManager } from "../config/settings-manager"; 13 | 14 | /** 15 | * Manages newtmgr connection profiles and operations 16 | */ 17 | export class NewtmgrManager { 18 | private static readonly PROFILE_NAME = "vscode-zephyr-tools"; 19 | 20 | /** 21 | * Sets up a newtmgr serial connection profile 22 | */ 23 | static async setupConnection( 24 | config: GlobalConfig, 25 | port: string, 26 | baud: string 27 | ): Promise { 28 | const exec = util.promisify(cp.exec); 29 | 30 | try { 31 | const tools = PlatformUtils.getToolExecutables(); 32 | const cmd = `${tools.newtmgr} conn add ${this.PROFILE_NAME} type=serial connstring="dev=${port},baud=${baud}"`; 33 | const result = await exec(cmd, { env: SettingsManager.buildEnvironmentForExecution() }); 34 | 35 | if (result.stderr) { 36 | const output = OutputChannelManager.getChannel(); 37 | output.append(result.stderr); 38 | output.show(); 39 | return false; 40 | } 41 | 42 | return true; 43 | } catch (error) { 44 | console.error("Newtmgr setup connection error:", error); 45 | return false; 46 | } 47 | } 48 | 49 | /** 50 | * Verifies that the newtmgr connection profile exists 51 | */ 52 | static async verifyConnection(config: GlobalConfig): Promise { 53 | const exec = util.promisify(cp.exec); 54 | 55 | try { 56 | const tools = PlatformUtils.getToolExecutables(); 57 | const cmd = `${tools.newtmgr} conn show`; 58 | const result = await exec(cmd, { env: SettingsManager.buildEnvironmentForExecution() }); 59 | 60 | if (result.stderr) { 61 | const output = OutputChannelManager.getChannel(); 62 | output.append(result.stderr); 63 | output.show(); 64 | return false; 65 | } 66 | 67 | return result.stdout.includes(this.PROFILE_NAME); 68 | } catch (error) { 69 | console.error("Newtmgr verify connection error:", error); 70 | return false; 71 | } 72 | } 73 | 74 | /** 75 | * Checks if the vscode-zephyr-tools profile is configured 76 | */ 77 | static async isProfileConfigured(config: GlobalConfig): Promise { 78 | return this.verifyConnection(config); 79 | } 80 | 81 | /** 82 | * Checks if newtmgr is installed and available 83 | */ 84 | static async isInstalled(config: GlobalConfig): Promise { 85 | const exec = util.promisify(cp.exec); 86 | 87 | try { 88 | const tools = PlatformUtils.getToolExecutables(); 89 | await exec(`${tools.newtmgr} version`, { env: SettingsManager.buildEnvironmentForExecution() }); 90 | return true; 91 | } catch (error) { 92 | return false; 93 | } 94 | } 95 | 96 | /** 97 | * Gets the profile name used by the extension 98 | */ 99 | static getProfileName(): string { 100 | return this.PROFILE_NAME; 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /src/ui/dialogs.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @author Jared Wolff 3 | * @copyright Circuit Dojo LLC 4 | * @license Apache 2.0 5 | */ 6 | 7 | import * as vscode from "vscode"; 8 | 9 | export class DialogManager { 10 | static async getRepositoryUrl(): Promise { 11 | const inputOptions: vscode.InputBoxOptions = { 12 | prompt: "Enter git repository URL.", 13 | placeHolder: "", 14 | ignoreFocusOut: true, 15 | validateInput: text => { 16 | return text !== undefined && text !== "" ? null : "Enter a valid git repository address."; 17 | }, 18 | }; 19 | 20 | return await vscode.window.showInputBox(inputOptions); 21 | } 22 | 23 | static async getBranchName(): Promise { 24 | const branchInputOptions: vscode.InputBoxOptions = { 25 | prompt: "Enter branch name.", 26 | placeHolder: "Press enter for default", 27 | ignoreFocusOut: true, 28 | }; 29 | 30 | return await vscode.window.showInputBox(branchInputOptions); 31 | } 32 | 33 | static async getDestinationFolder(): Promise { 34 | const dialogOptions: vscode.OpenDialogOptions = { 35 | canSelectFiles: false, 36 | canSelectFolders: true, 37 | title: "Select destination folder." 38 | }; 39 | 40 | const result = await vscode.window.showOpenDialog(dialogOptions); 41 | return result?.[0]; 42 | } 43 | 44 | /** 45 | * Gets destination folder, prompting user if not provided 46 | * This replicates the behavior of the old helper.get_dest function 47 | */ 48 | static async getDestination(dest?: vscode.Uri): Promise { 49 | // If destination is provided, use it 50 | if (dest) { 51 | return dest; 52 | } 53 | 54 | // If not provided, prompt user to select folder 55 | const dialogOptions: vscode.OpenDialogOptions = { 56 | canSelectFiles: false, 57 | canSelectFolders: true, 58 | title: "Select destination folder." 59 | }; 60 | 61 | const result = await vscode.window.showOpenDialog(dialogOptions); 62 | if (!result) { 63 | vscode.window.showErrorMessage('Provide a target folder.'); 64 | return null; 65 | } 66 | 67 | return result[0]; 68 | } 69 | 70 | static async getRunnerArguments(): Promise { 71 | return await vscode.window.showInputBox({ 72 | placeHolder: "Enter runner args..", 73 | ignoreFocusOut: true, 74 | }); 75 | } 76 | 77 | static async selectSysBuildOption(): Promise { 78 | const result = await vscode.window.showQuickPick(["Yes", "No"], { 79 | placeHolder: "Enable sysbuild?", 80 | ignoreFocusOut: true, 81 | }); 82 | 83 | if (result === "Yes") return true; 84 | if (result === "No") return false; 85 | return undefined; 86 | } 87 | 88 | static async selectSerialPort(ports: string[]): Promise { 89 | return await vscode.window.showQuickPick(ports, { 90 | title: "Pick your serial port.", 91 | placeHolder: ports[0], 92 | ignoreFocusOut: true, 93 | }); 94 | } 95 | 96 | static async selectBaudRate(baudList: string[], defaultBaud?: string): Promise { 97 | const result = await vscode.window.showQuickPick(baudList, { 98 | title: "Pick your baud rate.", 99 | placeHolder: defaultBaud || baudList[0], 100 | ignoreFocusOut: true, 101 | }); 102 | 103 | return result || defaultBaud; 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /src/environment/dependency-installer.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @author Jared Wolff 3 | * @copyright Circuit Dojo LLC 4 | * @license Apache 2.0 5 | */ 6 | 7 | import * as vscode from "vscode"; 8 | import * as util from "util"; 9 | import * as cp from "child_process"; 10 | import * as path from "path"; 11 | import { platform, getPlatformConfig, SettingsManager } from "../config"; 12 | 13 | export async function createVirtualEnvironment(pythonCmd: string, env: { [key: string]: string | undefined }, output: vscode.OutputChannel): Promise { 14 | const exec = util.promisify(cp.exec); 15 | const currentToolsDir = SettingsManager.getToolsDirectory(); 16 | const pythonenv = path.join(currentToolsDir, "env"); 17 | 18 | try { 19 | const cmd = `${pythonCmd} -m venv "${pythonenv}"`; 20 | output.appendLine(cmd); 21 | const result = await exec(cmd, { env }); 22 | output.append(result.stdout); 23 | output.appendLine("[SETUP] virtual python environment created"); 24 | return true; 25 | } catch (error) { 26 | output.appendLine("[SETUP] unable to setup virtualenv"); 27 | console.error(error); 28 | return false; 29 | } 30 | } 31 | 32 | export async function installWest(pythonCmd: string, env: { [key: string]: string | undefined }, output: vscode.OutputChannel): Promise { 33 | const exec = util.promisify(cp.exec); 34 | 35 | try { 36 | const result = await exec(`${pythonCmd} -m pip install west`, { env }); 37 | output.append(result.stdout); 38 | output.append(result.stderr); 39 | output.appendLine("[SETUP] west installed"); 40 | return true; 41 | } catch (error) { 42 | output.appendLine("[SETUP] unable to install west"); 43 | output.append(JSON.stringify(error)); 44 | return false; 45 | } 46 | } 47 | 48 | export async function setupVirtualEnvironmentPaths(env: { [key: string]: string | undefined }, context: vscode.ExtensionContext): Promise { 49 | const currentToolsDir = SettingsManager.getToolsDirectory(); 50 | const pythonenv = path.join(currentToolsDir, "env"); 51 | const platformConfig = getPlatformConfig(); 52 | const pathDivider = platformConfig.pathDivider; 53 | 54 | // Set VIRTUAL_ENV path otherwise we get terribly annoying errors setting up 55 | env["VIRTUAL_ENV"] = pythonenv; 56 | 57 | // Save VIRTUAL_ENV to settings 58 | await SettingsManager.setVirtualEnv(pythonenv); 59 | 60 | // Add Python paths to VS Code environment 61 | context.environmentVariableCollection.prepend("PATH", path.join(pythonenv, "Scripts") + pathDivider); 62 | context.environmentVariableCollection.prepend("PATH", path.join(pythonenv, "bin") + pathDivider); 63 | } 64 | 65 | export async function installPythonDependencies(pythonCmd: string, zephyrBasePath: string, env: { [key: string]: string | undefined }, output: vscode.OutputChannel): Promise { 66 | const exec = util.promisify(cp.exec); 67 | const currentToolsDir = SettingsManager.getToolsDirectory(); 68 | const pythonenv = path.join(currentToolsDir, "env"); 69 | 70 | const venvPython = platform === "win32" 71 | ? path.join(pythonenv, "Scripts", "python.exe") 72 | : path.join(pythonenv, "bin", "python"); 73 | 74 | const requirementsPath = path.join(zephyrBasePath, "scripts", "requirements.txt"); 75 | const cmd = `"${venvPython}" -m pip install -r ${requirementsPath}`; 76 | 77 | try { 78 | output.appendLine(`[INIT] Starting pip install: ${cmd}`); 79 | const result = await exec(cmd, { env }); 80 | output.append(result.stdout); 81 | output.append(result.stderr); 82 | output.appendLine("[INIT] Python dependencies installed"); 83 | return true; 84 | } catch (error) { 85 | output.appendLine("[INIT] Failed to install Python dependencies"); 86 | output.append(String(error)); 87 | return false; 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /src/commands/clean.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @author Jared Wolff 3 | * @copyright Circuit Dojo LLC 4 | * @license Apache 2.0 5 | */ 6 | 7 | import * as vscode from "vscode"; 8 | import * as fs from "fs-extra"; 9 | import * as path from "path"; 10 | import { GlobalConfig } from "../types"; 11 | import { ProjectConfigManager, ConfigValidator } from "../config"; 12 | import { DialogManager } from "../ui"; 13 | 14 | export async function cleanCommand( 15 | config: GlobalConfig, 16 | context: vscode.ExtensionContext 17 | ): Promise { 18 | // Check manifest version and setup state 19 | const validationResult = await ConfigValidator.validateSetupState(config, context, false); 20 | if (!validationResult.isValid) { 21 | vscode.window.showErrorMessage(validationResult.error!); 22 | return; 23 | } 24 | 25 | const project = await ProjectConfigManager.load(context); 26 | 27 | if (!project.target) { 28 | vscode.window.showErrorMessage('No project target set.'); 29 | return; 30 | } 31 | 32 | try { 33 | // Clean build directory for the specific board 34 | const buildPath = project.board 35 | ? path.join(project.target, 'build', project.board.split('/')[0]) 36 | : path.join(project.target, 'build'); 37 | 38 | await fs.remove(buildPath); 39 | vscode.window.showInformationMessage(`Cleaning ${project.target}`); 40 | } catch (error) { 41 | vscode.window.showErrorMessage('Failed to clean build directory.'); 42 | console.error('Clean error:', error); 43 | } 44 | } 45 | 46 | export async function cleanIncompleteProjectCommand( 47 | config: GlobalConfig, 48 | context: vscode.ExtensionContext 49 | ): Promise { 50 | const workspaceRoot = vscode.workspace.workspaceFolders?.[0]?.uri.fsPath; 51 | 52 | if (!workspaceRoot) { 53 | vscode.window.showErrorMessage('No workspace folder open.'); 54 | return; 55 | } 56 | 57 | // Check if .west folder exists 58 | const westPath = path.join(workspaceRoot, '.west'); 59 | const westExists = await fs.pathExists(westPath); 60 | 61 | if (!westExists) { 62 | vscode.window.showInformationMessage('No incomplete project found to clean.'); 63 | return; 64 | } 65 | 66 | // Show confirmation dialog 67 | const folderName = path.basename(workspaceRoot); 68 | const choice = await vscode.window.showWarningMessage( 69 | `This will delete all files in "${folderName}". This action cannot be undone.`, 70 | { modal: true }, 71 | "Delete All Files" 72 | ); 73 | 74 | if (choice !== "Delete All Files") { 75 | return; 76 | } 77 | 78 | try { 79 | // Show progress 80 | await vscode.window.withProgress({ 81 | location: vscode.ProgressLocation.Notification, 82 | title: "Cleaning incomplete project", 83 | cancellable: false 84 | }, async (progress) => { 85 | progress.report({ message: "Removing all files..." }); 86 | 87 | // Get all items in the workspace root 88 | const items = await fs.readdir(workspaceRoot); 89 | 90 | // Remove each item 91 | for (const item of items) { 92 | const itemPath = path.join(workspaceRoot, item); 93 | await fs.remove(itemPath); 94 | } 95 | 96 | // Reset project configuration 97 | const project = await ProjectConfigManager.load(context); 98 | project.isInit = false; 99 | project.isInitializing = false; 100 | await ProjectConfigManager.save(context, project); 101 | }); 102 | 103 | vscode.window.showInformationMessage('Incomplete project cleaned successfully. You can now start fresh.'); 104 | } catch (error) { 105 | vscode.window.showErrorMessage(`Failed to clean project: ${error}`); 106 | console.error('Clean incomplete project error:', error); 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /src/files/file-downloader.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @author Jared Wolff 3 | * @copyright Circuit Dojo LLC 4 | * @license Apache 2.0 5 | */ 6 | 7 | import * as path from 'path'; 8 | import * as fs from 'fs-extra'; 9 | import * as crypto from 'crypto'; 10 | import { HttpClient } from "typed-rest-client/HttpClient"; 11 | 12 | export class FileDownloader { 13 | private static downloadsdir: string = ""; 14 | 15 | // Set the download target directory 16 | public static init(dir: string) { 17 | this.downloadsdir = dir; 18 | } 19 | 20 | // Check if file exists 21 | public static async exists(file: string): Promise { 22 | const dest = path.join(this.downloadsdir, file); 23 | 24 | if (await fs.pathExists(dest)) { 25 | return dest; 26 | } else { 27 | return null; 28 | } 29 | } 30 | 31 | // Compares file with provided hash 32 | public static async check(file: string, hash: string): Promise { 33 | const dest = path.join(this.downloadsdir, file); 34 | 35 | // Check if exists first 36 | if (!await fs.pathExists(dest)) { 37 | console.log("doesn't exist! " + dest); 38 | return false; 39 | } 40 | 41 | // Get file contents 42 | const fileBuffer = fs.readFileSync(dest); 43 | 44 | // Create hash 45 | const hashSum = crypto.createHash('md5'); 46 | hashSum.update(fileBuffer); 47 | 48 | // Get hex representation 49 | const hex = hashSum.digest('hex'); 50 | 51 | return hex === hash; 52 | } 53 | 54 | // Delete files in download directory 55 | public static async clean() { 56 | await fs.remove(this.downloadsdir); 57 | } 58 | 59 | // Downloads file to filestore with progress reporting 60 | public static async downloadWithProgress( 61 | url: string, 62 | onProgress?: (progress: { percent: number; downloaded: number; total: number }) => void 63 | ): Promise { 64 | const client = new HttpClient("download"); 65 | const response = await client.get(url); 66 | 67 | // Get file name 68 | const filename = path.basename(url); 69 | 70 | // Determine dest 71 | const dest = path.join(this.downloadsdir, filename); 72 | 73 | // Make sure downloadsdir exists 74 | let exists = await fs.pathExists(this.downloadsdir); 75 | if (!exists) { 76 | console.log("downloadsdir not found"); 77 | await fs.mkdirp(this.downloadsdir); 78 | } 79 | 80 | if (response.message.statusCode !== 200) { 81 | const err: Error = new Error(`Unexpected HTTP response: ${response.message.statusCode}`); 82 | throw err; 83 | } 84 | 85 | return new Promise((resolve, reject) => { 86 | const file: NodeJS.WritableStream = fs.createWriteStream(dest); 87 | const contentLength = parseInt(response.message.headers['content-length'] as string || '0'); 88 | let downloaded = 0; 89 | 90 | file.on("error", (err) => reject(err)); 91 | 92 | response.message.on('data', (chunk) => { 93 | downloaded += chunk.length; 94 | if (onProgress && contentLength > 0) { 95 | onProgress({ 96 | percent: (downloaded / contentLength) * 100, 97 | downloaded, 98 | total: contentLength 99 | }); 100 | } 101 | }); 102 | 103 | const stream = response.message.pipe(file); 104 | stream.on("close", () => { 105 | try { 106 | resolve(dest); 107 | } catch (err) { 108 | reject(err); 109 | } 110 | }); 111 | }); 112 | } 113 | 114 | // Downloads file to filestore (backward compatibility) 115 | public static async fetch(url: string): Promise { 116 | return this.downloadWithProgress(url); 117 | } 118 | 119 | // Validate checksum of downloaded file 120 | public static async validateChecksum(file: string, expectedMd5: string): Promise { 121 | return this.check(file, expectedMd5); 122 | } 123 | } 124 | -------------------------------------------------------------------------------- /src/commands/create-project.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @author Jared Wolff 3 | * @copyright Circuit Dojo LLC 4 | * @license Apache 2.0 5 | */ 6 | 7 | import * as vscode from "vscode"; 8 | import * as fs from "fs-extra"; 9 | import * as path from "path"; 10 | import * as util from "util"; 11 | import * as cp from "child_process"; 12 | import { GlobalConfig } from "../types"; 13 | import { DialogManager } from "../ui"; 14 | import { initRepoCommand } from "./project-management"; 15 | import { SettingsManager } from "../config/settings-manager"; 16 | 17 | async function copyFilesRecursively(source: string, destination: string) { 18 | const files = fs.readdirSync(source); 19 | for (const file of files) { 20 | const sourcePath = path.join(source, file); 21 | const destinationPath = path.join(destination, file); 22 | console.log("target: " + destinationPath); 23 | const stats = fs.statSync(sourcePath); 24 | if (stats.isDirectory()) { 25 | console.log("making dir: " + destinationPath); 26 | 27 | let exists = await fs.pathExists(destinationPath); 28 | if (!exists) { 29 | fs.mkdirSync(destinationPath); 30 | } 31 | 32 | await copyFilesRecursively(sourcePath, destinationPath); 33 | } else if (!fs.existsSync(destinationPath)) { 34 | console.log("copying file: " + destinationPath); 35 | const contents = fs.readFileSync(sourcePath, "utf8"); 36 | fs.writeFileSync(destinationPath, contents, "utf8"); 37 | } 38 | } 39 | } 40 | 41 | export async function createProjectCommand( 42 | context: vscode.ExtensionContext, 43 | config: GlobalConfig, 44 | _dest: vscode.Uri | undefined 45 | ): Promise { 46 | // Get destination, prompting user if not provided (same behavior as init-repo) 47 | const dest = await DialogManager.getDestination(_dest); 48 | if (!dest) { 49 | // Error message already shown by getDestination if user cancelled 50 | return; 51 | } 52 | 53 | // Check if .west folder exists in destination - project must be in clean/empty folder 54 | const westFolderPath = path.join(dest.fsPath, ".west"); 55 | const westFolderExists = await fs.pathExists(westFolderPath); 56 | if (westFolderExists) { 57 | vscode.window.showErrorMessage("Cannot create project: .west folder found. Target folder must be clean/empty."); 58 | return; 59 | } 60 | 61 | // Merge path 62 | const appDest = path.join(dest.fsPath, "app"); 63 | 64 | console.log("dest: " + appDest); 65 | 66 | // Create app folder 67 | const exists = await fs.pathExists(appDest); 68 | if (!exists) { 69 | console.log(`${appDest} not found`); 70 | await fs.mkdirp(appDest); 71 | } 72 | 73 | // Popup asking for which SDK (vanilla vs NCS vs NFED) 74 | const choices = ["Vanilla", "NRF Connect SDK", "NFED (Circuit Dojo Boards)"]; 75 | const templates = ["vanilla", "ncs", "nfed"]; 76 | const sdk = await vscode.window.showQuickPick(choices, { 77 | title: "Pick your Zephyr SDK variant.", 78 | placeHolder: choices[0], 79 | ignoreFocusOut: true, 80 | }) ?? choices[0]; 81 | 82 | let templateSubPath = ""; 83 | for (let i = 0; i < choices.length; i++) { 84 | if (choices[i] === sdk) { 85 | templateSubPath = templates[i]; 86 | } 87 | } 88 | 89 | if (templateSubPath === "") { 90 | vscode.window.showErrorMessage("Invalid SDK choice."); 91 | return; 92 | } 93 | 94 | // Get the static files 95 | const extensionPath = context.extensionPath; 96 | await copyFilesRecursively(path.join(extensionPath, "templates", templateSubPath), appDest); 97 | 98 | // Promisified exec 99 | const exec = util.promisify(cp.exec); 100 | 101 | // Init git repo 102 | await exec("git init " + appDest, { env: SettingsManager.buildEnvironmentForExecution() }); 103 | 104 | // West init 105 | const initCmd = `west init -l ${appDest}`; 106 | await exec(initCmd, { env: SettingsManager.buildEnvironmentForExecution(), cwd: dest.fsPath }); 107 | 108 | console.log("init_cmd: " + initCmd); 109 | 110 | // Init the rest of the way 111 | await initRepoCommand(config, context, dest); 112 | } 113 | -------------------------------------------------------------------------------- /src/files/file-validator.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @author Jared Wolff 3 | * @copyright Circuit Dojo LLC 4 | * @license Apache 2.0 5 | */ 6 | 7 | import * as fs from "fs-extra"; 8 | import * as crypto from "crypto"; 9 | import * as path from "path"; 10 | 11 | export class FileValidator { 12 | static async validateMd5(filePath: string, expectedMd5: string): Promise { 13 | try { 14 | if (!(await fs.pathExists(filePath))) { 15 | console.log(`File doesn't exist: ${filePath}`); 16 | return false; 17 | } 18 | 19 | const fileBuffer = fs.readFileSync(filePath); 20 | const hashSum = crypto.createHash('md5'); 21 | hashSum.update(fileBuffer); 22 | const hex = hashSum.digest('hex'); 23 | 24 | const isValid = hex === expectedMd5; 25 | if (!isValid) { 26 | console.log(`MD5 mismatch for ${filePath}. Expected: ${expectedMd5}, Got: ${hex}`); 27 | } 28 | 29 | return isValid; 30 | } catch (error) { 31 | console.error(`MD5 validation error for ${filePath}:`, error); 32 | return false; 33 | } 34 | } 35 | 36 | static async validateSha256(filePath: string, expectedSha256: string): Promise { 37 | try { 38 | if (!(await fs.pathExists(filePath))) { 39 | console.log(`File doesn't exist: ${filePath}`); 40 | return false; 41 | } 42 | 43 | const fileBuffer = fs.readFileSync(filePath); 44 | const hashSum = crypto.createHash('sha256'); 45 | hashSum.update(fileBuffer); 46 | const hex = hashSum.digest('hex'); 47 | 48 | const isValid = hex === expectedSha256; 49 | if (!isValid) { 50 | console.log(`SHA256 mismatch for ${filePath}. Expected: ${expectedSha256}, Got: ${hex}`); 51 | } 52 | 53 | return isValid; 54 | } catch (error) { 55 | console.error(`SHA256 validation error for ${filePath}:`, error); 56 | return false; 57 | } 58 | } 59 | 60 | static async validateFileSize(filePath: string, expectedSize: number): Promise { 61 | try { 62 | if (!(await fs.pathExists(filePath))) { 63 | console.log(`File doesn't exist: ${filePath}`); 64 | return false; 65 | } 66 | 67 | const stats = await fs.stat(filePath); 68 | const isValid = stats.size === expectedSize; 69 | 70 | if (!isValid) { 71 | console.log(`File size mismatch for ${filePath}. Expected: ${expectedSize}, Got: ${stats.size}`); 72 | } 73 | 74 | return isValid; 75 | } catch (error) { 76 | console.error(`File size validation error for ${filePath}:`, error); 77 | return false; 78 | } 79 | } 80 | 81 | static async validateFileExists(filePath: string): Promise { 82 | try { 83 | return await fs.pathExists(filePath); 84 | } catch (error) { 85 | console.error(`File existence validation error for ${filePath}:`, error); 86 | return false; 87 | } 88 | } 89 | 90 | static async validateDirectory(dirPath: string): Promise { 91 | try { 92 | if (!(await fs.pathExists(dirPath))) { 93 | return false; 94 | } 95 | 96 | const stats = await fs.stat(dirPath); 97 | return stats.isDirectory(); 98 | } catch (error) { 99 | console.error(`Directory validation error for ${dirPath}:`, error); 100 | return false; 101 | } 102 | } 103 | 104 | static async validateDirectoryNotEmpty(dirPath: string): Promise { 105 | try { 106 | if (!(await this.validateDirectory(dirPath))) { 107 | return false; 108 | } 109 | 110 | const files = await fs.readdir(dirPath); 111 | return files.length > 0; 112 | } catch (error) { 113 | console.error(`Directory content validation error for ${dirPath}:`, error); 114 | return false; 115 | } 116 | } 117 | 118 | static getFileExtension(filePath: string): string { 119 | return path.extname(filePath).toLowerCase(); 120 | } 121 | 122 | static async validateFilePermissions(filePath: string, expectedMode: number): Promise { 123 | try { 124 | if (!(await fs.pathExists(filePath))) { 125 | return false; 126 | } 127 | 128 | const stats = await fs.stat(filePath); 129 | const actualMode = stats.mode & parseInt('777', 8); 130 | return actualMode === expectedMode; 131 | } catch (error) { 132 | console.error(`File permissions validation error for ${filePath}:`, error); 133 | return false; 134 | } 135 | } 136 | } 137 | -------------------------------------------------------------------------------- /debug-config-plan.md: -------------------------------------------------------------------------------- 1 | # Auto-Generate probe-rs Debug Configuration Plan 2 | 3 | ## Goal 4 | - Auto-generate a probe-rs debug configuration based on the selected project/board, and surface it in Run and Debug without manual `launch.json` edits. 5 | 6 | ## Inputs 7 | - `ProjectConfig` (`board`, `target`, `sysbuild`) from `ProjectConfigManager`. 8 | - probe-rs settings (`probeId`, `chipName`, `preverify`, `verify`) from `SettingsManager`. 9 | - Build output structure under `target/build//...`. 10 | - Optional: detected probe list and chip list via `ProbeManager`. 11 | 12 | ## User Flow 13 | - User selects project and board as usual. 14 | - On first build or explicit “Create Debug Configuration” command, the extension generates a probe-rs launch configuration. 15 | - The generated config appears in the debug dropdown; user can start a session immediately. 16 | - If chip or probe is unknown, prompt to select once and remember settings. 17 | 18 | ## ELF Discovery 19 | - Determine build dir: `target/build//`. 20 | - Preferred ELF: `build//zephyr/zephyr.elf`. 21 | - Fallbacks: 22 | - `build//app/zephyr/zephyr.elf` (app image in sysbuild). 23 | - Additional core images if detected (e.g., `spm/zephyr/zephyr.elf`, `tfm/zephyr/zephyr.elf`). 24 | - Resolve multicore by scanning for sibling `*/zephyr/zephyr.elf` under the same build directory; order by known core naming or heuristics. 25 | 26 | ## Chip and Probe Resolution 27 | - Use `SettingsManager.getProbeRsChipName()`; if unset, prompt with `ProbeManager.getProbeRsChipName()` to select and save. 28 | - If multiple probes: 29 | - Use saved `probeId` if still connected; else prompt via `ProbeManager.selectProbe()` and save. 30 | - If one probe connected: auto-select and save. 31 | - Respect `preverify`/`verify` flags if enabled. 32 | 33 | ## Launch Configuration Shape 34 | - Single-core: 35 | - `type: "probe-rs-debug"`, `request: "attach"`, `chip`, `coreConfigs[0].programBinary = `, `cwd = ${workspaceFolder}`, optional `speed = 4000`, `consoleLogLevel = "Console"`. 36 | - Multicore: 37 | - `coreConfigs` array with entries for each discovered ELF; set `coreIndex` deterministically (e.g., 0 = app, 1 = SPM/TF-M) and `programBinary` per core. 38 | - Naming: 39 | - `name = " • App"` or `"• App (Non-Secure)"` when applicable; include chip for clarity when helpful. 40 | 41 | ## Persistence Strategy 42 | - Prefer persistent configuration in `.vscode/launch.json`: 43 | - Create file if missing, merge if present. 44 | - Update or replace entries this extension owns (match by a stable `zephyrToolsId` field or name pattern). 45 | - Provide a user setting to opt into “ephemeral session” (don’t persist) as a phase-2 enhancement. 46 | 47 | ## Triggers to Generate/Update 48 | - On successful build completion. 49 | - On `ProjectConfigManager.onDidChangeConfig` when `board` or `target` changes. 50 | - On explicit command “Zephyr Tools: Create Debug Configuration”. 51 | - Optionally, on probe/chip setting changes. 52 | 53 | ## Safety and Merging Rules 54 | - Never overwrite user-defined non-Zephyr entries. 55 | - When updating, match on: 56 | - A hidden marker field (e.g., `zephyrToolsId`) or 57 | - Name pattern + `type: probe-rs-debug` within this workspace. 58 | - Keep an idempotent merge to avoid duplicate entries. 59 | 60 | ## Commands and UX 61 | - Add command: “Create Debug Configuration” (creates/updates and notifies). 62 | - Add command: “Debug App Now” (ensures config exists, then calls `vscode.debug.startDebugging` with it). 63 | - If required fields missing (chip/probe/ELF), show actionable prompts with clear next steps. 64 | 65 | ## Edge Cases 66 | - No build artifacts: prompt to build, offer to run “Build”. 67 | - Sysbuild off: expect ELF at `zephyr/zephyr.elf` only. 68 | - Different templates (vanilla/nfed/ncs): scan build tree rather than hardcode. 69 | - Multiple workspaces: scope to the active folder only. 70 | - Absent probe-rs VS Code debugger: warn and link to install. 71 | 72 | ## Validation 73 | - Test with: 74 | - nRF91x1 non-secure app only. 75 | - Sysbuild project with app + SPM/TF-M. 76 | - Single and multiple connected probes. 77 | - Verify launch dropdown displays exactly one up-to-date config per project/board. 78 | - Confirm session starts, attaches, and breaks at main. 79 | 80 | ## Documentation 81 | - Short section in README: 82 | - How generation works. 83 | - Where files live (`.vscode/launch.json`). 84 | - How to change chip/probe settings. 85 | - Troubleshooting missing ELF and multiple probes. 86 | 87 | ## Future Enhancements 88 | - Dynamic `DebugConfigurationProvider` to avoid writing files. 89 | - Board-to-chip auto-mapping from Zephyr board metadata. 90 | - Multi-target workspaces and multiple named configs. 91 | - Speed and RTT/semihosting settings per user preference. 92 | 93 | -------------------------------------------------------------------------------- /src/tasks/task-manager.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @author Jared Wolff 3 | * @copyright Circuit Dojo LLC 4 | * @license Apache 2.0 5 | */ 6 | 7 | import * as vscode from "vscode"; 8 | 9 | type TaskManagerCallback = (data: any) => void; 10 | 11 | export interface TaskManagerTaskOptions { 12 | errorMessage?: string; 13 | ignoreError: boolean; 14 | lastTask: boolean; 15 | successMessage?: string; 16 | callback?: TaskManagerCallback; 17 | callbackData?: any; 18 | } 19 | 20 | interface TaskManagerTask { 21 | task: vscode.Task; 22 | options?: TaskManagerTaskOptions; 23 | } 24 | 25 | export class TaskManager { 26 | private static tasks: TaskManagerTask[] = []; 27 | private static current: vscode.TaskExecution | undefined = undefined; 28 | private static currentOptions: TaskManagerTaskOptions | undefined = undefined; 29 | private static initialized = false; 30 | 31 | static init() { 32 | if (this.initialized) { 33 | return; 34 | } 35 | 36 | vscode.tasks.onDidEndTaskProcess(async e => { 37 | // Check if matches the current running task 38 | if (this.current === e.execution) { 39 | // Check return code 40 | if (e.exitCode !== 0 && this.currentOptions?.ignoreError === false) { 41 | if (this.currentOptions?.errorMessage !== undefined) { 42 | vscode.window.showErrorMessage(`${this.currentOptions.errorMessage}`); 43 | } else { 44 | vscode.window.showErrorMessage(`Task ${e.execution.task.name} exited with code ${e.exitCode}`); 45 | } 46 | 47 | this.cancel(); 48 | return; 49 | } else { 50 | // Show success message only for last tasks 51 | if (this.currentOptions?.lastTask === true && this.currentOptions.successMessage !== undefined) { 52 | vscode.window.showInformationMessage(`${this.currentOptions.successMessage}`); 53 | } 54 | 55 | // Call the callback on success for all tasks (not just last ones) 56 | if (this.currentOptions?.callback !== undefined) { 57 | this.currentOptions?.callback(this.currentOptions.callbackData); 58 | } 59 | } 60 | 61 | // Execute next task 62 | let next = this.tasks.shift(); 63 | if (next !== undefined) { 64 | this.currentOptions = next.options; 65 | this.current = await vscode.tasks.executeTask(next.task); 66 | } else { 67 | this.currentOptions = undefined; 68 | this.current = undefined; 69 | } 70 | } 71 | }); 72 | 73 | this.initialized = true; 74 | } 75 | 76 | static async push(task: vscode.Task, options?: TaskManagerTaskOptions) { 77 | // If a task is already running, queue it 78 | if (this.current !== undefined) { 79 | this.tasks.push({ task, options }); 80 | return; 81 | } 82 | 83 | // Otherwise, start the execution 84 | this.current = await vscode.tasks.executeTask(task); 85 | this.currentOptions = options; 86 | } 87 | 88 | static async cancel() { 89 | // Cancel current task 90 | if (this.current !== undefined) { 91 | this.current?.terminate(); 92 | this.current = undefined; 93 | this.currentOptions = undefined; 94 | } 95 | 96 | // Clear queue 97 | this.tasks = []; 98 | } 99 | 100 | static async executeWithProgress( 101 | task: vscode.Task, 102 | options: TaskManagerTaskOptions, 103 | progressOptions?: vscode.ProgressOptions 104 | ): Promise { 105 | if (progressOptions) { 106 | return vscode.window.withProgress(progressOptions, async (progress, token) => { 107 | token.onCancellationRequested(() => { 108 | this.cancel(); 109 | }); 110 | 111 | await this.push(task, { 112 | ...options, 113 | callback: (data) => { 114 | progress.report({ increment: 100 }); 115 | options.callback?.(data); 116 | } 117 | }); 118 | }); 119 | } else { 120 | await this.push(task, options); 121 | } 122 | } 123 | 124 | static getCurrentTask(): vscode.TaskExecution | undefined { 125 | return this.current; 126 | } 127 | 128 | static getQueueLength(): number { 129 | return this.tasks.length; 130 | } 131 | 132 | /** 133 | * Execute a sequence of tasks/commands in order 134 | */ 135 | static async executeSequence(tasks: (() => Promise)[]): Promise { 136 | for (const task of tasks) { 137 | await task(); 138 | } 139 | } 140 | 141 | /** 142 | * Execute a task with a dependency 143 | */ 144 | static async executeWithDependency( 145 | dependency: () => Promise, 146 | dependent: (result: T) => Promise 147 | ): Promise { 148 | const result = await dependency(); 149 | await dependent(result); 150 | } 151 | } 152 | -------------------------------------------------------------------------------- /src/files/archive-extractor.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @author Jared Wolff 3 | * @copyright Circuit Dojo LLC 4 | * @license Apache 2.0 5 | */ 6 | 7 | import * as fs from "fs-extra"; 8 | import * as unzip from "node-stream-zip"; 9 | import * as sevenzip from "7zip-bin"; 10 | import * as node7zip from "node-7z"; 11 | import * as util from "util"; 12 | import * as cp from "child_process"; 13 | import { platform } from "../config"; 14 | 15 | export class ArchiveExtractor { 16 | static async extractZip(source: string, destination: string): Promise { 17 | return new Promise((resolve, reject) => { 18 | try { 19 | const zip = new unzip.async({ file: source }); 20 | 21 | zip.on("extract", (entry, file) => { 22 | // Make executable on non-Windows platforms 23 | if (platform !== "win32") { 24 | fs.chmodSync(file, 0o755); 25 | } 26 | }); 27 | 28 | zip.extract(null, destination) 29 | .then(() => { 30 | zip.close(); 31 | resolve(); 32 | }) 33 | .catch(reject); 34 | } catch (error) { 35 | reject(error); 36 | } 37 | }); 38 | } 39 | 40 | static async extract7z(source: string, destination: string): Promise { 41 | return new Promise((resolve, reject) => { 42 | try { 43 | const myStream = node7zip.extractFull(source, destination, { 44 | $bin: sevenzip.path7za, 45 | }); 46 | 47 | myStream.on('error', (err) => { 48 | console.error('7z extraction error:', err); 49 | reject(err); 50 | }); 51 | 52 | myStream.on('end', () => { 53 | console.log('7z extraction completed'); 54 | resolve(); 55 | }); 56 | } catch (error) { 57 | reject(error); 58 | } 59 | }); 60 | } 61 | 62 | static async extractTar(source: string, destination: string): Promise { 63 | const exec = util.promisify(cp.exec); 64 | 65 | try { 66 | // Use native tar command for better compatibility 67 | const cmd = `tar -xf "${source}" -C "${destination}"`; 68 | console.log(`Executing tar command: ${cmd}`); 69 | 70 | const result = await exec(cmd); 71 | if (result.stderr) { 72 | console.log(`Tar extraction stderr: ${result.stderr}`); 73 | } 74 | 75 | // Make extracted files executable on non-Windows platforms 76 | if (platform !== "win32") { 77 | try { 78 | await exec(`find "${destination}" -type f -exec chmod +x {} +`); 79 | } catch (chmodError) { 80 | // Don't fail extraction if chmod fails 81 | console.log(`chmod warning: ${chmodError}`); 82 | } 83 | } 84 | } catch (error) { 85 | throw new Error(`Tar extraction failed: ${error}`); 86 | } 87 | } 88 | 89 | static async validateExtraction(extractionPath: string): Promise { 90 | try { 91 | if (!(await fs.pathExists(extractionPath))) { 92 | console.log(`Extraction validation failed: ${extractionPath} does not exist`); 93 | return false; 94 | } 95 | 96 | const extractedFiles = await fs.readdir(extractionPath); 97 | if (extractedFiles.length === 0) { 98 | console.log(`Extraction validation failed: No files extracted to ${extractionPath}`); 99 | return false; 100 | } 101 | 102 | console.log(`Extraction validated: ${extractedFiles.length} items extracted`); 103 | return true; 104 | } catch (error) { 105 | console.log(`Extraction validation error: ${error}`); 106 | return false; 107 | } 108 | } 109 | 110 | static async extractArchive(source: string, destination: string): Promise { 111 | try { 112 | // Ensure destination directory exists 113 | await fs.mkdirp(destination); 114 | 115 | const sourceLower = source.toLowerCase(); 116 | 117 | if (sourceLower.endsWith('.zip')) { 118 | await this.extractZip(source, destination); 119 | } else if (sourceLower.endsWith('.7z')) { 120 | await this.extract7z(source, destination); 121 | } else if (sourceLower.endsWith('.tar.gz') || 122 | sourceLower.endsWith('.tar.xz') || 123 | sourceLower.includes('.tar')) { 124 | // Use native tar for tar files on Linux/macOS, 7z on Windows 125 | if (platform === "win32") { 126 | await this.extract7z(source, destination); 127 | } else { 128 | await this.extractTar(source, destination); 129 | } 130 | } else { 131 | throw new Error(`Unsupported archive format: ${source}`); 132 | } 133 | 134 | // Validate extraction 135 | return await this.validateExtraction(destination); 136 | } catch (error) { 137 | console.error(`Archive extraction failed: ${error}`); 138 | return false; 139 | } 140 | } 141 | } 142 | -------------------------------------------------------------------------------- /src/utils/path-utils.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @author Jared Wolff 3 | * @copyright Circuit Dojo LLC 4 | * @license Apache 2.0 5 | */ 6 | 7 | import * as path from "path"; 8 | import * as os from "os"; 9 | import { platform } from "../config"; 10 | 11 | export class PathUtils { 12 | static normalizePath(inputPath: string): string { 13 | return path.normalize(inputPath); 14 | } 15 | 16 | static joinPaths(...paths: string[]): string { 17 | return path.join(...paths); 18 | } 19 | 20 | static getRelativePath(from: string, to: string): string { 21 | return path.relative(from, to); 22 | } 23 | 24 | static getAbsolutePath(inputPath: string): string { 25 | return path.resolve(inputPath); 26 | } 27 | 28 | static getHomeDirectory(): string { 29 | return os.homedir(); 30 | } 31 | 32 | static getTempDirectory(): string { 33 | return os.tmpdir(); 34 | } 35 | 36 | static getPathSeparator(): string { 37 | return path.sep; 38 | } 39 | 40 | static getPathDelimiter(): string { 41 | return path.delimiter; 42 | } 43 | 44 | static isAbsolute(inputPath: string): boolean { 45 | return path.isAbsolute(inputPath); 46 | } 47 | 48 | static getDirectory(filePath: string): string { 49 | return path.dirname(filePath); 50 | } 51 | 52 | static getFileName(filePath: string): string { 53 | return path.basename(filePath); 54 | } 55 | 56 | static getFileNameWithoutExtension(filePath: string): string { 57 | return path.basename(filePath, path.extname(filePath)); 58 | } 59 | 60 | static getFileExtension(filePath: string): string { 61 | return path.extname(filePath); 62 | } 63 | 64 | static changeExtension(filePath: string, newExtension: string): string { 65 | const dir = path.dirname(filePath); 66 | const name = path.basename(filePath, path.extname(filePath)); 67 | return path.join(dir, name + newExtension); 68 | } 69 | 70 | static convertToUnixPath(inputPath: string): string { 71 | return inputPath.replace(/\\/g, '/'); 72 | } 73 | 74 | static convertToWindowsPath(inputPath: string): string { 75 | return inputPath.replace(/\//g, '\\'); 76 | } 77 | 78 | static convertPathForPlatform(inputPath: string): string { 79 | if (platform === "win32") { 80 | return this.convertToWindowsPath(inputPath); 81 | } else { 82 | return this.convertToUnixPath(inputPath); 83 | } 84 | } 85 | 86 | static isPathsEqual(path1: string, path2: string): boolean { 87 | const normalizedPath1 = path.resolve(path1); 88 | const normalizedPath2 = path.resolve(path2); 89 | 90 | if (platform === "win32") { 91 | return normalizedPath1.toLowerCase() === normalizedPath2.toLowerCase(); 92 | } else { 93 | return normalizedPath1 === normalizedPath2; 94 | } 95 | } 96 | 97 | static containsPath(parentPath: string, childPath: string): boolean { 98 | const normalizedParent = path.resolve(parentPath); 99 | const normalizedChild = path.resolve(childPath); 100 | 101 | const relativePath = path.relative(normalizedParent, normalizedChild); 102 | return !relativePath.startsWith('..') && !path.isAbsolute(relativePath); 103 | } 104 | 105 | static findCommonPath(paths: string[]): string { 106 | if (paths.length === 0) return ""; 107 | if (paths.length === 1) return path.dirname(paths[0]); 108 | 109 | const resolvedPaths = paths.map(p => path.resolve(p)); 110 | const splitPaths = resolvedPaths.map(p => p.split(path.sep)); 111 | 112 | let commonPath = splitPaths[0]; 113 | 114 | for (let i = 1; i < splitPaths.length; i++) { 115 | const currentPath = splitPaths[i]; 116 | const newCommonPath = []; 117 | 118 | for (let j = 0; j < Math.min(commonPath.length, currentPath.length); j++) { 119 | if (platform === "win32") { 120 | if (commonPath[j].toLowerCase() === currentPath[j].toLowerCase()) { 121 | newCommonPath.push(commonPath[j]); 122 | } else { 123 | break; 124 | } 125 | } else { 126 | if (commonPath[j] === currentPath[j]) { 127 | newCommonPath.push(commonPath[j]); 128 | } else { 129 | break; 130 | } 131 | } 132 | } 133 | 134 | commonPath = newCommonPath; 135 | } 136 | 137 | return commonPath.join(path.sep); 138 | } 139 | 140 | static makePathRelativeToHome(inputPath: string): string { 141 | const homedir = os.homedir(); 142 | const absolutePath = path.resolve(inputPath); 143 | 144 | if (this.containsPath(homedir, absolutePath)) { 145 | return "~" + path.sep + path.relative(homedir, absolutePath); 146 | } 147 | 148 | return inputPath; 149 | } 150 | 151 | static expandTildeInPath(inputPath: string): string { 152 | if (inputPath.startsWith("~")) { 153 | return path.join(os.homedir(), inputPath.slice(1)); 154 | } 155 | return inputPath; 156 | } 157 | } 158 | -------------------------------------------------------------------------------- /src/commands/build.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @author Jared Wolff 3 | * @copyright Circuit Dojo LLC 4 | * @license Apache 2.0 5 | */ 6 | 7 | import * as vscode from "vscode"; 8 | import * as path from "path"; 9 | import { GlobalConfig } from "../types"; 10 | import { ProjectConfigManager, ConfigValidator } from "../config"; 11 | import { TaskManager } from "../tasks"; 12 | import { changeBoardCommand } from "./board-management"; 13 | import { changeProjectCommand } from "./project-management"; 14 | import { EnvironmentUtils } from "../utils"; 15 | import { SettingsManager } from "../config/settings-manager"; 16 | export async function buildCommand( 17 | config: GlobalConfig, 18 | context: vscode.ExtensionContext, 19 | pristine: boolean = false, 20 | sidebarProvider?: any 21 | ): Promise { 22 | // Validate setup state and manifest version 23 | const setupValidation = await ConfigValidator.validateSetupState(config, context, false); 24 | if (!setupValidation.isValid) { 25 | vscode.window.showErrorMessage(setupValidation.error!); 26 | return; 27 | } 28 | 29 | // Fetch the project config 30 | let project = await ProjectConfigManager.load(context); 31 | 32 | // Validate project initialization 33 | const projectValidation = ConfigValidator.validateProjectInit(project); 34 | if (!projectValidation.isValid) { 35 | vscode.window.showErrorMessage(projectValidation.error!); 36 | return; 37 | } 38 | 39 | // Build environment for execution using SettingsManager 40 | const env = SettingsManager.buildEnvironmentForExecution(); 41 | 42 | // Auto-prompt for board if undefined (replicates old extension behavior) 43 | if (project.board === undefined) { 44 | await changeBoardCommand(config, context); 45 | 46 | // Reload project config after changeBoardCommand 47 | project = await ProjectConfigManager.load(context); 48 | 49 | // Check again - if still undefined, show error and return 50 | if (project.board === undefined) { 51 | vscode.window.showErrorMessage("You must choose a board to continue."); 52 | return; 53 | } 54 | } 55 | 56 | // Auto-prompt for project target if undefined (replicates old extension behavior) 57 | if (project.target === undefined) { 58 | await changeProjectCommand(config, context); 59 | 60 | // Reload project config after changeProjectCommand 61 | project = await ProjectConfigManager.load(context); 62 | 63 | // Check again - if still undefined, show error and return 64 | if (project.target === undefined) { 65 | vscode.window.showErrorMessage("You must choose a project to build."); 66 | return; 67 | } 68 | } 69 | 70 | // Get the active workspace root path 71 | let rootPaths = vscode.workspace.workspaceFolders; 72 | if (rootPaths === undefined) { 73 | return; 74 | } 75 | 76 | const rootPath = rootPaths[0].uri; 77 | 78 | // Options for Shell Execution with normalized environment 79 | let options: vscode.ShellExecutionOptions = { 80 | env: EnvironmentUtils.normalizeEnvironment(env), 81 | cwd: project.target, 82 | }; 83 | 84 | // Tasks 85 | let taskName = "Zephyr Tools: Build"; 86 | 87 | // Generate universal build path that works on windows & *nix 88 | let buildPath = path.join("build", project.board?.split("/")[0] ?? ""); 89 | 90 | // Build command 91 | let cmd = `west build -b ${project.board}${pristine ? " -p" : ""} -d ${buildPath}${ 92 | project.sysbuild ? " --sysbuild" : "" 93 | }`; 94 | 95 | let exec = new vscode.ShellExecution(cmd, options); 96 | 97 | // Task 98 | let task = new vscode.Task( 99 | { type: "zephyr-tools", command: taskName }, 100 | vscode.TaskScope.Workspace, 101 | taskName, 102 | "zephyr-tools", 103 | exec, 104 | ); 105 | 106 | vscode.window.showInformationMessage(`Building for ${project.board}`); 107 | 108 | // Set up task completion listener to refresh sidebar 109 | let taskCompletionDisposable: vscode.Disposable | undefined; 110 | if (sidebarProvider) { 111 | taskCompletionDisposable = vscode.tasks.onDidEndTask((taskEvent) => { 112 | // Check if this is our build task that completed 113 | if (taskEvent.execution.task === task) { 114 | console.log('Build task completed, refreshing sidebar in 1 second...'); 115 | // Small delay to ensure build artifacts are fully written 116 | setTimeout(() => { 117 | if (sidebarProvider && typeof sidebarProvider.refresh === 'function') { 118 | sidebarProvider.refresh(); 119 | } 120 | }, 1000); 121 | 122 | // Clean up the listener 123 | taskCompletionDisposable?.dispose(); 124 | } 125 | }); 126 | } 127 | 128 | // Start execution 129 | await vscode.tasks.executeTask(task); 130 | } 131 | 132 | export async function buildPristineCommand( 133 | config: GlobalConfig, 134 | context: vscode.ExtensionContext, 135 | sidebarProvider?: any 136 | ): Promise { 137 | await buildCommand(config, context, true, sidebarProvider); 138 | } 139 | -------------------------------------------------------------------------------- /src/hardware/board-detector.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @author Jared Wolff 3 | * @copyright Circuit Dojo LLC 4 | * @license Apache 2.0 5 | */ 6 | 7 | import * as vscode from "vscode"; 8 | import * as fs from "fs-extra"; 9 | import * as path from "path"; 10 | import { YamlParser } from "../utils"; 11 | 12 | export interface BoardInfo { 13 | name: string; 14 | arch: string; 15 | vendor?: string; 16 | soc?: string; 17 | variants?: string[]; 18 | path: string; 19 | } 20 | 21 | export class BoardDetector { 22 | static async detectBoards(rootDirectory: vscode.Uri): Promise { 23 | const boards: BoardInfo[] = []; 24 | const boardDirectories = await this.findBoardDirectories(rootDirectory); 25 | 26 | for (const boardDir of boardDirectories) { 27 | const boardsInDir = await this.scanBoardDirectory(boardDir); 28 | boards.push(...boardsInDir); 29 | } 30 | 31 | return boards; 32 | } 33 | 34 | private static async findBoardDirectories(rootDirectory: vscode.Uri): Promise { 35 | const boardDirectories: string[] = []; 36 | 37 | try { 38 | const files = await vscode.workspace.fs.readDirectory(rootDirectory); 39 | 40 | for (const [fileName, fileType] of files) { 41 | if (fileType === vscode.FileType.Directory && !fileName.startsWith('.')) { 42 | const boardsPath = path.join(rootDirectory.fsPath, fileName, 'boards'); 43 | 44 | if (await fs.pathExists(boardsPath)) { 45 | boardDirectories.push(boardsPath); 46 | } 47 | } 48 | } 49 | } catch (error) { 50 | console.error(`Error finding board directories in ${rootDirectory.fsPath}:`, error); 51 | } 52 | 53 | return boardDirectories; 54 | } 55 | 56 | private static async scanBoardDirectory(boardDirectory: string): Promise { 57 | const boards: BoardInfo[] = []; 58 | const foldersToIgnore = ["build", ".git", "bindings"]; 59 | 60 | try { 61 | const scanQueue: string[] = [boardDirectory]; 62 | 63 | while (scanQueue.length > 0) { 64 | const currentDir = scanQueue.shift()!; 65 | const entries = await fs.readdir(currentDir, { withFileTypes: true }); 66 | 67 | for (const entry of entries) { 68 | const fullPath = path.join(currentDir, entry.name); 69 | 70 | if (entry.isDirectory() && !foldersToIgnore.includes(entry.name)) { 71 | scanQueue.push(fullPath); 72 | } else if (entry.isFile()) { 73 | if (entry.name === 'board.yml') { 74 | // Parse the board.yml file 75 | const boardsFromYaml = await this.parseBoardYaml(fullPath); 76 | boards.push(...boardsFromYaml); 77 | } else if (entry.name.endsWith('.yaml') && entry.name !== 'board.yml') { 78 | // Legacy board definition 79 | const boardName = path.parse(entry.name).name; 80 | boards.push({ 81 | name: boardName, 82 | arch: 'unknown', 83 | path: fullPath 84 | }); 85 | } 86 | } 87 | } 88 | } 89 | } catch (error) { 90 | console.error(`Error scanning board directory ${boardDirectory}:`, error); 91 | } 92 | 93 | return boards; 94 | } 95 | 96 | private static async parseBoardYaml(yamlPath: string): Promise { 97 | try { 98 | // Use YamlParser to get board names 99 | const boardNames = await YamlParser.parseBoardYaml(yamlPath); 100 | 101 | return boardNames.map(name => ({ 102 | name, 103 | arch: 'detected', // YamlParser should provide this 104 | path: yamlPath 105 | })); 106 | } catch (error) { 107 | console.error(`Error parsing board YAML ${yamlPath}:`, error); 108 | return []; 109 | } 110 | } 111 | 112 | static async getBoardsForArchitecture(rootDirectory: vscode.Uri, architecture: string): Promise { 113 | const allBoards = await this.detectBoards(rootDirectory); 114 | return allBoards.filter(board => board.arch === architecture); 115 | } 116 | 117 | static async searchBoardsByName(rootDirectory: vscode.Uri, searchTerm: string): Promise { 118 | const allBoards = await this.detectBoards(rootDirectory); 119 | const lowerSearchTerm = searchTerm.toLowerCase(); 120 | 121 | return allBoards.filter(board => 122 | board.name.toLowerCase().includes(lowerSearchTerm) || 123 | board.vendor?.toLowerCase().includes(lowerSearchTerm) || 124 | board.soc?.toLowerCase().includes(lowerSearchTerm) 125 | ); 126 | } 127 | 128 | static async validateBoardExists(rootDirectory: vscode.Uri, boardName: string): Promise { 129 | const allBoards = await this.detectBoards(rootDirectory); 130 | return allBoards.some(board => board.name === boardName); 131 | } 132 | 133 | static async getBoardInfo(rootDirectory: vscode.Uri, boardName: string): Promise { 134 | const allBoards = await this.detectBoards(rootDirectory); 135 | return allBoards.find(board => board.name === boardName); 136 | } 137 | } 138 | -------------------------------------------------------------------------------- /src/environment/python-checker.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @author Jared Wolff 3 | * @copyright Circuit Dojo LLC 4 | * @license Apache 2.0 5 | */ 6 | 7 | import * as vscode from "vscode"; 8 | import * as util from "util"; 9 | import * as cp from "child_process"; 10 | import { platform, getPlatformConfig } from "../config"; 11 | 12 | // Function to find a suitable Python 3.10+ version 13 | export async function findSuitablePython(output: vscode.OutputChannel): Promise { 14 | const exec = util.promisify(cp.exec); 15 | const platformConfig = getPlatformConfig(); 16 | 17 | // List of Python executables to try, in order of preference 18 | const pythonCandidates = platform === "win32" ? ["python", "python3", "py"] : ["python3", "python"]; 19 | 20 | for (const pythonCmd of pythonCandidates) { 21 | try { 22 | output.appendLine(`[SETUP] Checking ${pythonCmd}...`); 23 | const result = await exec(`${pythonCmd} --version`); 24 | const versionOutput = result.stdout || result.stderr; 25 | const versionMatch = versionOutput.match(/Python (\d+)\.(\d+)\.(\d+)/); 26 | 27 | if (versionMatch) { 28 | const major = parseInt(versionMatch[1]); 29 | const minor = parseInt(versionMatch[2]); 30 | const version = `${major}.${minor}`; 31 | 32 | output.appendLine(`[SETUP] Found ${pythonCmd}: Python ${version}`); 33 | 34 | // Check if version is 3.10 or higher (including future major versions) 35 | if ((major === 3 && minor >= 10) || major > 3) { 36 | output.appendLine(`[SETUP] Python ${version} meets requirements (>= 3.10)`); 37 | return pythonCmd; 38 | } else { 39 | output.appendLine(`[SETUP] Python ${version} is too old (requires >= 3.10)`); 40 | } 41 | } 42 | } catch (error) { 43 | // Python executable not found or failed to run, continue to next candidate 44 | output.appendLine(`[SETUP] ${pythonCmd} not found or failed to execute`); 45 | } 46 | } 47 | 48 | output.appendLine("[SETUP] No suitable Python 3.10+ version found"); 49 | return null; 50 | } 51 | 52 | export async function validatePythonInstallation(pythonCmd: string, env: { [key: string]: string | undefined }, output: vscode.OutputChannel): Promise { 53 | const exec = util.promisify(cp.exec); 54 | 55 | try { 56 | const cmd = `${pythonCmd} --version`; 57 | output.appendLine(cmd); 58 | const result = await exec(cmd, { env }); 59 | 60 | if (result.stdout.includes("Python 3")) { 61 | output.appendLine("[SETUP] python3 found"); 62 | return true; 63 | } else { 64 | output.appendLine("[SETUP] python3 not found"); 65 | showPythonInstallInstructions(output); 66 | return false; 67 | } 68 | } catch (error) { 69 | output.appendLine("[SETUP] python validation failed"); 70 | showPythonInstallInstructions(output); 71 | return false; 72 | } 73 | } 74 | 75 | export async function validatePipInstallation(pythonCmd: string, env: { [key: string]: string | undefined }, output: vscode.OutputChannel): Promise { 76 | const exec = util.promisify(cp.exec); 77 | 78 | try { 79 | const cmd = `${pythonCmd} -m pip --version`; 80 | output.appendLine(cmd); 81 | const result = await exec(cmd, { env }); 82 | output.append(result.stdout); 83 | output.append(result.stderr); 84 | output.appendLine("[SETUP] pip installed"); 85 | return true; 86 | } catch (error) { 87 | output.appendLine("[SETUP] pip validation failed"); 88 | showPipInstallInstructions(output); 89 | return false; 90 | } 91 | } 92 | 93 | export async function validateVenvSupport(pythonCmd: string, env: { [key: string]: string | undefined }, output: vscode.OutputChannel): Promise { 94 | const exec = util.promisify(cp.exec); 95 | 96 | try { 97 | const cmd = `${pythonCmd} -m venv --help`; 98 | output.appendLine(cmd); 99 | await exec(cmd, { env }); 100 | output.appendLine("[SETUP] python3 venv OK"); 101 | return true; 102 | } catch (error) { 103 | output.appendLine("[SETUP] venv validation failed"); 104 | showVenvInstallInstructions(output); 105 | return false; 106 | } 107 | } 108 | 109 | function showPythonInstallInstructions(output: vscode.OutputChannel): void { 110 | switch (platform) { 111 | case "darwin": 112 | output.appendLine("[SETUP] use `brew` to install `python3`"); 113 | output.appendLine("[SETUP] Install `brew` first: https://brew.sh"); 114 | output.appendLine("[SETUP] Then run `brew install python3`"); 115 | break; 116 | case "linux": 117 | output.appendLine("[SETUP] install `python` using `apt get install python3.10 python3.10-pip python3.10-venv`"); 118 | break; 119 | default: 120 | break; 121 | } 122 | } 123 | 124 | function showPipInstallInstructions(output: vscode.OutputChannel): void { 125 | switch (platform) { 126 | case "linux": 127 | output.appendLine("[SETUP] please install `python3.10-pip` package (or newer)"); 128 | break; 129 | default: 130 | output.appendLine("[SETUP] please install `python3` with `pip` support"); 131 | break; 132 | } 133 | } 134 | 135 | function showVenvInstallInstructions(output: vscode.OutputChannel): void { 136 | switch (platform) { 137 | case "linux": 138 | output.appendLine("[SETUP] please install `python3.10-venv` package (or newer)"); 139 | break; 140 | default: 141 | output.appendLine("[SETUP] please install `python3` with `venv` support"); 142 | break; 143 | } 144 | } 145 | -------------------------------------------------------------------------------- /src/utils/yaml-parser.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @author Jared Wolff 3 | * @copyright Circuit Dojo LLC 4 | * @license Apache 2.0 5 | */ 6 | 7 | import * as vscode from "vscode"; 8 | import * as yaml from "yaml"; 9 | 10 | interface BoardYamlSoc { 11 | name: string; 12 | variants?: Array<{ name: string }>; 13 | } 14 | 15 | interface BoardYamlBoard { 16 | name: string; 17 | socs: BoardYamlSoc[]; 18 | revision?: { 19 | default?: string; 20 | revisions?: Array<{ name: string }>; 21 | }; 22 | } 23 | 24 | interface BoardYamlRoot { 25 | board?: BoardYamlBoard; 26 | boards?: BoardYamlBoard[]; 27 | } 28 | 29 | export class YamlParser { 30 | static async parseBoardYaml(file: string): Promise { 31 | const boards: string[] = []; 32 | 33 | try { 34 | const contents = await vscode.workspace.openTextDocument(file).then(document => { 35 | return document.getText(); 36 | }); 37 | 38 | const parsed: BoardYamlRoot = yaml.parse(contents); 39 | let parsedBoards: BoardYamlBoard[] = []; 40 | 41 | // Handle both single board and multiple boards format 42 | if (parsed.boards !== undefined) { 43 | parsedBoards = parsed.boards; 44 | } else if (parsed.board !== undefined) { 45 | parsedBoards.push(parsed.board); 46 | } 47 | 48 | for (const board of parsedBoards) { 49 | // Check if socs has entries 50 | if (board.socs.length === 0) { 51 | continue; 52 | } 53 | 54 | const soc = board.socs[0]; 55 | 56 | // Add basic board entry 57 | boards.push(`${board.name}/${soc.name}`); 58 | 59 | // Add all variants 60 | if (soc.variants !== undefined) { 61 | for (const variant of soc.variants) { 62 | boards.push(`${board.name}/${soc.name}/${variant.name}`); 63 | } 64 | } 65 | 66 | // Iterate all revisions if revision exists 67 | if (board.revision !== undefined && board.revision.revisions !== undefined) { 68 | for (const revision of board.revision.revisions) { 69 | // Check if default and continue 70 | if (board.revision.default === revision.name) { 71 | continue; 72 | } 73 | 74 | // Add board revision entry 75 | boards.push(`${board.name}@${revision.name}/${soc.name}`); 76 | 77 | // Add all variants for this revision 78 | if (soc.variants !== undefined) { 79 | for (const variant of soc.variants) { 80 | boards.push(`${board.name}@${revision.name}/${soc.name}/${variant.name}`); 81 | } 82 | } 83 | } 84 | } 85 | } 86 | } catch (error) { 87 | console.error(`Error parsing board YAML file ${file}:`, error); 88 | } 89 | 90 | return boards; 91 | } 92 | 93 | static async parseGenericYaml(file: string): Promise { 94 | try { 95 | const contents = await vscode.workspace.openTextDocument(file).then(document => { 96 | return document.getText(); 97 | }); 98 | 99 | return yaml.parse(contents) as T; 100 | } catch (error) { 101 | console.error(`Error parsing YAML file ${file}:`, error); 102 | return null; 103 | } 104 | } 105 | 106 | static parseYamlString(yamlString: string): T | null { 107 | try { 108 | return yaml.parse(yamlString) as T; 109 | } catch (error) { 110 | console.error("Error parsing YAML string:", error); 111 | return null; 112 | } 113 | } 114 | 115 | static stringifyYaml(obj: any, options?: yaml.ToStringOptions): string { 116 | try { 117 | return yaml.stringify(obj, options); 118 | } catch (error) { 119 | console.error("Error stringifying object to YAML:", error); 120 | return ""; 121 | } 122 | } 123 | 124 | static validateYamlFile(file: string): Promise { 125 | return new Promise(async (resolve) => { 126 | try { 127 | await this.parseGenericYaml(file); 128 | resolve(true); 129 | } catch (error) { 130 | console.error(`YAML validation failed for ${file}:`, error); 131 | resolve(false); 132 | } 133 | }); 134 | } 135 | 136 | static async extractBoardArchitecture(file: string): Promise { 137 | try { 138 | const parsed = await this.parseGenericYaml(file); 139 | if (!parsed) return null; 140 | 141 | const board = parsed.board || (parsed.boards && parsed.boards[0]); 142 | if (board && board.socs && board.socs.length > 0) { 143 | // This is a simplified approach - in reality, you'd need to cross-reference 144 | // with SoC definitions to get the actual architecture 145 | return board.socs[0].name; // Return SoC name as a proxy for architecture 146 | } 147 | } catch (error) { 148 | console.error(`Error extracting architecture from ${file}:`, error); 149 | } 150 | 151 | return null; 152 | } 153 | 154 | static async getBoardMetadata(file: string): Promise<{ 155 | name: string; 156 | soc: string; 157 | arch?: string; 158 | variants?: string[]; 159 | revisions?: string[]; 160 | } | null> { 161 | try { 162 | const parsed = await this.parseGenericYaml(file); 163 | if (!parsed) return null; 164 | 165 | const board = parsed.board || (parsed.boards && parsed.boards[0]); 166 | if (!board || !board.socs || board.socs.length === 0) return null; 167 | 168 | const soc = board.socs[0]; 169 | 170 | return { 171 | name: board.name, 172 | soc: soc.name, 173 | variants: soc.variants?.map(v => v.name) || [], 174 | revisions: board.revision?.revisions?.map(r => r.name) || [] 175 | }; 176 | } catch (error) { 177 | console.error(`Error extracting board metadata from ${file}:`, error); 178 | return null; 179 | } 180 | } 181 | } 182 | -------------------------------------------------------------------------------- /src/config/validation.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @author Jared Wolff 3 | * @copyright Circuit Dojo LLC 4 | * @license Apache 2.0 5 | */ 6 | 7 | import * as vscode from "vscode"; 8 | import { GlobalConfig, ProjectConfig } from "../types"; 9 | import { GlobalConfigManager } from "./global-config"; 10 | import { ManifestValidator, ManifestValidationResult } from "./manifest-validator"; 11 | 12 | export interface ValidationResult { 13 | isValid: boolean; 14 | error?: string; 15 | details?: string[]; 16 | } 17 | 18 | /** 19 | * Configuration validation utilities with comprehensive manifest checking 20 | */ 21 | export class ConfigValidator { 22 | /** 23 | * Validates that the global config has a compatible manifest version 24 | */ 25 | static validateManifestVersion(config: GlobalConfig): boolean { 26 | const manifest = require("../../manifest/manifest.json"); 27 | return config.manifestVersion === manifest.version; 28 | } 29 | 30 | /** 31 | * Validates setup state with comprehensive manifest verification 32 | * Automatically resets setup flag if physical validation fails 33 | */ 34 | static async validateSetupState( 35 | config: GlobalConfig, 36 | context?: vscode.ExtensionContext, 37 | performPhysicalValidation: boolean = true 38 | ): Promise { 39 | const manifest = require("../../manifest/manifest.json"); 40 | 41 | // Check manifest version first 42 | if (config.manifestVersion !== manifest.version) { 43 | return { 44 | isValid: false, 45 | error: "An update is required. Run `Zephyr Tools: Setup` command first.", 46 | details: [`Expected manifest version ${manifest.version}, found ${config.manifestVersion}`] 47 | }; 48 | } 49 | 50 | // Check basic setup flag 51 | if (!config.isSetup) { 52 | return { 53 | isValid: false, 54 | error: "Run `Zephyr Tools: Setup` command first.", 55 | details: ["Setup has not been completed"] 56 | }; 57 | } 58 | 59 | // Perform physical validation if requested and context available 60 | if (performPhysicalValidation && context) { 61 | try { 62 | const physicalValidation = await ManifestValidator.validateCompleteSetup(config); 63 | 64 | if (!physicalValidation.isValid) { 65 | // Reset setup flag due to physical validation failure 66 | console.log("Physical validation failed, resetting setup flag"); 67 | config.isSetup = false; 68 | await GlobalConfigManager.save(context, config); 69 | 70 | return { 71 | isValid: false, 72 | error: "Setup validation failed. Run `Zephyr Tools: Setup` command again.", 73 | details: [ 74 | "Physical validation detected missing or corrupted components:", 75 | ...physicalValidation.errors, 76 | ...physicalValidation.warnings 77 | ] 78 | }; 79 | } 80 | 81 | // Log warnings but don't fail validation 82 | if (physicalValidation.warnings.length > 0) { 83 | console.log("Setup validation warnings:", physicalValidation.warnings); 84 | } 85 | } catch (error) { 86 | console.warn("Physical validation failed with error:", error); 87 | // Don't fail validation due to validation errors, but log them 88 | return { 89 | isValid: true, // Allow to proceed but with warning 90 | error: undefined, 91 | details: [`Warning: Could not verify physical setup: ${error}`] 92 | }; 93 | } 94 | } 95 | 96 | return { isValid: true }; 97 | } 98 | 99 | /** 100 | * Quick validation without physical checks (for performance-critical paths) 101 | */ 102 | static validateSetupStateQuick(config: GlobalConfig): ValidationResult { 103 | const manifest = require("../../manifest/manifest.json"); 104 | 105 | if (config.manifestVersion !== manifest.version) { 106 | return { 107 | isValid: false, 108 | error: "An update is required. Run `Zephyr Tools: Setup` command first." 109 | }; 110 | } 111 | 112 | if (!config.isSetup) { 113 | return { 114 | isValid: false, 115 | error: "Run `Zephyr Tools: Setup` command first." 116 | }; 117 | } 118 | 119 | return { isValid: true }; 120 | } 121 | 122 | /** 123 | * Validates that the project has been properly initialized 124 | */ 125 | static validateProjectInit(project: ProjectConfig): ValidationResult { 126 | if (!project.isInit) { 127 | return { 128 | isValid: false, 129 | error: "Run `Zephyr Tools: Init Repo` command first." 130 | }; 131 | } 132 | return { isValid: true }; 133 | } 134 | 135 | /** 136 | * Validates both setup state and project initialization 137 | */ 138 | static async validateSetupAndProject( 139 | config: GlobalConfig, 140 | project: ProjectConfig, 141 | context?: vscode.ExtensionContext, 142 | performPhysicalValidation: boolean = false 143 | ): Promise { 144 | // First check setup state 145 | const setupValidation = await this.validateSetupState(config, context, performPhysicalValidation); 146 | if (!setupValidation.isValid) { 147 | return setupValidation; 148 | } 149 | 150 | // Then check project initialization 151 | return this.validateProjectInit(project); 152 | } 153 | 154 | /** 155 | * Quick validation of both setup and project (synchronous) 156 | */ 157 | static validateSetupAndProjectQuick(config: GlobalConfig, project: ProjectConfig): ValidationResult { 158 | // First check setup state 159 | const setupValidation = this.validateSetupStateQuick(config); 160 | if (!setupValidation.isValid) { 161 | return setupValidation; 162 | } 163 | 164 | // Then check project initialization 165 | return this.validateProjectInit(project); 166 | } 167 | } 168 | -------------------------------------------------------------------------------- /src/files/project-scanner.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @author Jared Wolff 3 | * @copyright Circuit Dojo LLC 4 | * @license Apache 2.0 5 | */ 6 | 7 | import * as vscode from "vscode"; 8 | import * as fs from "fs-extra"; 9 | import * as path from "path"; 10 | import { YamlParser } from "../utils"; 11 | 12 | export class ProjectScanner { 13 | static async getProjectList(folder: vscode.Uri): Promise { 14 | const files = await vscode.workspace.fs.readDirectory(folder); 15 | const projects: string[] = []; 16 | 17 | const queue = [...files.map(([name, type]) => ({ name, type, path: folder }))]; 18 | 19 | while (queue.length > 0) { 20 | const file = queue.shift(); 21 | if (!file) break; 22 | 23 | if (file.name.includes("CMakeLists.txt")) { 24 | // Check the file content 25 | const filepath = vscode.Uri.joinPath(file.path, file.name); 26 | try { 27 | const contents = await vscode.workspace.openTextDocument(filepath).then(document => { 28 | return document.getText(); 29 | }); 30 | 31 | if (contents.includes("project(")) { 32 | const project = path.parse(filepath.fsPath); 33 | projects.push(project.dir); 34 | } 35 | } catch (error) { 36 | console.error(`Error reading ${filepath.fsPath}:`, error); 37 | } 38 | } else if (file.name.includes("build") || file.name.includes(".git")) { 39 | // Skip these directories 40 | continue; 41 | } else if (file.type === vscode.FileType.Directory) { 42 | try { 43 | const subPath = vscode.Uri.joinPath(file.path, file.name); 44 | const subfolders = await vscode.workspace.fs.readDirectory(subPath); 45 | 46 | for (const [subName, subType] of subfolders) { 47 | queue.push({ 48 | name: path.join(file.name, subName), 49 | type: subType, 50 | path: file.path 51 | }); 52 | } 53 | } catch (error) { 54 | console.error(`Error reading directory ${file.name}:`, error); 55 | } 56 | } 57 | } 58 | 59 | return projects; 60 | } 61 | 62 | static async getBoardList(folder: vscode.Uri): Promise { 63 | const result: string[] = []; 64 | const foldersToIgnore = ["build", ".git", "bindings"]; 65 | 66 | const folderQueue: string[] = [folder.fsPath]; 67 | 68 | while (folderQueue.length > 0) { 69 | const currentFolder = folderQueue.shift() as string; 70 | 71 | try { 72 | // Check if board.yml exists in currentFolder 73 | const boardYamlPath = path.join(currentFolder, "board.yml"); 74 | if (fs.existsSync(boardYamlPath)) { 75 | try { 76 | const boards = await YamlParser.parseBoardYaml(boardYamlPath); 77 | result.push(...boards); 78 | } catch (error) { 79 | console.error(`Error parsing board YAML ${boardYamlPath}:`, error); 80 | // Fallback to folder name 81 | const folderName = path.basename(currentFolder); 82 | result.push(folderName); 83 | } 84 | continue; 85 | } 86 | 87 | // If board.yml isn't found we'll have to do a deeper search 88 | const entries = fs.readdirSync(currentFolder, { withFileTypes: true }); 89 | 90 | // Iterate over all entries 91 | for (const entry of entries) { 92 | if (entry.isDirectory() && !foldersToIgnore.includes(entry.name)) { 93 | folderQueue.push(path.join(currentFolder, entry.name)); 94 | } else if (entry.isFile()) { 95 | if (entry.name.endsWith(".yaml")) { 96 | const filePath = path.join(currentFolder, entry.name); 97 | 98 | // Remove .yaml from name 99 | const name = path.parse(filePath).name; 100 | 101 | // Add name to result 102 | result.push(name); 103 | } 104 | } 105 | } 106 | } catch (error) { 107 | console.error(`Error scanning folder ${currentFolder}:`, error); 108 | } 109 | } 110 | 111 | return result; 112 | } 113 | 114 | static async findFilesByPattern(rootDir: string, pattern: RegExp): Promise { 115 | const results: string[] = []; 116 | 117 | try { 118 | const scanDirectory = async (dir: string) => { 119 | const items = await fs.readdir(dir, { withFileTypes: true }); 120 | 121 | for (const item of items) { 122 | const fullPath = path.join(dir, item.name); 123 | 124 | if (item.isDirectory()) { 125 | // Skip common directories we don't want to scan 126 | if (!["node_modules", ".git", "build", ".vscode"].includes(item.name)) { 127 | await scanDirectory(fullPath); 128 | } 129 | } else if (item.isFile() && pattern.test(item.name)) { 130 | results.push(fullPath); 131 | } 132 | } 133 | }; 134 | 135 | await scanDirectory(rootDir); 136 | } catch (error) { 137 | console.error(`Error scanning directory ${rootDir}:`, error); 138 | } 139 | 140 | return results; 141 | } 142 | 143 | static async findCMakeProjects(rootDir: string): Promise { 144 | const cmakeFiles = await this.findFilesByPattern(rootDir, /^CMakeLists\.txt$/); 145 | const projects: string[] = []; 146 | 147 | for (const cmakeFile of cmakeFiles) { 148 | try { 149 | const content = await fs.readFile(cmakeFile, 'utf8'); 150 | if (content.includes('project(')) { 151 | projects.push(path.dirname(cmakeFile)); 152 | } 153 | } catch (error) { 154 | console.error(`Error reading CMakeLists.txt at ${cmakeFile}:`, error); 155 | } 156 | } 157 | 158 | return projects; 159 | } 160 | 161 | static async findSourceFiles(rootDir: string, extensions: string[] = ['.c', '.cpp', '.h', '.hpp']): Promise { 162 | const sourceFiles: string[] = []; 163 | const extensionPattern = new RegExp(`\\.(${extensions.map(ext => ext.replace('.', '')).join('|')})$`, 'i'); 164 | 165 | return this.findFilesByPattern(rootDir, extensionPattern); 166 | } 167 | } 168 | -------------------------------------------------------------------------------- /src/commands/monitor.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @author Jared Wolff 3 | * @copyright Circuit Dojo LLC 4 | * @license Apache 2.0 5 | */ 6 | 7 | import * as vscode from "vscode"; 8 | import { GlobalConfig, ProjectConfig } from "../types"; 9 | import { ProjectConfigManager } from "../config"; 10 | import { SerialPortManager } from "../hardware"; 11 | import { TaskManager } from "../tasks"; 12 | import { EnvironmentUtils } from "../utils"; 13 | import { SettingsManager } from "../config/settings-manager"; 14 | 15 | export async function monitorCommand( 16 | config: GlobalConfig, 17 | context: vscode.ExtensionContext 18 | ): Promise { 19 | const project = await ProjectConfigManager.load(context); 20 | 21 | if (!config.isSetup) { 22 | vscode.window.showErrorMessage("Run `Zephyr Tools: Setup` command before monitoring."); 23 | return; 24 | } 25 | 26 | // Set port if necessary 27 | let port = SettingsManager.getSerialPort(); 28 | if (!port) { 29 | port = await SerialPortManager.selectPort(config); 30 | if (!port) { 31 | vscode.window.showErrorMessage("Error obtaining serial port."); 32 | return; 33 | } 34 | 35 | // Save settings 36 | await SettingsManager.setSerialPort(port); 37 | } 38 | 39 | // Options for Shell Execution with normalized environment 40 | let options: vscode.ShellExecutionOptions = { 41 | env: EnvironmentUtils.normalizeEnvironment(SettingsManager.buildEnvironmentForExecution()), 42 | cwd: project.target, 43 | }; 44 | 45 | // Tasks 46 | let taskName = "Zephyr Tools: Serial Monitor"; 47 | 48 | // Command to run - conditionally include --save based on setting 49 | const saveFlag = SettingsManager.getSerialSaveLogsToFile() ? ' --save' : ''; 50 | let cmd = `zephyr-tools --port ${port} --follow${saveFlag}`; 51 | let exec = new vscode.ShellExecution(cmd, options); 52 | 53 | // Task 54 | let task = new vscode.Task( 55 | { type: "zephyr-tools", command: taskName }, 56 | vscode.TaskScope.Workspace, 57 | taskName, 58 | "zephyr-tools", 59 | exec, 60 | ); 61 | 62 | // Start execution 63 | await TaskManager.push(task, { 64 | ignoreError: false, 65 | lastTask: true, 66 | errorMessage: "Serial monitor error!", 67 | }); 68 | } 69 | 70 | export async function setupMonitorCommand( 71 | config: GlobalConfig, 72 | context: vscode.ExtensionContext 73 | ): Promise { 74 | if (!config.isSetup) { 75 | vscode.window.showErrorMessage("Run `Zephyr Tools: Setup` command first."); 76 | return; 77 | } 78 | 79 | // Get serial settings 80 | const port = await SerialPortManager.selectPort(config); 81 | if (!port) { 82 | vscode.window.showErrorMessage("Error obtaining serial port."); 83 | return; 84 | } 85 | 86 | // Save to settings 87 | await SettingsManager.setSerialPort(port); 88 | 89 | // Message output 90 | vscode.window.showInformationMessage(`Serial monitor set to use ${port}`); 91 | } 92 | 93 | export async function toggleSerialLoggingCommand( 94 | config: GlobalConfig, 95 | context: vscode.ExtensionContext 96 | ): Promise { 97 | if (!config.isSetup) { 98 | vscode.window.showErrorMessage("Run `Zephyr Tools: Setup` command first."); 99 | return; 100 | } 101 | 102 | // Show dropdown with Enable/Disable options 103 | const currentStatus = SettingsManager.getSerialSaveLogsToFile() ? 'Enabled' : 'Disabled'; 104 | 105 | const loggingOptions = [ 106 | { 107 | label: "Enable", 108 | description: "Save serial output to log files", 109 | value: true 110 | }, 111 | { 112 | label: "Disable", 113 | description: "Do not save serial output", 114 | value: false 115 | } 116 | ]; 117 | 118 | const selectedOption = await vscode.window.showQuickPick(loggingOptions, { 119 | title: "Serial Logging Configuration", 120 | placeHolder: `Currently: ${currentStatus}`, 121 | ignoreFocusOut: true, 122 | }); 123 | 124 | if (!selectedOption) { 125 | return; // User canceled 126 | } 127 | 128 | // Only update if the value changed 129 | const currentValue = SettingsManager.getSerialSaveLogsToFile(); 130 | if (currentValue !== selectedOption.value) { 131 | await SettingsManager.setSerialSaveLogsToFile(selectedOption.value); 132 | 133 | const status = selectedOption.value ? 'enabled' : 'disabled'; 134 | vscode.window.showInformationMessage(`Serial logging ${status}`); 135 | } 136 | } 137 | 138 | export async function changeSerialSettingsCommand( 139 | config: GlobalConfig, 140 | context: vscode.ExtensionContext 141 | ): Promise { 142 | if (!config.isSetup) { 143 | vscode.window.showErrorMessage("Run `Zephyr Tools: Setup` command first."); 144 | return; 145 | } 146 | 147 | // Show current settings 148 | const currentPort = SettingsManager.getSerialPort(); 149 | const currentPortDisplay = currentPort ? `Port: ${currentPort}` : "No port configured"; 150 | const loggingStatus = SettingsManager.getSerialSaveLogsToFile() ? "Logging: Enabled" : "Logging: Disabled"; 151 | 152 | // Options for what to change 153 | const changeOptions = [ 154 | { 155 | label: "Change Serial Port", 156 | description: currentPortDisplay, 157 | action: "port" 158 | }, 159 | { 160 | label: "Change Serial Logging", 161 | description: loggingStatus, 162 | action: "logging" 163 | }, 164 | { 165 | label: "Configure Both", 166 | description: "Change port and logging settings", 167 | action: "both" 168 | } 169 | ]; 170 | 171 | const selectedOption = await vscode.window.showQuickPick(changeOptions, { 172 | title: "Configure Serial Monitor Settings", 173 | placeHolder: "What would you like to change?", 174 | ignoreFocusOut: true, 175 | }); 176 | 177 | if (!selectedOption) { 178 | return; // User canceled 179 | } 180 | 181 | switch (selectedOption.action) { 182 | case "port": 183 | await setupMonitorCommand(config, context); 184 | break; 185 | 186 | case "logging": 187 | await toggleSerialLoggingCommand(config, context); 188 | break; 189 | 190 | case "both": 191 | await setupMonitorCommand(config, context); 192 | await toggleSerialLoggingCommand(config, context); 193 | break; 194 | } 195 | } 196 | -------------------------------------------------------------------------------- /src/environment/path-manager.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @file path-manager.ts 3 | * Handles environment path modifications for the Zephyr Tools. 4 | * 5 | * @license Apache-2.0 6 | */ 7 | 8 | import * as vscode from 'vscode'; 9 | import * as path from 'path'; 10 | import { getPlatformConfig } from '../config'; 11 | import { SettingsManager } from '../config/settings-manager'; 12 | import { GlobalConfig } from '../types'; 13 | 14 | export class PathManager { 15 | static async restorePaths(config: GlobalConfig, context: vscode.ExtensionContext): Promise { 16 | const platformConfig = getPlatformConfig(); 17 | const pathDivider = platformConfig.pathDivider; 18 | 19 | if (config.isSetup) { 20 | try { 21 | // Get all paths from settings 22 | const allPaths = SettingsManager.getAllPaths(); 23 | 24 | // If no paths are saved, try to build them from standard locations 25 | if (allPaths.length === 0) { 26 | const toolsDirectory = SettingsManager.getToolsDirectory(); 27 | const standardPaths = await this.getStandardToolPaths(toolsDirectory); 28 | 29 | // Save the discovered paths 30 | if (standardPaths.length > 0) { 31 | await SettingsManager.setAllPaths(standardPaths); 32 | } 33 | 34 | // Use the discovered paths 35 | for (const pathToAdd of standardPaths) { 36 | if (pathToAdd && pathToAdd.trim()) { 37 | context.environmentVariableCollection.prepend("PATH", pathToAdd + pathDivider); 38 | } 39 | } 40 | } else { 41 | // Use the saved paths 42 | for (const pathToAdd of allPaths) { 43 | if (pathToAdd && pathToAdd.trim()) { 44 | context.environmentVariableCollection.prepend("PATH", pathToAdd + pathDivider); 45 | } 46 | } 47 | } 48 | } catch (error) { 49 | console.log('Warning: Failed to restore paths:', error); 50 | // Don't throw - extension should still activate 51 | } 52 | } 53 | } 54 | 55 | private static async getStandardToolPaths(toolsDirectory: string): Promise { 56 | const paths: string[] = []; 57 | const fs = await import("fs-extra"); 58 | 59 | try { 60 | // Check if tools directory exists 61 | if (await fs.pathExists(toolsDirectory)) { 62 | // Get all subdirectories in tools directory 63 | const entries = await fs.readdir(toolsDirectory, { withFileTypes: true }); 64 | const toolDirs = entries.filter((entry: any) => entry.isDirectory()).map((entry: any) => entry.name); 65 | 66 | // Standard Python virtual environment path 67 | const pythonEnvPath = path.join(toolsDirectory, "env"); 68 | if (await fs.pathExists(pythonEnvPath)) { 69 | const binPath = path.join(pythonEnvPath, process.platform === "win32" ? "Scripts" : "bin"); 70 | if (await fs.pathExists(binPath)) { 71 | paths.push(binPath); 72 | } 73 | } 74 | 75 | // Check each tool directory for executable paths 76 | for (const toolDir of toolDirs) { 77 | if (toolDir === "env") continue; // Already handled above 78 | 79 | const toolPath = path.join(toolsDirectory, toolDir); 80 | 81 | // Common patterns for tool executable locations 82 | const possibleBinPaths = [ 83 | toolPath, // Root directory (ninja, newtmgr, etc.) 84 | path.join(toolPath, "bin"), // Standard bin subdirectory 85 | ]; 86 | 87 | // Check for Zephyr SDK toolchain paths 88 | if (toolDir.startsWith("zephyr-sdk-")) { 89 | possibleBinPaths.push( 90 | path.join(toolPath, "arm-zephyr-eabi", "bin"), 91 | path.join(toolPath, "riscv64-zephyr-elf", "bin"), 92 | path.join(toolPath, "xtensa-espressif_esp32_zephyr-elf", "bin") 93 | ); 94 | } 95 | 96 | // Check each possible path and add if it exists and contains executables 97 | for (const binPath of possibleBinPaths) { 98 | try { 99 | if (await fs.pathExists(binPath)) { 100 | const stat = await fs.stat(binPath); 101 | if (stat.isDirectory()) { 102 | // Check if directory contains executable files 103 | const files = await fs.readdir(binPath); 104 | const hasExecutables = files.some((file: string) => 105 | file.endsWith('.exe') || 106 | file.includes('gcc') || 107 | file.includes('cmake') || 108 | file.includes('ninja') || 109 | file.includes('probe-rs') || 110 | file.includes('newtmgr') || 111 | file.includes('zephyr-tools') 112 | ); 113 | if (hasExecutables) { 114 | paths.push(binPath); 115 | } 116 | } 117 | } 118 | } catch (error) { 119 | // Ignore errors for individual paths 120 | continue; 121 | } 122 | } 123 | } 124 | } 125 | } catch (error) { 126 | // If scanning fails, fall back to basic Python env path only 127 | const pythonEnvPath = path.join(toolsDirectory, "env"); 128 | const binPath = path.join(pythonEnvPath, process.platform === "win32" ? "Scripts" : "bin"); 129 | paths.push(binPath); 130 | } 131 | 132 | return paths; 133 | } 134 | 135 | static async setupEnvironmentPaths(context: vscode.ExtensionContext, config: GlobalConfig): Promise { 136 | // Restore all environment variables from settings 137 | const envVars = SettingsManager.getEnvironmentVariables(); 138 | for (const [key, value] of Object.entries(envVars)) { 139 | if (value) { 140 | context.environmentVariableCollection.replace(key, value); 141 | } 142 | } 143 | 144 | // VIRTUAL_ENV should be based on current tools directory 145 | const pythonenv = path.join(SettingsManager.getToolsDirectory(), "env"); 146 | context.environmentVariableCollection.replace("VIRTUAL_ENV", pythonenv); 147 | } 148 | } 149 | 150 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Zephyr Tools for VS Code 2 | 3 | Circuit Dojo designed Zephyr Tools to make getting started with Zephyr a snap. This extension simplifies working with the Zephyr RTOS by providing project management, build automation, flashing, monitoring, and hardware integration capabilities. 4 | 5 | ## Features 6 | 7 | ### Core Functionality 8 | - **Multi-platform support** - Works on Windows, macOS, and Linux 9 | - **Automated setup** - Install Zephyr dependencies, SDK, and toolchains with one command 10 | - **Project management** - Create new projects, initialize repositories, and manage existing ones 11 | - **Build system integration** - Build, build pristine, and clean your projects 12 | - **Hardware support** - Flash devices, monitor serial output, and manage hardware configurations 13 | - **Debugging** - Create debug configurations and launch debugging sessions 14 | 15 | ### Project Management 16 | - Create new Zephyr projects from templates (vanilla Zephyr, nRF Connect SDK, NFED) 17 | - Initialize local and remote repositories 18 | - Change board targets and project configurations 19 | - Support for custom Zephyr modules 20 | - Sysbuild support for complex applications 21 | 22 | ### Build Features 23 | - Standard and pristine builds 24 | - Clean build artifacts 25 | - Build status in VS Code status bar 26 | - Build output in dedicated output channel 27 | - Support for CMake and Ninja build systems 28 | 29 | ### Hardware Integration 30 | - **Multiple flash methods**: 31 | - Standard West flash 32 | - probe-rs for cross-platform flashing 33 | - newtmgr for MCUboot operations 34 | - **Serial monitoring**: 35 | - Configurable baud rates 36 | - Save logs to file 37 | - Toggle logging on/off 38 | - **Hardware detection**: 39 | - Auto-detect serial ports 40 | - Multi-probe support with selection UI 41 | - probe-rs chip detection 42 | 43 | ### Developer Tools 44 | - Open Zephyr-configured terminal 45 | - View and manage build assets in sidebar 46 | - Environment path management 47 | - Python virtual environment per workspace 48 | - VS Code task integration for long-running operations 49 | 50 | ### UI Features 51 | - **Custom sidebar** with project information and build assets 52 | - **Status bar items** showing current board and project 53 | - **Command palette** integration with 25+ commands 54 | - **Webview** for enhanced project management 55 | 56 | ## Recommended Extensions 57 | 58 | For the best development experience with Zephyr Tools, we recommend installing these VS Code extensions: 59 | 60 | - **C/C++ Extension Pack** (`ms-vscode.cpptools`) - Provides IntelliSense, debugging, and code browsing for C/C++ code 61 | - **nRF DeviceTree** (`nordic-semiconductor.nrf-devicetree`) - Syntax highlighting and IntelliSense for DeviceTree files 62 | - **nRF Kconfig** (`nordic-semiconductor.nrf-kconfig`) - Syntax highlighting and IntelliSense for Kconfig files 63 | - **Probe-rs Debugger** (`probe-rs.probe-rs-debugger`) - Debug embedded applications using probe-rs 64 | 65 | These extensions will be automatically suggested when you open a workspace with Zephyr Tools. You can install them individually from the Extensions marketplace or all at once when prompted. 66 | 67 | ## Requirements 68 | 69 | ### Mac 70 | 71 | Requires `git` and `python3` to be installed. The easiest way to do that is with [Homebrew](https://brew.sh). 72 | 73 | ``` 74 | > brew install git python3 75 | ``` 76 | 77 | ### Windows 78 | 79 | Requires `git` and `python` to be installed. 80 | 81 | - Download and install `git` from here: https://git-scm.com/download/win 82 | - Download and install `python` from here: https://www.python.org/ftp/python/3.9.9/python-3.9.9-amd64.exe 83 | 84 | ### Linux 85 | 86 | Requires `git` and `python` to be installed. 87 | 88 | Use your distro's package manager of choice to install. 89 | 90 | For example on Ubuntu: 91 | 92 | ``` 93 | sudo apt install git python3 python3-pip python3-venv 94 | ``` 95 | 96 | ## Getting Started 97 | 98 | 1. **Install the extension** from the VS Code marketplace 99 | 2. **Run setup**: Open Command Palette (`Cmd/Ctrl+Shift+P`) and run `Zephyr Tools: Setup` 100 | 3. **Create a project**: Run `Zephyr Tools: Create Project` or initialize an existing repository with `Zephyr Tools: Init Repo` 101 | 4. **Build your project**: Run `Zephyr Tools: Build` or click the build button in the status bar 102 | 5. **Flash and monitor**: Run `Zephyr Tools: Flash and Monitor` to deploy and debug your application 103 | 104 | ## Configuration 105 | 106 | Zephyr Tools provides several configuration options accessible through VS Code settings: 107 | 108 | ### Path Configuration 109 | - `zephyr-tools.paths.toolsDirectory` - Custom path to Zephyr tools directory (default: ~/.zephyr-tools/) 110 | - `zephyr-tools.paths.pythonExecutable` - Custom Python executable path 111 | - `zephyr-tools.paths.zephyrBase` - Custom ZEPHYR_BASE path 112 | - `zephyr-tools.paths.westExecutable` - Custom West executable path 113 | 114 | ### Hardware Configuration 115 | - `zephyr-tools.probeRs.chipName` - Chip name for probe-rs operations 116 | - `zephyr-tools.probeRs.probeId` - Specific probe ID to use 117 | - `zephyr-tools.probeRs.preverify` - Verify memory before flashing 118 | - `zephyr-tools.probeRs.verify` - Verify memory after flashing 119 | 120 | ### Serial Configuration 121 | - `zephyr-tools.serial.port` - Default serial port for monitoring 122 | - `zephyr-tools.serial.saveLogsToFile` - Enable saving serial logs to file 123 | - `zephyr-tools.newtmgr.baudRate` - Baud rate for newtmgr connections 124 | 125 | ## Project Structure 126 | 127 | Zephyr Tools projects use the following structure: 128 | ``` 129 | your-project/ 130 | ├── .zephyr-tools/ 131 | │ └── project.json # Project configuration 132 | ├── .venv/ # Python virtual environment 133 | ├── target/ # Build artifacts 134 | │ └── build/ 135 | │ └── / # Board-specific build output 136 | ├── CMakeLists.txt # Project CMake configuration 137 | ├── prj.conf # Kconfig configuration 138 | └── src/ # Source code 139 | └── main.c 140 | ``` 141 | 142 | ## Troubleshooting 143 | 144 | ### Common Issues 145 | 146 | 1. **Setup fails**: Ensure you have git and python3 installed as per the requirements 147 | 2. **Build errors**: Check that your board is supported and project configuration is correct 148 | 3. **Flash failures**: Verify your hardware is connected and the correct runner is selected 149 | 4. **Serial monitor issues**: Check port permissions and that no other application is using the port 150 | 151 | ### Logs and Debugging 152 | 153 | - View extension logs in the Output panel under "Zephyr Tools" 154 | - Build output appears in "Zephyr Tools - Build" 155 | - Serial output appears in "Zephyr Tools - Serial" 156 | 157 | ## Support 158 | 159 | For issues, feature requests, or contributions, please visit the [GitHub repository](https://github.com/circuitdojo/zephyr-tools-vscode). -------------------------------------------------------------------------------- /src/hardware/probe-manager.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @file probe-manager.ts 3 | * Handles probe management and detection for the Zephyr Tools. 4 | * 5 | * @license Apache-2.0 6 | */ 7 | 8 | import * as vscode from 'vscode'; 9 | import * as cp from 'child_process'; 10 | import * as util from 'util'; 11 | import { ProbeInfo } from '../types'; 12 | import { PlatformUtils, EnvironmentUtils } from '../utils'; 13 | 14 | export class ProbeManager { 15 | 16 | 17 | static async getAvailableProbes(configEnv?: { [key: string]: string }): Promise { 18 | try { 19 | const exec = util.promisify(cp.exec); 20 | const tools = PlatformUtils.getToolExecutables(); 21 | const cmd = `${tools.probeRs} list`; 22 | 23 | // Use normalized environment - either from config or system default 24 | const execEnv = configEnv ? 25 | EnvironmentUtils.normalizeEnvironment(configEnv) : 26 | EnvironmentUtils.getSystemEnvironment(); 27 | 28 | console.log(`ProbeManager: About to execute: ${cmd}`); 29 | console.log(`ProbeManager: PATH in execEnv: ${execEnv.PATH}`); 30 | console.log(`ProbeManager: Full execEnv:`, JSON.stringify(execEnv, null, 2)); 31 | 32 | const result = await exec(cmd, { env: execEnv }); 33 | 34 | if (result.stderr && result.stderr.trim() !== "") { 35 | console.error(`probe-rs list stderr: ${result.stderr}`); 36 | } 37 | 38 | return this.parseProbeRsList(result.stdout); 39 | } catch (error) { 40 | console.error(`Error running probe-rs list: ${error}`); 41 | return null; 42 | } 43 | } 44 | 45 | static async selectProbe(probes: ProbeInfo[]): Promise { 46 | const probeItems = probes.map((probe, index) => { 47 | // Build the label with just probe name and ID 48 | let label = `${probe.name} ID:${index}`; 49 | 50 | // Build description with VID:PID:Serial information 51 | let description = ''; 52 | if (probe.vidPid && probe.serial) { 53 | description = `${probe.vidPid}:${probe.serial}`; 54 | } else if (probe.vidPid) { 55 | description = probe.vidPid; 56 | } else if (probe.serial) { 57 | description = `Serial: ${probe.serial}`; 58 | } else { 59 | description = `Probe ID: ${probe.id}`; 60 | } 61 | 62 | return { 63 | label: label, 64 | description: description, 65 | detail: `Internal ID: ${probe.id}${probe.probeId ? ` | Probe Identifier: ${probe.probeId}` : ''}`, 66 | probe: probe 67 | }; 68 | }); 69 | 70 | const selectedItem = await vscode.window.showQuickPick(probeItems, { 71 | title: "Select debug probe for flashing", 72 | placeHolder: "Choose which probe to use for flashing...", 73 | ignoreFocusOut: true, 74 | }); 75 | 76 | return selectedItem?.probe; 77 | } 78 | 79 | static async getProbeRsChipName(configEnv?: { [key: string]: string }): Promise { 80 | try { 81 | const exec = util.promisify(cp.exec); 82 | const tools = PlatformUtils.getToolExecutables(); 83 | const cmd = `${tools.probeRs} chip list`; 84 | 85 | // Use normalized environment - either from config or system default 86 | const execEnv = configEnv ? 87 | EnvironmentUtils.normalizeEnvironment(configEnv) : 88 | EnvironmentUtils.getSystemEnvironment(); 89 | 90 | console.log(`ProbeManager: About to execute: ${cmd}`); 91 | console.log(`ProbeManager: PATH in execEnv: ${execEnv.PATH}`); 92 | 93 | const result = await exec(cmd, { env: execEnv }); 94 | 95 | if (result.stderr) { 96 | console.error(`Error getting probe-rs chip list: ${result.stderr}`); 97 | return undefined; 98 | } 99 | 100 | const chipNames = this.parseProbeRsChipList(result.stdout); 101 | 102 | if (chipNames.length === 0) { 103 | console.error("No chips found in probe-rs chip list."); 104 | return undefined; 105 | } 106 | 107 | return await vscode.window.showQuickPick(chipNames, { 108 | title: "Select probe-rs target chip", 109 | placeHolder: "Choose the target chip for flashing...", 110 | ignoreFocusOut: true, 111 | }); 112 | } catch (error) { 113 | console.error(`Error running probe-rs chip list: ${error}`); 114 | return undefined; 115 | } 116 | } 117 | 118 | private static parseProbeRsList(output: string): ProbeInfo[] { 119 | const lines = output.split('\n'); 120 | const probes: ProbeInfo[] = []; 121 | 122 | for (const line of lines) { 123 | const trimmedLine = line.trim(); 124 | 125 | if (trimmedLine && (trimmedLine.includes('VID:') || trimmedLine.includes('Serial:') || 126 | (trimmedLine.startsWith('[') && trimmedLine.includes(']:')))) { 127 | const idMatch = trimmedLine.match(/\[([^\]]+)\]/); 128 | if (idMatch) { 129 | const id = idMatch[1]; 130 | const descriptionMatch = trimmedLine.match(/\[([^\]]+)\]:\s*(.+)/); 131 | if (descriptionMatch) { 132 | const fullDescription = descriptionMatch[2]; 133 | const nameMatch = fullDescription.match(/^([^()]+)/); 134 | const probeName = nameMatch ? nameMatch[1].trim() : fullDescription; 135 | // Extract VID:PID information 136 | const vidPidMatch = fullDescription.match(/VID:\s*([0-9a-fA-F]{4})\s*PID:\s*([0-9a-fA-F]{4})/); 137 | let vidPid = ""; 138 | if (vidPidMatch) { 139 | vidPid = `${vidPidMatch[1]}:${vidPidMatch[2]}`; 140 | } 141 | 142 | // Extract serial number 143 | const serialMatch = fullDescription.match(/Serial:\s*([^,\s)]+)/); 144 | const serial = serialMatch ? serialMatch[1] : undefined; 145 | 146 | // Build probe identifier for --probe flag 147 | let probeIdentifier: string | undefined; 148 | 149 | // First try to find CMSIS-DAP identifier format 150 | const cmsisMatch = fullDescription.match(/--\s*([0-9a-fA-F:]+)/); 151 | if (cmsisMatch) { 152 | probeIdentifier = cmsisMatch[1]; 153 | } else if (vidPid && serial) { 154 | // Build identifier from VID:PID:Serial for probe-rs 155 | probeIdentifier = `${vidPid}:${serial}`; 156 | } else if (serial) { 157 | // Use just serial if no VID:PID available 158 | probeIdentifier = serial; 159 | } 160 | 161 | probes.push({ id, name: probeName, probeId: probeIdentifier, fullDescription, vidPid, serial }); 162 | } 163 | } 164 | } 165 | } 166 | 167 | return probes; 168 | } 169 | 170 | private static parseProbeRsChipList(output: string): string[] { 171 | const lines = output.split('\n'); 172 | const chipNames: string[] = []; 173 | 174 | for (const line of lines) { 175 | const trimmedLine = line.trim(); 176 | 177 | if (trimmedLine && !trimmedLine.endsWith('Series') && !trimmedLine.startsWith('Variants:') && 178 | trimmedLine !== 'Variants:' && 179 | line.startsWith(' ')) { 180 | chipNames.push(trimmedLine); 181 | } 182 | } 183 | 184 | return chipNames.sort(); 185 | } 186 | } 187 | -------------------------------------------------------------------------------- /src/commands/debug.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @author Jared Wolff 3 | * @copyright Circuit Dojo LLC 4 | * @license Apache 2.0 5 | */ 6 | 7 | import * as vscode from "vscode"; 8 | import * as path from "path"; 9 | import * as fs from "fs-extra"; 10 | import { GlobalConfig } from "../types"; 11 | import { ProjectConfigManager } from "../config"; 12 | import { SettingsManager } from "../config/settings-manager"; 13 | import { ProbeManager } from "../hardware"; 14 | import { EnvironmentUtils } from "../utils"; 15 | 16 | /** 17 | * Create or update a probe-rs debug configuration in launch.json based on current project. 18 | */ 19 | export async function createDebugConfigurationCommand( 20 | config: GlobalConfig, 21 | context: vscode.ExtensionContext 22 | ): Promise { 23 | // Check for probe-rs debugger extension availability and offer install/enable guidance 24 | await ensureProbeRsDebuggerInstalled(); 25 | // Validate workspace 26 | const rootFolders = vscode.workspace.workspaceFolders; 27 | if (!rootFolders || rootFolders.length === 0) { 28 | vscode.window.showErrorMessage("Open a workspace to create a debug configuration."); 29 | return; 30 | } 31 | const workspaceUri = rootFolders[0].uri; 32 | 33 | // Load project configuration 34 | let project = await ProjectConfigManager.load(context); 35 | if (!project.board || !project.target) { 36 | vscode.window.showErrorMessage("Select a board and project before creating a debug configuration."); 37 | return; 38 | } 39 | 40 | // Resolve chip name (from settings or prompt via probe-rs) 41 | let chipName: string | undefined = SettingsManager.getProbeRsChipName(); 42 | if (!chipName) { 43 | const env = EnvironmentUtils.normalizeEnvironment(SettingsManager.buildEnvironmentForExecution()); 44 | chipName = await ProbeManager.getProbeRsChipName(env); 45 | if (!chipName) { 46 | vscode.window.showWarningMessage("Chip name not selected. Debug configuration not created."); 47 | return; 48 | } 49 | await SettingsManager.setProbeRsChipName(chipName); 50 | } 51 | 52 | // Derive build directory and candidate ELF paths 53 | const boardBase = project.board.split("/")[0]; 54 | const buildDir = path.join(project.target, "build", boardBase); 55 | 56 | const candidates: Array<{ key: string; fullPath: string }> = [ 57 | { key: "app", fullPath: path.join(buildDir, "app", "zephyr", "zephyr.elf") }, 58 | { key: "default", fullPath: path.join(buildDir, "zephyr", "zephyr.elf") }, 59 | { key: "tfm", fullPath: path.join(buildDir, "tfm", "zephyr", "zephyr.elf") }, 60 | { key: "spm", fullPath: path.join(buildDir, "spm", "zephyr", "zephyr.elf") }, 61 | { key: "mcuboot", fullPath: path.join(buildDir, "mcuboot", "zephyr", "zephyr.elf") }, 62 | ]; 63 | 64 | // Also scan direct children of buildDir for /zephyr/zephyr.elf 65 | try { 66 | const children = await fs.readdir(buildDir); 67 | for (const child of children) { 68 | const probe = path.join(buildDir, child, "zephyr", "zephyr.elf"); 69 | candidates.push({ key: child, fullPath: probe }); 70 | } 71 | } catch (e) { 72 | // ignore read errors; build dir may not exist yet 73 | } 74 | 75 | // Filter to existing ELF files and de-duplicate by path 76 | const elfSet = new Map(); 77 | for (const c of candidates) { 78 | if (await fs.pathExists(c.fullPath)) { 79 | elfSet.set(c.fullPath, c.key); 80 | } 81 | } 82 | 83 | if (elfSet.size === 0) { 84 | const choice = await vscode.window.showErrorMessage( 85 | "No ELF found. Build the project first.", 86 | "Build Now", 87 | "Cancel" 88 | ); 89 | if (choice === "Build Now") { 90 | await vscode.commands.executeCommand('zephyr-tools.build'); 91 | } 92 | return; 93 | } 94 | 95 | // Sort by preference: app -> default -> tfm -> spm -> others 96 | const order = ["app", "default", "tfm", "spm"]; // earlier means lower coreIndex 97 | const entries = Array.from(elfSet.entries()) 98 | .map(([fullPath, key]) => ({ fullPath, key })) 99 | .sort((a, b) => { 100 | const ai = order.indexOf(a.key); 101 | const bi = order.indexOf(b.key); 102 | const av = ai === -1 ? Number.MAX_SAFE_INTEGER : ai; 103 | const bv = bi === -1 ? Number.MAX_SAFE_INTEGER : bi; 104 | return av - bv || a.key.localeCompare(b.key); 105 | }); 106 | 107 | // Select a single ELF (primary) to satisfy probe-rs single-core requirement 108 | const primary = entries[0]; 109 | const relPrimary = path.relative(workspaceUri.fsPath, primary.fullPath).split(path.sep).join("/"); 110 | const coreConfigs = [ 111 | { 112 | coreIndex: 0, 113 | programBinary: '${workspaceFolder}/' + relPrimary, 114 | } 115 | ]; 116 | 117 | // Compose debug configuration 118 | const configName = `${project.board} • App`; 119 | const debugConfig: any = { 120 | name: configName, 121 | type: "probe-rs-debug", 122 | request: "attach", 123 | chip: chipName, 124 | cwd: "${workspaceFolder}", 125 | speed: 4000, 126 | coreConfigs, 127 | consoleLogLevel: "Console", 128 | zephyrToolsId: "zephyr-tools.probe-rs", 129 | }; 130 | 131 | // Update launch configurations 132 | const launchCfg = vscode.workspace.getConfiguration("launch"); 133 | const existing = launchCfg.get("configurations") || []; 134 | const filtered = existing.filter(c => c.zephyrToolsId !== "zephyr-tools.probe-rs"); 135 | filtered.push(debugConfig); 136 | 137 | await launchCfg.update("configurations", filtered, vscode.ConfigurationTarget.Workspace); 138 | const version = launchCfg.get("version"); 139 | if (!version) { 140 | await launchCfg.update("version", "0.2.0", vscode.ConfigurationTarget.Workspace); 141 | } 142 | 143 | vscode.window.showInformationMessage(`Debug configuration created for ${project.board}.`); 144 | } 145 | 146 | /** 147 | * Create/update the config and immediately start the debugger. 148 | */ 149 | export async function debugNowCommand( 150 | config: GlobalConfig, 151 | context: vscode.ExtensionContext 152 | ): Promise { 153 | // Ensure probe-rs debugger is available before starting to avoid VS Code modal 154 | const dbgAvailable = await ensureProbeRsDebuggerInstalled(); 155 | if (!dbgAvailable) { 156 | // User needs to install/enable; don't start to avoid modal 157 | return; 158 | } 159 | // Ensure configuration exists/updated 160 | await createDebugConfigurationCommand(config, context); 161 | 162 | // Resolve workspace and config name 163 | const folders = vscode.workspace.workspaceFolders; 164 | if (!folders || folders.length === 0) { 165 | return; 166 | } 167 | const folder = folders[0]; 168 | 169 | const project = await ProjectConfigManager.load(context); 170 | if (!project.board) { 171 | return; 172 | } 173 | const configName = `${project.board} • App`; 174 | 175 | // Verify the configuration exists before attempting to start 176 | const launchCfg = vscode.workspace.getConfiguration("launch"); 177 | const existing = launchCfg.get("configurations") || []; 178 | const hasConfig = existing.some(c => c && c.name === configName && c.type === 'probe-rs-debug'); 179 | if (!hasConfig) { 180 | vscode.window.showWarningMessage("Debug configuration not found or incomplete. Build the project and try again."); 181 | return; 182 | } 183 | 184 | const ok = await vscode.debug.startDebugging(folder, configName); 185 | if (!ok) { 186 | vscode.window.showErrorMessage("Failed to start debug session. Check launch configuration."); 187 | } 188 | } 189 | 190 | /** 191 | * Ensure the probe-rs debugger extension is installed or offer to install. 192 | */ 193 | async function ensureProbeRsDebuggerInstalled(): Promise { 194 | // Try to locate a likely probe-rs debugger extension by ID or fuzzy match 195 | const preferredId = 'probe-rs.probe-rs-debugger'; 196 | const installedMatch = vscode.extensions.all.find(ext => { 197 | const id = (ext.id || '').toLowerCase(); 198 | return id === preferredId || (id.includes('probe') && id.includes('rs') && id.includes('debug')); 199 | }); 200 | const installId = installedMatch?.id || preferredId; 201 | 202 | // Check if any extension (enabled) contributes the probe-rs debugger type 203 | const hasContrib = vscode.extensions.all.some(ext => { 204 | try { 205 | const dbg = (ext.packageJSON?.contributes?.debuggers || []) as any[]; 206 | return dbg.some(d => (d?.type || '').toLowerCase() === 'probe-rs-debug'); 207 | } catch { 208 | return false; 209 | } 210 | }); 211 | 212 | if (!hasContrib) { 213 | // Not contributing; either not installed or disabled 214 | if (!installedMatch) { 215 | const action = await vscode.window.showWarningMessage( 216 | 'probe-rs Debugger extension is not installed. Debugging may not be available.', 217 | 'Install', 218 | 'Open Marketplace', 219 | 'Dismiss' 220 | ); 221 | if (action === 'Install') { 222 | try { 223 | await vscode.commands.executeCommand('workbench.extensions.installExtension', installId); 224 | const reload = await vscode.window.showInformationMessage('probe-rs Debugger installed. Reload to activate.', 'Reload', 'Later'); 225 | if (reload === 'Reload') { 226 | await vscode.commands.executeCommand('workbench.action.reloadWindow'); 227 | } 228 | } catch (e) { 229 | await vscode.env.openExternal(vscode.Uri.parse(`vscode:extension/${installId}`)); 230 | } 231 | } else if (action === 'Open Marketplace') { 232 | await vscode.env.openExternal(vscode.Uri.parse(`vscode:extension/${installId}`)); 233 | } 234 | return false; 235 | } else { 236 | // Installed but disabled — offer to enable 237 | const action = await vscode.window.showWarningMessage( 238 | 'probe-rs Debugger is installed but disabled. Enable it to use debugging.', 239 | 'Enable', 240 | 'Open Extensions', 241 | 'Dismiss' 242 | ); 243 | if (action === 'Enable') { 244 | await vscode.commands.executeCommand('workbench.extensions.enableExtension', installedMatch.id); 245 | const reload = await vscode.window.showInformationMessage('probe-rs Debugger enabled. Reload to activate.', 'Reload', 'Later'); 246 | if (reload === 'Reload') { 247 | await vscode.commands.executeCommand('workbench.action.reloadWindow'); 248 | } 249 | } else if (action === 'Open Extensions') { 250 | await vscode.commands.executeCommand('workbench.extensions.search', `@installed ${installedMatch.id}`); 251 | } 252 | return false; 253 | } 254 | } 255 | 256 | // Contributed: available; VS Code will activate on demand 257 | return true; 258 | } 259 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "zephyr-tools", 3 | "displayName": "Circuit Dojo Zephyr SDK Tools", 4 | "description": "Used for building your Zephyr projects.", 5 | "version": "0.5.4", 6 | "license": "Apache-2.0", 7 | "publisher": "circuitdojo", 8 | "icon": "img/bulb.png", 9 | "engines": { 10 | "vscode": "^1.101.0", 11 | "node": ">=16" 12 | }, 13 | "repository": { 14 | "type": "git", 15 | "url": "https://github.com/circuitdojo/zephyr-tools.git" 16 | }, 17 | "unresolvedDependencies": [ 18 | "ms-vscode.cpptools-extension-pack", 19 | "nordic-semiconductor.nrf-devicetree", 20 | "nordic-semiconductor.nrf-kconfig", 21 | "probe-rs.probe-rs-debugger" 22 | ], 23 | "categories": [ 24 | "Other" 25 | ], 26 | "activationEvents": [ 27 | "onStartupFinished", 28 | "onCommand:zephyr-tools.setup", 29 | "onCommand:zephyr-tools.create-project", 30 | "onCommand:zephyr-tools.build", 31 | "onCommand:zephyr-tools.build-pristine", 32 | "onCommand:zephyr-tools.change-board", 33 | "onCommand:zephyr-tools.change-project", 34 | "onCommand:zephyr-tools.flash", 35 | "onCommand:zephyr-tools.flash-probe-rs", 36 | "onCommand:zephyr-tools.flash-and-monitor", 37 | "onCommand:zephyr-tools.flash-probe-rs-and-monitor", 38 | "onCommand:zephyr-tools.setup-newtmgr", 39 | "onCommand:zephyr-tools.monitor", 40 | "onCommand:zephyr-tools.setup-monitor", 41 | "onCommand:zephyr-tools.toggle-serial-logging", 42 | "onCommand:zephyr-tools.change-serial-settings", 43 | "onCommand:zephyr-tools.load", 44 | "onCommand:zephyr-tools.load-and-monitor", 45 | "onCommand:zephyr-tools.init-repo", 46 | "onCommand:zephyr-tools.clean", 47 | "onCommand:zephyr-tools.update", 48 | "onCommand:zephyr-tools.change-runner", 49 | "onCommand:zephyr-tools.change-sysbuild", 50 | "onCommand:zephyr-tools.change-probe-rs-settings", 51 | "onCommand:zephyr-tools.open-zephyr-terminal", 52 | "onCommand:zephyr-tools.reset-paths", 53 | "onCommand:zephyr-tools.create-debug-config", 54 | "onCommand:zephyr-tools.debug-now" 55 | ], 56 | "main": "./out/extension.js", 57 | "contributes": { 58 | "commands": [ 59 | { 60 | "command": "zephyr-tools.setup", 61 | "title": "Zephyr Tools: Setup" 62 | }, 63 | { 64 | "command": "zephyr-tools.build-pristine", 65 | "title": "Zephyr Tools: Build Pristine" 66 | }, 67 | { 68 | "command": "zephyr-tools.build", 69 | "title": "Zephyr Tools: Build" 70 | }, 71 | { 72 | "command": "zephyr-tools.change-board", 73 | "title": "Zephyr Tools: Change Board" 74 | }, 75 | { 76 | "command": "zephyr-tools.create-project", 77 | "title": "Zephyr Tools: Create Project" 78 | }, 79 | { 80 | "command": "zephyr-tools.change-project", 81 | "title": "Zephyr Tools: Change Project" 82 | }, 83 | { 84 | "command": "zephyr-tools.flash", 85 | "title": "Zephyr Tools: Flash" 86 | }, 87 | { 88 | "command": "zephyr-tools.flash-probe-rs", 89 | "title": "Zephyr Tools: Flash with probe-rs" 90 | }, 91 | { 92 | "command": "zephyr-tools.flash-and-monitor", 93 | "title": "Zephyr Tools: Flash and Monitor" 94 | }, 95 | { 96 | "command": "zephyr-tools.flash-probe-rs-and-monitor", 97 | "title": "Zephyr Tools: Flash with probe-rs and Monitor" 98 | }, 99 | { 100 | "command": "zephyr-tools.load", 101 | "title": "Zephyr Tools: Load via Bootloader" 102 | }, 103 | { 104 | "command": "zephyr-tools.load-and-monitor", 105 | "title": "Zephyr Tools: Load via Bootloader and Monitor" 106 | }, 107 | { 108 | "command": "zephyr-tools.setup-newtmgr", 109 | "title": "Zephyr Tools: Newtmgr Settings" 110 | }, 111 | { 112 | "command": "zephyr-tools.setup-monitor", 113 | "title": "Zephyr Tools: Setup Serial Monitor" 114 | }, 115 | { 116 | "command": "zephyr-tools.monitor", 117 | "title": "Zephyr Tools: Serial Monitor" 118 | }, 119 | { 120 | "command": "zephyr-tools.toggle-serial-logging", 121 | "title": "Zephyr Tools: Change Serial Logging" 122 | }, 123 | { 124 | "command": "zephyr-tools.change-serial-settings", 125 | "title": "Zephyr Tools: Serial Settings" 126 | }, 127 | { 128 | "command": "zephyr-tools.init-repo", 129 | "title": "Zephyr Tools: Init Repo" 130 | }, 131 | { 132 | "command": "zephyr-tools.clean", 133 | "title": "Zephyr Tools: Clean" 134 | }, 135 | { 136 | "command": "zephyr-tools.update", 137 | "title": "Zephyr Tools: Update Dependencies" 138 | }, 139 | { 140 | "command": "zephyr-tools.change-runner", 141 | "title": "Zephyr Tools: Change Runner" 142 | }, 143 | { 144 | "command": "zephyr-tools.change-sysbuild", 145 | "title": "Zephyr Tools: Change Sysbuild Enable" 146 | }, 147 | { 148 | "command": "zephyr-tools.change-probe-rs-settings", 149 | "title": "Zephyr Tools: Change probe-rs Settings" 150 | }, 151 | { 152 | "command": "zephyr-tools.open-zephyr-terminal", 153 | "title": "Zephyr Tools: Open Zephyr Terminal" 154 | }, 155 | { 156 | "command": "zephyr-tools.reset-paths", 157 | "title": "Zephyr Tools: Reset Paths" 158 | }, 159 | { 160 | "command": "zephyr-tools.create-debug-config", 161 | "title": "Zephyr Tools: Create Debug Configuration" 162 | }, 163 | { 164 | "command": "zephyr-tools.debug-now", 165 | "title": "Zephyr Tools: Debug Now" 166 | } 167 | ], 168 | "viewsContainers": { 169 | "activitybar": [ 170 | { 171 | "id": "zephyr-tools", 172 | "title": "Zephyr Tools", 173 | "icon": "./icons/bulb.svg" 174 | } 175 | ] 176 | }, 177 | "views": { 178 | "zephyr-tools": [ 179 | { 180 | "type": "webview", 181 | "id": "zephyrToolsSidebar", 182 | "name": "Project", 183 | "when": "" 184 | } 185 | ] 186 | }, 187 | "configuration": { 188 | "title": "Zephyr Tools", 189 | "properties": { 190 | "zephyr-tools.paths.toolsDirectory": { 191 | "type": "string", 192 | "default": "", 193 | "description": "Custom path to Zephyr tools directory (default: ~/.zephyrtools)", 194 | "scope": "machine-overridable" 195 | }, 196 | "zephyr-tools.paths.pythonExecutable": { 197 | "type": "string", 198 | "default": "", 199 | "description": "Custom path to Python executable (auto-detected if empty)", 200 | "scope": "machine-overridable" 201 | }, 202 | "zephyr-tools.paths.zephyrBase": { 203 | "type": "string", 204 | "default": "", 205 | "description": "Custom ZEPHYR_BASE path (auto-detected if empty)", 206 | "scope": "workspace" 207 | }, 208 | "zephyr-tools.paths.westExecutable": { 209 | "type": "string", 210 | "default": "", 211 | "description": "Custom path to West executable (auto-detected if empty)", 212 | "scope": "machine-overridable" 213 | }, 214 | "zephyr-tools.paths.allPaths": { 215 | "type": "array", 216 | "items": { 217 | "type": "string" 218 | }, 219 | "default": [], 220 | "description": "All paths added to PATH environment by Zephyr Tools", 221 | "scope": "machine" 222 | }, 223 | "zephyr-tools.environment.variables": { 224 | "type": "object", 225 | "default": {}, 226 | "description": "Environment variables for Zephyr Tools", 227 | "scope": "machine" 228 | }, 229 | "zephyr-tools.probeRs.chipName": { 230 | "type": "string", 231 | "default": "", 232 | "description": "Chip name for probe-rs operations (e.g., nRF52840_xxAA)", 233 | "scope": "workspace" 234 | }, 235 | "zephyr-tools.probeRs.probeId": { 236 | "type": "string", 237 | "default": "", 238 | "description": "Probe ID for probe-rs operations (auto-detected if empty)", 239 | "scope": "workspace" 240 | }, 241 | "zephyr-tools.probeRs.preverify": { 242 | "type": "boolean", 243 | "default": false, 244 | "description": "Enable --preverify flag to verify memory before flashing", 245 | "scope": "workspace" 246 | }, 247 | "zephyr-tools.probeRs.verify": { 248 | "type": "boolean", 249 | "default": false, 250 | "description": "Enable --verify flag to verify memory after flashing", 251 | "scope": "workspace" 252 | }, 253 | "zephyr-tools.serial.port": { 254 | "type": "string", 255 | "default": "", 256 | "description": "Serial port for monitoring and flashing (auto-detected if empty)", 257 | "scope": "workspace" 258 | }, 259 | "zephyr-tools.serial.saveLogsToFile": { 260 | "type": "boolean", 261 | "default": false, 262 | "description": "Enable saving serial monitor output to log files", 263 | "scope": "workspace" 264 | }, 265 | "zephyr-tools.newtmgr.baudRate": { 266 | "type": "number", 267 | "default": 1000000, 268 | "description": "Default baud rate for newtmgr connections", 269 | "scope": "workspace" 270 | } 271 | } 272 | } 273 | }, 274 | "scripts": { 275 | "vscode:prepublish": "npm run package", 276 | "compile": "node esbuild.js", 277 | "compile:tsc": "tsc -p ./", 278 | "watch": "node esbuild.js --watch", 279 | "watch:tsc": "tsc -watch -p ./", 280 | "pretest": "npm run compile:tsc && npm run lint", 281 | "lint": "eslint src --ext ts", 282 | "test": "node ./out/test/runTest.js", 283 | "check-types": "tsc --noEmit", 284 | "package": "npm run check-types && node esbuild.js --production" 285 | }, 286 | "devDependencies": { 287 | "@types/fs-extra": "^11.0.4", 288 | "@types/glob": "^9.0.0", 289 | "@types/mocha": "^10.0.10", 290 | "@types/node": "^24.0.12", 291 | "@types/node-7z": "^2.1.10", 292 | "@types/vscode": "^1.101.0", 293 | "@typescript-eslint/eslint-plugin": "^8.36.0", 294 | "@typescript-eslint/parser": "^8.36.0", 295 | "esbuild": "^0.25.6", 296 | "eslint": "^9.30.1", 297 | "glob": "^11.0.3", 298 | "mocha": "^11.7.1", 299 | "typescript": "^5.8.3", 300 | "vscode-test": "^1.6.1" 301 | }, 302 | "dependencies": { 303 | "7zip-bin": "^5.2.0", 304 | "fs-extra": "^11.3.0", 305 | "node-7z": "^3.0.0", 306 | "node-stream-zip": "^1.15.0", 307 | "typed-rest-client": "^2.1.0", 308 | "yaml": "^2.8.0" 309 | } 310 | } 311 | -------------------------------------------------------------------------------- /src/build/build-assets-manager.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @author Jared Wolff 3 | * @copyright Circuit Dojo LLC 4 | * @license Apache 2.0 5 | */ 6 | 7 | import * as vscode from "vscode"; 8 | import * as fs from "fs-extra"; 9 | import * as path from "path"; 10 | import { ProjectConfig } from "../types"; 11 | 12 | export interface BuildAssetInfo { 13 | name: string; 14 | displayName: string; 15 | path: string; 16 | exists: boolean; 17 | size?: number; 18 | lastModified?: Date; 19 | } 20 | 21 | export interface BuildAssetsState { 22 | hasAssets: boolean; 23 | assets: BuildAssetInfo[]; 24 | lastBuild?: Date; 25 | buildPath: string; 26 | } 27 | 28 | export class BuildAssetsManager { 29 | private static readonly ASSET_DEFINITIONS = [ 30 | { 31 | name: "dfu_application.zip", 32 | displayName: "DFU Package", 33 | location: "root" as const, 34 | }, 35 | { 36 | name: "dfu_application.zip_manifest.json", 37 | displayName: "Manifest", 38 | location: "root" as const, 39 | }, 40 | { 41 | name: "merged.hex", 42 | displayName: "Merged Hex", 43 | location: "root" as const, 44 | }, 45 | { 46 | name: "zephyr.elf", 47 | displayName: "Zephyr ELF", 48 | location: "zephyr" as const, 49 | }, 50 | { 51 | name: "zephyr.hex", 52 | displayName: "Zephyr Hex", 53 | location: "zephyr" as const, 54 | }, 55 | ]; 56 | 57 | /** 58 | * Get build assets state for the current project configuration 59 | */ 60 | public static async getBuildAssetsState(project: ProjectConfig): Promise { 61 | const buildPath = this.getBuildPath(project); 62 | const assets: BuildAssetInfo[] = []; 63 | let hasAssets = false; 64 | let lastBuild: Date | undefined; 65 | 66 | for (const assetDef of this.ASSET_DEFINITIONS) { 67 | const assetPath = this.getAssetPath(buildPath, assetDef.name, assetDef.location, project); 68 | const assetInfo: BuildAssetInfo = { 69 | name: assetDef.name, 70 | displayName: assetDef.displayName, 71 | path: assetPath, 72 | exists: false, 73 | }; 74 | 75 | try { 76 | const stats = await fs.stat(assetPath); 77 | assetInfo.exists = true; 78 | assetInfo.size = stats.size; 79 | assetInfo.lastModified = stats.mtime; 80 | hasAssets = true; 81 | 82 | // Track the most recent build time 83 | if (!lastBuild || stats.mtime > lastBuild) { 84 | lastBuild = stats.mtime; 85 | } 86 | } catch (error) { 87 | // File doesn't exist, keep exists: false 88 | } 89 | 90 | assets.push(assetInfo); 91 | } 92 | 93 | return { 94 | hasAssets, 95 | assets, 96 | lastBuild, 97 | buildPath, 98 | }; 99 | } 100 | 101 | /** 102 | * Get the build directory path for a project 103 | */ 104 | public static getBuildPath(project: ProjectConfig): string { 105 | if (!project.target || !project.board) { 106 | return ""; 107 | } 108 | 109 | // Extract the base board name (before any slash) 110 | // e.g., "circuitdojo_feather_nrf9151/nrf9151/ns" -> "circuitdojo_feather_nrf9151" 111 | const baseBoardName = project.board.split('/')[0]; 112 | 113 | return path.join(project.target, "build", baseBoardName); 114 | } 115 | 116 | /** 117 | * Get the full path to a specific asset 118 | */ 119 | private static getAssetPath(buildPath: string, assetName: string, location: "root" | "zephyr", project?: ProjectConfig): string { 120 | if (location === "zephyr") { 121 | // For zephyr assets, they're typically in build/board/chip/zephyr/ 122 | // e.g., build/circuitdojo_feather_nrf9151/nrf9160/zephyr/ 123 | const chipName = this.extractChipName(project?.board); 124 | return path.join(buildPath, chipName, "zephyr", assetName); 125 | } 126 | return path.join(buildPath, assetName); 127 | } 128 | 129 | /** 130 | * Extract chip name from board configuration 131 | * e.g., "circuitdojo_feather_nrf9151/nrf9151/ns" -> "nrf9160" (based on common patterns) 132 | */ 133 | private static extractChipName(board?: string): string { 134 | if (!board) return "zephyr"; 135 | 136 | // For nRF91 series boards, the chip is typically nrf9160 137 | if (board.includes('nrf91')) { 138 | return 'nrf9160'; 139 | } 140 | 141 | // For other boards, try to extract from the board path 142 | const parts = board.split('/'); 143 | if (parts.length > 1) { 144 | return parts[1]; // Second part is usually the chip 145 | } 146 | 147 | // Default fallback 148 | return "zephyr"; 149 | } 150 | 151 | /** 152 | * Format file size in human-readable format 153 | */ 154 | public static formatFileSize(bytes: number): string { 155 | if (bytes === 0) return "0 B"; 156 | 157 | const k = 1024; 158 | const sizes = ["B", "KB", "MB", "GB"]; 159 | const i = Math.floor(Math.log(bytes) / Math.log(k)); 160 | 161 | return `${(bytes / Math.pow(k, i)).toFixed(1)} ${sizes[i]}`; 162 | } 163 | 164 | /** 165 | * Format time relative to now (e.g., "2 min ago") 166 | */ 167 | public static formatTimeAgo(date: Date): string { 168 | const now = new Date(); 169 | const diffMs = now.getTime() - date.getTime(); 170 | const diffMins = Math.floor(diffMs / (1000 * 60)); 171 | const diffHours = Math.floor(diffMs / (1000 * 60 * 60)); 172 | const diffDays = Math.floor(diffMs / (1000 * 60 * 60 * 24)); 173 | 174 | if (diffMins < 1) { 175 | return "just now"; 176 | } else if (diffMins < 60) { 177 | return `${diffMins} min${diffMins > 1 ? "s" : ""} ago`; 178 | } else if (diffHours < 24) { 179 | return `${diffHours} hour${diffHours > 1 ? "s" : ""} ago`; 180 | } else if (diffDays < 7) { 181 | return `${diffDays} day${diffDays > 1 ? "s" : ""} ago`; 182 | } else { 183 | return date.toLocaleDateString(); 184 | } 185 | } 186 | 187 | /** 188 | * Watch build directory for changes - optimized to only watch for final build outputs 189 | */ 190 | public static createFileWatcher( 191 | project: ProjectConfig, 192 | onChanged: () => void 193 | ): vscode.FileSystemWatcher | null { 194 | const buildPath = this.getBuildPath(project); 195 | console.log('Creating file watcher for build path:', buildPath); 196 | 197 | if (!buildPath) { 198 | console.log('No build path available, cannot create watcher'); 199 | return null; 200 | } 201 | 202 | try { 203 | // Watch the build directory and its subdirectories 204 | // Use the project target directory to watch for build folder creation 205 | const watchPath = project.target || ''; 206 | if (!watchPath) { 207 | console.log('No project target path available'); 208 | return null; 209 | } 210 | 211 | console.log('Setting up file watcher for path:', watchPath); 212 | 213 | // Watch only for specific build output files that we care about 214 | // This reduces the number of events significantly 215 | const patterns = [ 216 | 'build/**/zephyr.elf', 217 | 'build/**/zephyr.hex', 218 | 'build/**/merged.hex', 219 | 'build/**/dfu_application.zip', 220 | 'build/**/dfu_application.zip_manifest.json' 221 | ]; 222 | 223 | const watchers: vscode.FileSystemWatcher[] = []; 224 | let debounceTimer: NodeJS.Timeout | null = null; 225 | 226 | const debouncedChangeHandler = () => { 227 | if (debounceTimer) { 228 | clearTimeout(debounceTimer); 229 | } 230 | 231 | debounceTimer = setTimeout(() => { 232 | console.log('Build assets change detected (debounced)'); 233 | onChanged(); 234 | debounceTimer = null; 235 | }, 2000); // 2 second debounce to let build process complete 236 | }; 237 | 238 | // Create watchers for each specific file pattern 239 | for (const pattern of patterns) { 240 | try { 241 | const filePattern = new vscode.RelativePattern(watchPath, pattern); 242 | console.log(`Creating watcher for pattern: ${pattern} in ${watchPath}`); 243 | const watcher = vscode.workspace.createFileSystemWatcher(filePattern); 244 | 245 | watcher.onDidCreate((uri) => { 246 | console.log(`File created: ${uri.fsPath}`); 247 | debouncedChangeHandler(); 248 | }); 249 | watcher.onDidChange((uri) => { 250 | console.log(`File changed: ${uri.fsPath}`); 251 | debouncedChangeHandler(); 252 | }); 253 | watcher.onDidDelete((uri) => { 254 | console.log(`File deleted: ${uri.fsPath}`); 255 | debouncedChangeHandler(); 256 | }); 257 | 258 | watchers.push(watcher); 259 | } catch (error) { 260 | console.error(`Failed to create watcher for pattern ${pattern}:`, error); 261 | } 262 | } 263 | 264 | if (watchers.length === 0) { 265 | console.log('No watchers created'); 266 | return null; 267 | } 268 | 269 | console.log(`File watchers created successfully for ${watchers.length} patterns`); 270 | 271 | // Return a composite watcher that disposes all individual watchers 272 | return { 273 | dispose: () => { 274 | if (debounceTimer) { 275 | clearTimeout(debounceTimer); 276 | } 277 | watchers.forEach(w => w.dispose()); 278 | } 279 | } as vscode.FileSystemWatcher; 280 | 281 | } catch (error) { 282 | console.error("Failed to create file watcher for build assets:", error); 283 | return null; 284 | } 285 | } 286 | 287 | /** 288 | * Open build folder in file manager 289 | */ 290 | public static async openBuildFolder(project: ProjectConfig): Promise { 291 | const buildPath = this.getBuildPath(project); 292 | 293 | if (!buildPath || !fs.existsSync(buildPath)) { 294 | vscode.window.showErrorMessage("Build directory does not exist. Build the project first."); 295 | return; 296 | } 297 | 298 | try { 299 | await vscode.commands.executeCommand("revealFileInOS", vscode.Uri.file(buildPath)); 300 | } catch (error) { 301 | vscode.window.showErrorMessage(`Failed to open build folder: ${error}`); 302 | } 303 | } 304 | 305 | /** 306 | * Reveal specific build asset file in file manager 307 | */ 308 | public static async revealBuildAsset(filePath: string): Promise { 309 | if (!fs.existsSync(filePath)) { 310 | vscode.window.showErrorMessage("Build asset file does not exist. The file may have been moved or deleted."); 311 | return; 312 | } 313 | 314 | try { 315 | await vscode.commands.executeCommand("revealFileInOS", vscode.Uri.file(filePath)); 316 | } catch (error) { 317 | vscode.window.showErrorMessage(`Failed to reveal build asset: ${error}`); 318 | } 319 | } 320 | } 321 | -------------------------------------------------------------------------------- /src/config/settings-manager.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @author Jared Wolff 3 | * @copyright Circuit Dojo LLC 4 | * @license Apache 2.0 5 | */ 6 | 7 | import * as vscode from "vscode"; 8 | import * as os from "os"; 9 | import * as path from "path"; 10 | import { TOOLS_FOLDER_NAME, getPlatformConfig } from "./constants"; 11 | 12 | export class SettingsManager { 13 | private static readonly CONFIG_SECTION = "zephyr-tools"; 14 | 15 | static getToolsDirectory(): string { 16 | const config = vscode.workspace.getConfiguration(this.CONFIG_SECTION); 17 | const customPath = config.get("paths.toolsDirectory"); 18 | return customPath || path.join(os.homedir(), TOOLS_FOLDER_NAME); 19 | } 20 | 21 | static async setToolsDirectory(toolsPath: string): Promise { 22 | const config = vscode.workspace.getConfiguration(this.CONFIG_SECTION); 23 | await config.update("paths.toolsDirectory", toolsPath, vscode.ConfigurationTarget.Global); 24 | } 25 | 26 | static getPythonExecutable(): string | undefined { 27 | const config = vscode.workspace.getConfiguration(this.CONFIG_SECTION); 28 | const customPath = config.get("paths.pythonExecutable"); 29 | return customPath || undefined; 30 | } 31 | 32 | static async setPythonExecutable(pythonPath: string): Promise { 33 | const config = vscode.workspace.getConfiguration(this.CONFIG_SECTION); 34 | await config.update("paths.pythonExecutable", pythonPath, vscode.ConfigurationTarget.Global); 35 | } 36 | 37 | static getZephyrBase(): string | undefined { 38 | const config = vscode.workspace.getConfiguration(this.CONFIG_SECTION); 39 | const customPath = config.get("paths.zephyrBase"); 40 | return customPath || undefined; 41 | } 42 | 43 | static async setZephyrBase(zephyrBasePath: string): Promise { 44 | const config = vscode.workspace.getConfiguration(this.CONFIG_SECTION); 45 | // Use Global scope if no workspace is open, otherwise use Workspace scope 46 | const target = vscode.workspace.workspaceFolders ? 47 | vscode.ConfigurationTarget.Workspace : 48 | vscode.ConfigurationTarget.Global; 49 | await config.update("paths.zephyrBase", zephyrBasePath, target); 50 | } 51 | 52 | static getWestExecutable(): string | undefined { 53 | const config = vscode.workspace.getConfiguration(this.CONFIG_SECTION); 54 | const customPath = config.get("paths.westExecutable"); 55 | return customPath || undefined; 56 | } 57 | 58 | static async setWestExecutable(westPath: string): Promise { 59 | const config = vscode.workspace.getConfiguration(this.CONFIG_SECTION); 60 | await config.update("paths.westExecutable", westPath, vscode.ConfigurationTarget.Global); 61 | } 62 | 63 | static getAllPaths(): string[] { 64 | const config = vscode.workspace.getConfiguration(this.CONFIG_SECTION); 65 | return config.get("paths.allPaths") || []; 66 | } 67 | 68 | static async setAllPaths(paths: string[]): Promise { 69 | const config = vscode.workspace.getConfiguration(this.CONFIG_SECTION); 70 | await config.update("paths.allPaths", paths, vscode.ConfigurationTarget.Global); 71 | } 72 | 73 | static async addPath(newPath: string): Promise { 74 | const currentPaths = this.getAllPaths(); 75 | if (!currentPaths.includes(newPath)) { 76 | await this.setAllPaths([...currentPaths, newPath]); 77 | } 78 | } 79 | 80 | static async removePath(pathToRemove: string): Promise { 81 | const currentPaths = this.getAllPaths(); 82 | const filteredPaths = currentPaths.filter(p => p !== pathToRemove); 83 | await this.setAllPaths(filteredPaths); 84 | } 85 | 86 | static getAllConfiguredPaths(): { [key: string]: string | string[] | undefined } { 87 | return { 88 | toolsDirectory: this.getToolsDirectory(), 89 | pythonExecutable: this.getPythonExecutable(), 90 | zephyrBase: this.getZephyrBase(), 91 | westExecutable: this.getWestExecutable(), 92 | allPaths: this.getAllPaths() 93 | }; 94 | } 95 | 96 | static async detectZephyrBase(): Promise { 97 | const fs = await import("fs-extra"); 98 | 99 | // Check if ZEPHYR_BASE is already configured 100 | const configured = this.getZephyrBase(); 101 | if (configured) { 102 | return configured; 103 | } 104 | 105 | // Check workspace folders for a zephyr directory 106 | if (vscode.workspace.workspaceFolders) { 107 | for (const folder of vscode.workspace.workspaceFolders) { 108 | const zephyrPath = path.join(folder.uri.fsPath, "zephyr"); 109 | const versionFile = path.join(zephyrPath, "VERSION"); 110 | 111 | // Check if this looks like a Zephyr installation 112 | if (await fs.pathExists(versionFile)) { 113 | return zephyrPath; 114 | } 115 | } 116 | } 117 | 118 | return undefined; 119 | } 120 | 121 | // Environment variable management 122 | static getEnvironmentVariables(): { [key: string]: string } { 123 | const config = vscode.workspace.getConfiguration(this.CONFIG_SECTION); 124 | return config.get<{ [key: string]: string }>("environment.variables") || {}; 125 | } 126 | 127 | static async setEnvironmentVariables(vars: { [key: string]: string }): Promise { 128 | const config = vscode.workspace.getConfiguration(this.CONFIG_SECTION); 129 | await config.update("environment.variables", vars, vscode.ConfigurationTarget.Global); 130 | } 131 | 132 | static async setEnvironmentVariable(name: string, value: string): Promise { 133 | const vars = this.getEnvironmentVariables(); 134 | vars[name] = value; 135 | await this.setEnvironmentVariables(vars); 136 | } 137 | 138 | static getEnvironmentVariable(name: string): string | undefined { 139 | const vars = this.getEnvironmentVariables(); 140 | return vars[name]; 141 | } 142 | 143 | // Convenience methods for specific environment variables 144 | static getZephyrSdkInstallDir(): string | undefined { 145 | return this.getEnvironmentVariable("ZEPHYR_SDK_INSTALL_DIR"); 146 | } 147 | 148 | static async setZephyrSdkInstallDir(path: string): Promise { 149 | await this.setEnvironmentVariable("ZEPHYR_SDK_INSTALL_DIR", path); 150 | } 151 | 152 | static getZephyrToolchainVariant(): string | undefined { 153 | return this.getEnvironmentVariable("ZEPHYR_TOOLCHAIN_VARIANT"); 154 | } 155 | 156 | static async setZephyrToolchainVariant(variant: string): Promise { 157 | await this.setEnvironmentVariable("ZEPHYR_TOOLCHAIN_VARIANT", variant); 158 | } 159 | 160 | static getVirtualEnv(): string | undefined { 161 | return this.getEnvironmentVariable("VIRTUAL_ENV"); 162 | } 163 | 164 | static async setVirtualEnv(path: string): Promise { 165 | await this.setEnvironmentVariable("VIRTUAL_ENV", path); 166 | } 167 | 168 | // Helper to build complete environment for command execution 169 | static buildEnvironmentForExecution(): { [key: string]: string } { 170 | // Start with system environment 171 | const env: { [key: string]: string } = {}; 172 | for (const [key, value] of Object.entries(process.env)) { 173 | if (value !== undefined) { 174 | env[key] = value; 175 | } 176 | } 177 | 178 | // Add all configured environment variables from settings 179 | const envVars = this.getEnvironmentVariables(); 180 | for (const [key, value] of Object.entries(envVars)) { 181 | if (value) { 182 | env[key] = value; 183 | } 184 | } 185 | 186 | // Set VIRTUAL_ENV to current tools directory 187 | const pythonenv = path.join(this.getToolsDirectory(), "env"); 188 | env["VIRTUAL_ENV"] = pythonenv; 189 | 190 | // Build PATH with all tool paths 191 | const allPaths = this.getAllPaths(); 192 | let pathComponents: string[] = []; 193 | 194 | // Add Python environment paths first 195 | pathComponents.push(path.join(pythonenv, "Scripts")); 196 | pathComponents.push(path.join(pythonenv, "bin")); 197 | 198 | // Add all saved tool paths 199 | pathComponents = pathComponents.concat(allPaths); 200 | 201 | // Add existing PATH 202 | if (env.PATH) { 203 | pathComponents.push(env.PATH); 204 | } 205 | 206 | // Join path components with platform-appropriate separator 207 | const platformConfig = getPlatformConfig(); 208 | env.PATH = pathComponents.filter(p => p).join(platformConfig.pathDivider); 209 | 210 | return env; 211 | } 212 | 213 | // probe-rs specific settings 214 | static getProbeRsChipName(): string | undefined { 215 | const config = vscode.workspace.getConfiguration(this.CONFIG_SECTION); 216 | return config.get("probeRs.chipName") || undefined; 217 | } 218 | 219 | static async setProbeRsChipName(chipName: string): Promise { 220 | const config = vscode.workspace.getConfiguration(this.CONFIG_SECTION); 221 | await config.update("probeRs.chipName", chipName, vscode.ConfigurationTarget.Workspace); 222 | } 223 | 224 | static getProbeRsProbeId(): string | undefined { 225 | const config = vscode.workspace.getConfiguration(this.CONFIG_SECTION); 226 | return config.get("probeRs.probeId") || undefined; 227 | } 228 | 229 | static async setProbeRsProbeId(probeId: string): Promise { 230 | const config = vscode.workspace.getConfiguration(this.CONFIG_SECTION); 231 | await config.update("probeRs.probeId", probeId, vscode.ConfigurationTarget.Workspace); 232 | } 233 | 234 | static getProbeRsPreverify(): boolean { 235 | const config = vscode.workspace.getConfiguration(this.CONFIG_SECTION); 236 | return config.get("probeRs.preverify") || false; 237 | } 238 | 239 | static async setProbeRsPreverify(enabled: boolean): Promise { 240 | const config = vscode.workspace.getConfiguration(this.CONFIG_SECTION); 241 | await config.update("probeRs.preverify", enabled, vscode.ConfigurationTarget.Workspace); 242 | } 243 | 244 | static getProbeRsVerify(): boolean { 245 | const config = vscode.workspace.getConfiguration(this.CONFIG_SECTION); 246 | return config.get("probeRs.verify") || false; 247 | } 248 | 249 | static async setProbeRsVerify(enabled: boolean): Promise { 250 | const config = vscode.workspace.getConfiguration(this.CONFIG_SECTION); 251 | await config.update("probeRs.verify", enabled, vscode.ConfigurationTarget.Workspace); 252 | } 253 | 254 | // Serial port settings 255 | static getSerialPort(): string | undefined { 256 | const config = vscode.workspace.getConfiguration(this.CONFIG_SECTION); 257 | return config.get("serial.port") || undefined; 258 | } 259 | 260 | static async setSerialPort(port: string): Promise { 261 | const config = vscode.workspace.getConfiguration(this.CONFIG_SECTION); 262 | await config.update("serial.port", port, vscode.ConfigurationTarget.Workspace); 263 | } 264 | 265 | static getSerialSaveLogsToFile(): boolean { 266 | const config = vscode.workspace.getConfiguration(this.CONFIG_SECTION); 267 | return config.get("serial.saveLogsToFile") || false; 268 | } 269 | 270 | static async setSerialSaveLogsToFile(enabled: boolean): Promise { 271 | const config = vscode.workspace.getConfiguration(this.CONFIG_SECTION); 272 | await config.update("serial.saveLogsToFile", enabled, vscode.ConfigurationTarget.Workspace); 273 | } 274 | 275 | // Newtmgr settings 276 | static getNewtmgrBaudRate(): number { 277 | const config = vscode.workspace.getConfiguration(this.CONFIG_SECTION); 278 | return config.get("newtmgr.baudRate") || 1000000; 279 | } 280 | 281 | static async setNewtmgrBaudRate(baudRate: number): Promise { 282 | const config = vscode.workspace.getConfiguration(this.CONFIG_SECTION); 283 | await config.update("newtmgr.baudRate", baudRate, vscode.ConfigurationTarget.Workspace); 284 | } 285 | 286 | } -------------------------------------------------------------------------------- /src/commands/project-management.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @author Jared Wolff 3 | * @copyright Circuit Dojo LLC 4 | * @license Apache 2.0 5 | */ 6 | 7 | import * as vscode from "vscode"; 8 | import * as util from "util"; 9 | import * as cp from "child_process"; 10 | import * as fs from "fs-extra"; 11 | import * as path from "path"; 12 | import { GlobalConfig, ProjectConfig, ZephyrTask } from "../types"; 13 | import { ProjectConfigManager } from "../config"; 14 | import { QuickPickManager, DialogManager, OutputChannelManager, StatusBarManager } from "../ui"; 15 | import { TaskManager } from "../tasks"; 16 | import { installPythonDependencies } from "../environment"; 17 | import { platform, SettingsManager } from "../config"; 18 | 19 | export async function changeProjectCommand( 20 | config: GlobalConfig, 21 | context: vscode.ExtensionContext 22 | ): Promise { 23 | const project = await ProjectConfigManager.load(context); 24 | 25 | if (!config.isSetup) { 26 | vscode.window.showErrorMessage("Run `Zephyr Tools: Setup` command first."); 27 | return; 28 | } 29 | 30 | const output = OutputChannelManager.getChannel(); 31 | output.clear(); 32 | 33 | // Get the workspace root 34 | const rootPaths = vscode.workspace.workspaceFolders; 35 | if (!rootPaths) { 36 | return; 37 | } 38 | const rootPath = rootPaths[0].uri; 39 | 40 | const exec = util.promisify(cp.exec); 41 | 42 | // Get manifest path 43 | const cmd = "west config manifest.path"; 44 | const result = await exec(cmd, { env: SettingsManager.buildEnvironmentForExecution(), cwd: rootPath.fsPath }); 45 | 46 | if (result.stderr) { 47 | output.append(result.stderr); 48 | output.show(); 49 | return; 50 | } 51 | 52 | // Find all CMakeLists.txt files with `project(` in them 53 | const projectList = await getProjectList(vscode.Uri.joinPath(rootPath, result.stdout.trim())); 54 | console.log("Available projects:", projectList); 55 | 56 | // Turn that into a project selection 57 | const selectedProject = await QuickPickManager.selectProject(projectList); 58 | 59 | if (selectedProject) { 60 | console.log("Changing project to " + selectedProject); 61 | vscode.window.showInformationMessage(`Project changed to ${selectedProject}`); 62 | project.target = selectedProject; 63 | await ProjectConfigManager.save(context, project); 64 | 65 | // Update status bar 66 | StatusBarManager.updateProjectStatusBar(project.target); 67 | } 68 | } 69 | 70 | export async function initRepoCommand( 71 | config: GlobalConfig, 72 | context: vscode.ExtensionContext, 73 | dest: vscode.Uri 74 | ): Promise { 75 | const output = OutputChannelManager.getChannel(); 76 | output.show(); 77 | 78 | // Load and update project configuration 79 | const project = await ProjectConfigManager.load(context); 80 | 81 | // Set isInitializing flag 82 | project.isInitializing = true; 83 | await ProjectConfigManager.save(context, project); 84 | 85 | // Schedule a delayed sidebar reveal to ensure it happens even if the immediate reveal doesn't work 86 | setTimeout(async () => { 87 | try { 88 | await vscode.commands.executeCommand('workbench.view.extension.zephyr-tools'); 89 | } catch (error) { 90 | // Silently fail if commands are not available 91 | } 92 | }, 2000); 93 | 94 | try { 95 | const taskName = "Zephyr Tools: Init Repo"; 96 | 97 | // Get the root path of the workspace 98 | const rootPath = getRootPath(); 99 | 100 | // Check if we're in the right workspace 101 | if (rootPath?.fsPath !== dest.fsPath) { 102 | console.log("Setting task!"); 103 | 104 | // Reset isInitializing flag since we're switching workspaces 105 | project.isInitializing = false; 106 | await ProjectConfigManager.save(context, project); 107 | 108 | // Set init-repo task next 109 | const task: ZephyrTask = { name: "zephyr-tools.init-repo", data: dest }; 110 | await ProjectConfigManager.savePendingTask(context, task); 111 | 112 | // Change workspace 113 | await vscode.commands.executeCommand("vscode.openFolder", dest); 114 | return; 115 | } 116 | 117 | // Set .vscode/settings.json 118 | const settings = { 119 | "git.enabled": false, 120 | "git.path": null, 121 | "git.autofetch": false, 122 | }; 123 | 124 | // Make .vscode dir and settings.json 125 | await fs.mkdirp(path.join(dest.fsPath, ".vscode")); 126 | await fs.writeFile(path.join(dest.fsPath, ".vscode", "settings.json"), JSON.stringify(settings)); 127 | 128 | // Options for Shell execution 129 | const shellOptions: vscode.ShellExecutionOptions = { 130 | env: <{ [key: string]: string }>SettingsManager.buildEnvironmentForExecution(), 131 | cwd: dest.fsPath, 132 | }; 133 | 134 | // Check if .west is already here 135 | const exists = await fs.pathExists(path.join(dest.fsPath, ".west")); 136 | 137 | if (!exists) { 138 | // Get repository URL 139 | const url = await DialogManager.getRepositoryUrl(); 140 | if (!url) { 141 | // Reset isInitializing flag on cancellation 142 | const project = await ProjectConfigManager.load(context); 143 | project.isInitializing = false; 144 | await ProjectConfigManager.save(context, project); 145 | 146 | vscode.window.showErrorMessage("Zephyr Tools: invalid repository url provided."); 147 | return; 148 | } 149 | 150 | // Ask for branch 151 | const branch = await DialogManager.getBranchName(); 152 | 153 | // TODO: determine choices for west.yml 154 | const manifest = "west.yml"; 155 | 156 | // git clone to destination 157 | let cmd = `west init -m ${url} --mf ${manifest}`; 158 | 159 | // Set branch option 160 | if (branch && branch !== "") { 161 | console.log(`Branch '${branch}'`); 162 | cmd = cmd + ` --mr ${branch}`; 163 | } 164 | 165 | const exec = new vscode.ShellExecution(cmd, shellOptions); 166 | 167 | // Task 168 | const task = new vscode.Task( 169 | { type: "zephyr-tools", command: taskName }, 170 | vscode.TaskScope.Workspace, 171 | taskName, 172 | "zephyr-tools", 173 | exec, 174 | ); 175 | 176 | // Start execution 177 | await TaskManager.push(task, { ignoreError: true, lastTask: false }); 178 | } 179 | 180 | // `west update` 181 | const updateCmd = "west update"; 182 | const updateExec = new vscode.ShellExecution(updateCmd, shellOptions); 183 | 184 | // Task 185 | const updateTask = new vscode.Task( 186 | { type: "zephyr-tools", command: taskName }, 187 | vscode.TaskScope.Workspace, 188 | taskName, 189 | "zephyr-tools", 190 | updateExec, 191 | ); 192 | 193 | // Callback to run after west update completes 194 | const westUpdateCallback = async (data: any) => { 195 | output.appendLine("[INIT] West update completed, determining zephyr base path..."); 196 | 197 | // Get zephyr BASE 198 | let base = "zephyr"; 199 | 200 | const exec = util.promisify(cp.exec); 201 | const cmd = "west list -f {path:28}"; 202 | output.appendLine(`[INIT] Running: ${cmd}`); 203 | 204 | const result = await exec(cmd, { env: SettingsManager.buildEnvironmentForExecution(), cwd: dest.fsPath }); 205 | if (result.stderr) { 206 | output.append(result.stderr); 207 | output.show(); 208 | } else { 209 | result.stdout.split("\n").forEach((line: string) => { 210 | if (line.includes("zephyr")) { 211 | base = line.trim(); 212 | } 213 | }); 214 | } 215 | output.appendLine(`[INIT] Determined zephyr base path: ${base}`); 216 | 217 | // Install python dependencies 218 | const pythonenv = path.join(SettingsManager.getToolsDirectory(), "env"); 219 | const venvPython = platform === "win32" 220 | ? path.join(pythonenv, "Scripts", "python.exe") 221 | : path.join(pythonenv, "bin", "python"); 222 | 223 | const installCmd = `"${venvPython}" -m pip install -r ${path.join(base, "scripts", "requirements.txt")}`; 224 | output.appendLine(`[INIT] Starting pip install: ${installCmd}`); 225 | 226 | const installExec = new vscode.ShellExecution(installCmd, shellOptions); 227 | 228 | // Task 229 | const installTask = new vscode.Task( 230 | { type: "zephyr-tools", command: taskName }, 231 | vscode.TaskScope.Workspace, 232 | taskName, 233 | "zephyr-tools", 234 | installExec, 235 | ); 236 | 237 | // Final callback after pip install completes 238 | const done = async (data: any) => { 239 | // Set the isInit flag 240 | const project = await ProjectConfigManager.load(context); 241 | project.isInit = true; 242 | project.isInitializing = false; 243 | await ProjectConfigManager.save(context, project); 244 | }; 245 | 246 | // Start execution 247 | await TaskManager.push(installTask, { 248 | ignoreError: false, 249 | lastTask: true, 250 | successMessage: "Init complete!", 251 | callback: done, 252 | callbackData: { dest: dest }, 253 | }); 254 | }; 255 | 256 | // Start execution - west update with callback to run pip install after completion 257 | output.appendLine("[INIT] Starting west update..."); 258 | await TaskManager.push(updateTask, { 259 | ignoreError: false, 260 | lastTask: false, 261 | callback: westUpdateCallback, 262 | callbackData: { dest: dest }, 263 | }); 264 | } catch (error) { 265 | // Reset isInitializing flag on error 266 | const project = await ProjectConfigManager.load(context); 267 | project.isInitializing = false; 268 | await ProjectConfigManager.save(context, project); 269 | 270 | let text = ""; 271 | if (typeof error === "string") { 272 | text = error; 273 | } else if (error instanceof Error) { 274 | text = error.message; 275 | } 276 | 277 | output.append(text); 278 | vscode.window.showErrorMessage("Zephyr Tools: Init Repo error. See output for details."); 279 | } 280 | } 281 | 282 | async function getProjectList(folder: vscode.Uri): Promise { 283 | const files = await vscode.workspace.fs.readDirectory(folder); 284 | const projects: string[] = []; 285 | 286 | const queue = [...files.map(([name, type]) => ({ name, type, path: folder }))]; 287 | 288 | while (queue.length > 0) { 289 | const file = queue.shift(); 290 | if (!file) break; 291 | 292 | if (file.name.includes("CMakeLists.txt")) { 293 | // Check the file content 294 | const filepath = vscode.Uri.joinPath(file.path, file.name); 295 | const contents = await vscode.workspace.openTextDocument(filepath).then(document => { 296 | return document.getText(); 297 | }); 298 | 299 | if (contents.includes("project(")) { 300 | const project = path.parse(filepath.fsPath); 301 | projects.push(project.dir); 302 | } 303 | } else if (file.name.includes("build") || file.name.includes(".git")) { 304 | // Skip these directories 305 | } else if (file.type === vscode.FileType.Directory) { 306 | const subPath = vscode.Uri.joinPath(file.path, file.name); 307 | const subfolders = await vscode.workspace.fs.readDirectory(subPath); 308 | 309 | for (const [subName, subType] of subfolders) { 310 | queue.push({ 311 | name: path.join(file.name, subName), 312 | type: subType, 313 | path: file.path 314 | }); 315 | } 316 | } 317 | } 318 | 319 | return projects; 320 | } 321 | 322 | function getRootPath(): vscode.Uri | undefined { 323 | if (vscode.workspace.workspaceFolders?.length ?? 0 > 0) { 324 | return vscode.workspace.workspaceFolders?.[0].uri; 325 | } 326 | return undefined; 327 | } 328 | --------------------------------------------------------------------------------