alignfirst 0.1.0-beta.6 → 0.1.0-beta.7
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/plans.js +4 -2
- package/dist/commands/ticket.js +1 -1
- package/dist/conventions.js +2 -2
- package/dist/plans/catchup.js +2 -2
- package/dist/plans/layout.d.ts +3 -1
- package/dist/plans/layout.js +11 -4
- package/dist/plans/mode.js +3 -9
- package/dist/plans/ticket.js +1 -1
- package/dist/project-config.js +20 -0
- package/package.json +1 -1
package/dist/commands/plans.js
CHANGED
|
@@ -5,6 +5,7 @@ import { CliError } from "../cli-error.js";
|
|
|
5
5
|
import { assertMainWorktreeRoot } from "../git.js";
|
|
6
6
|
import { parseBareCommandArgs, parseCommandArgs } from "../parse-args.js";
|
|
7
7
|
import { archiveEntry, archiveThresholdDays, autoArchive } from "../plans/archive.js";
|
|
8
|
+
import { isTicketName } from "../plans/layout.js";
|
|
8
9
|
import { linkPlans } from "../plans/link.js";
|
|
9
10
|
import { resolvePlansMode } from "../plans/mode.js";
|
|
10
11
|
import { findStoppedRebase, renderStoppedRebase } from "../plans/rebase.js";
|
|
@@ -156,9 +157,10 @@ function resolveArchiveTarget(ctx, args, usage) {
|
|
|
156
157
|
const stats = statSync(target, { throwIfNoEntry: false });
|
|
157
158
|
if (!stats?.isDirectory() || realpathSync(dirname(target)) !== realpathSync(plansDir))
|
|
158
159
|
throw new CliError(`${argument} must be an existing directory directly under .plans.`);
|
|
159
|
-
|
|
160
|
+
const name = basename(target);
|
|
161
|
+
if (!isTicketName(name))
|
|
160
162
|
throw new CliError(`${argument}: names starting with _ are not tickets.`);
|
|
161
|
-
return
|
|
163
|
+
return join(plansDir, name);
|
|
162
164
|
}
|
|
163
165
|
function isPathArgument(argument) {
|
|
164
166
|
return argument.includes("/") || argument.includes("\\");
|
package/dist/commands/ticket.js
CHANGED
|
@@ -16,11 +16,11 @@ Files over 64 KiB are listed without content. Above 30 KiB of output, only the e
|
|
|
16
16
|
list is printed.
|
|
17
17
|
`;
|
|
18
18
|
export function runTicket(ctx, args) {
|
|
19
|
-
assertPlansGate(ctx.cwd, ctx.form);
|
|
20
19
|
const usage = renderUsage(ctx);
|
|
21
20
|
const parsed = parseTicketArgs(ctx, args, usage);
|
|
22
21
|
if (parsed === undefined)
|
|
23
22
|
return 0;
|
|
23
|
+
assertPlansGate(ctx.cwd, ctx.form);
|
|
24
24
|
const result = resolveTicket(ctx, parsed);
|
|
25
25
|
if (parsed.catchup) {
|
|
26
26
|
ctx.stdout.write(renderCatchup(ctx.cwd, result, renderReport(ctx, parsed, result)));
|
package/dist/conventions.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { existsSync, lstatSync } from "node:fs";
|
|
2
2
|
import { join } from "node:path";
|
|
3
3
|
import { renderDefaultBranchLine, resolveDefaultBranch } from "./default-branch.js";
|
|
4
|
+
import { errorMessage } from "./errors.js";
|
|
4
5
|
import { gitOutputOrUndefined, gitSucceeds } from "./git.js";
|
|
5
6
|
import { resolvePlansMode } from "./plans/mode.js";
|
|
6
7
|
export function renderConventions(ctx) {
|
|
@@ -59,8 +60,7 @@ function renderPlans(ctx) {
|
|
|
59
60
|
return `${base}${archival}`;
|
|
60
61
|
}
|
|
61
62
|
catch (error) {
|
|
62
|
-
|
|
63
|
-
return `Plans: ${message.split("\n", 1)[0]}`;
|
|
63
|
+
return `Plans: ${errorMessage(error).split("\n", 1)[0]}`;
|
|
64
64
|
}
|
|
65
65
|
}
|
|
66
66
|
function plansEntryExists(cwd) {
|
package/dist/plans/catchup.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { readFileSync } from "node:fs";
|
|
2
2
|
import { join, relative } from "node:path";
|
|
3
3
|
import { CliError } from "../cli-error.js";
|
|
4
|
+
import { errorMessage } from "../errors.js";
|
|
4
5
|
import { formatLocalTimestamp, formatSize } from "../format.js";
|
|
5
6
|
const MAX_FILE_BYTES = 64 * 1024;
|
|
6
7
|
const MAX_OUTPUT_BYTES = 30 * 1024;
|
|
@@ -49,7 +50,6 @@ function readBody(cwd, file) {
|
|
|
49
50
|
return readFileSync(join(cwd, file.path), "utf-8");
|
|
50
51
|
}
|
|
51
52
|
catch (error) {
|
|
52
|
-
|
|
53
|
-
throw new CliError(`Cannot load catchup file ${file.path}: ${detail}`);
|
|
53
|
+
throw new CliError(`Cannot load catchup file ${file.path}: ${errorMessage(error)}`);
|
|
54
54
|
}
|
|
55
55
|
}
|
package/dist/plans/layout.d.ts
CHANGED
|
@@ -1,9 +1,11 @@
|
|
|
1
|
+
import { type Stats } from "node:fs";
|
|
1
2
|
import { CliError } from "../cli-error.js";
|
|
2
3
|
export declare const PLANS_DIR = ".plans";
|
|
3
4
|
export declare const ARCHIVES_DIR = "_archives";
|
|
4
5
|
export declare function isTicketName(name: string): boolean;
|
|
5
6
|
export declare function plansDir(cwd: string): string;
|
|
6
7
|
export declare function archivesDir(cwd: string): string;
|
|
7
|
-
|
|
8
|
+
/** Returns the lstat of `.plans`, so callers can tell a symlink from a directory. */
|
|
9
|
+
export declare function assertPlansGate(cwd: string, form: string): Stats;
|
|
8
10
|
export declare function missingPlansMessage(form: string): string;
|
|
9
11
|
export declare function missingPlansError(form: string): CliError;
|
package/dist/plans/layout.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { existsSync } from "node:fs";
|
|
1
|
+
import { existsSync, lstatSync, statSync } from "node:fs";
|
|
2
2
|
import { join } from "node:path";
|
|
3
3
|
import { CliError } from "../cli-error.js";
|
|
4
4
|
export const PLANS_DIR = ".plans";
|
|
@@ -12,10 +12,17 @@ export function plansDir(cwd) {
|
|
|
12
12
|
export function archivesDir(cwd) {
|
|
13
13
|
return join(plansDir(cwd), ARCHIVES_DIR);
|
|
14
14
|
}
|
|
15
|
+
/** Returns the lstat of `.plans`, so callers can tell a symlink from a directory. */
|
|
15
16
|
export function assertPlansGate(cwd, form) {
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
17
|
+
const path = plansDir(cwd);
|
|
18
|
+
const stats = lstatSync(path, { throwIfNoEntry: false });
|
|
19
|
+
if (!stats)
|
|
20
|
+
throw missingPlansError(form);
|
|
21
|
+
if (stats.isSymbolicLink() && !existsSync(path))
|
|
22
|
+
throw new CliError(`The .plans symlink is broken. Re-run ${form} plans setup with the clone location.`);
|
|
23
|
+
if (!statSync(path).isDirectory())
|
|
24
|
+
throw new CliError(`.plans is not a directory. Remove it, then run ${form} plans setup (see the project documentation).`);
|
|
25
|
+
return stats;
|
|
19
26
|
}
|
|
20
27
|
export function missingPlansMessage(form) {
|
|
21
28
|
return `No .plans/ directory in the current directory.\nLocal plans: mkdir .plans && echo .plans >> .gitignore\nTeam plans: ${form} plans setup <clone-dir>`;
|
package/dist/plans/mode.js
CHANGED
|
@@ -1,17 +1,11 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { realpathSync } from "node:fs";
|
|
2
2
|
import { join } from "node:path";
|
|
3
3
|
import { CliError } from "../cli-error.js";
|
|
4
4
|
import { gitOutput } from "../git.js";
|
|
5
|
-
import {
|
|
5
|
+
import { assertPlansGate } from "./layout.js";
|
|
6
6
|
export function resolvePlansMode(cwd, form) {
|
|
7
7
|
const plansPath = join(cwd, ".plans");
|
|
8
|
-
const stats =
|
|
9
|
-
if (!stats)
|
|
10
|
-
throw missingPlansError(form);
|
|
11
|
-
if (stats.isSymbolicLink() && !existsSync(plansPath))
|
|
12
|
-
throw new CliError(`The .plans symlink is broken. Re-run ${form} plans setup with the clone location.`);
|
|
13
|
-
if (!statSync(plansPath).isDirectory())
|
|
14
|
-
throw new CliError(`.plans is not a directory. Remove it, then run ${form} plans setup (see the project documentation).`);
|
|
8
|
+
const stats = assertPlansGate(cwd, form);
|
|
15
9
|
if (plansRepositoryId(plansPath, stats.isSymbolicLink(), form) === repositoryId(cwd))
|
|
16
10
|
return { kind: "local" };
|
|
17
11
|
return { kind: "shared", repoToplevel: gitOutput(plansPath, "rev-parse", "--show-toplevel") };
|
package/dist/plans/ticket.js
CHANGED
|
@@ -105,7 +105,7 @@ export function isPathSafeTicketId(id) {
|
|
|
105
105
|
return id !== "." && !id.includes("..") && PATH_SAFE_TICKET.test(id);
|
|
106
106
|
}
|
|
107
107
|
export function validateTicketId(id, pattern) {
|
|
108
|
-
if (!isPathSafeTicketId(id))
|
|
108
|
+
if (!isPathSafeTicketId(id) || !isTicketName(id))
|
|
109
109
|
throw new CliError(`Invalid ticket id: ${id}`);
|
|
110
110
|
if (pattern !== undefined && !new RegExp(pattern).test(id) && !SIDE_TICKET.test(id))
|
|
111
111
|
throw new CliError(`Ticket id "${id}" does not match ticketIdPattern "${pattern}".`);
|
package/dist/project-config.js
CHANGED
|
@@ -75,6 +75,26 @@ function assertValidPattern(pattern, label) {
|
|
|
75
75
|
catch (error) {
|
|
76
76
|
throw invalidConfig(label, `ticketIdPattern is not a valid regular expression: ${errorMessage(error)}`);
|
|
77
77
|
}
|
|
78
|
+
if (hasInnerAnchor(pattern))
|
|
79
|
+
throw invalidConfig(label, "ticketIdPattern must be one anchored expression; group alternatives: ^(ABC|XYZ)-\\d+$");
|
|
80
|
+
}
|
|
81
|
+
/** Branch detection unanchors the pattern by trimming its ends, so inner anchors never match. */
|
|
82
|
+
function hasInnerAnchor(pattern) {
|
|
83
|
+
let inClass = false;
|
|
84
|
+
for (let i = 0; i < pattern.length; ++i) {
|
|
85
|
+
const char = pattern[i];
|
|
86
|
+
if (char === "\\")
|
|
87
|
+
++i;
|
|
88
|
+
else if (inClass)
|
|
89
|
+
inClass = char !== "]";
|
|
90
|
+
else if (char === "[")
|
|
91
|
+
inClass = true;
|
|
92
|
+
else if (char === "^" && i !== 0)
|
|
93
|
+
return true;
|
|
94
|
+
else if (char === "$" && i !== pattern.length - 1)
|
|
95
|
+
return true;
|
|
96
|
+
}
|
|
97
|
+
return false;
|
|
78
98
|
}
|
|
79
99
|
function assertValidPortRange(range, label) {
|
|
80
100
|
if (range.first > range.last)
|