@pellux/goodvibes-daemon 1.28.18 → 1.28.20
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 +100 -63
- package/README.md +13 -13
- package/bin/launcher-support.js +5 -5
- package/package.json +6 -4
- package/scripts/postinstall.js +8 -8
- package/src/cli/command-catalog.ts +22 -22
- package/src/cli/completion.ts +4 -4
- package/src/cli/help.ts +5 -5
- package/src/cli/index.ts +3 -3
- package/src/cli/parser.ts +2 -2
- package/src/cli/surface-catalog.ts +1 -1
- package/src/cli/types.ts +2 -2
- package/src/cluster/daemon-ws-call.ts +5 -5
- package/src/cluster/raw-reply-route.ts +5 -5
- package/src/config/checkpoint-settings.ts +7 -7
- package/src/config/config-key-guard.ts +22 -0
- package/src/config/run-daemon-config-migration.ts +3 -3
- package/src/config/secret-config.ts +7 -7
- package/src/config/surface.ts +3 -3
- package/src/core/pairing-banner.ts +5 -5
- package/src/daemon/cli.ts +45 -43
- package/src/daemon/config-command.ts +15 -15
- package/src/daemon/handlers/context.ts +1 -1
- package/src/daemon/handlers/contracts.ts +4 -4
- package/src/daemon/handlers/credentials.ts +1 -1
- package/src/daemon/handlers/drafts/draft-store.ts +3 -3
- package/src/daemon/handlers/drafts/register.ts +4 -4
- package/src/daemon/handlers/inbox/aggregator.ts +8 -8
- package/src/daemon/handlers/inbox/cursor-store.ts +10 -10
- package/src/daemon/handlers/inbox/index.ts +7 -7
- package/src/daemon/handlers/inbox/mapping.ts +2 -2
- package/src/daemon/handlers/inbox/poller.ts +5 -5
- package/src/daemon/handlers/inbox/provider-adapter.ts +8 -8
- package/src/daemon/handlers/inbox/providers/discord.ts +6 -6
- package/src/daemon/handlers/inbox/providers/email.ts +3 -3
- package/src/daemon/handlers/inbox/providers/imap-client.ts +1 -1
- package/src/daemon/handlers/inbox/providers/slack.ts +4 -4
- package/src/daemon/handlers/index.ts +18 -8
- package/src/daemon/handlers/payments/card-store.ts +486 -0
- package/src/daemon/handlers/payments/index.ts +32 -0
- package/src/daemon/handlers/payments/purchase-ledger.ts +108 -0
- package/src/daemon/handlers/payments/register.ts +391 -0
- package/src/daemon/handlers/register.ts +3 -3
- package/src/daemon/handlers/remote/backends/cloud-terminal.ts +9 -1
- package/src/daemon/handlers/remote/backends/process-runner.ts +1 -1
- package/src/daemon/handlers/remote/backends/ssh.ts +9 -1
- package/src/daemon/handlers/remote/backends/types.ts +2 -2
- package/src/daemon/handlers/remote/dispatcher.ts +3 -3
- package/src/daemon/handlers/remote/index.ts +1 -1
- package/src/daemon/handlers/remote/peer-registry.ts +62 -13
- package/src/daemon/handlers/routing/inbox-bridge.ts +5 -5
- package/src/daemon/handlers/routing/index.ts +1 -1
- package/src/daemon/handlers/routing/route-store.ts +1 -1
- package/src/daemon/handlers/routing/routing-resolver.ts +3 -3
- package/src/daemon/handlers/sqlite-store.ts +9 -9
- package/src/daemon/handlers/triage/index.ts +1 -1
- package/src/daemon/handlers/triage/integration.ts +3 -3
- package/src/daemon/handlers/triage/pipeline.ts +2 -2
- package/src/daemon/handlers/triage/scorer.ts +2 -2
- package/src/daemon/handlers/triage/tagger/discord.ts +3 -3
- package/src/daemon/handlers/triage/tagger/imap.ts +7 -7
- package/src/daemon/handlers/triage/tagger/index.ts +1 -1
- package/src/daemon/handlers/triage/tagger/shared.ts +3 -3
- package/src/daemon/handlers/triage/tagger/slack.ts +1 -1
- package/src/daemon/handlers/triage/types.ts +2 -2
- package/src/daemon/lifecycle.ts +5 -5
- package/src/daemon/local-daemon-state.ts +7 -7
- package/src/daemon/pair-command.ts +14 -14
- package/src/daemon/provision-wake-model.ts +5 -5
- package/src/daemon/send/channels.ts +7 -7
- package/src/daemon/send/command.ts +11 -11
- package/src/daemon/send/composition.ts +7 -7
- package/src/daemon/send/failure-text.ts +6 -6
- package/src/daemon/send/inert-text.ts +18 -18
- package/src/daemon/send/stdin.ts +3 -3
- package/src/daemon/service-commands.ts +32 -32
- package/src/daemon/sessions-command.ts +7 -7
- package/src/daemon/status-command.ts +22 -22
- package/src/daemon/webui-command.ts +14 -14
- package/src/runtime/boot-tasks.ts +1 -1
- package/src/runtime/cluster-composition.ts +9 -9
- package/src/runtime/cluster-group-composition.ts +7 -7
- package/src/runtime/conversation-rewind-port.ts +8 -8
- package/src/runtime/credential-composition.ts +2 -2
- package/src/runtime/daemon-handler-composition.ts +44 -4
- package/src/runtime/device-posture-composition.ts +10 -10
- package/src/runtime/disposal-wiring.ts +8 -8
- package/src/runtime/fleet-needs-input-push.ts +4 -4
- package/src/runtime/fleet-services.ts +1 -1
- package/src/runtime/hosted-session-composition.ts +13 -13
- package/src/runtime/index.ts +1 -1
- package/src/runtime/knowledge-services.ts +2 -2
- package/src/runtime/legacy-daemon-migration.ts +43 -43
- package/src/runtime/legacy-daemon-reconcile.ts +30 -30
- package/src/runtime/mail-composition.ts +6 -6
- package/src/runtime/notification-dispatch.ts +7 -7
- package/src/runtime/payments-composition.ts +143 -0
- package/src/runtime/plugin-composition.ts +7 -7
- package/src/runtime/runtime-services-types.ts +9 -9
- package/src/runtime/services.ts +31 -29
- package/src/runtime/trigger-services.ts +1 -1
- package/src/runtime/trust/checkpoint-eligibility.ts +5 -5
- package/src/runtime/trust/trust-gated-approvals.ts +9 -9
- package/src/runtime/update-check.ts +4 -4
- package/src/runtime/workspace-checkpointing.ts +6 -6
- package/src/testing/daemon-fixture.ts +11 -11
- package/src/testing/hosted-session-failures.ts +4 -4
- package/src/version.ts +2 -2
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* command.ts
|
|
2
|
+
* command.ts, `goodvibes-daemon send`, the shell's way to put a message on one
|
|
3
3
|
* of the owner's channels.
|
|
4
4
|
*
|
|
5
5
|
* ## Why this exists
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
* Nothing on this machine could send the owner a message from a script. The
|
|
8
8
|
* three binaries exposed no send verb, the daemon's HTTP API answers
|
|
9
9
|
* `401 AUTH_REQUIRED` to the operator token as stored, and driving the agent to
|
|
10
|
-
* do it failed with `Missing Telegram bot token
|
|
10
|
+
* do it failed with `Missing Telegram bot token`, because the credential lives
|
|
11
11
|
* in the DAEMON tier (`~/.goodvibes/daemon/settings.json`) and the agent was
|
|
12
12
|
* reading its own surface silo. So the one process that could always send was
|
|
13
13
|
* the daemon, and only while it was running.
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
* ## Three properties this command is built around
|
|
16
16
|
*
|
|
17
17
|
* 1. **It uses the delivery path, it is not a second sender.** Everything below
|
|
18
|
-
* ends in `ChannelDeliveryRouter.deliver()
|
|
18
|
+
* ends in `ChannelDeliveryRouter.deliver()`, the same call
|
|
19
19
|
* `AutomationDeliveryManager.sendTarget` makes, reaching the same
|
|
20
20
|
* per-surface strategies in `strategies-core.ts`. Nothing here talks to a
|
|
21
21
|
* provider API directly.
|
|
@@ -26,7 +26,7 @@
|
|
|
26
26
|
* was NOT used, deliberately: it returns an empty array when a feature gate
|
|
27
27
|
* is off and returns failed attempts rather than throwing, so a caller that
|
|
28
28
|
* did not inspect its result would report success for a message that never
|
|
29
|
-
* left the machine
|
|
29
|
+
* left the machine, the exact false-green this command exists to avoid. The
|
|
30
30
|
* gate check it would have done is done here instead, explicitly, and a gate
|
|
31
31
|
* that is off produces a refusal naming the settings key rather than silence.
|
|
32
32
|
*
|
|
@@ -34,8 +34,8 @@
|
|
|
34
34
|
* through `inertBodyFor` for the target surface before it reaches the router,
|
|
35
35
|
* and there is no flag, env var or code path that skips that. The message
|
|
36
36
|
* normally comes from the operator's own shell, but the command must not
|
|
37
|
-
* become the way something else's text
|
|
38
|
-
* remote agent's output piped in
|
|
37
|
+
* become the way something else's text, a log line, a captured error, a
|
|
38
|
+
* remote agent's output piped in, arrives on the owner's phone rendered as
|
|
39
39
|
* live markup with a clickable link in it. See inert-text.ts.
|
|
40
40
|
*/
|
|
41
41
|
|
|
@@ -51,7 +51,7 @@ import { inertBodyFor } from './inert-text.ts';
|
|
|
51
51
|
export type SendDeliver = (request: ChannelDeliveryRequest) => Promise<string | undefined>;
|
|
52
52
|
|
|
53
53
|
export interface SendCommandDeps {
|
|
54
|
-
/** Built with a `homeDir` so the daemon tier overlays
|
|
54
|
+
/** Built with a `homeDir` so the daemon tier overlays, see channels.ts. */
|
|
55
55
|
readonly configManager: Pick<ConfigManager, 'get'>;
|
|
56
56
|
readonly deliver: SendDeliver;
|
|
57
57
|
/** Reads the whole of stdin; only called when no message argument was given. */
|
|
@@ -93,12 +93,12 @@ const USAGE = [
|
|
|
93
93
|
' --list Show every channel, whether it is on, and where it sends.',
|
|
94
94
|
' -h, --help Print this help',
|
|
95
95
|
'',
|
|
96
|
-
'A channel that is switched off is refused by name and NOTHING is sent
|
|
96
|
+
'A channel that is switched off is refused by name and NOTHING is sent: the',
|
|
97
97
|
'command never quietly falls back to the default, so a message meant for a',
|
|
98
98
|
'quiet channel cannot end up on a noisy one.',
|
|
99
99
|
'',
|
|
100
100
|
'The message is always delivered as literal text: markup a channel would',
|
|
101
|
-
'otherwise render
|
|
101
|
+
'otherwise render (a Discord masked link, a Slack mention) arrives inert.',
|
|
102
102
|
].join('\n');
|
|
103
103
|
|
|
104
104
|
function parseSendArgs(argv: readonly string[]): ParsedSendArgs {
|
|
@@ -153,7 +153,7 @@ function describeConfiguredChannels(config: Pick<ConfigManager, 'get'>): string
|
|
|
153
153
|
.map((entry) => entry.channel.id);
|
|
154
154
|
return usable.length > 0
|
|
155
155
|
? `Configured and ready: ${usable.join(', ')}.`
|
|
156
|
-
: 'No channel is currently both switched on and given a destination
|
|
156
|
+
: 'No channel is currently both switched on and given a destination; run: goodvibes-daemon send --list';
|
|
157
157
|
}
|
|
158
158
|
|
|
159
159
|
function renderChannelList(config: Pick<ConfigManager, 'get'>): string[] {
|
|
@@ -304,7 +304,7 @@ export async function runSendCommand(
|
|
|
304
304
|
};
|
|
305
305
|
|
|
306
306
|
const preamble = usedDefault
|
|
307
|
-
? [`No channel named
|
|
307
|
+
? [`No channel named; using ${channel.id}: ${defaultReason}.`]
|
|
308
308
|
: [];
|
|
309
309
|
try {
|
|
310
310
|
const responseId = await deps.deliver(request);
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* composition.ts
|
|
2
|
+
* composition.ts, the smallest set of services that can put a message on a
|
|
3
3
|
* channel, built from the daemon's own tier.
|
|
4
4
|
*
|
|
5
5
|
* ## Why not `createRuntimeServices`
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
* The daemon's full runtime graph starts a LAN scan, a cluster coordinator, an
|
|
8
8
|
* inbox poller, a fleet tick, a memory governor and a config watch. Composing
|
|
9
9
|
* it to send one message would build a second, competing set of that state on a
|
|
10
|
-
* machine that is already running a daemon
|
|
10
|
+
* machine that is already running a daemon, the same reason `cluster …` is
|
|
11
11
|
* intercepted before any runtime is constructed (see src/daemon/cli.ts). This
|
|
12
12
|
* builds only the five objects `ChannelDeliveryRouter` needs and starts no
|
|
13
13
|
* timers, binds no sockets and joins no election, so it is safe to run beside a
|
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
* That was tried before this command existed: the control-plane API answers
|
|
19
19
|
* `401 AUTH_REQUIRED` to the operator token as stored on disk. More importantly
|
|
20
20
|
* an HTTP-backed send would only work while a daemon is up, and the case this
|
|
21
|
-
* command is for
|
|
21
|
+
* command is for, telling the owner that something has stopped, is exactly
|
|
22
22
|
* when it may not be.
|
|
23
23
|
*
|
|
24
24
|
* ## Where the credentials come from
|
|
@@ -34,7 +34,7 @@
|
|
|
34
34
|
|
|
35
35
|
import { ChannelDeliveryRouter } from '@pellux/goodvibes-sdk/platform/channels';
|
|
36
36
|
import { ArtifactStore } from '@pellux/goodvibes-sdk/platform/artifacts';
|
|
37
|
-
import { ConfigManager, ServiceRegistry, SubscriptionManager } from '@pellux/goodvibes-sdk/platform/config';
|
|
37
|
+
import { ConfigManager, ServiceRegistry, SubscriptionManager, sharedSubscriptionsPath } from '@pellux/goodvibes-sdk/platform/config';
|
|
38
38
|
import { createShellPathService } from '@/runtime/index.ts';
|
|
39
39
|
import { SecretsManager } from '../../config/secrets.ts';
|
|
40
40
|
import type { SendDeliver } from './command.ts';
|
|
@@ -42,7 +42,7 @@ import { GOODVIBES_DAEMON_SURFACE_ROOT } from '../../config/surface.ts';
|
|
|
42
42
|
|
|
43
43
|
export interface SendStackRoots {
|
|
44
44
|
readonly workingDirectory: string;
|
|
45
|
-
/** The GoodVibes tree root
|
|
45
|
+
/** The GoodVibes tree root, the directory `.goodvibes/` sits under. */
|
|
46
46
|
readonly homeDirectory: string;
|
|
47
47
|
/** The daemon's own state root, holding the daemon-scoped secret stores. */
|
|
48
48
|
readonly daemonHomeDirectory: string;
|
|
@@ -62,7 +62,7 @@ export interface SendStack {
|
|
|
62
62
|
* roots (goodvibes-tui and goodvibes-agent) omitted it, still type-checked,
|
|
63
63
|
* still delivered on every surface whose credential happens to sit in config or
|
|
64
64
|
* the environment, and failed only on the surfaces that use a secret reference
|
|
65
|
-
|
|
65
|
+
*, at send time, as `Missing Telegram bot token`. This composition root does
|
|
66
66
|
* not repeat that.
|
|
67
67
|
*/
|
|
68
68
|
export function createSendStack(roots: SendStackRoots): SendStack {
|
|
@@ -85,7 +85,7 @@ export function createSendStack(roots: SendStackRoots): SendStack {
|
|
|
85
85
|
});
|
|
86
86
|
const serviceRegistry = new ServiceRegistry(shellPaths.resolveProjectPath(GOODVIBES_DAEMON_SURFACE_ROOT, 'services.json'), {
|
|
87
87
|
secretsManager,
|
|
88
|
-
subscriptionManager: new SubscriptionManager(shellPaths.resolveUserPath(GOODVIBES_DAEMON_SURFACE_ROOT, 'subscriptions.json')),
|
|
88
|
+
subscriptionManager: new SubscriptionManager(sharedSubscriptionsPath(shellPaths), { legacyPath: shellPaths.resolveUserPath(GOODVIBES_DAEMON_SURFACE_ROOT, 'subscriptions.json') }),
|
|
89
89
|
});
|
|
90
90
|
const router = new ChannelDeliveryRouter({
|
|
91
91
|
configManager,
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* failure-text.ts
|
|
2
|
+
* failure-text.ts, what the operator is told when a send does not happen.
|
|
3
3
|
*
|
|
4
4
|
* ## Why this is not `summarizeError`
|
|
5
5
|
*
|
|
@@ -10,10 +10,10 @@
|
|
|
10
10
|
*
|
|
11
11
|
* Telegram delivery failed HTTP 401: {"ok":false,"description":"Unauthorized"}
|
|
12
12
|
*
|
|
13
|
-
* it returns `Telegram delivery failed HTTP 401
|
|
13
|
+
* it returns `Telegram delivery failed HTTP 401`, its `stripJson` pass removes
|
|
14
14
|
* the `{...}`, which is exactly the part naming what went wrong. Given
|
|
15
15
|
* `connect ECONNREFUSED 149.154.167.220:443` it returns "Cannot connect to the
|
|
16
|
-
* provider. Check whether the service is reachable."
|
|
16
|
+
* provider. Check whether the service is reachable.", advice in place of the
|
|
17
17
|
* address that was refused.
|
|
18
18
|
*
|
|
19
19
|
* A person debugging a message that did not arrive needs the provider's own
|
|
@@ -39,7 +39,7 @@ import { redactSensitiveData } from '@pellux/goodvibes-sdk/platform/utils';
|
|
|
39
39
|
|
|
40
40
|
/**
|
|
41
41
|
* A generous cap. The point of this text is diagnosis, so it is far larger than
|
|
42
|
-
* the 240 characters `summarizeError` allows
|
|
42
|
+
* the 240 characters `summarizeError` allows, but an HTML error page or a
|
|
43
43
|
* multi-megabyte body still must not flood the terminal.
|
|
44
44
|
*/
|
|
45
45
|
const MAX_FAILURE_TEXT = 2_000;
|
|
@@ -47,10 +47,10 @@ const MAX_FAILURE_TEXT = 2_000;
|
|
|
47
47
|
const URL_CREDENTIAL_PATTERNS: ReadonlyArray<{ readonly pattern: RegExp; readonly replacement: string }> = [
|
|
48
48
|
// Telegram: the bot token IS the path segment.
|
|
49
49
|
{ pattern: /\/bot\d{5,}:[A-Za-z0-9_-]{10,}/g, replacement: '/bot[REDACTED_BOT_TOKEN]' },
|
|
50
|
-
// Any URL carrying `user:password@host
|
|
50
|
+
// Any URL carrying `user:password@host`, BlueBubbles, Mattermost and Matrix
|
|
51
51
|
// base URLs are all operator-supplied and can be written this way.
|
|
52
52
|
{ pattern: /(\b[a-z][a-z0-9+.-]*:\/\/)[^/\s:@]+:[^/\s@]+@/gi, replacement: '$1[REDACTED_CREDENTIALS]@' },
|
|
53
|
-
// A credential passed as a query parameter
|
|
53
|
+
// A credential passed as a query parameter, BlueBubbles sends `?password=`.
|
|
54
54
|
{
|
|
55
55
|
pattern: /([?&](?:password|token|secret|access_token|api_?key|auth)=)[^&\s"']+/gi,
|
|
56
56
|
replacement: '$1[REDACTED]',
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* inert-text.ts
|
|
2
|
+
* inert-text.ts, make a message arrive as LITERAL TEXT on whichever channel
|
|
3
3
|
* `goodvibes-daemon send` delivers it to.
|
|
4
4
|
*
|
|
5
5
|
* ## What "inert" means here, and why it is per (surface × transport), not
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
*
|
|
13
13
|
* - Telegram's `sendMessage` CAN parse MarkdownV2, but the strategy sends with
|
|
14
14
|
* NO `parse_mode`, which is Telegram's plain-text mode. Running a MarkdownV2
|
|
15
|
-
* escaper over the body on this path would not protect anything
|
|
15
|
+
* escaper over the body on this path would not protect anything, it would
|
|
16
16
|
* put visible backslashes in front of every `.` `-` `!` `(` in the owner's
|
|
17
17
|
* message. The correct transform is identity, and the property that makes it
|
|
18
18
|
* correct is asserted by a test against the real wire payload rather than
|
|
@@ -29,7 +29,7 @@
|
|
|
29
29
|
*
|
|
30
30
|
* `escapeDiscordMarkdown` and `escapeSlackMrkdwn` are the escapers written for
|
|
31
31
|
* the inbound-mail structured notices, in the SDK at
|
|
32
|
-
* `packages/sdk/src/platform/email/inbound-notice.ts
|
|
32
|
+
* `packages/sdk/src/platform/email/inbound-notice.ts`, same character classes,
|
|
33
33
|
* same zero-width-space mention break, same rationale. They are reproduced here
|
|
34
34
|
* rather than imported for one reason: that module lives on the unmerged
|
|
35
35
|
* `inbound-email-*` branches and is absent from the published
|
|
@@ -72,8 +72,8 @@ function breakMentionForms(text: string): string {
|
|
|
72
72
|
*
|
|
73
73
|
* `[` `]` `(` `)` are escaped too, and this is REQUIRED rather than
|
|
74
74
|
* precautionary: masked links (`[text](url)`) DO render as clickable in
|
|
75
|
-
* bot-sent and webhook messages
|
|
76
|
-
* Discord
|
|
75
|
+
* bot-sent and webhook messages, which is exactly how this product delivers to
|
|
76
|
+
* Discord, even though they do not render for text a human typed into the
|
|
77
77
|
* client.
|
|
78
78
|
* https://github.com/discord/discord-api-docs/issues/6096
|
|
79
79
|
* https://gist.github.com/matthewzring/9f7bbfd102003963f9be7dbcf7d40e51
|
|
@@ -97,11 +97,11 @@ function escapeDiscordMarkdown(text: string): string {
|
|
|
97
97
|
* unescaped `<`.
|
|
98
98
|
*
|
|
99
99
|
* Slack has no backslash escape for `* _ ~ \``, so the zero-width break is
|
|
100
|
-
* applied to those as the best available mitigation
|
|
100
|
+
* applied to those as the best available mitigation, stated as a mitigation,
|
|
101
101
|
* not a guarantee, because Slack's whitespace-adjacency rule for what breaks a
|
|
102
102
|
* delimiter pair is not publicly specified to that precision. The residual risk
|
|
103
103
|
* is cosmetic (accidental bold/italic); the injection class this exists to
|
|
104
|
-
* close
|
|
104
|
+
* close, a clickable link or a real mention, is closed by the entity escaping
|
|
105
105
|
* above.
|
|
106
106
|
*/
|
|
107
107
|
function escapeSlackMrkdwn(text: string): string {
|
|
@@ -122,8 +122,8 @@ function escapeGoogleChatMarkup(text: string): string {
|
|
|
122
122
|
|
|
123
123
|
/**
|
|
124
124
|
* WhatsApp Cloud API text messages render `*bold*`, `_italic_`, `~strike~` and
|
|
125
|
-
* ```` ```mono``` ````. There is no masked-link syntax
|
|
126
|
-
* auto-linked showing its real address
|
|
125
|
+
* ```` ```mono``` ````. There is no masked-link syntax, a URL in the body is
|
|
126
|
+
* auto-linked showing its real address, so the injection class Discord has
|
|
127
127
|
* does not exist here and only the delimiters need neutralizing. WhatsApp
|
|
128
128
|
* documents no backslash escape, so the zero-width break is used.
|
|
129
129
|
*/
|
|
@@ -136,24 +136,24 @@ function escapeWhatsAppMarkup(text: string): string {
|
|
|
136
136
|
* markup. Every other routable surface is absent DELIBERATELY, and each absence
|
|
137
137
|
* is a checked claim about the strategy, not an oversight:
|
|
138
138
|
*
|
|
139
|
-
* - `telegram`
|
|
139
|
+
* - `telegram` , `sendMessage` without `parse_mode`; Telegram's plain-text
|
|
140
140
|
* mode. Escaping here would corrupt, not protect.
|
|
141
|
-
* - `ntfy`
|
|
141
|
+
* - `ntfy` , `publish` sends the body as `text/plain` and never sets the
|
|
142
142
|
* `Markdown` header, so ntfy renders it literally. The title
|
|
143
143
|
* is derived by `titleFromBody` (first non-empty LINE) and
|
|
144
144
|
* passed through `toHeaderSafeTitle`, so no line break or
|
|
145
145
|
* non-ASCII byte from the body can reach an HTTP header.
|
|
146
|
-
* - `webhook`
|
|
146
|
+
* - `webhook` , the body is a JSON string field; the receiver decides what
|
|
147
147
|
* to do with it and there is no markup layer to neutralize.
|
|
148
148
|
*
|
|
149
|
-
* - `signal`
|
|
149
|
+
* - `signal` , the strategy posts `text` to a signal bridge. Signal renders
|
|
150
150
|
* no markup in a plain message body; styling travels as
|
|
151
151
|
* explicit range metadata the strategy never sends.
|
|
152
152
|
* - `imessage`,
|
|
153
|
-
* `bluebubbles
|
|
154
|
-
* - `msteams`
|
|
153
|
+
* `bluebubbles`, both end at iMessage, which renders no markup at all.
|
|
154
|
+
* - `msteams` , the strategy sends `textFormat: 'plain'` alongside the text,
|
|
155
155
|
* which is Teams' own instruction not to parse it.
|
|
156
|
-
* - `matrix`
|
|
156
|
+
* - `matrix` , `msgtype: 'm.text'` with no `format`/`formatted_body`. A
|
|
157
157
|
* Matrix event without the HTML format field is rendered
|
|
158
158
|
* literally by clients, per the spec.
|
|
159
159
|
*
|
|
@@ -161,7 +161,7 @@ function escapeWhatsAppMarkup(text: string): string {
|
|
|
161
161
|
* `ControlPlaneGateway` in the same process, which a short-lived CLI does not
|
|
162
162
|
* have, so the command does not offer it as a channel at all. `telephony` is
|
|
163
163
|
* absent because a CLI `send` to it would place a phone call or an SMS through
|
|
164
|
-
* a paid carrier
|
|
164
|
+
* a paid carrier, a different act from messaging a channel, and because the
|
|
165
165
|
* strategy already XML-escapes the voice path itself via `escapeTwiml`.
|
|
166
166
|
*/
|
|
167
167
|
const INERT_TRANSFORMS: Partial<Record<ChannelDeliverySurfaceKind, (text: string) => string>> = {
|
|
@@ -173,7 +173,7 @@ const INERT_TRANSFORMS: Partial<Record<ChannelDeliverySurfaceKind, (text: string
|
|
|
173
173
|
'google-chat': escapeGoogleChatMarkup,
|
|
174
174
|
whatsapp: escapeWhatsAppMarkup,
|
|
175
175
|
// Delivered as plain text by the strategy: transforming would corrupt, not
|
|
176
|
-
// protect. Each of these is a checked claim about the strategy, not a guess
|
|
176
|
+
// protect. Each of these is a checked claim about the strategy, not a guess,
|
|
177
177
|
// see the list above.
|
|
178
178
|
telegram: (text) => text,
|
|
179
179
|
ntfy: (text) => text,
|
package/src/daemon/send/stdin.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* stdin.ts
|
|
2
|
+
* stdin.ts, read the whole of stdin as the message body.
|
|
3
3
|
*
|
|
4
4
|
* Kept out of command.ts so the command stays a pure function of its arguments
|
|
5
5
|
* and its injected dependencies: every test drives it with a stub rather than
|
|
@@ -10,8 +10,8 @@
|
|
|
10
10
|
* Read stdin to end, as UTF-8.
|
|
11
11
|
*
|
|
12
12
|
* There is deliberately no timeout. `send` reads stdin only when the operator
|
|
13
|
-
* gave no message argument AND stdin is not a terminal
|
|
14
|
-
* piping into it
|
|
13
|
+
* gave no message argument AND stdin is not a terminal, meaning something is
|
|
14
|
+
* piping into it, so waiting for that producer to finish is the correct
|
|
15
15
|
* behaviour, and cutting it off at an arbitrary deadline would silently
|
|
16
16
|
* truncate a long message.
|
|
17
17
|
*/
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Service-lifecycle subcommands
|
|
2
|
+
* Service-lifecycle subcommands, `goodvibes-daemon install-service |
|
|
3
3
|
* uninstall-service | service-status`.
|
|
4
4
|
*
|
|
5
5
|
* The daemon is a SYSTEM SERVICE. These subcommands install it as a durable host
|
|
@@ -9,8 +9,8 @@
|
|
|
9
9
|
*
|
|
10
10
|
* Drift note: the SDK's older `systemd-user-service.ts` (a Linux-only,
|
|
11
11
|
* bespoke systemd shim) was dead code and got deleted from the SDK. The SDK's
|
|
12
|
-
* REAL wired service machinery
|
|
13
|
-
* `/api/service/*` routes via facade-composition.ts
|
|
12
|
+
* REAL wired service machinery, reached in production by the daemon's own HTTP
|
|
13
|
+
* `/api/service/*` routes via facade-composition.ts, is
|
|
14
14
|
* `PlatformServiceManager` (`@pellux/goodvibes-sdk/platform/daemon`): a single
|
|
15
15
|
* systemd/launchd/windows-aware manager with install/uninstall/status/start/
|
|
16
16
|
* stop/restart and a `suggestedCommands` hint list. This module now rewires the
|
|
@@ -38,16 +38,16 @@
|
|
|
38
38
|
* honestly queries systemd via the injected actionRunner) BEFORE the
|
|
39
39
|
* legacy unit is stopped, disabled, or removed. A failed or unhealthy new
|
|
40
40
|
* unit rolls itself back (uninstalled) and never touches the legacy one
|
|
41
|
-
*
|
|
41
|
+
* , a botched takeover must never cost the user their working daemon.
|
|
42
42
|
* - ADOPT-OR-WARN, NEVER KILL. If the legacy unit file is simply absent but
|
|
43
43
|
* something is already listening on the configured host:port (Mike's real
|
|
44
44
|
* dev-host case: a manual `nohup`'d daemon with no unit at all), this is
|
|
45
|
-
* an unidentified process, not a managed unit
|
|
45
|
+
* an unidentified process, not a managed unit, there is nothing to stop
|
|
46
46
|
* or disable, and this module will not attempt to kill it. It warns and
|
|
47
47
|
* leaves the decision to the operator.
|
|
48
48
|
* - Every action (legacy stop/disable, unit-file removal, daemon-reload)
|
|
49
49
|
* goes through the SAME injectable `actionRunner`/`legacyUnitFileRemove`
|
|
50
|
-
* seams tests already use
|
|
50
|
+
* seams tests already use, this module never has a code path that bypasses
|
|
51
51
|
* them, so the migration is exercised deterministically via fakes and never
|
|
52
52
|
* touches a real running service in tests.
|
|
53
53
|
*/
|
|
@@ -67,7 +67,7 @@ import {
|
|
|
67
67
|
type ManagedServiceActionRunner,
|
|
68
68
|
} from '../runtime/legacy-daemon-migration.ts';
|
|
69
69
|
|
|
70
|
-
// `resolveInstalledDaemonBinary` lives in the runtime module
|
|
70
|
+
// `resolveInstalledDaemonBinary` lives in the runtime module, re-exported
|
|
71
71
|
// here so this module stays the CLI's stable public surface (and so existing
|
|
72
72
|
// test imports keep working).
|
|
73
73
|
export {
|
|
@@ -83,7 +83,7 @@ export type { ManagedServiceActionRunner } from '../runtime/legacy-daemon-migrat
|
|
|
83
83
|
// literal name `goodvibes-daemon.service`. This module (rewired onto
|
|
84
84
|
// PlatformServiceManager, see the file banner above) manages a DIFFERENT
|
|
85
85
|
// unit name (`goodvibes`, SERVICE_NAME). A host that still has the legacy
|
|
86
|
-
// unit installed
|
|
86
|
+
// unit installed, this dev machine included, would otherwise see
|
|
87
87
|
// service-status honestly report installed:false/running:false for the
|
|
88
88
|
// tracked name while the legacy unit keeps running untouched underneath it,
|
|
89
89
|
// uninstall-service would silently leave the legacy unit orphaned with no
|
|
@@ -94,13 +94,13 @@ export type { ManagedServiceActionRunner } from '../runtime/legacy-daemon-migrat
|
|
|
94
94
|
// migration engine (`runLegacyDaemonMigration`) both live in
|
|
95
95
|
// `../runtime/legacy-daemon-migration.ts` rather than here, because this
|
|
96
96
|
// daemon's own boot-time reconcile (`../runtime/legacy-daemon-reconcile.ts`)
|
|
97
|
-
// needs them too
|
|
97
|
+
// needs them too, the entrypoint-agnostic `runtime` layer is what both of
|
|
98
98
|
// this repository's consumers can import, so this CLI module is just one of
|
|
99
99
|
// them. The terminal app's own onboarding guided UX solves the same problem
|
|
100
100
|
// independently, under its own architecture constraints.
|
|
101
101
|
//
|
|
102
102
|
// This detection is entirely independent of PlatformServiceManager's own
|
|
103
|
-
// status()
|
|
103
|
+
// status(), it does not rely on (or get invalidated by) the parallel SDK
|
|
104
104
|
// fix that makes status().running itself query systemd honestly for the
|
|
105
105
|
// TRACKED unit name.
|
|
106
106
|
// ---------------------------------------------------------------------------
|
|
@@ -125,7 +125,7 @@ export function isDaemonServiceSubcommand(value: string | undefined): value is D
|
|
|
125
125
|
* Exit codes `service-status` reports, so a script never parses prose.
|
|
126
126
|
*
|
|
127
127
|
* 0/3/4 are the three answers the question actually has; 1 stays what it has
|
|
128
|
-
* always been
|
|
128
|
+
* always been, the platform refused the query and the error is printed.
|
|
129
129
|
*/
|
|
130
130
|
export const SERVICE_STATUS_EXIT_RUNNING = 0;
|
|
131
131
|
export const SERVICE_STATUS_EXIT_INSTALLED_NOT_RUNNING = 3;
|
|
@@ -134,10 +134,10 @@ export const SERVICE_STATUS_EXIT_NOT_INSTALLED = 4;
|
|
|
134
134
|
export interface DaemonServiceCliInput {
|
|
135
135
|
readonly subcommand: DaemonServiceSubcommand;
|
|
136
136
|
readonly binaryPath: string;
|
|
137
|
-
/** The GoodVibes tree home (GOODVIBES_HOME-overridable)
|
|
137
|
+
/** The GoodVibes tree home (GOODVIBES_HOME-overridable), config/state root only, never unit-path resolution. See `unitHomeDir`. */
|
|
138
138
|
readonly homeDir: string;
|
|
139
139
|
/**
|
|
140
|
-
* The LOGIN user's home
|
|
140
|
+
* The LOGIN user's home, where the systemd/launchd/Windows unit actually
|
|
141
141
|
* lives, regardless of any GOODVIBES_HOME/GOODVIBES_DAEMON_HOME override.
|
|
142
142
|
* Required (no default to `homeDir`) so a unit-management subcommand can
|
|
143
143
|
* never silently search for its unit under a relocated tree home instead of
|
|
@@ -148,14 +148,14 @@ export interface DaemonServiceCliInput {
|
|
|
148
148
|
readonly port: number;
|
|
149
149
|
/**
|
|
150
150
|
* Whether the invoking CLI line carried an explicit `--hostname`/`--port`
|
|
151
|
-
* flag (not merely whether `host`/`port` above are set
|
|
151
|
+
* flag (not merely whether `host`/`port` above are set, those are always
|
|
152
152
|
* set, resolved from config either way). `install-service`/`migrate-service`
|
|
153
|
-
* refuse when either is true
|
|
153
|
+
* refuse when either is true, see `validateServiceEndpointFlags`. Defaults
|
|
154
154
|
* to false (no flag), matching every subcommand these two don't apply to.
|
|
155
155
|
*/
|
|
156
156
|
readonly hostnameFlagProvided?: boolean | undefined;
|
|
157
157
|
readonly portFlagProvided?: boolean | undefined;
|
|
158
|
-
/** Defaults to `homeDir
|
|
158
|
+
/** Defaults to `homeDir`, overridable so tests can scope both to one tempdir. */
|
|
159
159
|
readonly workingDirectory?: string | undefined;
|
|
160
160
|
/** Injected in tests; a real `ConfigManager` rooted at `homeDir` otherwise. */
|
|
161
161
|
readonly configManager?: ConfigManager | undefined;
|
|
@@ -166,7 +166,7 @@ export interface DaemonServiceCliInput {
|
|
|
166
166
|
/**
|
|
167
167
|
* `migrate-service` only: explicit consent to actually execute the
|
|
168
168
|
* migration (wired from the CLI's `-y`/`--yes` flag). Without it, the
|
|
169
|
-
* subcommand prints the exact plan and changes nothing
|
|
169
|
+
* subcommand prints the exact plan and changes nothing, never auto-migrate.
|
|
170
170
|
*/
|
|
171
171
|
readonly confirmMigration?: boolean | undefined;
|
|
172
172
|
/**
|
|
@@ -183,7 +183,7 @@ export interface DaemonServiceCliInput {
|
|
|
183
183
|
readonly legacyUnitFileRemove?: ((path: string) => void) | undefined;
|
|
184
184
|
/**
|
|
185
185
|
* `service-status` only: report one JSON document instead of prose. The exit
|
|
186
|
-
* code is the same either way
|
|
186
|
+
* code is the same either way, the codes are the machine-readable answer,
|
|
187
187
|
* and JSON is for when a script wants the fields as well.
|
|
188
188
|
*/
|
|
189
189
|
readonly json?: boolean | undefined;
|
|
@@ -200,7 +200,7 @@ export interface DaemonServiceCliResult {
|
|
|
200
200
|
/**
|
|
201
201
|
* The manager's definition (`ExecStart` command/args, name, description) is
|
|
202
202
|
* built once, in `../runtime/legacy-daemon-migration.ts`, and shared with the
|
|
203
|
-
* onboarding guided UX
|
|
203
|
+
* onboarding guided UX, see that module's doc comment for why.
|
|
204
204
|
*/
|
|
205
205
|
function buildManager(input: DaemonServiceCliInput): PlatformServiceManager {
|
|
206
206
|
return buildManagedDaemonServiceManager({
|
|
@@ -225,12 +225,12 @@ function statusLines(status: ManagedServiceStatus): string[] {
|
|
|
225
225
|
if (status.pid !== undefined) lines.push(`pid: ${status.pid}`);
|
|
226
226
|
if (status.platform !== 'manual' && status.installed && !status.running) {
|
|
227
227
|
// W3 Finding 4: this used to assert "'running' here only reflects
|
|
228
|
-
// processes this tool started directly"
|
|
228
|
+
// processes this tool started directly", true for the pid-file-only
|
|
229
229
|
// check the (currently linked) SDK still uses, but the parallel SDK
|
|
230
230
|
// batch is making status().running query systemd honestly via
|
|
231
231
|
// `is-active`, which would make that specific claim stale. Drop the
|
|
232
232
|
// claim about HOW running was computed and just offer the escape
|
|
233
|
-
// hatch
|
|
233
|
+
// hatch, true and useful under either SDK version.
|
|
234
234
|
lines.push(
|
|
235
235
|
`note: if this looks wrong, verify directly: ${status.suggestedCommands[status.suggestedCommands.length - 1] ?? 'the platform service-status command'}`,
|
|
236
236
|
);
|
|
@@ -264,7 +264,7 @@ export function buildInstallResultLines(status: ManagedServiceStatus): string[]
|
|
|
264
264
|
* Every line names the RESOLVED unit (`status.serviceName`, `status.path`) and
|
|
265
265
|
* says what the platform reported afterwards, rather than asserting the verb
|
|
266
266
|
* worked because it was dispatched without throwing. A verb aimed at a service
|
|
267
|
-
* that is not installed says exactly that
|
|
267
|
+
* that is not installed says exactly that, installing it silently would be a
|
|
268
268
|
* different command than the one that was run.
|
|
269
269
|
*/
|
|
270
270
|
export function buildLifecycleResultLines(
|
|
@@ -364,7 +364,7 @@ function ok(action: 'install' | 'uninstall' | 'status', status: ManagedServiceSt
|
|
|
364
364
|
lines.push(`removed the ${status.platform} service at ${status.path}`);
|
|
365
365
|
if (status.platform === 'systemd') {
|
|
366
366
|
lines.push(
|
|
367
|
-
"note: this removes the unit file but does not run `disable
|
|
367
|
+
"note: this removes the unit file but does not run `disable`; run " +
|
|
368
368
|
"`systemctl --user daemon-reload` to clear any stale enablement symlink.",
|
|
369
369
|
);
|
|
370
370
|
}
|
|
@@ -388,12 +388,12 @@ function failed(action: 'install' | 'uninstall' | 'status', status: ManagedServi
|
|
|
388
388
|
* `install-service` and `migrate-service` refuse an explicit
|
|
389
389
|
* `--hostname`/`--port` flag rather than silently accepting it. Both flags are
|
|
390
390
|
* RUNTIME-ONLY overrides applied to this one invocation's in-memory config
|
|
391
|
-
* (`applyRuntimeEndpointFlagOverrides` in `src/daemon/cli.ts`)
|
|
391
|
+
* (`applyRuntimeEndpointFlagOverrides` in `src/daemon/cli.ts`), but the unit
|
|
392
392
|
* this subcommand writes carries no endpoint flags at all (see
|
|
393
393
|
* `buildManagedDaemonServiceManager`'s ExecStart doc): the daemon always
|
|
394
394
|
* re-resolves `controlPlane.host`/`port`/`hostMode` from PERSISTED settings at
|
|
395
395
|
* boot. A flag accepted here would print/probe a binding the installed unit
|
|
396
|
-
* will never actually have
|
|
396
|
+
* will never actually have, an honest gap, not a cosmetic one, since the
|
|
397
397
|
* printed value is exactly what an operator would expect the running service
|
|
398
398
|
* to bind to. Refusing with a pointer to the persistent config path is
|
|
399
399
|
* strictly better than silently doing nothing with the flag.
|
|
@@ -408,7 +408,7 @@ export function validateServiceEndpointFlags(
|
|
|
408
408
|
.filter((name): name is string => name !== null)
|
|
409
409
|
.join('/');
|
|
410
410
|
return [
|
|
411
|
-
`${subcommand} refused: ${flagNames} only override this process's runtime config
|
|
411
|
+
`${subcommand} refused: ${flagNames} only override this process's runtime config; the installed unit carries ` +
|
|
412
412
|
'no endpoint flags and re-resolves controlPlane.host/port/hostMode from persisted settings at boot, so the ' +
|
|
413
413
|
'override would never take effect there.',
|
|
414
414
|
`Set the persistent binding instead: goodvibes-daemon config set controlPlane.host / controlPlane.port (or ` +
|
|
@@ -419,7 +419,7 @@ export function validateServiceEndpointFlags(
|
|
|
419
419
|
/**
|
|
420
420
|
* `migrate-service`: the guided, consented takeover of the legacy
|
|
421
421
|
* `goodvibes-daemon.service` unit. Thin wrapper over
|
|
422
|
-
* `runLegacyDaemonMigration` (`../runtime/legacy-daemon-migration.ts`)
|
|
422
|
+
* `runLegacyDaemonMigration` (`../runtime/legacy-daemon-migration.ts`), see
|
|
423
423
|
* that module for the design constraints (never auto-migrate,
|
|
424
424
|
* new-up-then-old-down, adopt-or-warn/never kill an unrecognized process,
|
|
425
425
|
* every action through an injectable seam) and the onboarding UX consumer.
|
|
@@ -461,7 +461,7 @@ export async function runDaemonServiceCli(input: DaemonServiceCliInput): Promise
|
|
|
461
461
|
case 'install-service': {
|
|
462
462
|
// W3 Finding 4: refuse rather than risk starting a second daemon
|
|
463
463
|
// alongside an already-installed legacy unit. Refuses whenever the
|
|
464
|
-
// legacy unit is present at all (not just when currently active)
|
|
464
|
+
// legacy unit is present at all (not just when currently active),
|
|
465
465
|
// an installed-but-inactive legacy unit can still be enabled and
|
|
466
466
|
// start competing for the same port later, and "never silently
|
|
467
467
|
// start a second daemon" is the bar here, not "never right now."
|
|
@@ -483,7 +483,7 @@ export async function runDaemonServiceCli(input: DaemonServiceCliInput): Promise
|
|
|
483
483
|
if (installed.actionError) return failed('install', installed);
|
|
484
484
|
const started = manager.start();
|
|
485
485
|
// start()'s actionError (e.g. a platform this manager can't dispatch
|
|
486
|
-
// actions for) doesn't undo the write
|
|
486
|
+
// actions for) doesn't undo the write, report install as ok, but surface
|
|
487
487
|
// the follow-up problem honestly instead of claiming it is running.
|
|
488
488
|
return started.actionError
|
|
489
489
|
? ok('install', { ...started, running: false }, [`could not start it automatically: ${started.actionError}`])
|
|
@@ -496,7 +496,7 @@ export async function runDaemonServiceCli(input: DaemonServiceCliInput): Promise
|
|
|
496
496
|
const extra: string[] = [];
|
|
497
497
|
if (stopped.actionError) extra.push(`(it may not have been running: ${stopped.actionError})`);
|
|
498
498
|
// This command only ever touches the TRACKED unit (whatever name/path
|
|
499
|
-
// actually resolved
|
|
499
|
+
// actually resolved, not necessarily the SERVICE_NAME constant) above,
|
|
500
500
|
// say so explicitly when an install-script unit also exists, so its
|
|
501
501
|
// continued presence is never a silent surprise.
|
|
502
502
|
if (legacy.present) extra.push(legacyUnitNote(legacy, resolveManagedUnitName(uninstalled)));
|
|
@@ -520,8 +520,8 @@ export async function runDaemonServiceCli(input: DaemonServiceCliInput): Promise
|
|
|
520
520
|
};
|
|
521
521
|
}
|
|
522
522
|
const result = ok('status', status, legacy.present ? [legacyUnitNote(legacy, resolveManagedUnitName(status))] : []);
|
|
523
|
-
// The status verb reports; it does not fail. `ok` is still true
|
|
524
|
-
// question was answered
|
|
523
|
+
// The status verb reports; it does not fail. `ok` is still true, the
|
|
524
|
+
// question was answered, while the exit code carries the answer itself
|
|
525
525
|
// (0 running / 3 installed-not-running / 4 not installed), so a script
|
|
526
526
|
// never has to read the prose above.
|
|
527
527
|
return { ...result, exitCode: serviceStatusExitCode(status) };
|
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* sessions-command.ts
|
|
2
|
+
* sessions-command.ts, `goodvibes-daemon sessions list|kill <id>`.
|
|
3
3
|
*
|
|
4
4
|
* The sessions this daemon HOSTS: conversation loops running inside it, which
|
|
5
5
|
* outlive the client that started them. That is the whole reason they need a
|
|
6
|
-
* command
|
|
6
|
+
* command, a terminal's own session dies with the terminal and never needs
|
|
7
7
|
* listing from outside, while a hosted one can be running on a headless box
|
|
8
8
|
* with nothing attached to it at all.
|
|
9
9
|
*
|
|
10
10
|
* The verbs are `sessions.hosted.list` and `sessions.hosted.kill`, and both are
|
|
11
|
-
* declared ws-only in the method catalog
|
|
11
|
+
* declared ws-only in the method catalog, they have no REST binding, so they
|
|
12
12
|
* go through `callDaemonWsVerb` rather than `callDaemonVerb`. Same target, same
|
|
13
13
|
* operator token, same --host/--port/--token convention; only the transport is
|
|
14
14
|
* different. A daemon built without hosted sessions answers "does not know the
|
|
@@ -62,7 +62,7 @@ export interface SessionsCommandFlags extends RemoteCommandFlags {
|
|
|
62
62
|
|
|
63
63
|
export interface RunSessionsCommandInput extends RemoteCommandDeps {
|
|
64
64
|
readonly flags: SessionsCommandFlags;
|
|
65
|
-
/** Positional words after `sessions
|
|
65
|
+
/** Positional words after `sessions`, the subcommand and its argument. */
|
|
66
66
|
readonly args: readonly string[];
|
|
67
67
|
}
|
|
68
68
|
|
|
@@ -119,7 +119,7 @@ function renderSession(session: HostedSessionRecord, now: number): string[] {
|
|
|
119
119
|
*
|
|
120
120
|
* Exit 0 when the daemon answered, 1 when it refused or could not be reached,
|
|
121
121
|
* 2 when the command line was wrong. A `kill` with no id is a usage refusal
|
|
122
|
-
* rather than a "kill everything"
|
|
122
|
+
* rather than a "kill everything", there is no shape of this command that ends
|
|
123
123
|
* more than the one session named.
|
|
124
124
|
*/
|
|
125
125
|
export async function runSessionsCommand(input: RunSessionsCommandInput): Promise<DaemonCommandResult> {
|
|
@@ -129,11 +129,11 @@ export async function runSessionsCommand(input: RunSessionsCommandInput): Promis
|
|
|
129
129
|
return refusal('name what to do with the sessions.', flags.json);
|
|
130
130
|
}
|
|
131
131
|
if (!isSessionsSubcommand(subcommand)) {
|
|
132
|
-
return refusal(`'${subcommand}' is not a sessions command
|
|
132
|
+
return refusal(`'${subcommand}' is not a sessions command; try list or kill.`, flags.json);
|
|
133
133
|
}
|
|
134
134
|
const sessionId = args[1];
|
|
135
135
|
if (subcommand === 'kill' && sessionId === undefined) {
|
|
136
|
-
return refusal('kill needs the session to end
|
|
136
|
+
return refusal('kill needs the session to end; run `sessions list` to see them.', flags.json);
|
|
137
137
|
}
|
|
138
138
|
if (args.length > (subcommand === 'kill' ? 2 : 1)) {
|
|
139
139
|
return refusal(`'${args[subcommand === 'kill' ? 2 : 1]}' is one argument too many.`, flags.json);
|