@phyzess/pappra 0.1.0-beta.11 → 0.1.0-beta.13

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.
Files changed (37) hide show
  1. package/README.md +8 -2
  2. package/assets/integrations/skills/pappra/references/operations.md +5 -0
  3. package/dist/app/collab-from.d.ts +35 -0
  4. package/dist/app/collab-from.js +84 -0
  5. package/dist/app/host-mcp.d.ts +25 -0
  6. package/dist/app/host-mcp.js +227 -0
  7. package/dist/app/host-probe.d.ts +16 -0
  8. package/dist/app/host-probe.js +125 -0
  9. package/dist/app/init.d.ts +15 -0
  10. package/dist/app/init.js +82 -3
  11. package/dist/app/onboard.d.ts +60 -0
  12. package/dist/app/onboard.js +259 -0
  13. package/dist/app/task-pin.d.ts +23 -0
  14. package/dist/app/task-pin.js +113 -0
  15. package/dist/cli/commands/docs.js +12 -76
  16. package/dist/cli/commands/host.d.ts +3 -1
  17. package/dist/cli/commands/host.js +55 -1
  18. package/dist/cli/commands/init.d.ts +0 -8
  19. package/dist/cli/commands/init.js +5 -126
  20. package/dist/cli/commands/mcp.d.ts +8 -0
  21. package/dist/cli/commands/mcp.js +13 -0
  22. package/dist/cli/commands/onboard.d.ts +14 -0
  23. package/dist/cli/commands/onboard.js +225 -0
  24. package/dist/cli/commands/task.d.ts +10 -0
  25. package/dist/cli/commands/task.js +76 -0
  26. package/dist/cli/run.js +206 -7
  27. package/dist/hosts/integration/hook.js +4 -1
  28. package/dist/hosts/integration/pappra-extension-runtime.mjs +50 -8
  29. package/dist/hosts/integration/pappra-hook.mjs +50 -8
  30. package/dist/knowledge/scan.d.ts +30 -0
  31. package/dist/knowledge/scan.js +91 -0
  32. package/dist/mcp/server.d.ts +11 -0
  33. package/dist/mcp/server.js +518 -0
  34. package/dist/schemas/workspace.d.ts +6 -0
  35. package/dist/schemas/workspace.js +4 -0
  36. package/package.json +2 -1
  37. package/skills/pappra/references/operations.md +5 -0
@@ -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) {
@@ -1,6 +1,4 @@
1
- import type { Diagnostic } from '../../schemas/diagnostics.js';
2
1
  import { type InitChoices } from '../../schemas/init.js';
3
- import { type RuntimeInstallResult } from '../../install/runtime.js';
4
2
  import { type InitContext } from '../../app/init.js';
5
3
  import type { CliOutput } from '../output.js';
6
4
  export interface SelectOption {
@@ -41,12 +39,6 @@ export interface InitCommandOptions {
41
39
  /** Rebuilds workspace.json from the selections even when a valid config exists. */
42
40
  readonly reconfigure?: boolean;
43
41
  }
44
- export interface RuntimeFlowResult {
45
- readonly ok: boolean;
46
- readonly completion: boolean;
47
- readonly runtime: RuntimeInstallResult | null;
48
- readonly diagnostics: readonly Diagnostic[];
49
- }
50
42
  export interface InitCommandDeps {
51
43
  readonly ui?: WizardUi;
52
44
  readonly interactive?: boolean;
@@ -2,12 +2,10 @@ import { basename, resolve } from 'node:path';
2
2
  import * as prompts from '@clack/prompts';
3
3
  import { diagnosticCodes } from '../../schemas/errors.js';
4
4
  import { INIT_CHOICES_SCHEMA, parseInitChoices, } from '../../schemas/init.js';
5
- import { observePath, readTextFileBounded } from '../../infra/fs.js';
6
- import { getPackageInfo, getPackageRoot } from '../../package-info.js';
7
- import { installRuntime, } from '../../install/runtime.js';
8
- import { updateMachineRuntime } from '../../install/apply.js';
9
- import { inspectMachineConfig } from '../../workspace/config.js';
10
- import { applyInitPlan, inspectInitContext, listKnowledgeCandidates, deriveInitChoices, listRepositoryCandidates, planWorkspaceInit, } from '../../app/init.js';
5
+ import { readTextFileBounded } from '../../infra/fs.js';
6
+ import { getPackageInfo } from '../../package-info.js';
7
+ import { installRuntime } from '../../install/runtime.js';
8
+ import { applyInitPlan, bindAndRecordRuntime, ensureDefaultRuntime, inspectInitContext, listKnowledgeCandidates, deriveInitChoices, listRepositoryCandidates, planWorkspaceInit, } from '../../app/init.js';
11
9
  export const clackWizardUi = {
12
10
  intro: (message) => prompts.intro(message),
13
11
  outro: (message) => prompts.outro(message),
@@ -250,84 +248,6 @@ function runtimeOptionsFailure(message, nextAction) {
250
248
  nextAction,
251
249
  };
252
250
  }
253
- async function bindAndRecordRuntime(root, installed) {
254
- if (!installed.ok || installed.manifest === null) {
255
- return {
256
- ok: false,
257
- completion: false,
258
- runtime: installed,
259
- diagnostics: installed.diagnostics,
260
- };
261
- }
262
- const updated = await updateMachineRuntime(root, installed.manifest);
263
- if (!updated.ok) {
264
- return {
265
- ok: false,
266
- completion: false,
267
- runtime: installed,
268
- diagnostics: updated.diagnostics,
269
- };
270
- }
271
- const followUp = await planWorkspaceInit({ root });
272
- if (!followUp.ok) {
273
- return {
274
- ok: false,
275
- completion: false,
276
- runtime: installed,
277
- diagnostics: followUp.diagnostics,
278
- };
279
- }
280
- if (followUp.bundle.plan.files.length > 0 ||
281
- followUp.bundle.plan.externalActions.length > 0) {
282
- return {
283
- ok: false,
284
- completion: false,
285
- runtime: installed,
286
- diagnostics: [
287
- {
288
- code: diagnosticCodes.initRuntimeSourceRequired,
289
- severity: 'error',
290
- message: 'Runtime installation succeeded but init still has pending file or external actions.',
291
- source: 'init runtime',
292
- pointer: null,
293
- hint: 'Run pappra init again to review the remaining plan; initialization is not complete.',
294
- },
295
- ],
296
- };
297
- }
298
- return {
299
- ok: true,
300
- completion: true,
301
- runtime: installed,
302
- diagnostics: [],
303
- };
304
- }
305
- /**
306
- * Converges the runtime binding by installing a durable copy of the package that
307
- * is currently executing. Reusing an already verified copy is cheap: the install
308
- * machinery short-circuits before packing.
309
- */
310
- async function ensureDefaultRuntime(root, targetVersion) {
311
- const machine = await inspectMachineConfig(root);
312
- const binding = machine.value?.runtime ?? null;
313
- if (binding !== null && binding.version === targetVersion) {
314
- const executable = await observePath(binding.executable);
315
- if (executable.kind === 'file') {
316
- return {
317
- ok: true,
318
- completion: true,
319
- runtime: null,
320
- diagnostics: [],
321
- };
322
- }
323
- }
324
- const installed = await installRuntime({
325
- version: targetVersion,
326
- source: { kind: 'self', path: getPackageRoot() },
327
- packageName: getPackageInfo().name,
328
- });
329
- return bindAndRecordRuntime(root, installed);
330
- }
331
251
  async function installAndRecordRuntime(root, targetVersion, options) {
332
252
  const tarball = options.runtimeTarball ?? null;
333
253
  const requestedVersion = options.runtimeVersion ?? null;
@@ -400,48 +320,7 @@ async function installAndRecordRuntime(root, targetVersion, options) {
400
320
  diagnostics: installed.diagnostics,
401
321
  };
402
322
  }
403
- const updated = await updateMachineRuntime(root, installed.manifest);
404
- if (!updated.ok) {
405
- return {
406
- ok: false,
407
- completion: false,
408
- runtime: installed,
409
- diagnostics: updated.diagnostics,
410
- };
411
- }
412
- const followUp = await planWorkspaceInit({ root });
413
- if (!followUp.ok) {
414
- return {
415
- ok: false,
416
- completion: false,
417
- runtime: installed,
418
- diagnostics: followUp.diagnostics,
419
- };
420
- }
421
- if (followUp.bundle.plan.files.length > 0 ||
422
- followUp.bundle.plan.externalActions.length > 0) {
423
- return {
424
- ok: false,
425
- completion: false,
426
- runtime: installed,
427
- diagnostics: [
428
- {
429
- code: diagnosticCodes.initRuntimeSourceRequired,
430
- severity: 'error',
431
- message: 'Runtime installation succeeded but init still has pending file or external actions.',
432
- source: 'init runtime',
433
- pointer: null,
434
- hint: 'Run pappra init again to review the remaining plan; initialization is not complete.',
435
- },
436
- ],
437
- };
438
- }
439
- return {
440
- ok: true,
441
- completion: true,
442
- runtime: installed,
443
- diagnostics: [],
444
- };
323
+ return bindAndRecordRuntime(root, installed);
445
324
  }
446
325
  function formatFailure(failure) {
447
326
  switch (failure.status) {
@@ -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,14 @@
1
+ import type { CliOutput } from '../output.js';
2
+ export declare const onboardUsage: string;
3
+ export interface OnboardCommandOptions {
4
+ readonly cwd: string;
5
+ readonly root: string | null;
6
+ readonly configPath: string | null;
7
+ readonly hosts: string | null;
8
+ readonly apply: boolean;
9
+ readonly applyHosts: boolean;
10
+ readonly dryRun: boolean;
11
+ readonly printHandoff: boolean;
12
+ readonly json: boolean;
13
+ }
14
+ export declare function runOnboardCommand(options: OnboardCommandOptions, output: CliOutput): Promise<number>;
@@ -0,0 +1,225 @@
1
+ import { readTextFileBounded } from '../../infra/fs.js';
2
+ import { parseInitChoices } from '../../schemas/init.js';
3
+ import { getPackageInfo } from '../../package-info.js';
4
+ import { applyOnboard, parseHostList, planOnboard, } from '../../app/onboard.js';
5
+ export const onboardUsage = 'pappra onboard [--root <dir>] [--config <file>] [--hosts claude,codex,pi]\n' +
6
+ ' [--yes] [--apply-host] [--dry-run] [--print-handoff] [--json]\n\n' +
7
+ 'Plans (default) or applies a workspace setup: init files, durable runtime, and with\n' +
8
+ '--apply-host the verified host wiring. Trust prompts are never approved automatically.\n' +
9
+ '--print-handoff lists the files to copy to another machine and the commands to run there.';
10
+ function printDiagnostics(output, diagnostics) {
11
+ for (const entry of diagnostics) {
12
+ output.stderr(` ${entry.severity} ${entry.code}: ${entry.message}\n`);
13
+ }
14
+ }
15
+ function failureExit(failure) {
16
+ return failure.reason === 'invalid'
17
+ ? 2
18
+ : failure.reason === 'unavailable'
19
+ ? 3
20
+ : 1;
21
+ }
22
+ function writeFailure(output, options, failure) {
23
+ if (options.json) {
24
+ output.stdout(`${JSON.stringify({
25
+ schema: 'pappra/onboard/1',
26
+ ok: false,
27
+ dryRun: !options.apply,
28
+ reason: failure.reason,
29
+ message: failure.message,
30
+ diagnostics: failure.diagnostics,
31
+ })}\n`);
32
+ }
33
+ else {
34
+ output.stderr(`pappra onboard: ${failure.message}\n`);
35
+ printDiagnostics(output, failure.diagnostics);
36
+ }
37
+ return failureExit(failure);
38
+ }
39
+ async function loadChoices(path) {
40
+ const read = await readTextFileBounded(path);
41
+ if (!read.ok) {
42
+ return {
43
+ ok: false,
44
+ failure: {
45
+ ok: false,
46
+ reason: 'unavailable',
47
+ message: read.message,
48
+ diagnostics: [],
49
+ },
50
+ };
51
+ }
52
+ let raw;
53
+ try {
54
+ raw = JSON.parse(read.value.text);
55
+ }
56
+ catch (error) {
57
+ return {
58
+ ok: false,
59
+ failure: {
60
+ ok: false,
61
+ reason: 'invalid',
62
+ message: `Invalid onboard config JSON: ${error instanceof Error ? error.message : String(error)}`,
63
+ diagnostics: [],
64
+ },
65
+ };
66
+ }
67
+ const parsed = parseInitChoices(raw, path);
68
+ if (parsed.value === null) {
69
+ return {
70
+ ok: false,
71
+ failure: {
72
+ ok: false,
73
+ reason: 'invalid',
74
+ message: parsed.diagnostics[0]?.message ??
75
+ 'Onboard config does not match pappra/init-choices/1.',
76
+ diagnostics: parsed.diagnostics,
77
+ },
78
+ };
79
+ }
80
+ return { ok: true, choices: parsed.value };
81
+ }
82
+ function plannedHosts(plan) {
83
+ return plan.hosts.map((host) => ({
84
+ ...host,
85
+ action: 'planned',
86
+ appliedFiles: [],
87
+ nextActions: ['Rerun with --apply-host to write this host integration.'],
88
+ diagnostics: [],
89
+ }));
90
+ }
91
+ function planReport(plan) {
92
+ const nextActions = ['Rerun with --yes to apply this plan.'];
93
+ if (plan.choices.knowledge.sources.length > 0) {
94
+ nextActions.push('pappra docs scan --apply');
95
+ }
96
+ if (plan.choices.code.graph !== 'none' &&
97
+ plan.choices.repositories.length > 0) {
98
+ nextActions.push('pappra code prepare --repo <repo> --apply');
99
+ }
100
+ return {
101
+ schema: 'pappra/onboard/1',
102
+ ok: true,
103
+ dryRun: true,
104
+ workspaceRoot: plan.bundle.scope.workspaceRoot,
105
+ files: plan.bundle.plan.files.map((file) => ({
106
+ path: file.path,
107
+ operation: file.operation,
108
+ })),
109
+ runtime: {
110
+ version: plan.bundle.scope.runtimeVersion,
111
+ status: 'pending',
112
+ },
113
+ initComplete: false,
114
+ hosts: plannedHosts(plan),
115
+ manualSteps: plan.hosts.map((host) => `${host.command}: detected ${host.detectedVersion ?? '(missing)'}, pinned ${host.pinnedVersion} (${host.state})`),
116
+ nextActions,
117
+ diagnostics: plan.bundle.diagnostics.filter((entry) => entry.severity !== 'info'),
118
+ };
119
+ }
120
+ function printResult(output, result) {
121
+ output.stdout(`Workspace: ${result.workspaceRoot}\n`);
122
+ output.stdout(`Status: ${result.initComplete ? 'complete' : 'planned'}\n`);
123
+ for (const file of result.files) {
124
+ output.stdout(` ${file.operation} ${file.path}\n`);
125
+ }
126
+ if (result.runtime !== null) {
127
+ output.stdout(`Runtime: ${result.runtime.status} ${result.runtime.version}\n`);
128
+ }
129
+ for (const host of result.hosts) {
130
+ output.stdout(`Host ${host.host}: detected ${host.detectedVersion ?? '(missing)'} / pinned ${host.pinnedVersion} (${host.state}) -> ${host.action}\n`);
131
+ }
132
+ for (const step of result.manualSteps) {
133
+ output.stdout(` manual: ${step}\n`);
134
+ }
135
+ for (const action of result.nextActions) {
136
+ output.stdout(` next: ${action}\n`);
137
+ }
138
+ printDiagnostics(output, result.diagnostics);
139
+ }
140
+ function printHandoff(output, options, root) {
141
+ const report = {
142
+ schema: 'pappra/handoff/1',
143
+ workspaceRoot: root,
144
+ share: [
145
+ '.pappra/workspace.json',
146
+ '.pappra-choices.json (or the choices file used for onboard)',
147
+ 'repository checkouts at the same relative paths',
148
+ ],
149
+ doNotCopy: [
150
+ '.pappra/local/machine.json',
151
+ '.pappra/local/code/*.json',
152
+ '.pappra/local/knowledge/catalog.json',
153
+ '.pappra/local/host-events/**',
154
+ ],
155
+ commands: [
156
+ `npm install -g @phyzess/pappra@${getPackageInfo().version}`,
157
+ `cd ${root}`,
158
+ 'pappra onboard --yes --apply-host',
159
+ 'pappra doctor --json',
160
+ ],
161
+ };
162
+ if (options.json) {
163
+ output.stdout(`${JSON.stringify(report)}\n`);
164
+ }
165
+ else {
166
+ output.stdout('Share:\n');
167
+ for (const entry of report.share)
168
+ output.stdout(` ${entry}\n`);
169
+ output.stdout('Do not copy:\n');
170
+ for (const entry of report.doNotCopy)
171
+ output.stdout(` ${entry}\n`);
172
+ output.stdout('On the new machine:\n');
173
+ for (const entry of report.commands)
174
+ output.stdout(` ${entry}\n`);
175
+ }
176
+ return 0;
177
+ }
178
+ export async function runOnboardCommand(options, output) {
179
+ const root = options.root ?? options.cwd;
180
+ if (options.printHandoff) {
181
+ return printHandoff(output, options, root);
182
+ }
183
+ let hosts;
184
+ try {
185
+ hosts = parseHostList(options.hosts);
186
+ }
187
+ catch (error) {
188
+ return writeFailure(output, options, {
189
+ ok: false,
190
+ reason: 'invalid',
191
+ message: error instanceof Error ? error.message : String(error),
192
+ diagnostics: [],
193
+ });
194
+ }
195
+ let choices = null;
196
+ if (options.configPath !== null) {
197
+ const loaded = await loadChoices(options.configPath);
198
+ if (!loaded.ok)
199
+ return writeFailure(output, options, loaded.failure);
200
+ choices = loaded.choices;
201
+ }
202
+ const planned = await planOnboard({ root, choices, hosts });
203
+ if ('reason' in planned)
204
+ return writeFailure(output, options, planned);
205
+ if (!options.apply || options.dryRun) {
206
+ const report = planReport(planned);
207
+ if (options.json) {
208
+ output.stdout(`${JSON.stringify(report)}\n`);
209
+ }
210
+ else {
211
+ printResult(output, report);
212
+ }
213
+ return 0;
214
+ }
215
+ const result = await applyOnboard(planned, {
216
+ applyHosts: options.applyHosts,
217
+ });
218
+ if (options.json) {
219
+ output.stdout(`${JSON.stringify(result)}\n`);
220
+ }
221
+ else {
222
+ printResult(output, result);
223
+ }
224
+ return result.ok ? 0 : 1;
225
+ }
@@ -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
+ }