sidecar-cli 0.1.1-beta.1 → 0.1.1-beta.3

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/README.md CHANGED
@@ -103,6 +103,7 @@ Global:
103
103
 
104
104
  - `sidecar init [--force] [--name <project-name>] [--json]`
105
105
  - `sidecar status [--json]`
106
+ - `sidecar ui [--no-open] [--port <port>] [--install-only] [--project <path>] [--reinstall]`
106
107
  - `sidecar capabilities --json`
107
108
  - `sidecar help`
108
109
 
@@ -216,6 +217,50 @@ Primary tables:
216
217
 
217
218
  No network dependency is required for normal operation.
218
219
 
220
+ ## Optional local UI
221
+
222
+ `sidecar ui` launches a local browser UI for the selected Sidecar project.
223
+
224
+ Lazy-install behavior:
225
+
226
+ 1. `sidecar ui` resolves the nearest `.sidecar` project root (or uses `--project`).
227
+ 2. Sidecar checks for `@sidecar/ui` in `~/.sidecar/ui`.
228
+ 3. If missing/incompatible, Sidecar installs or updates it automatically.
229
+ 4. Sidecar starts a local UI server and opens the browser (unless `--no-open`).
230
+
231
+ UI runtime location:
232
+
233
+ - `~/.sidecar/ui`
234
+ - the CLI installs `@sidecar/ui` here (not in your project repo)
235
+
236
+ Version compatibility rule:
237
+
238
+ - CLI and UI must share the same major version.
239
+ - If majors differ, `sidecar ui` auto-reinstalls/updates UI.
240
+
241
+ Common examples:
242
+
243
+ ```bash
244
+ sidecar ui
245
+ sidecar ui --no-open --port 4311
246
+ sidecar ui --install-only
247
+ sidecar ui --project ../other-repo
248
+ sidecar ui --reinstall
249
+ ```
250
+
251
+ Initial UI screens:
252
+
253
+ - Overview: project info, active session, recent decisions/worklogs, open tasks, recent notes
254
+ - Timeline: recent events in chronological order
255
+ - Tasks: open and completed tasks
256
+ - Decisions: decision records with summary and timestamps
257
+ - Preferences: `.sidecar/preferences.json` and `.sidecar/summary.md`
258
+
259
+ UI write support (v1):
260
+
261
+ - Add notes from Overview
262
+ - Add open tasks from Overview
263
+
219
264
  ## JSON output
220
265
 
221
266
  Most commands support `--json` and return structured output:
package/dist/cli.js CHANGED
@@ -5,12 +5,13 @@ import { Command } from 'commander';
5
5
  import Database from 'better-sqlite3';
6
6
  import { z } from 'zod';
7
7
  import { initializeSchema } from './db/schema.js';
8
- import { getSidecarPaths } from './lib/paths.js';
8
+ import { findSidecarRoot, getSidecarPaths } from './lib/paths.js';
9
9
  import { nowIso, humanTime, stringifyJson } from './lib/format.js';
10
10
  import { SidecarError } from './lib/errors.js';
11
11
  import { jsonFailure, jsonSuccess, printJsonEnvelope } from './lib/output.js';
12
12
  import { bannerDisabled, renderBanner } from './lib/banner.js';
13
13
  import { getUpdateNotice } from './lib/update-check.js';
14
+ import { ensureUiInstalled, launchUiServer } from './lib/ui.js';
14
15
  import { requireInitialized } from './db/client.js';
15
16
  import { renderAgentsMarkdown, renderClaudeMarkdown } from './templates/agents.js';
16
17
  import { refreshSummaryFile } from './services/summary-service.js';
@@ -166,6 +167,14 @@ function renderContextMarkdown(data) {
166
167
  }
167
168
  return lines.join('\n');
168
169
  }
170
+ function resolveProjectRoot(projectPath) {
171
+ const basePath = projectPath ? path.resolve(projectPath) : process.cwd();
172
+ const root = findSidecarRoot(basePath);
173
+ if (!root) {
174
+ throw new SidecarError(NOT_INITIALIZED_MSG);
175
+ }
176
+ return root;
177
+ }
169
178
  const program = new Command();
170
179
  program.name('sidecar').description('Local-first project memory and recording CLI').version(pkg.version);
171
180
  program.option('--no-banner', 'Disable Sidecar banner output');
@@ -183,6 +192,53 @@ function maybePrintUpdateNotice() {
183
192
  console.log(`Update available: ${pkg.version} -> ${notice.latestVersion}`);
184
193
  console.log(`Run: npm install -g sidecar-cli@${installTag}`);
185
194
  }
195
+ program
196
+ .command('ui')
197
+ .description('Launch the optional local Sidecar UI')
198
+ .option('--no-open', 'Do not open the browser automatically')
199
+ .option('--port <port>', 'Port to run the UI on', (v) => Number.parseInt(v, 10), 4310)
200
+ .option('--install-only', 'Install/update UI package but do not launch')
201
+ .option('--project <path>', 'Project path (defaults to nearest Sidecar root)')
202
+ .option('--reinstall', 'Force reinstall UI package')
203
+ .addHelpText('after', '\nExamples:\n $ sidecar ui\n $ sidecar ui --no-open --port 4311\n $ sidecar ui --project ../my-repo --install-only')
204
+ .action((opts) => {
205
+ const command = 'ui';
206
+ try {
207
+ const projectRoot = resolveProjectRoot(opts.project);
208
+ const port = Number(opts.port);
209
+ if (!Number.isInteger(port) || port < 1 || port > 65535) {
210
+ fail('Port must be an integer between 1 and 65535');
211
+ }
212
+ if (!bannerDisabled()) {
213
+ console.log(renderBanner());
214
+ console.log('');
215
+ }
216
+ console.log('Launching Sidecar UI');
217
+ console.log(`Project: ${projectRoot}`);
218
+ const { installedVersion } = ensureUiInstalled({
219
+ cliVersion: pkg.version,
220
+ reinstall: Boolean(opts.reinstall),
221
+ onStatus: (line) => console.log(line),
222
+ });
223
+ console.log(`UI version: ${installedVersion}`);
224
+ if (opts.installOnly) {
225
+ console.log('Install-only mode complete.');
226
+ return;
227
+ }
228
+ const { url } = launchUiServer({
229
+ projectPath: projectRoot,
230
+ port,
231
+ openBrowser: opts.open !== false,
232
+ });
233
+ console.log(`URL: ${url}`);
234
+ if (opts.open === false) {
235
+ console.log('Browser auto-open disabled.');
236
+ }
237
+ }
238
+ catch (err) {
239
+ handleCommandError(command, false, err);
240
+ }
241
+ });
186
242
  program
187
243
  .command('init')
188
244
  .description('Initialize Sidecar in the current directory')
package/dist/lib/ui.js ADDED
@@ -0,0 +1,106 @@
1
+ import fs from 'node:fs';
2
+ import os from 'node:os';
3
+ import path from 'node:path';
4
+ import { execFileSync, spawn } from 'node:child_process';
5
+ import { detectReleaseChannel } from './update-check.js';
6
+ import { SidecarError } from './errors.js';
7
+ const UI_PACKAGE = '@sidecar/ui';
8
+ const UI_RUNTIME_DIR = path.join(os.homedir(), '.sidecar', 'ui');
9
+ function ensureRuntimeDir() {
10
+ fs.mkdirSync(UI_RUNTIME_DIR, { recursive: true });
11
+ const runtimePkgPath = path.join(UI_RUNTIME_DIR, 'package.json');
12
+ if (!fs.existsSync(runtimePkgPath)) {
13
+ fs.writeFileSync(runtimePkgPath, JSON.stringify({ name: 'sidecar-ui-runtime', private: true, version: '0.0.0' }, null, 2));
14
+ }
15
+ }
16
+ function readInstalledUiVersion() {
17
+ const p = path.join(UI_RUNTIME_DIR, 'node_modules', '@sidecar', 'ui', 'package.json');
18
+ if (!fs.existsSync(p))
19
+ return null;
20
+ try {
21
+ const pkg = JSON.parse(fs.readFileSync(p, 'utf8'));
22
+ return pkg.version ?? null;
23
+ }
24
+ catch {
25
+ return null;
26
+ }
27
+ }
28
+ function major(version) {
29
+ const m = version.match(/^(\d+)\./);
30
+ if (!m)
31
+ return null;
32
+ return Number.parseInt(m[1], 10);
33
+ }
34
+ function isCompatible(cliVersion, uiVersion) {
35
+ if (!uiVersion)
36
+ return false;
37
+ const cliMajor = major(cliVersion);
38
+ const uiMajor = major(uiVersion);
39
+ return cliMajor !== null && uiMajor !== null && cliMajor === uiMajor;
40
+ }
41
+ function npmInstall(spec) {
42
+ execFileSync('npm', ['install', '--no-audit', '--no-fund', spec], {
43
+ cwd: UI_RUNTIME_DIR,
44
+ stdio: 'inherit',
45
+ });
46
+ }
47
+ function getDesiredTag(cliVersion) {
48
+ return detectReleaseChannel(cliVersion);
49
+ }
50
+ export function ensureUiInstalled(options) {
51
+ ensureRuntimeDir();
52
+ const tag = getDesiredTag(options.cliVersion);
53
+ const installed = readInstalledUiVersion();
54
+ const shouldInstall = Boolean(options.reinstall) || !isCompatible(options.cliVersion, installed);
55
+ if (shouldInstall) {
56
+ options.onStatus?.(installed
57
+ ? `Updating Sidecar UI (${installed}) for CLI compatibility...`
58
+ : 'Installing Sidecar UI for first use...');
59
+ const spec = `${UI_PACKAGE}@${tag}`;
60
+ try {
61
+ npmInstall(spec);
62
+ }
63
+ catch {
64
+ const localUiPkg = path.resolve(process.cwd(), 'packages', 'ui');
65
+ if (fs.existsSync(path.join(localUiPkg, 'package.json'))) {
66
+ options.onStatus?.('Falling back to local workspace UI package...');
67
+ npmInstall(localUiPkg);
68
+ }
69
+ else {
70
+ throw new SidecarError('Failed to install Sidecar UI package. Check npm access/network and retry `sidecar ui --reinstall`.');
71
+ }
72
+ }
73
+ }
74
+ const finalVersion = readInstalledUiVersion();
75
+ if (!finalVersion) {
76
+ throw new SidecarError('Sidecar UI appears missing after install. Retry with `sidecar ui --reinstall`.');
77
+ }
78
+ return { installedVersion: finalVersion };
79
+ }
80
+ export function launchUiServer(options) {
81
+ const serverPath = path.join(UI_RUNTIME_DIR, 'node_modules', '@sidecar', 'ui', 'server.js');
82
+ if (!fs.existsSync(serverPath)) {
83
+ throw new SidecarError('Sidecar UI server entry was not found after install.');
84
+ }
85
+ const child = spawn(process.execPath, [serverPath, '--project', options.projectPath, '--port', String(options.port)], {
86
+ stdio: 'inherit',
87
+ env: { ...process.env },
88
+ });
89
+ const url = `http://localhost:${options.port}`;
90
+ if (options.openBrowser) {
91
+ openBrowser(url);
92
+ }
93
+ return { url, child };
94
+ }
95
+ export function openBrowser(url) {
96
+ const platform = process.platform;
97
+ if (platform === 'darwin') {
98
+ spawn('open', [url], { detached: true, stdio: 'ignore' }).unref();
99
+ return;
100
+ }
101
+ if (platform === 'win32') {
102
+ spawn('cmd', ['/c', 'start', '', url], { detached: true, stdio: 'ignore' }).unref();
103
+ return;
104
+ }
105
+ spawn('xdg-open', [url], { detached: true, stdio: 'ignore' }).unref();
106
+ }
@@ -20,6 +20,13 @@ export function getCapabilitiesManifest(version) {
20
20
  arguments: [],
21
21
  options: ['--json'],
22
22
  },
23
+ {
24
+ name: 'ui',
25
+ description: 'Launch optional local Sidecar UI (lazy-installed on first run)',
26
+ json_output: false,
27
+ arguments: [],
28
+ options: ['--no-open', '--port <port>', '--install-only', '--project <path>', '--reinstall'],
29
+ },
23
30
  {
24
31
  name: 'capabilities',
25
32
  description: 'Show machine-readable CLI manifest',
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sidecar-cli",
3
- "version": "0.1.1-beta.1",
3
+ "version": "0.1.1-beta.3",
4
4
  "description": "Local-first project memory and recording tool",
5
5
  "scripts": {
6
6
  "build": "npm run clean && tsc -p tsconfig.json",