@alashi/cli 0.8.0 → 0.9.0

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/assets/v1/chat.js CHANGED
@@ -1,4 +1,4 @@
1
- /* alashi chat component v1
1
+ /* alashi chat + markdown components v1
2
2
  *
3
3
  * The system chat UI, served by the host. Apps drop in:
4
4
  *
@@ -9,6 +9,10 @@
9
9
  * and cost display. Renders into light DOM using system.css classes, so it
10
10
  * inherits the app's theme tokens.
11
11
  *
12
+ * The same script also registers <alashi-markdown>, the markdown renderer on
13
+ * its own for apps that display prose outside a conversation (see the bottom
14
+ * of this file). One renderer, one place formatting bugs get fixed.
15
+ *
12
16
  * Attributes:
13
17
  * app app name (default: derived from /app/<name>/ in the URL)
14
18
  * placeholder input placeholder text
@@ -31,7 +35,8 @@
31
35
  * Breaking changes ship as /assets/v2/chat.js. */
32
36
 
33
37
  (() => {
34
- if (customElements.get('alashi-chat')) return;
38
+ // Loading the script twice is a no-op for every element it defines.
39
+ if (customElements.get('alashi-chat') || customElements.get('alashi-markdown')) return;
35
40
 
36
41
  const escapeHtml = (s) =>
37
42
  s
@@ -357,5 +362,40 @@
357
362
  }
358
363
  }
359
364
 
365
+ /* <alashi-markdown> — the same renderMarkdown() as chat bubbles, as a
366
+ * standalone display primitive. No fetching, no SSE, no conversations.
367
+ *
368
+ * <alashi-markdown text="**hi**"></alashi-markdown> // static markup
369
+ * node.text = card.description; // re-renders
370
+ *
371
+ * The `text` property is the primary path: apps pass model-derived,
372
+ * multi-line strings that do not belong in an attribute. Renders into light
373
+ * DOM (like _setMsgText) so it inherits the app's theme tokens, and carries
374
+ * the `markdown` class so system.css can style its children. */
375
+ class AlashiMarkdown extends HTMLElement {
376
+ connectedCallback() {
377
+ this.classList.add('markdown');
378
+ // A property set before connect wins; the attribute is the static path.
379
+ if (this._text === undefined) this._text = this.getAttribute('text') ?? '';
380
+ this._render();
381
+ }
382
+
383
+ get text() {
384
+ return this._text ?? '';
385
+ }
386
+
387
+ set text(value) {
388
+ this._text = String(value ?? '');
389
+ this._render();
390
+ }
391
+
392
+ _render() {
393
+ // Escape-first pipeline: renderMarkdown escapes before rebuilding a
394
+ // known-safe subset of tags, so untrusted markdown stays inert.
395
+ this.innerHTML = renderMarkdown(String(this._text ?? ''));
396
+ }
397
+ }
398
+
360
399
  customElements.define('alashi-chat', AlashiChat);
400
+ customElements.define('alashi-markdown', AlashiMarkdown);
361
401
  })();
@@ -578,41 +578,39 @@ input[type="checkbox"] {
578
578
  /* assistant bubbles hold rendered markdown (chat.js), not preformatted text */
579
579
  white-space: normal;
580
580
  }
581
- .msg.assistant p,
582
- .msg.assistant ul,
583
- .msg.assistant ol,
584
- .msg.assistant pre,
585
- .msg.assistant h3,
586
- .msg.assistant h4,
587
- .msg.assistant h5,
588
- .msg.assistant h6 {
581
+ /* Rendered-markdown prose. Shared by assistant chat bubbles and the
582
+ * <alashi-markdown> element (or any `.markdown` container) — chat.js renders
583
+ * both with the same renderMarkdown(), so the styling lives in one place too.
584
+ * Headings stay h3..h6 because that is what renderMarkdown emits. */
585
+ alashi-markdown {
586
+ /* unknown element to CSS, same as <alashi-chat> in consumer apps */
587
+ display: block;
588
+ }
589
+ :is(.msg.assistant, alashi-markdown, .markdown) :is(p, ul, ol, pre, h3, h4, h5, h6) {
589
590
  margin: 0 0 0.6rem;
590
591
  }
591
- .msg.assistant > :last-child {
592
+ :is(.msg.assistant, alashi-markdown, .markdown) > :last-child {
592
593
  margin-bottom: 0;
593
594
  }
594
- .msg.assistant ul,
595
- .msg.assistant ol {
595
+ :is(.msg.assistant, alashi-markdown, .markdown) :is(ul, ol) {
596
596
  padding-left: 1.3rem;
597
597
  }
598
- .msg.assistant li {
598
+ :is(.msg.assistant, alashi-markdown, .markdown) li {
599
599
  margin: 0.15rem 0;
600
600
  }
601
- .msg.assistant img {
601
+ :is(.msg.assistant, alashi-markdown, .markdown) img {
602
602
  max-width: 100%;
603
603
  border-radius: var(--radius-sm);
604
604
  border: 1px solid var(--border);
605
605
  }
606
- .msg.assistant pre {
606
+ :is(.msg.assistant, alashi-markdown, .markdown) pre {
607
607
  background: var(--panel);
608
608
  padding: 0.6rem 0.75rem;
609
609
  }
610
- .msg.assistant h3 {
610
+ :is(.msg.assistant, alashi-markdown, .markdown) h3 {
611
611
  font-size: 1rem;
612
612
  }
613
- .msg.assistant h4,
614
- .msg.assistant h5,
615
- .msg.assistant h6 {
613
+ :is(.msg.assistant, alashi-markdown, .markdown) :is(h4, h5, h6) {
616
614
  font-size: 0.92rem;
617
615
  }
618
616
  .msg.assistant.pending {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@alashi/cli",
3
- "version": "0.8.0",
3
+ "version": "0.9.0",
4
4
  "description": "alashi host: install, manage, and run Claude Code-powered apps",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -29,14 +29,14 @@
29
29
  "templates"
30
30
  ],
31
31
  "dependencies": {
32
- "@alashi/apps-sdk": "^0.5.0",
32
+ "@alashi/apps-sdk": "^0.6.0",
33
33
  "@hono/node-server": "^2.1.1",
34
34
  "commander": "^14.0.0",
35
35
  "croner": "^10.0.1",
36
36
  "hono": "^4.13.2",
37
37
  "semver": "^7.8.5",
38
38
  "zod": "^4.4.3",
39
- "@alashi/provider-claude": "^0.2.0"
39
+ "@alashi/provider-claude": "^0.2.1"
40
40
  },
41
41
  "devDependencies": {
42
42
  "@types/node": "^26.2.0",
@@ -29,13 +29,22 @@ Beside the manifest: `routes` (fetch-style `(Request, ctx) => Response`) and
29
29
  `jobHandlers` (`{ [name]: (ctx) => Promise<void> }`).
30
30
 
31
31
  Known gap: `onInstall`/`onUpdate` exist in the SDK but the host never calls
32
- them — create DB schema lazily (`CREATE TABLE IF NOT EXISTS` on first open).
32
+ them — so nothing runs at install time and the app owns its own schema. Schema
33
+ lives in ordered `.sql` files in `migrations/` (`001-init.sql`,
34
+ `002-add-....sql`), applied by `runMigrations(db, join(import.meta.dirname,
35
+ 'migrations'))` every time the DB is opened. It is forward-only and tracked in
36
+ `PRAGMA user_version`: a schema change is **a new numbered file**, never an
37
+ edit to one that has already been applied, and there are no down-migrations.
38
+ Statements that cannot run inside a transaction — notably
39
+ `PRAGMA journal_mode = WAL` — must not live in a migration file; set those on
40
+ the connection before calling `runMigrations`.
33
41
 
34
42
  ## Runtime surface (`ctx`)
35
43
 
36
44
  - `ctx.dataDir` — the **only** writable directory. SQLite DB and files go
37
45
  here (`node:sqlite` `DatabaseSync`). Treat the repo itself as read-only at
38
- runtime.
46
+ runtime — the DB lives in `ctx.dataDir`, but the `migrations/` files that
47
+ shape it are code and are read from the app repo (`import.meta.dirname`).
39
48
  - `ctx.config` — merged config (readonly).
40
49
  - `ctx.state` — small key/value store; `ctx.logger` — log here, not console.
41
50
  - `ctx.ai.createSession()` — an agent session, pre-scoped by the host
@@ -64,6 +73,10 @@ them — create DB schema lazily (`CREATE TABLE IF NOT EXISTS` on first open).
64
73
  with `openConversation`/`adoptSession`). Methods: `adoptSession(sessionId, title?)`,
65
74
  `openConversation(id)`, `newChat()`. Bubbling events: `alashi-chat:turn`
66
75
  (`{ok, costUsd, error, conversationId}`), `alashi-chat:conversation`.
76
+ The same script also registers `<alashi-markdown>` — the host's markdown
77
+ renderer as a standalone display primitive, for prose outside a conversation
78
+ (`node.text = card.description` re-renders on assignment; `text="**hi**"` is
79
+ honoured on connect for static markup). No fetching, no conversation logic.
67
80
  Never vendor or fork system UI; breaking changes ship as `/assets/v2/`.
68
81
 
69
82
  ## App routes
@@ -75,7 +88,8 @@ them — create DB schema lazily (`CREATE TABLE IF NOT EXISTS` on first open).
75
88
 
76
89
  - Never `innerHTML` external or model-derived data in the UI — build DOM via
77
90
  the `el()` helper / `textContent`. Markdown rendering belongs to
78
- `<alashi-chat>`, which is escape-first.
91
+ `<alashi-chat>` / `<alashi-markdown>`, the sanctioned exception because the
92
+ host's renderer is escape-first. Never bundle a markdown library.
79
93
  - Validate at boundaries (webhooks, user input): JSON only, cap body sizes,
80
94
  4xx on garbage. Model output is a boundary too — parse defensively.
81
95
  - Secrets come from declared `config` keys (`secret: true`), referenced as
@@ -18,6 +18,7 @@ your changes are live.
18
18
  | File | What it is |
19
19
  |---|---|
20
20
  | `index.mjs` | The app: manifest, API routes, job handlers |
21
+ | `migrations/` | Ordered `.sql` files — the DB schema, applied at DB open |
21
22
  | `ui/index.html` | The web UI, served at `/app/__APP_NAME__/` |
22
23
  | `AGENTS.md` | The platform contract, written for coding agents working in this repo |
23
24
 
@@ -27,7 +28,12 @@ your `routes` handler, so you can also server-render pages.
27
28
  ## What the app gets
28
29
 
29
30
  - **`ctx.dataDir`** — the only writable directory; put your SQLite DB and
30
- files here (the template creates `app.db` on first request)
31
+ files here (the template opens `app.db` there on first request and applies
32
+ `migrations/` to it)
33
+ - **`migrations/`** — the schema, as ordered `.sql` files applied by
34
+ `runMigrations` at DB open: forward-only, tracked in `PRAGMA user_version`.
35
+ To change the schema, add the next numbered file (`002-...sql`) — never edit
36
+ one that has already run
31
37
  - **`ctx.state`** — small key/value store (`state.json`)
32
38
  - **`ctx.config`** — `configDefaults` merged with the user's per-app config
33
39
  - **`ctx.ai`** — AI sessions through the host; the Chat page uses the host's
@@ -1,12 +1,12 @@
1
1
  import { join } from 'node:path';
2
2
  import { DatabaseSync } from 'node:sqlite';
3
- import { defineApp } from '@alashi/apps-sdk';
3
+ import { defineApp, runMigrations } from '@alashi/apps-sdk';
4
4
 
5
5
  export default defineApp({
6
6
  name: '__APP_NAME__',
7
7
  version: '0.1.0',
8
8
  description: 'An alashi app',
9
- sdkVersion: '^0.5.0',
9
+ sdkVersion: '^0.6.0',
10
10
  ui: 'ui',
11
11
  configDefaults: {
12
12
  greeting: 'What should we work on?',
@@ -27,11 +27,12 @@ export default defineApp({
27
27
  routes: (ctx) => {
28
28
  // The app's own SQLite database, living in its data dir.
29
29
  const db = new DatabaseSync(join(ctx.dataDir, 'app.db'));
30
- db.exec(`CREATE TABLE IF NOT EXISTS notes (
31
- id INTEGER PRIMARY KEY AUTOINCREMENT,
32
- text TEXT NOT NULL,
33
- created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%SZ', 'now'))
34
- )`);
30
+ // Schema comes from the ordered .sql files in this repo's migrations/
31
+ // (not ctx.dataDir), applied forward-only at DB open. A schema change
32
+ // ships as a new numbered file — never as an edit to an applied one.
33
+ // Pragmas that cannot run inside a transaction (journal_mode) go here,
34
+ // before the call, not into a migration file.
35
+ runMigrations(db, join(import.meta.dirname, 'migrations'), { logger: ctx.logger });
35
36
 
36
37
  return async (req) => {
37
38
  const url = new URL(req.url);
@@ -0,0 +1,7 @@
1
+ -- Applied once, then never touched again. Schema changes ship as a new
2
+ -- numbered file (002-..., 003-...), never as an edit to this one.
3
+ CREATE TABLE notes (
4
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
5
+ text TEXT NOT NULL,
6
+ created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%SZ', 'now'))
7
+ );
@@ -4,6 +4,6 @@
4
4
  "type": "module",
5
5
  "main": "index.mjs",
6
6
  "dependencies": {
7
- "@alashi/apps-sdk": "^0.5.0"
7
+ "@alashi/apps-sdk": "^0.6.0"
8
8
  }
9
9
  }