@vincentt-xr/cli 0.22.0 → 0.23.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/MIGRATION.md +11 -0
- package/dist/cli/args.js +2 -0
- package/dist/cli/index.js +27 -2
- package/dist/cli/usage-text.js +10 -1
- package/dist/cli/verb-surface.d.ts +2 -1
- package/dist/cli/verb-surface.js +5 -0
- package/dist/dx/journal.d.ts +3 -0
- package/dist/dx/journal.js +6 -1
- package/dist/dx/skeleton.js +3 -0
- package/dist/editor/app.d.ts +61 -0
- package/dist/editor/app.js +180 -0
- package/dist/editor/copy.d.ts +22 -0
- package/dist/editor/copy.js +95 -0
- package/dist/editor/handoffs.d.ts +12 -0
- package/dist/editor/handoffs.js +93 -0
- package/dist/editor/launcher.d.ts +86 -0
- package/dist/editor/launcher.js +255 -0
- package/dist/editor/localFiles.d.ts +31 -0
- package/dist/editor/localFiles.js +100 -0
- package/dist/editor/reads.d.ts +47 -0
- package/dist/editor/reads.js +269 -0
- package/dist/editor/text.d.ts +7 -0
- package/dist/editor/text.js +58 -0
- package/dist/editor/tools.d.ts +5 -0
- package/dist/editor/tools.js +20 -0
- package/package.json +2 -2
|
@@ -0,0 +1,269 @@
|
|
|
1
|
+
// `vincentt editor --selection [--json]` and `--status --json`: read the editor's local files
|
|
2
|
+
// and print them. A read flag never talks to the running app; when none runs for the folder it
|
|
3
|
+
// says so on stderr, still prints one JSON object on stdout, and exits 1. A file in a schema
|
|
4
|
+
// major this CLI does not know is refused whole ("too new"), never printed in part.
|
|
5
|
+
import { pidAlive } from "./app.js";
|
|
6
|
+
import { NOT_RUNNING, tooNew } from "./copy.js";
|
|
7
|
+
import { realpath } from "node:fs/promises";
|
|
8
|
+
import { lastHandoff } from "./handoffs.js";
|
|
9
|
+
import { parsePidFile, readLocalFile, readLocalFileStat, } from "./localFiles.js";
|
|
10
|
+
import { agentSafe, agentSafeValue } from "./text.js";
|
|
11
|
+
export const SELECTION_SCHEMA = "vincentt.editor-selection/1";
|
|
12
|
+
export const STATE_SCHEMA = "vincentt.editor-state/1";
|
|
13
|
+
/**
|
|
14
|
+
* True when the code `pid` is running meets the requirement, from any path. Checked on the
|
|
15
|
+
* running process itself, so no path the OS reports for it is trusted.
|
|
16
|
+
*/
|
|
17
|
+
export async function isVerifiedEditor(pid, p) {
|
|
18
|
+
const verdict = await p.system.verifyPid(pid, p.requirement).catch(() => undefined);
|
|
19
|
+
return verdict?.ok === true;
|
|
20
|
+
}
|
|
21
|
+
/** `ps` reports whole seconds and the app writes whole seconds. */
|
|
22
|
+
const START_SLACK_MS = 1_000;
|
|
23
|
+
/**
|
|
24
|
+
* True when the process now holding `pid` was already running when the record was written. A
|
|
25
|
+
* process that started after `startedAt` reused the pid of the editor that wrote it.
|
|
26
|
+
*/
|
|
27
|
+
export function startedBeforeRecord(processStart, rec) {
|
|
28
|
+
return processStart !== undefined && processStart <= rec.startedAt + START_SLACK_MS;
|
|
29
|
+
}
|
|
30
|
+
/** The editor recorded in `pid`, when that process is live, is that record's process, and is a verified editor. */
|
|
31
|
+
export async function runningEditor(root, p) {
|
|
32
|
+
const text = await readLocalFile(root, "pid").catch(() => undefined);
|
|
33
|
+
const rec = text === undefined ? undefined : parsePidFile(text);
|
|
34
|
+
if (rec === undefined || !(p.isAlive ?? pidAlive)(rec.pid))
|
|
35
|
+
return undefined;
|
|
36
|
+
const start = await p.system.startTime(rec.pid).catch(() => undefined);
|
|
37
|
+
if (!startedBeforeRecord(start, rec))
|
|
38
|
+
return undefined;
|
|
39
|
+
return (await isVerifiedEditor(rec.pid, p)) ? rec : undefined;
|
|
40
|
+
}
|
|
41
|
+
/** Whether `schema` is `<family>/1`, a later major of the family, or something else. */
|
|
42
|
+
export function schemaMajor(schema, family) {
|
|
43
|
+
if (typeof schema !== "string")
|
|
44
|
+
return "foreign";
|
|
45
|
+
const m = /^(.+)\/(\d+)$/.exec(schema);
|
|
46
|
+
if (m === null || m[1] !== family)
|
|
47
|
+
return "foreign";
|
|
48
|
+
const major = Number(m[2]);
|
|
49
|
+
if (major === 1)
|
|
50
|
+
return "known";
|
|
51
|
+
return major > 1 ? { newer: schema } : "foreign";
|
|
52
|
+
}
|
|
53
|
+
const SELECTION_FAMILY = "vincentt.editor-selection";
|
|
54
|
+
const STATE_FAMILY = "vincentt.editor-state";
|
|
55
|
+
async function readJson(root, name) {
|
|
56
|
+
return (await readJsonStat(root, name))?.value;
|
|
57
|
+
}
|
|
58
|
+
async function readJsonStat(root, name) {
|
|
59
|
+
const file = await readLocalFileStat(root, name).catch(() => undefined);
|
|
60
|
+
if (file === undefined)
|
|
61
|
+
return undefined;
|
|
62
|
+
try {
|
|
63
|
+
const v = JSON.parse(file.text);
|
|
64
|
+
return typeof v === "object" && v !== null && !Array.isArray(v)
|
|
65
|
+
? { value: v, mtimeMs: file.mtimeMs }
|
|
66
|
+
: undefined;
|
|
67
|
+
}
|
|
68
|
+
catch {
|
|
69
|
+
return undefined;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* A snapshot belongs to the running editor when it carries the pid record's session and, when it
|
|
74
|
+
* names a pid, that pid.
|
|
75
|
+
*/
|
|
76
|
+
function sameEditor(file, rec) {
|
|
77
|
+
if (typeof file.session !== "string" || file.session !== rec.session)
|
|
78
|
+
return false;
|
|
79
|
+
return typeof file.pid !== "number" || file.pid === rec.pid;
|
|
80
|
+
}
|
|
81
|
+
/** Fields the CLI states itself; file content never supplies them. */
|
|
82
|
+
const RESERVED_STATE_KEYS = ["schema", "running", "pid", "session"];
|
|
83
|
+
const num = (v) => typeof v === "number" && Number.isFinite(v) ? v : undefined;
|
|
84
|
+
/** A string field, sanitised; anything that is not a string reads as empty. */
|
|
85
|
+
const str = (v, max) => typeof v === "string" ? agentSafe(v, max) : "";
|
|
86
|
+
/** The selection as the agent may read it: an allowlist of fields, every string sanitised. */
|
|
87
|
+
export function shapeSelection(sel) {
|
|
88
|
+
if (typeof sel !== "object" || sel === null)
|
|
89
|
+
return null;
|
|
90
|
+
const s = sel;
|
|
91
|
+
const src = (typeof s.source === "object" && s.source !== null ? s.source : {});
|
|
92
|
+
const values = Array.isArray(s.values) ? s.values : [];
|
|
93
|
+
return {
|
|
94
|
+
objectKey: str(s.objectKey, 400),
|
|
95
|
+
label: str(s.label, 200),
|
|
96
|
+
type: str(s.type, 80),
|
|
97
|
+
kind: str(s.kind, 20),
|
|
98
|
+
source: {
|
|
99
|
+
file: str(src.file, 400),
|
|
100
|
+
line: num(src.line) ?? null,
|
|
101
|
+
column: num(src.column) ?? null,
|
|
102
|
+
},
|
|
103
|
+
values: values.slice(0, 256).map((raw) => {
|
|
104
|
+
const v = (typeof raw === "object" && raw !== null ? raw : {});
|
|
105
|
+
const out = {
|
|
106
|
+
path: str(v.path, 200),
|
|
107
|
+
type: str(v.type, 20),
|
|
108
|
+
value: agentSafeValue(v.value),
|
|
109
|
+
};
|
|
110
|
+
if (typeof v.unit === "string")
|
|
111
|
+
out.unit = agentSafe(v.unit, 10);
|
|
112
|
+
out.editable =
|
|
113
|
+
v.editable === true
|
|
114
|
+
? true
|
|
115
|
+
: agentSafeValue(v.editable ?? { readonly: "unsupported" }, 40);
|
|
116
|
+
if (typeof v.decl === "string")
|
|
117
|
+
out.decl = agentSafe(v.decl, 200);
|
|
118
|
+
if (num(v.sharedBy) !== undefined)
|
|
119
|
+
out.sharedBy = num(v.sharedBy);
|
|
120
|
+
return out;
|
|
121
|
+
}),
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
function tooNewResult(schema) {
|
|
125
|
+
return { stdout: [], stderr: tooNew(schema), exitCode: 1, reason: "too_new" };
|
|
126
|
+
}
|
|
127
|
+
export async function selectionRead(root, json, p) {
|
|
128
|
+
const file = await readJson(root, "selection.json");
|
|
129
|
+
const major = schemaMajor(file?.schema, SELECTION_FAMILY);
|
|
130
|
+
if (typeof major === "object")
|
|
131
|
+
return tooNewResult(major.newer);
|
|
132
|
+
const rec = await runningEditor(root, p);
|
|
133
|
+
if (rec === undefined) {
|
|
134
|
+
return {
|
|
135
|
+
stdout: json
|
|
136
|
+
? [JSON.stringify({ schema: SELECTION_SCHEMA, running: false, selection: null })]
|
|
137
|
+
: [],
|
|
138
|
+
stderr: [NOT_RUNNING],
|
|
139
|
+
exitCode: 1,
|
|
140
|
+
reason: "not_running",
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
const selection = file && major === "known" && sameEditor(file, rec)
|
|
144
|
+
? shapeSelection(file.selection)
|
|
145
|
+
: null;
|
|
146
|
+
if (json) {
|
|
147
|
+
return {
|
|
148
|
+
stdout: [JSON.stringify({ schema: SELECTION_SCHEMA, running: true, selection })],
|
|
149
|
+
stderr: [],
|
|
150
|
+
exitCode: 0,
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
return { stdout: humanSelection(selection), stderr: [], exitCode: 0 };
|
|
154
|
+
}
|
|
155
|
+
function display(v) {
|
|
156
|
+
if (Array.isArray(v))
|
|
157
|
+
return v.map(display).join(", ");
|
|
158
|
+
if (v !== null && typeof v === "object") {
|
|
159
|
+
return Object.entries(v)
|
|
160
|
+
.map(([k, x]) => `${k}: ${display(x)}`)
|
|
161
|
+
.join(", ");
|
|
162
|
+
}
|
|
163
|
+
return String(v);
|
|
164
|
+
}
|
|
165
|
+
export function humanSelection(sel) {
|
|
166
|
+
if (sel === null)
|
|
167
|
+
return ["Nothing is selected in the editor."];
|
|
168
|
+
const src = sel.source;
|
|
169
|
+
const kind = sel.kind === "sdk" ? " (Vincentt SDK)" : "";
|
|
170
|
+
const out = [
|
|
171
|
+
`${sel.label} ${sel.type}${kind} ${src.file}${src.line !== null ? `:${src.line}` : ""}`,
|
|
172
|
+
];
|
|
173
|
+
for (const v of sel.values) {
|
|
174
|
+
const unit = typeof v.unit === "string" ? ` ${v.unit}` : "";
|
|
175
|
+
const ro = v.editable === true
|
|
176
|
+
? ""
|
|
177
|
+
: ` (read-only: ${display(v.editable?.readonly ?? "unsupported")})`;
|
|
178
|
+
out.push(` ${String(v.path).padEnd(26)} ${display(v.value)}${unit}${ro}`);
|
|
179
|
+
}
|
|
180
|
+
return out;
|
|
181
|
+
}
|
|
182
|
+
const APP_VERSION = /^\d+\.\d+\.\d+([-+][0-9A-Za-z.-]{1,32})?$/;
|
|
183
|
+
const LAST_OPEN = /^(opened|awaiting_trust|cancelled|refused:[A-Za-z0-9_-]{1,16})$/;
|
|
184
|
+
/**
|
|
185
|
+
* `app` as the agent may read it: exactly `{version, lastOpen}`, each null unless it is one of
|
|
186
|
+
* the shapes the app writes. `trustLastOpen` false nulls `lastOpen` whatever the file says.
|
|
187
|
+
*/
|
|
188
|
+
export function shapeApp(app, trustLastOpen) {
|
|
189
|
+
const a = (typeof app === "object" && app !== null ? app : {});
|
|
190
|
+
const version = typeof a.version === "string" && APP_VERSION.test(a.version) ? a.version : null;
|
|
191
|
+
const lastOpen = trustLastOpen && typeof a.lastOpen === "string" && LAST_OPEN.test(a.lastOpen)
|
|
192
|
+
? a.lastOpen
|
|
193
|
+
: null;
|
|
194
|
+
return { version, lastOpen };
|
|
195
|
+
}
|
|
196
|
+
/**
|
|
197
|
+
* A state file no running editor vouches for is believed about the last open only when it
|
|
198
|
+
* changed after this machine's last hand-off of this folder: a committed or copied state.json
|
|
199
|
+
* predates it, and a folder never handed over has no open to report. A time in the future (an
|
|
200
|
+
* unpacked archive keeps whatever mtime it was given) is not after anything.
|
|
201
|
+
*/
|
|
202
|
+
async function lastOpenTrusted(root, mtimeMs) {
|
|
203
|
+
const project = await realpath(root).catch(() => undefined);
|
|
204
|
+
if (project === undefined)
|
|
205
|
+
return false;
|
|
206
|
+
const at = await lastHandoff(project).catch(() => undefined);
|
|
207
|
+
return at !== undefined && mtimeMs > at && mtimeMs <= Date.now() + 5_000;
|
|
208
|
+
}
|
|
209
|
+
function appField(file, trustLastOpen) {
|
|
210
|
+
return file !== undefined && file.app !== undefined
|
|
211
|
+
? { app: shapeApp(file.app, trustLastOpen) }
|
|
212
|
+
: {};
|
|
213
|
+
}
|
|
214
|
+
export async function statusRead(root, p) {
|
|
215
|
+
const read = await readJsonStat(root, "state.json");
|
|
216
|
+
const file = read?.value;
|
|
217
|
+
const major = schemaMajor(file?.schema, STATE_FAMILY);
|
|
218
|
+
if (typeof major === "object")
|
|
219
|
+
return tooNewResult(major.newer);
|
|
220
|
+
const known = major === "known" ? file : undefined;
|
|
221
|
+
const rec = await runningEditor(root, p);
|
|
222
|
+
if (known && rec && known.running === true && sameEditor(known, rec)) {
|
|
223
|
+
// The running, verified editor wrote this file. Sanitised first, so a key that only
|
|
224
|
+
// becomes `pid` once its control characters are stripped is removed too.
|
|
225
|
+
const shaped = agentSafeValue(known, 400);
|
|
226
|
+
for (const k of RESERVED_STATE_KEYS)
|
|
227
|
+
delete shaped[k];
|
|
228
|
+
if (known.app !== undefined)
|
|
229
|
+
shaped.app = shapeApp(known.app, true);
|
|
230
|
+
return {
|
|
231
|
+
stdout: [
|
|
232
|
+
JSON.stringify({ schema: STATE_SCHEMA, running: true, pid: rec.pid, ...shaped }),
|
|
233
|
+
],
|
|
234
|
+
stderr: [],
|
|
235
|
+
exitCode: 0,
|
|
236
|
+
};
|
|
237
|
+
}
|
|
238
|
+
const trusted = known !== undefined &&
|
|
239
|
+
read !== undefined &&
|
|
240
|
+
(await lastOpenTrusted(root, read.mtimeMs));
|
|
241
|
+
if (rec === undefined) {
|
|
242
|
+
// A refused or cancelled open leaves no running editor behind, so the outcome of the last
|
|
243
|
+
// hand-off is reported here too, when the file is believed.
|
|
244
|
+
return {
|
|
245
|
+
stdout: [
|
|
246
|
+
JSON.stringify({
|
|
247
|
+
schema: STATE_SCHEMA,
|
|
248
|
+
running: false,
|
|
249
|
+
...appField(known, trusted),
|
|
250
|
+
}),
|
|
251
|
+
],
|
|
252
|
+
stderr: [NOT_RUNNING],
|
|
253
|
+
exitCode: 1,
|
|
254
|
+
reason: "not_running",
|
|
255
|
+
};
|
|
256
|
+
}
|
|
257
|
+
return {
|
|
258
|
+
stdout: [
|
|
259
|
+
JSON.stringify({
|
|
260
|
+
schema: STATE_SCHEMA,
|
|
261
|
+
running: true,
|
|
262
|
+
pid: rec.pid,
|
|
263
|
+
...appField(known, trusted),
|
|
264
|
+
}),
|
|
265
|
+
],
|
|
266
|
+
stderr: [],
|
|
267
|
+
exitCode: 0,
|
|
268
|
+
};
|
|
269
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export declare function isUnsafeChar(ch: string): boolean;
|
|
2
|
+
/** Unsafe characters removed and the result capped at `max` code points. */
|
|
3
|
+
export declare function agentSafe(s: string, max: number): string;
|
|
4
|
+
/** Nesting kept in a value an agent reads; anything deeper is dropped (null). */
|
|
5
|
+
export declare const MAX_DEPTH = 8;
|
|
6
|
+
/** A JSON value with every string (keys included) made agent-safe, depth- and size-capped. */
|
|
7
|
+
export declare function agentSafeValue(v: unknown, max?: number, depth?: number): unknown;
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
// Text that reaches the creator's terminal or their agent from the editor's files. The app
|
|
2
|
+
// already strips these characters (B-F21-8); the CLI strips them again because it is the last
|
|
3
|
+
// step before a shell, and a stale or planted file never passed the app.
|
|
4
|
+
// C0 and C1 controls, DEL, the Arabic letter mark, zero-width characters and direction marks,
|
|
5
|
+
// line and paragraph separators, bidi embeddings, overrides and isolates, the word joiner,
|
|
6
|
+
// invisible operators, the byte order mark, the soft hyphen, the combining grapheme joiner,
|
|
7
|
+
// the Mongolian vowel separator, variation selectors and tag characters. Code points rather
|
|
8
|
+
// than a character class, so no bidi character sits in source.
|
|
9
|
+
const UNSAFE = [
|
|
10
|
+
[0x0000, 0x001f],
|
|
11
|
+
[0x007f, 0x009f],
|
|
12
|
+
[0x00ad, 0x00ad],
|
|
13
|
+
[0x034f, 0x034f],
|
|
14
|
+
[0x061c, 0x061c],
|
|
15
|
+
[0x180e, 0x180e],
|
|
16
|
+
[0x200b, 0x200f],
|
|
17
|
+
[0x2028, 0x2029],
|
|
18
|
+
[0x2060, 0x2064],
|
|
19
|
+
[0xfe00, 0xfe0f],
|
|
20
|
+
[0xfeff, 0xfeff],
|
|
21
|
+
[0x202a, 0x202e],
|
|
22
|
+
[0x2066, 0x2069],
|
|
23
|
+
[0xe0000, 0xe007f],
|
|
24
|
+
[0xe0100, 0xe01ef],
|
|
25
|
+
];
|
|
26
|
+
export function isUnsafeChar(ch) {
|
|
27
|
+
const cp = ch.codePointAt(0) ?? 0;
|
|
28
|
+
return UNSAFE.some(([lo, hi]) => cp >= lo && cp <= hi);
|
|
29
|
+
}
|
|
30
|
+
// CSI and OSC sequences, removed whole so no `[31m` residue is left behind.
|
|
31
|
+
// eslint-disable-next-line no-control-regex
|
|
32
|
+
const ANSI_G = /\u001b\[[0-?]*[ -/]*[@-~]|\u001b\][^\u0007\u001b]*(?:\u0007|\u001b\\)?|\u009b[0-?]*[ -/]*[@-~]/g;
|
|
33
|
+
/** Unsafe characters removed and the result capped at `max` code points. */
|
|
34
|
+
export function agentSafe(s, max) {
|
|
35
|
+
return Array.from(s.replace(ANSI_G, ""))
|
|
36
|
+
.filter((ch) => !isUnsafeChar(ch))
|
|
37
|
+
.slice(0, max)
|
|
38
|
+
.join("");
|
|
39
|
+
}
|
|
40
|
+
/** Nesting kept in a value an agent reads; anything deeper is dropped (null). */
|
|
41
|
+
export const MAX_DEPTH = 8;
|
|
42
|
+
/** A JSON value with every string (keys included) made agent-safe, depth- and size-capped. */
|
|
43
|
+
export function agentSafeValue(v, max = 200, depth = 0) {
|
|
44
|
+
if (typeof v === "string")
|
|
45
|
+
return agentSafe(v, max);
|
|
46
|
+
if (depth >= MAX_DEPTH && v !== null && typeof v === "object")
|
|
47
|
+
return null;
|
|
48
|
+
if (Array.isArray(v))
|
|
49
|
+
return v.slice(0, 64).map((x) => agentSafeValue(x, max, depth + 1));
|
|
50
|
+
if (v !== null && typeof v === "object") {
|
|
51
|
+
const out = {};
|
|
52
|
+
for (const [k, x] of Object.entries(v).slice(0, 256)) {
|
|
53
|
+
out[agentSafe(k, 80)] = agentSafeValue(x, max, depth + 1);
|
|
54
|
+
}
|
|
55
|
+
return out;
|
|
56
|
+
}
|
|
57
|
+
return v;
|
|
58
|
+
}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export type Tool = "codesign" | "open" | "sw_vers" | "plutil" | "lsappinfo" | "ps";
|
|
2
|
+
/** Where each tool lives, most likely first. Never a relative path; none off macOS. */
|
|
3
|
+
export declare function toolCandidates(tool: Tool, platform?: NodeJS.Platform): string[];
|
|
4
|
+
/** The tool's absolute path, or undefined when it is not where the OS keeps it. */
|
|
5
|
+
export declare function toolPath(tool: Tool, platform?: NodeJS.Platform, exists?: (p: string) => boolean): string | undefined;
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
// The macOS tools the launcher runs, by absolute path only. A bare name would be resolved from
|
|
2
|
+
// PATH, so a project (or anything earlier on PATH) could plant a `codesign` that answers "this
|
|
3
|
+
// app is signed by Vincentt".
|
|
4
|
+
import { existsSync } from "node:fs";
|
|
5
|
+
const DARWIN = {
|
|
6
|
+
ps: ["/bin/ps"],
|
|
7
|
+
codesign: ["/usr/bin/codesign"],
|
|
8
|
+
open: ["/usr/bin/open"],
|
|
9
|
+
sw_vers: ["/usr/bin/sw_vers"],
|
|
10
|
+
plutil: ["/usr/bin/plutil"],
|
|
11
|
+
lsappinfo: ["/usr/bin/lsappinfo"],
|
|
12
|
+
};
|
|
13
|
+
/** Where each tool lives, most likely first. Never a relative path; none off macOS. */
|
|
14
|
+
export function toolCandidates(tool, platform = process.platform) {
|
|
15
|
+
return platform === "darwin" ? DARWIN[tool] : [];
|
|
16
|
+
}
|
|
17
|
+
/** The tool's absolute path, or undefined when it is not where the OS keeps it. */
|
|
18
|
+
export function toolPath(tool, platform = process.platform, exists = existsSync) {
|
|
19
|
+
return toolCandidates(tool, platform).find((p) => exists(p));
|
|
20
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vincentt-xr/cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.23.0",
|
|
4
4
|
"description": "Vincentt CLI — the vincentt command-line dev loop (login, create, publish, preview, diagnostics, feedback, relay)",
|
|
5
5
|
"license": "UNLICENSED",
|
|
6
6
|
"type": "module",
|
|
@@ -28,7 +28,7 @@
|
|
|
28
28
|
"qrcode": "^1.5.4",
|
|
29
29
|
"ws": "^8.18.0",
|
|
30
30
|
"zod": "^3.23.8",
|
|
31
|
-
"@vincentt-xr/harness": "3.
|
|
31
|
+
"@vincentt-xr/harness": "3.1.0"
|
|
32
32
|
},
|
|
33
33
|
"devDependencies": {
|
|
34
34
|
"@types/qrcode": "^1.5.5",
|