@proletariat/cli 0.3.18 → 0.3.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/LICENSE +21 -0
- package/bin/dev.js +0 -0
- package/dist/commands/agent/staff/remove.d.ts +1 -0
- package/dist/commands/agent/staff/remove.js +34 -26
- package/dist/commands/agent/temp/cleanup.js +10 -17
- package/dist/commands/board/view.d.ts +15 -0
- package/dist/commands/board/view.js +136 -0
- package/dist/commands/config/index.js +6 -3
- package/dist/commands/execution/config.d.ts +34 -0
- package/dist/commands/execution/config.js +411 -0
- package/dist/commands/execution/index.js +6 -1
- package/dist/commands/execution/kill.d.ts +9 -0
- package/dist/commands/execution/kill.js +16 -0
- package/dist/commands/execution/view.d.ts +17 -0
- package/dist/commands/execution/view.js +288 -0
- package/dist/commands/phase/template/create.js +67 -20
- package/dist/commands/pr/index.js +6 -2
- package/dist/commands/pr/list.d.ts +17 -0
- package/dist/commands/pr/list.js +163 -0
- package/dist/commands/project/update.d.ts +19 -0
- package/dist/commands/project/update.js +163 -0
- package/dist/commands/roadmap/create.js +5 -0
- package/dist/commands/spec/delete.d.ts +18 -0
- package/dist/commands/spec/delete.js +111 -0
- package/dist/commands/spec/edit.d.ts +23 -0
- package/dist/commands/spec/edit.js +232 -0
- package/dist/commands/spec/index.js +5 -0
- package/dist/commands/status/create.js +38 -34
- package/dist/commands/template/phase/create.d.ts +1 -0
- package/dist/commands/template/phase/create.js +10 -1
- package/dist/commands/template/ticket/create.d.ts +20 -0
- package/dist/commands/template/ticket/create.js +87 -0
- package/dist/commands/template/ticket/save.d.ts +2 -0
- package/dist/commands/template/ticket/save.js +11 -0
- package/dist/commands/ticket/create.js +7 -0
- package/dist/commands/ticket/template/create.d.ts +9 -1
- package/dist/commands/ticket/template/create.js +224 -52
- package/dist/commands/ticket/template/save.d.ts +2 -1
- package/dist/commands/ticket/template/save.js +58 -7
- package/dist/commands/work/ready.js +8 -8
- package/dist/lib/agents/index.js +14 -4
- package/dist/lib/branch/index.js +24 -0
- package/dist/lib/execution/config.d.ts +2 -0
- package/dist/lib/execution/config.js +12 -0
- package/dist/lib/pmo/utils.d.ts +4 -2
- package/dist/lib/pmo/utils.js +4 -2
- package/oclif.manifest.json +2555 -1781
- package/package.json +5 -6
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
import { Args, Flags } from '@oclif/core';
|
|
2
|
+
import inquirer from 'inquirer';
|
|
3
|
+
import { PMOCommand, pmoBaseFlags } from '../../lib/pmo/index.js';
|
|
4
|
+
import { styles } from '../../lib/styles.js';
|
|
5
|
+
import { shouldOutputJson, outputPromptAsJson, outputErrorAsJson, outputSuccessAsJson, createMetadata, buildFormPromptConfig, } from '../../lib/prompt-json.js';
|
|
6
|
+
export default class ProjectUpdate extends PMOCommand {
|
|
7
|
+
static description = 'Update project metadata (name, description)';
|
|
8
|
+
static examples = [
|
|
9
|
+
'<%= config.bin %> <%= command.id %> my-project --name "New Project Name"',
|
|
10
|
+
'<%= config.bin %> <%= command.id %> my-project --description "Updated description"',
|
|
11
|
+
'<%= config.bin %> <%= command.id %> my-project --name "New Name" --description "New description"',
|
|
12
|
+
'<%= config.bin %> <%= command.id %> my-project # Interactive mode',
|
|
13
|
+
];
|
|
14
|
+
static args = {
|
|
15
|
+
id: Args.string({
|
|
16
|
+
description: 'Project ID or name',
|
|
17
|
+
required: false,
|
|
18
|
+
}),
|
|
19
|
+
};
|
|
20
|
+
static flags = {
|
|
21
|
+
...pmoBaseFlags,
|
|
22
|
+
name: Flags.string({
|
|
23
|
+
char: 'n',
|
|
24
|
+
description: 'New project name',
|
|
25
|
+
}),
|
|
26
|
+
description: Flags.string({
|
|
27
|
+
char: 'd',
|
|
28
|
+
description: 'New project description',
|
|
29
|
+
}),
|
|
30
|
+
};
|
|
31
|
+
getPMOOptions() {
|
|
32
|
+
return { promptIfMultiple: false };
|
|
33
|
+
}
|
|
34
|
+
async execute() {
|
|
35
|
+
const { args, flags } = await this.parse(ProjectUpdate);
|
|
36
|
+
// Check if JSON output mode is active
|
|
37
|
+
const jsonMode = shouldOutputJson(flags);
|
|
38
|
+
// Helper to handle errors in JSON mode
|
|
39
|
+
const handleError = (code, message) => {
|
|
40
|
+
if (jsonMode) {
|
|
41
|
+
outputErrorAsJson(code, message, createMetadata('project update', flags));
|
|
42
|
+
this.exit(1);
|
|
43
|
+
}
|
|
44
|
+
this.error(message);
|
|
45
|
+
};
|
|
46
|
+
// Agent mode config for prompts
|
|
47
|
+
const agentConfig = jsonMode ? { flags, commandName: 'project update' } : null;
|
|
48
|
+
// Get project ID - from args or prompt
|
|
49
|
+
let projectId = args.id;
|
|
50
|
+
if (!projectId) {
|
|
51
|
+
// List available projects for selection
|
|
52
|
+
const projects = await this.storage.listProjects({ isArchived: false });
|
|
53
|
+
if (projects.length === 0) {
|
|
54
|
+
return handleError('NO_PROJECTS', 'No projects found. Create one with: prlt project create');
|
|
55
|
+
}
|
|
56
|
+
const projectChoices = projects.map(p => ({
|
|
57
|
+
name: `${p.id} - ${p.name}`,
|
|
58
|
+
value: p.id,
|
|
59
|
+
command: `prlt project update ${p.id} --json`,
|
|
60
|
+
}));
|
|
61
|
+
// Handle JSON mode for project selection
|
|
62
|
+
if (jsonMode) {
|
|
63
|
+
outputPromptAsJson({
|
|
64
|
+
type: 'list',
|
|
65
|
+
name: 'projectId',
|
|
66
|
+
message: 'Select project to update:',
|
|
67
|
+
choices: projectChoices,
|
|
68
|
+
}, createMetadata('project update', flags));
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
const { selectedProjectId } = await inquirer.prompt([{
|
|
72
|
+
type: 'list',
|
|
73
|
+
name: 'selectedProjectId',
|
|
74
|
+
message: 'Select project to update:',
|
|
75
|
+
choices: projectChoices,
|
|
76
|
+
}]);
|
|
77
|
+
projectId = selectedProjectId;
|
|
78
|
+
}
|
|
79
|
+
// Get the project
|
|
80
|
+
const project = await this.storage.getProject(projectId);
|
|
81
|
+
if (!project) {
|
|
82
|
+
return handleError('PROJECT_NOT_FOUND', `Project "${projectId}" not found.`);
|
|
83
|
+
}
|
|
84
|
+
// Determine what to update
|
|
85
|
+
let newName = flags.name;
|
|
86
|
+
let newDescription = flags.description;
|
|
87
|
+
// If no flags provided, enter interactive mode
|
|
88
|
+
if (newName === undefined && newDescription === undefined) {
|
|
89
|
+
const fields = [
|
|
90
|
+
{
|
|
91
|
+
type: 'input',
|
|
92
|
+
name: 'name',
|
|
93
|
+
message: 'Project name:',
|
|
94
|
+
default: project.name,
|
|
95
|
+
},
|
|
96
|
+
{
|
|
97
|
+
type: 'input',
|
|
98
|
+
name: 'description',
|
|
99
|
+
message: 'Project description:',
|
|
100
|
+
default: project.description || '',
|
|
101
|
+
},
|
|
102
|
+
];
|
|
103
|
+
// Handle JSON mode for field input
|
|
104
|
+
if (jsonMode) {
|
|
105
|
+
outputPromptAsJson(buildFormPromptConfig(fields), createMetadata('project update', flags));
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
const answers = await inquirer.prompt(fields.map(field => ({
|
|
109
|
+
...field,
|
|
110
|
+
validate: field.name === 'name'
|
|
111
|
+
? ((input) => input.length > 0 || 'Name is required')
|
|
112
|
+
: undefined,
|
|
113
|
+
})));
|
|
114
|
+
newName = answers.name;
|
|
115
|
+
newDescription = answers.description;
|
|
116
|
+
}
|
|
117
|
+
// Build changes object - only include fields that are different
|
|
118
|
+
const changes = {};
|
|
119
|
+
if (newName !== undefined && newName !== project.name) {
|
|
120
|
+
changes.name = newName;
|
|
121
|
+
}
|
|
122
|
+
if (newDescription !== undefined && newDescription !== (project.description || '')) {
|
|
123
|
+
// Allow clearing description with empty string (storage layer converts '' to null)
|
|
124
|
+
changes.description = newDescription;
|
|
125
|
+
}
|
|
126
|
+
// Check if anything changed
|
|
127
|
+
if (Object.keys(changes).length === 0) {
|
|
128
|
+
if (jsonMode) {
|
|
129
|
+
outputSuccessAsJson({
|
|
130
|
+
projectId: project.id,
|
|
131
|
+
projectName: project.name,
|
|
132
|
+
description: project.description,
|
|
133
|
+
noChanges: true,
|
|
134
|
+
}, createMetadata('project update', flags));
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
this.log(styles.muted('No changes made.'));
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
140
|
+
// Update the project
|
|
141
|
+
const updated = await this.storage.updateProject(project.id, changes);
|
|
142
|
+
// Output success
|
|
143
|
+
if (jsonMode) {
|
|
144
|
+
outputSuccessAsJson({
|
|
145
|
+
projectId: updated.id,
|
|
146
|
+
projectName: updated.name,
|
|
147
|
+
description: updated.description,
|
|
148
|
+
changes: Object.keys(changes),
|
|
149
|
+
}, createMetadata('project update', flags));
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
152
|
+
this.log(styles.success(`\nUpdated project "${styles.emphasis(updated.name)}"`));
|
|
153
|
+
this.log(styles.muted(` ID: ${updated.id}`));
|
|
154
|
+
if (changes.name) {
|
|
155
|
+
this.log(styles.muted(` Name: ${project.name} → ${updated.name}`));
|
|
156
|
+
}
|
|
157
|
+
if ('description' in changes) {
|
|
158
|
+
const oldDesc = project.description || '(none)';
|
|
159
|
+
const newDesc = updated.description || '(none)';
|
|
160
|
+
this.log(styles.muted(` Description: ${oldDesc} → ${newDesc}`));
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
}
|
|
@@ -94,6 +94,11 @@ export default class RoadmapCreate extends PMOCommand {
|
|
|
94
94
|
description: roadmapData.description,
|
|
95
95
|
isDefault: roadmapData.isDefault,
|
|
96
96
|
});
|
|
97
|
+
// In JSON mode, output roadmap data and return (skip interactive prompts)
|
|
98
|
+
if (jsonMode) {
|
|
99
|
+
this.log(JSON.stringify(roadmap, null, 2));
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
97
102
|
this.log(styles.success(`\nCreated roadmap "${styles.emphasis(roadmap.name)}"`));
|
|
98
103
|
this.log(styles.muted(` ID: ${roadmap.id}`));
|
|
99
104
|
if (roadmap.description) {
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { PMOCommand } from '../../lib/pmo/index.js';
|
|
2
|
+
export default class SpecDelete extends PMOCommand {
|
|
3
|
+
static description: string;
|
|
4
|
+
static examples: string[];
|
|
5
|
+
static args: {
|
|
6
|
+
id: import("@oclif/core/interfaces").Arg<string | undefined, Record<string, unknown>>;
|
|
7
|
+
};
|
|
8
|
+
static flags: {
|
|
9
|
+
force: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
10
|
+
machine: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
11
|
+
json: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
12
|
+
project: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
13
|
+
};
|
|
14
|
+
protected getPMOOptions(): {
|
|
15
|
+
promptIfMultiple: boolean;
|
|
16
|
+
};
|
|
17
|
+
execute(): Promise<void>;
|
|
18
|
+
}
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import { Args, Flags } from '@oclif/core';
|
|
2
|
+
import { PMOCommand, pmoBaseFlags } from '../../lib/pmo/index.js';
|
|
3
|
+
import { styles } from '../../lib/styles.js';
|
|
4
|
+
import { shouldOutputJson, outputErrorAsJson, outputSuccessAsJson, createMetadata, } from '../../lib/prompt-json.js';
|
|
5
|
+
import { FlagResolver } from '../../lib/flags/index.js';
|
|
6
|
+
export default class SpecDelete extends PMOCommand {
|
|
7
|
+
static description = 'Delete a spec';
|
|
8
|
+
static examples = [
|
|
9
|
+
'<%= config.bin %> <%= command.id %> my-spec',
|
|
10
|
+
'<%= config.bin %> <%= command.id %> my-spec --force',
|
|
11
|
+
'<%= config.bin %> <%= command.id %> # Interactive selection',
|
|
12
|
+
];
|
|
13
|
+
static args = {
|
|
14
|
+
id: Args.string({
|
|
15
|
+
description: 'Spec ID to delete',
|
|
16
|
+
required: false,
|
|
17
|
+
}),
|
|
18
|
+
};
|
|
19
|
+
static flags = {
|
|
20
|
+
...pmoBaseFlags,
|
|
21
|
+
force: Flags.boolean({
|
|
22
|
+
char: 'f',
|
|
23
|
+
description: 'Skip confirmation prompt',
|
|
24
|
+
default: false,
|
|
25
|
+
}),
|
|
26
|
+
};
|
|
27
|
+
getPMOOptions() {
|
|
28
|
+
return { promptIfMultiple: false };
|
|
29
|
+
}
|
|
30
|
+
async execute() {
|
|
31
|
+
const { args, flags } = await this.parse(SpecDelete);
|
|
32
|
+
const jsonMode = shouldOutputJson(flags);
|
|
33
|
+
const handleError = (code, message) => {
|
|
34
|
+
if (jsonMode) {
|
|
35
|
+
outputErrorAsJson(code, message, createMetadata('spec delete', flags));
|
|
36
|
+
this.exit(1);
|
|
37
|
+
}
|
|
38
|
+
this.error(message);
|
|
39
|
+
};
|
|
40
|
+
let specId = args.id;
|
|
41
|
+
// Interactive selection if no ID provided
|
|
42
|
+
if (!specId) {
|
|
43
|
+
const specs = await this.storage.listSpecs();
|
|
44
|
+
if (specs.length === 0) {
|
|
45
|
+
return handleError('NO_SPECS', 'No specs found');
|
|
46
|
+
}
|
|
47
|
+
const resolver = new FlagResolver({
|
|
48
|
+
commandName: 'spec delete',
|
|
49
|
+
baseCommand: 'prlt spec delete',
|
|
50
|
+
jsonMode,
|
|
51
|
+
flags: {},
|
|
52
|
+
});
|
|
53
|
+
resolver.addPrompt({
|
|
54
|
+
flagName: 'spec',
|
|
55
|
+
type: 'list',
|
|
56
|
+
message: 'Select spec to delete:',
|
|
57
|
+
choices: () => specs.map(s => ({
|
|
58
|
+
name: `${s.title} [${s.status}]${s.type ? ` (${s.type})` : ''}`,
|
|
59
|
+
value: s.id,
|
|
60
|
+
})),
|
|
61
|
+
getCommand: (value) => `prlt spec delete ${value} --json`,
|
|
62
|
+
});
|
|
63
|
+
const resolved = await resolver.resolve();
|
|
64
|
+
specId = resolved.spec;
|
|
65
|
+
}
|
|
66
|
+
// Get spec details
|
|
67
|
+
const spec = await this.storage.getSpec(specId);
|
|
68
|
+
if (!spec) {
|
|
69
|
+
return handleError('SPEC_NOT_FOUND', `Spec not found: ${specId}`);
|
|
70
|
+
}
|
|
71
|
+
// Confirmation prompt
|
|
72
|
+
if (!flags.force) {
|
|
73
|
+
const resolver = new FlagResolver({
|
|
74
|
+
commandName: 'spec delete',
|
|
75
|
+
baseCommand: `prlt spec delete ${specId}`,
|
|
76
|
+
jsonMode,
|
|
77
|
+
flags: {},
|
|
78
|
+
});
|
|
79
|
+
resolver.addPrompt({
|
|
80
|
+
flagName: 'confirmed',
|
|
81
|
+
type: 'list',
|
|
82
|
+
message: `Delete spec "${spec.title}"?`,
|
|
83
|
+
choices: () => [
|
|
84
|
+
{ name: 'No', value: false },
|
|
85
|
+
{ name: 'Yes', value: true },
|
|
86
|
+
],
|
|
87
|
+
getCommand: (value) => value
|
|
88
|
+
? `prlt spec delete ${specId} --force --json`
|
|
89
|
+
: '',
|
|
90
|
+
});
|
|
91
|
+
const resolved = await resolver.resolve();
|
|
92
|
+
if (!resolved.confirmed) {
|
|
93
|
+
this.log(styles.muted('Cancelled'));
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
// Delete the spec
|
|
98
|
+
await this.storage.deleteSpec(specId);
|
|
99
|
+
// JSON response for agents
|
|
100
|
+
if (jsonMode) {
|
|
101
|
+
outputSuccessAsJson({
|
|
102
|
+
specId: spec.id,
|
|
103
|
+
title: spec.title,
|
|
104
|
+
deleted: true,
|
|
105
|
+
}, createMetadata('spec delete', flags));
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
// Human-readable response
|
|
109
|
+
this.log(styles.success(`\nDeleted spec "${styles.emphasis(spec.title)}"`));
|
|
110
|
+
}
|
|
111
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { PMOCommand } from '../../lib/pmo/index.js';
|
|
2
|
+
export default class SpecEdit extends PMOCommand {
|
|
3
|
+
static description: string;
|
|
4
|
+
static examples: string[];
|
|
5
|
+
static args: {
|
|
6
|
+
spec: import("@oclif/core/interfaces").Arg<string | undefined, Record<string, unknown>>;
|
|
7
|
+
};
|
|
8
|
+
static flags: {
|
|
9
|
+
spec: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
10
|
+
title: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
11
|
+
status: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
12
|
+
type: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
13
|
+
problem: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
14
|
+
solution: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
15
|
+
decisions: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
16
|
+
interactive: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
17
|
+
machine: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
18
|
+
json: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
19
|
+
project: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
20
|
+
};
|
|
21
|
+
execute(): Promise<void>;
|
|
22
|
+
private promptForEdits;
|
|
23
|
+
}
|
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
import { Flags, Args } from '@oclif/core';
|
|
2
|
+
import inquirer from 'inquirer';
|
|
3
|
+
import { PMOCommand, pmoBaseFlags } from '../../lib/pmo/index.js';
|
|
4
|
+
import { styles } from '../../lib/styles.js';
|
|
5
|
+
import { shouldOutputJson, outputErrorAsJson, createMetadata, } from '../../lib/prompt-json.js';
|
|
6
|
+
import { FlagResolver } from '../../lib/flags/index.js';
|
|
7
|
+
export default class SpecEdit extends PMOCommand {
|
|
8
|
+
static description = 'Edit an existing spec';
|
|
9
|
+
static examples = [
|
|
10
|
+
'<%= config.bin %> <%= command.id %> user-authentication',
|
|
11
|
+
'<%= config.bin %> <%= command.id %> --spec api-design --title "New API Design"',
|
|
12
|
+
'<%= config.bin %> <%= command.id %> user-auth --status active',
|
|
13
|
+
'<%= config.bin %> <%= command.id %> user-auth --type product --problem "Need better auth"',
|
|
14
|
+
'<%= config.bin %> <%= command.id %> -i # Interactive mode',
|
|
15
|
+
];
|
|
16
|
+
static args = {
|
|
17
|
+
spec: Args.string({
|
|
18
|
+
description: 'Spec ID to edit',
|
|
19
|
+
required: false,
|
|
20
|
+
}),
|
|
21
|
+
};
|
|
22
|
+
static flags = {
|
|
23
|
+
...pmoBaseFlags,
|
|
24
|
+
spec: Flags.string({
|
|
25
|
+
char: 's',
|
|
26
|
+
description: 'Spec ID to edit',
|
|
27
|
+
}),
|
|
28
|
+
title: Flags.string({
|
|
29
|
+
char: 't',
|
|
30
|
+
description: 'New spec title',
|
|
31
|
+
}),
|
|
32
|
+
status: Flags.string({
|
|
33
|
+
description: 'Spec status',
|
|
34
|
+
options: ['draft', 'active', 'implemented'],
|
|
35
|
+
}),
|
|
36
|
+
type: Flags.string({
|
|
37
|
+
description: 'Spec type',
|
|
38
|
+
options: ['product', 'platform', 'infra', 'integration', 'none'],
|
|
39
|
+
}),
|
|
40
|
+
problem: Flags.string({
|
|
41
|
+
description: 'Problem statement',
|
|
42
|
+
}),
|
|
43
|
+
solution: Flags.string({
|
|
44
|
+
description: 'Solution description',
|
|
45
|
+
}),
|
|
46
|
+
decisions: Flags.string({
|
|
47
|
+
description: 'Design decisions',
|
|
48
|
+
}),
|
|
49
|
+
interactive: Flags.boolean({
|
|
50
|
+
char: 'i',
|
|
51
|
+
description: 'Interactive mode - prompts for all fields',
|
|
52
|
+
default: false,
|
|
53
|
+
}),
|
|
54
|
+
};
|
|
55
|
+
async execute() {
|
|
56
|
+
const { args, flags } = await this.parse(SpecEdit);
|
|
57
|
+
// Check if JSON output mode is active
|
|
58
|
+
const jsonMode = shouldOutputJson(flags);
|
|
59
|
+
// Helper to handle errors in JSON mode
|
|
60
|
+
const handleError = (code, message) => {
|
|
61
|
+
if (jsonMode) {
|
|
62
|
+
outputErrorAsJson(code, message, createMetadata('spec edit', flags));
|
|
63
|
+
this.exit(1);
|
|
64
|
+
}
|
|
65
|
+
this.error(message);
|
|
66
|
+
};
|
|
67
|
+
// Get spec ID from args or flags
|
|
68
|
+
let specId = args.spec || flags.spec;
|
|
69
|
+
if (!specId) {
|
|
70
|
+
// List specs for selection
|
|
71
|
+
const specs = await this.storage.listSpecs();
|
|
72
|
+
if (specs.length === 0) {
|
|
73
|
+
return handleError('NO_SPECS', 'No specs found. Create one first with: prlt spec create');
|
|
74
|
+
}
|
|
75
|
+
// Use FlagResolver for spec selection
|
|
76
|
+
const resolver = new FlagResolver({
|
|
77
|
+
commandName: 'spec edit',
|
|
78
|
+
baseCommand: 'prlt spec edit',
|
|
79
|
+
jsonMode,
|
|
80
|
+
flags: { spec: flags.spec },
|
|
81
|
+
});
|
|
82
|
+
resolver.addPrompt({
|
|
83
|
+
flagName: 'spec',
|
|
84
|
+
type: 'list',
|
|
85
|
+
message: 'Select spec to edit:',
|
|
86
|
+
choices: () => specs.map(s => ({
|
|
87
|
+
name: `${s.title} [${s.status}]${s.type ? ` (${s.type})` : ''}`,
|
|
88
|
+
value: s.id,
|
|
89
|
+
})),
|
|
90
|
+
});
|
|
91
|
+
const resolved = await resolver.resolve();
|
|
92
|
+
specId = resolved.spec;
|
|
93
|
+
}
|
|
94
|
+
// Get current spec
|
|
95
|
+
const spec = await this.storage.getSpec(specId);
|
|
96
|
+
if (!spec) {
|
|
97
|
+
return handleError('NOT_FOUND', `Spec "${specId}" not found.`);
|
|
98
|
+
}
|
|
99
|
+
// Build choices for prompts
|
|
100
|
+
const typeChoices = [
|
|
101
|
+
{ name: 'Product (user-facing feature)', value: 'product' },
|
|
102
|
+
{ name: 'Platform (internal tooling)', value: 'platform' },
|
|
103
|
+
{ name: 'Infra (technical infrastructure)', value: 'infra' },
|
|
104
|
+
{ name: 'Integration (external service)', value: 'integration' },
|
|
105
|
+
{ name: 'None', value: '' },
|
|
106
|
+
];
|
|
107
|
+
const statusChoices = [
|
|
108
|
+
{ name: 'Draft (planning)', value: 'draft' },
|
|
109
|
+
{ name: 'Active (in progress)', value: 'active' },
|
|
110
|
+
{ name: 'Implemented (complete)', value: 'implemented' },
|
|
111
|
+
];
|
|
112
|
+
// Determine what to update
|
|
113
|
+
let updates = {};
|
|
114
|
+
const hasFlags = flags.title || flags.status || flags.type || flags.problem ||
|
|
115
|
+
flags.solution || flags.decisions;
|
|
116
|
+
if (flags.interactive || !hasFlags) {
|
|
117
|
+
// Interactive mode - prompt for editable fields
|
|
118
|
+
updates = await this.promptForEdits(spec, typeChoices, statusChoices);
|
|
119
|
+
}
|
|
120
|
+
else {
|
|
121
|
+
// Use flag values
|
|
122
|
+
if (flags.title)
|
|
123
|
+
updates.title = flags.title;
|
|
124
|
+
if (flags.status)
|
|
125
|
+
updates.status = flags.status;
|
|
126
|
+
if (flags.type) {
|
|
127
|
+
updates.type = flags.type === 'none' ? undefined : flags.type;
|
|
128
|
+
}
|
|
129
|
+
if (flags.problem)
|
|
130
|
+
updates.problem = flags.problem;
|
|
131
|
+
if (flags.solution)
|
|
132
|
+
updates.solution = flags.solution;
|
|
133
|
+
if (flags.decisions)
|
|
134
|
+
updates.decisions = flags.decisions;
|
|
135
|
+
}
|
|
136
|
+
// Check if anything changed
|
|
137
|
+
if (Object.keys(updates).length === 0) {
|
|
138
|
+
this.log(styles.muted('\nNo changes made.'));
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
141
|
+
// Update the spec
|
|
142
|
+
const updatedSpec = await this.storage.updateSpec(specId, updates);
|
|
143
|
+
// Display updated spec
|
|
144
|
+
this.log(styles.success(`\n✅ Updated spec "${styles.emphasis(updatedSpec.title)}"`));
|
|
145
|
+
const changedFields = [];
|
|
146
|
+
if (updates.title)
|
|
147
|
+
changedFields.push(`Title: ${updatedSpec.title}`);
|
|
148
|
+
if (updates.status)
|
|
149
|
+
changedFields.push(`Status: ${updatedSpec.status}`);
|
|
150
|
+
if (updates.type !== undefined)
|
|
151
|
+
changedFields.push(`Type: ${updatedSpec.type || 'none'}`);
|
|
152
|
+
if (updates.problem !== undefined)
|
|
153
|
+
changedFields.push(`Problem: ${updates.problem ? 'updated' : '(cleared)'}`);
|
|
154
|
+
if (updates.solution !== undefined)
|
|
155
|
+
changedFields.push(`Solution: ${updates.solution ? 'updated' : '(cleared)'}`);
|
|
156
|
+
if (updates.decisions !== undefined)
|
|
157
|
+
changedFields.push(`Decisions: ${updates.decisions ? 'updated' : '(cleared)'}`);
|
|
158
|
+
for (const field of changedFields) {
|
|
159
|
+
this.log(styles.muted(` ${field}`));
|
|
160
|
+
}
|
|
161
|
+
this.log('');
|
|
162
|
+
this.log(styles.muted(`View spec: prlt spec view ${updatedSpec.id}`));
|
|
163
|
+
}
|
|
164
|
+
async promptForEdits(spec, typeChoices, statusChoices) {
|
|
165
|
+
const answers = await inquirer.prompt([
|
|
166
|
+
{
|
|
167
|
+
type: 'input',
|
|
168
|
+
name: 'title',
|
|
169
|
+
message: 'Title:',
|
|
170
|
+
default: spec.title,
|
|
171
|
+
validate: (input) => input.length > 0 || 'Title is required',
|
|
172
|
+
},
|
|
173
|
+
{
|
|
174
|
+
type: 'list',
|
|
175
|
+
name: 'status',
|
|
176
|
+
message: 'Status:',
|
|
177
|
+
choices: statusChoices,
|
|
178
|
+
default: spec.status,
|
|
179
|
+
},
|
|
180
|
+
{
|
|
181
|
+
type: 'list',
|
|
182
|
+
name: 'type',
|
|
183
|
+
message: 'Type:',
|
|
184
|
+
choices: typeChoices,
|
|
185
|
+
default: spec.type || '',
|
|
186
|
+
},
|
|
187
|
+
{
|
|
188
|
+
type: 'editor',
|
|
189
|
+
name: 'problem',
|
|
190
|
+
message: 'Problem statement (opens $EDITOR):',
|
|
191
|
+
default: spec.problem || '',
|
|
192
|
+
waitForUseInput: false,
|
|
193
|
+
},
|
|
194
|
+
{
|
|
195
|
+
type: 'editor',
|
|
196
|
+
name: 'solution',
|
|
197
|
+
message: 'Solution (opens $EDITOR):',
|
|
198
|
+
default: spec.solution || '',
|
|
199
|
+
waitForUseInput: false,
|
|
200
|
+
},
|
|
201
|
+
{
|
|
202
|
+
type: 'editor',
|
|
203
|
+
name: 'decisions',
|
|
204
|
+
message: 'Design decisions (opens $EDITOR):',
|
|
205
|
+
default: spec.decisions || '',
|
|
206
|
+
waitForUseInput: false,
|
|
207
|
+
},
|
|
208
|
+
]);
|
|
209
|
+
// Build updates object with only changed fields
|
|
210
|
+
const updates = {};
|
|
211
|
+
if (answers.title !== spec.title) {
|
|
212
|
+
updates.title = answers.title;
|
|
213
|
+
}
|
|
214
|
+
if (answers.status !== spec.status) {
|
|
215
|
+
updates.status = answers.status;
|
|
216
|
+
}
|
|
217
|
+
const newType = answers.type === '' ? undefined : answers.type;
|
|
218
|
+
if (newType !== spec.type) {
|
|
219
|
+
updates.type = newType;
|
|
220
|
+
}
|
|
221
|
+
if (answers.problem !== (spec.problem || '')) {
|
|
222
|
+
updates.problem = answers.problem || undefined;
|
|
223
|
+
}
|
|
224
|
+
if (answers.solution !== (spec.solution || '')) {
|
|
225
|
+
updates.solution = answers.solution || undefined;
|
|
226
|
+
}
|
|
227
|
+
if (answers.decisions !== (spec.decisions || '')) {
|
|
228
|
+
updates.decisions = answers.decisions || undefined;
|
|
229
|
+
}
|
|
230
|
+
return updates;
|
|
231
|
+
}
|
|
232
|
+
}
|
|
@@ -21,6 +21,7 @@ export default class Spec extends PMOCommand {
|
|
|
21
21
|
{ name: 'Create new spec', value: 'create' },
|
|
22
22
|
{ name: 'List all specs', value: 'list' },
|
|
23
23
|
{ name: 'View spec', value: 'view' },
|
|
24
|
+
{ name: 'Delete spec', value: 'delete' },
|
|
24
25
|
{ name: 'Generate tickets from spec', value: 'generate' },
|
|
25
26
|
{ name: 'Assign ticket to spec', value: 'ticket' },
|
|
26
27
|
{ name: 'Manage dependencies', value: 'link' },
|
|
@@ -44,6 +45,7 @@ export default class Spec extends PMOCommand {
|
|
|
44
45
|
create: 'prlt spec create --json',
|
|
45
46
|
list: 'prlt spec list --json',
|
|
46
47
|
view: 'prlt spec view --json',
|
|
48
|
+
delete: 'prlt spec delete --json',
|
|
47
49
|
generate: 'prlt spec plan --json',
|
|
48
50
|
ticket: 'prlt spec ticket --json',
|
|
49
51
|
link: 'prlt spec link --json',
|
|
@@ -68,6 +70,9 @@ export default class Spec extends PMOCommand {
|
|
|
68
70
|
case 'view':
|
|
69
71
|
await this.config.runCommand('spec:view', []);
|
|
70
72
|
break;
|
|
73
|
+
case 'delete':
|
|
74
|
+
await this.config.runCommand('spec:delete', []);
|
|
75
|
+
break;
|
|
71
76
|
case 'generate':
|
|
72
77
|
await this.config.runCommand('spec:generate-tickets', []);
|
|
73
78
|
break;
|
|
@@ -88,40 +88,44 @@ export default class StatusCreate extends PMOCommand {
|
|
|
88
88
|
default: flags.category || 'backlog',
|
|
89
89
|
when: (ctx) => !ctx.flags.category || flags.interactive,
|
|
90
90
|
});
|
|
91
|
-
//
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
91
|
+
// Optional prompts - only shown in interactive mode
|
|
92
|
+
// In JSON/machine mode, optional fields should be passed as flags or omitted
|
|
93
|
+
if (flags.interactive) {
|
|
94
|
+
// Add color prompt
|
|
95
|
+
resolver.addPrompt({
|
|
96
|
+
flagName: 'color',
|
|
97
|
+
type: 'input',
|
|
98
|
+
message: 'Color (hex, optional):',
|
|
99
|
+
default: flags.color,
|
|
100
|
+
validate: (value) => {
|
|
101
|
+
const v = value;
|
|
102
|
+
if (!v)
|
|
103
|
+
return true;
|
|
104
|
+
return /^#[0-9A-Fa-f]{6}$/.test(v) || 'Invalid hex color (e.g., #FF0000)';
|
|
105
|
+
},
|
|
106
|
+
when: (ctx) => ctx.flags.color === undefined,
|
|
107
|
+
});
|
|
108
|
+
// Add description prompt
|
|
109
|
+
resolver.addPrompt({
|
|
110
|
+
flagName: 'description',
|
|
111
|
+
type: 'input',
|
|
112
|
+
message: 'Description (optional):',
|
|
113
|
+
default: flags.description,
|
|
114
|
+
when: (ctx) => ctx.flags.description === undefined,
|
|
115
|
+
});
|
|
116
|
+
// Add default prompt
|
|
117
|
+
resolver.addPrompt({
|
|
118
|
+
flagName: 'default',
|
|
119
|
+
type: 'list',
|
|
120
|
+
message: 'Set as default status for new tickets?',
|
|
121
|
+
choices: () => [
|
|
122
|
+
{ name: 'No', value: false, command: 'prlt status create --json' },
|
|
123
|
+
{ name: 'Yes', value: true, command: 'prlt status create --default --json' },
|
|
124
|
+
],
|
|
125
|
+
default: flags.default || false,
|
|
126
|
+
when: (ctx) => ctx.flags.default === undefined,
|
|
127
|
+
});
|
|
128
|
+
}
|
|
125
129
|
// Resolve all flags
|
|
126
130
|
const resolved = await resolver.resolve();
|
|
127
131
|
const status = await this.storage.createStatus(projectId, {
|