runlist 0.0.0 → 0.94.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/LICENSE +21 -0
- package/README.md +1112 -6
- package/assets/app/app.css +283 -0
- package/assets/app/app.mjs +381 -0
- package/assets/app/block-editor.mjs +182 -0
- package/assets/app/block-model.mjs +52 -0
- package/assets/app/document-create.mjs +86 -0
- package/assets/app/document-lifecycle.mjs +36 -0
- package/assets/app/document-stages.mjs +22 -0
- package/assets/app/document-views.mjs +33 -0
- package/assets/app/document-yardstick.mjs +258 -0
- package/assets/app/editor-navigation.mjs +51 -0
- package/assets/app/filing-navigation.mjs +138 -0
- package/assets/app/git-changes.mjs +80 -0
- package/assets/app/git-commit.mjs +46 -0
- package/assets/app/index.html +70 -0
- package/assets/app/library-navigation.mjs +93 -0
- package/assets/app/outline.mjs +18 -0
- package/assets/app/quick-navigation.mjs +52 -0
- package/assets/app/record-navigation.mjs +78 -0
- package/assets/app/recovery-center.mjs +53 -0
- package/assets/app/semantic-search.mjs +14 -0
- package/assets/app/settings.mjs +15 -0
- package/assets/app/shared.mjs +104 -0
- package/assets/app/stage-source.mjs +75 -0
- package/assets/app/template-editor.mjs +47 -0
- package/assets/app/theme.mjs +25 -0
- package/assets/app/transport.mjs +43 -0
- package/assets/contracts/source-editing-v1.md +232 -0
- package/assets/opencode/plugin.js +162 -0
- package/assets/schemas/runlist-record-v1.schema.json +912 -0
- package/bin/dotmd.mjs +5 -0
- package/bin/runlist.mjs +2714 -0
- package/package.json +53 -6
- package/plugins/runlist-codex/.codex-plugin/plugin.json +18 -0
- package/plugins/runlist-codex/bin/runlist-hook +86 -0
- package/plugins/runlist-codex/hooks/hooks.json +16 -0
- package/plugins/runlist-codex/skills/runlist/SKILL.md +25 -0
- package/runlist.config.example.mjs +370 -0
- package/scripts/postinstall.mjs +56 -0
- package/src/agent-context.mjs +201 -0
- package/src/ai.mjs +30 -0
- package/src/app-create.mjs +169 -0
- package/src/app-filing.mjs +139 -0
- package/src/app-git-commits.mjs +48 -0
- package/src/app-git.mjs +180 -0
- package/src/app-library.mjs +152 -0
- package/src/app-lifecycle-worker.mjs +12 -0
- package/src/app-lifecycle.mjs +79 -0
- package/src/app-records.mjs +134 -0
- package/src/app-search.mjs +80 -0
- package/src/app-semantic-search.mjs +105 -0
- package/src/app-service.mjs +208 -0
- package/src/app-yardstick.mjs +269 -0
- package/src/app.mjs +102 -0
- package/src/atomic-mutation.mjs +1736 -0
- package/src/baton.mjs +435 -0
- package/src/body-link.mjs +48 -0
- package/src/bulk-tag.mjs +174 -0
- package/src/check-collapse.mjs +75 -0
- package/src/claude-commands.mjs +75 -0
- package/src/cli-editor.mjs +8 -0
- package/src/code-refs.mjs +545 -0
- package/src/codex-integration.mjs +81 -0
- package/src/color.mjs +14 -0
- package/src/commands.mjs +374 -0
- package/src/completions.mjs +93 -0
- package/src/config-edit.mjs +606 -0
- package/src/config.mjs +705 -0
- package/src/decision-action.mjs +58 -0
- package/src/decision.mjs +220 -0
- package/src/decisions.mjs +787 -0
- package/src/deps.mjs +317 -0
- package/src/desktop-install.mjs +158 -0
- package/src/desktop-runtime.mjs +4 -0
- package/src/desktop.mjs +25 -0
- package/src/diff.mjs +101 -0
- package/src/doctor.mjs +591 -0
- package/src/durable-rename.mjs +55 -0
- package/src/errors-read.mjs +44 -0
- package/src/export.mjs +496 -0
- package/src/extractors.mjs +101 -0
- package/src/filing.mjs +255 -0
- package/src/fix-membership.mjs +134 -0
- package/src/fix-refs.mjs +174 -0
- package/src/flags.mjs +318 -0
- package/src/frontmatter-fix.mjs +195 -0
- package/src/frontmatter.mjs +252 -0
- package/src/git-commit-filters.mjs +32 -0
- package/src/git-commit-protocol.mjs +198 -0
- package/src/git-commit-signing.mjs +75 -0
- package/src/git-commit-store.mjs +26 -0
- package/src/git-commit-support.mjs +16 -0
- package/src/git-commit-worker.mjs +32 -0
- package/src/git.mjs +909 -0
- package/src/glossary-check.mjs +35 -0
- package/src/glossary.mjs +243 -0
- package/src/graph.mjs +298 -0
- package/src/guard.mjs +488 -0
- package/src/health.mjs +212 -0
- package/src/hints.mjs +129 -0
- package/src/host-integration.mjs +339 -0
- package/src/hub-membership.mjs +284 -0
- package/src/hub.mjs +296 -0
- package/src/hud.mjs +436 -0
- package/src/index-file.mjs +163 -0
- package/src/index.mjs +478 -0
- package/src/init.mjs +512 -0
- package/src/install.mjs +222 -0
- package/src/journal-read.mjs +116 -0
- package/src/journal.mjs +472 -0
- package/src/lifecycle.mjs +1444 -0
- package/src/lint.mjs +386 -0
- package/src/managed-path.mjs +192 -0
- package/src/markdown-code-spans.mjs +58 -0
- package/src/migrate-prompts.mjs +171 -0
- package/src/migrate-template.mjs +170 -0
- package/src/migrate.mjs +90 -0
- package/src/misuse-read.mjs +61 -0
- package/src/model-request.mjs +33 -0
- package/src/model.mjs +523 -0
- package/src/modules.mjs +355 -0
- package/src/naming.mjs +55 -0
- package/src/native-action.mjs +59 -0
- package/src/native-record.mjs +244 -0
- package/src/new.mjs +1321 -0
- package/src/output-identity.mjs +111 -0
- package/src/parent-plan.mjs +58 -0
- package/src/parse-cache.mjs +99 -0
- package/src/path-containment.mjs +15 -0
- package/src/pickup-card.mjs +293 -0
- package/src/pickup.mjs +671 -0
- package/src/prompt.mjs +25 -0
- package/src/prompts.mjs +585 -0
- package/src/public-inspection.mjs +19 -0
- package/src/query.mjs +977 -0
- package/src/record-action.mjs +11 -0
- package/src/record-create.mjs +82 -0
- package/src/reference-planner.mjs +388 -0
- package/src/reference-values.mjs +11 -0
- package/src/rename.mjs +107 -0
- package/src/render.mjs +678 -0
- package/src/roadmap.mjs +184 -0
- package/src/runlist.mjs +944 -0
- package/src/section.mjs +225 -0
- package/src/semantic-search-client.mjs +58 -0
- package/src/ship.mjs +166 -0
- package/src/show.mjs +101 -0
- package/src/skill-drift.mjs +98 -0
- package/src/source-editor.mjs +491 -0
- package/src/stages.mjs +73 -0
- package/src/state-migration.mjs +123 -0
- package/src/stats.mjs +200 -0
- package/src/status-metadata.mjs +87 -0
- package/src/statuses.mjs +721 -0
- package/src/summary.mjs +74 -0
- package/src/surfaces.mjs +28 -0
- package/src/sync-status.mjs +316 -0
- package/src/template-store.mjs +62 -0
- package/src/update.mjs +338 -0
- package/src/use.mjs +58 -0
- package/src/util.mjs +337 -0
- package/src/validate.mjs +795 -0
- package/src/watch.mjs +56 -0
- package/src/xref.mjs +406 -0
- package/src/yardstick-command.mjs +178 -0
- package/src/yardstick.mjs +112 -0
|
@@ -0,0 +1,545 @@
|
|
|
1
|
+
// Reference repair outside the doc roots.
|
|
2
|
+
//
|
|
3
|
+
// Every sweep the lifecycle runs is pushed through `authorizeManagedSweep` →
|
|
4
|
+
// `authorizeManagedSource`, which refuses anything that is not a `.md` file
|
|
5
|
+
// inside a configured docs root. That chokepoint is what makes a move's write
|
|
6
|
+
// set provable, so it is deliberately NOT widened here: this module walks the
|
|
7
|
+
// configured code roots on its own, outside the move transaction, and archive
|
|
8
|
+
// and rename call it after their own doc-root repair has committed.
|
|
9
|
+
//
|
|
10
|
+
// Two rules separate it from the document rewriter:
|
|
11
|
+
// 1. It matches the full repo-relative path only, never a bare basename. A
|
|
12
|
+
// corpus cites slugs as words in prose comments, so a basename match is
|
|
13
|
+
// how a rename corrupts an unrelated line.
|
|
14
|
+
// 2. It replaces the path segment and nothing else, so a `§`/`#` anchor, a
|
|
15
|
+
// closing backtick, a quote or a trailing comma all survive untouched.
|
|
16
|
+
// That also means the rendering is repo-relative, which is the spelling a
|
|
17
|
+
// reader of a source comment can paste; the doc rewriter's doc-relative
|
|
18
|
+
// rendering is not reused.
|
|
19
|
+
|
|
20
|
+
import { accessSync, constants, readFileSync, readdirSync, realpathSync, statSync, writeFileSync } from 'node:fs';
|
|
21
|
+
import path from 'node:path';
|
|
22
|
+
import { escapeRegex, toRepoPath } from './util.mjs';
|
|
23
|
+
import { collectDocFiles } from './index.mjs';
|
|
24
|
+
import { bold, dim, green, yellow } from './color.mjs';
|
|
25
|
+
|
|
26
|
+
// Supported source kinds include TypeScript, Swift, SQL, ESM scripts, TSX,
|
|
27
|
+
// shell, Python, GraphQL and Dockerfiles.
|
|
28
|
+
// An entry without a leading dot is an exact basename, which is how a file
|
|
29
|
+
// with no extension opts in.
|
|
30
|
+
export const DEFAULT_CODE_EXTENSIONS = Object.freeze([
|
|
31
|
+
'.ts', '.tsx', '.js', '.jsx', '.mjs', '.cjs',
|
|
32
|
+
'.swift', '.kt', '.sql', '.py', '.sh', '.rb', '.go', '.rs',
|
|
33
|
+
'.graphql', '.gql', '.yml', '.yaml', '.css', '.scss', '.toml',
|
|
34
|
+
'Dockerfile', 'Justfile', 'Makefile',
|
|
35
|
+
]);
|
|
36
|
+
|
|
37
|
+
// Generated output is excluded rather than reported: rewriting an artifact is
|
|
38
|
+
// undone by the next codegen run, and the citation lives in the schema
|
|
39
|
+
// description upstream.
|
|
40
|
+
export const DEFAULT_CODE_EXCLUDES = Object.freeze([
|
|
41
|
+
'**/node_modules/**',
|
|
42
|
+
'**/.git/**',
|
|
43
|
+
'**/dist/**',
|
|
44
|
+
'**/build/**',
|
|
45
|
+
'**/.next/**',
|
|
46
|
+
'**/*.generated.*',
|
|
47
|
+
'**/__generated__/**',
|
|
48
|
+
'**/generated/**',
|
|
49
|
+
]);
|
|
50
|
+
|
|
51
|
+
// Comment openers by file kind. The classifier only asks whether an opener
|
|
52
|
+
// precedes the match on the same line, so a table this small is enough — and
|
|
53
|
+
// getting it wrong is safe in one direction only, which is why `#` is not
|
|
54
|
+
// handed to every language.
|
|
55
|
+
const COMMENT_OPENERS = {
|
|
56
|
+
c: ['//', '/*', '*'],
|
|
57
|
+
hash: ['#'],
|
|
58
|
+
sql: ['--', '/*', '*'],
|
|
59
|
+
all: ['//', '/*', '*', '#', '--'],
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
const OPENERS_BY_EXTENSION = new Map([
|
|
63
|
+
['.ts', 'c'], ['.tsx', 'c'], ['.js', 'c'], ['.jsx', 'c'], ['.mjs', 'c'], ['.cjs', 'c'],
|
|
64
|
+
['.swift', 'c'], ['.kt', 'c'], ['.css', 'c'], ['.scss', 'c'], ['.go', 'c'], ['.rs', 'c'],
|
|
65
|
+
['.sql', 'sql'],
|
|
66
|
+
['.py', 'hash'], ['.sh', 'hash'], ['.rb', 'hash'], ['.yml', 'hash'], ['.yaml', 'hash'],
|
|
67
|
+
['.toml', 'hash'], ['.graphql', 'hash'], ['.gql', 'hash'],
|
|
68
|
+
]);
|
|
69
|
+
|
|
70
|
+
export function codeRefsConfig(config) {
|
|
71
|
+
const roots = config?.codeRefs?.roots ?? [];
|
|
72
|
+
return {
|
|
73
|
+
roots,
|
|
74
|
+
extensions: config?.codeRefs?.extensions ?? [...DEFAULT_CODE_EXTENSIONS],
|
|
75
|
+
excludes: config?.codeRefs?.excludes ?? [...DEFAULT_CODE_EXCLUDES],
|
|
76
|
+
untouched: new Set(config?.codeRefs?.untouched ?? []),
|
|
77
|
+
enabled: roots.length > 0,
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// ── Globs ────────────────────────────────────────────────────────────────
|
|
82
|
+
|
|
83
|
+
// A deliberately small subset: `**` spans segments, `*` stays inside one, `?`
|
|
84
|
+
// is one character. Enough for the exclude shapes a code root needs, and it
|
|
85
|
+
// refuses to grow into a dependency.
|
|
86
|
+
export function globToRegex(pattern) {
|
|
87
|
+
let out = '^';
|
|
88
|
+
for (let i = 0; i < pattern.length; i++) {
|
|
89
|
+
const ch = pattern[i];
|
|
90
|
+
if (ch === '*') {
|
|
91
|
+
if (pattern[i + 1] === '*') {
|
|
92
|
+
i++;
|
|
93
|
+
if (pattern[i + 1] === '/') { i++; out += '(?:.*/)?'; }
|
|
94
|
+
else out += '.*';
|
|
95
|
+
} else {
|
|
96
|
+
out += '[^/]*';
|
|
97
|
+
}
|
|
98
|
+
} else if (ch === '?') {
|
|
99
|
+
out += '[^/]';
|
|
100
|
+
} else {
|
|
101
|
+
out += escapeRegex(ch);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
return new RegExp(out + '$');
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export function matchesAnyGlob(repoPath, patterns) {
|
|
108
|
+
return patterns.some(pattern => globToRegex(pattern).test(repoPath));
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// ── Walking the code roots ───────────────────────────────────────────────
|
|
112
|
+
|
|
113
|
+
function extensionAllowed(basename, extensions) {
|
|
114
|
+
const ext = path.extname(basename);
|
|
115
|
+
if (ext && extensions.includes(ext)) return true;
|
|
116
|
+
return extensions.includes(basename);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
export function collectCodeFiles(config) {
|
|
120
|
+
const settings = codeRefsConfig(config);
|
|
121
|
+
if (!settings.enabled) return [];
|
|
122
|
+
const files = [];
|
|
123
|
+
const seen = new Set();
|
|
124
|
+
|
|
125
|
+
const walk = (dir) => {
|
|
126
|
+
let entries;
|
|
127
|
+
try { entries = readdirSync(dir, { withFileTypes: true }); } catch { return; }
|
|
128
|
+
for (const entry of entries) {
|
|
129
|
+
const abs = path.join(dir, entry.name);
|
|
130
|
+
const repoPath = toRepoPath(abs, config.repoRoot);
|
|
131
|
+
if (matchesAnyGlob(repoPath, settings.excludes)) continue;
|
|
132
|
+
if (entry.isSymbolicLink()) continue;
|
|
133
|
+
if (entry.isDirectory()) { walk(abs); continue; }
|
|
134
|
+
if (!entry.isFile()) continue;
|
|
135
|
+
if (!extensionAllowed(entry.name, settings.extensions)) continue;
|
|
136
|
+
if (seen.has(abs)) continue;
|
|
137
|
+
seen.add(abs);
|
|
138
|
+
files.push(abs);
|
|
139
|
+
}
|
|
140
|
+
};
|
|
141
|
+
|
|
142
|
+
for (const root of settings.roots) {
|
|
143
|
+
const abs = path.resolve(config.repoRoot, root);
|
|
144
|
+
let stat;
|
|
145
|
+
try { stat = statSync(abs); } catch { continue; }
|
|
146
|
+
if (!stat.isDirectory()) continue;
|
|
147
|
+
walk(abs);
|
|
148
|
+
}
|
|
149
|
+
return files.sort((a, b) => a.localeCompare(b));
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// ── The matcher ──────────────────────────────────────────────────────────
|
|
153
|
+
|
|
154
|
+
// The path may not continue in either direction: a preceding path character
|
|
155
|
+
// means this is a longer path that merely ends with ours, and a following one
|
|
156
|
+
// means the citation names something below it. Everything else — a quote, a
|
|
157
|
+
// backtick, a bracket, a space, a line start, a trailing `§` or `#` anchor —
|
|
158
|
+
// is a boundary, and is left exactly as it was found.
|
|
159
|
+
export function referenceRegex(repoPath) {
|
|
160
|
+
return new RegExp(`(?<![A-Za-z0-9_\\-/.])${escapeRegex(repoPath)}(?![A-Za-z0-9_\\-/])`, 'g');
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
const ANCHOR = /^\s*(?:§|#[A-Za-z0-9])/;
|
|
164
|
+
|
|
165
|
+
function commentOpenerIndex(line, kind) {
|
|
166
|
+
const openers = COMMENT_OPENERS[kind] ?? COMMENT_OPENERS.all;
|
|
167
|
+
let best = -1;
|
|
168
|
+
for (const opener of openers) {
|
|
169
|
+
let from = 0;
|
|
170
|
+
for (;;) {
|
|
171
|
+
const at = line.indexOf(opener, from);
|
|
172
|
+
if (at === -1) break;
|
|
173
|
+
from = at + 1;
|
|
174
|
+
// `https://` is not a comment. A lone `*` only opens a continuation
|
|
175
|
+
// line when it is the first thing on the line.
|
|
176
|
+
if (opener === '//' && line[at - 1] === ':') continue;
|
|
177
|
+
if (opener === '*' && line.slice(0, at).trim() !== '') continue;
|
|
178
|
+
if (best === -1 || at < best) best = at;
|
|
179
|
+
break;
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
return best;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
function insideQuotes(line, index) {
|
|
186
|
+
let quote = null;
|
|
187
|
+
for (let i = 0; i < index; i++) {
|
|
188
|
+
const ch = line[i];
|
|
189
|
+
if (ch === '\\') { i++; continue; }
|
|
190
|
+
if (quote) { if (ch === quote) quote = null; continue; }
|
|
191
|
+
if (ch === '"' || ch === "'" || ch === '`') quote = ch;
|
|
192
|
+
}
|
|
193
|
+
return quote;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// `comment` is written by `--fix`; `string` and `code` wait for `--strings`,
|
|
197
|
+
// because a path inside a quoted string can be data a test or a guard asserts
|
|
198
|
+
// on rather than prose a reader follows.
|
|
199
|
+
export function classifyMatch(line, index, fileKind) {
|
|
200
|
+
const opener = commentOpenerIndex(line, fileKind);
|
|
201
|
+
if (opener !== -1 && opener < index) return 'comment';
|
|
202
|
+
return insideQuotes(line, index) ? 'string' : 'code';
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
function fileKindFor(filePath) {
|
|
206
|
+
return OPENERS_BY_EXTENSION.get(path.extname(filePath)) ?? 'all';
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
export function findCodeReferences(content, repoPath, filePath) {
|
|
210
|
+
const kind = fileKindFor(filePath);
|
|
211
|
+
const lines = content.split('\n');
|
|
212
|
+
const hits = [];
|
|
213
|
+
for (let n = 0; n < lines.length; n++) {
|
|
214
|
+
const line = lines[n];
|
|
215
|
+
const regex = referenceRegex(repoPath);
|
|
216
|
+
let match;
|
|
217
|
+
while ((match = regex.exec(line)) !== null) {
|
|
218
|
+
hits.push({
|
|
219
|
+
line: n + 1,
|
|
220
|
+
column: match.index + 1,
|
|
221
|
+
text: line.trim(),
|
|
222
|
+
form: classifyMatch(line, match.index, kind),
|
|
223
|
+
anchored: ANCHOR.test(line.slice(match.index + repoPath.length)),
|
|
224
|
+
});
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
return hits;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
export function rewriteCodeReferences(content, oldRepoPath, newRepoPath, filePath, { strings = false } = {}) {
|
|
231
|
+
const kind = fileKindFor(filePath);
|
|
232
|
+
const lines = content.split('\n');
|
|
233
|
+
let changed = 0;
|
|
234
|
+
const out = lines.map(line => {
|
|
235
|
+
const regex = referenceRegex(oldRepoPath);
|
|
236
|
+
return line.replace(regex, (found, offset) => {
|
|
237
|
+
const form = classifyMatch(line, offset, kind);
|
|
238
|
+
if (form !== 'comment' && !strings) return found;
|
|
239
|
+
changed++;
|
|
240
|
+
return newRepoPath;
|
|
241
|
+
});
|
|
242
|
+
});
|
|
243
|
+
return { content: out.join('\n'), changed };
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
// ── Scanning ─────────────────────────────────────────────────────────────
|
|
247
|
+
|
|
248
|
+
function writability(absPath, repoRoot) {
|
|
249
|
+
let canonical;
|
|
250
|
+
try { canonical = realpathSync(absPath); } catch (err) { return `cannot be resolved (${err.code ?? err.message})`; }
|
|
251
|
+
const repoCanonical = realpathSync(repoRoot);
|
|
252
|
+
const relative = path.relative(repoCanonical, canonical);
|
|
253
|
+
if (relative.startsWith('..') || path.isAbsolute(relative)) return 'resolves outside the repository';
|
|
254
|
+
try { accessSync(canonical, constants.W_OK); } catch { return 'is not writable'; }
|
|
255
|
+
return null;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
/**
|
|
259
|
+
* Report every code-root citation of `oldRepoPath`. Pure read: `runlist refs`
|
|
260
|
+
* and the archive / rename count line share it, the same way `fixBrokenRefs`
|
|
261
|
+
* is shared by `fix-refs` and `check --fix`.
|
|
262
|
+
*/
|
|
263
|
+
export function scanCodeRefs(config, oldRepoPath, { files = null } = {}) {
|
|
264
|
+
const settings = codeRefsConfig(config);
|
|
265
|
+
const result = emptyScan(settings.enabled);
|
|
266
|
+
if (!settings.enabled) return result;
|
|
267
|
+
|
|
268
|
+
for (const absPath of files ?? collectCodeFiles(config)) {
|
|
269
|
+
let content;
|
|
270
|
+
try { content = readFileSync(absPath, 'utf8'); } catch { continue; }
|
|
271
|
+
if (!content.includes(oldRepoPath)) continue;
|
|
272
|
+
const repoPath = toRepoPath(absPath, config.repoRoot);
|
|
273
|
+
const hits = findCodeReferences(content, oldRepoPath, absPath);
|
|
274
|
+
if (!hits.length) continue;
|
|
275
|
+
const untouched = settings.untouched.has(repoPath);
|
|
276
|
+
result.files.push({ path: repoPath, absPath, untouched, hits });
|
|
277
|
+
result.total += hits.length;
|
|
278
|
+
if (untouched) result.untouchedCount += hits.length;
|
|
279
|
+
for (const hit of hits) result.byForm[hit.form]++;
|
|
280
|
+
}
|
|
281
|
+
return result;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
// Any document-shaped path, so a sweep reads each file once instead of once
|
|
285
|
+
// per moved document. The captured token is looked up in the work list,
|
|
286
|
+
// avoiding a separate file read for each archived document while keeping the rule
|
|
287
|
+
// identical to the single-document scan.
|
|
288
|
+
const ANY_DOC_PATH = /(?<![A-Za-z0-9_\-/.])([A-Za-z0-9_\-./]+\.md)(?![A-Za-z0-9_\-/])/g;
|
|
289
|
+
|
|
290
|
+
function emptyScan(enabled) {
|
|
291
|
+
return { enabled, files: [], total: 0, byForm: { comment: 0, string: 0, code: 0 }, untouchedCount: 0, refused: [] };
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
/**
|
|
295
|
+
* One pass over the code roots for many document paths at once. Returns a Map
|
|
296
|
+
* of path → the same scan shape `scanCodeRefs` returns, for the paths that
|
|
297
|
+
* were actually cited.
|
|
298
|
+
*/
|
|
299
|
+
export function scanManyCodeRefs(config, wanted, { files = null } = {}) {
|
|
300
|
+
const settings = codeRefsConfig(config);
|
|
301
|
+
const byPath = new Map();
|
|
302
|
+
if (!settings.enabled || wanted.size === 0) return byPath;
|
|
303
|
+
|
|
304
|
+
for (const absPath of files ?? collectCodeFiles(config)) {
|
|
305
|
+
let content;
|
|
306
|
+
try { content = readFileSync(absPath, 'utf8'); } catch { continue; }
|
|
307
|
+
if (!content.includes('.md')) continue;
|
|
308
|
+
const repoPath = toRepoPath(absPath, config.repoRoot);
|
|
309
|
+
const untouched = settings.untouched.has(repoPath);
|
|
310
|
+
const kind = fileKindFor(absPath);
|
|
311
|
+
const lines = content.split('\n');
|
|
312
|
+
const perPath = new Map();
|
|
313
|
+
|
|
314
|
+
for (let n = 0; n < lines.length; n++) {
|
|
315
|
+
const line = lines[n];
|
|
316
|
+
if (!line.includes('.md')) continue;
|
|
317
|
+
ANY_DOC_PATH.lastIndex = 0;
|
|
318
|
+
let match;
|
|
319
|
+
while ((match = ANY_DOC_PATH.exec(line)) !== null) {
|
|
320
|
+
const found = match[1];
|
|
321
|
+
if (!wanted.has(found)) continue;
|
|
322
|
+
if (!perPath.has(found)) perPath.set(found, []);
|
|
323
|
+
perPath.get(found).push({
|
|
324
|
+
line: n + 1,
|
|
325
|
+
column: match.index + 1,
|
|
326
|
+
text: line.trim(),
|
|
327
|
+
form: classifyMatch(line, match.index, kind),
|
|
328
|
+
anchored: ANCHOR.test(line.slice(match.index + found.length)),
|
|
329
|
+
});
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
for (const [found, hits] of perPath) {
|
|
334
|
+
if (!byPath.has(found)) byPath.set(found, emptyScan(true));
|
|
335
|
+
const scan = byPath.get(found);
|
|
336
|
+
scan.files.push({ path: repoPath, absPath, untouched, hits });
|
|
337
|
+
scan.total += hits.length;
|
|
338
|
+
if (untouched) scan.untouchedCount += hits.length;
|
|
339
|
+
for (const hit of hits) scan.byForm[hit.form]++;
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
return byPath;
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
/** Writable citations only: what `--fix` (or `--fix --strings`) would change. */
|
|
346
|
+
export function fixableCount(scan, { strings = false } = {}) {
|
|
347
|
+
let count = 0;
|
|
348
|
+
for (const file of scan.files) {
|
|
349
|
+
if (file.untouched) continue;
|
|
350
|
+
for (const hit of file.hits) {
|
|
351
|
+
if (hit.form === 'comment' || strings) count++;
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
return count;
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
export function applyCodeRefs(config, scan, oldRepoPath, newRepoPath, { strings = false, dryRun = false } = {}) {
|
|
358
|
+
const written = [];
|
|
359
|
+
const refused = [];
|
|
360
|
+
let changed = 0;
|
|
361
|
+
for (const file of scan.files) {
|
|
362
|
+
if (file.untouched) continue;
|
|
363
|
+
const reason = writability(file.absPath, config.repoRoot);
|
|
364
|
+
if (reason) { refused.push({ path: file.path, reason }); continue; }
|
|
365
|
+
const content = readFileSync(file.absPath, 'utf8');
|
|
366
|
+
const result = rewriteCodeReferences(content, oldRepoPath, newRepoPath, file.absPath, { strings });
|
|
367
|
+
if (!result.changed || result.content === content) continue;
|
|
368
|
+
if (!dryRun) writeFileSync(file.absPath, result.content, 'utf8');
|
|
369
|
+
written.push({ path: file.path, changed: result.changed });
|
|
370
|
+
changed += result.changed;
|
|
371
|
+
}
|
|
372
|
+
return { written, refused, changed };
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
// ── The work list for `refs repair` ──────────────────────────────────────
|
|
376
|
+
|
|
377
|
+
// Nothing in the tool records where a document used to live: a rename moves
|
|
378
|
+
// the file and rewrites the doc-root references, and neither writes the old
|
|
379
|
+
// path anywhere. The one move that leaves a readable trace is the archive,
|
|
380
|
+
// whose destination is the source path with the archive directory inserted,
|
|
381
|
+
// so the previous path is that segment removed. A hand-moved or renamed
|
|
382
|
+
// document is out of reach here and `refs <old> <new>` covers it by hand.
|
|
383
|
+
export function archivedPreviousPaths(config) {
|
|
384
|
+
const docs = collectDocFiles(config).map(file => toRepoPath(file, config.repoRoot));
|
|
385
|
+
const live = new Set(docs);
|
|
386
|
+
const pairs = [];
|
|
387
|
+
for (const current of docs) {
|
|
388
|
+
const segments = current.split('/');
|
|
389
|
+
const at = segments.lastIndexOf(config.archiveDir);
|
|
390
|
+
if (at === -1) continue;
|
|
391
|
+
const previous = [...segments.slice(0, at), ...segments.slice(at + 1)].join('/');
|
|
392
|
+
if (previous === current) continue;
|
|
393
|
+
// A live document at that path means the citation is not stale.
|
|
394
|
+
if (live.has(previous)) continue;
|
|
395
|
+
pairs.push({ previous, current });
|
|
396
|
+
}
|
|
397
|
+
return pairs;
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
// ── Output ───────────────────────────────────────────────────────────────
|
|
401
|
+
|
|
402
|
+
const FORM_LABELS = { comment: 'In a comment', string: 'In a string literal', code: 'In code' };
|
|
403
|
+
|
|
404
|
+
function writeGroup(out, label, entries, { prefix = '' } = {}) {
|
|
405
|
+
if (!entries.length) return;
|
|
406
|
+
const lines = entries.reduce((sum, entry) => sum + entry.hits.length, 0);
|
|
407
|
+
out.write(`${prefix}${bold(label)} (${lines} in ${entries.length} file${entries.length === 1 ? '' : 's'}):\n`);
|
|
408
|
+
for (const entry of entries) {
|
|
409
|
+
for (const hit of entry.hits) {
|
|
410
|
+
out.write(`${prefix} ${entry.path}:${hit.line} ${dim(hit.text)}\n`);
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
function groupBy(scan, form) {
|
|
416
|
+
return scan.files
|
|
417
|
+
.filter(file => !file.untouched)
|
|
418
|
+
.map(file => ({ path: file.path, hits: file.hits.filter(hit => hit.form === form) }))
|
|
419
|
+
.filter(file => file.hits.length);
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
export function reportScan(scan, oldRepoPath, newRepoPath, out, { prefix = '' } = {}) {
|
|
423
|
+
out.write(`${prefix}${oldRepoPath} → ${newRepoPath}\n\n`);
|
|
424
|
+
for (const form of ['comment', 'string', 'code']) {
|
|
425
|
+
writeGroup(out, FORM_LABELS[form], groupBy(scan, form), { prefix });
|
|
426
|
+
}
|
|
427
|
+
const untouched = scan.files.filter(file => file.untouched);
|
|
428
|
+
if (untouched.length) {
|
|
429
|
+
writeGroup(out, 'Never written (listed in codeRefsUntouched)', untouched, { prefix });
|
|
430
|
+
}
|
|
431
|
+
const anchored = scan.files.reduce((sum, file) => sum + file.hits.filter(hit => hit.anchored).length, 0);
|
|
432
|
+
if (anchored) out.write(`${prefix}${dim(`${anchored} carry a section anchor, which is kept as written.`)}\n`);
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
export function countLine(scan, oldRepoPath, newRepoPath) {
|
|
436
|
+
const files = scan.files.length;
|
|
437
|
+
return `${scan.total} code reference${scan.total === 1 ? '' : 's'} in ${files} file${files === 1 ? '' : 's'} still ${scan.total === 1 ? 'cites' : 'cite'} the old path; run \`runlist refs ${oldRepoPath} ${newRepoPath} --fix\``;
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
// ── The verb ─────────────────────────────────────────────────────────────
|
|
441
|
+
|
|
442
|
+
function requireConfigured(settings, out) {
|
|
443
|
+
if (settings.enabled) return true;
|
|
444
|
+
out.write(`${yellow('No code roots configured.')} Set \`codeRoots\` in runlist.config.mjs to scan source files for document citations.\n`);
|
|
445
|
+
return false;
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
export function runRefs(argv, config, opts = {}) {
|
|
449
|
+
const out = opts.out ?? process.stdout;
|
|
450
|
+
const fix = argv.includes('--fix');
|
|
451
|
+
const strings = argv.includes('--strings');
|
|
452
|
+
const dryRun = Boolean(opts.dryRun);
|
|
453
|
+
const positional = argv.filter(arg => !arg.startsWith('-'));
|
|
454
|
+
const settings = codeRefsConfig(config);
|
|
455
|
+
if (!requireConfigured(settings, out)) return { enabled: false };
|
|
456
|
+
|
|
457
|
+
if (positional[0] === 'repair') return repairAll(config, { fix, strings, dryRun, out });
|
|
458
|
+
|
|
459
|
+
const [oldInput, newInput] = positional;
|
|
460
|
+
if (!oldInput || !newInput) {
|
|
461
|
+
out.write('Usage: runlist refs <old> <new> [--fix] [--strings]\n runlist refs repair [--fix] [--strings]\n');
|
|
462
|
+
return { enabled: true };
|
|
463
|
+
}
|
|
464
|
+
const oldRepoPath = normalizeInput(oldInput, config);
|
|
465
|
+
const newRepoPath = normalizeInput(newInput, config);
|
|
466
|
+
return reportOne(config, oldRepoPath, newRepoPath, { fix, strings, dryRun, out, showEmpty: true });
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
function normalizeInput(input, config) {
|
|
470
|
+
const abs = path.resolve(config.repoRoot, input);
|
|
471
|
+
return toRepoPath(abs, config.repoRoot);
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
function reportOne(config, oldRepoPath, newRepoPath, { fix, strings, dryRun, out, showEmpty = false, files = null, scan: given = null }) {
|
|
475
|
+
const scan = given ?? scanCodeRefs(config, oldRepoPath, { files });
|
|
476
|
+
if (!scan.total) {
|
|
477
|
+
if (showEmpty) out.write(green(`No code references to ${oldRepoPath}.\n`));
|
|
478
|
+
return { enabled: true, scan, changed: 0 };
|
|
479
|
+
}
|
|
480
|
+
reportScan(scan, oldRepoPath, newRepoPath, out);
|
|
481
|
+
if (!fix) {
|
|
482
|
+
const writable = fixableCount(scan, { strings });
|
|
483
|
+
out.write(`\n${scan.total} reference${scan.total === 1 ? '' : 's'} in ${scan.files.length} file${scan.files.length === 1 ? '' : 's'}. `);
|
|
484
|
+
out.write(`${writable} would be rewritten by --fix${strings ? ' --strings' : ''}.\n`);
|
|
485
|
+
if (!strings && (scan.byForm.string || scan.byForm.code)) {
|
|
486
|
+
out.write(dim('Add --strings to rewrite the ones inside string literals and code.\n'));
|
|
487
|
+
}
|
|
488
|
+
return { enabled: true, scan, changed: 0 };
|
|
489
|
+
}
|
|
490
|
+
const applied = applyCodeRefs(config, scan, oldRepoPath, newRepoPath, { strings, dryRun });
|
|
491
|
+
const prefix = dryRun ? dim('[dry-run] ') : '';
|
|
492
|
+
out.write(`\n${prefix}${green(`Rewrote ${applied.changed} reference(s) in ${applied.written.length} file(s).`)}\n`);
|
|
493
|
+
for (const entry of applied.refused) {
|
|
494
|
+
out.write(`${yellow('Refused')}: ${entry.path} ${entry.reason}.\n`);
|
|
495
|
+
}
|
|
496
|
+
return { enabled: true, scan, changed: applied.changed, refused: applied.refused };
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
function repairAll(config, { fix, strings, dryRun, out }) {
|
|
500
|
+
const pairs = archivedPreviousPaths(config);
|
|
501
|
+
const wanted = new Map(pairs.map(pair => [pair.previous, pair.current]));
|
|
502
|
+
const found = scanManyCodeRefs(config, new Set(wanted.keys()));
|
|
503
|
+
let total = 0;
|
|
504
|
+
let changed = 0;
|
|
505
|
+
const stale = [];
|
|
506
|
+
for (const pair of pairs) {
|
|
507
|
+
const scan = found.get(pair.previous);
|
|
508
|
+
if (!scan?.total) continue;
|
|
509
|
+
stale.push({ ...pair, scan });
|
|
510
|
+
total += scan.total;
|
|
511
|
+
}
|
|
512
|
+
if (!stale.length) {
|
|
513
|
+
out.write(green(`No stale code references across ${pairs.length} archived document(s).\n`));
|
|
514
|
+
return { enabled: true, total: 0, changed: 0, documents: 0 };
|
|
515
|
+
}
|
|
516
|
+
out.write(`${bold(`${total} stale code reference(s) across ${stale.length} archived document(s).`)}\n\n`);
|
|
517
|
+
for (const entry of stale) {
|
|
518
|
+
const applied = reportOne(config, entry.previous, entry.current, { fix, strings, dryRun, out, scan: entry.scan });
|
|
519
|
+
changed += applied.changed ?? 0;
|
|
520
|
+
out.write('\n');
|
|
521
|
+
}
|
|
522
|
+
out.write(dim('Previous paths come from the archive directory mapping — a rename records none, so `refs <old> <new>` covers those by hand.\n'));
|
|
523
|
+
return { enabled: true, total, changed, documents: stale.length };
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
/**
|
|
527
|
+
* The line archive and rename print after their own doc-root repair. Returns
|
|
528
|
+
* null when no code root is configured, which is today's behaviour exactly.
|
|
529
|
+
*/
|
|
530
|
+
export function reportMovedCodeRefs(config, oldRepoPath, newRepoPath, { fix = false, strings = false, dryRun = false, out = process.stdout, prefix = '' } = {}) {
|
|
531
|
+
const settings = codeRefsConfig(config);
|
|
532
|
+
if (!settings.enabled) return null;
|
|
533
|
+
const scan = scanCodeRefs(config, oldRepoPath);
|
|
534
|
+
if (!scan.total) return { total: 0, changed: 0 };
|
|
535
|
+
if (!fix) {
|
|
536
|
+
out.write(`${prefix}${countLine(scan, oldRepoPath, newRepoPath)}\n`);
|
|
537
|
+
return { total: scan.total, changed: 0 };
|
|
538
|
+
}
|
|
539
|
+
const applied = applyCodeRefs(config, scan, oldRepoPath, newRepoPath, { strings, dryRun });
|
|
540
|
+
out.write(`${prefix}Rewrote ${applied.changed} code reference(s) in ${applied.written.length} file(s).\n`);
|
|
541
|
+
for (const entry of applied.refused) {
|
|
542
|
+
out.write(`${prefix}${yellow('Refused')}: ${entry.path} ${entry.reason}.\n`);
|
|
543
|
+
}
|
|
544
|
+
return { total: scan.total, changed: applied.changed, refused: applied.refused };
|
|
545
|
+
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
// Codex's default personal marketplace is discovered without a CLI registration.
|
|
2
|
+
// The npm package carries this plugin so installation does not need a Git checkout.
|
|
3
|
+
import { cpSync, existsSync, mkdirSync, mkdtempSync, readFileSync, renameSync, rmSync, writeFileSync } from 'node:fs';
|
|
4
|
+
import os from 'node:os';
|
|
5
|
+
import path from 'node:path';
|
|
6
|
+
import { fileURLToPath } from 'node:url';
|
|
7
|
+
|
|
8
|
+
const SOURCE = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..', 'plugins', 'runlist-codex');
|
|
9
|
+
const NAME = 'runlist-codex';
|
|
10
|
+
const MARKER = '.runlist-generated.json';
|
|
11
|
+
|
|
12
|
+
export function codexPaths(homedir = os.homedir()) {
|
|
13
|
+
const root = path.join(homedir, '.agents', 'plugins');
|
|
14
|
+
// The implicit personal marketplace file lives under ~/.agents/plugins,
|
|
15
|
+
// but Codex resolves its `./plugins/...` sources relative to the home dir.
|
|
16
|
+
return { root, marketplace: path.join(root, 'marketplace.json'), plugin: path.join(homedir, 'plugins', NAME) };
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function marketplaceAt(file) {
|
|
20
|
+
if (!existsSync(file)) return { name: 'personal', interface: { displayName: 'Personal' }, plugins: [] };
|
|
21
|
+
const data = JSON.parse(readFileSync(file, 'utf8'));
|
|
22
|
+
if (!data || typeof data.name !== 'string' || !Array.isArray(data.plugins)) {
|
|
23
|
+
throw new Error(`${file} is not a valid personal marketplace`);
|
|
24
|
+
}
|
|
25
|
+
return data;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function codexStatus({ version, homedir = os.homedir() } = {}) {
|
|
29
|
+
const paths = codexPaths(homedir);
|
|
30
|
+
let installed = null;
|
|
31
|
+
try { installed = JSON.parse(readFileSync(path.join(paths.plugin, MARKER), 'utf8')).version; } catch { /* absent or foreign */ }
|
|
32
|
+
const exists = existsSync(paths.plugin);
|
|
33
|
+
return { ...paths, exists, foreign: exists && !installed, version: installed, stale: Boolean(installed && version && installed !== version) };
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function installCodexPlugin({ version, homedir = os.homedir(), dryRun = false } = {}) {
|
|
37
|
+
const sourceVersion = JSON.parse(readFileSync(path.join(SOURCE, '.codex-plugin', 'plugin.json'), 'utf8')).version;
|
|
38
|
+
if (sourceVersion !== version) throw new Error(`Codex plugin source is ${sourceVersion}, CLI is ${version}`);
|
|
39
|
+
const status = codexStatus({ version, homedir });
|
|
40
|
+
if (status.foreign) return { ...status, action: 'refused', reason: 'plugin directory exists without a runlist marker' };
|
|
41
|
+
let market;
|
|
42
|
+
try { market = marketplaceAt(status.marketplace); }
|
|
43
|
+
catch (err) { return { ...status, action: 'refused', reason: err.message }; }
|
|
44
|
+
const expected = `./plugins/${NAME}`;
|
|
45
|
+
const entry = market.plugins.find(p => p.name === NAME);
|
|
46
|
+
if (entry && (entry.source?.source !== 'local' || entry.source?.path !== expected)) {
|
|
47
|
+
return { ...status, action: 'refused', reason: 'marketplace entry points to a different plugin source' };
|
|
48
|
+
}
|
|
49
|
+
if (!entry) market.plugins.push({
|
|
50
|
+
name: NAME, source: { source: 'local', path: expected },
|
|
51
|
+
policy: { installation: 'AVAILABLE', authentication: 'ON_INSTALL' }, category: 'Productivity',
|
|
52
|
+
});
|
|
53
|
+
if (status.exists && !status.stale && entry) return { ...status, action: 'current', marketplaceName: market.name };
|
|
54
|
+
if (dryRun) return { ...status, action: status.exists ? 'updated' : 'installed', marketplaceName: market.name, dryRun: true };
|
|
55
|
+
|
|
56
|
+
const parent = path.dirname(status.plugin);
|
|
57
|
+
mkdirSync(parent, { recursive: true });
|
|
58
|
+
mkdirSync(path.dirname(status.marketplace), { recursive: true });
|
|
59
|
+
const stage = mkdtempSync(path.join(parent, '.runlist-codex-stage-'));
|
|
60
|
+
const backup = `${stage}-previous`;
|
|
61
|
+
const marketStage = `${status.marketplace}.runlist-stage-${process.pid}`;
|
|
62
|
+
let movedOld = false;
|
|
63
|
+
let published = false;
|
|
64
|
+
try {
|
|
65
|
+
cpSync(SOURCE, stage, { recursive: true });
|
|
66
|
+
writeFileSync(path.join(stage, MARKER), JSON.stringify({ version }) + '\n');
|
|
67
|
+
writeFileSync(marketStage, JSON.stringify(market, null, 2) + '\n');
|
|
68
|
+
if (status.exists) { renameSync(status.plugin, backup); movedOld = true; }
|
|
69
|
+
renameSync(stage, status.plugin);
|
|
70
|
+
published = true;
|
|
71
|
+
renameSync(marketStage, status.marketplace);
|
|
72
|
+
if (movedOld) rmSync(backup, { recursive: true, force: true });
|
|
73
|
+
} catch (err) {
|
|
74
|
+
if (published) rmSync(status.plugin, { recursive: true, force: true });
|
|
75
|
+
if (movedOld) renameSync(backup, status.plugin);
|
|
76
|
+
rmSync(stage, { recursive: true, force: true });
|
|
77
|
+
rmSync(marketStage, { force: true });
|
|
78
|
+
throw err;
|
|
79
|
+
}
|
|
80
|
+
return { ...codexStatus({ version, homedir }), action: status.exists ? 'updated' : 'installed', marketplaceName: market.name };
|
|
81
|
+
}
|
package/src/color.mjs
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
const enabled = (process.env.FORCE_COLOR === '1')
|
|
2
|
+
|| (process.stdout.isTTY && !process.env.NO_COLOR);
|
|
3
|
+
|
|
4
|
+
const wrap = (code) => enabled ? (s) => `\x1b[${code}m${s}\x1b[0m` : (s) => s;
|
|
5
|
+
|
|
6
|
+
export const bold = wrap('1');
|
|
7
|
+
export const dim = wrap('2');
|
|
8
|
+
export const red = wrap('31');
|
|
9
|
+
export const green = wrap('32');
|
|
10
|
+
export const yellow = wrap('33');
|
|
11
|
+
export const blue = wrap('34');
|
|
12
|
+
export const magenta = wrap('35');
|
|
13
|
+
export const cyan = wrap('36');
|
|
14
|
+
export const brightYellow = wrap('93');
|