@dac-tech-team-tw/itsaplan-cli 0.1.0 → 0.3.0
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 +94 -49
- package/dist/cli.js +269 -89
- package/dist/commands.js +192 -25
- package/dist/config.d.ts +9 -0
- package/dist/config.js +62 -2
- package/dist/contracts.d.ts +1 -1
- package/dist/contracts.js +1 -1
- package/dist/initiatives.d.ts +16 -0
- package/dist/initiatives.js +241 -0
- package/dist/issues.d.ts +2 -0
- package/dist/issues.js +97 -6
- package/dist/output.d.ts +12 -0
- package/dist/output.js +261 -0
- package/dist/project-config.d.ts +2 -0
- package/dist/project-config.js +56 -0
- package/dist/writes.d.ts +3 -3
- package/dist/writes.js +6 -6
- package/package.json +2 -1
package/dist/output.js
ADDED
|
@@ -0,0 +1,261 @@
|
|
|
1
|
+
import { CliError } from "./contracts.js";
|
|
2
|
+
const scalarFields = (names) => Object.fromEntries(names.split(" ").map((name) => [name, true]));
|
|
3
|
+
const projectFields = scalarFields("id ref key name description teamId teamName teamRef mcpEnabled teamMcpEnabled initiativesEnabled dashboardsEnabled documentsEnabled notesEnabled cyclesEnabled subtasksEnabled checklistsEnabled issueStatsEnabled pointsEstimateEnabled timeEstimateEnabled timeLoggingEnabled availableFeatures archivedAt createdAt updatedAt lastActivityAt isFavorite isHidden");
|
|
4
|
+
const columnFields = scalarFields("id projectId name stateType color position wipLimit wipMode autoAssignUserId unavailable");
|
|
5
|
+
const typeFields = scalarFields("id projectId name icon color isDefault position");
|
|
6
|
+
const labelFields = scalarFields("id projectId groupId name color unavailable");
|
|
7
|
+
const memberFields = scalarFields("id userId name email username image kind agentKind restrictedToUserId respondsToMention canReadWorkItems isAgent");
|
|
8
|
+
const optionFields = scalarFields("id value color position unavailable");
|
|
9
|
+
const customFieldFields = {
|
|
10
|
+
...scalarFields("id projectId issueTypeId name fieldType memberScope showInBody position"),
|
|
11
|
+
options: optionFields,
|
|
12
|
+
};
|
|
13
|
+
const fieldValueFields = {
|
|
14
|
+
...scalarFields("fieldId name fieldType value valueEnd optionIds"),
|
|
15
|
+
options: optionFields,
|
|
16
|
+
};
|
|
17
|
+
const issueFields = {
|
|
18
|
+
...scalarFields("id projectId sequenceNumber identifier typeId assigneeUserId delegateUserId columnId parentId title description priority estimatePoints estimateMinutes loggedMinutes startDate dueDate position createdAt updatedAt completedAt archivedAt statusSince labelIds"),
|
|
19
|
+
initiative: scalarFields("id title status"),
|
|
20
|
+
cycle: scalarFields("id name status"),
|
|
21
|
+
fieldValues: fieldValueFields,
|
|
22
|
+
fields: fieldValueFields,
|
|
23
|
+
state: columnFields,
|
|
24
|
+
};
|
|
25
|
+
const relatedIssueFields = {
|
|
26
|
+
...scalarFields("id projectId sequenceNumber identifier title columnId typeId archived"),
|
|
27
|
+
state: columnFields,
|
|
28
|
+
type: typeFields,
|
|
29
|
+
};
|
|
30
|
+
const feedValueFields = scalarFields("value id stateType repo number date");
|
|
31
|
+
const feedFields = {
|
|
32
|
+
...scalarFields("id issueId kind replyToId actorUserId actorName body action createdAt editedAt"),
|
|
33
|
+
payload: {
|
|
34
|
+
subject: feedValueFields,
|
|
35
|
+
from: feedValueFields,
|
|
36
|
+
to: feedValueFields,
|
|
37
|
+
},
|
|
38
|
+
};
|
|
39
|
+
// Replies use the same finite entry fields, without an unbounded recursive schema.
|
|
40
|
+
feedFields.replies = { ...feedFields };
|
|
41
|
+
const schemas = {
|
|
42
|
+
"project.list": projectFields,
|
|
43
|
+
"project.view": {
|
|
44
|
+
project: projectFields,
|
|
45
|
+
columns: columnFields,
|
|
46
|
+
issueTypes: typeFields,
|
|
47
|
+
labels: labelFields,
|
|
48
|
+
labelGroups: scalarFields("id projectId name color"),
|
|
49
|
+
assignees: memberFields,
|
|
50
|
+
customFields: customFieldFields,
|
|
51
|
+
issueTemplates: scalarFields("id name description titleTemplate descriptionTemplate typeId columnId priority assigneeUserId labelIds"),
|
|
52
|
+
viewer: scalarFields("role teamRole via"),
|
|
53
|
+
permissions: true,
|
|
54
|
+
},
|
|
55
|
+
"issue.list": {
|
|
56
|
+
...issueFields,
|
|
57
|
+
parent: scalarFields("id identifier title"),
|
|
58
|
+
state: scalarFields("id name stateType unavailable"),
|
|
59
|
+
},
|
|
60
|
+
"issue.resolve": scalarFields("id issueId projectId identifier sequenceNumber projectRef"),
|
|
61
|
+
"issue.comments": feedFields,
|
|
62
|
+
"issue.activity": feedFields,
|
|
63
|
+
"issue.view": {
|
|
64
|
+
...issueFields,
|
|
65
|
+
type: typeFields,
|
|
66
|
+
project: projectFields,
|
|
67
|
+
assignee: memberFields,
|
|
68
|
+
delegate: memberFields,
|
|
69
|
+
labels: labelFields,
|
|
70
|
+
customFields: customFieldFields,
|
|
71
|
+
parent: relatedIssueFields,
|
|
72
|
+
subtasks: relatedIssueFields,
|
|
73
|
+
links: { ...scalarFields("id kind direction"), issue: relatedIssueFields },
|
|
74
|
+
watchers: memberFields,
|
|
75
|
+
checklists: {
|
|
76
|
+
...scalarFields("id title position"),
|
|
77
|
+
items: scalarFields("id content done position"),
|
|
78
|
+
},
|
|
79
|
+
attachments: scalarFields("id filename contentType sizeBytes createdAt url"),
|
|
80
|
+
comments: { items: feedFields, nextCursor: true },
|
|
81
|
+
development: {
|
|
82
|
+
...scalarFields("id provider repository kind number title url state draft sourceBranch targetBranch headSha pipelineStatus pipelineUrl checkStatus updatedAt"),
|
|
83
|
+
checks: true,
|
|
84
|
+
},
|
|
85
|
+
},
|
|
86
|
+
};
|
|
87
|
+
const isObject = (value) => value !== null && typeof value === "object" && !Array.isArray(value);
|
|
88
|
+
const owns = (value, key) => Object.prototype.hasOwnProperty.call(value, key);
|
|
89
|
+
function selectedFields(kind, options) {
|
|
90
|
+
if (options.summary && options.fields !== undefined)
|
|
91
|
+
throw new CliError("CONFLICTING_ARGUMENTS", "--summary and --fields are mutually exclusive");
|
|
92
|
+
if (options.fields === undefined)
|
|
93
|
+
return [];
|
|
94
|
+
const fields = options.fields.split(",").map((field) => field.trim());
|
|
95
|
+
for (const field of fields) {
|
|
96
|
+
const parts = field.split(".");
|
|
97
|
+
if (!field ||
|
|
98
|
+
parts.some((part) => !/^[A-Za-z_][A-Za-z0-9_]*$/.test(part) ||
|
|
99
|
+
part === "prototype" ||
|
|
100
|
+
owns(Object.prototype, part)))
|
|
101
|
+
throw new CliError("INVALID_FIELDS", "--fields must contain nonempty comma-separated dotted field names, without array indexes or prototype properties", 2, { kind, field });
|
|
102
|
+
let schema = schemas[kind];
|
|
103
|
+
for (const part of parts) {
|
|
104
|
+
if (schema === true || !owns(schema, part))
|
|
105
|
+
throw new CliError("INVALID_FIELDS", `Unknown --fields path ${JSON.stringify(field)} for ${kind}`, 2, { kind, field });
|
|
106
|
+
schema = schema[part];
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
return [...new Set(fields)];
|
|
110
|
+
}
|
|
111
|
+
/** Validate against command schemas even when the eventual result is empty. */
|
|
112
|
+
export function validateOutput(kind, options) {
|
|
113
|
+
selectedFields(kind, options);
|
|
114
|
+
}
|
|
115
|
+
/** Only opt-in issue list state output needs project state lookup enrichment. */
|
|
116
|
+
export function needsIssueStates(kind, options) {
|
|
117
|
+
const fields = selectedFields(kind, options);
|
|
118
|
+
return (kind === "issue.list" &&
|
|
119
|
+
(options.summary === true ||
|
|
120
|
+
fields.some((field) => field === "state" || field.startsWith("state."))));
|
|
121
|
+
}
|
|
122
|
+
function selection(fields) {
|
|
123
|
+
const root = { all: false, children: new Map() };
|
|
124
|
+
for (const field of fields) {
|
|
125
|
+
let node = root;
|
|
126
|
+
for (const part of field.split(".")) {
|
|
127
|
+
let child = node.children.get(part);
|
|
128
|
+
if (!child) {
|
|
129
|
+
child = { all: false, children: new Map() };
|
|
130
|
+
node.children.set(part, child);
|
|
131
|
+
}
|
|
132
|
+
node = child;
|
|
133
|
+
}
|
|
134
|
+
node.all = true;
|
|
135
|
+
}
|
|
136
|
+
return root;
|
|
137
|
+
}
|
|
138
|
+
function missingSelection(node, path, missing) {
|
|
139
|
+
if (node.all)
|
|
140
|
+
missing.add(path);
|
|
141
|
+
for (const [key, child] of node.children)
|
|
142
|
+
missingSelection(child, path ? `${path}.${key}` : key, missing);
|
|
143
|
+
}
|
|
144
|
+
function select(value, node, path, missing) {
|
|
145
|
+
if (node.all && node.children.size === 0)
|
|
146
|
+
return value;
|
|
147
|
+
// A present null is not an absent descendant, and array positions never shift.
|
|
148
|
+
if (value === null)
|
|
149
|
+
return null;
|
|
150
|
+
if (Array.isArray(value))
|
|
151
|
+
return value.map((entry, index) => select(entry, node, `${path}[${index}]`, missing));
|
|
152
|
+
const projected = {};
|
|
153
|
+
for (const [key, child] of node.children) {
|
|
154
|
+
const childPath = path ? `${path}.${key}` : key;
|
|
155
|
+
if (!isObject(value) || !owns(value, key) || value[key] === undefined) {
|
|
156
|
+
missingSelection(child, childPath, missing);
|
|
157
|
+
continue;
|
|
158
|
+
}
|
|
159
|
+
const nested = select(value[key], child, childPath, missing);
|
|
160
|
+
if (!node.all)
|
|
161
|
+
projected[key] = nested;
|
|
162
|
+
}
|
|
163
|
+
// A whole-field selection includes unknown future server properties unchanged.
|
|
164
|
+
return node.all ? value : projected;
|
|
165
|
+
}
|
|
166
|
+
const projectIdentity = selection(["id", "ref", "key", "name"]);
|
|
167
|
+
const projectListSummary = selection(["id", "ref", "name"]);
|
|
168
|
+
const projectViewSummary = selection([
|
|
169
|
+
"project.id",
|
|
170
|
+
"project.ref",
|
|
171
|
+
"project.key",
|
|
172
|
+
"project.name",
|
|
173
|
+
"columns.id",
|
|
174
|
+
"columns.name",
|
|
175
|
+
"columns.stateType",
|
|
176
|
+
"issueTypes.id",
|
|
177
|
+
"issueTypes.name",
|
|
178
|
+
"labels.id",
|
|
179
|
+
"labels.name",
|
|
180
|
+
"customFields",
|
|
181
|
+
]);
|
|
182
|
+
const issueListSummary = selection(["id", "identifier", "title", "columnId"]);
|
|
183
|
+
const stateSummary = selection(["id", "name", "stateType"]);
|
|
184
|
+
function summary(kind, value, path, missing) {
|
|
185
|
+
if (kind === "project.list")
|
|
186
|
+
return select(value, projectListSummary, path, missing);
|
|
187
|
+
if (kind === "project.view")
|
|
188
|
+
return select(value, projectViewSummary, path, missing);
|
|
189
|
+
if (kind === "issue.view" && isObject(value)) {
|
|
190
|
+
const projected = { ...value };
|
|
191
|
+
if (owns(value, "project"))
|
|
192
|
+
projected.project = select(value.project, projectIdentity, "project", missing);
|
|
193
|
+
else
|
|
194
|
+
missing.add("project");
|
|
195
|
+
return projected;
|
|
196
|
+
}
|
|
197
|
+
if (kind === "issue.list" && isObject(value)) {
|
|
198
|
+
const projected = select(value, issueListSummary, path, missing);
|
|
199
|
+
const state = value.state;
|
|
200
|
+
projected.state =
|
|
201
|
+
isObject(state) && state.unavailable === true
|
|
202
|
+
? state
|
|
203
|
+
: select(state, stateSummary, `${path}.state`, missing);
|
|
204
|
+
return projected;
|
|
205
|
+
}
|
|
206
|
+
// Resolve is already compact; feed bodies, payloads and replies stay lossless.
|
|
207
|
+
return value;
|
|
208
|
+
}
|
|
209
|
+
/** Shape data only: serialization, redaction and fetching belong to callers. */
|
|
210
|
+
export function projectOutput(kind, result, options) {
|
|
211
|
+
// Error and write-outcome envelopes must remain exactly as received.
|
|
212
|
+
if (result.error !== undefined)
|
|
213
|
+
return result;
|
|
214
|
+
const fields = selectedFields(kind, options);
|
|
215
|
+
if (!options.summary && options.fields === undefined)
|
|
216
|
+
return result;
|
|
217
|
+
const missing = new Set();
|
|
218
|
+
const tree = selection(fields);
|
|
219
|
+
const project = (value, path) => {
|
|
220
|
+
if (options.summary)
|
|
221
|
+
return summary(kind, value, path, missing);
|
|
222
|
+
const projected = select(value, tree, path, missing);
|
|
223
|
+
// Unknown list states retain their diagnostic identity even for state.name.
|
|
224
|
+
if (kind === "issue.list" &&
|
|
225
|
+
tree.children.has("state") &&
|
|
226
|
+
isObject(value) &&
|
|
227
|
+
isObject(value.state) &&
|
|
228
|
+
value.state.unavailable === true &&
|
|
229
|
+
isObject(projected))
|
|
230
|
+
projected.state = { id: value.state.id, unavailable: true };
|
|
231
|
+
return projected;
|
|
232
|
+
};
|
|
233
|
+
const list = kind === "project.list" ||
|
|
234
|
+
kind === "issue.list" ||
|
|
235
|
+
kind === "issue.comments" ||
|
|
236
|
+
kind === "issue.activity";
|
|
237
|
+
const data = options.summary &&
|
|
238
|
+
(kind === "issue.resolve" ||
|
|
239
|
+
kind === "issue.comments" ||
|
|
240
|
+
kind === "issue.activity")
|
|
241
|
+
? result.data
|
|
242
|
+
: list && isObject(result.data) && Array.isArray(result.data.items)
|
|
243
|
+
? {
|
|
244
|
+
...result.data,
|
|
245
|
+
items: result.data.items.map((entry, index) => project(entry, `items[${index}]`)),
|
|
246
|
+
}
|
|
247
|
+
: project(result.data, "");
|
|
248
|
+
return {
|
|
249
|
+
...result,
|
|
250
|
+
data,
|
|
251
|
+
meta: {
|
|
252
|
+
...result.meta,
|
|
253
|
+
output: {
|
|
254
|
+
...(isObject(result.meta?.output) ? result.meta?.output : {}),
|
|
255
|
+
mode: options.summary ? "summary" : "fields",
|
|
256
|
+
...(options.summary ? {} : { fields }),
|
|
257
|
+
missing: [...missing],
|
|
258
|
+
},
|
|
259
|
+
},
|
|
260
|
+
};
|
|
261
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { CliError, } from "./contracts.js";
|
|
2
|
+
import { createContext, readUserConfig, resolveApiUrl, resolveProjectSelection, saveUserProject, } from "./config.js";
|
|
3
|
+
import { runProject } from "./commands.js";
|
|
4
|
+
export async function runProjectConfig(action, options, ref) {
|
|
5
|
+
try {
|
|
6
|
+
// Inspect directory trust even for local-only commands and explicit overrides.
|
|
7
|
+
const selection = await resolveProjectSelection(options);
|
|
8
|
+
const apiUrl = await resolveApiUrl(options);
|
|
9
|
+
if (!apiUrl)
|
|
10
|
+
throw new CliError("MISSING_API_URL", "Set ITSAPLAN_URL, use --api-url, or run auth login.");
|
|
11
|
+
if (action === "get")
|
|
12
|
+
return {
|
|
13
|
+
data: {
|
|
14
|
+
action,
|
|
15
|
+
apiUrl,
|
|
16
|
+
savedProjectRef: (await readUserConfig()).project_defaults?.[apiUrl] ?? null,
|
|
17
|
+
effectiveProjectRef: selection.projectRef ?? null,
|
|
18
|
+
source: selection.source,
|
|
19
|
+
},
|
|
20
|
+
};
|
|
21
|
+
let projectRef;
|
|
22
|
+
if (action === "set") {
|
|
23
|
+
if (!ref || !/^(?:[A-Za-z0-9_-]+\.)?[A-Za-z][A-Za-z0-9_-]*$/u.test(ref))
|
|
24
|
+
throw new CliError("INVALID_PROJECT", "Use a project key or team.KEY ref.");
|
|
25
|
+
const ctx = await createContext({ ...options, project: ref });
|
|
26
|
+
if (!ctx.auth.tokenAvailable)
|
|
27
|
+
throw new CliError("AUTHENTICATION_REQUIRED", "Authenticate before validating a default project.", 3);
|
|
28
|
+
// Reuse project view's complete setup validation, not just a permissive ref lookup.
|
|
29
|
+
const result = await runProject(ctx, "view", ref);
|
|
30
|
+
const project = result.data.project;
|
|
31
|
+
projectRef = project.ref;
|
|
32
|
+
if (!/^[A-Za-z0-9_-]+\.[A-Za-z][A-Za-z0-9_-]*$/u.test(projectRef) ||
|
|
33
|
+
project.key !== projectRef.split(".")[1] ||
|
|
34
|
+
(ref.includes(".") ? projectRef !== ref : project.key !== ref))
|
|
35
|
+
throw new CliError("PROJECT_MISMATCH", "The API returned a different project ref; local configuration was not changed.");
|
|
36
|
+
}
|
|
37
|
+
const { changed } = await saveUserProject(apiUrl, projectRef);
|
|
38
|
+
return {
|
|
39
|
+
data: { action, apiUrl, projectRef: projectRef ?? null, changed },
|
|
40
|
+
outcome: changed ? "applied" : "not_applied",
|
|
41
|
+
verification: "passed",
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
catch (error) {
|
|
45
|
+
if (action === "get")
|
|
46
|
+
throw error;
|
|
47
|
+
if (error instanceof CliError)
|
|
48
|
+
throw new CliError(error.code, error.message, error.exitCode, {
|
|
49
|
+
...error.details,
|
|
50
|
+
outcome: "not_applied",
|
|
51
|
+
verification: "not_run",
|
|
52
|
+
retryable: false,
|
|
53
|
+
});
|
|
54
|
+
throw new CliError("LOCAL_OPERATION_FAILED", "The default project was not changed; no API write was sent.", 1, { outcome: "not_applied", verification: "not_run", retryable: false });
|
|
55
|
+
}
|
|
56
|
+
}
|
package/dist/writes.d.ts
CHANGED
|
@@ -2,7 +2,7 @@ import { type Actor, type Context, type ObjectData, type Result } from "./contra
|
|
|
2
2
|
import { type IssueOptions } from "./issues.js";
|
|
3
3
|
/** Preflight failures are never mistaken for a write with an unknown outcome. */
|
|
4
4
|
export declare function withWrite(options: IssueOptions, operation: (actor: Actor) => Promise<Result>): Promise<Result>;
|
|
5
|
-
export declare function preview(ctx: Context, actor: Actor, data: ObjectData, path: string, body: ObjectData, warnings?: string[]): Promise<Result>;
|
|
6
|
-
export declare function writeFailure(error: unknown, data: ObjectData, actor: Actor): Result;
|
|
7
|
-
export declare function verificationFailed(data: ObjectData, actor: Actor): Result;
|
|
5
|
+
export declare function preview(ctx: Context, actor: Actor, data: ObjectData, path: string, body: ObjectData, warnings?: string[], method?: string): Promise<Result>;
|
|
6
|
+
export declare function writeFailure(error: unknown, data: ObjectData, actor: Actor, resource?: string): Result;
|
|
7
|
+
export declare function verificationFailed(data: ObjectData, actor: Actor, resource?: string): Result;
|
|
8
8
|
export declare function applied(data: ObjectData, actor: Actor): Result;
|
package/dist/writes.js
CHANGED
|
@@ -26,13 +26,13 @@ export async function withWrite(options, operation) {
|
|
|
26
26
|
});
|
|
27
27
|
}
|
|
28
28
|
}
|
|
29
|
-
export async function preview(ctx, actor, data, path, body, warnings = []) {
|
|
29
|
+
export async function preview(ctx, actor, data, path, body, warnings = [], method = "POST") {
|
|
30
30
|
return {
|
|
31
31
|
data: {
|
|
32
32
|
...data,
|
|
33
33
|
dryRun: true,
|
|
34
34
|
identity: await me(ctx),
|
|
35
|
-
request: { method
|
|
35
|
+
request: { method, path, body },
|
|
36
36
|
warnings: [
|
|
37
37
|
"Preview sends no writes and does not guarantee server permissions or concurrent state.",
|
|
38
38
|
...warnings,
|
|
@@ -43,14 +43,14 @@ export async function preview(ctx, actor, data, path, body, warnings = []) {
|
|
|
43
43
|
verification: "not_run",
|
|
44
44
|
};
|
|
45
45
|
}
|
|
46
|
-
export function writeFailure(error, data, actor) {
|
|
47
|
-
const result = issueWriteFailure(error, data, actor,
|
|
46
|
+
export function writeFailure(error, data, actor, resource = "document or relation") {
|
|
47
|
+
const result = issueWriteFailure(error, data, actor, resource);
|
|
48
48
|
if (result.outcome === "unknown")
|
|
49
49
|
delete data.changed;
|
|
50
50
|
return result;
|
|
51
51
|
}
|
|
52
|
-
export function verificationFailed(data, actor) {
|
|
53
|
-
return issueVerificationFailed(data, actor,
|
|
52
|
+
export function verificationFailed(data, actor, resource = "document or relation") {
|
|
53
|
+
return issueVerificationFailed(data, actor, resource);
|
|
54
54
|
}
|
|
55
55
|
export function applied(data, actor) {
|
|
56
56
|
return { data, meta: { actor }, outcome: "applied", verification: "passed" };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dac-tech-team-tw/itsaplan-cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"description": "Composable It's a Plan CLI for humans and coding agents",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"repository": {
|
|
@@ -27,6 +27,7 @@
|
|
|
27
27
|
"build": "tsc",
|
|
28
28
|
"typecheck": "tsc --noEmit && tsc -p tsconfig.test.json",
|
|
29
29
|
"test": "tsx --test test/*.test.ts",
|
|
30
|
+
"release": "node scripts/publish-local.mjs",
|
|
30
31
|
"format": "prettier --write src test package.json tsconfig*.json",
|
|
31
32
|
"prepack": "npm run build",
|
|
32
33
|
"install-local": "node scripts/install-local.mjs"
|