@hunterzhu/pulse-cli 0.1.11 → 0.2.1
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/README.md +55 -0
- package/dist/bin.d.ts +1 -1
- package/dist/bin.js +104 -4
- package/dist/commands/mcp.d.ts +2 -0
- package/dist/commands/mcp.js +29 -0
- package/dist/commands/resume.js +5 -1
- package/dist/commands/run.js +7 -1
- package/dist/commands/scheduled.d.ts +2 -0
- package/dist/commands/scheduled.js +131 -0
- package/dist/commands/service.d.ts +2 -0
- package/dist/commands/service.js +81 -0
- package/dist/commands/template.d.ts +2 -0
- package/dist/commands/template.js +31 -0
- package/dist/components/App.js +8 -2
- package/dist/components/AskPrompt.js +1 -1
- package/dist/components/Header.d.ts +1 -1
- package/dist/components/Header.js +2 -3
- package/dist/components/HelpView.js +2 -1
- package/dist/components/InputArea.js +3 -1
- package/dist/components/MessageList.d.ts +2 -1
- package/dist/components/MessageList.js +2 -2
- package/dist/components/MessageViewport.d.ts +2 -6
- package/dist/components/MessageViewport.js +42 -69
- package/dist/components/StatusHud.d.ts +2 -1
- package/dist/components/StatusHud.js +4 -8
- package/dist/components/TextInput.d.ts +9 -0
- package/dist/components/TextInput.js +46 -0
- package/dist/config.d.ts +36 -0
- package/dist/config.js +4 -0
- package/dist/hooks/useRun.js +33 -1
- package/dist/hooks/useSlashCommands.d.ts +1 -0
- package/dist/hooks/useSlashCommands.js +1 -0
- package/dist/utils/markdown.js +1 -1
- package/dist/utils/mouse.d.ts +6 -0
- package/dist/utils/mouse.js +11 -0
- package/dist/utils/transcript.d.ts +11 -0
- package/dist/utils/transcript.js +37 -0
- package/package.json +18 -6
package/README.md
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
# @hunterzhu/pulse-cli
|
|
2
|
+
|
|
3
|
+
**A local-first AI assistant for coding, research, and everyday file tasks.**
|
|
4
|
+
|
|
5
|
+
Pulse runs in your project folder, keeps conversations on your machine, and can resume unfinished work. Requires Node.js 22+.
|
|
6
|
+
|
|
7
|
+
## Install
|
|
8
|
+
|
|
9
|
+
Install the CLI globally so the `pulse` command is on your `PATH`:
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
npm install -g @hunterzhu/pulse-cli
|
|
13
|
+
pulse --version
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
For a one-off run, use `npx @hunterzhu/pulse-cli`. Commands below assume a global installation.
|
|
17
|
+
|
|
18
|
+
## Configure a model and API key
|
|
19
|
+
|
|
20
|
+
Create a starter configuration:
|
|
21
|
+
|
|
22
|
+
```bash
|
|
23
|
+
pulse setup
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
The configuration lives at `~/.pulse/config.json` (Windows:
|
|
27
|
+
`%USERPROFILE%\.pulse\config.json`). A fresh configuration includes the DeepSeek
|
|
28
|
+
provider and `deepseek-chat` model. Set `activeModel` to `deepseek-chat`, or use
|
|
29
|
+
`--model deepseek-chat` as below. The default `mock` model makes no API calls.
|
|
30
|
+
|
|
31
|
+
Set the API key in your shell; the DeepSeek provider uses
|
|
32
|
+
`apiKeyEnv: "DEEPSEEK_API_KEY"` to read it:
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
export DEEPSEEK_API_KEY="your-key"
|
|
36
|
+
# Windows PowerShell: $env:DEEPSEEK_API_KEY="your-key"
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
## Usage
|
|
40
|
+
|
|
41
|
+
```bash
|
|
42
|
+
pulse --model deepseek-chat # interactive session
|
|
43
|
+
pulse run "Summarize this project" --model deepseek-chat
|
|
44
|
+
pulse sessions
|
|
45
|
+
pulse resume <conversation-id> "Continue" --model deepseek-chat
|
|
46
|
+
pulse doctor
|
|
47
|
+
pulse --read-only
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
See `pulse --help` for all options.
|
|
51
|
+
|
|
52
|
+
## Links
|
|
53
|
+
|
|
54
|
+
- [Full CLI configuration guide](https://github.com/zhuhengtan/Pulse/blob/main/docs/cli-config.md)
|
|
55
|
+
- [Source code and documentation](https://github.com/zhuhengtan/Pulse)
|
package/dist/bin.d.ts
CHANGED
package/dist/bin.js
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { readFile } from 'node:fs/promises';
|
|
3
3
|
import { createRequire } from 'node:module';
|
|
4
|
-
import { resolve } from 'node:path';
|
|
4
|
+
import { isAbsolute, resolve } from 'node:path';
|
|
5
5
|
import { fileURLToPath } from 'node:url';
|
|
6
|
+
import { createMcpCapabilityPack, createPdfCapabilityPack, createSkillCapabilityPack, createSpreadsheetCapabilityPack } from '@hunterzhu/pulse-server';
|
|
6
7
|
import { ensurePulseUserConfig, expandHome, loadPulseConfig } from './config.js';
|
|
7
8
|
import { runInteractive } from './commands/interactive.js';
|
|
8
9
|
import { runOneShot } from './commands/run.js';
|
|
@@ -10,6 +11,10 @@ import { runDoctor } from './commands/doctor.js';
|
|
|
10
11
|
import { runSessions } from './commands/sessions.js';
|
|
11
12
|
import { runResume } from './commands/resume.js';
|
|
12
13
|
import { runSetup } from './commands/setup.js';
|
|
14
|
+
import { runScheduledCommand } from './commands/scheduled.js';
|
|
15
|
+
import { runServiceCommand } from './commands/service.js';
|
|
16
|
+
import { runTemplateCommand } from './commands/template.js';
|
|
17
|
+
import { runMcpDoctor } from './commands/mcp.js';
|
|
13
18
|
import { isSameModulePath } from './utils/is-main-module.js';
|
|
14
19
|
const packageManifest = createRequire(import.meta.url)('../package.json');
|
|
15
20
|
export const version = packageManifest.version;
|
|
@@ -26,6 +31,11 @@ Usage:
|
|
|
26
31
|
pulse sessions [options] list saved conversations
|
|
27
32
|
pulse resume <conversation-id> [task] continue or recover a conversation
|
|
28
33
|
pulse doctor [options] check local configuration
|
|
34
|
+
pulse schedule <add|list|pause|resume|remove|daemon> [options]
|
|
35
|
+
manage recurring tasks (daemon requires read-only or auto approval)
|
|
36
|
+
pulse service <install|status|start|stop|uninstall>
|
|
37
|
+
pulse template <list|show|run> [name] [values...]
|
|
38
|
+
pulse mcp doctor [server-id]
|
|
29
39
|
|
|
30
40
|
Options:
|
|
31
41
|
--cwd <path> workspace directory
|
|
@@ -36,6 +46,7 @@ Options:
|
|
|
36
46
|
--max-output-tokens <n> maximum generated tokens (default 4096)
|
|
37
47
|
--reasoning-effort <x> low, medium, or high
|
|
38
48
|
--max-turns <n> maximum ReAct model/tool turns (default 32)
|
|
49
|
+
--execution-mode <mode> serial (default) or parallel-read (up to three read-only lanes)
|
|
39
50
|
--auto-compact-percent <n> compact automatically at this percent of the context window (1-90, default 90)
|
|
40
51
|
--format <text|jsonl> output format
|
|
41
52
|
--resume enter the latest saved session
|
|
@@ -43,10 +54,12 @@ Options:
|
|
|
43
54
|
--auto-approve approve local writes and shell execution on your behalf
|
|
44
55
|
--approval-mode <mode> read-only, ask, or auto
|
|
45
56
|
--allow-network enable public web search and fetch tools
|
|
57
|
+
--no-network disable network tools even when user config enables them
|
|
46
58
|
--trust-workspace treat workspace .pulse/config.json as user-trusted
|
|
47
59
|
--system-prompt <text> custom system instructions
|
|
48
60
|
--system-prompt-file <path> load custom system instructions from file
|
|
49
61
|
--mock-response <text> deterministic response for local debugging
|
|
62
|
+
--mock-task-assessment <json> deterministic verifier result for local debugging
|
|
50
63
|
--live doctor: make one real provider request
|
|
51
64
|
--no-color disable terminal styling
|
|
52
65
|
--help, -h show this help
|
|
@@ -91,7 +104,7 @@ export function parse(argv) {
|
|
|
91
104
|
options[key] = true;
|
|
92
105
|
continue;
|
|
93
106
|
}
|
|
94
|
-
if (!command && ['run', 'sessions', 'resume', 'doctor', 'setup'].includes(arg)) {
|
|
107
|
+
if (!command && ['run', 'sessions', 'resume', 'doctor', 'setup', 'schedule', 'service', 'template', 'mcp'].includes(arg)) {
|
|
95
108
|
command = arg;
|
|
96
109
|
}
|
|
97
110
|
else {
|
|
@@ -124,6 +137,8 @@ export async function hostOptions(parsed) {
|
|
|
124
137
|
if (!profile)
|
|
125
138
|
throw new Error(`MODEL_PROVIDER_NOT_FOUND:${providerName}`);
|
|
126
139
|
const model = modelSelection.modelCode;
|
|
140
|
+
if (modelSelection.pricing && (!/^[A-Z]{3}$/.test(modelSelection.pricing.currency) || !Number.isFinite(modelSelection.pricing.inputPerMillion) || modelSelection.pricing.inputPerMillion < 0 || !Number.isFinite(modelSelection.pricing.outputPerMillion) || modelSelection.pricing.outputPerMillion < 0 || !modelSelection.pricing.version.trim()))
|
|
141
|
+
throw new Error(`INVALID_MODEL_PRICING:${activeModelName}`);
|
|
127
142
|
const baseURL = profile.baseURL;
|
|
128
143
|
const contextTokens = option(parsed.options, 'context-tokens') ?? process.env.PULSE_CONTEXT_TOKENS;
|
|
129
144
|
const maxOutputTokens = option(parsed.options, 'max-output-tokens') ?? process.env.PULSE_MAX_OUTPUT_TOKENS;
|
|
@@ -180,18 +195,86 @@ export async function hostOptions(parsed) {
|
|
|
180
195
|
if (modelDisplayNames.has(displayName))
|
|
181
196
|
throw new Error(`DUPLICATE_MODEL_DISPLAY_NAME:${displayName}`);
|
|
182
197
|
modelDisplayNames.add(displayName);
|
|
183
|
-
return [[displayName, {
|
|
198
|
+
return [[displayName, {
|
|
199
|
+
provider: item.provider,
|
|
200
|
+
model: item.modelCode,
|
|
201
|
+
...(item.maxContextTokens === undefined ? {} : { maxContextTokens: item.maxContextTokens }),
|
|
202
|
+
...(item.maxOutputTokens === undefined ? {} : { maxOutputTokens: item.maxOutputTokens }),
|
|
203
|
+
...(item.reasoningEffort === undefined ? {} : { reasoningEffort: item.reasoningEffort }),
|
|
204
|
+
}]];
|
|
184
205
|
}));
|
|
206
|
+
const taskRouting = config.taskRouting;
|
|
207
|
+
for (const [task, models] of Object.entries(taskRouting ?? {})) {
|
|
208
|
+
if (!Array.isArray(models) || models.length === 0 || models.some((model) => typeof model !== 'string' || !providerModels[model])) {
|
|
209
|
+
throw new Error(`INVALID_TASK_MODEL_ROUTE:${task}`);
|
|
210
|
+
}
|
|
211
|
+
if (new Set(models).size !== models.length)
|
|
212
|
+
throw new Error(`DUPLICATE_TASK_MODEL_ROUTE_CANDIDATE:${task}`);
|
|
213
|
+
}
|
|
185
214
|
const cwd = requestedCwd ?? expandHome(config.cwd);
|
|
186
215
|
const dataDir = expandHome(option(parsed.options, 'data-dir') ?? process.env.PULSE_DATA_DIR ?? config.dataDir);
|
|
187
216
|
const mockResponse = option(parsed.options, 'mock-response');
|
|
217
|
+
const mockTaskAssessment = option(parsed.options, 'mock-task-assessment');
|
|
218
|
+
let mockTaskAssessments;
|
|
219
|
+
if (mockTaskAssessment !== undefined) {
|
|
220
|
+
try {
|
|
221
|
+
const value = JSON.parse(mockTaskAssessment);
|
|
222
|
+
if (!Array.isArray(value) || value.length === 0 || value.some((item) => !item || typeof item !== 'object' || Array.isArray(item)))
|
|
223
|
+
throw new Error('INVALID');
|
|
224
|
+
mockTaskAssessments = value;
|
|
225
|
+
}
|
|
226
|
+
catch {
|
|
227
|
+
throw new Error('INVALID_MOCK_TASK_ASSESSMENT: expected a non-empty JSON array of assessment objects');
|
|
228
|
+
}
|
|
229
|
+
}
|
|
188
230
|
const requestedApprovalMode = option(parsed.options, 'approval-mode') ?? process.env.PULSE_APPROVAL_MODE;
|
|
189
231
|
const approvalMode = requestedApprovalMode === 'read-only' || parsed.options['read-only'] === true
|
|
190
232
|
? 'read-only'
|
|
191
233
|
: requestedApprovalMode === 'auto' || parsed.options['auto-approve'] === true || process.env.PULSE_AUTO_APPROVE === '1'
|
|
192
234
|
? 'auto'
|
|
193
235
|
: requestedApprovalMode === 'ask' ? 'ask' : config.approvalMode;
|
|
194
|
-
const allowNetwork = parsed.options['allow-network'] === true || process.env.PULSE_ALLOW_NETWORK === '1' ? true : config.allowNetwork;
|
|
236
|
+
const allowNetwork = parsed.options['no-network'] === true ? false : parsed.options['allow-network'] === true || process.env.PULSE_ALLOW_NETWORK === '1' ? true : config.allowNetwork;
|
|
237
|
+
const executionMode = option(parsed.options, 'execution-mode') ?? process.env.PULSE_EXECUTION_MODE ?? config.executionMode ?? 'serial';
|
|
238
|
+
if (executionMode !== 'serial' && executionMode !== 'parallel-read')
|
|
239
|
+
throw new Error('INVALID_EXECUTION_MODE: expected serial or parallel-read');
|
|
240
|
+
const capabilitySettings = config.capabilities ?? {};
|
|
241
|
+
const skillRoots = capabilitySettings.trustedSkillRoots ?? [];
|
|
242
|
+
if (!Array.isArray(skillRoots) || skillRoots.some((root) => typeof root !== 'string' || !isAbsolute(expandHome(root) ?? root)))
|
|
243
|
+
throw new Error('SKILL_TRUSTED_ROOT_MUST_BE_ABSOLUTE');
|
|
244
|
+
const enabledCapabilityPacks = capabilitySettings.enabled ?? [];
|
|
245
|
+
if (!Array.isArray(enabledCapabilityPacks) || enabledCapabilityPacks.some((id) => typeof id !== 'string') || new Set(enabledCapabilityPacks).size !== enabledCapabilityPacks.length)
|
|
246
|
+
throw new Error('INVALID_ENABLED_CAPABILITY_LIST');
|
|
247
|
+
const skillNames = capabilitySettings.skills ?? [];
|
|
248
|
+
if (!Array.isArray(skillNames) || skillNames.some((name) => typeof name !== 'string'))
|
|
249
|
+
throw new Error('INVALID_SKILL_SELECTION');
|
|
250
|
+
const capabilityPacks = [createPdfCapabilityPack(), createSpreadsheetCapabilityPack(), createSkillCapabilityPack({ trustedRoots: skillRoots.map((root) => resolve(expandHome(root) ?? root)) })];
|
|
251
|
+
for (const [id, server] of Object.entries(capabilitySettings.mcpServers ?? {})) {
|
|
252
|
+
if (!server || typeof server.command !== 'string' || server.command.trim().length === 0 || (server.args !== undefined && (!Array.isArray(server.args) || server.args.some((arg) => typeof arg !== 'string'))))
|
|
253
|
+
throw new Error(`INVALID_MCP_SERVER_CONFIG:${id}`);
|
|
254
|
+
if (server.toolPolicies && Object.entries(server.toolPolicies).some(([name, policy]) => !name || !['read', 'write', 'external'].includes(policy)))
|
|
255
|
+
throw new Error(`INVALID_MCP_TOOL_POLICY:${id}`);
|
|
256
|
+
const resolvedEnv = { ...(server.env ?? {}) };
|
|
257
|
+
for (const [childName, sourceName] of Object.entries(server.envFrom ?? {})) {
|
|
258
|
+
if (!/^[A-Z_][A-Z0-9_]*$/i.test(childName) || !/^[A-Z_][A-Z0-9_]*$/i.test(sourceName))
|
|
259
|
+
throw new Error(`INVALID_MCP_ENV_REFERENCE:${id}`);
|
|
260
|
+
const value = process.env[sourceName];
|
|
261
|
+
if (value === undefined)
|
|
262
|
+
throw new Error(`MCP_ENVIRONMENT_REQUIRED:${id}:${sourceName}`);
|
|
263
|
+
resolvedEnv[childName] = value;
|
|
264
|
+
}
|
|
265
|
+
capabilityPacks.push(createMcpCapabilityPack(id, {
|
|
266
|
+
command: server.command,
|
|
267
|
+
args: server.args ?? [],
|
|
268
|
+
...(server.cwd === undefined ? {} : { cwd: resolve(expandHome(server.cwd) ?? server.cwd) }),
|
|
269
|
+
...(Object.keys(resolvedEnv).length === 0 ? {} : { env: resolvedEnv }),
|
|
270
|
+
...(server.timeoutMs === undefined ? {} : { timeoutMs: server.timeoutMs }),
|
|
271
|
+
...(server.toolPolicies === undefined ? {} : { toolPolicies: server.toolPolicies }),
|
|
272
|
+
}));
|
|
273
|
+
}
|
|
274
|
+
for (const id of enabledCapabilityPacks) {
|
|
275
|
+
if (!capabilityPacks.some((pack) => pack.manifest.id === id))
|
|
276
|
+
throw new Error(`UNKNOWN_ENABLED_CAPABILITY:${id}`);
|
|
277
|
+
}
|
|
195
278
|
const rawSystemPrompt = option(parsed.options, 'system-prompt') ?? process.env.PULSE_SYSTEM_PROMPT ?? config.systemPrompt;
|
|
196
279
|
const rawSystemPromptFile = option(parsed.options, 'system-prompt-file') ?? process.env.PULSE_SYSTEM_PROMPT_FILE ?? config.systemPromptFile;
|
|
197
280
|
let systemPrompt = rawSystemPrompt;
|
|
@@ -218,13 +301,21 @@ export async function hostOptions(parsed) {
|
|
|
218
301
|
provider,
|
|
219
302
|
providerProfiles,
|
|
220
303
|
providerModels,
|
|
304
|
+
modelPricing: Object.fromEntries(Object.values(config.models ?? {}).flatMap((item) => item.pricing ? [[item.displayName, item.pricing]] : [])),
|
|
305
|
+
...(taskRouting === undefined ? {} : { taskRouting }),
|
|
306
|
+
capabilityPacks,
|
|
307
|
+
enabledCapabilityPacks,
|
|
308
|
+
capabilityConfig: { skills: skillNames },
|
|
221
309
|
activeProviderCode: providerName,
|
|
222
310
|
activeModel: activeModelName,
|
|
223
311
|
...(mockResponse === undefined ? {} : { mockResponse }),
|
|
312
|
+
...(mockTaskAssessments === undefined ? {} : { mockTaskAssessments }),
|
|
224
313
|
...(approvalMode === undefined ? {} : { approvalMode }),
|
|
225
314
|
...(configuredMaxTurns === undefined ? {} : { maxTurns: configuredMaxTurns }),
|
|
226
315
|
...(configuredAutoCompactPercent === undefined ? {} : { autoCompactPercent: Math.min(90, configuredAutoCompactPercent) }),
|
|
227
316
|
...(allowNetwork === undefined ? {} : { allowNetwork }),
|
|
317
|
+
...(config.networkHosts === undefined ? {} : { networkHosts: config.networkHosts }),
|
|
318
|
+
executionMode,
|
|
228
319
|
};
|
|
229
320
|
}
|
|
230
321
|
export async function main() {
|
|
@@ -247,6 +338,15 @@ export async function main() {
|
|
|
247
338
|
if (parsed.command === 'sessions') {
|
|
248
339
|
return runSessions(options, option(parsed.options, 'format') ?? 'text', version);
|
|
249
340
|
}
|
|
341
|
+
if (parsed.command === 'schedule') {
|
|
342
|
+
return runScheduledCommand(options, parsed.positionals, parsed.options);
|
|
343
|
+
}
|
|
344
|
+
if (parsed.command === 'service')
|
|
345
|
+
return runServiceCommand(options, parsed.positionals);
|
|
346
|
+
if (parsed.command === 'mcp' && parsed.positionals[0] === 'doctor')
|
|
347
|
+
return runMcpDoctor(options, parsed.positionals.slice(1));
|
|
348
|
+
if (parsed.command === 'template')
|
|
349
|
+
return runTemplateCommand(options, parsed.positionals, option(parsed.options, 'format') ?? 'text', version);
|
|
250
350
|
if (parsed.command === 'run') {
|
|
251
351
|
const task = parsed.positionals.join(' ').trim();
|
|
252
352
|
if (!task)
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
export async function runMcpDoctor(options, ids) {
|
|
2
|
+
const selected = options.enabledCapabilityPacks ?? [];
|
|
3
|
+
const requested = ids[0] ? selected.filter(id => id === ids[0]) : selected.filter(id => options.capabilityPacks?.some(pack => pack.manifest.id === id && pack.manifest.kind === 'mcp'));
|
|
4
|
+
if (ids[0] && !requested.includes(ids[0]))
|
|
5
|
+
throw new Error(`MCP_SERVER_NOT_ENABLED:${ids[0]}`);
|
|
6
|
+
const rows = [];
|
|
7
|
+
for (const id of requested) {
|
|
8
|
+
const pack = options.capabilityPacks?.find(item => item.manifest.id === id && item.manifest.kind === 'mcp');
|
|
9
|
+
if (!pack)
|
|
10
|
+
continue;
|
|
11
|
+
let activation;
|
|
12
|
+
try {
|
|
13
|
+
activation = await pack.activate({ workspaceRoot: options.cwd ?? process.cwd(), config: options.capabilityConfig ?? {}, signal: new AbortController().signal });
|
|
14
|
+
rows.push({ id, status: 'connected', tools: activation.tools.map(tool => tool.manifest.name) });
|
|
15
|
+
}
|
|
16
|
+
catch (error) {
|
|
17
|
+
rows.push({ id, status: 'failed', error: error instanceof Error ? error.message.replace(/(API[_-]?KEY|TOKEN|SECRET|PASSWORD)\s*[:=]\s*[^\s,;]+/gi, '$1=[REDACTED]') : 'MCP_CONNECTION_FAILED' });
|
|
18
|
+
}
|
|
19
|
+
finally {
|
|
20
|
+
await activation?.dispose?.();
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
if (!rows.length) {
|
|
24
|
+
process.stdout.write('No enabled MCP servers. Configure capabilities.mcpServers and capabilities.enabled.\n');
|
|
25
|
+
return 1;
|
|
26
|
+
}
|
|
27
|
+
process.stdout.write(`${JSON.stringify({ servers: rows }, null, 2)}\n`);
|
|
28
|
+
return rows.some(row => row.status !== 'connected') ? 1 : 0;
|
|
29
|
+
}
|
package/dist/commands/resume.js
CHANGED
|
@@ -47,12 +47,14 @@ export async function runResume(options, conversationId, task, format = 'text',
|
|
|
47
47
|
}
|
|
48
48
|
}
|
|
49
49
|
const outcome = await run.outcome();
|
|
50
|
+
const taskOutcome = await run.taskOutcome();
|
|
50
51
|
if (format === 'jsonl') {
|
|
51
52
|
process.stdout.write(`${JSON.stringify({
|
|
52
53
|
schemaVersion: 1,
|
|
53
54
|
type: 'result',
|
|
54
55
|
runId: run.id,
|
|
55
56
|
status: outcome.status,
|
|
57
|
+
taskOutcome: taskOutcome ?? null,
|
|
56
58
|
text: outcome.text ?? null,
|
|
57
59
|
error: outcome.error ?? null,
|
|
58
60
|
})}\n`);
|
|
@@ -66,7 +68,9 @@ export async function runResume(options, conversationId, task, format = 'text',
|
|
|
66
68
|
else {
|
|
67
69
|
process.stdout.write(`\n[${outcome.status}]\n`);
|
|
68
70
|
}
|
|
69
|
-
|
|
71
|
+
if (outcome.status === 'cancelled')
|
|
72
|
+
return 3;
|
|
73
|
+
return outcome.status === 'succeeded' && (!taskOutcome || taskOutcome.status === 'accepted') ? 0 : 1;
|
|
70
74
|
}
|
|
71
75
|
catch (error) {
|
|
72
76
|
process.stderr.write(`[错误] ${error instanceof Error ? error.message : String(error)}\n`);
|
package/dist/commands/run.js
CHANGED
|
@@ -49,12 +49,16 @@ export async function runOneShot(options, task, format = 'text', version = '0.1.
|
|
|
49
49
|
}
|
|
50
50
|
}
|
|
51
51
|
const outcome = await run.outcome();
|
|
52
|
+
const taskOutcome = await run.taskOutcome();
|
|
53
|
+
const usage = await run.usage();
|
|
52
54
|
if (format === 'jsonl') {
|
|
53
55
|
process.stdout.write(`${JSON.stringify({
|
|
54
56
|
schemaVersion: 1,
|
|
55
57
|
type: 'result',
|
|
56
58
|
runId: run.id,
|
|
57
59
|
status: outcome.status,
|
|
60
|
+
taskOutcome: taskOutcome ?? null,
|
|
61
|
+
usage,
|
|
58
62
|
text: outcome.text ?? null,
|
|
59
63
|
error: outcome.error ?? null,
|
|
60
64
|
})}\n`);
|
|
@@ -68,7 +72,9 @@ export async function runOneShot(options, task, format = 'text', version = '0.1.
|
|
|
68
72
|
else {
|
|
69
73
|
process.stdout.write(`\n[${outcome.status}]\n`);
|
|
70
74
|
}
|
|
71
|
-
|
|
75
|
+
if (outcome.status === 'cancelled')
|
|
76
|
+
return 3;
|
|
77
|
+
return outcome.status === 'succeeded' && (!taskOutcome || taskOutcome.status === 'accepted') ? 0 : 1;
|
|
72
78
|
}
|
|
73
79
|
finally {
|
|
74
80
|
unbindSignals();
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
import { realpath } from 'node:fs/promises';
|
|
2
|
+
import { resolve, join } from 'node:path';
|
|
3
|
+
import { createLocalHost, MAX_SCHEDULED_TASK_INTERVAL_MS, MIN_SCHEDULED_TASK_INTERVAL_MS, ScheduledTaskStore, ScheduledTaskWorker, pulseDataPath } from '@hunterzhu/pulse-server';
|
|
4
|
+
function parseInterval(raw) {
|
|
5
|
+
if (!raw)
|
|
6
|
+
throw new Error('SCHEDULE_EVERY_REQUIRED');
|
|
7
|
+
const match = /^(\d+)(ms|s|m|h|d)$/.exec(raw.trim());
|
|
8
|
+
if (!match)
|
|
9
|
+
throw new Error('INVALID_SCHEDULE_INTERVAL: use 30s, 15m, 2h, or 1d');
|
|
10
|
+
const value = Number(match[1]);
|
|
11
|
+
const multiplier = match[2] === 'ms' ? 1 : match[2] === 's' ? 1_000 : match[2] === 'm' ? 60_000 : match[2] === 'h' ? 3_600_000 : 86_400_000;
|
|
12
|
+
const intervalMs = value * multiplier;
|
|
13
|
+
if (!Number.isSafeInteger(intervalMs) || intervalMs < MIN_SCHEDULED_TASK_INTERVAL_MS || intervalMs > MAX_SCHEDULED_TASK_INTERVAL_MS)
|
|
14
|
+
throw new Error('SCHEDULE_INTERVAL_OUT_OF_RANGE');
|
|
15
|
+
return intervalMs;
|
|
16
|
+
}
|
|
17
|
+
function storeFor(options) {
|
|
18
|
+
return new ScheduledTaskStore(join(resolve(options.dataDir ?? pulseDataPath()), 'scheduled-tasks.json'));
|
|
19
|
+
}
|
|
20
|
+
function output(value, format) {
|
|
21
|
+
process.stdout.write(format === 'jsonl' ? `${JSON.stringify(value)}\n` : `${JSON.stringify(value, null, 2)}\n`);
|
|
22
|
+
}
|
|
23
|
+
export async function runScheduledCommand(options, args, flags) {
|
|
24
|
+
const [action, id] = args;
|
|
25
|
+
if (!action)
|
|
26
|
+
throw new Error('SCHEDULE_ACTION_REQUIRED');
|
|
27
|
+
const store = storeFor(options);
|
|
28
|
+
if (action === 'add') {
|
|
29
|
+
const name = typeof flags.name === 'string' ? flags.name : undefined;
|
|
30
|
+
const instruction = args.slice(1).join(' ').trim();
|
|
31
|
+
if (!instruction)
|
|
32
|
+
throw new Error('SCHEDULE_TASK_REQUIRED');
|
|
33
|
+
const task = await store.create({ workspace: await realpath(resolve(options.cwd ?? process.cwd())), name: name ?? instruction.slice(0, 120), instruction, intervalMs: parseInterval(typeof flags.every === 'string' ? flags.every : undefined) });
|
|
34
|
+
output(task, typeof flags.format === 'string' ? flags.format : 'text');
|
|
35
|
+
return 0;
|
|
36
|
+
}
|
|
37
|
+
if (action === 'list') {
|
|
38
|
+
output(await store.list(), typeof flags.format === 'string' ? flags.format : 'text');
|
|
39
|
+
return 0;
|
|
40
|
+
}
|
|
41
|
+
if (action === 'recover') {
|
|
42
|
+
if (!id)
|
|
43
|
+
throw new Error('SCHEDULE_RECOVER_REQUIRES_ID');
|
|
44
|
+
if (flags['confirm-stopped'] !== true)
|
|
45
|
+
throw new Error('SCHEDULE_RECOVER_REQUIRES_CONFIRMATION: stop the worker and child processes, then pass --confirm-stopped');
|
|
46
|
+
const task = await store.get(id);
|
|
47
|
+
if (!task?.run?.quarantined)
|
|
48
|
+
throw new Error(`SCHEDULED_TASK_NOT_QUARANTINED:${id}`);
|
|
49
|
+
const recovered = await store.recoverQuarantined(id, task.run.id);
|
|
50
|
+
if (!recovered)
|
|
51
|
+
throw new Error('SCHEDULED_TASK_RECOVERY_RACE');
|
|
52
|
+
output(recovered, typeof flags.format === 'string' ? flags.format : 'text');
|
|
53
|
+
return 0;
|
|
54
|
+
}
|
|
55
|
+
if (action === 'pause' || action === 'resume' || action === 'remove') {
|
|
56
|
+
if (!id)
|
|
57
|
+
throw new Error(`SCHEDULE_${action.toUpperCase()}_REQUIRES_ID`);
|
|
58
|
+
const result = action === 'remove' ? await store.remove(id) : await store.pause(id, action === 'pause');
|
|
59
|
+
if (!result)
|
|
60
|
+
throw new Error(`SCHEDULED_TASK_NOT_FOUND:${id}`);
|
|
61
|
+
output(result, typeof flags.format === 'string' ? flags.format : 'text');
|
|
62
|
+
return 0;
|
|
63
|
+
}
|
|
64
|
+
if (action !== 'daemon' && action !== 'run-once')
|
|
65
|
+
throw new Error(`UNKNOWN_SCHEDULE_ACTION:${action}`);
|
|
66
|
+
if (options.approvalMode !== 'read-only' && options.approvalMode !== 'auto')
|
|
67
|
+
throw new Error('SCHEDULED_EXECUTION_REQUIRES_READ_ONLY_OR_AUTO_APPROVAL');
|
|
68
|
+
const host = createLocalHost(options);
|
|
69
|
+
await host.init();
|
|
70
|
+
let currentRun;
|
|
71
|
+
const worker = new ScheduledTaskWorker(store, async (task, signal) => {
|
|
72
|
+
if (signal.aborted)
|
|
73
|
+
throw Object.assign(new Error('SCHEDULED_RUN_ABORTED'), { code: 'SCHEDULED_RUN_ABORTED' });
|
|
74
|
+
if (!task.workspace)
|
|
75
|
+
throw new Error('SCHEDULED_TASK_WORKSPACE_REQUIRED: recreate this legacy task');
|
|
76
|
+
const conversation = await host.createConversation({ cwd: task.workspace, title: `Scheduled: ${task.name}` });
|
|
77
|
+
const run = await host.sendMessage(conversation.id, { text: task.instruction });
|
|
78
|
+
currentRun = run;
|
|
79
|
+
const abort = () => { void run.cancel('SCHEDULED_WORKER_STOPPING').catch(() => { }); };
|
|
80
|
+
signal.addEventListener('abort', abort, { once: true });
|
|
81
|
+
if (signal.aborted)
|
|
82
|
+
abort();
|
|
83
|
+
try {
|
|
84
|
+
for await (const _event of run.events) {
|
|
85
|
+
if (signal.aborted) {
|
|
86
|
+
await run.cancel('SCHEDULED_WORKER_STOPPING');
|
|
87
|
+
throw Object.assign(new Error('SCHEDULED_RUN_ABORTED'), { code: 'SCHEDULED_RUN_ABORTED' });
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
const outcome = await run.outcome();
|
|
91
|
+
const taskOutcome = await run.taskOutcome();
|
|
92
|
+
if (signal.aborted)
|
|
93
|
+
throw new Error('SCHEDULED_RUN_ABORTED');
|
|
94
|
+
if (outcome.status !== 'succeeded')
|
|
95
|
+
throw Object.assign(new Error(`RUNTIME_${outcome.status.toUpperCase()}`), { code: `RUNTIME_${outcome.status.toUpperCase()}` });
|
|
96
|
+
if (taskOutcome?.status !== 'accepted')
|
|
97
|
+
throw Object.assign(new Error(`TASK_${(taskOutcome?.status ?? 'unverified').toUpperCase()}`), { code: `TASK_${(taskOutcome?.status ?? 'unverified').toUpperCase()}` });
|
|
98
|
+
}
|
|
99
|
+
catch (error) {
|
|
100
|
+
// Runtime cancellation may settle before an uncooperative tool stops.
|
|
101
|
+
// Keep the durable claim until this worker process has exited.
|
|
102
|
+
if (signal.aborted)
|
|
103
|
+
throw Object.assign(new Error('SCHEDULED_TASK_CANCELLATION_UNCONFIRMED'), { code: 'SCHEDULED_TASK_CANCELLATION_UNCONFIRMED', cause: error });
|
|
104
|
+
throw error;
|
|
105
|
+
}
|
|
106
|
+
finally {
|
|
107
|
+
signal.removeEventListener('abort', abort);
|
|
108
|
+
if (currentRun === run)
|
|
109
|
+
currentRun = undefined;
|
|
110
|
+
}
|
|
111
|
+
});
|
|
112
|
+
const controller = new AbortController();
|
|
113
|
+
const stop = () => { controller.abort(); void currentRun?.cancel('SCHEDULED_WORKER_STOPPING'); };
|
|
114
|
+
process.once('SIGINT', stop);
|
|
115
|
+
process.once('SIGTERM', stop);
|
|
116
|
+
let runOnceSummary;
|
|
117
|
+
try {
|
|
118
|
+
if (action === 'run-once') {
|
|
119
|
+
runOnceSummary = await worker.runOnce(controller.signal);
|
|
120
|
+
output(runOnceSummary, typeof flags.format === 'string' ? flags.format : 'text');
|
|
121
|
+
}
|
|
122
|
+
else
|
|
123
|
+
await worker.start(controller.signal);
|
|
124
|
+
}
|
|
125
|
+
finally {
|
|
126
|
+
process.removeListener('SIGINT', stop);
|
|
127
|
+
process.removeListener('SIGTERM', stop);
|
|
128
|
+
await host.close();
|
|
129
|
+
}
|
|
130
|
+
return runOnceSummary && runOnceSummary.failed > 0 ? 1 : 0;
|
|
131
|
+
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { access, mkdir, rm, writeFile } from 'node:fs/promises';
|
|
2
|
+
import { homedir } from 'node:os';
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
import { fileURLToPath } from 'node:url';
|
|
5
|
+
import { spawnSync } from 'node:child_process';
|
|
6
|
+
const serviceId = 'com.hunterzhu.pulse.scheduler';
|
|
7
|
+
function checked(file, args) { const result = spawnSync(file, args, { encoding: 'utf8' }); if (result.status !== 0)
|
|
8
|
+
throw new Error(`${file} ${args[0]} failed: ${(result.stderr || result.stdout || '').trim()}`); }
|
|
9
|
+
function xml(value) { return value.replaceAll('&', '&').replaceAll('<', '<').replaceAll('>', '>').replaceAll('"', '"').replaceAll("'", '''); }
|
|
10
|
+
function quoteSystemd(value) { return `"${value.replaceAll('\\', '\\\\').replaceAll('"', '\\"').replaceAll('%', '%%')}"`; }
|
|
11
|
+
export async function runServiceCommand(options, args) {
|
|
12
|
+
const [action] = args;
|
|
13
|
+
if (!['install', 'status', 'start', 'stop', 'uninstall'].includes(action ?? ''))
|
|
14
|
+
throw new Error('SERVICE_ACTION_REQUIRED');
|
|
15
|
+
const platform = process.platform;
|
|
16
|
+
const home = process.env.PULSE_HOME ?? homedir();
|
|
17
|
+
const executable = process.execPath;
|
|
18
|
+
const entry = fileURLToPath(new URL('../bin.js', import.meta.url));
|
|
19
|
+
const config = process.env.PULSE_CONFIG ?? join(home, '.pulse', 'config.json');
|
|
20
|
+
const data = options.dataDir ?? process.env.PULSE_DATA_DIR ?? join(home, '.pulse', 'data');
|
|
21
|
+
const cwd = options.cwd ?? process.cwd();
|
|
22
|
+
if (action === 'install') {
|
|
23
|
+
if (!['darwin', 'linux', 'win32'].includes(platform))
|
|
24
|
+
throw new Error(`SERVICE_PLATFORM_UNSUPPORTED:${platform}`);
|
|
25
|
+
if (platform === 'darwin') {
|
|
26
|
+
const directory = join(home, 'Library', 'LaunchAgents');
|
|
27
|
+
const file = join(directory, `${serviceId}.plist`);
|
|
28
|
+
await mkdir(directory, { recursive: true });
|
|
29
|
+
await writeFile(file, `<?xml version="1.0" encoding="UTF-8"?>\n<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">\n<plist version="1.0"><dict><key>Label</key><string>${serviceId}</string><key>ProgramArguments</key><array>${[executable, entry, 'schedule', 'daemon', '--approval-mode', options.approvalMode === 'auto' ? 'auto' : 'read-only'].map(v => `<string>${xml(v)}</string>`).join('')}</array><key>WorkingDirectory</key><string>${xml(cwd)}</string><key>EnvironmentVariables</key><dict><key>PULSE_HOME</key><string>${xml(home)}</string><key>PULSE_CONFIG</key><string>${xml(config)}</string><key>PULSE_DATA_DIR</key><string>${xml(data)}</string></dict><key>RunAtLoad</key><true/><key>KeepAlive</key><dict><key>SuccessfulExit</key><false/></dict><key>ThrottleInterval</key><integer>30</integer><key>StandardOutPath</key><string>${xml(join(data, 'service.log'))}</string><key>StandardErrorPath</key><string>${xml(join(data, 'service.error.log'))}</string></dict></plist>\n`, { mode: 0o600 });
|
|
30
|
+
await mkdir(data, { recursive: true });
|
|
31
|
+
checked('launchctl', ['bootstrap', `gui/${process.getuid?.() ?? 0}`, file]);
|
|
32
|
+
}
|
|
33
|
+
else if (platform === 'linux') {
|
|
34
|
+
const directory = join(home, '.config', 'systemd', 'user');
|
|
35
|
+
const file = join(directory, 'pulse-scheduler.service');
|
|
36
|
+
await mkdir(directory, { recursive: true });
|
|
37
|
+
await writeFile(file, `[Unit]\nDescription=Pulse scheduled task worker\n\n[Service]\nType=simple\nWorkingDirectory=${quoteSystemd(cwd)}\nEnvironment=${quoteSystemd(`PULSE_HOME=${home}`)}\nEnvironment=${quoteSystemd(`PULSE_CONFIG=${config}`)}\nEnvironment=${quoteSystemd(`PULSE_DATA_DIR=${data}`)}\nExecStart=${quoteSystemd(executable)} ${quoteSystemd(entry)} schedule daemon --approval-mode ${options.approvalMode === 'auto' ? 'auto' : 'read-only'}\nRestart=on-failure\nRestartSec=30s\n\n[Install]\nWantedBy=default.target\n`, { mode: 0o600 });
|
|
38
|
+
checked('systemctl', ['--user', 'daemon-reload']);
|
|
39
|
+
checked('systemctl', ['--user', 'enable', '--now', 'pulse-scheduler.service']);
|
|
40
|
+
}
|
|
41
|
+
else {
|
|
42
|
+
const task = 'Pulse Scheduler';
|
|
43
|
+
const command = `"${executable}" "${entry}" schedule daemon --approval-mode ${options.approvalMode === 'auto' ? 'auto' : 'read-only'}`;
|
|
44
|
+
checked('schtasks.exe', ['/Create', '/F', '/SC', 'ONLOGON', '/TN', task, '/TR', command]);
|
|
45
|
+
}
|
|
46
|
+
process.stdout.write(`Installed user scheduler service (${platform}); status: pulse service status\n`);
|
|
47
|
+
return 0;
|
|
48
|
+
}
|
|
49
|
+
if (platform === 'darwin') {
|
|
50
|
+
const file = join(home, 'Library', 'LaunchAgents', `${serviceId}.plist`);
|
|
51
|
+
if (action === 'status' && await access(file).then(() => false, () => true)) {
|
|
52
|
+
process.stdout.write('Scheduler service is not installed.\n');
|
|
53
|
+
return 0;
|
|
54
|
+
}
|
|
55
|
+
if (action === 'uninstall') {
|
|
56
|
+
spawnSync('launchctl', ['bootout', `gui/${process.getuid?.() ?? 0}`, file], { stdio: 'ignore' });
|
|
57
|
+
await rm(file, { force: true });
|
|
58
|
+
return 0;
|
|
59
|
+
}
|
|
60
|
+
const result = spawnSync('launchctl', [action === 'status' ? 'print' : action === 'start' ? 'kickstart' : 'kill', ...(action === 'start' ? ['-k'] : action === 'stop' ? ['SIGTERM'] : []), `gui/${process.getuid?.() ?? 0}/${serviceId}`], { encoding: 'utf8' });
|
|
61
|
+
process.stdout.write(result.stdout || result.stderr || `${action}: ${result.status === 0 ? 'ok' : 'not running'}\n`);
|
|
62
|
+
return result.status === 0 ? 0 : 1;
|
|
63
|
+
}
|
|
64
|
+
if (platform === 'linux') {
|
|
65
|
+
if (action === 'status' && await access(join(home, '.config', 'systemd', 'user', 'pulse-scheduler.service')).then(() => false, () => true)) {
|
|
66
|
+
process.stdout.write('Scheduler service is not installed.\n');
|
|
67
|
+
return 0;
|
|
68
|
+
}
|
|
69
|
+
const verb = action === 'status' ? 'status' : action === 'start' ? 'start' : action === 'stop' ? 'stop' : 'disable';
|
|
70
|
+
const result = spawnSync('systemctl', ['--user', verb, ...(action === 'uninstall' ? ['--now'] : []), 'pulse-scheduler.service'], { encoding: 'utf8' });
|
|
71
|
+
if (action === 'uninstall')
|
|
72
|
+
await rm(join(home, '.config', 'systemd', 'user', 'pulse-scheduler.service'), { force: true });
|
|
73
|
+
process.stdout.write(result.stdout || result.stderr || '');
|
|
74
|
+
return result.status === 0 ? 0 : 1;
|
|
75
|
+
}
|
|
76
|
+
const task = 'Pulse Scheduler';
|
|
77
|
+
const verb = action === 'status' ? '/Query' : action === 'start' ? '/Run' : action === 'stop' ? '/End' : '/Delete';
|
|
78
|
+
const result = spawnSync('schtasks.exe', [verb, '/TN', task, ...(action === 'uninstall' ? ['/F'] : [])], { encoding: 'utf8' });
|
|
79
|
+
process.stdout.write(result.stdout || result.stderr || '');
|
|
80
|
+
return result.status === 0 ? 0 : 1;
|
|
81
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { runOneShot } from './run.js';
|
|
2
|
+
const templates = [
|
|
3
|
+
{ version: 1, name: 'code-review', description: '审查当前工作区中的代码变更并给出有证据的问题清单。', parameters: ['focus'], deliverables: ['按严重程度排序的 findings', '文件路径与行号'], acceptance: ['每条问题有可复核代码证据', '不把风格偏好列作缺陷'], prompt: '审查当前工作区代码变更。重点:{{focus}}。只报告能从源码证明的问题,按严重度排序,给出路径和行号;若没有发现,说明审查范围和未覆盖部分。' },
|
|
4
|
+
{ version: 1, name: 'research', description: '围绕问题检索资料并整理可追溯结论。', parameters: ['question', 'sources'], deliverables: ['结论摘要', '来源及支持的主张'], acceptance: ['区分事实与推断', '每项关键结论附来源'], prompt: '研究问题:{{question}}。优先使用这些来源范围:{{sources}}。给出结论、证据链接、更新时间和不确定性;无法核验的内容标明未知。' },
|
|
5
|
+
{ version: 1, name: 'file-organization', description: '按规则整理工作区文件,先规划并遵守工具审批。', parameters: ['rules'], deliverables: ['整理后的文件', '移动清单'], acceptance: ['不覆盖已有目标', '总结每次移动'], prompt: '按以下规则整理当前工作区文件:{{rules}}。先检查现状并制定计划,再执行获批的移动;遇到目标冲突时跳过并报告。' },
|
|
6
|
+
{ version: 1, name: 'project-health', description: '检查项目入口、构建、测试和近期变更,报告健康状态。', parameters: ['scope'], deliverables: ['检查结果', '风险与建议'], acceptance: ['运行检查前说明具体命令', '区分已验证和推测'], prompt: '检查项目健康状态,范围:{{scope}}。阅读项目脚本与文档,运行适用的本地只读检查,汇报证据、失败和未覆盖项;不得安装、发布或修改文件。' },
|
|
7
|
+
];
|
|
8
|
+
function render(template, values) {
|
|
9
|
+
if (values.length !== template.parameters.length)
|
|
10
|
+
throw new Error(`TEMPLATE_ARGUMENTS_REQUIRED:${template.name}:${template.parameters.join(',')}`);
|
|
11
|
+
let prompt = template.prompt;
|
|
12
|
+
template.parameters.forEach((name, index) => { prompt = prompt.replaceAll(`{{${name}}}`, values[index] ?? ''); });
|
|
13
|
+
return prompt;
|
|
14
|
+
}
|
|
15
|
+
export async function runTemplateCommand(options, args, format, version) {
|
|
16
|
+
const [action, name, ...values] = args;
|
|
17
|
+
if (action === 'list') {
|
|
18
|
+
process.stdout.write(templates.map(item => `${item.name}\t${item.description}`).join('\n') + '\n');
|
|
19
|
+
return 0;
|
|
20
|
+
}
|
|
21
|
+
const template = templates.find(item => item.name === name);
|
|
22
|
+
if (!template)
|
|
23
|
+
throw new Error(action === 'run' || action === 'show' ? `TEMPLATE_NOT_FOUND:${name ?? ''}` : 'TEMPLATE_ACTION_REQUIRED');
|
|
24
|
+
if (action === 'show') {
|
|
25
|
+
process.stdout.write(`${JSON.stringify(template, null, 2)}\n`);
|
|
26
|
+
return 0;
|
|
27
|
+
}
|
|
28
|
+
if (action !== 'run')
|
|
29
|
+
throw new Error('TEMPLATE_ACTION_REQUIRED');
|
|
30
|
+
return runOneShot(options, render(template, values), format, version);
|
|
31
|
+
}
|