Skip to content

Commit 4a1cf85

Browse files
thymikeeagent
andcommitted
refactor: migrate is to the request-bound device runtime (#1883)
* refactor: migrate is to the request-bound device runtime `is` declares the shared selector capture use, admits once from exact owner facts, refuses before binding, and binds exactly once. Its capability bucket, the static HarmonyOS/Web command sets that augmented it, and `requireCommandSupported` admission for `is` are gone; `'is'` leaves the `createSelectorRuntime` capability union. Admission now runs BEFORE the direct-iOS selector fast path. ADR 0019 requires resolve -> admit -> bind before anything in a `device-runtime` command's request path reaches the device, so that query becomes a fast path *within* an admitted request rather than a way around exact-owner facts. The rule is documented once, on `createBoundSelectorRuntime`, replacing the two duplicated call-site comments `get` and `is` were each carrying. Declared behaviour change: `is` takes the active-app plan split, so the facts decide per family. On iOS `appBundleId` is the XCUITest attach target — with no tracked app the runner's own process comes to the foreground, displaces the app under test, and the capture then answers confidently about the runner's own blank screen. An iOS `is` on a session with no tracked app is now a typed SESSION_NOT_FOUND refusal carrying the `open` hint. Refusing beats displacing-and-lying. Android captures the real launcher in that state and is unchanged, which is what the platform facts already encoded. The two Apple watchOS cells move from capability-admitted-then-runner-failure to a typed unavailable refusal, the same classification snapshot, diff, and get already landed. R37 is the new parametrized cutover row. `find` keeps `createSelectorRuntime` and its `requireCommandSupported` call, so `captureData` stays optional and `captureSnapshotWithInteractor` stays: this unit is not the last selector unit. * fix(is): a failing iOS assertion fails instead of exiting zero Reverses part of #557, on thymikee's explicit instruction. `is` is an assertion: the docs state it "exits non-zero on failure". The direct-iOS fast path broke that contract — it reported a failed predicate as a completed command, so on device $ agent-device is text id=… "Wrong Expected Text" Passed: is text (exit 0) because `{ok: true, pass: false}` reaches `isCliOutput`, which renders "Passed: is <predicate>" without reading `pass`. A failing assertion reported as success lets a replay run on past a broken state. Now: Error (COMMAND_FAILED): is text failed for selector id=…: expected="Wrong Expected Text" actual="Apple Account, …" (exit 1) The renderer needed no patch: a negative can no longer produce a success envelope, so it is correct by construction. Direction chosen deliberately. Making the two paths agree could have gone either way, and "an agent asked a question and got an answer" is a real argument for the other one. This follows the DOCUMENTED contract rather than merely the incumbent behaviour, and the alternative is a far larger change: a zero-exit `is` would alter every platform and path, break scripts that rely on it failing the shell, and needs its own PR, docs, and probably a major version. It is also already how `is hidden` and `is exists` behave end to end. PASSING assertion, and that arm still answers with zero captures (pinned). Only the negative falls through — what #557's own summary asked for, "preserving snapshot fallback for misses", refusing fallback only for hard failures like ambiguity. The fall-through was #557's own design, never armed: the `| null` return and the caller's `if (!payload) return null;` guard were unreachable. This makes that dead guard live. Measured on iPhone 17 (median of 9, warm daemon): predicate holds 0.14s / 0 snapshots, unchanged; predicate fails 0.25s / 1 snapshot. ~+0.11s on failing assertions only. Correctness gain beyond the envelope: the fast path evaluates a ONE-NODE tree, so `visible` cannot see the ancestor geometry a list row inherits and its negative can be wrong. Falling through re-asks the real tree and can turn a spurious negative into a pass. The #557 pin moved with its reasoning at the pin site. * fix(layering): let a cutover row state a data-only admission retirement Review blocker on #1883: R37 claimed `legacyRetirement.routeNames: ['WEB_QUERY_COMMANDS_WITH_IS', 'HARMONYOS_IS_SUPPORT']`. Neither identifier has ever existed. They satisfied the non-empty shape check while proving nothing — the vacuous registry claim AGENTS.md warns about, and a green gate that would stay green if the deletion were reverted. The cause was the model, not the row. Every `LegacyRetirementClaim` form names something that must NOT exist, which a row can always satisfy by inventing a name. `is` retired no module, route, or dispatch projection because it had none: its legacy admission was a capability bucket plus membership in two static platform command sets, so its real retirement is a DATA deletion the model could not express. Rather than patch around that with sentinels or a per-command policy file — both forbidden by the playbook — this generalizes the model. `staticCommandSets` names the sets themselves and is proven from both sides: each must still be DECLARED in production source, and must no longer list the command. A fictional set fails the first half; a skipped deletion fails the second. That is what an identifier-shaped claim cannot state. R37 now claims HARMONYOS_SUPPORTED_COMMANDS and WEB_QUERY_COMMANDS, which is the deletion it actually performed. Planted red, both halves, against the real gate: [R37 is-runtime-cutover] 2 violation(s): (is cutover row):1 — claims retired static command set 'WEB_QUERY_COMMANDS_WITH_IS', which no production source declares (is cutover row):1 — claims retired static command set 'HARMONYOS_IS_SUPPORT', which no production source declares [R37 is-runtime-cutover] 2 violation(s): src/core/capabilities.ts:59 — static command set WEB_QUERY_COMMANDS still admits is so the exact claim that shipped is now rejected by name, and so is restoring the membership it claims to have removed. Mechanism cases live with the other planted-row tests; layering goes 177 -> 181. * test(is): pin the exit-code guarantee independently of what answers the predicate Prep for the Blocker 1 retirement, which deletes `buildDirectIosIsResult` — the function the #557 reversal fixed. The reversal's guarantee must not evaporate with it, so it gets a case that does not know how the daemon decided. `is` is documented to "exit non-zero on failure". The reversal proved that at the JSON envelope; nothing pinned it at the CLI boundary, which is where the defect was actually visible (`Passed: is text`, exit 0). This asserts the CLI contract directly: a `predicate_failed` response exits 1 and never renders as passed. It survives the retirement untouched, because it asserts the outcome rather than the path. Planted red with the exact pre-#1739 envelope the shortcut produced (`{ok: true, data: {pass: false}}`): `exitSpy.calls` is `[]` — no exit call at all — so the case fails, which is the regression it exists to catch. Unpushed on purpose: the restack will carry it into the retirement cycle. * refactor(is): retire the direct-iOS selector shortcut thymikee's ruling (option b). `is` declares `device-runtime`, so its request path must reach the device only through the operations R37 declares. It did not: a simple iOS `id=`/`label=` target was answered by a direct XCUITest querySelector without any capture, ordered after admission but not executing through the seam. This is not retired because it was wrong. `wait` hypothesized that the degenerate one-node evaluation mis-answers `is visible` for off-viewport nodes, traced it through the code convincingly, then tested it on device and it did not reproduce — XCUITest's own query is conservative about visibility, so the degenerate evaluation never gets the chance. It is retired because it was an undeclared, unmeasured bypass that made R37's singularExecution claim false: the same class of untruth as the sentinel retirement names fixed in the previous commit. Declaring querySelector as a real operation instead was rejected for a concrete reason: offscreen-target-probe.ts consumes queryDirectIosSelector with a plain session and cannot take a bound operation, so declaring it now would ship it twice until Wave 5 moves the probe — the deferred-duplication shape that got get's read deferral overruled. It returns as a declared, fact-admitted, section 9-measured operation in the unit that also moves the probe. Retired: dispatchDirectIosSelectorIs, its call site, buildDirectIosIsResult, and resolveDirectIosSelectorQuery — each had exactly one caller, all on this path — plus the ResolvedDirectIosSelectorQuery type they orphaned and two imports. queryDirectIosSelector itself stays: the offscreen probe still consumes it and it remains single-copy. Latency cost, stated plainly and not softened: a held predicate on a simple iOS selector goes from ~0.14s with no capture to ~0.25s with one, measured as the median of 9 warm runs on iPhone 17. There is no fallback and no fast path. R37's comment finally describes the code: "every predicate answers from the resolved tree" was written while the shortcut existed. Its scope is now stated too, so it is not read as absolute — the Android foreground-blocker diagnostic still reaches adb on the failure path, where it cannot produce or change a verdict; that edge is pre-existing, co-owned with wait, and recorded as Wave 6 denominator work with R22's appState as its declared replacement. Seven tests lost their subject. Those whose only content was the shortcut's own mechanics are deleted; the outcome-level ones are retargeted and keep asserting what survives. --------- Co-authored-by: agent <agent@local>
1 parent a3978d4 commit 4a1cf85

16 files changed

Lines changed: 517 additions & 288 deletions

scripts/layering/check.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -36,8 +36,8 @@
3636
// composition file; premature implementation loading and forbidden cross-boundary edges fail (R13).
3737
// - Over COMMAND-ATOMIC RUNTIME CUTOVERS: one parametrized gate reads the migrated-command
3838
// table (appstate R22, shutdown R23, boot R20, apps R21, install/deploy R24-R27,
39-
// lifecycle R28-R31, devices R17, logs R14, network R15, record R16, snapshot R32, diff R33)
40-
// and proves each command keeps
39+
// lifecycle R28-R31, devices R17, logs R14, network R15, record R16, snapshot R32, diff R33,
40+
// viewport R34, get R36, is R37 — R35 reserved for find) and proves each command keeps
4141
// exactly one platform-execution path — retired routes, admission, modules, and widened
4242
// runtime access cannot coexist with its operation-fact-derived descriptor and handler.
4343
// - Over CONTRACTS PRODUCTION SOURCE: contracts owns vocabulary only — host, process, and timer

scripts/layering/runtime-command-cutover-model.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,17 @@ export type LegacyRetirementClaim = Readonly<{
2828
daemonOnlyProviderMethods?: readonly string[];
2929
/** `PlatformPlugin` facet keys retired with the legacy adapter. */
3030
pluginFacetKeys?: readonly string[];
31+
/**
32+
* Static platform command sets this command's admission DATA was removed from — the whole
33+
* retirement of a command whose legacy admission was a capability bucket plus set membership,
34+
* with no adapter module, route, or dispatch projection to name.
35+
*
36+
* Every other form above names something that must NOT exist, which a row can satisfy by
37+
* inventing a name that never existed. This one is two-sided and cannot: each named set must
38+
* still EXIST in production source, and must no longer list the command. A fictional set fails
39+
* the first half, a skipped deletion the second.
40+
*/
41+
staticCommandSets?: readonly string[];
3142
}>;
3243

3344
/**
@@ -157,6 +168,7 @@ const RETIREMENT_FORMS = [
157168
'daemonOnlyRouteNames',
158169
'daemonOnlyProviderMethods',
159170
'pluginFacetKeys',
171+
'staticCommandSets',
160172
] as const satisfies readonly (keyof LegacyRetirementClaim)[];
161173

162174
/**

scripts/layering/runtime-command-cutover-policy.test.ts

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -285,3 +285,57 @@ test('every shipped row states its claims', () => {
285285
[],
286286
);
287287
});
288+
289+
// A data-only admission retirement — a capability bucket plus static-set membership, with no
290+
// module, route, or dispatch projection to name as gone. Both halves are planted, because the
291+
// half that matters is the one an identifier-shaped claim cannot state: a set that never existed.
292+
const DATA_ONLY_ROW: MigratedCommandCutover = {
293+
...PLANTED_ROW,
294+
legacyRetirement: { staticCommandSets: ['WEB_QUERY_COMMANDS'] },
295+
};
296+
297+
test('a data-only retirement is a stated claim, so a row needs no invented identifier', () => {
298+
assert.deepEqual(cutoverRowDefects(DATA_ONLY_ROW), []);
299+
});
300+
301+
test('planted red: a row claiming a static command set that does not exist is rejected', () => {
302+
assert.deepEqual(
303+
summariesFor(
304+
PLANTED_RULE,
305+
[['src/core/capabilities.ts', `const WEB_QUERY_COMMANDS = ['find'];`]],
306+
[
307+
{
308+
...DATA_ONLY_ROW,
309+
legacyRetirement: { staticCommandSets: ['WEB_QUERY_COMMANDS_WITH_PLANTED'] },
310+
},
311+
],
312+
).filter((summary) => summary.includes('static command set')),
313+
[
314+
"(planted cutover row): claims retired static command set 'WEB_QUERY_COMMANDS_WITH_PLANTED', which no production source declares",
315+
],
316+
);
317+
});
318+
319+
test('planted red: a claimed static command set that still lists the command is rejected', () => {
320+
assert.deepEqual(
321+
summariesFor(
322+
PLANTED_RULE,
323+
[['src/core/capabilities.ts', `const WEB_QUERY_COMMANDS = ['find', 'planted'];`]],
324+
[DATA_ONLY_ROW],
325+
).filter((summary) => summary.includes('still admits')),
326+
['src/core/capabilities.ts: static command set WEB_QUERY_COMMANDS still admits planted'],
327+
);
328+
});
329+
330+
test('a claimed static command set that exists and dropped the command passes', () => {
331+
// Scoped to this column: the planted row's singular-execution claims are unrelated here and
332+
// have their own cases above.
333+
assert.deepEqual(
334+
summariesFor(
335+
PLANTED_RULE,
336+
[['src/core/capabilities.ts', `const WEB_QUERY_COMMANDS = ['find'];`]],
337+
[DATA_ONLY_ROW],
338+
).filter((summary) => summary.includes('static command set')),
339+
[],
340+
);
341+
});

scripts/layering/runtime-command-cutover-policy.ts

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,7 @@ function rowViolations(
7979
violations.push(...narrowingViolations(row, file, program));
8080
}
8181
violations.push(...exactCallViolations(row, files, programs));
82+
violations.push(...staticCommandSetViolations(row, files, programs));
8283
const sources = new Map(files.map(({ path, source }) => [path, source]));
8384
for (const check of rowChecks(row)) violations.push(...check(sources));
8485
return violations;
@@ -356,6 +357,59 @@ function isAdmissionMember(
356357
);
357358
}
358359

360+
/**
361+
* A data-only admission retirement, proven from both sides.
362+
*
363+
* A command whose legacy admission was a capability bucket plus membership in a static platform
364+
* command set retires no module, route, or dispatch projection — there is no identifier to name
365+
* as gone. Naming an invented one satisfies the non-empty shape check while proving nothing, so
366+
* the row names the sets themselves: each must still be DECLARED in production source, and must
367+
* no longer carry this command.
368+
*
369+
* The existence half is what an identifier-shaped claim cannot express. The membership half
370+
* overlaps the automatic static-set column for `WEB`/`HARMONY`-named sets, deliberately: stating
371+
* it here keeps the declared claim self-sufficient rather than dependent on that regex.
372+
*/
373+
function staticCommandSetViolations(
374+
row: MigratedCommandCutover,
375+
files: readonly ProductionSource[],
376+
programs: ReadonlyMap<string, AstNode>,
377+
): UnruledViolation[] {
378+
const declared = row.legacyRetirement.staticCommandSets ?? [];
379+
if (declared.length === 0) return [];
380+
const violations: UnruledViolation[] = [];
381+
const seen = new Set<string>();
382+
for (const file of files) {
383+
const program = programs.get(file.path);
384+
if (!program) continue;
385+
visitAst(program, (node) => {
386+
const name = staticCommandSetName(node, declared);
387+
if (name === undefined) return;
388+
seen.add(name);
389+
if (containsStringLiteral(node['init'], row.command)) {
390+
violations.push(at(file, node, `static command set ${name} still admits ${row.command}`));
391+
}
392+
});
393+
}
394+
for (const name of declared) {
395+
if (seen.has(name)) continue;
396+
violations.push({
397+
file: `(${row.command} cutover row)`,
398+
line: 1,
399+
message: `claims retired static command set '${name}', which no production source declares`,
400+
});
401+
}
402+
return violations;
403+
}
404+
405+
function staticCommandSetName(node: AstNode, declared: readonly string[]): string | undefined {
406+
if (node['type'] !== 'VariableDeclarator') return undefined;
407+
const id = node['id'] as AstNode | undefined;
408+
if (id?.['type'] !== 'Identifier') return undefined;
409+
const name = String(id['name']);
410+
return declared.includes(name) ? name : undefined;
411+
}
412+
359413
function containsStringLiteral(node: unknown, expected: string): boolean {
360414
let found = false;
361415
visitAst(node, (candidate) => {

scripts/layering/runtime-command-cutover-table.ts

Lines changed: 39 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,8 @@ import { retiredDispatchProjectionViolations } from './runtime-command-cutover-d
2525
* A row id is a report heading, so it must be unique across every stack that adds rows here.
2626
* `cutoverTableDefects` rejects a duplicate; lifecycle starts at R28 after the accepted
2727
* shutdown, install/deploy, and application-lifecycle allocations. Snapshot starts at R32;
28-
* diff follows at R33, viewport at R34, and get at R36 (R35 is reserved for find).
28+
* diff follows at R33, viewport at R34, get at R36, and is at R37. R35 stays reserved for
29+
* find, whose cutover is deferred behind the Wave 5 `focus`/`type` surfaces.
2930
*/
3031
export const MIGRATED_COMMAND_CUTOVERS: readonly MigratedCommandCutover[] = [
3132
{
@@ -548,6 +549,43 @@ export const MIGRATED_COMMAND_CUTOVERS: readonly MigratedCommandCutover[] = [
548549
},
549550
},
550551
},
552+
{
553+
rule: 'R37 is-runtime-cutover',
554+
command: 'is',
555+
subject: 'element predicate',
556+
tier: 'request-scoped',
557+
execution: 'device-runtime',
558+
// `is` retired no module, route, or dispatch projection — it had none. Its whole legacy
559+
// admission was the capability bucket (rejected by this row's automatic descriptor column)
560+
// plus membership in these two static sets, which is a DATA deletion. Naming the sets proves
561+
// it from both sides: each must still be declared in production source and must no longer
562+
// list `is`, so neither an invented name nor a skipped deletion can satisfy it.
563+
legacyRetirement: {
564+
staticCommandSets: ['HARMONYOS_SUPPORTED_COMMANDS', 'WEB_QUERY_COMMANDS'],
565+
},
566+
runtimeTypeNames: ['SnapshotRuntimeOperations'],
567+
operations: { names: ['captureSnapshot', 'captureSnapshotWithoutActiveApp'] },
568+
singularExecution: {
569+
routes: ['dispatchIsViaRuntime'],
570+
operations: ['captureSnapshot', 'captureSnapshotWithoutActiveApp'],
571+
// `is` executes through the shared selector seam, so its capture owners are the SAME
572+
// selectors `snapshot`/`diff`/`get` count. It declares no operation of its own: every
573+
// predicate answers from the resolved tree, so `readTextAtPoint` stays R36's alone.
574+
//
575+
// Scope, stated so this is not read as absolute: the claim covers how a predicate is
576+
// EXECUTED. Since the direct-iOS selector shortcut retired, the bound capture is the only
577+
// thing that answers one. It does NOT claim the route makes no other device call — the
578+
// Android foreground-blocker diagnostic still reaches adb through
579+
// `platforms/android/app-lifecycle.ts`, on the FAILURE path only, where it can enrich an
580+
// already-failed response's message but can never produce or change a verdict. That edge
581+
// is pre-existing, co-owned with `wait`, and recorded as Wave 6 denominator work; R22's
582+
// `appState` is its declared replacement.
583+
operationOwners: {
584+
captureSnapshot: ['selectActiveAppSnapshot'],
585+
captureSnapshotWithoutActiveApp: ['selectSnapshotWithoutActiveApp'],
586+
},
587+
},
588+
},
551589
{
552590
rule: 'R34 viewport-runtime-cutover',
553591
command: 'viewport',

src/__tests__/cli-exit-paths.test.ts

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -222,3 +222,39 @@ test('a --debug failure caps the daemon-log-tail dump instead of printing it unb
222222
'expected the byte cap to drop the oldest lines, not just the 200-line cap',
223223
);
224224
});
225+
226+
// The end-to-end half of `is`'s documented contract: "is evaluates UI predicates against a
227+
// selector expression and exits non-zero on failure" (website/docs/docs/commands.md).
228+
//
229+
// This deliberately does NOT know how the daemon decided. It was written when the direct-iOS
230+
// shortcut answered some predicates itself and returned `{ok: true, pass: false}`, which the CLI
231+
// rendered as `Passed: is text` with exit 0 (#1739). The shortcut is retired and every predicate
232+
// now answers from the bound capture, so the guarantee is structural rather than guard-based —
233+
// and this case survives that change untouched, because a failed assertion must exit non-zero
234+
// whatever produced the failure.
235+
test('a failed `is` predicate exits non-zero, whatever answered it', async () => {
236+
const restoreEnv = installIsolatedCliTestEnv();
237+
const exitSpy = installExitSpy();
238+
const stderr = captureStderr();
239+
const sendToDaemon = async (): Promise<DaemonResponse> => ({
240+
ok: false,
241+
error: {
242+
code: 'COMMAND_FAILED',
243+
message: 'is text failed for selector id=greeting: expected="Welcome" actual="Goodbye"',
244+
details: { command: 'is', reason: 'predicate_failed', predicate: 'text' },
245+
},
246+
});
247+
248+
try {
249+
await runCli(['is', 'text', 'id=greeting', 'Welcome'], { sendToDaemon });
250+
} finally {
251+
stderr.restore();
252+
exitSpy.restore();
253+
restoreEnv();
254+
}
255+
256+
assert.deepEqual(exitSpy.calls, [1]);
257+
const output = stderr.read();
258+
assert.ok(output.includes('COMMAND_FAILED'), 'expected the typed failure on stderr');
259+
assert.ok(!output.includes('Passed'), 'a failed assertion must never render as passed');
260+
});

src/core/__tests__/capabilities.test.ts

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -232,7 +232,6 @@ test('macOS supports the Apple runner interaction core but excludes mobile-only
232232
'find',
233233
'focus',
234234
'get',
235-
'is',
236235
'longpress',
237236
'logs',
238237
'perf',
@@ -306,7 +305,6 @@ test('Linux supports desktop interaction commands and blocks mobile/unsupported
306305
'focus',
307306
'get',
308307
'home',
309-
'is',
310308
'longpress',
311309
'press',
312310
'screenshot',
@@ -334,7 +332,6 @@ test('web supports only the initial browser interaction slice', () => {
334332
'find',
335333
'get',
336334
'hover',
337-
'is',
338335
'press',
339336
'record',
340337
'screenshot',

src/core/__tests__/capability-plugin-routing-parity.test.ts

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -180,11 +180,9 @@ const HARMONYOS_SUPPORTED_COMMANDS_REF = new Set([
180180
'fill',
181181
'find',
182182
'focus',
183-
'get',
184183
'home',
185184
'gesture',
186185
'keyboard',
187-
'is',
188186
'longpress',
189187
'press',
190188
'screenshot',
@@ -271,7 +269,6 @@ test('HarmonyOS static capabilities omit runtime-backed command admissions', ()
271269
'focus',
272270
'gesture',
273271
'home',
274-
'is',
275272
'keyboard',
276273
'longpress',
277274
'perf',

src/core/capabilities.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,6 @@ const HARMONYOS_SUPPORTED_COMMANDS = new Set<string>([
4747
'home',
4848
'gesture',
4949
'keyboard',
50-
'is',
5150
'longpress',
5251
'press',
5352
'scroll',
@@ -56,7 +55,7 @@ const HARMONYOS_SUPPORTED_COMMANDS = new Set<string>([
5655
'type',
5756
'wait',
5857
]);
59-
const WEB_QUERY_COMMANDS = ['audio', 'find', 'is', 'wait'] as const;
58+
const WEB_QUERY_COMMANDS = ['audio', 'find', 'wait'] as const;
6059
const WEB_INTERACTION_COMMANDS = [
6160
'click',
6261
'fill',

src/core/command-descriptor/__tests__/parity.test.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,7 @@ const NO_CAPABILITY_PUBLIC_COMMANDS = new Set<string>([
6060
PUBLIC_COMMANDS.get,
6161
PUBLIC_COMMANDS.install,
6262
PUBLIC_COMMANDS.installFromSource,
63+
PUBLIC_COMMANDS.is,
6364
PUBLIC_COMMANDS.logs,
6465
PUBLIC_COMMANDS.network,
6566
PUBLIC_COMMANDS.open,

0 commit comments

Comments
 (0)