@shardflux/cli 0.15.0 → 0.15.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/CHANGELOG.md +21 -0
- package/README.md +5 -3
- package/dist/account-args.js +3 -3
- package/dist/account-commands.d.ts +2 -0
- package/dist/account-commands.js +18 -3
- package/dist/agent-commands.js +22 -8
- package/dist/agent-target-commands.d.ts +14 -0
- package/dist/agent-target-commands.js +73 -32
- package/dist/args.d.ts +1 -1
- package/dist/args.js +5 -2
- package/dist/commands.d.ts +1 -1
- package/dist/format.js +3 -1
- package/dist/main.js +30 -12
- package/dist/menubar.d.ts +57 -0
- package/dist/menubar.js +264 -45
- package/dist/registry.d.ts +48 -0
- package/dist/registry.js +102 -0
- package/dist/update-check.d.ts +9 -1
- package/dist/update-check.js +55 -17
- package/dist/update.d.ts +2 -0
- package/dist/update.js +26 -8
- package/package.json +2 -2
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,26 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.15.2
|
|
4
|
+
|
|
5
|
+
- Updates show up the moment they are published: the latest version comes from npm's `latest` tag (what
|
|
6
|
+
`npm i -g shardflux` installs), checked at most every 12 hours with its ETag. The API still says which versions it
|
|
7
|
+
supports, and can hold back a release. Offline, nothing is printed. `shard update --check` and the menu bar's update
|
|
8
|
+
check use the same versions.
|
|
9
|
+
- The menu bar keeps up with npm by itself: after `npm i -g shardflux`, every `shard` command compares the installed app
|
|
10
|
+
with the one the package carries (two Info.plist reads, no network) and, when that one is newer, brings it in in the
|
|
11
|
+
background: a stopped app is replaced on disk, a running one is left to update itself (menu bar 0.3.2+) or, for
|
|
12
|
+
earlier apps, restarted on the new version when none of its windows is open. `shard update` swaps in an app the
|
|
13
|
+
package already carries without another install. App versions compare as semver (0.3.10 after 0.3.9).
|
|
14
|
+
- Installs menu bar 0.3.2 (`@shardflux/menubar-darwin ^0.3.2`).
|
|
15
|
+
|
|
16
|
+
## 0.15.1
|
|
17
|
+
|
|
18
|
+
- `shard setup`, `shard signup` and `shard api-keys create` make keys with every workspace tool, `computer` included, so the saved key can watch and screenshot a workspace's desktop (`shard ws computer stream`, `screenshot`), as the first-run demo does. `shard setup` replaces a saved key from an earlier setup (every tool but `computer`) with one that has every tool; the old key keeps working until revoked. `--tool` lists `computer`.
|
|
19
|
+
|
|
20
|
+
- `shard agents open --in auto` opens a Claude conversation where you work: in Claude Desktop when it is installed (its own session, `claude://code/<session>`), else Claude Code in this terminal. With `--json` it prints `{target: "claude-desktop", url, app_bundle_id}` or `{target: "terminal", command}`. claude.ai opens with `--in web` (or a saved web target). Codex conversations open in the Codex app as before.
|
|
21
|
+
|
|
22
|
+
- `shard setup` and `shard signup` print "Starting your Codex remote device…" while they wait for it.
|
|
23
|
+
|
|
3
24
|
## 0.15.0
|
|
4
25
|
|
|
5
26
|
- The onboarding activity report includes the conversation's turn state for Claude and Codex, including while its VM is suspended.
|
package/README.md
CHANGED
|
@@ -258,10 +258,12 @@ shard auth totp recovery-codes
|
|
|
258
258
|
"<your name>'s organization" (`--org-name` to choose); with several it lists them;
|
|
259
259
|
2. the project: `--project`, the saved default, or the organization's only one; with none it creates
|
|
260
260
|
"Default" (`--project-name`);
|
|
261
|
-
3. an API key with every workspace tool (`--tool` repeatable to narrow it), named
|
|
261
|
+
3. an API key with every workspace tool, `computer` included (0.15.1+; `--tool` repeatable to narrow it), named
|
|
262
262
|
`shard-cli <hostname>` (`--key-name`), saved in the credentials file and checked against the API.
|
|
263
263
|
|
|
264
|
-
It is idempotent: a saved key that still works is kept (`--new-key` makes a new one).
|
|
264
|
+
It is idempotent: a saved key that still works is kept (`--new-key` makes a new one). A saved key from an earlier
|
|
265
|
+
`shard setup` (every tool but `computer`) is replaced with one that has every tool (0.15.1+); the old key keeps working
|
|
266
|
+
until you revoke it. It prints what
|
|
265
267
|
it found or created and the next command. The key is never printed; `shard env` prints it for tools
|
|
266
268
|
that read `SHARDFLUX_API_KEY`:
|
|
267
269
|
|
|
@@ -1261,7 +1263,7 @@ shard agents new --app claude --path ./ --prompt "What is wrong on this page?" -
|
|
|
1261
1263
|
|
|
1262
1264
|
`--image <file|->` (0.15.0+, repeatable) attaches up to five PNG, JPEG, GIF or WebP images of up to 10 MiB each to that first message. They are written into the conversation VM under `/home/user/.shardflux/attachments/` and named in the message: Claude Code attaches them as images and Codex opens them with its image viewer. `--image -` reads one image, or PNG images back to back, from standard input; use a file for `--prompt-file` when images come from standard input.
|
|
1263
1265
|
|
|
1264
|
-
Use `shard agents open <workspace> --in auto --json` to resolve
|
|
1266
|
+
Use `shard agents open <workspace> --in auto --json` to resolve where a conversation opens: a saved target first; then, for Claude (0.15.1+), the conversation itself in Claude Desktop when it is installed (`{"target":"claude-desktop","url":"claude://code/session_…","app_bundle_id":…}`), else Claude Code in the terminal (`{"target":"terminal","command":["shard","agents","open","<workspace>"]}`; without `--json` it attaches in this terminal). Codex opens its Cloud thread in the Codex app. claude.ai opens with `--in web`. The caller opens the `url` or app, or runs the command in a terminal. Explicit `--in terminal` retains terminal attachment. The menu uses this resolver for new and existing conversations, without provider URLs in Swift.
|
|
1265
1267
|
|
|
1266
1268
|
## Codex Cloud (0.15.0+, unreleased)
|
|
1267
1269
|
|
package/dist/account-args.js
CHANGED
|
@@ -58,7 +58,7 @@ export const ACCOUNT_COMMANDS = [
|
|
|
58
58
|
name: { type: 'string', value: '<display name>', description: 'Your name as others see it (setup names the organization after it).' },
|
|
59
59
|
codex: { type: 'boolean', description: 'Use the Codex login on this machine (default; --no-codex signs up with --email).' },
|
|
60
60
|
setup: { type: 'boolean', description: 'Run shard setup afterwards (default; --no-setup skips it).' },
|
|
61
|
-
tool: { type: 'string', multiple: true, value: '<tool>', description: 'Workspace tool the saved API key may use (repeatable; exec, files, pty, process, git, browser). Default: all.' },
|
|
61
|
+
tool: { type: 'string', multiple: true, value: '<tool>', description: 'Workspace tool the saved API key may use (repeatable; exec, files, pty, process, git, browser, computer). Default: all.' },
|
|
62
62
|
...ACCEPT_TERMS,
|
|
63
63
|
...SECOND_FACTOR,
|
|
64
64
|
},
|
|
@@ -210,7 +210,7 @@ export const ACCOUNT_COMMANDS = [
|
|
|
210
210
|
'org-name': { type: 'string', value: '<name>', description: 'Name of the organization to create when you have none.' },
|
|
211
211
|
'project-name': { type: 'string', value: '<name>', description: 'Name of the project to create when the organization has none (default Default).' },
|
|
212
212
|
'key-name': { type: 'string', value: '<name>', description: 'Name of the API key (default "shard-cli <hostname>").' },
|
|
213
|
-
tool: { type: 'string', multiple: true, value: '<tool>', description: 'Workspace tool the key may use (repeatable; exec, files, pty, process, git, browser). Default: all.' },
|
|
213
|
+
tool: { type: 'string', multiple: true, value: '<tool>', description: 'Workspace tool the key may use (repeatable; exec, files, pty, process, git, browser, computer). Default: all.' },
|
|
214
214
|
'new-key': { type: 'boolean', description: 'Create and save a new key even when the saved one works (the old key stays valid until revoked).' },
|
|
215
215
|
codex: { type: 'boolean', description: 'Set up the account Codex Cloud executor with this machine’s Codex login (default when signed in); --no-codex skips it.' },
|
|
216
216
|
},
|
|
@@ -281,7 +281,7 @@ export const ACCOUNT_COMMANDS = [
|
|
|
281
281
|
description: 'Owners and admins grant workspace tools; billing members create keys without tools. The secret cannot be shown again.',
|
|
282
282
|
positionals: [{ name: 'name', required: true }],
|
|
283
283
|
options: {
|
|
284
|
-
tool: { type: 'string', multiple: true, value: '<tool>', description: 'Workspace tool the key may use (repeatable). Default: all
|
|
284
|
+
tool: { type: 'string', multiple: true, value: '<tool>', description: 'Workspace tool the key may use (repeatable; exec, files, pty, process, git, browser, computer). Default: all.' },
|
|
285
285
|
'read-only': { type: 'boolean', description: 'No workspace tools (read-only API use).' },
|
|
286
286
|
expires: { type: 'string', value: '<30d|90d|365d|never>', description: 'Expiry (default never).' },
|
|
287
287
|
save: { type: 'boolean', description: 'Save it in the credentials file for the workspace commands instead of printing it.' },
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import type { Handler } from './commands.js';
|
|
2
2
|
export declare const DEFAULT_API_URL = "https://api.shardflux.dev";
|
|
3
|
+
/** What `shard setup` gave a key before 0.15.1: every tool but `computer`. Such a saved key is replaced with a full one. */
|
|
4
|
+
export declare function outdatedDefaultKey(permissions: readonly string[]): boolean;
|
|
3
5
|
/**
|
|
4
6
|
* The organization name setup creates: "<display name or email local part>'s organization", within the API's rules
|
|
5
7
|
* for names (1-200 characters, no control characters or line breaks).
|
package/dist/account-commands.js
CHANGED
|
@@ -27,7 +27,15 @@ import { apiKeyTable, attachmentTable, auditTable, authSessionTable, billingStat
|
|
|
27
27
|
export const DEFAULT_API_URL = 'https://api.shardflux.dev';
|
|
28
28
|
const TOTP = /^\d{6}$/;
|
|
29
29
|
const ROLES = ['owner', 'admin', 'member', 'billing'];
|
|
30
|
-
|
|
30
|
+
/**
|
|
31
|
+
* Every tool permission a key can carry, `computer` included (0.15.1+): the SDK's list before 0.21.1 left it out, so a
|
|
32
|
+
* key made by `shard setup` could not watch or use its workspace's desktop (computer stream 403).
|
|
33
|
+
*/
|
|
34
|
+
const TOOL_NAMES = [...new Set([...API_KEY_TOOL_PERMISSIONS, 'computer'])];
|
|
35
|
+
/** What `shard setup` gave a key before 0.15.1: every tool but `computer`. Such a saved key is replaced with a full one. */
|
|
36
|
+
export function outdatedDefaultKey(permissions) {
|
|
37
|
+
return !permissions.includes('computer') && TOOL_NAMES.every((t) => t === 'computer' || permissions.includes(t));
|
|
38
|
+
}
|
|
31
39
|
const DEFAULT_CHECKOUT_WAIT_MS = 15 * 60_000;
|
|
32
40
|
const CHECKOUT_POLL_MS = 2_000;
|
|
33
41
|
const NEXT_WORKSPACE = 'shard ws open demo --template python-node-browser';
|
|
@@ -307,7 +315,7 @@ async function checkKey(ctx, token) {
|
|
|
307
315
|
const cloud = new Shardflux({ ...ctx.accountOptions(), apiKey: token });
|
|
308
316
|
const me = await cloud.me();
|
|
309
317
|
if (me.type === 'api_key' && me.api_key)
|
|
310
|
-
return { ok: true, organizationId: me.api_key.organization_id, projectId: me.api_key.project_id };
|
|
318
|
+
return { ok: true, organizationId: me.api_key.organization_id, projectId: me.api_key.project_id, toolPermissions: me.api_key.tool_permissions ?? [] };
|
|
311
319
|
return { ok: false, reason: 'not a project API key' };
|
|
312
320
|
}
|
|
313
321
|
catch (err) {
|
|
@@ -622,7 +630,12 @@ async function runSetup(ctx, values, account) {
|
|
|
622
630
|
else
|
|
623
631
|
throw new UsageError(`${org.name} has ${projects.length} projects; pick one with --project <id|slug>:\n${choices(projects)}\n`, 'or save a default first: shard projects use <id|slug>');
|
|
624
632
|
}
|
|
625
|
-
|
|
633
|
+
// A key from an earlier `shard setup` (every tool but computer) is replaced unless --tool asks for a set: the desktop
|
|
634
|
+
// stream and screenshots need `computer`. The old key stays valid until revoked.
|
|
635
|
+
const outdated = keyCheck?.ok === true && strs(values.tool).length === 0 && values['read-only'] !== true && outdatedDefaultKey(keyCheck.toolPermissions);
|
|
636
|
+
if (outdated && !ctx.json)
|
|
637
|
+
ctx.io.stderr.write(`note: the saved API key ${profile.api_key.key_id} cannot use computer use; creating one with every tool.\n`);
|
|
638
|
+
const keep = keyCheck?.ok === true && profile.api_key?.project_id === project.id && !outdated;
|
|
626
639
|
let key;
|
|
627
640
|
let permissions = tools;
|
|
628
641
|
if (keep)
|
|
@@ -655,6 +668,8 @@ async function runSetup(ctx, values, account) {
|
|
|
655
668
|
const cloud = new Shardflux({ apiKey: key.token, baseUrl: ctx.apiUrl });
|
|
656
669
|
await syncAgentCredentials(cloud, { home: credentialHome, redact: value => ctx.addSecret(value) });
|
|
657
670
|
await cloud.codex.enable({ signal: ctx.signal });
|
|
671
|
+
if (!ctx.json)
|
|
672
|
+
ctx.io.stderr.write('Starting your Codex remote device…\n');
|
|
658
673
|
codex = { ...await cloud.codex.waitConnector({ timeoutMs: ctx.wakeTimeoutMs, signal: ctx.signal }) };
|
|
659
674
|
}
|
|
660
675
|
const envNote = ctx.io.env.SHARDFLUX_API_KEY?.trim() ? 'note: SHARDFLUX_API_KEY is set in this environment and wins over the saved key; unset it to use the saved key.\n' : '';
|
package/dist/agent-commands.js
CHANGED
|
@@ -11,7 +11,7 @@ import { prepareProjectTemplate } from "./project-template-setup.js";
|
|
|
11
11
|
import { projectIdentity, projectLabels } from "./project-identity.js";
|
|
12
12
|
import { ensureProjectGit } from "./project-git.js";
|
|
13
13
|
import { json } from "./format.js";
|
|
14
|
-
import { resolveWorkspace } from "./commands.js";
|
|
14
|
+
import { out, resolveWorkspace } from "./commands.js";
|
|
15
15
|
import { savedMachine } from "./filesystem-state.js";
|
|
16
16
|
import { startAgentConfigTunnels, syncAgentConfig } from "./agent-config-sync.js";
|
|
17
17
|
import { rememberGitAgent } from "./agent-git-commands.js";
|
|
@@ -22,7 +22,7 @@ import { progress } from "./progress.js";
|
|
|
22
22
|
import { rememberLaunch } from "./agent-live.js";
|
|
23
23
|
import { prepareAgentMachine } from "./agent-machine.js";
|
|
24
24
|
import { installedTargets } from "./agent-targets.js";
|
|
25
|
-
import { openDesktopWorkspace, queueT3Prewarm } from "./agent-target-commands.js";
|
|
25
|
+
import { openClaudeDesktopSession, openDesktopWorkspace, queueT3Prewarm } from "./agent-target-commands.js";
|
|
26
26
|
import { submitInitialPrompt, prepareInitialCodexProject } from "./initial-prompt.js";
|
|
27
27
|
import { conversationWorkspace, conversationsUnavailable, getConversation, listConversations, resumeConversation } from "./conversations.js";
|
|
28
28
|
import { conversationTerminal } from "./conversation-terminal.js";
|
|
@@ -272,12 +272,26 @@ export const AGENT_HANDLERS = {
|
|
|
272
272
|
delete next.in;
|
|
273
273
|
return AGENT_HANDLERS['agents open'](ctx, next, [ws.key]);
|
|
274
274
|
}
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
275
|
+
if (saved && detected.targets.includes(saved)) {
|
|
276
|
+
const { HANDLERS } = await import("./handlers.js");
|
|
277
|
+
const handler = HANDLERS[`agents open ${saved}`];
|
|
278
|
+
if (!handler)
|
|
279
|
+
throw new UsageError('Choose an installed open target with shard agents targets.');
|
|
280
|
+
return handler(ctx, { ...values, in: saved }, [ws.key]);
|
|
281
|
+
}
|
|
282
|
+
// Claude opens where people work (0.15.1+): the conversation in Claude Desktop when it is installed, else Claude
|
|
283
|
+
// Code in a terminal. The browser (claude.ai) only when it is the chosen target (--in web).
|
|
284
|
+
if (detected.clients.includes('claude-desktop')) {
|
|
285
|
+
return openClaudeDesktopSession(ctx, ws);
|
|
286
|
+
}
|
|
287
|
+
if (ctx.json) {
|
|
288
|
+
// An app runs this in the person's terminal (the menu bar opens Terminal with it).
|
|
289
|
+
out(ctx, `shard agents open ${ws.key}\n`, { target: 'terminal', workspace: ws.data, command: ['shard', 'agents', 'open', ws.key] });
|
|
290
|
+
return 0;
|
|
291
|
+
}
|
|
292
|
+
const next = { ...values };
|
|
293
|
+
delete next.in;
|
|
294
|
+
return AGENT_HANDLERS['agents open'](ctx, next, [ws.key]);
|
|
281
295
|
}
|
|
282
296
|
if (values.in !== undefined && values.in !== 'terminal') {
|
|
283
297
|
const { HANDLERS } = await import("./handlers.js");
|
|
@@ -8,6 +8,20 @@ export declare function installDeviceKey(cell: CellClient, key: string, signal?:
|
|
|
8
8
|
/** Host trust is established over the authenticated files API, before sending any SSH protocol bytes. */
|
|
9
9
|
export declare function prepareSsh(ws: Workspace, ctx: Ctx, targets: AgentTargets, host: string): Promise<CellClient>;
|
|
10
10
|
export declare function proxyStream(io: Io, ended?: () => void, outputGate?: Promise<unknown>): Duplex;
|
|
11
|
+
/**
|
|
12
|
+
* The Remote Control URL (https://claude.ai/code/session_…) of the workspace's live Claude session. A Claude process
|
|
13
|
+
* this computer just started can accept its first prompt before Remote Control registers, so an open of our recent
|
|
14
|
+
* launch probes the guest for up to 30 s; history browsing stays passive.
|
|
15
|
+
*/
|
|
16
|
+
export declare function remoteControlUrl(ctx: Ctx, ws: Workspace): Promise<string>;
|
|
17
|
+
/** Claude Desktop's own link to a Remote Control session: claude://code/<session id> opens it in its Code tab. */
|
|
18
|
+
export declare function claudeDesktopLink(remoteControl: string): string;
|
|
19
|
+
export declare const CLAUDE_DESKTOP_BUNDLE = "com.anthropic.claudefordesktop";
|
|
20
|
+
/**
|
|
21
|
+
* Opens the conversation itself (its live Remote Control session) in Claude Desktop (0.15.1+), the way `--in auto`
|
|
22
|
+
* opens a Claude conversation when Claude Desktop is installed. --json prints {target, url, app_bundle_id} for apps.
|
|
23
|
+
*/
|
|
24
|
+
export declare function openClaudeDesktopSession(ctx: Ctx, ws: Workspace): Promise<number>;
|
|
11
25
|
export declare function openDesktopWorkspace(ctx: Ctx, ws: Workspace, target: DesktopTarget, t3Version?: string, prewarmed?: boolean): Promise<number>;
|
|
12
26
|
export declare function queueT3Prewarm(ctx: Ctx, ws: Workspace, version: string): Promise<void>;
|
|
13
27
|
export declare const TARGET_HANDLERS: Record<string, Handler>;
|
|
@@ -132,6 +132,78 @@ export function proxyStream(io, ended, outputGate) {
|
|
|
132
132
|
source.once('error', (error) => stream.destroy(error));
|
|
133
133
|
return stream;
|
|
134
134
|
}
|
|
135
|
+
/**
|
|
136
|
+
* The Remote Control URL (https://claude.ai/code/session_…) of the workspace's live Claude session. A Claude process
|
|
137
|
+
* this computer just started can accept its first prompt before Remote Control registers, so an open of our recent
|
|
138
|
+
* launch probes the guest for up to 30 s; history browsing stays passive.
|
|
139
|
+
*/
|
|
140
|
+
export async function remoteControlUrl(ctx, ws) {
|
|
141
|
+
let cursor;
|
|
142
|
+
let url;
|
|
143
|
+
do {
|
|
144
|
+
const page = await ctx.cloud().agents.list({ limit: 200, ...(cursor ? { cursor } : {}) });
|
|
145
|
+
url = page.data.find(a => a.workspace_id === ws.id && a.app === 'claude' && a.state !== 'exited' && a.remote_control_url)?.remote_control_url;
|
|
146
|
+
cursor = page.nextCursor ?? undefined;
|
|
147
|
+
} while (!url && cursor && !ctx.signal.aborted);
|
|
148
|
+
// A new Claude process can accept its first prompt before Remote Control registers.
|
|
149
|
+
// Only an explicit open of our recent launch probes the guest; history browsing stays passive.
|
|
150
|
+
const launchedAt = new AgentStateStore(ctx.store.dir).read().agent_launches?.[ws.id];
|
|
151
|
+
if (!url && typeof launchedAt === 'number' && Date.now() - launchedAt < LAUNCH_WINDOW_MS && ws.ready) {
|
|
152
|
+
const waitMs = Math.min(ctx.wakeTimeoutMs, RECENT_LAUNCH_URL_WAIT_MS);
|
|
153
|
+
const deadline = Date.now() + waitMs;
|
|
154
|
+
const cell = ws.cell({ agentLabel: ctx.agentLabel });
|
|
155
|
+
while (!url && Date.now() < deadline) {
|
|
156
|
+
const result = await cell.exec.run(['sf-agent', 'list'], {
|
|
157
|
+
timeoutMs: Math.min(10_000, Math.max(1, deadline - Date.now())), signal: ctx.signal, maxOutputBytes: 1 << 20,
|
|
158
|
+
});
|
|
159
|
+
if (result.exitCode !== 0)
|
|
160
|
+
break;
|
|
161
|
+
url = guestSessions(result.stdout).find(a => a.app === 'claude' && a.state !== 'exited' && a.remote_control_url)?.remote_control_url;
|
|
162
|
+
if (!url && Date.now() < deadline)
|
|
163
|
+
await delay(Math.min(250, Math.max(1, deadline - Date.now())), undefined, { signal: ctx.signal });
|
|
164
|
+
}
|
|
165
|
+
if (!url)
|
|
166
|
+
throw new Error(`Claude in ${ws.key} did not publish a Remote Control URL within ${Math.ceil(waitMs / 1000)} s; run shard agents open ${ws.key} again`);
|
|
167
|
+
}
|
|
168
|
+
if (!url)
|
|
169
|
+
throw new UsageError(`${ws.key} has no active Remote Control URL; start Claude with Remote Control first`);
|
|
170
|
+
const parsed = new URL(url);
|
|
171
|
+
if (parsed.protocol !== 'https:' || parsed.hostname !== 'claude.ai' || !parsed.pathname.startsWith('/code/') || parsed.username || parsed.password)
|
|
172
|
+
throw new Error('agent returned an invalid Claude Remote Control URL');
|
|
173
|
+
return url;
|
|
174
|
+
}
|
|
175
|
+
/** Claude Desktop's own link to a Remote Control session: claude://code/<session id> opens it in its Code tab. */
|
|
176
|
+
export function claudeDesktopLink(remoteControl) {
|
|
177
|
+
const id = new URL(remoteControl).pathname.split('/')[2] ?? '';
|
|
178
|
+
if (!/^(cse|session)_[A-Za-z0-9_-]+$/.test(id))
|
|
179
|
+
throw new Error('agent returned an invalid Claude Remote Control URL');
|
|
180
|
+
return `claude://code/${id}`;
|
|
181
|
+
}
|
|
182
|
+
export const CLAUDE_DESKTOP_BUNDLE = 'com.anthropic.claudefordesktop';
|
|
183
|
+
/**
|
|
184
|
+
* Opens the conversation itself (its live Remote Control session) in Claude Desktop (0.15.1+), the way `--in auto`
|
|
185
|
+
* opens a Claude conversation when Claude Desktop is installed. --json prints {target, url, app_bundle_id} for apps.
|
|
186
|
+
*/
|
|
187
|
+
export async function openClaudeDesktopSession(ctx, ws) {
|
|
188
|
+
await ensureCloudClaude(ctx, ws);
|
|
189
|
+
const session = await remoteControlUrl(ctx, ws);
|
|
190
|
+
const url = claudeDesktopLink(session);
|
|
191
|
+
if (!ctx.json)
|
|
192
|
+
await openLink(url, ctx.io.env);
|
|
193
|
+
out(ctx, `${url}\n`, { target: 'claude-desktop', workspace: ws.data, url, remote_control_url: session, app_bundle_id: CLAUDE_DESKTOP_BUNDLE });
|
|
194
|
+
return 0;
|
|
195
|
+
}
|
|
196
|
+
async function openLink(url, env) {
|
|
197
|
+
const child = spawn('open', [url], { env, stdio: 'ignore' });
|
|
198
|
+
await new Promise((resolve, reject) => {
|
|
199
|
+
const timer = setTimeout(() => { child.kill('SIGKILL'); reject(new Error('Claude Desktop did not open in time')); }, 10_000);
|
|
200
|
+
child.once('error', e => { clearTimeout(timer); reject(e); });
|
|
201
|
+
child.once('exit', code => { clearTimeout(timer); if (code === 0)
|
|
202
|
+
resolve();
|
|
203
|
+
else
|
|
204
|
+
reject(new Error(`could not open ${url}`)); });
|
|
205
|
+
});
|
|
206
|
+
}
|
|
135
207
|
const openTarget = (target) => async (ctx, values, [ref = 'main']) => {
|
|
136
208
|
const detected = installedTargets(ctx.io.env);
|
|
137
209
|
if (target !== 'web' && !detected.targets.includes(target))
|
|
@@ -142,38 +214,7 @@ const openTarget = (target) => async (ctx, values, [ref = 'main']) => {
|
|
|
142
214
|
if (target !== 'codex-app')
|
|
143
215
|
await ensureCloudClaude(ctx, ws);
|
|
144
216
|
if (target === 'web') {
|
|
145
|
-
|
|
146
|
-
let url;
|
|
147
|
-
do {
|
|
148
|
-
const page = await ctx.cloud().agents.list({ limit: 200, ...(cursor ? { cursor } : {}) });
|
|
149
|
-
url = page.data.find(a => a.workspace_id === ws.id && a.app === 'claude' && a.state !== 'exited' && a.remote_control_url)?.remote_control_url;
|
|
150
|
-
cursor = page.nextCursor ?? undefined;
|
|
151
|
-
} while (!url && cursor && !ctx.signal.aborted);
|
|
152
|
-
// A new Claude process can accept its first prompt before Remote Control registers.
|
|
153
|
-
// Only an explicit open of our recent launch probes the guest; history browsing stays passive.
|
|
154
|
-
const launchedAt = new AgentStateStore(ctx.store.dir).read().agent_launches?.[ws.id];
|
|
155
|
-
if (!url && typeof launchedAt === 'number' && Date.now() - launchedAt < LAUNCH_WINDOW_MS && ws.ready) {
|
|
156
|
-
const waitMs = Math.min(ctx.wakeTimeoutMs, RECENT_LAUNCH_URL_WAIT_MS);
|
|
157
|
-
const deadline = Date.now() + waitMs;
|
|
158
|
-
const cell = ws.cell({ agentLabel: ctx.agentLabel });
|
|
159
|
-
while (!url && Date.now() < deadline) {
|
|
160
|
-
const result = await cell.exec.run(['sf-agent', 'list'], {
|
|
161
|
-
timeoutMs: Math.min(10_000, Math.max(1, deadline - Date.now())), signal: ctx.signal, maxOutputBytes: 1 << 20,
|
|
162
|
-
});
|
|
163
|
-
if (result.exitCode !== 0)
|
|
164
|
-
break;
|
|
165
|
-
url = guestSessions(result.stdout).find(a => a.app === 'claude' && a.state !== 'exited' && a.remote_control_url)?.remote_control_url;
|
|
166
|
-
if (!url && Date.now() < deadline)
|
|
167
|
-
await delay(Math.min(250, Math.max(1, deadline - Date.now())), undefined, { signal: ctx.signal });
|
|
168
|
-
}
|
|
169
|
-
if (!url)
|
|
170
|
-
throw new Error(`Claude in ${ws.key} did not publish a Remote Control URL within ${Math.ceil(waitMs / 1000)} s; run shard agents open ${ws.key} again`);
|
|
171
|
-
}
|
|
172
|
-
if (!url)
|
|
173
|
-
throw new UsageError(`${ws.key} has no active Remote Control URL; start Claude with Remote Control first`);
|
|
174
|
-
const parsed = new URL(url);
|
|
175
|
-
if (parsed.protocol !== 'https:' || parsed.hostname !== 'claude.ai' || !parsed.pathname.startsWith('/code/') || parsed.username || parsed.password)
|
|
176
|
-
throw new Error('agent returned an invalid Claude Remote Control URL');
|
|
217
|
+
const url = await remoteControlUrl(ctx, ws);
|
|
177
218
|
if (!ctx.json && !openUrl(url, ctx.io.env))
|
|
178
219
|
throw new Error(`could not open ${url}`);
|
|
179
220
|
out(ctx, `${url}\n`, { target, workspace: ws.data, url });
|
package/dist/args.d.ts
CHANGED
package/dist/args.js
CHANGED
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
import { parseArgs } from 'node:util';
|
|
13
13
|
import { ACCOUNT_COMMANDS } from "./account-args.js";
|
|
14
14
|
import { UsageError } from "./errors.js";
|
|
15
|
-
export const CLI_VERSION = '0.15.
|
|
15
|
+
export const CLI_VERSION = '0.15.2';
|
|
16
16
|
export const GLOBAL_OPTIONS = {
|
|
17
17
|
'api-url': { type: 'string', value: '<url>', description: 'API base URL (default $SHARDFLUX_API_URL, else https://api.shardflux.dev).' },
|
|
18
18
|
json: { type: 'boolean', description: 'Print machine-readable JSON on stdout (errors as JSON on stderr).' },
|
|
@@ -1207,7 +1207,7 @@ const AGENT_COMMANDS = [
|
|
|
1207
1207
|
},
|
|
1208
1208
|
})),
|
|
1209
1209
|
...['status', 'enable', 'disable'].map(action => ({ path: ['agents', 'codex', action], summary: `${action} the account's billed Codex Cloud connector`, positionals: [], options: {} })),
|
|
1210
|
-
{ path: ['agents', 'open'], summary: 'Open a conversation (Codex Cloud or the native Claude session)', description: 'Codex opens its existing Cloud thread without waking its VM. Claude attaches its native session.', positionals: [{ name: 'agent', required: true }], options: { ...AGENT_OPTIONS, app: { type: 'string', value: '<claude|codex>', description: 'Agent app (default remembered choice).' }, in: { type: 'string', value: '<terminal|auto|claude-desktop|codex-app|web|t3>', description: 'Where to open a
|
|
1210
|
+
{ path: ['agents', 'open'], summary: 'Open a conversation (Codex Cloud or the native Claude session)', description: 'Codex opens its existing Cloud thread without waking its VM. Claude attaches its native session.', positionals: [{ name: 'agent', required: true }], options: { ...AGENT_OPTIONS, app: { type: 'string', value: '<claude|codex>', description: 'Agent app (default remembered choice).' }, in: { type: 'string', value: '<terminal|auto|claude-desktop|codex-app|web|t3>', description: 'Where to open it. auto (0.15.1+): the saved target, else a Claude conversation in Claude Desktop when it is installed, else in this terminal; web opens claude.ai.' } } },
|
|
1211
1211
|
{ path: ['agents', 'new'], summary: 'Start a conversation in a fresh VM; Codex opens a genuine Cloud thread', positionals: [{ name: 'agent' }], options: {
|
|
1212
1212
|
...AGENT_OPTIONS,
|
|
1213
1213
|
computer: COMPUTER_OPTION,
|
|
@@ -1407,6 +1407,9 @@ export function parseCommandLine(argv, options = {}) {
|
|
|
1407
1407
|
// `acp --login` is the ACP terminal sign-in an editor runs (ACP appends the method's args to `shard acp`).
|
|
1408
1408
|
if (argv.length === 2 && argv[0] === 'acp' && argv[1] === '--login')
|
|
1409
1409
|
return { kind: 'run', command: { path: ['acp'], summary: '', positionals: [], options: {} }, values: { login: true }, positionals: [], global: { apiUrl: undefined, json: false, agentLabel: undefined, wake: undefined, wakeTimeout: undefined } };
|
|
1410
|
+
// `menubar refresh` (0.15.2+, not in help): the background step each command may start (menubar.ts).
|
|
1411
|
+
if (argv.length === 2 && argv[0] === 'menubar' && argv[1] === 'refresh')
|
|
1412
|
+
return { kind: 'run', command: { path: ['menubar', 'refresh'], summary: '', positionals: [], options: {} }, values: {}, positionals: [], global: { apiUrl: undefined, json: false, agentLabel: undefined, wake: undefined, wakeTimeout: undefined } };
|
|
1410
1413
|
if (argv.length === 1 && (argv[0] === 'acp' || argv[0] === 'device-helper'))
|
|
1411
1414
|
return { kind: 'run', command: { path: [argv[0]], summary: '', positionals: [], options: {} }, values: {}, positionals: [], global: { apiUrl: undefined, json: false, agentLabel: undefined, wake: undefined, wakeTimeout: undefined } };
|
|
1412
1415
|
const terminator = argv.indexOf('--');
|
package/dist/commands.d.ts
CHANGED
|
@@ -66,7 +66,7 @@ export interface Ctx {
|
|
|
66
66
|
distribution: Distribution;
|
|
67
67
|
/** The Node.js binary and script of this process; null when run in-process (tests, embedders). */
|
|
68
68
|
runtime: import('./update.js').Runtime | null;
|
|
69
|
-
/** GET /v1/client-versions (
|
|
69
|
+
/** Every client's versions: GET /v1/client-versions (minimum_supported, hold) with `latest` from npm (0.15.2+). */
|
|
70
70
|
clientVersions(): Promise<unknown>;
|
|
71
71
|
/** This machine's name (default API key name of `shard setup`). */
|
|
72
72
|
hostname(): string;
|
package/dist/format.js
CHANGED
|
@@ -813,10 +813,12 @@ export function keyStatus(k, now = Date.now()) {
|
|
|
813
813
|
return 'expired';
|
|
814
814
|
return 'active';
|
|
815
815
|
}
|
|
816
|
+
/** Every workspace tool a key can carry (`computer` since the API's computer use). */
|
|
817
|
+
const ALL_TOOLS = ['exec', 'files', 'pty', 'process', 'git', 'browser', 'computer'];
|
|
816
818
|
export function toolsText(tools) {
|
|
817
819
|
if (tools.length === 0)
|
|
818
820
|
return '(none: read-only)';
|
|
819
|
-
return
|
|
821
|
+
return ALL_TOOLS.every((t) => tools.includes(t)) ? 'all' : tools.join(',');
|
|
820
822
|
}
|
|
821
823
|
export function apiKeyTable(rows, savedId) {
|
|
822
824
|
return table(['', 'ID', 'KEY ID', 'NAME', 'TOOLS', 'STATUS', 'CREATED', 'EXPIRES', 'LAST USED'], rows.map((k) => [mark(k.id === savedId), k.id, k.key_id, k.name, toolsText(k.tool_permissions), keyStatus(k), time(k.created_at), time(k.expires_at), time(k.last_used_at)]));
|
package/dist/main.js
CHANGED
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
* (step-up, password change, TOTP change) is saved at once; a saved session the API rejects (401) is removed.
|
|
11
11
|
* Every credential the command loaded is redacted from what it prints on failure.
|
|
12
12
|
*/
|
|
13
|
-
import { hostname } from 'node:os';
|
|
13
|
+
import { homedir, hostname } from 'node:os';
|
|
14
14
|
import { statSync } from 'node:fs';
|
|
15
15
|
import { SDK_VERSION, Shardflux, ShardfluxAccount, isSessionToken } from '@shardflux/sdk';
|
|
16
16
|
import { CLI_VERSION, CENTER_COMMAND, PROJECT_COMMAND, commandHelp, parseCommandLine, resolveApiUrl, resolveWake, resolveWakeTimeout } from "./args.js";
|
|
@@ -23,6 +23,8 @@ import { runDeviceHelper } from "./device-helper.js";
|
|
|
23
23
|
import { housekeeping, startupHousekeeping } from "./agent-git-commands.js";
|
|
24
24
|
import { CredentialStore, configDir } from "./credentials.js";
|
|
25
25
|
import { recordInstall } from "./update.js";
|
|
26
|
+
import { refreshMenubarInBackground, runMenubarRefresh } from "./menubar.js";
|
|
27
|
+
import { NPM_CLIENTS, npmLatestAll, npmRegistry, withRegistryLatest } from "./registry.js";
|
|
26
28
|
import { AuthConfigError, EXIT, ExecutionPendingError, InterruptedError, UsageError, describeError, exitCodeFor, ownHint, redact } from "./errors.js";
|
|
27
29
|
import { errorText, feedbackCommand, json, time } from "./format.js";
|
|
28
30
|
import { HANDLERS } from "./handlers.js";
|
|
@@ -540,12 +542,20 @@ export async function run(argv, io, opts = {}) {
|
|
|
540
542
|
distribution,
|
|
541
543
|
runtime: opts.runtime ?? null,
|
|
542
544
|
async clientVersions() {
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
545
|
+
// The latest versions come from npm (0.15.2+); the API adds minimum_supported and hold. Either one is enough.
|
|
546
|
+
const catalog = (async () => {
|
|
547
|
+
const res = await base(new URL('/v1/client-versions', `${apiUrl.replace(/\/+$/, '')}/`), {
|
|
548
|
+
headers: { accept: 'application/json', 'user-agent': userAgent }, signal: AbortSignal.any([signal, AbortSignal.timeout(10_000)]),
|
|
549
|
+
});
|
|
550
|
+
if (!res.ok)
|
|
551
|
+
throw new UsageError(`Could not read the latest versions from ${apiUrl} (HTTP ${res.status}).`);
|
|
552
|
+
const body = await res.json();
|
|
553
|
+
return body;
|
|
554
|
+
})();
|
|
555
|
+
const [api, latest] = await Promise.all([catalog.then((body) => ({ body }), (error) => ({ error })), npmLatestAll(NPM_CLIENTS, { fetch: base, registry: npmRegistry(io.env), userAgent, signal })]);
|
|
556
|
+
if ('error' in api && Object.keys(latest).length === 0)
|
|
557
|
+
throw api.error;
|
|
558
|
+
return withRegistryLatest('body' in api ? api.body : null, latest);
|
|
549
559
|
},
|
|
550
560
|
hostname: () => {
|
|
551
561
|
try {
|
|
@@ -624,7 +634,7 @@ export async function run(argv, io, opts = {}) {
|
|
|
624
634
|
secrets.push(value);
|
|
625
635
|
},
|
|
626
636
|
async versionStatus(force) {
|
|
627
|
-
const check = new UpdateCheck({ dir: store.dir, apiUrl, distribution, fetch: base, userAgent });
|
|
637
|
+
const check = new UpdateCheck({ dir: store.dir, apiUrl, distribution, fetch: base, userAgent, registry: npmRegistry(io.env) });
|
|
628
638
|
if (!force && updateCheckDisabled(io.env)) {
|
|
629
639
|
const cached = check.peek();
|
|
630
640
|
return cached ? updateView(cached.status, cached.checkedAt) : null;
|
|
@@ -634,10 +644,18 @@ export async function run(argv, io, opts = {}) {
|
|
|
634
644
|
return r ? updateView(r.status, r.checkedAt) : null;
|
|
635
645
|
},
|
|
636
646
|
};
|
|
647
|
+
// After `npm i -g shardflux` the package may carry a newer menu bar app: brought in by a detached process (no
|
|
648
|
+
// network; an Info.plist compare decides whether to start it).
|
|
649
|
+
// `shard update` brings the app in itself; `shard update --check` (the menu bar of an app before 0.3.2 runs it every
|
|
650
|
+
// 6 hours) is how such an app learns of a newer one with no action from the person.
|
|
651
|
+
const updating = command === 'update' && parsed.values.check !== true;
|
|
652
|
+
if (opts.runtime && !updating && !['menubar', 'device-helper'].includes(parsed.command.path[0])) {
|
|
653
|
+
refreshMenubarInBackground({ dir: store.dir, home: io.env.HOME || homedir(), runtime: opts.runtime, env: { ...process.env, ...io.env } });
|
|
654
|
+
}
|
|
637
655
|
// The update check runs next to the command (not for `version`, which reports it itself).
|
|
638
|
-
if (!updateCheckDisabled(io.env) && !['version', 'update', 'ssh-proxy', 'acp', 'device-helper'].includes(parsed.command.path[0])) {
|
|
656
|
+
if (!updateCheckDisabled(io.env) && !['version', 'update', 'ssh-proxy', 'acp', 'device-helper'].includes(parsed.command.path[0]) && command !== 'menubar refresh') {
|
|
639
657
|
try {
|
|
640
|
-
updates = new UpdateCheck({ dir: store.dir, apiUrl, distribution, fetch: base, userAgent });
|
|
658
|
+
updates = new UpdateCheck({ dir: store.dir, apiUrl, distribution, fetch: base, userAgent, registry: npmRegistry(io.env) });
|
|
641
659
|
updates.start();
|
|
642
660
|
}
|
|
643
661
|
catch {
|
|
@@ -648,10 +666,10 @@ export async function run(argv, io, opts = {}) {
|
|
|
648
666
|
catch (err) {
|
|
649
667
|
return report(err);
|
|
650
668
|
}
|
|
651
|
-
const handler = parsed.command.path[0] === 'acp' ? (ctx) => parsed.values.login === true ? runAcpLogin(ctx) : runAcp(ctx) : parsed.command.path[0] === 'device-helper' ? runDeviceHelper : parsed.command === PROJECT_COMMAND ? launchConversationProject : parsed.command === CENTER_COMMAND ? commandCenter : HANDLERS[parsed.command.path.join(' ')];
|
|
669
|
+
const handler = parsed.command.path[0] === 'acp' ? (ctx) => parsed.values.login === true ? runAcpLogin(ctx) : runAcp(ctx) : parsed.command.path[0] === 'device-helper' ? runDeviceHelper : command === 'menubar refresh' ? runMenubarRefresh : parsed.command === PROJECT_COMMAND ? launchConversationProject : parsed.command === CENTER_COMMAND ? commandCenter : HANDLERS[parsed.command.path.join(' ')];
|
|
652
670
|
if (!handler)
|
|
653
671
|
return report(new UsageError(`unknown command "${parsed.command.path.join(' ')}"`));
|
|
654
|
-
if (!['acp', 'device-helper'].includes(parsed.command.path[0]))
|
|
672
|
+
if (!['acp', 'device-helper'].includes(parsed.command.path[0]) && command !== 'menubar refresh')
|
|
655
673
|
await housekeeping(ctx);
|
|
656
674
|
const running = handler(ctx, parsed.values, parsed.positionals).then((code) => ({ code }), (err) => ({ err }));
|
|
657
675
|
// On Ctrl-C give the command a moment to clean up (exec sends a cancel), then stop regardless.
|
package/dist/menubar.d.ts
CHANGED
|
@@ -17,6 +17,24 @@ export interface MenubarStatus {
|
|
|
17
17
|
}
|
|
18
18
|
type NativeStatus = Omit<MenubarStatus, 'installed' | 'version'>;
|
|
19
19
|
export declare function compareBundleVersions(left: string, right: string): number;
|
|
20
|
+
/**
|
|
21
|
+
* Marketing versions (CFBundleShortVersionString) compare as semver: 0.3.10 > 0.3.9, 0.4.0-rc.1 < 0.4.0. A version that
|
|
22
|
+
* is not semver compares as a numeric bundle version.
|
|
23
|
+
*/
|
|
24
|
+
export declare function compareAppVersions(left: string, right: string): number;
|
|
25
|
+
/**
|
|
26
|
+
* The first app that swaps itself in when the npm package next to the CLI carries a newer one (it knows when no window
|
|
27
|
+
* is open and nothing is in progress). The CLI leaves a running app of this version or later to do that itself.
|
|
28
|
+
*/
|
|
29
|
+
export declare const SELF_UPDATING_APP = "0.3.2";
|
|
30
|
+
/** CFBundleShortVersionString of a Shardflux app from its XML Info.plist, read without any tool (null: not readable). */
|
|
31
|
+
export declare function plistVersion(app: string): string | null;
|
|
32
|
+
/**
|
|
33
|
+
* Whether any of these processes shows a window: the menu, the launcher, Settings, onboarding, an approval. The status
|
|
34
|
+
* item itself (a strip at the top of the screen on the status layer, owned by the app before macOS 26) does not count.
|
|
35
|
+
* Null when the probe could not answer: then nothing may be replaced.
|
|
36
|
+
*/
|
|
37
|
+
export declare function windowsOpen(text: string): boolean | null;
|
|
20
38
|
export declare function parseNativeStatus(text: string): NativeStatus;
|
|
21
39
|
/**
|
|
22
40
|
* The app's executables keep their execute permission (0.13.6+). @shardflux/menubar-darwin 0.1.1 and 0.1.2 reached npm
|
|
@@ -37,13 +55,52 @@ export declare class MenubarManager {
|
|
|
37
55
|
get platform(): NodeJS.Platform;
|
|
38
56
|
/** The version of the app this CLI's npm package carries (what `start` installs). */
|
|
39
57
|
packagedVersion(): Promise<string>;
|
|
58
|
+
/** Whether the app shows any window (see windowsOpen); null when that could not be read. */
|
|
59
|
+
windowsOpen(pids: number[]): Promise<boolean | null>;
|
|
40
60
|
status(): Promise<MenubarStatus>;
|
|
41
61
|
stop(): Promise<MenubarStatus>;
|
|
42
62
|
login(value: 'on' | 'off'): Promise<MenubarStatus>;
|
|
43
63
|
start(configDir: string, apiUrl: string): Promise<MenubarStatus>;
|
|
64
|
+
/**
|
|
65
|
+
* Brings an installed app up to the one this CLI's npm package carries, without disturbing the person: nothing when
|
|
66
|
+
* the versions are equal (each swap of an ad-hoc signed app costs its privacy grants), the copy on disk alone when it
|
|
67
|
+
* is not running, and a running app only when it shows no window and no `shard update` runs. A running app that
|
|
68
|
+
* swaps itself in (SELF_UPDATING_APP or later) is left to do so: it also knows when it is in the middle of something.
|
|
69
|
+
*/
|
|
70
|
+
refresh(configDir: string, apiUrl: string): Promise<{
|
|
71
|
+
result: 'current' | 'not_installed' | 'installed' | 'restarted' | 'deferred' | 'busy';
|
|
72
|
+
from: string | null;
|
|
73
|
+
to: string | null;
|
|
74
|
+
}>;
|
|
44
75
|
}
|
|
45
76
|
/** On macOS, `shard` brings the menu bar up by itself (with launch at login) unless the user ran `shard menubar stop`.
|
|
46
77
|
* It never fails the command center: a missing optional package or a refused start just leaves it off. */
|
|
47
78
|
export declare function ensureMenubar(dir: string, apiUrl: string, manager?: MenubarManager, platform?: NodeJS.Platform): Promise<string | undefined>;
|
|
79
|
+
/** How often a command may start a background refresh while the app is busy (a window open). */
|
|
80
|
+
export declare const REFRESH_RETRY_MS: number;
|
|
81
|
+
/** Where this CLI's npm package keeps the app it carries, or null when the optional package is missing. */
|
|
82
|
+
export declare function packagedAppPath(): string | null;
|
|
83
|
+
export interface BackgroundRefreshOptions {
|
|
84
|
+
dir: string;
|
|
85
|
+
home: string;
|
|
86
|
+
/** This `shard` process: the refresh runs the same Node.js and script. */
|
|
87
|
+
runtime: {
|
|
88
|
+
node: string;
|
|
89
|
+
script: string;
|
|
90
|
+
};
|
|
91
|
+
env: Record<string, string | undefined>;
|
|
92
|
+
platform?: NodeJS.Platform;
|
|
93
|
+
packageApp?: () => string | null;
|
|
94
|
+
now?: () => number;
|
|
95
|
+
spawn?: (node: string, args: string[], env: Record<string, string | undefined>) => void;
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* Every `shard` command (0.15.2+): when the npm package carries a newer app than the installed one (two Info.plist
|
|
99
|
+
* reads, no network, no tool), `shard menubar refresh` runs detached, so the command never waits for it. It is started
|
|
100
|
+
* at most every 5 minutes while the app is busy, and not at all after `shard menubar stop`. Returns whether it started.
|
|
101
|
+
*/
|
|
102
|
+
export declare function refreshMenubarInBackground(o: BackgroundRefreshOptions): boolean;
|
|
103
|
+
/** `shard menubar refresh` (not listed in help, so not in HANDLERS): what refreshMenubarInBackground starts. */
|
|
104
|
+
export declare const runMenubarRefresh: Handler;
|
|
48
105
|
export declare const MENUBAR_HANDLERS: Record<string, Handler>;
|
|
49
106
|
export {};
|