@litfamily/litclaude 1.0.21 → 1.0.22
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/CHANGELOG.md +12 -0
- package/README.md +34 -92
- package/README_ko-KR.md +42 -95
- package/bin/icon-font.mjs +264 -0
- package/bin/litclaude-ai.js +155 -18
- package/docs/assets/readme/badge-version.svg +1 -1
- package/docs/hooks.md +100 -5
- package/docs/privacy.md +2 -2
- package/package.json +13 -3
- package/plugins/litclaude/.claude-plugin/plugin.json +22 -2
- package/plugins/litclaude/bin/litclaude-hook.js +13 -7
- package/plugins/litclaude/bin/litclaude-hud.js +36 -19
- package/plugins/litclaude/bin/litclaude-mcp.js +1 -1
- package/plugins/litclaude/hooks/hooks.json +1 -0
- package/plugins/litclaude/hooks/lit-band-client.mjs +44 -0
- package/plugins/litclaude/hooks/lit-mod.mjs +269 -0
- package/plugins/litclaude/lib/auto-handoff.mjs +2 -0
- package/plugins/litclaude/lib/automatic-update-refresh.mjs +4 -0
- package/plugins/litclaude/lib/automatic-update.mjs +148 -1
- package/plugins/litclaude/lib/cache-measurement.mjs +62 -1
- package/plugins/litclaude/lib/canonical-skill-resources.mjs +9 -9
- package/plugins/litclaude/lib/hud-accent-palette.mjs +2 -2
- package/plugins/litclaude/lib/interface-probe-gate.mjs +2 -1
- package/plugins/litclaude/lib/jev-skill-hint.mjs +2 -0
- package/plugins/litclaude/lib/lit-plan-persistence.mjs +2 -1
- package/plugins/litclaude/lib/litgoal/autoloop.mjs +2 -0
- package/plugins/litclaude/lib/litgoal/cli.mjs +2 -0
- package/plugins/litclaude/lib/litgoal/ledger.mjs +3 -0
- package/plugins/litclaude/lib/litgoal/state.mjs +4 -0
- package/plugins/litclaude/lib/mod/frame.mjs +378 -0
- package/plugins/litclaude/lib/mod/layout.mjs +76 -0
- package/plugins/litclaude/lib/mod/motion.mjs +101 -0
- package/plugins/litclaude/lib/mod/render.mjs +24 -0
- package/plugins/litclaude/lib/mod/settings.mjs +57 -0
- package/plugins/litclaude/lib/mod/state.mjs +374 -0
- package/plugins/litclaude/lib/mod/timeline.mjs +124 -0
- package/plugins/litclaude/lib/mod/view.mjs +232 -0
- package/plugins/litclaude/lib/motion-render-gate.mjs +2 -1
- package/plugins/litclaude/lib/office-runtime.mjs +6 -0
- package/plugins/litclaude/lib/project-state-root.mjs +34 -2
- package/plugins/litclaude/lib/rules/engine.mjs +2 -1
- package/plugins/litclaude/lib/rules/format.mjs +24 -3
- package/plugins/litclaude/lib/rules/session-state.mjs +2 -1
- package/plugins/litclaude/lib/secure-path-read.mjs +46 -0
- package/plugins/litclaude/lib/start-work-lifecycle.mjs +4 -0
- package/plugins/litclaude/lib/wikify-knowledge.mjs +2 -1
- package/plugins/litclaude/scripts/scaffold-plan.mjs +2 -1
- package/plugins/litclaude/skills/lit-team/scripts/team.mjs +2 -1
- package/plugins/litclaude/skills/lit-typographic-motion/scripts/motion-runtime.mjs +7 -0
|
@@ -0,0 +1,264 @@
|
|
|
1
|
+
// Opt-in icon font for the LIT band: the symbols-only Nerd Font (Symbols Nerd Font Mono, MIT), so the
|
|
2
|
+
// band can draw its rounded pill caps (U+E0B6, U+E0B4) and icons while the person keeps their own
|
|
3
|
+
// terminal font. The font is not bundled: install downloads one pinned official release archive,
|
|
4
|
+
// refuses it unless its SHA-256 matches, takes the two pinned files out of it (each checked again),
|
|
5
|
+
// and writes them into the per-user font folder only. A manifest of what was written lets
|
|
6
|
+
// uninstall remove exactly those files and leave any copy the person installed alone.
|
|
7
|
+
|
|
8
|
+
import { spawnSync } from "node:child_process";
|
|
9
|
+
import { createHash } from "node:crypto";
|
|
10
|
+
import { linkSync, lstatSync, mkdirSync, readFileSync, renameSync, rmSync, unlinkSync, writeFileSync } from "node:fs";
|
|
11
|
+
import { dirname, join } from "node:path";
|
|
12
|
+
import { inflateRawSync } from "node:zlib";
|
|
13
|
+
|
|
14
|
+
export const ICON_FONT = Object.freeze({
|
|
15
|
+
family: "Symbols Nerd Font Mono",
|
|
16
|
+
release: "v3.5.1",
|
|
17
|
+
url: "https://github.com/ryanoasis/nerd-fonts/releases/download/v3.5.1/NerdFontsSymbolsOnly.zip",
|
|
18
|
+
archiveSha256: "fdca3682534f6f65e1ccb2345b0362ccf67d9b8eca7c8025330946e93e2473bc",
|
|
19
|
+
archiveMaxBytes: 8 * 1024 * 1024,
|
|
20
|
+
files: Object.freeze([
|
|
21
|
+
Object.freeze({ entry: "SymbolsNerdFontMono-Regular.ttf", name: "SymbolsNerdFontMono-Regular.ttf", sha256: "fe471e538392f51910faab985fa8e192a39dd3426125edd15b71b3680df0e749", kind: "font" }),
|
|
22
|
+
Object.freeze({ entry: "LICENSE", name: "SymbolsNerdFontMono-LICENSE.txt", sha256: "84a7a98c82140fb12c37fe42b93805baa16024cb3e5acc599b7ffe612c55d847", kind: "license" }),
|
|
23
|
+
]),
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
// Font file names the LIT band treats as "the icon font is installed": this one, and the
|
|
27
|
+
// proportional symbols font a person may have installed some other way.
|
|
28
|
+
export const ICON_FONT_FILE_NAMES = Object.freeze(["SymbolsNerdFontMono-Regular.ttf", "SymbolsNerdFont-Regular.ttf"]);
|
|
29
|
+
|
|
30
|
+
export const ICON_FONT_QUESTION = "Install the icon font for LitClaude's status band? (y/N) ";
|
|
31
|
+
|
|
32
|
+
const sha256 = (bytes) => createHash("sha256").update(bytes).digest("hex");
|
|
33
|
+
|
|
34
|
+
export class IconFontError extends Error {}
|
|
35
|
+
|
|
36
|
+
/** The per-user font folder, or null where installing one safely is not supported (Windows). */
|
|
37
|
+
export const iconFontDir = ({ platform = process.platform, env = process.env, home }) => {
|
|
38
|
+
if (platform === "darwin") return join(home, "Library", "Fonts");
|
|
39
|
+
if (platform === "win32") return null;
|
|
40
|
+
return join(env.XDG_DATA_HOME?.trim() || join(home, ".local", "share"), "fonts");
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
export const iconFontManifestPath = (litHome) => join(litHome, "icon-font", "manifest.json");
|
|
44
|
+
|
|
45
|
+
export const parseYesNo = (answer) => /^\s*y(?:es)?\s*$/iu.test(String(answer ?? ""));
|
|
46
|
+
|
|
47
|
+
const readU16 = (buffer, offset) => buffer.readUInt16LE(offset);
|
|
48
|
+
const readU32 = (buffer, offset) => buffer.readUInt32LE(offset);
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Reads the named entries out of a zip archive held in memory (stored or deflated, no zip64), by
|
|
52
|
+
* its central directory. Returns a Map of entry name to bytes; a missing entry is simply absent.
|
|
53
|
+
*/
|
|
54
|
+
export const extractZipEntries = (archive, names) => {
|
|
55
|
+
const wanted = new Set(names);
|
|
56
|
+
const found = new Map();
|
|
57
|
+
let end = -1;
|
|
58
|
+
for (let offset = archive.length - 22; offset >= Math.max(0, archive.length - 22 - 65_535); offset -= 1) {
|
|
59
|
+
if (readU32(archive, offset) === 0x06054b50) {
|
|
60
|
+
end = offset;
|
|
61
|
+
break;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
if (end < 0) throw new IconFontError("the archive is not a zip file");
|
|
65
|
+
const count = readU16(archive, end + 10);
|
|
66
|
+
let offset = readU32(archive, end + 16);
|
|
67
|
+
for (let index = 0; index < count; index += 1) {
|
|
68
|
+
if (readU32(archive, offset) !== 0x02014b50) throw new IconFontError("the archive's directory is damaged");
|
|
69
|
+
const method = readU16(archive, offset + 10);
|
|
70
|
+
const compressedSize = readU32(archive, offset + 20);
|
|
71
|
+
const nameLength = readU16(archive, offset + 28);
|
|
72
|
+
const extraLength = readU16(archive, offset + 30);
|
|
73
|
+
const commentLength = readU16(archive, offset + 32);
|
|
74
|
+
const localOffset = readU32(archive, offset + 42);
|
|
75
|
+
const name = archive.subarray(offset + 46, offset + 46 + nameLength).toString("utf8");
|
|
76
|
+
offset += 46 + nameLength + extraLength + commentLength;
|
|
77
|
+
if (!wanted.has(name)) continue;
|
|
78
|
+
if (readU32(archive, localOffset) !== 0x04034b50) throw new IconFontError(`the archive entry ${name} is damaged`);
|
|
79
|
+
const dataStart = localOffset + 30 + readU16(archive, localOffset + 26) + readU16(archive, localOffset + 28);
|
|
80
|
+
const data = archive.subarray(dataStart, dataStart + compressedSize);
|
|
81
|
+
if (method === 0) found.set(name, Buffer.from(data));
|
|
82
|
+
else if (method === 8) found.set(name, inflateRawSync(data));
|
|
83
|
+
else throw new IconFontError(`the archive entry ${name} uses an unsupported compression method`);
|
|
84
|
+
}
|
|
85
|
+
return found;
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* The pinned archive's bytes: a local copy named by LITCLAUDE_ICON_FONT_SOURCE (for offline
|
|
90
|
+
* installs), else the official release download. Either way it is refused unless its size is
|
|
91
|
+
* under the cap and its SHA-256 is the pinned one.
|
|
92
|
+
*/
|
|
93
|
+
export const loadIconFontArchive = async ({ env = process.env, pins = ICON_FONT, fetchImpl = globalThis.fetch } = {}) => {
|
|
94
|
+
let bytes;
|
|
95
|
+
const local = env.LITCLAUDE_ICON_FONT_SOURCE?.trim();
|
|
96
|
+
if (local) {
|
|
97
|
+
const stat = lstatSync(local);
|
|
98
|
+
if (!stat.isFile() || stat.size > pins.archiveMaxBytes) throw new IconFontError(`${local} is not a regular file under ${pins.archiveMaxBytes} bytes`);
|
|
99
|
+
bytes = readFileSync(local);
|
|
100
|
+
} else {
|
|
101
|
+
if (typeof fetchImpl !== "function") throw new IconFontError("this Node.js has no fetch; download the archive and set LITCLAUDE_ICON_FONT_SOURCE");
|
|
102
|
+
const response = await fetchImpl(pins.url, { redirect: "follow", signal: AbortSignal.timeout(60_000) });
|
|
103
|
+
if (!response.ok) throw new IconFontError(`the download answered HTTP ${response.status}`);
|
|
104
|
+
const declared = Number(response.headers?.get?.("content-length") ?? 0);
|
|
105
|
+
if (declared > pins.archiveMaxBytes) throw new IconFontError("the download is larger than the pinned archive");
|
|
106
|
+
bytes = Buffer.from(await response.arrayBuffer());
|
|
107
|
+
}
|
|
108
|
+
if (bytes.length > pins.archiveMaxBytes) throw new IconFontError("the archive is larger than the pinned archive");
|
|
109
|
+
const actual = sha256(bytes);
|
|
110
|
+
if (actual !== pins.archiveSha256) throw new IconFontError(`the archive's SHA-256 is ${actual}, not the pinned ${pins.archiveSha256}; nothing was installed`);
|
|
111
|
+
return bytes;
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
const lstatOrNull = (path) => {
|
|
115
|
+
try {
|
|
116
|
+
return lstatSync(path);
|
|
117
|
+
} catch {
|
|
118
|
+
return null;
|
|
119
|
+
}
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
// What sits at a destination now: nothing, the exact pinned file, a different file, or something
|
|
123
|
+
// that is not a regular file (a link, a folder), which is never written through or over.
|
|
124
|
+
const destinationState = (path, expectedSha) => {
|
|
125
|
+
const stat = lstatOrNull(path);
|
|
126
|
+
if (!stat) return "absent";
|
|
127
|
+
if (stat.isSymbolicLink() || !stat.isFile()) return "not-a-file";
|
|
128
|
+
return sha256(readFileSync(path)) === expectedSha ? "same" : "different";
|
|
129
|
+
};
|
|
130
|
+
|
|
131
|
+
export const planIconFontInstall = ({ platform = process.platform, env = process.env, home, pins = ICON_FONT }) => {
|
|
132
|
+
const dir = iconFontDir({ platform, env, home });
|
|
133
|
+
if (!dir) return { supported: false, dir: null, files: [] };
|
|
134
|
+
return {
|
|
135
|
+
supported: true,
|
|
136
|
+
dir,
|
|
137
|
+
files: pins.files.map((file) => {
|
|
138
|
+
const path = join(dir, file.name);
|
|
139
|
+
return { ...file, path, state: destinationState(path, file.sha256) };
|
|
140
|
+
}),
|
|
141
|
+
};
|
|
142
|
+
};
|
|
143
|
+
|
|
144
|
+
const readManifest = (litHome) => {
|
|
145
|
+
try {
|
|
146
|
+
const manifest = JSON.parse(readFileSync(iconFontManifestPath(litHome), "utf8"));
|
|
147
|
+
return Array.isArray(manifest?.files) ? manifest : null;
|
|
148
|
+
} catch {
|
|
149
|
+
return null;
|
|
150
|
+
}
|
|
151
|
+
};
|
|
152
|
+
|
|
153
|
+
const writeManifest = (litHome, manifest) => {
|
|
154
|
+
const path = iconFontManifestPath(litHome);
|
|
155
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
156
|
+
const staging = `${path}.${process.pid}.tmp`;
|
|
157
|
+
writeFileSync(staging, `${JSON.stringify(manifest, null, 2)}\n`, { mode: 0o600 });
|
|
158
|
+
renameSync(staging, path);
|
|
159
|
+
};
|
|
160
|
+
|
|
161
|
+
// Writes a new file without ever replacing one: the bytes go to a fresh temporary name in the same
|
|
162
|
+
// folder, then a hard link claims the destination, which fails if anything appeared there.
|
|
163
|
+
const writeNewFile = (path, bytes) => {
|
|
164
|
+
const staging = `${path}.litclaude-${process.pid}.tmp`;
|
|
165
|
+
writeFileSync(staging, bytes, { flag: "wx", mode: 0o644 });
|
|
166
|
+
try {
|
|
167
|
+
linkSync(staging, path);
|
|
168
|
+
} finally {
|
|
169
|
+
unlinkSync(staging);
|
|
170
|
+
}
|
|
171
|
+
};
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* Installs the pinned files from a verified archive into the per-user font folder. A destination
|
|
175
|
+
* that already holds the pinned bytes is left as it is (and recorded as LitClaude's only if an
|
|
176
|
+
* earlier LitClaude install wrote it); a different file or a link is never overwritten.
|
|
177
|
+
*/
|
|
178
|
+
export const installIconFont = ({ archive, platform = process.platform, env = process.env, home, litHome, pins = ICON_FONT, spawn = spawnSync, now = () => new Date() }) => {
|
|
179
|
+
const plan = planIconFontInstall({ platform, env, home, pins });
|
|
180
|
+
if (!plan.supported) return { status: "unsupported", installed: [], present: [], skipped: [] };
|
|
181
|
+
const entries = extractZipEntries(archive, pins.files.map(({ entry }) => entry));
|
|
182
|
+
const previous = readManifest(litHome)?.files ?? [];
|
|
183
|
+
const owned = [];
|
|
184
|
+
const installed = [];
|
|
185
|
+
const present = [];
|
|
186
|
+
const skipped = [];
|
|
187
|
+
for (const file of plan.files) {
|
|
188
|
+
const bytes = entries.get(file.entry);
|
|
189
|
+
if (!bytes) throw new IconFontError(`the archive has no ${file.entry}; nothing was installed`);
|
|
190
|
+
if (sha256(bytes) !== file.sha256) throw new IconFontError(`${file.entry} in the archive does not match its pinned SHA-256; nothing was installed`);
|
|
191
|
+
}
|
|
192
|
+
mkdirSync(plan.dir, { recursive: true });
|
|
193
|
+
for (const file of plan.files) {
|
|
194
|
+
if (file.state === "absent") {
|
|
195
|
+
writeNewFile(file.path, entries.get(file.entry));
|
|
196
|
+
installed.push(file.path);
|
|
197
|
+
owned.push({ path: file.path, sha256: file.sha256, kind: file.kind });
|
|
198
|
+
} else if (file.state === "same") {
|
|
199
|
+
present.push(file.path);
|
|
200
|
+
if (previous.some((entry) => entry.path === file.path && entry.sha256 === file.sha256)) owned.push({ path: file.path, sha256: file.sha256, kind: file.kind });
|
|
201
|
+
} else {
|
|
202
|
+
skipped.push({ path: file.path, reason: file.state === "different" ? "a different file is already there" : "something other than a regular file is there" });
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
if (owned.length > 0) writeManifest(litHome, { family: pins.family, release: pins.release, installedAt: now().toISOString(), files: owned });
|
|
206
|
+
let fontCache = null;
|
|
207
|
+
if (platform !== "darwin" && installed.length > 0) {
|
|
208
|
+
const result = spawn("fc-cache", ["-f", plan.dir], { encoding: "utf8", timeout: 60_000 });
|
|
209
|
+
fontCache = result.error?.code === "ENOENT" ? "fc-cache not found" : result.status === 0 ? "refreshed" : "fc-cache failed";
|
|
210
|
+
}
|
|
211
|
+
return { status: installed.length > 0 ? "installed" : skipped.length > 0 && present.length === 0 ? "skipped" : "present", dir: plan.dir, installed, present, skipped, fontCache };
|
|
212
|
+
};
|
|
213
|
+
|
|
214
|
+
/** Removes only the files the manifest records, and only while they still hold the recorded bytes. */
|
|
215
|
+
export const uninstallIconFont = ({ litHome, dryRun = false }) => {
|
|
216
|
+
const manifest = readManifest(litHome);
|
|
217
|
+
if (!manifest) return { removed: [], kept: [] };
|
|
218
|
+
const removed = [];
|
|
219
|
+
const kept = [];
|
|
220
|
+
for (const file of manifest.files) {
|
|
221
|
+
const state = destinationState(file.path, file.sha256);
|
|
222
|
+
if (state === "same") {
|
|
223
|
+
if (!dryRun) rmSync(file.path, { force: true });
|
|
224
|
+
removed.push(file.path);
|
|
225
|
+
} else if (state !== "absent") {
|
|
226
|
+
kept.push({ path: file.path, reason: state === "different" ? "its bytes changed since LitClaude wrote it" : "it is no longer a regular file" });
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
if (!dryRun) rmSync(dirname(iconFontManifestPath(litHome)), { recursive: true, force: true });
|
|
230
|
+
return { removed, kept };
|
|
231
|
+
};
|
|
232
|
+
|
|
233
|
+
/** Whether the icon font is installed for this user, and whether LitClaude wrote it. */
|
|
234
|
+
export const iconFontStatus = ({ platform = process.platform, env = process.env, home, litHome }) => {
|
|
235
|
+
const dir = iconFontDir({ platform, env, home });
|
|
236
|
+
if (!dir) return { supported: false, present: false, path: null, managed: false };
|
|
237
|
+
const managedPaths = new Set((readManifest(litHome)?.files ?? []).map(({ path }) => path));
|
|
238
|
+
for (const name of ICON_FONT_FILE_NAMES) {
|
|
239
|
+
const path = join(dir, name);
|
|
240
|
+
const stat = lstatOrNull(path);
|
|
241
|
+
if (stat?.isFile()) return { supported: true, present: true, path, managed: managedPaths.has(path) };
|
|
242
|
+
}
|
|
243
|
+
return { supported: true, present: false, path: join(dir, ICON_FONT.files[0].name), managed: false };
|
|
244
|
+
};
|
|
245
|
+
|
|
246
|
+
/**
|
|
247
|
+
* One line on pointing the terminal at the font, for the terminal named by TERM_PROGRAM (kitty by
|
|
248
|
+
* TERM). It is only needed when the terminal's own fallback does not pick the font up; nothing here
|
|
249
|
+
* changes a terminal's settings.
|
|
250
|
+
*/
|
|
251
|
+
export const terminalFontHint = (env = process.env, family = ICON_FONT.family) => {
|
|
252
|
+
const program = env.TERM_PROGRAM ?? "";
|
|
253
|
+
if (program === "iTerm.app") return `iTerm2: only if icons show as boxes, set Settings > Profiles > Text > Non-ASCII Font to ${family}.`;
|
|
254
|
+
if (program === "Apple_Terminal") return `Terminal.app: macOS font fallback usually finds ${family}; if icons show as boxes, pick a Nerd Font in Settings > Profiles > Text.`;
|
|
255
|
+
if (program === "vscode") return `VS Code: only if icons show as boxes, add it to terminal.integrated.fontFamily, for example "Menlo, '${family}'".`;
|
|
256
|
+
if (program === "ghostty") return `Ghostty: Nerd Font symbols are built in; if icons still show as boxes, add font-family = ${family} after your font in its config.`;
|
|
257
|
+
if (program === "WezTerm") return `WezTerm: Nerd Font symbols are built in; if icons still show as boxes, add '${family}' to config.font = wezterm.font_with_fallback { ... }.`;
|
|
258
|
+
if (env.TERM === "xterm-kitty" || env.KITTY_WINDOW_ID) return `kitty: only if icons show as boxes, add "symbol_map U+E000-U+F8FF ${family}" to kitty.conf.`;
|
|
259
|
+
return `Terminal: only if icons show as boxes, add ${family} as a fallback font in your terminal's font settings.`;
|
|
260
|
+
};
|
|
261
|
+
|
|
262
|
+
/** What to tell a Windows user, where LitClaude does not register fonts itself. */
|
|
263
|
+
export const windowsIconFontInstructions = (pins = ICON_FONT) =>
|
|
264
|
+
`Windows: LitClaude does not register fonts itself. Download ${pins.url}, check its SHA-256 is ${pins.archiveSha256}, then right-click SymbolsNerdFontMono-Regular.ttf and choose "Install for me".`;
|
package/bin/litclaude-ai.js
CHANGED
|
@@ -46,6 +46,18 @@ import { resolveProjectStateRoot } from "../plugins/litclaude/lib/project-state-
|
|
|
46
46
|
import { runUpdateNotifier } from "./update-notifier.mjs";
|
|
47
47
|
import { assertInstallPath, assertOwnedTree, assertCurrentPointer, assertOwnedRegistrations, migrateLegacyVendorPaths, planModifiedLegacySkill, preserveModifiedLegacySkill, registeredPluginInstallation, writeOwnershipReceipt, removeEmptyDirectory } from "./install-ownership.mjs";
|
|
48
48
|
import { banner, lockup, colorMode, supportsBlocks, terminalRows } from "./litfamily-banner.mjs";
|
|
49
|
+
import {
|
|
50
|
+
ICON_FONT,
|
|
51
|
+
ICON_FONT_QUESTION,
|
|
52
|
+
iconFontStatus,
|
|
53
|
+
installIconFont,
|
|
54
|
+
loadIconFontArchive,
|
|
55
|
+
parseYesNo,
|
|
56
|
+
planIconFontInstall,
|
|
57
|
+
terminalFontHint,
|
|
58
|
+
uninstallIconFont,
|
|
59
|
+
windowsIconFontInstructions,
|
|
60
|
+
} from "./icon-font.mjs";
|
|
49
61
|
|
|
50
62
|
const root = resolve(dirname(fileURLToPath(import.meta.url)), "..");
|
|
51
63
|
const packageJson = JSON.parse(readFileSync(join(root, "package.json"), "utf8"));
|
|
@@ -105,13 +117,32 @@ Install options:
|
|
|
105
117
|
safeguards are never weakened.
|
|
106
118
|
--yolo Shorthand for --permission-mode yolo.
|
|
107
119
|
--yes Accept today's shipped defaults for every question (HUD accent,
|
|
108
|
-
output style) and run without prompts. Model
|
|
109
|
-
host-owned: Claude Code picks its own models, so
|
|
110
|
-
never asks for a model or reasoning effort.
|
|
120
|
+
output style, no icon font) and run without prompts. Model
|
|
121
|
+
selection stays host-owned: Claude Code picks its own models, so
|
|
122
|
+
the installer never asks for a model or reasoning effort.
|
|
123
|
+
--icon-font Also install the symbols-only Nerd Font (Symbols Nerd Font Mono,
|
|
124
|
+
MIT) into your user font folder, so the LIT band can draw its
|
|
125
|
+
rounded pills and icons. Downloaded from the pinned Nerd Fonts
|
|
126
|
+
release and refused unless its SHA-256 matches; an offline copy
|
|
127
|
+
of that archive can be named with LITCLAUDE_ICON_FONT_SOURCE.
|
|
128
|
+
An interactive install asks instead (default No).
|
|
111
129
|
|
|
112
130
|
Typographic-motion engine adapted from mexicat/pdoom-video (MIT, Giacomo Magnanini), commit \`ca251e3\`.
|
|
113
131
|
`;
|
|
114
132
|
|
|
133
|
+
const dryRunAfterCommand = new Set(["install", "update", "uninstall", "doctor"]);
|
|
134
|
+
const dryRunTrueValues = new Set(["true", "1", "yes"]);
|
|
135
|
+
|
|
136
|
+
// `--dry-run` and `--dry-run=true` mean a dry run. Any other value is refused here, before any
|
|
137
|
+
// command runs: a mistyped dry run must never fall through to the real action.
|
|
138
|
+
const isDryRunFlag = (arg) => {
|
|
139
|
+
if (arg === "--dry-run") return true;
|
|
140
|
+
if (typeof arg !== "string" || !arg.startsWith("--dry-run=")) return false;
|
|
141
|
+
if (dryRunTrueValues.has(arg.slice("--dry-run=".length).toLowerCase())) return true;
|
|
142
|
+
process.stderr.write(`Unsupported value in ${arg}: use --dry-run (or --dry-run=true) for a dry run, or leave the flag out to run for real\n`);
|
|
143
|
+
process.exit(64);
|
|
144
|
+
};
|
|
145
|
+
|
|
115
146
|
const parseArgs = (argv) => {
|
|
116
147
|
const args = [];
|
|
117
148
|
let noAutoUpdate = false;
|
|
@@ -128,9 +159,15 @@ const parseArgs = (argv) => {
|
|
|
128
159
|
}
|
|
129
160
|
const wantsVersion = args[0] === "--version" || args[0] === "-v";
|
|
130
161
|
if (wantsVersion) return { dryRun: false, command: "version", rest: [], noAutoUpdate, invalidOption };
|
|
131
|
-
|
|
162
|
+
let dryRun = isDryRunFlag(args[0]);
|
|
132
163
|
if (dryRun) args.shift();
|
|
133
|
-
|
|
164
|
+
let rest = args.slice(1);
|
|
165
|
+
// The managed commands also take --dry-run after the subcommand (`install --dry-run`).
|
|
166
|
+
if (dryRunAfterCommand.has(args[0]) && rest.some(isDryRunFlag)) {
|
|
167
|
+
dryRun = true;
|
|
168
|
+
rest = rest.filter((arg) => !isDryRunFlag(arg));
|
|
169
|
+
}
|
|
170
|
+
return { dryRun, command: args[0], rest, noAutoUpdate, invalidOption };
|
|
134
171
|
};
|
|
135
172
|
|
|
136
173
|
const litHome = () => resolve(process.env.LITCLAUDE_HOME ?? join(homedir(), ".litclaude"));
|
|
@@ -185,6 +222,7 @@ const parseInstallOptions = (rest, existingPermissionMode = "safe") => {
|
|
|
185
222
|
let permissionMode = parsePermissionMode(process.env.LITCLAUDE_PERMISSION_MODE || preservedMode);
|
|
186
223
|
let permissionExplicit = Boolean(process.env.LITCLAUDE_PERMISSION_MODE);
|
|
187
224
|
let assumeYes = false;
|
|
225
|
+
let iconFont = false;
|
|
188
226
|
|
|
189
227
|
for (let index = 0; index < rest.length; index += 1) {
|
|
190
228
|
const arg = rest[index];
|
|
@@ -212,10 +250,14 @@ const parseInstallOptions = (rest, existingPermissionMode = "safe") => {
|
|
|
212
250
|
assumeYes = true;
|
|
213
251
|
continue;
|
|
214
252
|
}
|
|
253
|
+
if (arg === "--icon-font") {
|
|
254
|
+
iconFont = true;
|
|
255
|
+
continue;
|
|
256
|
+
}
|
|
215
257
|
fail(`Unknown install option: ${arg}`, 64);
|
|
216
258
|
}
|
|
217
259
|
|
|
218
|
-
return { permissionMode, permissionExplicit, assumeYes };
|
|
260
|
+
return { permissionMode, permissionExplicit, assumeYes, iconFont };
|
|
219
261
|
};
|
|
220
262
|
|
|
221
263
|
const permissionProfile = (mode) => {
|
|
@@ -257,6 +299,19 @@ const fail = (message, status = 1) => {
|
|
|
257
299
|
process.exit(status);
|
|
258
300
|
};
|
|
259
301
|
|
|
302
|
+
// A piped stdout is asynchronous on macOS, so process.exit() straight after a large write drops
|
|
303
|
+
// the queued tail (a 400 KB report arrived as 64 KiB). Exit once both streams have flushed.
|
|
304
|
+
const exitAfterFlush = (status) => {
|
|
305
|
+
process.exitCode = status;
|
|
306
|
+
let pending = 2;
|
|
307
|
+
const flushed = () => {
|
|
308
|
+
pending -= 1;
|
|
309
|
+
if (pending === 0) process.exit(status);
|
|
310
|
+
};
|
|
311
|
+
process.stdout.write("", flushed);
|
|
312
|
+
process.stderr.write("", flushed);
|
|
313
|
+
};
|
|
314
|
+
|
|
260
315
|
const shouldAnimateInstall = () => {
|
|
261
316
|
if (process.env.LITCLAUDE_SPINNER === "1") return true;
|
|
262
317
|
if (process.env.LITCLAUDE_SPINNER === "0") return false;
|
|
@@ -284,6 +339,8 @@ const formatInstallStep = (label) => {
|
|
|
284
339
|
// Shared LitFamily banner frame (plans/references/installer-choice-contract.md):
|
|
285
340
|
// the canonical Ignition B mark plus the existing install plan frame.
|
|
286
341
|
const FAMILY_RULE = ` ${"━".repeat(46)}`;
|
|
342
|
+
// The banner keeps its short tagline; package.json carries the longer npm search description.
|
|
343
|
+
const BANNER_TAGLINE = "Claude Code-native workflow distribution.";
|
|
287
344
|
const trueColorSupported = () => /truecolor|24bit/iu.test(process.env.COLORTERM ?? "");
|
|
288
345
|
const brandOrange = (text, color = shouldColorInstall()) => {
|
|
289
346
|
if (!color) return text;
|
|
@@ -311,7 +368,7 @@ const familyBanner = ({ color = shouldColorInstall() } = {}) => [
|
|
|
311
368
|
brandOrange(FAMILY_RULE, color),
|
|
312
369
|
"",
|
|
313
370
|
...renderLitClaudeWordmark({ color }),
|
|
314
|
-
` ${
|
|
371
|
+
` ${BANNER_TAGLINE}`,
|
|
315
372
|
"",
|
|
316
373
|
brandOrange(FAMILY_RULE, color),
|
|
317
374
|
" ╭─ INSTALL PLAN",
|
|
@@ -453,7 +510,7 @@ const hudAccentPreview = (theme) => {
|
|
|
453
510
|
? `\x1b[${ansi16CodeForAccent(theme.name)}m`
|
|
454
511
|
: `\x1b[38;5;${theme.code}m`;
|
|
455
512
|
const mark = `${accent}▌${accent ? "\x1b[0m" : ""}`;
|
|
456
|
-
const metrics = ` | O4.8 │ ctx [${mark}░░] 18%/
|
|
513
|
+
const metrics = ` | O4.8 │ ctx [${mark}░░] 18%/1m │ 5h [▏░]4%`;
|
|
457
514
|
return `${brand}${metrics}`;
|
|
458
515
|
};
|
|
459
516
|
|
|
@@ -536,6 +593,53 @@ const chooseOutputStyle = async (existingChoice = "off") => {
|
|
|
536
593
|
}
|
|
537
594
|
};
|
|
538
595
|
|
|
596
|
+
const shouldPromptIconFont = () => {
|
|
597
|
+
if (process.env.LITCLAUDE_ICON_FONT_PROMPT === "1") return true;
|
|
598
|
+
if (process.env.LITCLAUDE_ICON_FONT_PROMPT === "0") return false;
|
|
599
|
+
return Boolean(process.stdin.isTTY && process.stdout.isTTY && !Object.hasOwn(process.env, "CI"));
|
|
600
|
+
};
|
|
601
|
+
|
|
602
|
+
// The icon font is opt-in: the flag, or a Yes to the one question an interactive install asks.
|
|
603
|
+
// --yes keeps the shipped default, which is No.
|
|
604
|
+
const chooseIconFont = async ({ iconFont, assumeYes }) => {
|
|
605
|
+
if (iconFont) return true;
|
|
606
|
+
if (assumeYes || !shouldPromptIconFont()) return false;
|
|
607
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout, terminal: shouldColorInstall() });
|
|
608
|
+
try {
|
|
609
|
+
return parseYesNo(await rl.question(ICON_FONT_QUESTION));
|
|
610
|
+
} finally {
|
|
611
|
+
rl.close();
|
|
612
|
+
}
|
|
613
|
+
};
|
|
614
|
+
|
|
615
|
+
const iconFontPaths = () => ({ platform: process.platform, env: process.env, home: homedir(), litHome: litHome() });
|
|
616
|
+
|
|
617
|
+
const runIconFontInstall = async () => {
|
|
618
|
+
const where = iconFontPaths();
|
|
619
|
+
if (!planIconFontInstall(where).supported) {
|
|
620
|
+
process.stdout.write(`ICON_FONT_SKIPPED: ${windowsIconFontInstructions()}\n`);
|
|
621
|
+
return;
|
|
622
|
+
}
|
|
623
|
+
try {
|
|
624
|
+
const archive = await loadIconFontArchive({ env: process.env });
|
|
625
|
+
const result = installIconFont({ ...where, archive });
|
|
626
|
+
for (const path of result.installed) process.stdout.write(`ICON_FONT_INSTALLED: ${path}\n`);
|
|
627
|
+
for (const path of result.present) process.stdout.write(`ICON_FONT_PRESENT: ${path}\n`);
|
|
628
|
+
for (const { path, reason } of result.skipped) process.stdout.write(`ICON_FONT_LEFT_ALONE: ${path} (${reason})\n`);
|
|
629
|
+
if (result.fontCache) process.stdout.write(`ICON_FONT_CACHE: ${result.fontCache}\n`);
|
|
630
|
+
process.stdout.write(`ICON_FONT_HINT: ${terminalFontHint(process.env)}\n`);
|
|
631
|
+
} catch (error) {
|
|
632
|
+
process.stderr.write(`ICON_FONT_FAILED: ${error.message}\n`);
|
|
633
|
+
}
|
|
634
|
+
};
|
|
635
|
+
|
|
636
|
+
const iconFontDoctorLine = () => {
|
|
637
|
+
const status = iconFontStatus(iconFontPaths());
|
|
638
|
+
if (!status.supported) return "ICON_FONT: not managed on this platform (optional; see litclaude-ai install --help)";
|
|
639
|
+
if (status.present) return `ICON_FONT: present (${status.path}, ${status.managed ? "installed by LitClaude" : "not installed by LitClaude"})`;
|
|
640
|
+
return "ICON_FONT: not installed (optional; litclaude-ai install --icon-font)";
|
|
641
|
+
};
|
|
642
|
+
|
|
539
643
|
const hudCommandForPlugin = (pluginPath, accent = "cyan") =>
|
|
540
644
|
`LITCLAUDE_HUD_ACCENT=${normalizeHudAccent(accent)} node "${join(pluginPath, "bin", "litclaude-hud.js")}"`;
|
|
541
645
|
|
|
@@ -1030,6 +1134,8 @@ const verifyInstalledPayload = ({ home, litClaudeHome, targetPlugin }) => {
|
|
|
1030
1134
|
for (const relativePath of [
|
|
1031
1135
|
".claude-plugin/plugin.json",
|
|
1032
1136
|
"hooks/hooks.json",
|
|
1137
|
+
"hooks/lit-mod.mjs",
|
|
1138
|
+
"hooks/lit-band-client.mjs",
|
|
1033
1139
|
"bin/litclaude-hook.js",
|
|
1034
1140
|
"lib/start-work-lifecycle.mjs",
|
|
1035
1141
|
"lib/start-work-cli.mjs",
|
|
@@ -1135,7 +1241,7 @@ const install = async ({ dryRun, rest }) => {
|
|
|
1135
1241
|
const home = claudeHome();
|
|
1136
1242
|
const litClaudeHome = litHome();
|
|
1137
1243
|
const existingLitClaude = readClaudeSettings(home)[litClaudeSettingsKey] ?? {};
|
|
1138
|
-
const { permissionMode, permissionExplicit, assumeYes } = parseInstallOptions(rest, existingLitClaude.permissionMode);
|
|
1244
|
+
const { permissionMode, permissionExplicit, assumeYes, iconFont } = parseInstallOptions(rest, existingLitClaude.permissionMode);
|
|
1139
1245
|
const existingHudAccent = normalizeHudAccent(existingLitClaude.hudAccent);
|
|
1140
1246
|
const existingOutputStyle = existingLitClaude.outputStyleChoice ?? "off";
|
|
1141
1247
|
const requestedHudAccent = process.env.LITCLAUDE_HUD_ACCENT
|
|
@@ -1160,6 +1266,16 @@ const install = async ({ dryRun, rest }) => {
|
|
|
1160
1266
|
process.stdout.write(`Would mutate global Claude settings permissions.allow/deny: ${permissionMode} (${permissionSource})\n`);
|
|
1161
1267
|
process.stdout.write(`Permission profile: ${permissionProfile(permissionMode).description}\n`);
|
|
1162
1268
|
process.stdout.write("Would pre-warm the lit-typographic-motion runtime: litclaude-ai motion-runtime install\n");
|
|
1269
|
+
const fontPlan = planIconFontInstall(iconFontPaths());
|
|
1270
|
+
if (iconFont) {
|
|
1271
|
+
process.stdout.write(fontPlan.supported
|
|
1272
|
+
? `Would install icon font: ${ICON_FONT.family} ${ICON_FONT.release} into ${fontPlan.dir} from ${ICON_FONT.url} (SHA-256 ${ICON_FONT.archiveSha256})\n`
|
|
1273
|
+
: `Would skip icon font: ${windowsIconFontInstructions()}\n`);
|
|
1274
|
+
} else if (!assumeYes && shouldPromptIconFont()) {
|
|
1275
|
+
process.stdout.write(`Would ask: ${ICON_FONT_QUESTION.trim()}\n`);
|
|
1276
|
+
} else {
|
|
1277
|
+
process.stdout.write("Icon font: not requested (pass --icon-font)\n");
|
|
1278
|
+
}
|
|
1163
1279
|
process.stdout.write("Launch with: claude\n");
|
|
1164
1280
|
return;
|
|
1165
1281
|
}
|
|
@@ -1174,6 +1290,7 @@ const install = async ({ dryRun, rest }) => {
|
|
|
1174
1290
|
const outputStyleChoice = assumeYes
|
|
1175
1291
|
? normalizeOutputStyle(process.env.LITCLAUDE_OUTPUT_STYLE ?? existingOutputStyle)
|
|
1176
1292
|
: await chooseOutputStyle(existingOutputStyle);
|
|
1293
|
+
const wantsIconFont = await chooseIconFont({ iconFont, assumeYes });
|
|
1177
1294
|
const preservation = preflightInstallOwnership({ preserveModifiedSkill: true });
|
|
1178
1295
|
const preservedPath = preserveModifiedLegacySkill(preservation, litClaudeHome);
|
|
1179
1296
|
const targetCompatibility = versionRoot(litClaudeHome);
|
|
@@ -1268,6 +1385,7 @@ const install = async ({ dryRun, rest }) => {
|
|
|
1268
1385
|
process.stdout.write(hudManaged ? `HUD: LitClaude statusLine installed (${hudAccent})\n` : "HUD: user statusLine setting preserved\n");
|
|
1269
1386
|
process.stdout.write(`Global Claude settings permissions.allow/deny: ${permissionMode} — ${permissionProfile(permissionMode).description}\n`);
|
|
1270
1387
|
process.stdout.write(`${prewarmMotionRuntime()}\n`);
|
|
1388
|
+
if (wantsIconFont) await runIconFontInstall();
|
|
1271
1389
|
process.stdout.write("Launch with: claude\n");
|
|
1272
1390
|
if (preservedPath) process.stderr.write(`INSTALL_WARNING: PRESERVED_MODIFIED_SKILL lit-korean at ${preservedPath}\n`);
|
|
1273
1391
|
};
|
|
@@ -1295,6 +1413,7 @@ const doctor = ({ dryRun }) => {
|
|
|
1295
1413
|
process.stdout.write("Would verify exact canonical frontend corpus bytes, legal companions, no extras, and three canonical runtime closures\n");
|
|
1296
1414
|
process.stdout.write(`${jevStatusLine(process.env)}\n`);
|
|
1297
1415
|
writeAutoHandoffDoctorLines();
|
|
1416
|
+
process.stdout.write(`${iconFontDoctorLine()}\n`);
|
|
1298
1417
|
return;
|
|
1299
1418
|
}
|
|
1300
1419
|
|
|
@@ -1307,6 +1426,8 @@ const doctor = ({ dryRun }) => {
|
|
|
1307
1426
|
".mcp.json",
|
|
1308
1427
|
".lsp.json",
|
|
1309
1428
|
"hooks/hooks.json",
|
|
1429
|
+
"hooks/lit-mod.mjs",
|
|
1430
|
+
"hooks/lit-band-client.mjs",
|
|
1310
1431
|
"bin/litclaude-hook.js",
|
|
1311
1432
|
"bin/litclaude-mcp.js",
|
|
1312
1433
|
"lib/start-work-lifecycle.mjs",
|
|
@@ -1383,6 +1504,7 @@ const doctor = ({ dryRun }) => {
|
|
|
1383
1504
|
}
|
|
1384
1505
|
process.stdout.write(`${jevStatusLine(process.env)}\n`);
|
|
1385
1506
|
writeAutoHandoffDoctorLines();
|
|
1507
|
+
process.stdout.write(`${iconFontDoctorLine()}\n`);
|
|
1386
1508
|
|
|
1387
1509
|
// lit-typographic-motion: the five readiness probes, every run. Read-only and never fatal; a
|
|
1388
1510
|
// missing pre-warm names the command that fixes it.
|
|
@@ -1484,7 +1606,7 @@ const runClaude = ({ dryRun, rest }) => {
|
|
|
1484
1606
|
|
|
1485
1607
|
const result = spawnSync(command[0], command.slice(1), { stdio: "inherit" });
|
|
1486
1608
|
if (result.error) fail(`failed to start claude: ${result.error.message}`);
|
|
1487
|
-
|
|
1609
|
+
exitAfterFlush(result.status ?? 1);
|
|
1488
1610
|
};
|
|
1489
1611
|
|
|
1490
1612
|
const uninstall = ({ dryRun }) => {
|
|
@@ -1493,8 +1615,15 @@ const uninstall = ({ dryRun }) => {
|
|
|
1493
1615
|
const updateNotifierRoot = dirname(updateNotifierCachePath(home));
|
|
1494
1616
|
if (dryRun) {
|
|
1495
1617
|
process.stdout.write(`DRY_RUN: remove checked ${versionRoot(home)}, ${currentRoot(home)}, ${marketplaceRoot(home)}, ${claudePluginRoot()}, ${pluginKey}, and its marketplace/settings entries; preserve unrelated versions/cache entries and nonempty ${updateNotifierRoot}\n`);
|
|
1618
|
+
const fonts = uninstallIconFont({ litHome: home, dryRun: true });
|
|
1619
|
+
for (const path of fonts.removed) process.stdout.write(`Would remove icon font file: ${path}\n`);
|
|
1620
|
+
for (const { path, reason } of fonts.kept) process.stdout.write(`Would keep icon font file: ${path} (${reason})\n`);
|
|
1496
1621
|
return;
|
|
1497
1622
|
}
|
|
1623
|
+
// Before the LitClaude home goes: the manifest there says which font files LitClaude wrote.
|
|
1624
|
+
const fonts = uninstallIconFont({ litHome: home });
|
|
1625
|
+
for (const path of fonts.removed) process.stdout.write(`ICON_FONT_REMOVED: ${path}\n`);
|
|
1626
|
+
for (const { path, reason } of fonts.kept) process.stdout.write(`ICON_FONT_KEPT: ${path} (${reason})\n`);
|
|
1498
1627
|
rmSync(currentRoot(home), { recursive: true, force: true });
|
|
1499
1628
|
rmSync(versionRoot(home), { recursive: true, force: true });
|
|
1500
1629
|
removeEmptyDirectory(join(home, "litclaude-ai"));
|
|
@@ -1560,7 +1689,8 @@ const main = async () => {
|
|
|
1560
1689
|
|
|
1561
1690
|
if (!command) {
|
|
1562
1691
|
printUsage();
|
|
1563
|
-
|
|
1692
|
+
exitAfterFlush(64);
|
|
1693
|
+
return;
|
|
1564
1694
|
}
|
|
1565
1695
|
if (parsed.invalidOption) fail(`Unknown update option: ${parsed.invalidOption}`, 64);
|
|
1566
1696
|
|
|
@@ -1570,6 +1700,13 @@ const main = async () => {
|
|
|
1570
1700
|
return;
|
|
1571
1701
|
}
|
|
1572
1702
|
|
|
1703
|
+
// Unknown flags on the managed commands stop here, before the ownership preflight, the
|
|
1704
|
+
// automatic update or the command itself can change anything.
|
|
1705
|
+
if (command === "install" || command === "update") parseInstallOptions(parsed.rest);
|
|
1706
|
+
if ((command === "uninstall" || command === "doctor") && parsed.rest.length > 0) {
|
|
1707
|
+
fail(`Unknown ${command} option: ${parsed.rest[0]}`, 64);
|
|
1708
|
+
}
|
|
1709
|
+
|
|
1573
1710
|
if (!parsed.dryRun && ["install", "update", "uninstall"].includes(command)) {
|
|
1574
1711
|
preflightInstallOwnership({ preserveModifiedSkill: ["install", "update"].includes(parsed.command) });
|
|
1575
1712
|
}
|
|
@@ -1603,26 +1740,26 @@ const main = async () => {
|
|
|
1603
1740
|
runClaude(parsed);
|
|
1604
1741
|
break;
|
|
1605
1742
|
case "litgoal":
|
|
1606
|
-
|
|
1743
|
+
exitAfterFlush(runLitgoalCli(parsed.rest));
|
|
1607
1744
|
break;
|
|
1608
1745
|
case "wikify":
|
|
1609
|
-
|
|
1746
|
+
exitAfterFlush(runWikifyKnowledgeCli(parsed.rest, undefined, process.cwd()));
|
|
1610
1747
|
break;
|
|
1611
1748
|
case "workflow-check":
|
|
1612
|
-
|
|
1749
|
+
exitAfterFlush(runWorkflowCheckCli(root, version, parsed.rest));
|
|
1613
1750
|
break;
|
|
1614
1751
|
case "public-read":
|
|
1615
|
-
|
|
1752
|
+
exitAfterFlush(await runPublicRead(parsed));
|
|
1616
1753
|
break;
|
|
1617
1754
|
case "start-work-next":
|
|
1618
|
-
|
|
1755
|
+
exitAfterFlush(runStartWorkContinuationCli(root, parsed.rest));
|
|
1619
1756
|
break;
|
|
1620
1757
|
case "start-work":
|
|
1621
|
-
|
|
1758
|
+
exitAfterFlush(runStartWorkCli(parsed.rest, undefined, process.cwd()));
|
|
1622
1759
|
break;
|
|
1623
1760
|
case "motion-runtime": {
|
|
1624
1761
|
const { main: motionRuntime } = await import(pathToFileURL(join(motionSkillScripts, "motion-runtime.mjs")).href);
|
|
1625
|
-
|
|
1762
|
+
exitAfterFlush(await motionRuntime(parsed.rest));
|
|
1626
1763
|
break;
|
|
1627
1764
|
}
|
|
1628
1765
|
case "uninstall":
|
|
@@ -1 +1 @@
|
|
|
1
|
-
<svg xmlns="http://www.w3.org/2000/svg" width="146" height="20" role="img" aria-label="release: 1.0.
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" width="146" height="20" role="img" aria-label="release: 1.0.22"><title>release: 1.0.22</title><g shape-rendering="crispEdges"><rect width="49" height="20" fill="#080d14"/><rect x="49" width="97" height="20" fill="#ff6337"/></g><g fill="#fff" text-anchor="middle" font-family="Verdana,Geneva,DejaVu Sans,sans-serif" text-rendering="geometricPrecision" font-size="110"><text x="255" y="140" textLength="390" transform="scale(.1)">release</text><text x="965" y="140" textLength="870" transform="scale(.1)" fill="#080D14">1.0.22</text></g></svg>
|