@hienlh/ppm 0.13.13 → 0.13.15

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,9 +1,19 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.13.15] - 2026-04-24
4
+
5
+ ### Fixed
6
+ - **File compare with absolute paths**: Comparing files outside the project (e.g. `/tmp/`) no longer fails with "Path traversal not allowed" — absolute paths now use `readSystemFile` instead of project-scoped read
7
+
8
+ ## [0.13.14] - 2026-04-24
9
+
10
+ ### Fixed
11
+ - **DB CLI run multi-statement**: PostgreSQL `ppm db run` now splits SQL into individual statements and executes within `sql.begin()` transaction. Handles strings, comments, dollar-quoting. Strips user-supplied `BEGIN`/`COMMIT`/`ROLLBACK` to avoid conflicts with managed transaction
12
+
3
13
  ## [0.13.13] - 2026-04-24
4
14
 
5
15
  ### Added
6
- - **DB CLI run command**: `ppm db run <name> <file.sql>` executes SQL files against saved connections. Supports multi-statement files and transactions (`BEGIN...COMMIT`). Respects readonly flag. Works with both SQLite (`db.exec()`) and PostgreSQL (`sql.unsafe()`)
16
+ - **DB CLI run command**: `ppm db run <name> <file.sql>` executes SQL files against saved connections. Supports multi-statement files and transactions (`BEGIN...COMMIT`). Respects readonly flag. Works with both SQLite (`db.exec()`) and PostgreSQL
7
17
 
8
18
  ## [0.13.12] - 2026-04-24
9
19
 
@@ -71,4 +71,4 @@ This skill covers the `ppm` CLI, its HTTP API, and its config DB. It does **not*
71
71
  - Third-party extensions (inspect via `ppm ext list`).
72
72
  - The Claude Agent SDK internals (separate skill).
73
73
 
74
- <!-- Generated for PPM v0.13.13 at build time. Re-run `ppm export skill --install` to refresh. -->
74
+ <!-- Generated for PPM v0.13.15 at build time. Re-run `ppm export skill --install` to refresh. -->
@@ -201,4 +201,4 @@ _Base URL: `http://localhost:8080` (default; override via `ppm config set port <
201
201
  - `ws://<host>/ws/terminal` — PTY terminal multiplexer
202
202
  - `ws://<host>/ws/extensions` — extension host channel
203
203
 
204
- <!-- Generated from src/server/routes/ for PPM v0.13.13 -->
204
+ <!-- Generated from src/server/routes/ for PPM v0.13.15 -->
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hienlh/ppm",
3
- "version": "0.13.13",
3
+ "version": "0.13.15",
4
4
  "description": "Personal Project Manager — mobile-first web IDE with AI assistance",
5
5
  "author": "hienlh",
6
6
  "license": "MIT",
@@ -401,13 +401,9 @@ export function registerDbCommands(program: Command): void {
401
401
 
402
402
  if (conn.type === "postgres") {
403
403
  const { postgresService } = await import("../../services/postgres.service.ts");
404
- const result = await postgresService.executeQuery(cfg.connectionString!, sql);
404
+ const result = await postgresService.executeScript(cfg.connectionString!, sql);
405
405
  await postgresService.closeAll();
406
- if (result.changeType === "select") {
407
- formatRows(result.columns, result.rows);
408
- } else {
409
- console.log(`${C.green}OK${C.reset} — ${result.rowsAffected} row(s) affected (${result.executionTimeMs}ms)`);
410
- }
406
+ console.log(`${C.green}OK${C.reset} — ${result.statementsRun} statement(s) executed (${result.executionTimeMs}ms)`);
411
407
  } else {
412
408
  const { sqliteService } = await import("../../services/sqlite.service.ts");
413
409
  const result = sqliteService.executeScript(cfg.path!, cfg.path!, sql);
@@ -1,7 +1,8 @@
1
1
  import { Hono } from "hono";
2
- import { resolve } from "node:path";
2
+ import { resolve, isAbsolute } from "node:path";
3
3
  import { existsSync, mkdirSync } from "node:fs";
4
4
  import { fileService, SecurityError, NotFoundError, ValidationError } from "../../services/file.service.ts";
5
+ import { readSystemFile } from "../../services/fs-browse.service.ts";
5
6
  import { ok, err } from "../../types/api.ts";
6
7
  import { errorStatus } from "../helpers/error-status.ts";
7
8
 
@@ -196,9 +197,12 @@ fileRoutes.get("/compare", (c) => {
196
197
  if (!file1 || !file2) {
197
198
  return c.json(err("Missing query parameters: file1, file2"), 400);
198
199
  }
199
- const original = fileService.readFile(projectPath, file1);
200
- const modified = fileService.readFile(projectPath, file2);
201
- return c.json(ok({ original: original.content, modified: modified.content }));
200
+ // Support absolute paths (files outside project, e.g. /tmp/)
201
+ const readSide = (p: string) =>
202
+ isAbsolute(p) ? readSystemFile(p).content : fileService.readFile(projectPath, p).content;
203
+ const original = readSide(file1);
204
+ const modified = readSide(file2);
205
+ return c.json(ok({ original, modified }));
202
206
  } catch (e) {
203
207
  return c.json(err((e as Error).message), errorStatus(e));
204
208
  }
@@ -0,0 +1,95 @@
1
+ /**
2
+ * Split a SQL script into individual statements, respecting:
3
+ * - Single-quoted strings ('hello; world')
4
+ * - Double-quoted identifiers ("my;table")
5
+ * - Dollar-quoted strings ($$...$$, $tag$...$tag$)
6
+ * - Single-line comments (-- ...)
7
+ * - Multi-line comments (/* ... *​/)
8
+ *
9
+ * Returns non-empty trimmed statements without trailing semicolons.
10
+ */
11
+ export function splitSqlStatements(script: string): string[] {
12
+ const statements: string[] = [];
13
+ let current = "";
14
+ let i = 0;
15
+ const len = script.length;
16
+
17
+ while (i < len) {
18
+ const ch = script[i]!;
19
+
20
+ // Single-line comment
21
+ if (ch === "-" && script[i + 1] === "-") {
22
+ const end = script.indexOf("\n", i);
23
+ const lineEnd = end === -1 ? len : end + 1;
24
+ current += script.slice(i, lineEnd);
25
+ i = lineEnd;
26
+ continue;
27
+ }
28
+
29
+ // Multi-line comment
30
+ if (ch === "/" && script[i + 1] === "*") {
31
+ const end = script.indexOf("*/", i + 2);
32
+ const blockEnd = end === -1 ? len : end + 2;
33
+ current += script.slice(i, blockEnd);
34
+ i = blockEnd;
35
+ continue;
36
+ }
37
+
38
+ // Single-quoted string
39
+ if (ch === "'") {
40
+ let j = i + 1;
41
+ while (j < len) {
42
+ if (script[j] === "'" && script[j + 1] === "'") { j += 2; continue; }
43
+ if (script[j] === "'") { j++; break; }
44
+ j++;
45
+ }
46
+ current += script.slice(i, j);
47
+ i = j;
48
+ continue;
49
+ }
50
+
51
+ // Double-quoted identifier
52
+ if (ch === '"') {
53
+ let j = i + 1;
54
+ while (j < len) {
55
+ if (script[j] === '"' && script[j + 1] === '"') { j += 2; continue; }
56
+ if (script[j] === '"') { j++; break; }
57
+ j++;
58
+ }
59
+ current += script.slice(i, j);
60
+ i = j;
61
+ continue;
62
+ }
63
+
64
+ // Dollar-quoted string (PostgreSQL): $$...$$ or $tag$...$tag$
65
+ if (ch === "$") {
66
+ const tagMatch = script.slice(i).match(/^(\$[A-Za-z0-9_]*\$)/);
67
+ if (tagMatch) {
68
+ const tag = tagMatch[1]!;
69
+ const endIdx = script.indexOf(tag, i + tag.length);
70
+ const blockEnd = endIdx === -1 ? len : endIdx + tag.length;
71
+ current += script.slice(i, blockEnd);
72
+ i = blockEnd;
73
+ continue;
74
+ }
75
+ }
76
+
77
+ // Statement separator
78
+ if (ch === ";") {
79
+ const trimmed = current.trim();
80
+ if (trimmed) statements.push(trimmed);
81
+ current = "";
82
+ i++;
83
+ continue;
84
+ }
85
+
86
+ current += ch;
87
+ i++;
88
+ }
89
+
90
+ // Last statement (no trailing semicolon)
91
+ const trimmed = current.trim();
92
+ if (trimmed) statements.push(trimmed);
93
+
94
+ return statements;
95
+ }
@@ -1,4 +1,5 @@
1
1
  import postgres from "postgres";
2
+ import { splitSqlStatements } from "./database/split-sql-statements.ts";
2
3
 
3
4
  export interface PgTableInfo {
4
5
  name: string;
@@ -180,6 +181,23 @@ class PostgresService {
180
181
  return { columns: [], rows: [], rowsAffected: result.count ?? 0, changeType: "modify", executionTimeMs };
181
182
  }
182
183
 
184
+ /** Execute multi-statement SQL script inside a transaction via sql.begin().
185
+ * Strips user-supplied BEGIN/COMMIT/ROLLBACK since sql.begin() manages the transaction. */
186
+ async executeScript(connectionString: string, scriptText: string): Promise<{ statementsRun: number; executionTimeMs: number }> {
187
+ const sql = this.connect(connectionString);
188
+ const txControl = /^(BEGIN|COMMIT|ROLLBACK|END)(;|\s|$)/i;
189
+ const statements = splitSqlStatements(scriptText).filter((s) => !txControl.test(s));
190
+ if (statements.length === 0) return { statementsRun: 0, executionTimeMs: 0 };
191
+
192
+ const start = performance.now();
193
+ await sql.begin(async (tx) => {
194
+ for (const stmt of statements) {
195
+ await tx.unsafe(stmt);
196
+ }
197
+ });
198
+ return { statementsRun: statements.length, executionTimeMs: Math.round(performance.now() - start) };
199
+ }
200
+
183
201
  /** Update a single cell value */
184
202
  async updateCell(
185
203
  connectionString: string, table: string, schema = "public",