@phyzess/pappra 0.1.0-beta.12 → 0.1.0-beta.14
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 +2 -1
- package/assets/integrations/skills/pappra/references/operations.md +2 -0
- package/dist/app/collab-from.d.ts +35 -0
- package/dist/app/collab-from.js +84 -0
- package/dist/app/doctor.js +4 -2
- package/dist/app/host-mcp.d.ts +25 -0
- package/dist/app/host-mcp.js +227 -0
- package/dist/app/host-probe.d.ts +16 -0
- package/dist/app/host-probe.js +125 -0
- package/dist/app/task-pin.d.ts +23 -0
- package/dist/app/task-pin.js +113 -0
- package/dist/cli/commands/docs.js +12 -76
- package/dist/cli/commands/host.d.ts +3 -1
- package/dist/cli/commands/host.js +55 -1
- package/dist/cli/commands/mcp.d.ts +8 -0
- package/dist/cli/commands/mcp.js +13 -0
- package/dist/cli/commands/task.d.ts +10 -0
- package/dist/cli/commands/task.js +76 -0
- package/dist/cli/run.js +134 -4
- package/dist/hosts/integration/hook.js +4 -1
- package/dist/hosts/integration/pappra-extension-runtime.mjs +50 -8
- package/dist/hosts/integration/pappra-hook.mjs +50 -8
- package/dist/hosts/pilot.d.ts +77 -77
- package/dist/knowledge/scan.d.ts +30 -0
- package/dist/knowledge/scan.js +91 -0
- package/dist/mcp/server.d.ts +11 -0
- package/dist/mcp/server.js +518 -0
- package/dist/schemas/workspace.d.ts +6 -0
- package/dist/schemas/workspace.js +4 -0
- package/package.json +2 -1
- package/skills/pappra/references/operations.md +2 -0
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import { randomUUID } from 'node:crypto';
|
|
2
|
+
import { mkdir, readFile, rename, writeFile } from 'node:fs/promises';
|
|
3
|
+
import { dirname, join } from 'node:path';
|
|
4
|
+
import { readTextFileBounded } from '../infra/fs.js';
|
|
5
|
+
import { workspaceConfigSchema } from '../schemas/workspace.js';
|
|
6
|
+
export const TASK_PIN_SCHEMA = 'pappra/task-pin/1';
|
|
7
|
+
const TASK_ID = /^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/;
|
|
8
|
+
function pinPath(root) {
|
|
9
|
+
return join(root, '.pappra', 'local', 'current-task');
|
|
10
|
+
}
|
|
11
|
+
export async function readTaskPin(root) {
|
|
12
|
+
const read = await readTextFileBounded(pinPath(root), 64 * 1024);
|
|
13
|
+
if (!read.ok)
|
|
14
|
+
return null;
|
|
15
|
+
try {
|
|
16
|
+
const value = JSON.parse(read.value.text);
|
|
17
|
+
if (typeof value !== 'object' ||
|
|
18
|
+
value === null ||
|
|
19
|
+
!('schema' in value) ||
|
|
20
|
+
value.schema !== TASK_PIN_SCHEMA ||
|
|
21
|
+
!('taskId' in value) ||
|
|
22
|
+
typeof value.taskId !== 'string' ||
|
|
23
|
+
!TASK_ID.test(value.taskId)) {
|
|
24
|
+
return null;
|
|
25
|
+
}
|
|
26
|
+
return value;
|
|
27
|
+
}
|
|
28
|
+
catch {
|
|
29
|
+
return null;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
export async function pinTask(root, taskId) {
|
|
33
|
+
if (!TASK_ID.test(taskId)) {
|
|
34
|
+
return {
|
|
35
|
+
ok: false,
|
|
36
|
+
message: 'task id must be a short identifier (letters, digits, dot, dash, underscore).',
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
const pin = {
|
|
40
|
+
schema: TASK_PIN_SCHEMA,
|
|
41
|
+
taskId,
|
|
42
|
+
pinnedAt: new Date().toISOString(),
|
|
43
|
+
};
|
|
44
|
+
const target = pinPath(root);
|
|
45
|
+
await mkdir(dirname(target), { recursive: true, mode: 0o700 });
|
|
46
|
+
const temporary = `${target}.tmp-${randomUUID()}`;
|
|
47
|
+
await writeFile(temporary, `${JSON.stringify(pin, null, 2)}\n`, {
|
|
48
|
+
mode: 0o600,
|
|
49
|
+
});
|
|
50
|
+
await rename(temporary, target);
|
|
51
|
+
return pin;
|
|
52
|
+
}
|
|
53
|
+
export async function unpinTask(root) {
|
|
54
|
+
const target = pinPath(root);
|
|
55
|
+
const { rm } = await import('node:fs/promises');
|
|
56
|
+
await rm(target, { force: true });
|
|
57
|
+
return { ok: true };
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Toggles the workspace-level auto-resume switch. The hook only injects a resume
|
|
61
|
+
* packet when this switch is on and a task pin exists; neither is inferred.
|
|
62
|
+
*/
|
|
63
|
+
export async function setAutoResume(root, enabled) {
|
|
64
|
+
const configPath = join(root, '.pappra', 'workspace.json');
|
|
65
|
+
const read = await readTextFileBounded(configPath, 1024 * 1024);
|
|
66
|
+
if (!read.ok) {
|
|
67
|
+
return { ok: false, message: read.message };
|
|
68
|
+
}
|
|
69
|
+
let raw;
|
|
70
|
+
try {
|
|
71
|
+
raw = JSON.parse(read.value.text);
|
|
72
|
+
}
|
|
73
|
+
catch (error) {
|
|
74
|
+
return {
|
|
75
|
+
ok: false,
|
|
76
|
+
message: `workspace.json is not valid JSON: ${error instanceof Error ? error.message : String(error)}`,
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
const parsed = workspaceConfigSchema.safeParse({
|
|
80
|
+
...(typeof raw === 'object' && raw !== null ? raw : {}),
|
|
81
|
+
continuity: { autoResume: enabled },
|
|
82
|
+
});
|
|
83
|
+
if (!parsed.success) {
|
|
84
|
+
return {
|
|
85
|
+
ok: false,
|
|
86
|
+
message: `workspace.json rejected the continuity switch: ${parsed.error.issues[0]?.message ?? 'invalid'}`,
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
const backup = join(root, '.pappra', 'local', 'backups', `task-pin-${randomUUID()}`, 'workspace.json');
|
|
90
|
+
await mkdir(dirname(backup), { recursive: true, mode: 0o700 });
|
|
91
|
+
await writeFile(backup, read.value.text, { mode: 0o600 });
|
|
92
|
+
const temporary = `${configPath}.tmp-${randomUUID()}`;
|
|
93
|
+
await writeFile(temporary, `${JSON.stringify(parsed.data, null, 2)}\n`);
|
|
94
|
+
await rename(temporary, configPath);
|
|
95
|
+
return { ok: true };
|
|
96
|
+
}
|
|
97
|
+
export async function readAutoResume(root) {
|
|
98
|
+
const read = await readFile(join(root, '.pappra', 'workspace.json'), 'utf8');
|
|
99
|
+
try {
|
|
100
|
+
const raw = JSON.parse(read);
|
|
101
|
+
if (typeof raw !== 'object' || raw === null || !('continuity' in raw)) {
|
|
102
|
+
return false;
|
|
103
|
+
}
|
|
104
|
+
const continuity = raw.continuity;
|
|
105
|
+
return (typeof continuity === 'object' &&
|
|
106
|
+
continuity !== null &&
|
|
107
|
+
'autoResume' in continuity &&
|
|
108
|
+
continuity.autoResume === true);
|
|
109
|
+
}
|
|
110
|
+
catch {
|
|
111
|
+
return false;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { discoverWorkspace } from '../../workspace/discover.js';
|
|
2
1
|
import { resolveWorkspaceRoot } from '../../workspace/resolve.js';
|
|
2
|
+
import { scanDiscoveredDocuments } from '../../knowledge/scan.js';
|
|
3
3
|
import { formatDiagnostic } from '../../schemas/diagnostics.js';
|
|
4
4
|
import { docsArchiveReportSchema, docsCheckReportSchema, docsListReportSchema, } from '../../schemas/document.js';
|
|
5
5
|
import { KnowledgeService, } from '../../knowledge/index.js';
|
|
@@ -10,96 +10,32 @@ export const docsUsage = 'pappra docs <list|check|archive|scan> [options]\n' +
|
|
|
10
10
|
' scan [--apply] [--json]\n\n' +
|
|
11
11
|
'scan registers documents discovered under the configured knowledge sources; without\n' +
|
|
12
12
|
'--apply it only reports the candidates.';
|
|
13
|
-
function classifyScanKind(path) {
|
|
14
|
-
const lower = path.toLowerCase();
|
|
15
|
-
if (lower.endsWith('agents.md') || lower.endsWith('claude.md'))
|
|
16
|
-
return 'rule';
|
|
17
|
-
if (lower.includes('/research/'))
|
|
18
|
-
return 'research';
|
|
19
|
-
if (lower.includes('/adr/') || lower.includes('decision'))
|
|
20
|
-
return 'decision';
|
|
21
|
-
if (lower.includes('review') || lower.includes('retro'))
|
|
22
|
-
return 'review';
|
|
23
|
-
if (lower.includes('plan'))
|
|
24
|
-
return 'plan';
|
|
25
|
-
return 'architecture';
|
|
26
|
-
}
|
|
27
13
|
async function runDocsScan(options, output, root) {
|
|
28
|
-
const
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
}
|
|
33
|
-
const service = new KnowledgeService(root);
|
|
34
|
-
const listed = await service.list({ includePrivate: true });
|
|
35
|
-
const registered = new Set(listed.records.flatMap((record) => record.location.kind === 'workspace' ? [record.location.path] : []));
|
|
36
|
-
const sources = config.knowledge.sources;
|
|
37
|
-
const visibility = config.knowledge.taskVisibility === 'shared' ? 'shareable' : 'private';
|
|
38
|
-
const candidates = (discovery.documents ?? []).flatMap((document) => {
|
|
39
|
-
const path = document.workspaceRelativePath;
|
|
40
|
-
if (path === null)
|
|
41
|
-
return [];
|
|
42
|
-
if (!sources.some((source) => path === source || path.startsWith(`${source}/`))) {
|
|
43
|
-
return [];
|
|
44
|
-
}
|
|
45
|
-
if (registered.has(path))
|
|
46
|
-
return [];
|
|
47
|
-
return [{ path, kind: classifyScanKind(path), visibility }];
|
|
48
|
-
});
|
|
14
|
+
const result = await scanDiscoveredDocuments(root, { apply: options.apply });
|
|
15
|
+
if ('message' in result) {
|
|
16
|
+
return invalidArguments(output, options.json, result.message);
|
|
17
|
+
}
|
|
49
18
|
if (!options.apply) {
|
|
50
19
|
if (options.json) {
|
|
51
|
-
output.stdout(`${JSON.stringify(
|
|
52
|
-
ok: true,
|
|
53
|
-
schema: 'pappra/docs-scan/1',
|
|
54
|
-
mode: 'plan',
|
|
55
|
-
workspaceRoot: root,
|
|
56
|
-
candidates,
|
|
57
|
-
registered: registered.size,
|
|
58
|
-
nextActions: [
|
|
59
|
-
'Rerun with --apply to register these documents in the knowledge catalog.',
|
|
60
|
-
],
|
|
61
|
-
})}\n`);
|
|
20
|
+
output.stdout(`${JSON.stringify(result)}\n`);
|
|
62
21
|
}
|
|
63
22
|
else {
|
|
64
|
-
output.stdout(`Scan candidates: ${candidates.length}\n`);
|
|
65
|
-
for (const candidate of candidates) {
|
|
23
|
+
output.stdout(`Scan candidates: ${result.candidates.length}\n`);
|
|
24
|
+
for (const candidate of result.candidates) {
|
|
66
25
|
output.stdout(` ${candidate.kind} ${candidate.path}\n`);
|
|
67
26
|
}
|
|
68
27
|
output.stdout('Plan only; rerun with --apply to register them.\n');
|
|
69
28
|
}
|
|
70
29
|
return 0;
|
|
71
30
|
}
|
|
72
|
-
const registeredNow = [];
|
|
73
|
-
const diagnostics = [];
|
|
74
|
-
for (const candidate of candidates) {
|
|
75
|
-
const result = await service.register({
|
|
76
|
-
location: { kind: 'workspace', path: candidate.path },
|
|
77
|
-
kind: candidate.kind,
|
|
78
|
-
maturity: 'draft',
|
|
79
|
-
owner: 'human',
|
|
80
|
-
visibility: candidate.visibility,
|
|
81
|
-
});
|
|
82
|
-
diagnostics.push(...result.diagnostics);
|
|
83
|
-
if (result.ok)
|
|
84
|
-
registeredNow.push(result.record);
|
|
85
|
-
}
|
|
86
|
-
const ok = diagnostics.every((entry) => entry.severity !== 'error');
|
|
87
31
|
if (options.json) {
|
|
88
|
-
output.stdout(`${JSON.stringify(
|
|
89
|
-
ok,
|
|
90
|
-
schema: 'pappra/docs-scan/1',
|
|
91
|
-
mode: 'apply',
|
|
92
|
-
workspaceRoot: root,
|
|
93
|
-
registered: registeredNow,
|
|
94
|
-
skipped: candidates.length - registeredNow.length,
|
|
95
|
-
diagnostics,
|
|
96
|
-
})}\n`);
|
|
32
|
+
output.stdout(`${JSON.stringify(result)}\n`);
|
|
97
33
|
}
|
|
98
34
|
else {
|
|
99
|
-
output.stdout(`Registered ${
|
|
100
|
-
printDiagnostics(output, diagnostics);
|
|
35
|
+
output.stdout(`Registered ${result.registered.length} document(s).\n`);
|
|
36
|
+
printDiagnostics(output, result.diagnostics);
|
|
101
37
|
}
|
|
102
|
-
return ok ? 0 : 1;
|
|
38
|
+
return result.ok ? 0 : 1;
|
|
103
39
|
}
|
|
104
40
|
function printDiagnostics(output, diagnostics) {
|
|
105
41
|
for (const diagnostic of diagnostics) {
|
|
@@ -4,11 +4,13 @@ export declare const hostUsage: string;
|
|
|
4
4
|
export interface HostCommandOptions {
|
|
5
5
|
readonly cwd: string;
|
|
6
6
|
readonly json: boolean;
|
|
7
|
-
readonly action: 'detect' | 'describe' | 'plan' | 'install' | 'verify' | 'bundle' | null;
|
|
7
|
+
readonly action: 'detect' | 'describe' | 'plan' | 'install' | 'verify' | 'bundle' | 'mcp' | 'probe' | null;
|
|
8
8
|
readonly host: HostName | null;
|
|
9
9
|
readonly hostRoot: string | null;
|
|
10
10
|
readonly hostVersion: string | null;
|
|
11
11
|
readonly apply: boolean;
|
|
12
|
+
readonly force: boolean;
|
|
13
|
+
readonly subAction: string | null;
|
|
12
14
|
readonly out: string | null;
|
|
13
15
|
readonly mode: 'project-settings' | 'plugin' | null;
|
|
14
16
|
readonly marketplace: boolean;
|
|
@@ -2,9 +2,11 @@ import { homedir } from 'node:os';
|
|
|
2
2
|
import { join, resolve } from 'node:path';
|
|
3
3
|
import { HostIntegrationService } from '../../hosts/integration/service.js';
|
|
4
4
|
import { exportHostPluginBundle } from '../../hosts/integration/bundle-export.js';
|
|
5
|
+
import { applyHostMcp, planHostMcp, verifyHostMcp, } from '../../app/host-mcp.js';
|
|
6
|
+
import { probeHost } from '../../app/host-probe.js';
|
|
5
7
|
import { HOST_SUPPORT, isSupportedVersion, } from '../../hosts/integration/support.js';
|
|
6
8
|
import { getPackageInfo } from '../../package-info.js';
|
|
7
|
-
export const hostUsage = 'pappra host <detect|describe|plan|install|verify|bundle> --host <pi|codex|claude>\n' +
|
|
9
|
+
export const hostUsage = 'pappra host <detect|describe|plan|install|verify|bundle|mcp> --host <pi|codex|claude>\n' +
|
|
8
10
|
' [--host-root <dir>] [--host-version <version>] [--out <dir>]\n' +
|
|
9
11
|
' [--mode project-settings|plugin] [--apply] [--json]\n' +
|
|
10
12
|
'\n' +
|
|
@@ -80,6 +82,58 @@ export async function runHostCommand(options, output) {
|
|
|
80
82
|
? {}
|
|
81
83
|
: { hostVersion: options.hostVersion }),
|
|
82
84
|
};
|
|
85
|
+
if (options.action === 'mcp') {
|
|
86
|
+
if (options.subAction !== 'plan' &&
|
|
87
|
+
options.subAction !== 'install' &&
|
|
88
|
+
options.subAction !== 'verify') {
|
|
89
|
+
output.stderr('pappra host mcp requires plan, install, or verify.\n');
|
|
90
|
+
return 2;
|
|
91
|
+
}
|
|
92
|
+
const mcpPlan = await planHostMcp(options.host, options.cwd);
|
|
93
|
+
const result = options.subAction === 'verify'
|
|
94
|
+
? await verifyHostMcp(mcpPlan)
|
|
95
|
+
: options.subAction === 'install' && options.apply
|
|
96
|
+
? await applyHostMcp(mcpPlan, { force: options.force })
|
|
97
|
+
: {
|
|
98
|
+
...mcpPlan,
|
|
99
|
+
applied: false,
|
|
100
|
+
verified: false,
|
|
101
|
+
detail: null,
|
|
102
|
+
};
|
|
103
|
+
if (options.json) {
|
|
104
|
+
output.stdout(`${JSON.stringify({ schema: 'pappra/host-mcp/1', ...result })}\n`);
|
|
105
|
+
}
|
|
106
|
+
else {
|
|
107
|
+
output.stdout(`Host ${result.host} MCP: ${result.state}\n`);
|
|
108
|
+
if (result.target !== null)
|
|
109
|
+
output.stdout(` target: ${result.target}\n`);
|
|
110
|
+
if (result.command !== null) {
|
|
111
|
+
output.stdout(` command: ${result.command.join(' ')}\n`);
|
|
112
|
+
}
|
|
113
|
+
output.stdout(` ${result.message}\n`);
|
|
114
|
+
if (result.detail !== null)
|
|
115
|
+
output.stdout(` detail: ${result.detail}\n`);
|
|
116
|
+
if (options.subAction === 'install' && !options.apply) {
|
|
117
|
+
output.stdout(' plan only; rerun with --apply to write it.\n');
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
return result.verified || (!options.apply && options.subAction !== 'verify')
|
|
121
|
+
? 0
|
|
122
|
+
: 1;
|
|
123
|
+
}
|
|
124
|
+
if (options.action === 'probe') {
|
|
125
|
+
const result = probeHost(options.host);
|
|
126
|
+
if (options.json) {
|
|
127
|
+
output.stdout(`${JSON.stringify({ schema: 'pappra/host-probe/1', ...result })}\n`);
|
|
128
|
+
}
|
|
129
|
+
else {
|
|
130
|
+
output.stdout(`Host ${result.host} probe: ${result.ok ? 'ok' : 'failed'}\n`);
|
|
131
|
+
output.stdout(` version: ${result.version ?? '(missing)'} / pinned ${result.pinnedVersion}\n`);
|
|
132
|
+
output.stdout(` protocol: ${result.protocol}\n`);
|
|
133
|
+
output.stdout(` ${result.message}\n`);
|
|
134
|
+
}
|
|
135
|
+
return result.ok ? 0 : 1;
|
|
136
|
+
}
|
|
83
137
|
if (options.action === 'detect' || options.action === 'describe') {
|
|
84
138
|
const capabilities = service.describe(options.host);
|
|
85
139
|
if (options.json) {
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { CliOutput } from '../output.js';
|
|
2
|
+
export declare const mcpUsage: string;
|
|
3
|
+
export interface McpCommandOptions {
|
|
4
|
+
readonly cwd: string;
|
|
5
|
+
readonly root: string | null;
|
|
6
|
+
readonly json: boolean;
|
|
7
|
+
}
|
|
8
|
+
export declare function runMcpCommand(options: McpCommandOptions, output: CliOutput): Promise<number>;
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { runMcpServer } from '../../mcp/server.js';
|
|
2
|
+
export const mcpUsage = 'pappra mcp [--root <dir>] [--json]\n\n' +
|
|
3
|
+
'Starts the Pappra MCP server on stdio for the selected workspace. The server exposes\n' +
|
|
4
|
+
'read-only tools (workspace_status, resume, docs_list, docs_check, code_status,\n' +
|
|
5
|
+
'code_query, runtime_status). stdout carries MCP JSON-RPC; use --help outside a host.';
|
|
6
|
+
export async function runMcpCommand(options, output) {
|
|
7
|
+
const root = options.root ?? options.cwd;
|
|
8
|
+
if (options.json) {
|
|
9
|
+
output.stderr('pappra mcp: --json is not used; the server speaks MCP JSON-RPC on stdout.\n');
|
|
10
|
+
}
|
|
11
|
+
await runMcpServer(root);
|
|
12
|
+
return 0;
|
|
13
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { CliOutput } from '../output.js';
|
|
2
|
+
export declare const taskUsage: string;
|
|
3
|
+
export interface TaskCommandOptions {
|
|
4
|
+
readonly cwd: string;
|
|
5
|
+
readonly action: 'pin' | 'unpin' | 'show' | null;
|
|
6
|
+
readonly taskId: string | null;
|
|
7
|
+
readonly autoResume: boolean;
|
|
8
|
+
readonly json: boolean;
|
|
9
|
+
}
|
|
10
|
+
export declare function runTaskCommand(options: TaskCommandOptions, output: CliOutput): Promise<number>;
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { resolveWorkspaceRoot } from '../../workspace/resolve.js';
|
|
2
|
+
import { pinTask, readAutoResume, readTaskPin, setAutoResume, unpinTask, } from '../../app/task-pin.js';
|
|
3
|
+
export const taskUsage = 'pappra task <pin|unpin|show> [taskId] [--auto-resume] [--json]\n\n' +
|
|
4
|
+
'pin records the current task in .pappra/local/current-task. Hooks inject a resume\n' +
|
|
5
|
+
'packet only when the workspace continuity.autoResume switch is on (--auto-resume).';
|
|
6
|
+
function failure(output, options, message) {
|
|
7
|
+
if (options.json) {
|
|
8
|
+
output.stdout(`${JSON.stringify({ ok: false, code: 'task_invalid', message })}\n`);
|
|
9
|
+
}
|
|
10
|
+
else {
|
|
11
|
+
output.stderr(`pappra task: ${message}\n`);
|
|
12
|
+
}
|
|
13
|
+
return 2;
|
|
14
|
+
}
|
|
15
|
+
export async function runTaskCommand(options, output) {
|
|
16
|
+
const resolution = await resolveWorkspaceRoot({ startPath: options.cwd });
|
|
17
|
+
const root = resolution.root;
|
|
18
|
+
if (root === null) {
|
|
19
|
+
return failure(output, options, 'task requires a registered Pappra workspace above the current directory.');
|
|
20
|
+
}
|
|
21
|
+
if (options.action === 'pin') {
|
|
22
|
+
if (options.taskId === null) {
|
|
23
|
+
return failure(output, options, 'task pin requires a task id.');
|
|
24
|
+
}
|
|
25
|
+
const pinned = await pinTask(root, options.taskId);
|
|
26
|
+
if ('message' in pinned) {
|
|
27
|
+
return failure(output, options, pinned.message);
|
|
28
|
+
}
|
|
29
|
+
if (options.autoResume) {
|
|
30
|
+
const switched = await setAutoResume(root, true);
|
|
31
|
+
if ('message' in switched) {
|
|
32
|
+
return failure(output, options, switched.message);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
const report = {
|
|
36
|
+
ok: true,
|
|
37
|
+
action: 'pin',
|
|
38
|
+
task: pinned.taskId,
|
|
39
|
+
autoResume: options.autoResume ? true : await readAutoResume(root),
|
|
40
|
+
};
|
|
41
|
+
output.stdout(options.json
|
|
42
|
+
? `${JSON.stringify(report)}\n`
|
|
43
|
+
: `Pinned task ${report.task}; autoResume ${report.autoResume ? 'on' : 'off'}.\n`);
|
|
44
|
+
return 0;
|
|
45
|
+
}
|
|
46
|
+
if (options.action === 'unpin') {
|
|
47
|
+
await unpinTask(root);
|
|
48
|
+
const report = { ok: true, action: 'unpin' };
|
|
49
|
+
output.stdout(options.json
|
|
50
|
+
? `${JSON.stringify(report)}\n`
|
|
51
|
+
: 'Task pin removed; autoResume stays unchanged.\n');
|
|
52
|
+
return 0;
|
|
53
|
+
}
|
|
54
|
+
if (options.action === 'show') {
|
|
55
|
+
const pin = await readTaskPin(root);
|
|
56
|
+
const report = {
|
|
57
|
+
ok: true,
|
|
58
|
+
action: 'show',
|
|
59
|
+
pin,
|
|
60
|
+
autoResume: await readAutoResume(root),
|
|
61
|
+
};
|
|
62
|
+
output.stdout(options.json
|
|
63
|
+
? `${JSON.stringify(report)}\n`
|
|
64
|
+
: pin === null
|
|
65
|
+
? `No task pin; autoResume ${report.autoResume ? 'on' : 'off'}.\n`
|
|
66
|
+
: `Pinned task ${pin.taskId} at ${pin.pinnedAt}; autoResume ${report.autoResume ? 'on' : 'off'}.\n`);
|
|
67
|
+
return 0;
|
|
68
|
+
}
|
|
69
|
+
if (options.json) {
|
|
70
|
+
output.stdout(`${JSON.stringify({ commands: ['pin', 'unpin', 'show'], usage: taskUsage })}\n`);
|
|
71
|
+
}
|
|
72
|
+
else {
|
|
73
|
+
output.stdout(taskUsage);
|
|
74
|
+
}
|
|
75
|
+
return 0;
|
|
76
|
+
}
|