alignfirst 0.4.0 → 0.6.0-preview.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 +54 -2
- package/dist/cli.js +2 -1
- package/dist/commands/config.js +15 -4
- package/dist/commands/context.js +23 -3
- package/dist/commands/docmap.js +4 -1
- package/dist/commands/doctor.js +27 -4
- package/dist/commands/guide.js +6 -6
- package/dist/commands/plans.js +18 -15
- package/dist/commands/sync.js +5 -5
- package/dist/commands/ticket.js +22 -19
- package/dist/context.d.ts +2 -0
- package/dist/conventions.js +11 -9
- package/dist/format.d.ts +2 -0
- package/dist/format.js +8 -0
- package/dist/plans/archive.d.ts +8 -2
- package/dist/plans/archive.js +41 -17
- package/dist/plans/catchup.js +12 -10
- package/dist/plans/layout.d.ts +4 -7
- package/dist/plans/layout.js +10 -14
- package/dist/plans/link.d.ts +1 -1
- package/dist/plans/link.js +8 -6
- package/dist/plans/mode.d.ts +3 -1
- package/dist/plans/mode.js +11 -7
- package/dist/plans/ticket.d.ts +7 -4
- package/dist/plans/ticket.js +30 -20
- package/dist/project-config.d.ts +4 -3
- package/dist/project-config.js +7 -9
- package/dist/project-layout.d.ts +30 -0
- package/dist/project-layout.js +174 -0
- package/package.json +3 -3
- package/templates/guide/code-review/correctness-reviewer.md +1 -0
- package/templates/guide/code-review/quality-reviewer.md +1 -0
- package/templates/guide/code-review/reviewer-common.md +2 -0
- package/templates/guide/core.md +1 -1
- package/templates/guide/protocols/aad.md +24 -17
- package/templates/guide/protocols/description.md +8 -8
- package/templates/guide/protocols/merge.md +5 -5
- package/templates/guide/protocols/plan.md +59 -70
- package/templates/guide/protocols/spec.md +44 -37
package/dist/plans/archive.js
CHANGED
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
import { existsSync, mkdirSync, readdirSync, renameSync, statSync } from "node:fs";
|
|
2
2
|
import { basename, dirname, extname, join, relative } from "node:path";
|
|
3
3
|
import { CliError } from "../cli-error.js";
|
|
4
|
+
import { separateSessionTree } from "../project-layout.js";
|
|
4
5
|
import { isTicketName } from "./layout.js";
|
|
5
6
|
const DEFAULT_ARCHIVE_DAYS = 7;
|
|
6
7
|
const DAY_MS = 86_400_000;
|
|
8
|
+
const SESSION_TREE_LABEL = " (session tree)";
|
|
7
9
|
export function archiveThresholdDays(env) {
|
|
8
10
|
const value = env.ALIGNFIRST_ARCHIVE_DAYS;
|
|
9
11
|
if (value === undefined)
|
|
@@ -13,24 +15,35 @@ export function archiveThresholdDays(env) {
|
|
|
13
15
|
throw new CliError("ALIGNFIRST_ARCHIVE_DAYS must be a positive number of days.");
|
|
14
16
|
return days;
|
|
15
17
|
}
|
|
16
|
-
|
|
18
|
+
/**
|
|
19
|
+
* Archives the stale entries of `.plans`, and of a separate session tree into its own `_archives`.
|
|
20
|
+
* Returns whether `.plans` changed.
|
|
21
|
+
*/
|
|
22
|
+
export function autoArchive(layout, thresholdDays, stdout) {
|
|
17
23
|
const cutoff = Date.now() - thresholdDays * DAY_MS;
|
|
18
|
-
const
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
];
|
|
22
|
-
if (
|
|
24
|
+
const plansDir = layout.locations[".plans"].path;
|
|
25
|
+
const sessionsDir = separateSessionTree(layout);
|
|
26
|
+
const plansCandidates = staleEntries(plansDir, cutoff);
|
|
27
|
+
const sessionCandidates = sessionsDir === undefined ? [] : staleEntries(sessionsDir, cutoff);
|
|
28
|
+
if (plansCandidates.length === 0 && sessionCandidates.length === 0) {
|
|
23
29
|
stdout.write("Nothing to archive.\n");
|
|
24
30
|
return false;
|
|
25
31
|
}
|
|
26
|
-
for (const candidate of
|
|
32
|
+
for (const candidate of plansCandidates)
|
|
27
33
|
archiveEntry(plansDir, candidate, stdout);
|
|
28
|
-
|
|
34
|
+
if (sessionsDir !== undefined) {
|
|
35
|
+
for (const candidate of sessionCandidates)
|
|
36
|
+
archiveEntry(sessionsDir, candidate, stdout, SESSION_TREE_LABEL);
|
|
37
|
+
}
|
|
38
|
+
return plansCandidates.length > 0;
|
|
39
|
+
}
|
|
40
|
+
function staleEntries(root, cutoff) {
|
|
41
|
+
return [...staleTicketDirectories(root, cutoff), ...staleNoTicketSessionFiles(root, cutoff)];
|
|
29
42
|
}
|
|
30
|
-
function staleTicketDirectories(
|
|
31
|
-
return readdirSync(
|
|
43
|
+
function staleTicketDirectories(root, cutoff) {
|
|
44
|
+
return readdirSync(root, { withFileTypes: true })
|
|
32
45
|
.filter((entry) => entry.isDirectory() && isTicketName(entry.name))
|
|
33
|
-
.map((entry) => join(
|
|
46
|
+
.map((entry) => join(root, entry.name))
|
|
34
47
|
.filter((ticketDir) => newestFileMtime(ticketDir) < cutoff);
|
|
35
48
|
}
|
|
36
49
|
function newestFileMtime(dir) {
|
|
@@ -39,8 +52,8 @@ function newestFileMtime(dir) {
|
|
|
39
52
|
return statSync(dir).mtimeMs;
|
|
40
53
|
return Math.max(...files.map((entry) => statSync(join(entry.parentPath, entry.name)).mtimeMs));
|
|
41
54
|
}
|
|
42
|
-
function staleNoTicketSessionFiles(
|
|
43
|
-
const sessionDir = join(
|
|
55
|
+
function staleNoTicketSessionFiles(root, cutoff) {
|
|
56
|
+
const sessionDir = join(root, "_aligndev");
|
|
44
57
|
if (!existsSync(sessionDir))
|
|
45
58
|
return [];
|
|
46
59
|
return readdirSync(sessionDir, { withFileTypes: true })
|
|
@@ -48,13 +61,24 @@ function staleNoTicketSessionFiles(plansDir, cutoff) {
|
|
|
48
61
|
.map((entry) => join(sessionDir, entry.name))
|
|
49
62
|
.filter((path) => statSync(path).mtimeMs < cutoff);
|
|
50
63
|
}
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
const
|
|
64
|
+
/** Archiving ticket `name` also archives its directory in a separate session tree. */
|
|
65
|
+
export function archiveTicket(layout, name, stdout) {
|
|
66
|
+
const plansDir = layout.locations[".plans"].path;
|
|
67
|
+
archiveEntry(plansDir, join(plansDir, name), stdout);
|
|
68
|
+
const sessionsDir = separateSessionTree(layout);
|
|
69
|
+
if (sessionsDir === undefined)
|
|
70
|
+
return;
|
|
71
|
+
const sessionTicketDir = join(sessionsDir, name);
|
|
72
|
+
if (existsSync(sessionTicketDir))
|
|
73
|
+
archiveEntry(sessionsDir, sessionTicketDir, stdout, SESSION_TREE_LABEL);
|
|
74
|
+
}
|
|
75
|
+
function archiveEntry(root, sourcePath, stdout, label = "") {
|
|
76
|
+
const rel = relative(root, sourcePath);
|
|
77
|
+
const archivesDir = join(root, "_archives");
|
|
54
78
|
const targetDir = join(archivesDir, dirname(rel));
|
|
55
79
|
mkdirSync(targetDir, { recursive: true });
|
|
56
80
|
const target = moveToFreeName(sourcePath, targetDir, statSync(sourcePath).isFile());
|
|
57
|
-
stdout.write(`Archived ${rel} → _archives/${relative(archivesDir, target)}\n`);
|
|
81
|
+
stdout.write(`Archived ${rel}${label} → _archives/${relative(archivesDir, target)}\n`);
|
|
58
82
|
}
|
|
59
83
|
function moveToFreeName(sourcePath, targetDir, isFile) {
|
|
60
84
|
const name = basename(sourcePath);
|
package/dist/plans/catchup.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { readFileSync } from "node:fs";
|
|
2
|
-
import { join
|
|
2
|
+
import { join } from "node:path";
|
|
3
3
|
import { CliError } from "../cli-error.js";
|
|
4
4
|
import { errorMessage } from "../errors.js";
|
|
5
|
-
import { formatLocalTimestamp, formatSize } from "../format.js";
|
|
5
|
+
import { displayPath, formatLocalTimestamp, formatSize } from "../format.js";
|
|
6
6
|
const MAX_FILE_BYTES = 64 * 1024;
|
|
7
7
|
const MAX_OUTPUT_BYTES = 30 * 1024;
|
|
8
8
|
const PLAN_FILE = /^[A-Z]\d+-(?:main-plan|plan-.*)\.md$/;
|
|
@@ -15,14 +15,16 @@ export function renderCatchup(cwd, ticket, report) {
|
|
|
15
15
|
const outputBytes = files.reduce((total, file) => total + sectionBytes(file), Buffer.byteLength(report) + 1);
|
|
16
16
|
if (outputBytes > MAX_OUTPUT_BYTES)
|
|
17
17
|
return `${report}\n${TOO_LARGE_NOTICE}\n`;
|
|
18
|
-
return `${report}\n${files.map(
|
|
18
|
+
return `${report}\n${files.map(renderSection).join("")}`;
|
|
19
19
|
}
|
|
20
20
|
function catchupFile(cwd, dir, entry) {
|
|
21
21
|
if (entry.size === undefined || !isHistoryFile(entry.name))
|
|
22
22
|
return [];
|
|
23
|
+
const path = join(dir, entry.name);
|
|
23
24
|
return [
|
|
24
25
|
{
|
|
25
|
-
path
|
|
26
|
+
path,
|
|
27
|
+
displayPath: displayPath(cwd, path),
|
|
26
28
|
size: entry.size,
|
|
27
29
|
modified: formatLocalTimestamp(entry.modifiedAt),
|
|
28
30
|
},
|
|
@@ -39,17 +41,17 @@ function isOversized(file) {
|
|
|
39
41
|
return file.size > MAX_FILE_BYTES;
|
|
40
42
|
}
|
|
41
43
|
function wrapSection(file, body) {
|
|
42
|
-
return `<file path="${file.
|
|
44
|
+
return `<file path="${file.displayPath}" modified="${file.modified}">\n${body}\n</file>\n\n`;
|
|
43
45
|
}
|
|
44
|
-
function renderSection(
|
|
45
|
-
const body = isOversized(file) ? OMISSION_NOTICE : readBody(
|
|
46
|
+
function renderSection(file) {
|
|
47
|
+
const body = isOversized(file) ? OMISSION_NOTICE : readBody(file);
|
|
46
48
|
return wrapSection(file, body.replace(/\n+$/, ""));
|
|
47
49
|
}
|
|
48
|
-
function readBody(
|
|
50
|
+
function readBody(file) {
|
|
49
51
|
try {
|
|
50
|
-
return readFileSync(
|
|
52
|
+
return readFileSync(file.path, "utf-8");
|
|
51
53
|
}
|
|
52
54
|
catch (error) {
|
|
53
|
-
throw new CliError(`Cannot load catchup file ${file.
|
|
55
|
+
throw new CliError(`Cannot load catchup file ${file.displayPath}: ${errorMessage(error)}`);
|
|
54
56
|
}
|
|
55
57
|
}
|
package/dist/plans/layout.d.ts
CHANGED
|
@@ -1,11 +1,8 @@
|
|
|
1
1
|
import { type Stats } from "node:fs";
|
|
2
|
-
import {
|
|
3
|
-
export declare const PLANS_DIR = ".plans";
|
|
2
|
+
import type { ItemLocation } from "../project-layout.js";
|
|
4
3
|
export declare const ARCHIVES_DIR = "_archives";
|
|
5
4
|
export declare function isTicketName(name: string): boolean;
|
|
6
|
-
export declare function
|
|
7
|
-
export declare function archivesDir(cwd: string): string;
|
|
5
|
+
export declare function archivesDir(plansPath: string): string;
|
|
8
6
|
/** Returns the lstat of `.plans`, so callers can tell a symlink from a directory. */
|
|
9
|
-
export declare function assertPlansGate(
|
|
10
|
-
export declare function missingPlansMessage(form: string): string;
|
|
11
|
-
export declare function missingPlansError(form: string): CliError;
|
|
7
|
+
export declare function assertPlansGate(location: ItemLocation, form: string): Stats;
|
|
8
|
+
export declare function missingPlansMessage(location: ItemLocation, form: string): string;
|
package/dist/plans/layout.js
CHANGED
|
@@ -1,32 +1,28 @@
|
|
|
1
1
|
import { existsSync, lstatSync, statSync } from "node:fs";
|
|
2
2
|
import { join } from "node:path";
|
|
3
3
|
import { CliError } from "../cli-error.js";
|
|
4
|
-
export const PLANS_DIR = ".plans";
|
|
5
4
|
export const ARCHIVES_DIR = "_archives";
|
|
6
5
|
export function isTicketName(name) {
|
|
7
6
|
return !name.startsWith("_");
|
|
8
7
|
}
|
|
9
|
-
export function
|
|
10
|
-
return join(
|
|
11
|
-
}
|
|
12
|
-
export function archivesDir(cwd) {
|
|
13
|
-
return join(plansDir(cwd), ARCHIVES_DIR);
|
|
8
|
+
export function archivesDir(plansPath) {
|
|
9
|
+
return join(plansPath, ARCHIVES_DIR);
|
|
14
10
|
}
|
|
15
11
|
/** Returns the lstat of `.plans`, so callers can tell a symlink from a directory. */
|
|
16
|
-
export function assertPlansGate(
|
|
17
|
-
const path =
|
|
12
|
+
export function assertPlansGate(location, form) {
|
|
13
|
+
const { path } = location;
|
|
18
14
|
const stats = lstatSync(path, { throwIfNoEntry: false });
|
|
19
15
|
if (!stats)
|
|
20
|
-
throw
|
|
16
|
+
throw new CliError(missingPlansMessage(location, form));
|
|
21
17
|
if (stats.isSymbolicLink() && !existsSync(path))
|
|
22
18
|
throw new CliError(`The .plans symlink is broken. Re-run ${form} plans setup with the clone location.`);
|
|
23
19
|
if (!statSync(path).isDirectory())
|
|
24
20
|
throw new CliError(`.plans is not a directory. Remove it, then run ${form} plans setup (see the project documentation).`);
|
|
25
21
|
return stats;
|
|
26
22
|
}
|
|
27
|
-
export function missingPlansMessage(form) {
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
return
|
|
23
|
+
export function missingPlansMessage(location, form) {
|
|
24
|
+
const team = `Team work files: ${form} plans setup <clone-dir>`;
|
|
25
|
+
if (location.in === "companion")
|
|
26
|
+
return `No .plans/ directory at ${location.path} (companion).\nLocal work files: mkdir -p ${location.path}\n${team}`;
|
|
27
|
+
return `No .plans/ directory in the current directory.\nLocal work files: mkdir .plans && echo .plans >> .gitignore\n${team}`;
|
|
32
28
|
}
|
package/dist/plans/link.d.ts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
import type { CommandContext } from "../context.js";
|
|
2
|
-
export declare function linkPlans(ctx: CommandContext, targetDir: string): void;
|
|
2
|
+
export declare function linkPlans(ctx: CommandContext, plansPath: string, targetDir: string): void;
|
package/dist/plans/link.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import { cpSync, existsSync, lstatSync, readdirSync, realpathSync, rmSync, symlinkSync, } from "node:fs";
|
|
2
|
-
import { join, relative } from "node:path";
|
|
1
|
+
import { cpSync, existsSync, lstatSync, mkdirSync, readdirSync, realpathSync, rmSync, symlinkSync, } from "node:fs";
|
|
2
|
+
import { dirname, join, relative } from "node:path";
|
|
3
3
|
import { CliError } from "../cli-error.js";
|
|
4
|
-
|
|
5
|
-
|
|
4
|
+
import { displayPath } from "../format.js";
|
|
5
|
+
export function linkPlans(ctx, plansPath, targetDir) {
|
|
6
6
|
const stats = lstatSync(plansPath, { throwIfNoEntry: false });
|
|
7
7
|
if (stats?.isSymbolicLink()) {
|
|
8
8
|
if (existsSync(plansPath) && realpathSync(plansPath) === realpathSync(targetDir)) {
|
|
@@ -17,9 +17,11 @@ export function linkPlans(ctx, targetDir) {
|
|
|
17
17
|
else if (stats) {
|
|
18
18
|
throw new CliError(".plans exists and is not a directory.");
|
|
19
19
|
}
|
|
20
|
-
const
|
|
20
|
+
const parent = dirname(plansPath);
|
|
21
|
+
mkdirSync(parent, { recursive: true });
|
|
22
|
+
const target = relative(parent, targetDir);
|
|
21
23
|
symlinkSync(target, plansPath);
|
|
22
|
-
ctx.stdout.write(`Linked .
|
|
24
|
+
ctx.stdout.write(`Linked ${displayPath(ctx.cwd, plansPath)} → ${target}\n`);
|
|
23
25
|
ctx.stdout.write(`Publish with: ${ctx.form} sync\n`);
|
|
24
26
|
}
|
|
25
27
|
function migratePlansContent(ctx, plansPath, targetDir) {
|
package/dist/plans/mode.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { ItemLocation } from "../project-layout.js";
|
|
1
2
|
export type PlansMode = SharedPlans | LocalPlans;
|
|
2
3
|
export interface SharedPlans {
|
|
3
4
|
kind: "shared";
|
|
@@ -6,4 +7,5 @@ export interface SharedPlans {
|
|
|
6
7
|
export interface LocalPlans {
|
|
7
8
|
kind: "local";
|
|
8
9
|
}
|
|
9
|
-
|
|
10
|
+
/** `.plans` is shared when it sits in another repository than the project's. */
|
|
11
|
+
export declare function resolvePlansMode(cwd: string, location: ItemLocation, form: string): PlansMode;
|
package/dist/plans/mode.js
CHANGED
|
@@ -1,15 +1,19 @@
|
|
|
1
1
|
import { realpathSync } from "node:fs";
|
|
2
|
-
import { join } from "node:path";
|
|
3
2
|
import { CliError } from "../cli-error.js";
|
|
4
3
|
import { gitOutput } from "../git.js";
|
|
5
4
|
import { assertPlansGate } from "./layout.js";
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
const stats = assertPlansGate(
|
|
9
|
-
|
|
5
|
+
/** `.plans` is shared when it sits in another repository than the project's. */
|
|
6
|
+
export function resolvePlansMode(cwd, location, form) {
|
|
7
|
+
const stats = assertPlansGate(location, form);
|
|
8
|
+
const plansRepository = plansRepositoryId(location.path, stats.isSymbolicLink(), form);
|
|
9
|
+
if (plansRepository === undefined || plansRepository === repositoryId(cwd))
|
|
10
10
|
return { kind: "local" };
|
|
11
|
-
return {
|
|
11
|
+
return {
|
|
12
|
+
kind: "shared",
|
|
13
|
+
repoToplevel: gitOutput(location.path, "rev-parse", "--show-toplevel"),
|
|
14
|
+
};
|
|
12
15
|
}
|
|
16
|
+
/** Returns `undefined` for a real directory outside any git repository. */
|
|
13
17
|
function plansRepositoryId(plansPath, isSymlink, form) {
|
|
14
18
|
try {
|
|
15
19
|
return repositoryId(plansPath);
|
|
@@ -17,7 +21,7 @@ function plansRepositoryId(plansPath, isSymlink, form) {
|
|
|
17
21
|
catch {
|
|
18
22
|
if (isSymlink)
|
|
19
23
|
throw new CliError(`.plans points outside any git repository. Re-run ${form} plans setup with the clone location.`);
|
|
20
|
-
|
|
24
|
+
return;
|
|
21
25
|
}
|
|
22
26
|
}
|
|
23
27
|
function repositoryId(dir) {
|
package/dist/plans/ticket.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { type ProjectLayout } from "../project-layout.js";
|
|
1
2
|
export interface ResolvedTicketDir {
|
|
2
3
|
id: string;
|
|
3
4
|
dir: string;
|
|
@@ -27,9 +28,11 @@ export type TicketDetection = {
|
|
|
27
28
|
} | {
|
|
28
29
|
kind: "noBranch";
|
|
29
30
|
};
|
|
30
|
-
export declare function resolveTicketDir(
|
|
31
|
-
|
|
32
|
-
export declare function
|
|
31
|
+
export declare function resolveTicketDir(plansPath: string, id: string, { dryRun }: ResolveTicketOptions): ResolvedTicketDir;
|
|
32
|
+
/** Restores ticket `id` in a separate session tree, when only its archived copy exists. */
|
|
33
|
+
export declare function restoreSessionTreeTicket(layout: ProjectLayout, id: string): void;
|
|
34
|
+
export declare function reserveSideTicket(plansPath: string): string;
|
|
35
|
+
export declare function peekSideTicket(plansPath: string): string;
|
|
33
36
|
export interface NextFilePosition {
|
|
34
37
|
cycleLetter: string;
|
|
35
38
|
fileNumber: number;
|
|
@@ -43,4 +46,4 @@ export interface DeduceFromExistingOptions {
|
|
|
43
46
|
sideAllowed: boolean;
|
|
44
47
|
}
|
|
45
48
|
/** Without a ticket id pattern, the branch can still name an existing ticket directory. */
|
|
46
|
-
export declare function deduceTicketFromExisting(cwd: string, { sideAllowed }: DeduceFromExistingOptions): DeducedTicket;
|
|
49
|
+
export declare function deduceTicketFromExisting(cwd: string, plansPath: string, { sideAllowed }: DeduceFromExistingOptions): DeducedTicket;
|
package/dist/plans/ticket.js
CHANGED
|
@@ -4,7 +4,8 @@ import { CliError } from "../cli-error.js";
|
|
|
4
4
|
import { isNodeError } from "../errors.js";
|
|
5
5
|
import { formatLocalTimestamp } from "../format.js";
|
|
6
6
|
import { gitOutputOrUndefined } from "../git.js";
|
|
7
|
-
import {
|
|
7
|
+
import { separateSessionTree } from "../project-layout.js";
|
|
8
|
+
import { archivesDir, isTicketName } from "./layout.js";
|
|
8
9
|
const FILE_PREFIX = /^([A-Z])(\d+)-/;
|
|
9
10
|
const SIDE_TICKET = /^side-(\d+)$/;
|
|
10
11
|
const BRANCH_SEPARATORS = "[/_.-]";
|
|
@@ -12,11 +13,11 @@ const SIDE_TICKET_BRANCH = new RegExp(`^side-\\d+(?=$|${BRANCH_SEPARATORS})`);
|
|
|
12
13
|
const PATH_SAFE_TICKET = /^[A-Za-z0-9._-]+$/;
|
|
13
14
|
const ENTRY_ORDER = new Intl.Collator("en", { numeric: true });
|
|
14
15
|
const LISTED_TICKETS = 10;
|
|
15
|
-
export function resolveTicketDir(
|
|
16
|
-
const dir = join(
|
|
16
|
+
export function resolveTicketDir(plansPath, id, { dryRun }) {
|
|
17
|
+
const dir = join(plansPath, id);
|
|
17
18
|
if (existsSync(dir))
|
|
18
19
|
return { id, dir, state: "existing", entries: listEntries(dir) };
|
|
19
|
-
const archivedDir = join(archivesDir(
|
|
20
|
+
const archivedDir = join(archivesDir(plansPath), id);
|
|
20
21
|
if (existsSync(archivedDir)) {
|
|
21
22
|
const entries = listEntries(archivedDir);
|
|
22
23
|
if (!dryRun)
|
|
@@ -27,13 +28,21 @@ export function resolveTicketDir(cwd, id, { dryRun }) {
|
|
|
27
28
|
mkdirSync(dir);
|
|
28
29
|
return { id, dir, state: "created", entries: [] };
|
|
29
30
|
}
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
const
|
|
33
|
-
|
|
31
|
+
/** Restores ticket `id` in a separate session tree, when only its archived copy exists. */
|
|
32
|
+
export function restoreSessionTreeTicket(layout, id) {
|
|
33
|
+
const sessionsDir = separateSessionTree(layout);
|
|
34
|
+
if (sessionsDir === undefined)
|
|
35
|
+
return;
|
|
36
|
+
const dir = join(sessionsDir, id);
|
|
37
|
+
const archivedDir = join(archivesDir(sessionsDir), id);
|
|
38
|
+
if (!existsSync(dir) && existsSync(archivedDir))
|
|
39
|
+
renameSync(archivedDir, dir);
|
|
40
|
+
}
|
|
41
|
+
export function reserveSideTicket(plansPath) {
|
|
42
|
+
for (let number = highestSideTicket(plansPath) + 1;; ++number) {
|
|
34
43
|
const ticket = `side-${number}`;
|
|
35
44
|
try {
|
|
36
|
-
mkdirSync(join(
|
|
45
|
+
mkdirSync(join(plansPath, ticket));
|
|
37
46
|
return ticket;
|
|
38
47
|
}
|
|
39
48
|
catch (error) {
|
|
@@ -42,16 +51,17 @@ export function reserveSideTicket(cwd) {
|
|
|
42
51
|
}
|
|
43
52
|
}
|
|
44
53
|
}
|
|
45
|
-
export function peekSideTicket(
|
|
46
|
-
|
|
47
|
-
const highest = Math.max(highestSideTicket(root), highestSideTicket(archivesDir(cwd)));
|
|
48
|
-
for (let number = highest + 1;; ++number) {
|
|
54
|
+
export function peekSideTicket(plansPath) {
|
|
55
|
+
for (let number = highestSideTicket(plansPath) + 1;; ++number) {
|
|
49
56
|
const ticket = `side-${number}`;
|
|
50
|
-
if (!existsSync(join(
|
|
57
|
+
if (!existsSync(join(plansPath, ticket)))
|
|
51
58
|
return ticket;
|
|
52
59
|
}
|
|
53
60
|
}
|
|
54
|
-
function highestSideTicket(
|
|
61
|
+
function highestSideTicket(plansPath) {
|
|
62
|
+
return Math.max(highestSideTicketIn(plansPath), highestSideTicketIn(archivesDir(plansPath)));
|
|
63
|
+
}
|
|
64
|
+
function highestSideTicketIn(dir) {
|
|
55
65
|
let highest = 0;
|
|
56
66
|
for (const entry of readEntries(dir)) {
|
|
57
67
|
const match = entry.isDirectory() ? SIDE_TICKET.exec(entry.name) : null;
|
|
@@ -153,8 +163,8 @@ function currentBranch(cwd) {
|
|
|
153
163
|
return branch === undefined || branch === "" ? undefined : branch;
|
|
154
164
|
}
|
|
155
165
|
/** Without a ticket id pattern, the branch can still name an existing ticket directory. */
|
|
156
|
-
export function deduceTicketFromExisting(cwd, { sideAllowed }) {
|
|
157
|
-
const tickets = listTickets(
|
|
166
|
+
export function deduceTicketFromExisting(cwd, plansPath, { sideAllowed }) {
|
|
167
|
+
const tickets = listTickets(plansPath);
|
|
158
168
|
const branch = currentBranch(cwd);
|
|
159
169
|
if (branch !== undefined) {
|
|
160
170
|
const matches = tickets.filter((ticket) => branchNamesTicket(branch, ticket.id));
|
|
@@ -163,10 +173,10 @@ export function deduceTicketFromExisting(cwd, { sideAllowed }) {
|
|
|
163
173
|
}
|
|
164
174
|
throw new CliError(renderMissingTicketId(tickets, sideAllowed));
|
|
165
175
|
}
|
|
166
|
-
function listTickets(
|
|
176
|
+
function listTickets(plansPath) {
|
|
167
177
|
return [
|
|
168
|
-
...ticketDirectories(
|
|
169
|
-
...ticketDirectories(archivesDir(
|
|
178
|
+
...ticketDirectories(plansPath, false),
|
|
179
|
+
...ticketDirectories(archivesDir(plansPath), true),
|
|
170
180
|
].toSorted((left, right) => right.modifiedAt.getTime() - left.modifiedAt.getTime());
|
|
171
181
|
}
|
|
172
182
|
function ticketDirectories(dir, archived) {
|
package/dist/project-config.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { ProjectLayout } from "./project-layout.js";
|
|
1
2
|
export declare const PROJECT_CONFIG_FILENAME = ".alignfirst.json";
|
|
2
3
|
export interface ProjectConfig {
|
|
3
4
|
schemaVersion: 1;
|
|
@@ -27,8 +28,8 @@ export interface CommitConfig {
|
|
|
27
28
|
}
|
|
28
29
|
export interface ResolvedProjectConfig {
|
|
29
30
|
config: ProjectConfig;
|
|
30
|
-
source: "
|
|
31
|
+
source: "project" | "companion";
|
|
31
32
|
}
|
|
32
|
-
export declare function resolveProjectConfig(
|
|
33
|
-
export declare function readProjectConfig(
|
|
33
|
+
export declare function resolveProjectConfig(layout: ProjectLayout): ResolvedProjectConfig | undefined;
|
|
34
|
+
export declare function readProjectConfig(path: string): ProjectConfig;
|
|
34
35
|
export declare function validateProjectConfig(value: unknown, label: string): ProjectConfig;
|
package/dist/project-config.js
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { join } from "node:path";
|
|
1
|
+
import { readFileSync } from "node:fs";
|
|
3
2
|
import { type } from "arktype";
|
|
4
3
|
import semver from "semver";
|
|
5
4
|
import { CliError } from "./cli-error.js";
|
|
@@ -36,14 +35,13 @@ const projectConfigSchema = type({
|
|
|
36
35
|
"portRange?": portRangeSchema,
|
|
37
36
|
"git?": gitSchema,
|
|
38
37
|
});
|
|
39
|
-
export function resolveProjectConfig(
|
|
40
|
-
const
|
|
41
|
-
|
|
42
|
-
}
|
|
43
|
-
export function readProjectConfig(dir) {
|
|
44
|
-
const path = join(dir, PROJECT_CONFIG_FILENAME);
|
|
45
|
-
if (!existsSync(path))
|
|
38
|
+
export function resolveProjectConfig(layout) {
|
|
39
|
+
const location = layout.locations[PROJECT_CONFIG_FILENAME];
|
|
40
|
+
if (!location.exists)
|
|
46
41
|
return;
|
|
42
|
+
return { config: readProjectConfig(location.path), source: location.in };
|
|
43
|
+
}
|
|
44
|
+
export function readProjectConfig(path) {
|
|
47
45
|
let value;
|
|
48
46
|
try {
|
|
49
47
|
value = JSON.parse(readFileSync(path, "utf-8"));
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import type { CommandContext } from "./context.js";
|
|
2
|
+
export declare const ITEM_NAMES: readonly [".alignfirst.json", ".alignfirst.md", "DEVELOPERS.md", "docs", ".plans", "_aligndev"];
|
|
3
|
+
export type ItemName = (typeof ITEM_NAMES)[number];
|
|
4
|
+
export type Flag = boolean | "auto";
|
|
5
|
+
export interface ProjectLayout {
|
|
6
|
+
companion: CompanionLayout | null;
|
|
7
|
+
locations: Record<ItemName, ItemLocation>;
|
|
8
|
+
}
|
|
9
|
+
export interface CompanionLayout {
|
|
10
|
+
/** Absolute. */
|
|
11
|
+
dir: string;
|
|
12
|
+
exists: boolean;
|
|
13
|
+
/** Matching keys as written, most specific first. */
|
|
14
|
+
entries: string[];
|
|
15
|
+
/** Effective flags. */
|
|
16
|
+
flags: Record<ItemName, Flag>;
|
|
17
|
+
}
|
|
18
|
+
export interface ItemLocation {
|
|
19
|
+
/** Absolute. */
|
|
20
|
+
path: string;
|
|
21
|
+
in: "project" | "companion";
|
|
22
|
+
exists: boolean;
|
|
23
|
+
}
|
|
24
|
+
export declare function layoutOf(ctx: CommandContext): ProjectLayout;
|
|
25
|
+
export declare function resolveProjectLayout(cwd: string, home: string): ProjectLayout;
|
|
26
|
+
export declare function companionsPath(home: string): string;
|
|
27
|
+
/** One line: `<name>: <path> (<in>)`, with `, missing` when absent. */
|
|
28
|
+
export declare function renderItemLocation(name: ItemName, location: ItemLocation): string;
|
|
29
|
+
/** The `_aligndev` tree when it is not the resolved `.plans` and exists. */
|
|
30
|
+
export declare function separateSessionTree(layout: ProjectLayout): string | undefined;
|