opencode-rules-md 0.6.5 → 0.6.6
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 +5 -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/dist/cli.mjs
CHANGED
|
@@ -2,67 +2,104 @@
|
|
|
2
2
|
|
|
3
3
|
// src/cli/main.ts
|
|
4
4
|
import { parseArgs } from "node:util";
|
|
5
|
+
import { pathToFileURL } from "node:url";
|
|
6
|
+
import { realpathSync } from "node:fs";
|
|
5
7
|
|
|
6
8
|
// src/cli/config.ts
|
|
7
9
|
import * as os from "node:os";
|
|
8
10
|
import * as path from "node:path";
|
|
11
|
+
var SERVER_CONFIG_FILENAME = "opencode.json";
|
|
12
|
+
var TUI_CONFIG_FILENAME = "tui.json";
|
|
9
13
|
function stripJsoncComments(content) {
|
|
10
14
|
let result = "";
|
|
11
15
|
let i = 0;
|
|
12
|
-
|
|
16
|
+
let inString = false;
|
|
17
|
+
let escaped = false;
|
|
18
|
+
const len = content.length;
|
|
19
|
+
while (i < len) {
|
|
13
20
|
const char = content[i];
|
|
21
|
+
if (inString) {
|
|
22
|
+
result += char;
|
|
23
|
+
if (escaped) {
|
|
24
|
+
escaped = false;
|
|
25
|
+
} else if (char === "\\") {
|
|
26
|
+
escaped = true;
|
|
27
|
+
} else if (char === '"') {
|
|
28
|
+
inString = false;
|
|
29
|
+
}
|
|
30
|
+
i++;
|
|
31
|
+
continue;
|
|
32
|
+
}
|
|
14
33
|
if (char === '"') {
|
|
34
|
+
inString = true;
|
|
15
35
|
result += char;
|
|
16
36
|
i++;
|
|
17
|
-
while (i < content.length) {
|
|
18
|
-
const c = content[i];
|
|
19
|
-
if (c === "\\" && i + 1 < content.length) {
|
|
20
|
-
result += c + content[i + 1];
|
|
21
|
-
i += 2;
|
|
22
|
-
continue;
|
|
23
|
-
}
|
|
24
|
-
if (c === '"') {
|
|
25
|
-
result += c;
|
|
26
|
-
i++;
|
|
27
|
-
break;
|
|
28
|
-
}
|
|
29
|
-
result += c;
|
|
30
|
-
i++;
|
|
31
|
-
}
|
|
32
37
|
continue;
|
|
33
38
|
}
|
|
34
39
|
if (char === "/" && content[i + 1] === "/") {
|
|
35
|
-
|
|
36
|
-
`
|
|
40
|
+
i += 2;
|
|
41
|
+
while (i < len && content[i] !== `
|
|
42
|
+
`)
|
|
37
43
|
i++;
|
|
38
|
-
}
|
|
39
44
|
continue;
|
|
40
45
|
}
|
|
41
46
|
if (char === "/" && content[i + 1] === "*") {
|
|
42
47
|
i += 2;
|
|
43
|
-
while (i
|
|
48
|
+
while (i < len && !(content[i] === "*" && content[i + 1] === "/"))
|
|
44
49
|
i++;
|
|
45
|
-
|
|
46
|
-
if (i + 1 < content.length) {
|
|
50
|
+
if (i < len)
|
|
47
51
|
i += 2;
|
|
52
|
+
continue;
|
|
53
|
+
}
|
|
54
|
+
result += char;
|
|
55
|
+
i++;
|
|
56
|
+
}
|
|
57
|
+
let out = "";
|
|
58
|
+
let inStr = false;
|
|
59
|
+
let esc = false;
|
|
60
|
+
let j = 0;
|
|
61
|
+
const outLen = result.length;
|
|
62
|
+
while (j < outLen) {
|
|
63
|
+
const ch = result[j];
|
|
64
|
+
if (inStr) {
|
|
65
|
+
out += ch;
|
|
66
|
+
if (esc) {
|
|
67
|
+
esc = false;
|
|
68
|
+
} else if (ch === "\\") {
|
|
69
|
+
esc = true;
|
|
70
|
+
} else if (ch === '"') {
|
|
71
|
+
inStr = false;
|
|
48
72
|
}
|
|
73
|
+
j++;
|
|
49
74
|
continue;
|
|
50
75
|
}
|
|
51
|
-
if (
|
|
52
|
-
|
|
76
|
+
if (ch === '"') {
|
|
77
|
+
inStr = true;
|
|
78
|
+
out += ch;
|
|
79
|
+
j++;
|
|
53
80
|
continue;
|
|
54
81
|
}
|
|
55
|
-
|
|
56
|
-
|
|
82
|
+
if (ch === ",") {
|
|
83
|
+
let k = j + 1;
|
|
84
|
+
while (k < outLen && /\s/.test(result[k]))
|
|
85
|
+
k++;
|
|
86
|
+
if (k < outLen && /[}\]]/.test(result[k])) {
|
|
87
|
+
j = k;
|
|
88
|
+
continue;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
out += ch;
|
|
92
|
+
j++;
|
|
57
93
|
}
|
|
58
|
-
return
|
|
94
|
+
return out;
|
|
59
95
|
}
|
|
60
96
|
function resolveGlobalConfigPath(fs, opts = {}) {
|
|
61
97
|
const dir = resolveConfigDir();
|
|
62
98
|
if (opts.ensureDir && !fs.existsSync(dir)) {
|
|
63
99
|
fs.mkdirSync(dir, { recursive: true });
|
|
64
100
|
}
|
|
65
|
-
|
|
101
|
+
const filename = opts.filename ?? SERVER_CONFIG_FILENAME;
|
|
102
|
+
return path.join(dir, filename);
|
|
66
103
|
}
|
|
67
104
|
function resolveConfigDir() {
|
|
68
105
|
if (process.env.OPENCODE_CONFIG_DIR) {
|
|
@@ -81,8 +118,8 @@ function resolveConfigDir() {
|
|
|
81
118
|
}
|
|
82
119
|
throw new Error("Cannot resolve config dir: none of OPENCODE_CONFIG_DIR, XDG_CONFIG_HOME, HOME, or APPDATA is set");
|
|
83
120
|
}
|
|
84
|
-
function loadGlobalConfig(fs) {
|
|
85
|
-
const configPath = resolveGlobalConfigPath(fs);
|
|
121
|
+
function loadGlobalConfig(fs, opts = {}) {
|
|
122
|
+
const configPath = resolveGlobalConfigPath(fs, opts.filename ? { filename: opts.filename } : {});
|
|
86
123
|
if (!fs.existsSync(configPath)) {
|
|
87
124
|
return { path: configPath, existed: false, config: {} };
|
|
88
125
|
}
|
|
@@ -129,15 +166,14 @@ function addPlugin(config, specifier) {
|
|
|
129
166
|
config["plugin"] = removed;
|
|
130
167
|
}
|
|
131
168
|
function rotateBackups(fs, configPath, _pluginName) {
|
|
169
|
+
if (!fs.existsSync(configPath)) {
|
|
170
|
+
return null;
|
|
171
|
+
}
|
|
132
172
|
const dir = path.dirname(configPath);
|
|
133
173
|
const base = path.basename(configPath);
|
|
134
174
|
const timestamp = Date.now();
|
|
135
175
|
const newBak = path.join(dir, `${base}.bak.${timestamp}`);
|
|
136
|
-
|
|
137
|
-
fs.copyFileSync(configPath, newBak);
|
|
138
|
-
} else {
|
|
139
|
-
fs.writeFileSync(newBak, "", "utf-8");
|
|
140
|
-
}
|
|
176
|
+
fs.copyFileSync(configPath, newBak);
|
|
141
177
|
const entries = fs.readdirSync(dir);
|
|
142
178
|
const bakFiles = entries.filter((e) => e.startsWith(base + ".bak.")).sort();
|
|
143
179
|
while (bakFiles.length > 3) {
|
|
@@ -149,7 +185,7 @@ function rotateBackups(fs, configPath, _pluginName) {
|
|
|
149
185
|
function writeAtomically(fs, configPath, content) {
|
|
150
186
|
const dir = path.dirname(configPath);
|
|
151
187
|
const base = path.basename(configPath);
|
|
152
|
-
const tmpPath = path.join(dir, `.${base}.tmp.${Date.now()}`);
|
|
188
|
+
const tmpPath = path.join(dir, `.${base}.tmp.${Date.now()}-${process.pid}`);
|
|
153
189
|
fs.writeFileSync(tmpPath, content, "utf-8");
|
|
154
190
|
fs.renameSync(tmpPath, configPath);
|
|
155
191
|
}
|
|
@@ -198,71 +234,104 @@ var realFs = {
|
|
|
198
234
|
|
|
199
235
|
// src/cli/install.ts
|
|
200
236
|
function runInstall(opts, fs2 = realFs) {
|
|
201
|
-
const
|
|
237
|
+
const specifier = buildSpecifier(opts.version);
|
|
238
|
+
const server = updateConfig(specifier, opts.dryRun ?? false, fs2);
|
|
239
|
+
const tui = updateConfig(specifier, opts.dryRun ?? false, fs2, {
|
|
240
|
+
filename: TUI_CONFIG_FILENAME
|
|
241
|
+
});
|
|
242
|
+
const aggregate = server.status === "wrote" || tui.status === "wrote" ? "wrote" : server.status === "planned" || tui.status === "planned" ? "planned" : "noop";
|
|
243
|
+
printSummary(aggregate, specifier, server, tui);
|
|
244
|
+
return { status: aggregate, specifier, server, tui };
|
|
245
|
+
}
|
|
246
|
+
function updateConfig(specifier, dryRun, fs2, opts = {}) {
|
|
247
|
+
const loadResult = loadGlobalConfig(fs2, opts.filename ? { filename: opts.filename } : {});
|
|
202
248
|
if (loadResult.parseError) {
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
249
|
+
const filename = opts.filename ?? "opencode.json";
|
|
250
|
+
const err = new Error(`opencode-rules-md: ${filename} is malformed — aborting to avoid data loss.
|
|
251
|
+
` + ` path: ${loadResult.path}
|
|
252
|
+
` + ` error: ${loadResult.parseError.message}`);
|
|
253
|
+
err.configPath = loadResult.path;
|
|
254
|
+
throw err;
|
|
209
255
|
}
|
|
210
256
|
const configPath = loadResult.path;
|
|
211
257
|
const config = loadResult.config;
|
|
212
258
|
config["plugin"] = normalizePlugin(config);
|
|
213
|
-
const specifier = buildSpecifier(opts.version);
|
|
214
259
|
const existing = config["plugin"] ?? [];
|
|
215
260
|
if (existing.includes(specifier)) {
|
|
216
261
|
return { status: "noop", path: configPath, specifier };
|
|
217
262
|
}
|
|
218
263
|
removePlugin(config, "opencode-rules-md");
|
|
219
264
|
addPlugin(config, specifier);
|
|
220
|
-
if (
|
|
265
|
+
if (dryRun) {
|
|
221
266
|
const planned = JSON.stringify(config, null, 2);
|
|
222
|
-
console.log(`Planned
|
|
223
|
-
`
|
|
267
|
+
console.log(`Planned ${configPath}:
|
|
268
|
+
${planned}`);
|
|
224
269
|
return { status: "planned", path: configPath, specifier };
|
|
225
270
|
}
|
|
226
271
|
const backup = rotateBackups(fs2, configPath, "opencode-rules-md");
|
|
227
272
|
const serialized = JSON.stringify(config, null, 2);
|
|
228
273
|
writeAtomically(fs2, configPath, serialized);
|
|
229
|
-
|
|
230
|
-
if (backup)
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
return { status: "wrote", path: configPath, specifier, backup };
|
|
274
|
+
const result = { status: "wrote", path: configPath, specifier };
|
|
275
|
+
if (backup)
|
|
276
|
+
result.backup = backup;
|
|
277
|
+
return result;
|
|
234
278
|
}
|
|
235
279
|
function buildSpecifier(version) {
|
|
236
280
|
return version ? `opencode-rules-md@${version}` : "opencode-rules-md";
|
|
237
281
|
}
|
|
282
|
+
function printSummary(status, specifier, server, tui) {
|
|
283
|
+
if (status === "noop") {
|
|
284
|
+
console.log(`Already installed (${specifier})`);
|
|
285
|
+
console.log(` server config: ${server.path}`);
|
|
286
|
+
console.log(` tui config: ${tui.path}`);
|
|
287
|
+
return;
|
|
288
|
+
}
|
|
289
|
+
if (status === "planned") {
|
|
290
|
+
console.log("Dry run complete — no files written.");
|
|
291
|
+
return;
|
|
292
|
+
}
|
|
293
|
+
console.log(`Installed ${specifier}`);
|
|
294
|
+
console.log(` server config: ${server.path}`);
|
|
295
|
+
if (server.backup)
|
|
296
|
+
console.log(` backup: ${server.backup}`);
|
|
297
|
+
console.log(` tui config: ${tui.path}`);
|
|
298
|
+
if (tui.backup)
|
|
299
|
+
console.log(` backup: ${tui.backup}`);
|
|
300
|
+
}
|
|
238
301
|
|
|
239
302
|
// src/cli/status.ts
|
|
240
303
|
import * as fs2 from "node:fs";
|
|
241
304
|
import * as path2 from "node:path";
|
|
242
|
-
function runStatus(
|
|
243
|
-
const
|
|
244
|
-
const
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
path: configPath,
|
|
249
|
-
version: getVersion(),
|
|
250
|
-
parseError: loadResult.parseError
|
|
251
|
-
};
|
|
252
|
-
}
|
|
253
|
-
const config = loadResult.config;
|
|
254
|
-
const pluginList = Array.isArray(config["plugin"]) ? config["plugin"] : [];
|
|
255
|
-
const specifier = pluginList.find((p) => p.startsWith("opencode-rules-md"));
|
|
305
|
+
function runStatus(cliFs = realFs) {
|
|
306
|
+
const serverLoad = loadGlobalConfig(cliFs);
|
|
307
|
+
const tuiLoad = loadGlobalConfig(cliFs, { filename: TUI_CONFIG_FILENAME });
|
|
308
|
+
const serverSpecifier = findSpecifier(serverLoad);
|
|
309
|
+
const tuiSpecifier = findSpecifier(tuiLoad);
|
|
310
|
+
const installed = serverSpecifier !== undefined && tuiSpecifier !== undefined;
|
|
256
311
|
const result = {
|
|
257
|
-
installed
|
|
258
|
-
|
|
312
|
+
installed,
|
|
313
|
+
serverPath: serverLoad.path,
|
|
314
|
+
tuiPath: tuiLoad.path,
|
|
315
|
+
serverExisted: serverLoad.existed,
|
|
316
|
+
tuiExisted: tuiLoad.existed,
|
|
259
317
|
version: getVersion()
|
|
260
318
|
};
|
|
261
|
-
if (
|
|
262
|
-
result.
|
|
263
|
-
|
|
319
|
+
if (serverSpecifier)
|
|
320
|
+
result.serverSpecifier = serverSpecifier;
|
|
321
|
+
if (tuiSpecifier)
|
|
322
|
+
result.tuiSpecifier = tuiSpecifier;
|
|
323
|
+
const parseError = serverLoad.parseError ?? tuiLoad.parseError;
|
|
324
|
+
if (parseError)
|
|
325
|
+
result.parseError = parseError;
|
|
264
326
|
return result;
|
|
265
327
|
}
|
|
328
|
+
function findSpecifier(load) {
|
|
329
|
+
if (load.parseError)
|
|
330
|
+
return;
|
|
331
|
+
const config = load.config;
|
|
332
|
+
const pluginList = Array.isArray(config["plugin"]) ? config["plugin"] : [];
|
|
333
|
+
return pluginList.find((p) => p.startsWith("opencode-rules-md"));
|
|
334
|
+
}
|
|
266
335
|
function getVersion() {
|
|
267
336
|
try {
|
|
268
337
|
const pkgPath = path2.resolve(process.cwd(), "package.json");
|
|
@@ -280,7 +349,7 @@ var HELP_TEXT = `opencode-rules-md CLI
|
|
|
280
349
|
Usage: opencode-rules-md <command> [options]
|
|
281
350
|
|
|
282
351
|
Commands:
|
|
283
|
-
install Add opencode-rules-md to the global opencode
|
|
352
|
+
install Add opencode-rules-md to the global opencode + tui configs
|
|
284
353
|
status Report whether opencode-rules-md is installed
|
|
285
354
|
|
|
286
355
|
Options:
|
|
@@ -290,8 +359,7 @@ Options:
|
|
|
290
359
|
--yes Skip confirmation prompts (future use)
|
|
291
360
|
-h, --help Show this help text
|
|
292
361
|
`.trim();
|
|
293
|
-
var USAGE_ERROR_TEXT = `Error: unknown command. Run 'opencode-rules-md --help' for usage
|
|
294
|
-
`.trim();
|
|
362
|
+
var USAGE_ERROR_TEXT = `Error: unknown command. Run 'opencode-rules-md --help' for usage.`.trim();
|
|
295
363
|
function parseCliArgs(argv) {
|
|
296
364
|
const { positionals, values } = parseArgs({
|
|
297
365
|
args: argv,
|
|
@@ -339,16 +407,14 @@ async function runMain(argv, fs3 = realFs) {
|
|
|
339
407
|
installOpts.version = options.version;
|
|
340
408
|
if (options.dryRun)
|
|
341
409
|
installOpts.dryRun = true;
|
|
342
|
-
|
|
343
|
-
if (installResult.status === "error") {
|
|
344
|
-
return 1;
|
|
345
|
-
}
|
|
410
|
+
runInstall(installOpts, fs3);
|
|
346
411
|
return 0;
|
|
347
412
|
}
|
|
348
413
|
case "status": {
|
|
349
414
|
const statusResult = runStatus(fs3);
|
|
350
415
|
if (statusResult.installed) {
|
|
351
|
-
|
|
416
|
+
const spec = statusResult.serverSpecifier ?? statusResult.tuiSpecifier;
|
|
417
|
+
console.log(`opencode-rules-md is installed (${spec})`);
|
|
352
418
|
} else {
|
|
353
419
|
console.log("opencode-rules-md is not installed");
|
|
354
420
|
}
|
|
@@ -361,13 +427,27 @@ async function runMain(argv, fs3 = realFs) {
|
|
|
361
427
|
}
|
|
362
428
|
}
|
|
363
429
|
} catch (err) {
|
|
364
|
-
|
|
430
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
431
|
+
console.error(`Error: ${message}`);
|
|
365
432
|
return 1;
|
|
366
433
|
}
|
|
367
434
|
}
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
435
|
+
function isInvokedAsMain() {
|
|
436
|
+
if (!process.argv[1])
|
|
437
|
+
return false;
|
|
438
|
+
try {
|
|
439
|
+
const realArgv = pathToFileURL(realpathSync(process.argv[1])).href;
|
|
440
|
+
return import.meta.url === realArgv;
|
|
441
|
+
} catch {
|
|
442
|
+
try {
|
|
443
|
+
return import.meta.url === pathToFileURL(process.argv[1]).href;
|
|
444
|
+
} catch {
|
|
445
|
+
return false;
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
if (isInvokedAsMain()) {
|
|
450
|
+
runMain(process.argv.slice(2)).then((code) => process.exit(code));
|
|
371
451
|
}
|
|
372
452
|
export {
|
|
373
453
|
runMain
|
package/dist/src/cli/config.d.ts
CHANGED
|
@@ -6,6 +6,12 @@
|
|
|
6
6
|
* an in-memory implementation without touching real disk.
|
|
7
7
|
*/
|
|
8
8
|
import type { CliFs } from './real-fs.js';
|
|
9
|
+
/** npm package name for this plugin. */
|
|
10
|
+
export declare const PLUGIN_NAME = "opencode-rules-md";
|
|
11
|
+
/** Filename for the OpenCode server config. */
|
|
12
|
+
export declare const SERVER_CONFIG_FILENAME = "opencode.json";
|
|
13
|
+
/** Filename for the OpenCode TUI config. */
|
|
14
|
+
export declare const TUI_CONFIG_FILENAME = "tui.json";
|
|
9
15
|
export interface LoadResult {
|
|
10
16
|
path: string;
|
|
11
17
|
existed: boolean;
|
|
@@ -15,6 +21,9 @@ export interface LoadResult {
|
|
|
15
21
|
/**
|
|
16
22
|
* Strip JSONC comments and trailing commas while preserving
|
|
17
23
|
* comment-like sequences inside string literals.
|
|
24
|
+
*
|
|
25
|
+
* Pass 1: strip `//` and `/* * /` comments (string-aware).
|
|
26
|
+
* Pass 2: strip trailing commas before `}` or `]`, skipping whitespace.
|
|
18
27
|
*/
|
|
19
28
|
export declare function stripJsoncComments(content: string): string;
|
|
20
29
|
/**
|
|
@@ -26,13 +35,16 @@ export declare function stripJsoncComments(content: string): string;
|
|
|
26
35
|
*/
|
|
27
36
|
export declare function resolveGlobalConfigPath(fs: CliFs, opts?: {
|
|
28
37
|
ensureDir?: boolean;
|
|
38
|
+
filename?: string;
|
|
29
39
|
}): string;
|
|
30
40
|
/**
|
|
31
|
-
* Load and parse
|
|
41
|
+
* Load and parse a global OpenCode config file.
|
|
32
42
|
* Treats a missing file as an empty config object (not an error).
|
|
33
43
|
* Surfaces parse errors so callers can exit non-zero without corrupting data.
|
|
34
44
|
*/
|
|
35
|
-
export declare function loadGlobalConfig(fs: CliFs
|
|
45
|
+
export declare function loadGlobalConfig(fs: CliFs, opts?: {
|
|
46
|
+
filename?: string;
|
|
47
|
+
}): LoadResult;
|
|
36
48
|
/**
|
|
37
49
|
* Return the plugin array with opencode-rules-md* entries deduplicated by
|
|
38
50
|
* name prefix, keeping the last occurrence of each unique prefix.
|
|
@@ -51,9 +63,9 @@ export declare function addPlugin(config: Record<string, unknown>, specifier: st
|
|
|
51
63
|
/**
|
|
52
64
|
* Write a timestamped backup of the given config path and rotate so at most
|
|
53
65
|
* 3 backups are retained (oldest deleted first).
|
|
54
|
-
* Returns the path of the newly created backup.
|
|
66
|
+
* Returns the path of the newly created backup, or null if no backup was created.
|
|
55
67
|
*/
|
|
56
|
-
export declare function rotateBackups(fs: CliFs, configPath: string, _pluginName: string): string;
|
|
68
|
+
export declare function rotateBackups(fs: CliFs, configPath: string, _pluginName: string): string | null;
|
|
57
69
|
/**
|
|
58
70
|
* Write content to a temporary sibling file then rename it into place.
|
|
59
71
|
* The final path is never in a partially-written state.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../../../src/cli/config.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAIH,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,cAAc,CAAC;AAM1C,MAAM,WAAW,UAAU;IACzB,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,OAAO,CAAC;IACjB,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAChC,UAAU,CAAC,EAAE,KAAK,CAAC;CACpB;AAMD
|
|
1
|
+
{"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../../../src/cli/config.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAIH,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,cAAc,CAAC;AAM1C,wCAAwC;AACxC,eAAO,MAAM,WAAW,sBAAsB,CAAC;AAE/C,+CAA+C;AAC/C,eAAO,MAAM,sBAAsB,kBAAkB,CAAC;AAEtD,4CAA4C;AAC5C,eAAO,MAAM,mBAAmB,aAAa,CAAC;AAM9C,MAAM,WAAW,UAAU;IACzB,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,OAAO,CAAC;IACjB,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAChC,UAAU,CAAC,EAAE,KAAK,CAAC;CACpB;AAMD;;;;;;GAMG;AACH,wBAAgB,kBAAkB,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,CAgG1D;AAMD;;;;;;GAMG;AACH,wBAAgB,uBAAuB,CACrC,EAAE,EAAE,KAAK,EACT,IAAI,GAAE;IAAE,SAAS,CAAC,EAAE,OAAO,CAAC;IAAC,QAAQ,CAAC,EAAE,MAAM,CAAA;CAAO,GACpD,MAAM,CAOR;AA+BD;;;;GAIG;AACH,wBAAgB,gBAAgB,CAAC,EAAE,EAAE,KAAK,EAAE,IAAI,GAAE;IAAE,QAAQ,CAAC,EAAE,MAAM,CAAA;CAAO,GAAG,UAAU,CAuBxF;AAMD;;;GAGG;AACH,wBAAgB,eAAe,CAC7B,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAC9B,MAAM,EAAE,CAcV;AAED;;GAEG;AACH,wBAAgB,YAAY,CAC1B,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC/B,MAAM,EAAE,MAAM,GACb,IAAI,CAMN;AAED;;;;GAIG;AACH,wBAAgB,SAAS,CACvB,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC/B,SAAS,EAAE,MAAM,GAChB,IAAI,CAUN;AAMD;;;;GAIG;AACH,wBAAgB,aAAa,CAAC,EAAE,EAAE,KAAK,EAAE,UAAU,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAyB/F;AAMD;;;GAGG;AACH,wBAAgB,eAAe,CAAC,EAAE,EAAE,KAAK,EAAE,UAAU,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,IAAI,CAOpF"}
|
package/dist/src/cli/config.js
CHANGED
|
@@ -8,68 +8,115 @@
|
|
|
8
8
|
import * as os from 'node:os';
|
|
9
9
|
import * as path from 'node:path';
|
|
10
10
|
// ---------------------------------------------------------------------------
|
|
11
|
-
//
|
|
11
|
+
// Public constants
|
|
12
|
+
// ---------------------------------------------------------------------------
|
|
13
|
+
/** npm package name for this plugin. */
|
|
14
|
+
export const PLUGIN_NAME = 'opencode-rules-md';
|
|
15
|
+
/** Filename for the OpenCode server config. */
|
|
16
|
+
export const SERVER_CONFIG_FILENAME = 'opencode.json';
|
|
17
|
+
/** Filename for the OpenCode TUI config. */
|
|
18
|
+
export const TUI_CONFIG_FILENAME = 'tui.json';
|
|
19
|
+
// ---------------------------------------------------------------------------
|
|
20
|
+
// JSONC comment stripper — two-pass
|
|
12
21
|
// ---------------------------------------------------------------------------
|
|
13
22
|
/**
|
|
14
23
|
* Strip JSONC comments and trailing commas while preserving
|
|
15
24
|
* comment-like sequences inside string literals.
|
|
25
|
+
*
|
|
26
|
+
* Pass 1: strip `//` and `/* * /` comments (string-aware).
|
|
27
|
+
* Pass 2: strip trailing commas before `}` or `]`, skipping whitespace.
|
|
16
28
|
*/
|
|
17
29
|
export function stripJsoncComments(content) {
|
|
30
|
+
// Pass 1 — strip comments while preserving string contents
|
|
18
31
|
let result = '';
|
|
19
32
|
let i = 0;
|
|
20
|
-
|
|
33
|
+
let inString = false;
|
|
34
|
+
let escaped = false;
|
|
35
|
+
const len = content.length;
|
|
36
|
+
while (i < len) {
|
|
21
37
|
const char = content[i];
|
|
22
|
-
|
|
38
|
+
if (inString) {
|
|
39
|
+
result += char;
|
|
40
|
+
if (escaped) {
|
|
41
|
+
escaped = false;
|
|
42
|
+
}
|
|
43
|
+
else if (char === '\\') {
|
|
44
|
+
escaped = true;
|
|
45
|
+
}
|
|
46
|
+
else if (char === '"') {
|
|
47
|
+
inString = false;
|
|
48
|
+
}
|
|
49
|
+
i++;
|
|
50
|
+
continue;
|
|
51
|
+
}
|
|
23
52
|
if (char === '"') {
|
|
53
|
+
inString = true;
|
|
24
54
|
result += char;
|
|
25
55
|
i++;
|
|
26
|
-
while (i < content.length) {
|
|
27
|
-
const c = content[i];
|
|
28
|
-
if (c === '\\' && i + 1 < content.length) {
|
|
29
|
-
// Escape sequence — copy both chars verbatim
|
|
30
|
-
result += c + content[i + 1];
|
|
31
|
-
i += 2;
|
|
32
|
-
continue;
|
|
33
|
-
}
|
|
34
|
-
if (c === '"') {
|
|
35
|
-
result += c;
|
|
36
|
-
i++;
|
|
37
|
-
break;
|
|
38
|
-
}
|
|
39
|
-
result += c;
|
|
40
|
-
i++;
|
|
41
|
-
}
|
|
42
56
|
continue;
|
|
43
57
|
}
|
|
44
|
-
// Line comment
|
|
58
|
+
// Line comment: // until newline
|
|
45
59
|
if (char === '/' && content[i + 1] === '/') {
|
|
46
|
-
|
|
60
|
+
i += 2;
|
|
61
|
+
while (i < len && content[i] !== '\n')
|
|
47
62
|
i++;
|
|
48
|
-
}
|
|
49
63
|
continue;
|
|
50
64
|
}
|
|
51
|
-
// Block comment
|
|
65
|
+
// Block comment: /* until */
|
|
52
66
|
if (char === '/' && content[i + 1] === '*') {
|
|
53
67
|
i += 2;
|
|
54
|
-
while (i
|
|
68
|
+
while (i < len && !(content[i] === '*' && content[i + 1] === '/'))
|
|
55
69
|
i++;
|
|
70
|
+
if (i < len)
|
|
71
|
+
i += 2; // skip past */
|
|
72
|
+
continue;
|
|
73
|
+
}
|
|
74
|
+
result += char;
|
|
75
|
+
i++;
|
|
76
|
+
}
|
|
77
|
+
// Pass 2 — strip trailing commas (with whitespace skip) before } or ]
|
|
78
|
+
let out = '';
|
|
79
|
+
let inStr = false;
|
|
80
|
+
let esc = false;
|
|
81
|
+
let j = 0;
|
|
82
|
+
const outLen = result.length;
|
|
83
|
+
while (j < outLen) {
|
|
84
|
+
const ch = result[j];
|
|
85
|
+
if (inStr) {
|
|
86
|
+
out += ch;
|
|
87
|
+
if (esc) {
|
|
88
|
+
esc = false;
|
|
56
89
|
}
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
i += 2; // skip */
|
|
90
|
+
else if (ch === '\\') {
|
|
91
|
+
esc = true;
|
|
60
92
|
}
|
|
93
|
+
else if (ch === '"') {
|
|
94
|
+
inStr = false;
|
|
95
|
+
}
|
|
96
|
+
j++;
|
|
61
97
|
continue;
|
|
62
98
|
}
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
99
|
+
if (ch === '"') {
|
|
100
|
+
inStr = true;
|
|
101
|
+
out += ch;
|
|
102
|
+
j++;
|
|
67
103
|
continue;
|
|
68
104
|
}
|
|
69
|
-
|
|
70
|
-
|
|
105
|
+
if (ch === ',') {
|
|
106
|
+
// Skip whitespace before checking for } or ]
|
|
107
|
+
let k = j + 1;
|
|
108
|
+
while (k < outLen && /\s/.test(result[k]))
|
|
109
|
+
k++;
|
|
110
|
+
if (k < outLen && /[}\]]/.test(result[k])) {
|
|
111
|
+
// Drop the comma; jump to the bracket
|
|
112
|
+
j = k;
|
|
113
|
+
continue;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
out += ch;
|
|
117
|
+
j++;
|
|
71
118
|
}
|
|
72
|
-
return
|
|
119
|
+
return out;
|
|
73
120
|
}
|
|
74
121
|
// ---------------------------------------------------------------------------
|
|
75
122
|
// Config path resolution
|
|
@@ -86,7 +133,8 @@ export function resolveGlobalConfigPath(fs, opts = {}) {
|
|
|
86
133
|
if (opts.ensureDir && !fs.existsSync(dir)) {
|
|
87
134
|
fs.mkdirSync(dir, { recursive: true });
|
|
88
135
|
}
|
|
89
|
-
|
|
136
|
+
const filename = opts.filename ?? SERVER_CONFIG_FILENAME;
|
|
137
|
+
return path.join(dir, filename);
|
|
90
138
|
}
|
|
91
139
|
function resolveConfigDir() {
|
|
92
140
|
if (process.env.OPENCODE_CONFIG_DIR) {
|
|
@@ -110,12 +158,12 @@ function resolveConfigDir() {
|
|
|
110
158
|
// JSONC-safe config load
|
|
111
159
|
// ---------------------------------------------------------------------------
|
|
112
160
|
/**
|
|
113
|
-
* Load and parse
|
|
161
|
+
* Load and parse a global OpenCode config file.
|
|
114
162
|
* Treats a missing file as an empty config object (not an error).
|
|
115
163
|
* Surfaces parse errors so callers can exit non-zero without corrupting data.
|
|
116
164
|
*/
|
|
117
|
-
export function loadGlobalConfig(fs) {
|
|
118
|
-
const configPath = resolveGlobalConfigPath(fs);
|
|
165
|
+
export function loadGlobalConfig(fs, opts = {}) {
|
|
166
|
+
const configPath = resolveGlobalConfigPath(fs, opts.filename ? { filename: opts.filename } : {});
|
|
119
167
|
if (!fs.existsSync(configPath)) {
|
|
120
168
|
return { path: configPath, existed: false, config: {} };
|
|
121
169
|
}
|
|
@@ -186,19 +234,17 @@ export function addPlugin(config, specifier) {
|
|
|
186
234
|
/**
|
|
187
235
|
* Write a timestamped backup of the given config path and rotate so at most
|
|
188
236
|
* 3 backups are retained (oldest deleted first).
|
|
189
|
-
* Returns the path of the newly created backup.
|
|
237
|
+
* Returns the path of the newly created backup, or null if no backup was created.
|
|
190
238
|
*/
|
|
191
239
|
export function rotateBackups(fs, configPath, _pluginName) {
|
|
240
|
+
if (!fs.existsSync(configPath)) {
|
|
241
|
+
return null;
|
|
242
|
+
}
|
|
192
243
|
const dir = path.dirname(configPath);
|
|
193
244
|
const base = path.basename(configPath);
|
|
194
245
|
const timestamp = Date.now();
|
|
195
246
|
const newBak = path.join(dir, `${base}.bak.${timestamp}`);
|
|
196
|
-
|
|
197
|
-
fs.copyFileSync(configPath, newBak);
|
|
198
|
-
}
|
|
199
|
-
else {
|
|
200
|
-
fs.writeFileSync(newBak, '', 'utf-8');
|
|
201
|
-
}
|
|
247
|
+
fs.copyFileSync(configPath, newBak);
|
|
202
248
|
// Collect existing backups sorted by timestamp (lexical sort on numeric suffix)
|
|
203
249
|
const entries = fs.readdirSync(dir);
|
|
204
250
|
const bakFiles = entries
|
|
@@ -221,7 +267,7 @@ export function rotateBackups(fs, configPath, _pluginName) {
|
|
|
221
267
|
export function writeAtomically(fs, configPath, content) {
|
|
222
268
|
const dir = path.dirname(configPath);
|
|
223
269
|
const base = path.basename(configPath);
|
|
224
|
-
const tmpPath = path.join(dir, `.${base}.tmp.${Date.now()}`);
|
|
270
|
+
const tmpPath = path.join(dir, `.${base}.tmp.${Date.now()}-${process.pid}`);
|
|
225
271
|
fs.writeFileSync(tmpPath, content, 'utf-8');
|
|
226
272
|
fs.renameSync(tmpPath, configPath);
|
|
227
273
|
}
|