sidecar-cli 0.1.0-rc.1 → 0.1.1-beta.2
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 +52 -1
- package/dist/cli.js +57 -1
- package/dist/lib/ui.js +106 -0
- package/dist/services/capabilities-service.js +7 -0
- package/package.json +1 -1
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
|
|
|
@@ -168,7 +169,18 @@ Optional local enforcement:
|
|
|
168
169
|
npm run install:hooks
|
|
169
170
|
```
|
|
170
171
|
|
|
171
|
-
This
|
|
172
|
+
This is optional and per-repository clone. `sidecar init` does not install git hooks automatically.
|
|
173
|
+
|
|
174
|
+
This installs a pre-commit guard that checks staged non-doc code changes.
|
|
175
|
+
If staged code changes are present, commit is blocked unless both are recorded since the last commit:
|
|
176
|
+
|
|
177
|
+
- a `worklog` event
|
|
178
|
+
- a `summary refresh` event
|
|
179
|
+
|
|
180
|
+
The guard command is:
|
|
181
|
+
|
|
182
|
+
- `npm run sidecar:reminder -- --staged --enforce`
|
|
183
|
+
|
|
172
184
|
If a pre-commit hook already exists, Sidecar will not overwrite it unless you run:
|
|
173
185
|
|
|
174
186
|
```bash
|
|
@@ -205,6 +217,45 @@ Primary tables:
|
|
|
205
217
|
|
|
206
218
|
No network dependency is required for normal operation.
|
|
207
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
|
+
|
|
208
259
|
## JSON output
|
|
209
260
|
|
|
210
261
|
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',
|