alignfirst 0.1.0 → 0.2.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/dist/commands/guide.js +6 -5
- package/dist/commands/ticket.js +14 -6
- package/dist/conventions.js +5 -5
- package/dist/plans/ticket.d.ts +2 -3
- package/dist/plans/ticket.js +36 -16
- package/package.json +1 -1
- package/templates/guide/core.md +2 -2
- package/templates/guide/protocols/aad.md +1 -1
- package/templates/guide/protocols/description.md +1 -1
- package/templates/guide/protocols/merge.md +7 -3
- package/templates/guide/protocols/plan.md +1 -1
- package/templates/guide/protocols/review.md +1 -1
- package/templates/guide/protocols/spec.md +1 -1
package/dist/commands/guide.js
CHANGED
|
@@ -128,9 +128,10 @@ function renderCoreGuide(ctx, placeholders) {
|
|
|
128
128
|
}
|
|
129
129
|
function buildGuidePlaceholders(ctx, protocol) {
|
|
130
130
|
const pattern = ctx.projectConfig?.config.ticketIdPattern;
|
|
131
|
-
const
|
|
131
|
+
const template = ctx.projectConfig?.config.git?.branchNameTemplate;
|
|
132
|
+
const detection = detectTicketFromBranch(ctx.cwd, pattern, template);
|
|
132
133
|
return {
|
|
133
|
-
ticketCommand: detection
|
|
134
|
+
ticketCommand: detection.kind === "detected" ? "{{CMD}} ticket" : "{{CMD}} ticket <id>",
|
|
134
135
|
ticketDetection: renderTicketDetection(pattern, detection),
|
|
135
136
|
plansState: renderPlansState(ctx),
|
|
136
137
|
commitRule: renderCommitRule(ctx),
|
|
@@ -138,11 +139,11 @@ function buildGuidePlaceholders(ctx, protocol) {
|
|
|
138
139
|
};
|
|
139
140
|
}
|
|
140
141
|
function renderTicketDetection(pattern, detection) {
|
|
142
|
+
if (detection.kind === "detected")
|
|
143
|
+
return `Current ticket: \`${detection.id}\` (from branch \`${detection.branch}\`). The id argument of \`{{CMD}} ticket\` is optional and defaults to it; pass an id only when the user names another ticket.`;
|
|
141
144
|
if (pattern === undefined)
|
|
142
145
|
return "Ask the user for the ticket ID when it is not given.";
|
|
143
|
-
if (detection
|
|
144
|
-
return `Current ticket: \`${detection.id}\` (from branch \`${detection.branch}\`). The id argument of \`{{CMD}} ticket\` is optional and defaults to it; pass an id only when the user names another ticket.`;
|
|
145
|
-
if (detection?.kind === "noMatch")
|
|
146
|
+
if (detection.kind === "noMatch")
|
|
146
147
|
return `No ticket id on branch \`${detection.branch}\`. Ask the user for the id.`;
|
|
147
148
|
return "No ticket id: detached HEAD. Ask the user for the id.";
|
|
148
149
|
}
|
package/dist/commands/ticket.js
CHANGED
|
@@ -5,7 +5,7 @@ import { formatLocalTimestamp, formatSize } from "../format.js";
|
|
|
5
5
|
import { parseCommandArgs } from "../parse-args.js";
|
|
6
6
|
import { renderCatchup } from "../plans/catchup.js";
|
|
7
7
|
import { assertPlansGate } from "../plans/layout.js";
|
|
8
|
-
import {
|
|
8
|
+
import { detectTicketFromBranch, deduceTicketFromExisting, nextFilePosition, peekSideTicket, reserveSideTicket, resolveTicketDir, validateTicketId, } from "../plans/ticket.js";
|
|
9
9
|
const USAGE = `Usage:
|
|
10
10
|
{{FORM}} ticket [<id>] [--next [<filename>]] [--new-cycle] [--json] [--dry-run]
|
|
11
11
|
{{FORM}} ticket --side [--next [<filename>]] [--new-cycle] [--json] [--dry-run]
|
|
@@ -70,10 +70,11 @@ function parseTicketArgs(ctx, args, usage) {
|
|
|
70
70
|
if (values.catchup &&
|
|
71
71
|
(values.side || values.next !== undefined || values.json || values["dry-run"]))
|
|
72
72
|
throw new CliError(`--catchup cannot be combined with --side, --next, --json, or --dry-run.\n\n${usage}`);
|
|
73
|
+
const next = values.next === undefined ? undefined : resolveNextRequest(normalized.requests, usage);
|
|
73
74
|
const resolution = resolveTicketId(ctx, positionals[0], values);
|
|
74
75
|
return {
|
|
75
76
|
...resolution,
|
|
76
|
-
next
|
|
77
|
+
next,
|
|
77
78
|
newCycle: values["new-cycle"],
|
|
78
79
|
json: values.json,
|
|
79
80
|
dryRun: values["dry-run"],
|
|
@@ -125,16 +126,23 @@ function validateNextFilename(filename) {
|
|
|
125
126
|
function resolveTicketId(ctx, positional, flags) {
|
|
126
127
|
const pattern = ctx.projectConfig?.config.ticketIdPattern;
|
|
127
128
|
if (positional !== undefined) {
|
|
128
|
-
validateTicketId(positional
|
|
129
|
+
validateTicketId(positional);
|
|
129
130
|
return { id: positional };
|
|
130
131
|
}
|
|
131
132
|
if (flags.side)
|
|
132
133
|
return { id: flags["dry-run"] ? peekSideTicket(ctx.cwd) : reserveSideTicket(ctx.cwd) };
|
|
134
|
+
const template = ctx.projectConfig?.config.git?.branchNameTemplate;
|
|
135
|
+
const detection = detectTicketFromBranch(ctx.cwd, pattern, template);
|
|
136
|
+
if (detection.kind === "detected") {
|
|
137
|
+
validateTicketId(detection.id);
|
|
138
|
+
return detection;
|
|
139
|
+
}
|
|
133
140
|
if (pattern === undefined)
|
|
134
141
|
return deduceTicketFromExisting(ctx.cwd, { sideAllowed: !flags.catchup });
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
142
|
+
if (detection.kind === "noBranch")
|
|
143
|
+
throw new CliError("Cannot deduce a ticket id from a detached HEAD.");
|
|
144
|
+
const templateDetail = template?.includes("{TICKET_ID}") ? ` and template "${template}"` : "";
|
|
145
|
+
throw new CliError(`Cannot deduce a ticket id from branch "${detection.branch}" with pattern "${pattern}"${templateDetail}.`);
|
|
138
146
|
}
|
|
139
147
|
function resolveTicket(ctx, options) {
|
|
140
148
|
if (options.side && !options.dryRun)
|
package/dist/conventions.js
CHANGED
|
@@ -18,8 +18,8 @@ export function renderConventions(ctx) {
|
|
|
18
18
|
function renderTicketIds(ctx) {
|
|
19
19
|
const pattern = ctx.projectConfig?.config.ticketIdPattern;
|
|
20
20
|
if (pattern === undefined)
|
|
21
|
-
return "Ticket IDs: no configured format; ask the user for the ID. Without
|
|
22
|
-
return `Ticket IDs: \`${pattern}\`; infer a matching ID from the branch. Without
|
|
21
|
+
return "Ticket IDs: no configured format; ask the user for the ID. Without a ticket, or when the user asks for a side ticket, use the next `side-N`.";
|
|
22
|
+
return `Ticket IDs: \`${pattern}\`; infer a matching ID from the branch. Without a ticket, or when the user asks for a side ticket, use the next \`side-N\`.`;
|
|
23
23
|
}
|
|
24
24
|
function renderBranchNames(ctx) {
|
|
25
25
|
const template = ctx.projectConfig?.config.git?.branchNameTemplate;
|
|
@@ -50,10 +50,10 @@ function renderPlans(ctx) {
|
|
|
50
50
|
try {
|
|
51
51
|
const mode = resolvePlansMode(ctx.cwd, ctx.form);
|
|
52
52
|
const folder = ctx.projectConfig?.config.plans?.folder;
|
|
53
|
-
const sharedFolder = folder === undefined ? "" : ` (shared folder \`${folder}
|
|
53
|
+
const sharedFolder = folder === undefined ? "" : ` (shared folder \`${folder}\`, a separate git repository)`;
|
|
54
54
|
const base = mode.kind === "shared"
|
|
55
|
-
? `Plans: use \`.plans\`${sharedFolder};
|
|
56
|
-
: "Plans: use `.plans
|
|
55
|
+
? `Plans: use \`.plans\`${sharedFolder}; run \`${ctx.form} sync\` after changes.`
|
|
56
|
+
: "Plans: use `.plans`.";
|
|
57
57
|
const archival = ctx.projectConfig?.config.plans?.autoArchive === true
|
|
58
58
|
? " Automatic archival is enabled."
|
|
59
59
|
: "";
|
package/dist/plans/ticket.d.ts
CHANGED
|
@@ -37,11 +37,10 @@ export interface NextFilePosition {
|
|
|
37
37
|
export declare function nextFilePosition(entries: readonly string[], newCycle: boolean): NextFilePosition;
|
|
38
38
|
export declare function listEntries(dir: string): TicketEntry[];
|
|
39
39
|
export declare function isPathSafeTicketId(id: string): boolean;
|
|
40
|
-
export declare function validateTicketId(id: string
|
|
41
|
-
export declare function detectTicketFromBranch(cwd: string, pattern
|
|
40
|
+
export declare function validateTicketId(id: string): void;
|
|
41
|
+
export declare function detectTicketFromBranch(cwd: string, pattern?: string, template?: string): TicketDetection;
|
|
42
42
|
export interface DeduceFromExistingOptions {
|
|
43
43
|
sideAllowed: boolean;
|
|
44
44
|
}
|
|
45
45
|
/** Without a ticket id pattern, the branch can still name an existing ticket directory. */
|
|
46
46
|
export declare function deduceTicketFromExisting(cwd: string, { sideAllowed }: DeduceFromExistingOptions): DeducedTicket;
|
|
47
|
-
export declare function deduceTicketFromBranch(cwd: string, pattern: string): DeducedTicket;
|
package/dist/plans/ticket.js
CHANGED
|
@@ -7,9 +7,10 @@ import { gitOutputOrUndefined } from "../git.js";
|
|
|
7
7
|
import { archivesDir, isTicketName, plansDir } from "./layout.js";
|
|
8
8
|
const FILE_PREFIX = /^([A-Z])(\d+)-/;
|
|
9
9
|
const SIDE_TICKET = /^side-(\d+)$/;
|
|
10
|
+
const BRANCH_SEPARATORS = "[/_.-]";
|
|
11
|
+
const SIDE_TICKET_BRANCH = new RegExp(`^side-\\d+(?=$|${BRANCH_SEPARATORS})`);
|
|
10
12
|
const PATH_SAFE_TICKET = /^[A-Za-z0-9._-]+$/;
|
|
11
13
|
const ENTRY_ORDER = new Intl.Collator("en", { numeric: true });
|
|
12
|
-
const BRANCH_SEPARATORS = "[/_.-]";
|
|
13
14
|
const LISTED_TICKETS = 10;
|
|
14
15
|
export function resolveTicketDir(cwd, id, { dryRun }) {
|
|
15
16
|
const dir = join(plansDir(cwd), id);
|
|
@@ -104,22 +105,49 @@ function statOrLink(path) {
|
|
|
104
105
|
export function isPathSafeTicketId(id) {
|
|
105
106
|
return id !== "." && !id.includes("..") && PATH_SAFE_TICKET.test(id);
|
|
106
107
|
}
|
|
107
|
-
export function validateTicketId(id
|
|
108
|
+
export function validateTicketId(id) {
|
|
108
109
|
if (!isPathSafeTicketId(id) || !isTicketName(id))
|
|
109
110
|
throw new CliError(`Invalid ticket id: ${id}`);
|
|
110
|
-
if (pattern !== undefined && !new RegExp(pattern).test(id) && !SIDE_TICKET.test(id))
|
|
111
|
-
throw new CliError(`Ticket id "${id}" does not match ticketIdPattern "${pattern}".`);
|
|
112
111
|
}
|
|
113
|
-
export function detectTicketFromBranch(cwd, pattern) {
|
|
112
|
+
export function detectTicketFromBranch(cwd, pattern, template) {
|
|
114
113
|
const branch = currentBranch(cwd);
|
|
115
114
|
if (branch === undefined)
|
|
116
115
|
return { kind: "noBranch" };
|
|
117
|
-
const
|
|
118
|
-
|
|
116
|
+
const sideTicket = SIDE_TICKET_BRANCH.exec(branch);
|
|
117
|
+
if (sideTicket)
|
|
118
|
+
return { kind: "detected", id: sideTicket[0], branch };
|
|
119
|
+
if (pattern === undefined)
|
|
120
|
+
return { kind: "noMatch", branch };
|
|
121
|
+
if (template?.includes("{TICKET_ID}")) {
|
|
122
|
+
const match = branchPatternFromTemplate(template, unanchorPattern(pattern)).exec(branch);
|
|
123
|
+
const id = match?.[1];
|
|
124
|
+
if (id === undefined || !isPathSafeTicketId(id) || !isTicketName(id))
|
|
125
|
+
return { kind: "noMatch", branch };
|
|
126
|
+
return { kind: "detected", id, branch };
|
|
127
|
+
}
|
|
128
|
+
const match = new RegExp(unanchorPattern(pattern)).exec(branch);
|
|
119
129
|
if (!match)
|
|
120
130
|
return { kind: "noMatch", branch };
|
|
121
131
|
return { kind: "detected", id: match[0], branch };
|
|
122
132
|
}
|
|
133
|
+
function unanchorPattern(pattern) {
|
|
134
|
+
return pattern.replace(/^\^/, "").replace(/\$$/, "");
|
|
135
|
+
}
|
|
136
|
+
function branchPatternFromTemplate(template, ticketPattern) {
|
|
137
|
+
const placeholder = /\{[^{}]+\}/g;
|
|
138
|
+
let source = "";
|
|
139
|
+
let cursor = 0;
|
|
140
|
+
for (const match of template.matchAll(placeholder)) {
|
|
141
|
+
source += escapeRegexLiteral(template.slice(cursor, match.index));
|
|
142
|
+
source += match[0] === "{TICKET_ID}" ? `((?:${ticketPattern})|side-\\d+)` : ".+";
|
|
143
|
+
cursor = match.index + match[0].length;
|
|
144
|
+
}
|
|
145
|
+
source += escapeRegexLiteral(template.slice(cursor));
|
|
146
|
+
return new RegExp(`^${source}$`);
|
|
147
|
+
}
|
|
148
|
+
function escapeRegexLiteral(value) {
|
|
149
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
150
|
+
}
|
|
123
151
|
function currentBranch(cwd) {
|
|
124
152
|
const branch = gitOutputOrUndefined(cwd, "branch", "--show-current");
|
|
125
153
|
return branch === undefined || branch === "" ? undefined : branch;
|
|
@@ -157,7 +185,7 @@ function newestModification(dir) {
|
|
|
157
185
|
return new Date(Math.max(...entries.map((entry) => entry.modifiedAt.getTime())));
|
|
158
186
|
}
|
|
159
187
|
function branchNamesTicket(branch, id) {
|
|
160
|
-
const escaped = id
|
|
188
|
+
const escaped = escapeRegexLiteral(id);
|
|
161
189
|
return new RegExp(`(^|${BRANCH_SEPARATORS})${escaped}($|${BRANCH_SEPARATORS})`).test(branch);
|
|
162
190
|
}
|
|
163
191
|
function renderMissingTicketId(tickets, sideAllowed) {
|
|
@@ -175,11 +203,3 @@ function renderMissingTicketId(tickets, sideAllowed) {
|
|
|
175
203
|
lines.push(` … ${rest} more`);
|
|
176
204
|
return `No ticket id given. Existing tickets:\n${lines.join("\n")}\n${hint}`;
|
|
177
205
|
}
|
|
178
|
-
export function deduceTicketFromBranch(cwd, pattern) {
|
|
179
|
-
const result = detectTicketFromBranch(cwd, pattern);
|
|
180
|
-
if (result.kind === "noBranch")
|
|
181
|
-
throw new CliError("Cannot deduce a ticket id from a detached HEAD.");
|
|
182
|
-
if (result.kind === "noMatch")
|
|
183
|
-
throw new CliError(`Cannot deduce a ticket id from branch "${result.branch}" with pattern "${pattern}".`);
|
|
184
|
-
return result;
|
|
185
|
-
}
|
package/package.json
CHANGED
package/templates/guide/core.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
## Ticket Directory
|
|
4
4
|
|
|
5
|
-
TICKET_DIR holds a ticket's work files and includes a trailing slash. TICKET_ID is the external ticket ID or `side-N` for work without
|
|
5
|
+
TICKET_DIR holds a ticket's work files and includes a trailing slash. TICKET_ID is the external ticket ID, or `side-N` for a side ticket: work without a ticket.
|
|
6
6
|
|
|
7
7
|
{{TICKET_DETECTION}}
|
|
8
8
|
|
|
@@ -10,7 +10,7 @@ TICKET_DIR holds a ticket's work files and includes a trailing slash. TICKET_ID
|
|
|
10
10
|
|
|
11
11
|
{{PLANS_STATE}}
|
|
12
12
|
|
|
13
|
-
When the user says there is no
|
|
13
|
+
When the user says there is no ticket or asks for a side ticket, run `{{CMD}} ticket --side` and use the returned TICKET_ID in subsequent ticket commands. Reuse an existing `side-N` directory when the user refers to earlier work. Omit the ticket ID from commit messages.
|
|
14
14
|
|
|
15
15
|
## Work Files
|
|
16
16
|
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
## Prerequisites
|
|
4
4
|
|
|
5
|
-
Run `{{TICKET_CMD}}` once to identify TICKET_DIR and load the ticket directory context (`{{CMD}} ticket --side` when there is no
|
|
5
|
+
Run `{{TICKET_CMD}}` once to identify TICKET_DIR and load the ticket directory context (`{{CMD}} ticket --side` for a side ticket, when there is no ticket).
|
|
6
6
|
|
|
7
7
|
---
|
|
8
8
|
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
## Prerequisites
|
|
4
4
|
|
|
5
|
-
Run `{{TICKET_CMD}}` once to identify TICKET_DIR and load the ticket directory context (`{{CMD}} ticket --side` when there is no
|
|
5
|
+
Run `{{TICKET_CMD}}` once to identify TICKET_DIR and load the ticket directory context (`{{CMD}} ticket --side` for a side ticket, when there is no ticket).
|
|
6
6
|
|
|
7
7
|
## Steps
|
|
8
8
|
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
## Prerequisites
|
|
4
4
|
|
|
5
|
-
Run `{{TICKET_CMD}}` once to identify TICKET_DIR and load the ticket directory context (`{{CMD}} ticket --side` when there is no
|
|
5
|
+
Run `{{TICKET_CMD}}` once to identify TICKET_DIR and load the ticket directory context (`{{CMD}} ticket --side` for a side ticket, when there is no ticket).
|
|
6
6
|
|
|
7
7
|
---
|
|
8
8
|
|
|
@@ -24,14 +24,18 @@ Run `{{TICKET_CMD}} --next merge.summary.md` to continue the current cycle. Appe
|
|
|
24
24
|
|
|
25
25
|
Resolve the conflicts properly — preserve both intents whenever possible. Do not blindly accept one side.
|
|
26
26
|
|
|
27
|
+
Resolve conflicts one at a time. Avoid batch processing, broad search-and-replace operations, and other brute-force edits.
|
|
28
|
+
|
|
27
29
|
**Special case for lock files:** If a lock file has conflicts:
|
|
28
30
|
|
|
29
31
|
1. Accept all the changes from the incoming branch.
|
|
30
32
|
2. After all other conflicts are resolved, run the proper install command so the package manager re-applies the current branch's dependency changes.
|
|
31
33
|
|
|
32
|
-
## 4.
|
|
34
|
+
## 4. Validate and Commit
|
|
35
|
+
|
|
36
|
+
After resolving all conflicts, run the codebase's usual checks, such as compilation, linting, and unit tests. Commit the merge as soon as the available checks pass, using Git's default message (for example, `git commit --no-edit`).
|
|
33
37
|
|
|
34
|
-
|
|
38
|
+
If you need to execute the project, whether through E2E tests or manual checks, do so after the merge commit. Commit any resulting fixes separately.
|
|
35
39
|
|
|
36
40
|
## 5. Summarize
|
|
37
41
|
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
|
|
7
7
|
You need:
|
|
8
8
|
|
|
9
|
-
- the TICKET_DIR and ticket directory context — run `{{TICKET_CMD}}` once (`{{CMD}} ticket --side` when there is no
|
|
9
|
+
- the TICKET_DIR and ticket directory context — run `{{TICKET_CMD}}` once (`{{CMD}} ticket --side` for a side ticket, when there is no ticket)
|
|
10
10
|
- a **spec file** in the TICKET_DIR
|
|
11
11
|
|
|
12
12
|
Identify TICKET_DIR and the spec file before starting the protocol. If either is missing, STOP AND ASK THE USER.
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
|
|
5
5
|
You need:
|
|
6
6
|
|
|
7
|
-
- the TICKET_DIR and ticket directory context — run `{{TICKET_CMD}}` once (`{{CMD}} ticket --side` when there is no
|
|
7
|
+
- the TICKET_DIR and ticket directory context — run `{{TICKET_CMD}}` once (`{{CMD}} ticket --side` for a side ticket, when there is no ticket)
|
|
8
8
|
- {{BASE_BRANCH_RULE}}
|
|
9
9
|
|
|
10
10
|
Identify TICKET_DIR and the base branch before starting the protocol.
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
## Prerequisites
|
|
4
4
|
|
|
5
|
-
Run `{{TICKET_CMD}}` once to identify TICKET_DIR and load the ticket directory context (`{{CMD}} ticket --side` when there is no
|
|
5
|
+
Run `{{TICKET_CMD}}` once to identify TICKET_DIR and load the ticket directory context (`{{CMD}} ticket --side` for a side ticket, when there is no ticket).
|
|
6
6
|
|
|
7
7
|
## Phases
|
|
8
8
|
|