Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 38 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
# Repository Guidelines

## Project Structure & Module Organization
- `bin/kaluma.js`: CLI entry and command definitions.
- `lib/`: implementation for commands (`flash.js`, `erase.js`, `bundle.js`, `put.js`, `get.js`).
- `util/`: shared helpers (`ymodem.js`, `buffered-serial.js`, `eval.js`).
- `test/`: example Kaluma programs you can bundle/flash to a device.
- `.eslintrc.json`: code style (Airbnb base, 2‑space indent).

## Build, Test, and Development Commands
- Install deps: `npm install`
- Run CLI locally: `node bin/kaluma.js --help` or `npx kaluma`
- List ports: `node bin/kaluma.js ports`
- Bundle: `node bin/kaluma.js bundle test/index.js --minify`
- Flash + bundle + shell: `node bin/kaluma.js flash test/index.js -b -s`
- Lint: `npx eslint .`
- Tests: no unit test runner is configured (`npm test` is a placeholder). Use the samples under `test/` for manual verification with hardware.

## Coding Style & Naming Conventions
- Indentation: 2 spaces; follow Airbnb style enforced by ESLint.
- Names: `camelCase` for variables/functions, hyphenated filenames in `util/` and small lowercase modules in `lib/`.
- Structure: keep CLI parsing and UX in `bin/kaluma.js`; place reusable logic in `lib/`; low‑level serial/IO utilities in `util/`.

## Testing Guidelines
- Framework: none configured. Prefer manual integration tests on real devices.
- Smoke tests: use `test/index.js` or `test/blink.js` with `flash -b -s` and observe console output.
- File transfer: validate `put`/`get` paths are absolute on device and compare sizes reported by CLI.
- Bundling: verify output name/size and source maps when `--sourcemap` is used.

## Commit & Pull Request Guidelines
- Branching: create a branch for new work. Use prefixes like `feat/<topic>`, `fix/<topic>`, `chore/<topic>`, `docs/<topic>`. Example: `git checkout -b feat/flash-shell-mode`.
- Commits: imperative, concise messages; optional scope (e.g., `flash: handle shell mode`). Reference issues with `#123` when applicable.
- PRs: include a summary, repro steps, device/board and OS details, Node version, and console output/screenshots. Update README when flags/UX change.
- Quality: run `npx eslint .`, try `ports`, `bundle`, `flash`, `put`, `get` locally, and keep changes minimal and focused.

## Security & Configuration Tips
- Default port query uses Raspberry Pi VID `@2e8a`. Run `ports` before flashing to confirm the target device.
- Required Node: `>=16` (tested on 16/18/20/22). Avoid adding networked dependencies without discussion.
15 changes: 15 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
# Changelog

All notable changes to this project are documented here.

## [1.5.0] - 2025-08-28
- feat(flash): add `kaluma flash read <file>` to pull the current program from a connected Kaluma device using YMODEM. Does not overwrite existing files.
- feat(flash/read): add options `--stdout`, `--timestamp`, and `--quiet`.
- docs: update README usage (flash read), CLI help examples, and AGENTS.md (branching guidance).
- meta: align package.json license to `Apache-2.0` to match LICENSE/README.
- meta: add contributor Andrew Chalkley <andrew@chalkley.org> to package.json.
- feat(compat): support Node 20/22 by upgrading to `serialport@^12` and updating API usage.
- refactor(read): switch to size-first read (`.flash -s` then `.flash -r`) with CRLF normalization and ANSI stripping; remove internal YMODEM receive path.

## [1.4.0]
- Refer to git history for details prior to this release.
35 changes: 34 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ Kaluma CLI is a command-line tool to program devices and boards running [Kaluma]
- [`help` command](#help-command)
- [`ports` command](#ports-command)
- [`flash` command](#flash-command)
- [Read flashed program](#read-flashed-program)
- [`erase` command](#erase-command)
- [`shell` command](#shell-command)
- [`bundle` command](#bundle-command)
Expand All @@ -27,7 +28,9 @@ Install CLI via `npm` globally.
npm install -g @kaluma/cli
```

If you failed to install, sometime you need to install by building from source as below (e.g. Apple M1, Raspberry Pi, or some Linux).
Supported Node versions: 16, 18, 20, 22.

If you failed to install, sometimes you need to install by building from source as below (e.g., Apple Silicon, Raspberry Pi, or some Linux).

```sh
npm install -g @kaluma/cli --unsafe-perm --build-from-source
Expand Down Expand Up @@ -114,6 +117,36 @@ kaluma flash index.js --bundle
kaluma flash index.js --shell --bundle
```

#### Read flashed program

Read current user code from the device to a local file.

```sh
kaluma flash read <file> [--port <port>] [--stdout] [--timestamp] [--quiet]
```

- `<file>`: Output path on host to save code. The command aborts if the file already exists (unless `--stdout`). With `--timestamp`, the filename gets `-YYYYMMDDTHHMMSS` appended, or a `backup-<ts>.js` is created if `<file>` is a directory.
- `-p, --port <port>` option: See [`flash`](#flash-command) command.
- `--stdout`: Write program to standard output instead of a file.
- `-t, --timestamp`: Append a timestamp to the filename or generate a timestamped name in a target directory.
- `-q, --quiet`: Suppress progress dots and summary lines.

Examples:

```sh
# read current program to 'backup.js'
kaluma flash read backup.js

# read to a specific path using an explicit port
kaluma flash read ./backup/usercode.js --port /dev/tty.usbmodem1441

# write to stdout (quiet) and tee to file
kaluma flash read - --stdout --quiet | tee backup.js

# save with timestamped filename in current directory
kaluma flash read ./ --timestamp
```

### `erase` command

Erase code in device.
Expand Down
118 changes: 109 additions & 9 deletions bin/kaluma.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,14 @@

const fs = require("fs");
const path = require("path");
const { program } = require("commander");
const SerialPort = require("serialport");
const { program, Command } = require("commander");
const { SerialPort } = require("serialport");
const config = require("../package.json");
const colors = require("colors/safe");
const filesize = require("file-size");

const flash = require("../lib/flash");
const flashRead = require("../lib/flash_read");
const erase = require("../lib/erase");
const bundle = require("../lib/bundle");
const put = require("../lib/put");
Expand Down Expand Up @@ -113,6 +114,17 @@ async function findPort(portOrQuery, exit) {

program.version(config.version);

// Global examples in help
program.addHelpText(
"after",
`
Examples:
$ kaluma ports
$ kaluma flash index.js -b -s
$ kaluma flash read backup.js
`
);

program
.command("shell")
.description("[EXPERIMENTAL] shell connect (exit: ctrl+z)")
Expand All @@ -122,7 +134,7 @@ program
const port = await findPort(options.port, true);

// shell connect
const serial = new SerialPort(port, serialOptions);
const serial = new SerialPort({ path: port, ...serialOptions });
serial.open(async (err) => {
if (err) {
console.error(err);
Expand Down Expand Up @@ -168,9 +180,10 @@ program
});
});

program
.command("flash <file>")
.description("flash code (.js file) to device")
// Define parent 'flash' command. Parent handles default write: `kaluma flash <file>`
const flashCmd = new Command("flash")
.description("flash operations: write code to device or read from device")
.argument("<file>")
.option("-p, --port <port>", optionDescriptions.port, "@2e8a")
.option("--no-load", optionDescriptions.noLoad, false)
.option("-b, --bundle", optionDescriptions.bundle, false)
Expand Down Expand Up @@ -200,7 +213,7 @@ program
}

// flash code
const serial = new SerialPort(port, serialOptions);
const serial = new SerialPort({ path: port, ...serialOptions });
serial.open(async (err) => {
if (err) {
console.error(err);
Expand Down Expand Up @@ -252,6 +265,93 @@ program
});
});

// flash read: `kaluma flash read <file>`
flashCmd
.command("read <file>")
.description("read current flashed code from device to file")
.option("-p, --port <port>", optionDescriptions.port, "@2e8a")
.option("--stdout", "write program to stdout instead of a file")
.option("-q, --quiet", "suppress progress output", false)
.option("-t, --timestamp", "append YYYYMMDDTHHMMSS to filename or generate backup-<ts>.js in directory", false)
.action(async function (dest, options) {
try {
let destPath = dest;
// Resolve timestamped naming
if (options.timestamp) {
const ts = new Date().toISOString().replace(/[:.]/g, "").slice(0, 15);
const isDir = fs.existsSync(dest) && fs.statSync(dest).isDirectory();
if (isDir || /[\\\/]$/.test(dest)) {
destPath = path.join(dest, `backup-${ts}.js`);
} else {
const dir = path.dirname(dest);
const base = path.basename(dest);
const dot = base.lastIndexOf(".");
const name = dot > 0 ? base.slice(0, dot) : base;
const ext = dot > 0 ? base.slice(dot) : ".js";
destPath = path.join(dir, `${name}-${ts}${ext}`);
}
}
const absPath = options.stdout ? null : path.resolve(destPath);
if (!options.stdout) {
if (fs.existsSync(absPath)) {
console.log(`file already exists: ${destPath}`);
return;
}
}

// find port
const port = await findPort(options.port, true);

// read
const serial = new SerialPort({ path: port, ...serialOptions });
serial.open(async (err) => {
if (err) {
console.error(err);
} else {
if (!options.quiet && !options.stdout) {
console.log(`connected to ${port}`);
process.stdout.write(colors.grey("reading "));
}
try {
const result = await flashRead(
serial,
absPath || "-",
(!options.quiet && !options.stdout)
? () => process.stdout.write(colors.grey("."))
: undefined,
{ stdout: !!options.stdout }
);
if (!options.quiet && !options.stdout) process.stdout.write("\r\n");
if (serial.isOpen) serial.close();
if (!options.stdout && !options.quiet) {
console.log(
`${colorName(path.basename(absPath))} ${colorSize(
result.totalBytes || result.receivedBytes || 0
)} saved`
);
}
} catch (e) {
console.log(e);
}
}
});
} catch (e) {
console.log(e);
}
});

// Flash help examples
flashCmd.addHelpText(
"after",
`
Examples:
$ kaluma flash index.js -b -s
$ kaluma flash read backup.js
`
);

program.addCommand(flashCmd);

program
.command("erase")
.description("erase code in device")
Expand All @@ -261,7 +361,7 @@ program
const port = await findPort(options.port, true);

// erase
const serial = new SerialPort(port, serialOptions);
const serial = new SerialPort({ path: port, ...serialOptions });
serial.open(async (err) => {
if (err) {
console.error(err);
Expand Down Expand Up @@ -316,7 +416,7 @@ program
const port = await findPort(options.port, true);

// put
const serial = new SerialPort(port, serialOptions);
const serial = new SerialPort({ path: port, ...serialOptions });
serial.open(async (err) => {
if (err) {
console.error(err);
Expand Down
Loading