opencode-rules-md 0.6.5 → 0.7.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/dist/cli.mjs +163 -83
- package/dist/src/cli/config.d.ts +16 -4
- package/dist/src/cli/config.d.ts.map +1 -1
- package/dist/src/cli/config.js +92 -46
- package/dist/src/cli/config.js.map +1 -1
- package/dist/src/cli/install.d.ts +18 -6
- package/dist/src/cli/install.d.ts.map +1 -1
- package/dist/src/cli/install.js +58 -22
- package/dist/src/cli/install.js.map +1 -1
- package/dist/src/cli/main.d.ts +1 -1
- package/dist/src/cli/main.d.ts.map +1 -1
- package/dist/src/cli/main.js +33 -19
- package/dist/src/cli/main.js.map +1 -1
- package/dist/src/cli/status.d.ts +23 -8
- package/dist/src/cli/status.d.ts.map +1 -1
- package/dist/src/cli/status.js +36 -27
- package/dist/src/cli/status.js.map +1 -1
- package/package.json +22 -5
- package/src/cli/config.ts +106 -54
- package/src/cli/install.ts +104 -27
- package/src/cli/main.ts +33 -22
- package/src/cli/status.ts +50 -31
package/src/cli/config.ts
CHANGED
|
@@ -10,6 +10,19 @@ import * as os from 'node:os';
|
|
|
10
10
|
import * as path from 'node:path';
|
|
11
11
|
import type { CliFs } from './real-fs.js';
|
|
12
12
|
|
|
13
|
+
// ---------------------------------------------------------------------------
|
|
14
|
+
// Public constants
|
|
15
|
+
// ---------------------------------------------------------------------------
|
|
16
|
+
|
|
17
|
+
/** npm package name for this plugin. */
|
|
18
|
+
export const PLUGIN_NAME = 'opencode-rules-md';
|
|
19
|
+
|
|
20
|
+
/** Filename for the OpenCode server config. */
|
|
21
|
+
export const SERVER_CONFIG_FILENAME = 'opencode.json';
|
|
22
|
+
|
|
23
|
+
/** Filename for the OpenCode TUI config. */
|
|
24
|
+
export const TUI_CONFIG_FILENAME = 'tui.json';
|
|
25
|
+
|
|
13
26
|
// ---------------------------------------------------------------------------
|
|
14
27
|
// Public types
|
|
15
28
|
// ---------------------------------------------------------------------------
|
|
@@ -22,77 +35,112 @@ export interface LoadResult {
|
|
|
22
35
|
}
|
|
23
36
|
|
|
24
37
|
// ---------------------------------------------------------------------------
|
|
25
|
-
// JSONC comment stripper
|
|
38
|
+
// JSONC comment stripper — two-pass
|
|
26
39
|
// ---------------------------------------------------------------------------
|
|
27
40
|
|
|
28
41
|
/**
|
|
29
42
|
* Strip JSONC comments and trailing commas while preserving
|
|
30
43
|
* comment-like sequences inside string literals.
|
|
44
|
+
*
|
|
45
|
+
* Pass 1: strip `//` and `/* * /` comments (string-aware).
|
|
46
|
+
* Pass 2: strip trailing commas before `}` or `]`, skipping whitespace.
|
|
31
47
|
*/
|
|
32
48
|
export function stripJsoncComments(content: string): string {
|
|
49
|
+
// Pass 1 — strip comments while preserving string contents
|
|
33
50
|
let result = '';
|
|
34
51
|
let i = 0;
|
|
35
|
-
|
|
36
|
-
|
|
52
|
+
let inString = false;
|
|
53
|
+
let escaped = false;
|
|
54
|
+
const len = content.length;
|
|
55
|
+
|
|
56
|
+
while (i < len) {
|
|
57
|
+
const char = content[i]!;
|
|
58
|
+
|
|
59
|
+
if (inString) {
|
|
60
|
+
result += char;
|
|
61
|
+
if (escaped) {
|
|
62
|
+
escaped = false;
|
|
63
|
+
} else if (char === '\\') {
|
|
64
|
+
escaped = true;
|
|
65
|
+
} else if (char === '"') {
|
|
66
|
+
inString = false;
|
|
67
|
+
}
|
|
68
|
+
i++;
|
|
69
|
+
continue;
|
|
70
|
+
}
|
|
37
71
|
|
|
38
|
-
// Start of string (double-quoted only — JSON only supports double quotes)?
|
|
39
72
|
if (char === '"') {
|
|
73
|
+
inString = true;
|
|
40
74
|
result += char;
|
|
41
75
|
i++;
|
|
42
|
-
while (i < content.length) {
|
|
43
|
-
const c = content[i];
|
|
44
|
-
if (c === '\\' && i + 1 < content.length) {
|
|
45
|
-
// Escape sequence — copy both chars verbatim
|
|
46
|
-
result += c + content[i + 1];
|
|
47
|
-
i += 2;
|
|
48
|
-
continue;
|
|
49
|
-
}
|
|
50
|
-
if (c === '"') {
|
|
51
|
-
result += c;
|
|
52
|
-
i++;
|
|
53
|
-
break;
|
|
54
|
-
}
|
|
55
|
-
result += c;
|
|
56
|
-
i++;
|
|
57
|
-
}
|
|
58
76
|
continue;
|
|
59
77
|
}
|
|
60
78
|
|
|
61
|
-
// Line comment
|
|
79
|
+
// Line comment: // until newline
|
|
62
80
|
if (char === '/' && content[i + 1] === '/') {
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
}
|
|
81
|
+
i += 2;
|
|
82
|
+
while (i < len && content[i] !== '\n') i++;
|
|
66
83
|
continue;
|
|
67
84
|
}
|
|
68
85
|
|
|
69
|
-
// Block comment
|
|
86
|
+
// Block comment: /* until */
|
|
70
87
|
if (char === '/' && content[i + 1] === '*') {
|
|
71
88
|
i += 2;
|
|
72
|
-
while (i
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
89
|
+
while (i < len && !(content[i] === '*' && content[i + 1] === '/')) i++;
|
|
90
|
+
if (i < len) i += 2; // skip past */
|
|
91
|
+
continue;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
result += char;
|
|
95
|
+
i++;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// Pass 2 — strip trailing commas (with whitespace skip) before } or ]
|
|
99
|
+
let out = '';
|
|
100
|
+
let inStr = false;
|
|
101
|
+
let esc = false;
|
|
102
|
+
let j = 0;
|
|
103
|
+
const outLen = result.length;
|
|
104
|
+
|
|
105
|
+
while (j < outLen) {
|
|
106
|
+
const ch = result[j]!;
|
|
107
|
+
|
|
108
|
+
if (inStr) {
|
|
109
|
+
out += ch;
|
|
110
|
+
if (esc) {
|
|
111
|
+
esc = false;
|
|
112
|
+
} else if (ch === '\\') {
|
|
113
|
+
esc = true;
|
|
114
|
+
} else if (ch === '"') {
|
|
115
|
+
inStr = false;
|
|
78
116
|
}
|
|
117
|
+
j++;
|
|
79
118
|
continue;
|
|
80
119
|
}
|
|
81
120
|
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
) {
|
|
87
|
-
i++;
|
|
121
|
+
if (ch === '"') {
|
|
122
|
+
inStr = true;
|
|
123
|
+
out += ch;
|
|
124
|
+
j++;
|
|
88
125
|
continue;
|
|
89
126
|
}
|
|
90
127
|
|
|
91
|
-
|
|
92
|
-
|
|
128
|
+
if (ch === ',') {
|
|
129
|
+
// Skip whitespace before checking for } or ]
|
|
130
|
+
let k = j + 1;
|
|
131
|
+
while (k < outLen && /\s/.test(result[k]!)) k++;
|
|
132
|
+
if (k < outLen && /[}\]]/.test(result[k]!)) {
|
|
133
|
+
// Drop the comma; jump to the bracket
|
|
134
|
+
j = k;
|
|
135
|
+
continue;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
out += ch;
|
|
140
|
+
j++;
|
|
93
141
|
}
|
|
94
142
|
|
|
95
|
-
return
|
|
143
|
+
return out;
|
|
96
144
|
}
|
|
97
145
|
|
|
98
146
|
// ---------------------------------------------------------------------------
|
|
@@ -108,13 +156,14 @@ export function stripJsoncComments(content: string): string {
|
|
|
108
156
|
*/
|
|
109
157
|
export function resolveGlobalConfigPath(
|
|
110
158
|
fs: CliFs,
|
|
111
|
-
opts: { ensureDir?: boolean } = {}
|
|
159
|
+
opts: { ensureDir?: boolean; filename?: string } = {}
|
|
112
160
|
): string {
|
|
113
161
|
const dir = resolveConfigDir();
|
|
114
162
|
if (opts.ensureDir && !fs.existsSync(dir)) {
|
|
115
163
|
fs.mkdirSync(dir, { recursive: true });
|
|
116
164
|
}
|
|
117
|
-
|
|
165
|
+
const filename = opts.filename ?? SERVER_CONFIG_FILENAME;
|
|
166
|
+
return path.join(dir, filename);
|
|
118
167
|
}
|
|
119
168
|
|
|
120
169
|
function resolveConfigDir(): string {
|
|
@@ -147,12 +196,15 @@ function resolveConfigDir(): string {
|
|
|
147
196
|
// ---------------------------------------------------------------------------
|
|
148
197
|
|
|
149
198
|
/**
|
|
150
|
-
* Load and parse
|
|
199
|
+
* Load and parse a global OpenCode config file.
|
|
151
200
|
* Treats a missing file as an empty config object (not an error).
|
|
152
201
|
* Surfaces parse errors so callers can exit non-zero without corrupting data.
|
|
153
202
|
*/
|
|
154
|
-
export function loadGlobalConfig(fs: CliFs): LoadResult {
|
|
155
|
-
const configPath = resolveGlobalConfigPath(
|
|
203
|
+
export function loadGlobalConfig(fs: CliFs, opts: { filename?: string } = {}): LoadResult {
|
|
204
|
+
const configPath = resolveGlobalConfigPath(
|
|
205
|
+
fs,
|
|
206
|
+
opts.filename ? { filename: opts.filename } : {}
|
|
207
|
+
);
|
|
156
208
|
|
|
157
209
|
if (!fs.existsSync(configPath)) {
|
|
158
210
|
return { path: configPath, existed: false, config: {} };
|
|
@@ -240,19 +292,19 @@ export function addPlugin(
|
|
|
240
292
|
/**
|
|
241
293
|
* Write a timestamped backup of the given config path and rotate so at most
|
|
242
294
|
* 3 backups are retained (oldest deleted first).
|
|
243
|
-
* Returns the path of the newly created backup.
|
|
295
|
+
* Returns the path of the newly created backup, or null if no backup was created.
|
|
244
296
|
*/
|
|
245
|
-
export function rotateBackups(fs: CliFs, configPath: string, _pluginName: string): string {
|
|
297
|
+
export function rotateBackups(fs: CliFs, configPath: string, _pluginName: string): string | null {
|
|
298
|
+
if (!fs.existsSync(configPath)) {
|
|
299
|
+
return null;
|
|
300
|
+
}
|
|
301
|
+
|
|
246
302
|
const dir = path.dirname(configPath);
|
|
247
303
|
const base = path.basename(configPath);
|
|
248
304
|
const timestamp = Date.now();
|
|
249
305
|
const newBak = path.join(dir, `${base}.bak.${timestamp}`);
|
|
250
306
|
|
|
251
|
-
|
|
252
|
-
fs.copyFileSync(configPath, newBak);
|
|
253
|
-
} else {
|
|
254
|
-
fs.writeFileSync(newBak, '', 'utf-8');
|
|
255
|
-
}
|
|
307
|
+
fs.copyFileSync(configPath, newBak);
|
|
256
308
|
|
|
257
309
|
// Collect existing backups sorted by timestamp (lexical sort on numeric suffix)
|
|
258
310
|
const entries = fs.readdirSync(dir);
|
|
@@ -280,7 +332,7 @@ export function rotateBackups(fs: CliFs, configPath: string, _pluginName: string
|
|
|
280
332
|
export function writeAtomically(fs: CliFs, configPath: string, content: string): void {
|
|
281
333
|
const dir = path.dirname(configPath);
|
|
282
334
|
const base = path.basename(configPath);
|
|
283
|
-
const tmpPath = path.join(dir, `.${base}.tmp.${Date.now()}`);
|
|
335
|
+
const tmpPath = path.join(dir, `.${base}.tmp.${Date.now()}-${process.pid}`);
|
|
284
336
|
|
|
285
337
|
fs.writeFileSync(tmpPath, content, 'utf-8');
|
|
286
338
|
fs.renameSync(tmpPath, configPath);
|
|
@@ -309,4 +361,4 @@ function collapseByPrefix(
|
|
|
309
361
|
const keptIndices = new Set([...lastSeen.values()]);
|
|
310
362
|
|
|
311
363
|
return plugins.filter((_, idx) => keptIndices.has(idx));
|
|
312
|
-
}
|
|
364
|
+
}
|
package/src/cli/install.ts
CHANGED
|
@@ -1,13 +1,28 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* install command: idempotently append opencode-rules-md to the global
|
|
2
|
+
* install command: idempotently append opencode-rules-md to the global configs.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
4
|
+
* Registers the plugin in BOTH:
|
|
5
|
+
* - Server config: ~/.config/opencode/opencode.json
|
|
6
|
+
* - TUI config: ~/.config/opencode/tui.json
|
|
7
|
+
*
|
|
8
|
+
* Pipeline (per config):
|
|
9
|
+
* loadGlobalConfig → abort if parseError (throw) → normalizePlugin
|
|
6
10
|
* → check no-op → (dry-run? print & return "planned")
|
|
7
11
|
* → rotateBackups → writeAtomically → print & return "wrote"
|
|
12
|
+
*
|
|
13
|
+
* Errors are surfaced as exceptions instead of silent status returns so
|
|
14
|
+
* the CLI dispatcher can print them.
|
|
8
15
|
*/
|
|
9
16
|
|
|
10
|
-
import {
|
|
17
|
+
import {
|
|
18
|
+
loadGlobalConfig,
|
|
19
|
+
normalizePlugin,
|
|
20
|
+
removePlugin,
|
|
21
|
+
addPlugin,
|
|
22
|
+
rotateBackups,
|
|
23
|
+
writeAtomically,
|
|
24
|
+
TUI_CONFIG_FILENAME,
|
|
25
|
+
} from './config.js';
|
|
11
26
|
import type { CliFs } from './real-fs.js';
|
|
12
27
|
import { realFs } from './real-fs.js';
|
|
13
28
|
|
|
@@ -15,12 +30,18 @@ import { realFs } from './real-fs.js';
|
|
|
15
30
|
// Result types
|
|
16
31
|
// ---------------------------------------------------------------------------
|
|
17
32
|
|
|
18
|
-
export interface
|
|
19
|
-
status: 'wrote' | 'planned' | 'noop'
|
|
33
|
+
export interface ConfigUpdateResult {
|
|
34
|
+
status: 'wrote' | 'planned' | 'noop';
|
|
20
35
|
path: string;
|
|
21
36
|
specifier: string;
|
|
22
37
|
backup?: string;
|
|
23
|
-
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export interface InstallResult {
|
|
41
|
+
status: 'wrote' | 'planned' | 'noop';
|
|
42
|
+
specifier: string;
|
|
43
|
+
server: ConfigUpdateResult;
|
|
44
|
+
tui: ConfigUpdateResult;
|
|
24
45
|
}
|
|
25
46
|
|
|
26
47
|
// ---------------------------------------------------------------------------
|
|
@@ -31,17 +52,54 @@ export function runInstall(
|
|
|
31
52
|
opts: { version?: string; dryRun?: boolean },
|
|
32
53
|
fs: CliFs = realFs
|
|
33
54
|
): InstallResult {
|
|
34
|
-
|
|
35
|
-
|
|
55
|
+
const specifier = buildSpecifier(opts.version);
|
|
56
|
+
|
|
57
|
+
const server = updateConfig(specifier, opts.dryRun ?? false, fs);
|
|
58
|
+
const tui = updateConfig(specifier, opts.dryRun ?? false, fs, {
|
|
59
|
+
filename: TUI_CONFIG_FILENAME,
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
const aggregate: InstallResult['status'] =
|
|
63
|
+
server.status === 'wrote' || tui.status === 'wrote'
|
|
64
|
+
? 'wrote'
|
|
65
|
+
: server.status === 'planned' || tui.status === 'planned'
|
|
66
|
+
? 'planned'
|
|
67
|
+
: 'noop';
|
|
68
|
+
|
|
69
|
+
printSummary(aggregate, specifier, server, tui);
|
|
36
70
|
|
|
37
|
-
|
|
71
|
+
return { status: aggregate, specifier, server, tui };
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// ---------------------------------------------------------------------------
|
|
75
|
+
// Internal: update a single config file
|
|
76
|
+
// ---------------------------------------------------------------------------
|
|
77
|
+
|
|
78
|
+
interface UpdateOpts {
|
|
79
|
+
filename?: string;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function updateConfig(
|
|
83
|
+
specifier: string,
|
|
84
|
+
dryRun: boolean,
|
|
85
|
+
fs: CliFs,
|
|
86
|
+
opts: UpdateOpts = {}
|
|
87
|
+
): ConfigUpdateResult {
|
|
88
|
+
const loadResult = loadGlobalConfig(
|
|
89
|
+
fs,
|
|
90
|
+
opts.filename ? { filename: opts.filename } : {}
|
|
91
|
+
);
|
|
92
|
+
|
|
93
|
+
// Throw on parse error — better to fail fast than to silently corrupt config
|
|
38
94
|
if (loadResult.parseError) {
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
95
|
+
const filename = opts.filename ?? 'opencode.json';
|
|
96
|
+
const err: Error & { configPath?: string } = new Error(
|
|
97
|
+
`opencode-rules-md: ${filename} is malformed — aborting to avoid data loss.\n` +
|
|
98
|
+
` path: ${loadResult.path}\n` +
|
|
99
|
+
` error: ${loadResult.parseError.message}`
|
|
100
|
+
);
|
|
101
|
+
err.configPath = loadResult.path;
|
|
102
|
+
throw err;
|
|
45
103
|
}
|
|
46
104
|
|
|
47
105
|
const configPath = loadResult.path;
|
|
@@ -50,9 +108,6 @@ export function runInstall(
|
|
|
50
108
|
// Normalize plugin array (dedupe by prefix)
|
|
51
109
|
config['plugin'] = normalizePlugin(config);
|
|
52
110
|
|
|
53
|
-
// Build the specifier
|
|
54
|
-
const specifier = buildSpecifier(opts.version);
|
|
55
|
-
|
|
56
111
|
// Check if already installed with the same specifier — no-op
|
|
57
112
|
const existing = (config['plugin'] as string[] | undefined) ?? [];
|
|
58
113
|
if (existing.includes(specifier)) {
|
|
@@ -66,9 +121,9 @@ export function runInstall(
|
|
|
66
121
|
addPlugin(config, specifier);
|
|
67
122
|
|
|
68
123
|
// Dry-run: serialize and print planned content, make no changes
|
|
69
|
-
if (
|
|
124
|
+
if (dryRun) {
|
|
70
125
|
const planned = JSON.stringify(config, null, 2);
|
|
71
|
-
console.log(
|
|
126
|
+
console.log(`Planned ${configPath}:\n${planned}`);
|
|
72
127
|
return { status: 'planned', path: configPath, specifier };
|
|
73
128
|
}
|
|
74
129
|
|
|
@@ -77,12 +132,9 @@ export function runInstall(
|
|
|
77
132
|
const serialized = JSON.stringify(config, null, 2);
|
|
78
133
|
writeAtomically(fs, configPath, serialized);
|
|
79
134
|
|
|
80
|
-
|
|
81
|
-
if (backup)
|
|
82
|
-
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
return { status: 'wrote', path: configPath, specifier, backup };
|
|
135
|
+
const result: ConfigUpdateResult = { status: 'wrote', path: configPath, specifier };
|
|
136
|
+
if (backup) result.backup = backup;
|
|
137
|
+
return result;
|
|
86
138
|
}
|
|
87
139
|
|
|
88
140
|
// ---------------------------------------------------------------------------
|
|
@@ -92,3 +144,28 @@ export function runInstall(
|
|
|
92
144
|
function buildSpecifier(version?: string): string {
|
|
93
145
|
return version ? `opencode-rules-md@${version}` : 'opencode-rules-md';
|
|
94
146
|
}
|
|
147
|
+
|
|
148
|
+
function printSummary(
|
|
149
|
+
status: InstallResult['status'],
|
|
150
|
+
specifier: string,
|
|
151
|
+
server: ConfigUpdateResult,
|
|
152
|
+
tui: ConfigUpdateResult
|
|
153
|
+
): void {
|
|
154
|
+
if (status === 'noop') {
|
|
155
|
+
console.log(`Already installed (${specifier})`);
|
|
156
|
+
console.log(` server config: ${server.path}`);
|
|
157
|
+
console.log(` tui config: ${tui.path}`);
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
if (status === 'planned') {
|
|
162
|
+
console.log('Dry run complete — no files written.');
|
|
163
|
+
return;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
console.log(`Installed ${specifier}`);
|
|
167
|
+
console.log(` server config: ${server.path}`);
|
|
168
|
+
if (server.backup) console.log(` backup: ${server.backup}`);
|
|
169
|
+
console.log(` tui config: ${tui.path}`);
|
|
170
|
+
if (tui.backup) console.log(` backup: ${tui.backup}`);
|
|
171
|
+
}
|
package/src/cli/main.ts
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* CLI entrypoint for opencode-rules-md installer.
|
|
4
4
|
*
|
|
5
5
|
* Supports:
|
|
6
|
-
* install — add opencode-rules-md to the global opencode
|
|
6
|
+
* install — add opencode-rules-md to the global opencode + tui configs
|
|
7
7
|
* status — report whether opencode-rules-md is installed
|
|
8
8
|
*
|
|
9
9
|
* Flags:
|
|
@@ -15,6 +15,8 @@
|
|
|
15
15
|
*/
|
|
16
16
|
|
|
17
17
|
import { parseArgs } from 'node:util';
|
|
18
|
+
import { pathToFileURL } from 'node:url';
|
|
19
|
+
import { realpathSync } from 'node:fs';
|
|
18
20
|
import { runInstall } from './install.js';
|
|
19
21
|
import { runStatus } from './status.js';
|
|
20
22
|
import type { CliFs } from './real-fs.js';
|
|
@@ -29,7 +31,7 @@ const HELP_TEXT = `opencode-rules-md CLI
|
|
|
29
31
|
Usage: opencode-rules-md <command> [options]
|
|
30
32
|
|
|
31
33
|
Commands:
|
|
32
|
-
install Add opencode-rules-md to the global opencode
|
|
34
|
+
install Add opencode-rules-md to the global opencode + tui configs
|
|
33
35
|
status Report whether opencode-rules-md is installed
|
|
34
36
|
|
|
35
37
|
Options:
|
|
@@ -40,8 +42,7 @@ Options:
|
|
|
40
42
|
-h, --help Show this help text
|
|
41
43
|
`.trim();
|
|
42
44
|
|
|
43
|
-
const USAGE_ERROR_TEXT = `Error: unknown command. Run 'opencode-rules-md --help' for usage
|
|
44
|
-
`.trim();
|
|
45
|
+
const USAGE_ERROR_TEXT = `Error: unknown command. Run 'opencode-rules-md --help' for usage.`.trim();
|
|
45
46
|
|
|
46
47
|
// ---------------------------------------------------------------------------
|
|
47
48
|
// Argument parsing
|
|
@@ -82,9 +83,6 @@ function parseCliArgs(argv: string[]): { command: string | null; options: CliOpt
|
|
|
82
83
|
...((values.help || values.h) && { help: true }),
|
|
83
84
|
};
|
|
84
85
|
|
|
85
|
-
// Detect unknown flags (tokens that look like flags but aren't recognized)
|
|
86
|
-
// parseArgs already filters known tokens, so we check if any positional
|
|
87
|
-
// looks like an unknown option
|
|
88
86
|
const unknownFlags: string[] = [];
|
|
89
87
|
|
|
90
88
|
return { command, options, unknownFlags };
|
|
@@ -124,26 +122,19 @@ export async function runMain(argv: string[], fs: CliFs = realFs): Promise<ExitC
|
|
|
124
122
|
const installOpts: { version?: string; dryRun?: boolean } = {};
|
|
125
123
|
if (options.version !== undefined) installOpts.version = options.version;
|
|
126
124
|
if (options.dryRun) installOpts.dryRun = true;
|
|
127
|
-
|
|
128
|
-
// Map install status to exit code
|
|
129
|
-
if (installResult.status === 'error') {
|
|
130
|
-
return 1;
|
|
131
|
-
}
|
|
125
|
+
runInstall(installOpts, fs);
|
|
132
126
|
return 0;
|
|
133
127
|
}
|
|
134
|
-
|
|
135
128
|
case 'status': {
|
|
136
129
|
const statusResult = runStatus(fs);
|
|
137
|
-
// Print status to stdout
|
|
138
130
|
if (statusResult.installed) {
|
|
139
|
-
|
|
131
|
+
const spec = statusResult.serverSpecifier ?? statusResult.tuiSpecifier;
|
|
132
|
+
console.log(`opencode-rules-md is installed (${spec})`);
|
|
140
133
|
} else {
|
|
141
134
|
console.log('opencode-rules-md is not installed');
|
|
142
135
|
}
|
|
143
|
-
// Exit code 0 regardless of install state (status is read-only)
|
|
144
136
|
return 0;
|
|
145
137
|
}
|
|
146
|
-
|
|
147
138
|
default: {
|
|
148
139
|
console.error(`Error: unknown command '${command}'`);
|
|
149
140
|
console.error(USAGE_ERROR_TEXT);
|
|
@@ -151,7 +142,8 @@ export async function runMain(argv: string[], fs: CliFs = realFs): Promise<ExitC
|
|
|
151
142
|
}
|
|
152
143
|
}
|
|
153
144
|
} catch (err) {
|
|
154
|
-
|
|
145
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
146
|
+
console.error(`Error: ${message}`);
|
|
155
147
|
return 1;
|
|
156
148
|
}
|
|
157
149
|
}
|
|
@@ -160,8 +152,27 @@ export async function runMain(argv: string[], fs: CliFs = realFs): Promise<ExitC
|
|
|
160
152
|
// Entry point
|
|
161
153
|
// ---------------------------------------------------------------------------
|
|
162
154
|
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
155
|
+
/**
|
|
156
|
+
* Determine whether the current module is being executed as the main entry.
|
|
157
|
+
* Uses realpathSync + pathToFileURL so symlinked invocations (e.g. npx)
|
|
158
|
+
* are matched correctly.
|
|
159
|
+
*/
|
|
160
|
+
function isInvokedAsMain(): boolean {
|
|
161
|
+
if (!process.argv[1]) return false;
|
|
162
|
+
|
|
163
|
+
try {
|
|
164
|
+
const realArgv = pathToFileURL(realpathSync(process.argv[1])).href;
|
|
165
|
+
return import.meta.url === realArgv;
|
|
166
|
+
} catch {
|
|
167
|
+
try {
|
|
168
|
+
return import.meta.url === pathToFileURL(process.argv[1]).href;
|
|
169
|
+
} catch {
|
|
170
|
+
return false;
|
|
171
|
+
}
|
|
172
|
+
}
|
|
167
173
|
}
|
|
174
|
+
|
|
175
|
+
// Only run if executed directly (not imported as a module)
|
|
176
|
+
if (isInvokedAsMain()) {
|
|
177
|
+
void runMain(process.argv.slice(2)).then(code => process.exit(code));
|
|
178
|
+
}
|
package/src/cli/status.ts
CHANGED
|
@@ -1,14 +1,16 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* status command: read-only probe that reports install state.
|
|
3
3
|
*
|
|
4
|
-
* Reports:
|
|
5
|
-
* - installed: yes/no
|
|
6
|
-
* -
|
|
7
|
-
* -
|
|
8
|
-
* -
|
|
4
|
+
* Reports install state across BOTH the server and TUI configs:
|
|
5
|
+
* - installed: yes/no (true only when present in both configs)
|
|
6
|
+
* - serverSpecifier: the registered entry in opencode.json (or undefined)
|
|
7
|
+
* - tuiSpecifier: the registered entry in tui.json (or undefined)
|
|
8
|
+
* - serverPath: path to the server config
|
|
9
|
+
* - tuiPath: path to the TUI config
|
|
10
|
+
* - version: bundled CLI version from package.json
|
|
9
11
|
*/
|
|
10
12
|
|
|
11
|
-
import { loadGlobalConfig } from './config.js';
|
|
13
|
+
import { loadGlobalConfig, TUI_CONFIG_FILENAME } from './config.js';
|
|
12
14
|
import type { CliFs } from './real-fs.js';
|
|
13
15
|
import { realFs } from './real-fs.js';
|
|
14
16
|
import * as fs from 'node:fs';
|
|
@@ -19,10 +21,23 @@ import * as path from 'node:path';
|
|
|
19
21
|
// ---------------------------------------------------------------------------
|
|
20
22
|
|
|
21
23
|
export interface StatusResult {
|
|
24
|
+
/** True only when the plugin is registered in both server and TUI configs. */
|
|
22
25
|
installed: boolean;
|
|
23
|
-
path
|
|
24
|
-
|
|
26
|
+
/** Server config path. */
|
|
27
|
+
serverPath: string;
|
|
28
|
+
/** TUI config path. */
|
|
29
|
+
tuiPath: string;
|
|
30
|
+
/** Registered specifier in the server config, if any. */
|
|
31
|
+
serverSpecifier?: string;
|
|
32
|
+
/** Registered specifier in the TUI config, if any. */
|
|
33
|
+
tuiSpecifier?: string;
|
|
34
|
+
/** Whether the server config existed on disk before the probe. */
|
|
35
|
+
serverExisted: boolean;
|
|
36
|
+
/** Whether the TUI config existed on disk before the probe. */
|
|
37
|
+
tuiExisted: boolean;
|
|
38
|
+
/** Bundled CLI version from package.json. */
|
|
25
39
|
version: string;
|
|
40
|
+
/** First parse error encountered, if any (server takes priority). */
|
|
26
41
|
parseError?: Error;
|
|
27
42
|
}
|
|
28
43
|
|
|
@@ -30,35 +45,30 @@ export interface StatusResult {
|
|
|
30
45
|
// Run status
|
|
31
46
|
// ---------------------------------------------------------------------------
|
|
32
47
|
|
|
33
|
-
export function runStatus(
|
|
34
|
-
const
|
|
35
|
-
const
|
|
48
|
+
export function runStatus(cliFs: CliFs = realFs): StatusResult {
|
|
49
|
+
const serverLoad = loadGlobalConfig(cliFs);
|
|
50
|
+
const tuiLoad = loadGlobalConfig(cliFs, { filename: TUI_CONFIG_FILENAME });
|
|
36
51
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
return {
|
|
40
|
-
installed: false,
|
|
41
|
-
path: configPath,
|
|
42
|
-
version: getVersion(),
|
|
43
|
-
parseError: loadResult.parseError,
|
|
44
|
-
};
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
const config = loadResult.config;
|
|
48
|
-
const pluginList: string[] = Array.isArray(config['plugin']) ? (config['plugin'] as string[]) : [];
|
|
52
|
+
const serverSpecifier = findSpecifier(serverLoad);
|
|
53
|
+
const tuiSpecifier = findSpecifier(tuiLoad);
|
|
49
54
|
|
|
50
|
-
|
|
51
|
-
const specifier = pluginList.find(p => p.startsWith('opencode-rules-md'));
|
|
55
|
+
const installed = serverSpecifier !== undefined && tuiSpecifier !== undefined;
|
|
52
56
|
|
|
53
57
|
const result: StatusResult = {
|
|
54
|
-
installed
|
|
55
|
-
|
|
58
|
+
installed,
|
|
59
|
+
serverPath: serverLoad.path,
|
|
60
|
+
tuiPath: tuiLoad.path,
|
|
61
|
+
serverExisted: serverLoad.existed,
|
|
62
|
+
tuiExisted: tuiLoad.existed,
|
|
56
63
|
version: getVersion(),
|
|
57
64
|
};
|
|
58
65
|
|
|
59
|
-
if (
|
|
60
|
-
|
|
61
|
-
|
|
66
|
+
if (serverSpecifier) result.serverSpecifier = serverSpecifier;
|
|
67
|
+
if (tuiSpecifier) result.tuiSpecifier = tuiSpecifier;
|
|
68
|
+
|
|
69
|
+
// Surface the first parse error so callers can warn the user
|
|
70
|
+
const parseError = serverLoad.parseError ?? tuiLoad.parseError;
|
|
71
|
+
if (parseError) result.parseError = parseError;
|
|
62
72
|
|
|
63
73
|
return result;
|
|
64
74
|
}
|
|
@@ -67,6 +77,15 @@ export function runStatus(fs: CliFs = realFs): StatusResult {
|
|
|
67
77
|
// Internal helpers
|
|
68
78
|
// ---------------------------------------------------------------------------
|
|
69
79
|
|
|
80
|
+
function findSpecifier(load: ReturnType<typeof loadGlobalConfig>): string | undefined {
|
|
81
|
+
if (load.parseError) return undefined;
|
|
82
|
+
const config = load.config;
|
|
83
|
+
const pluginList: string[] = Array.isArray(config['plugin'])
|
|
84
|
+
? (config['plugin'] as string[])
|
|
85
|
+
: [];
|
|
86
|
+
return pluginList.find(p => p.startsWith('opencode-rules-md'));
|
|
87
|
+
}
|
|
88
|
+
|
|
70
89
|
function getVersion(): string {
|
|
71
90
|
try {
|
|
72
91
|
// Read package.json relative to the project root
|
|
@@ -77,4 +96,4 @@ function getVersion(): string {
|
|
|
77
96
|
} catch {
|
|
78
97
|
return 'unknown';
|
|
79
98
|
}
|
|
80
|
-
}
|
|
99
|
+
}
|