reffy-cli 1.2.1 → 1.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 +17 -0
- package/dist/manifest.d.ts +3 -0
- package/dist/manifest.js +65 -0
- package/dist/storage.d.ts +4 -0
- package/dist/storage.js +10 -29
- package/dist/types.d.ts +2 -0
- package/dist/workspace.js +23 -7
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -59,6 +59,23 @@ reffy diagram render --input .reffy/reffyspec/specs/auth/spec.md --format ascii
|
|
|
59
59
|
reffy diagram render --input .reffy/reffyspec/specs/auth/spec.md --format svg --output .reffy/artifacts/auth-spec.svg
|
|
60
60
|
```
|
|
61
61
|
|
|
62
|
+
## Manifest Contract
|
|
63
|
+
|
|
64
|
+
Reffy keeps workspace metadata in `.reffy/manifest.json`. New managed manifests include both core timestamps and workspace identity:
|
|
65
|
+
|
|
66
|
+
```json
|
|
67
|
+
{
|
|
68
|
+
"version": 1,
|
|
69
|
+
"created_at": "2026-04-18T00:00:00.000Z",
|
|
70
|
+
"updated_at": "2026-04-18T00:00:00.000Z",
|
|
71
|
+
"project_id": "my-project",
|
|
72
|
+
"workspace_name": "my-project",
|
|
73
|
+
"artifacts": []
|
|
74
|
+
}
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
Older v1 manifests without `project_id` and `workspace_name` still validate. Running `reffy init` migrates managed manifests by adding those fields with deterministic defaults derived from the repository name.
|
|
78
|
+
|
|
62
79
|
## Using Reffy With ReffySpec
|
|
63
80
|
|
|
64
81
|
A practical pattern is:
|
package/dist/manifest.d.ts
CHANGED
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
import type { Manifest } from "./types.js";
|
|
2
2
|
export declare const MANIFEST_VERSION = 1;
|
|
3
3
|
export declare function allowedKindExtensions(): Record<string, string[]>;
|
|
4
|
+
export declare function deriveManifestIdentity(repoRoot: string): Required<Pick<Manifest, "project_id" | "workspace_name">>;
|
|
5
|
+
export declare function createManifest(repoRoot: string, now?: string): Manifest;
|
|
6
|
+
export declare function normalizeManifest(raw: unknown, repoRoot: string): Manifest;
|
|
4
7
|
export declare function inferArtifactType(filePath: string): {
|
|
5
8
|
kind: string;
|
|
6
9
|
mime_type: string;
|
package/dist/manifest.js
CHANGED
|
@@ -12,6 +12,8 @@ const KIND_EXTENSIONS = {
|
|
|
12
12
|
doc: [".doc", ".docx"],
|
|
13
13
|
file: [],
|
|
14
14
|
};
|
|
15
|
+
const PROJECT_ID_PATTERN = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
|
|
16
|
+
const FALLBACK_WORKSPACE_ID = "reffy-workspace";
|
|
15
17
|
function isObject(value) {
|
|
16
18
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
17
19
|
}
|
|
@@ -20,6 +22,21 @@ function isIsoDate(value) {
|
|
|
20
22
|
return false;
|
|
21
23
|
return !Number.isNaN(Date.parse(value));
|
|
22
24
|
}
|
|
25
|
+
function isProjectId(value) {
|
|
26
|
+
return typeof value === "string" && PROJECT_ID_PATTERN.test(value);
|
|
27
|
+
}
|
|
28
|
+
function isWorkspaceName(value) {
|
|
29
|
+
return (typeof value === "string" &&
|
|
30
|
+
value.trim().length > 0 &&
|
|
31
|
+
!/[\\/\r\n\t]/.test(value));
|
|
32
|
+
}
|
|
33
|
+
function slugifyIdentity(value) {
|
|
34
|
+
return value
|
|
35
|
+
.trim()
|
|
36
|
+
.toLowerCase()
|
|
37
|
+
.replace(/[^a-z0-9]+/g, "-")
|
|
38
|
+
.replace(/^-+|-+$/g, "");
|
|
39
|
+
}
|
|
23
40
|
function relativePathSafe(filename) {
|
|
24
41
|
if (path.isAbsolute(filename))
|
|
25
42
|
return false;
|
|
@@ -29,6 +46,48 @@ function relativePathSafe(filename) {
|
|
|
29
46
|
export function allowedKindExtensions() {
|
|
30
47
|
return Object.fromEntries(Object.entries(KIND_EXTENSIONS).map(([kind, extensions]) => [kind, [...extensions]]));
|
|
31
48
|
}
|
|
49
|
+
export function deriveManifestIdentity(repoRoot) {
|
|
50
|
+
const candidate = slugifyIdentity(path.basename(path.resolve(repoRoot)));
|
|
51
|
+
const identity = candidate || FALLBACK_WORKSPACE_ID;
|
|
52
|
+
return {
|
|
53
|
+
project_id: identity,
|
|
54
|
+
workspace_name: identity,
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
export function createManifest(repoRoot, now = new Date().toISOString()) {
|
|
58
|
+
return {
|
|
59
|
+
version: MANIFEST_VERSION,
|
|
60
|
+
created_at: now,
|
|
61
|
+
updated_at: now,
|
|
62
|
+
...deriveManifestIdentity(repoRoot),
|
|
63
|
+
artifacts: [],
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
export function normalizeManifest(raw, repoRoot) {
|
|
67
|
+
const now = new Date().toISOString();
|
|
68
|
+
const defaults = deriveManifestIdentity(repoRoot);
|
|
69
|
+
if (Array.isArray(raw)) {
|
|
70
|
+
return {
|
|
71
|
+
version: 0,
|
|
72
|
+
created_at: now,
|
|
73
|
+
updated_at: now,
|
|
74
|
+
...defaults,
|
|
75
|
+
artifacts: raw,
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
if (!isObject(raw)) {
|
|
79
|
+
return createManifest(repoRoot, now);
|
|
80
|
+
}
|
|
81
|
+
const artifacts = Array.isArray(raw.artifacts) ? raw.artifacts : [];
|
|
82
|
+
return {
|
|
83
|
+
version: typeof raw.version === "number" ? raw.version : MANIFEST_VERSION,
|
|
84
|
+
created_at: typeof raw.created_at === "string" ? raw.created_at : now,
|
|
85
|
+
updated_at: typeof raw.updated_at === "string" ? raw.updated_at : now,
|
|
86
|
+
project_id: typeof raw.project_id === "string" ? raw.project_id : defaults.project_id,
|
|
87
|
+
workspace_name: typeof raw.workspace_name === "string" ? raw.workspace_name : defaults.workspace_name,
|
|
88
|
+
artifacts,
|
|
89
|
+
};
|
|
90
|
+
}
|
|
32
91
|
export function inferArtifactType(filePath) {
|
|
33
92
|
const ext = path.extname(filePath).toLowerCase();
|
|
34
93
|
if (ext === ".excalidraw")
|
|
@@ -121,6 +180,12 @@ export async function validateManifest(manifestPath, artifactsDir) {
|
|
|
121
180
|
if (!isIsoDate(raw.updated_at)) {
|
|
122
181
|
errors.push("updated_at must be an ISO timestamp");
|
|
123
182
|
}
|
|
183
|
+
if (raw.project_id !== undefined && !isProjectId(raw.project_id)) {
|
|
184
|
+
errors.push("project_id must be a non-empty kebab-case string");
|
|
185
|
+
}
|
|
186
|
+
if (raw.workspace_name !== undefined && !isWorkspaceName(raw.workspace_name)) {
|
|
187
|
+
errors.push("workspace_name must be a non-empty string without path separators or control characters");
|
|
188
|
+
}
|
|
124
189
|
if (!Array.isArray(raw.artifacts)) {
|
|
125
190
|
errors.push("artifacts must be an array");
|
|
126
191
|
}
|
package/dist/storage.d.ts
CHANGED
|
@@ -10,6 +10,10 @@ export declare class ReferencesStore {
|
|
|
10
10
|
private readManifest;
|
|
11
11
|
private writeManifest;
|
|
12
12
|
listArtifacts(): Promise<Artifact[]>;
|
|
13
|
+
getWorkspaceIdentity(): Promise<{
|
|
14
|
+
project_id?: string;
|
|
15
|
+
workspace_name?: string;
|
|
16
|
+
}>;
|
|
13
17
|
getArtifact(artifactId: string): Promise<Artifact | null>;
|
|
14
18
|
getArtifactPath(artifact: Artifact): string;
|
|
15
19
|
private slugify;
|
package/dist/storage.js
CHANGED
|
@@ -2,14 +2,11 @@ import { randomUUID } from "node:crypto";
|
|
|
2
2
|
import { mkdirSync } from "node:fs";
|
|
3
3
|
import { promises as fs } from "node:fs";
|
|
4
4
|
import path from "node:path";
|
|
5
|
-
import { inferArtifactType,
|
|
5
|
+
import { createManifest, inferArtifactType, normalizeManifest, validateManifest } from "./manifest.js";
|
|
6
6
|
import { resolveRefsDir } from "./refs-paths.js";
|
|
7
7
|
function utcNow() {
|
|
8
8
|
return new Date().toISOString();
|
|
9
9
|
}
|
|
10
|
-
function isObject(value) {
|
|
11
|
-
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
12
|
-
}
|
|
13
10
|
export class ReferencesStore {
|
|
14
11
|
repoRoot;
|
|
15
12
|
refsDir;
|
|
@@ -30,13 +27,7 @@ export class ReferencesStore {
|
|
|
30
27
|
});
|
|
31
28
|
}
|
|
32
29
|
emptyManifest() {
|
|
33
|
-
|
|
34
|
-
return {
|
|
35
|
-
version: MANIFEST_VERSION,
|
|
36
|
-
created_at: now,
|
|
37
|
-
updated_at: now,
|
|
38
|
-
artifacts: [],
|
|
39
|
-
};
|
|
30
|
+
return createManifest(this.repoRoot, utcNow());
|
|
40
31
|
}
|
|
41
32
|
async readManifest() {
|
|
42
33
|
let raw;
|
|
@@ -47,24 +38,7 @@ export class ReferencesStore {
|
|
|
47
38
|
catch {
|
|
48
39
|
return this.emptyManifest();
|
|
49
40
|
}
|
|
50
|
-
|
|
51
|
-
return {
|
|
52
|
-
version: 0,
|
|
53
|
-
created_at: utcNow(),
|
|
54
|
-
updated_at: utcNow(),
|
|
55
|
-
artifacts: raw,
|
|
56
|
-
};
|
|
57
|
-
}
|
|
58
|
-
if (!isObject(raw)) {
|
|
59
|
-
return this.emptyManifest();
|
|
60
|
-
}
|
|
61
|
-
const artifacts = Array.isArray(raw.artifacts) ? raw.artifacts : [];
|
|
62
|
-
return {
|
|
63
|
-
version: typeof raw.version === "number" ? raw.version : MANIFEST_VERSION,
|
|
64
|
-
created_at: typeof raw.created_at === "string" ? raw.created_at : utcNow(),
|
|
65
|
-
updated_at: typeof raw.updated_at === "string" ? raw.updated_at : utcNow(),
|
|
66
|
-
artifacts,
|
|
67
|
-
};
|
|
41
|
+
return normalizeManifest(raw, this.repoRoot);
|
|
68
42
|
}
|
|
69
43
|
async writeManifest(manifest) {
|
|
70
44
|
await fs.writeFile(this.manifestPath, JSON.stringify(manifest, null, 2));
|
|
@@ -73,6 +47,13 @@ export class ReferencesStore {
|
|
|
73
47
|
const manifest = await this.readManifest();
|
|
74
48
|
return manifest.artifacts;
|
|
75
49
|
}
|
|
50
|
+
async getWorkspaceIdentity() {
|
|
51
|
+
const manifest = await this.readManifest();
|
|
52
|
+
return {
|
|
53
|
+
project_id: manifest.project_id,
|
|
54
|
+
workspace_name: manifest.workspace_name,
|
|
55
|
+
};
|
|
56
|
+
}
|
|
76
57
|
async getArtifact(artifactId) {
|
|
77
58
|
const manifest = await this.readManifest();
|
|
78
59
|
return manifest.artifacts.find((item) => item.id === artifactId) ?? null;
|
package/dist/types.d.ts
CHANGED
package/dist/workspace.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { promises as fs } from "node:fs";
|
|
2
2
|
import path from "node:path";
|
|
3
|
+
import { deriveManifestIdentity, normalizeManifest } from "./manifest.js";
|
|
3
4
|
import { DEFAULT_REFS_DIRNAME, LEGACY_REFS_DIRNAME, detectWorkspaceState, resolveCanonicalRefsDir, } from "./refs-paths.js";
|
|
4
5
|
async function pathExists(targetPath) {
|
|
5
6
|
try {
|
|
@@ -20,13 +21,28 @@ async function ensureCanonicalStructure(repoRoot) {
|
|
|
20
21
|
}
|
|
21
22
|
await fs.mkdir(artifactsDir, { recursive: true });
|
|
22
23
|
if (!(await pathExists(manifestPath))) {
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
24
|
+
await fs.writeFile(manifestPath, JSON.stringify(normalizeManifest(undefined, repoRoot), null, 2), "utf8");
|
|
25
|
+
}
|
|
26
|
+
else {
|
|
27
|
+
const rawText = await fs.readFile(manifestPath, "utf8").catch(() => null);
|
|
28
|
+
if (rawText) {
|
|
29
|
+
try {
|
|
30
|
+
const raw = JSON.parse(rawText);
|
|
31
|
+
if (raw && typeof raw === "object" && !Array.isArray(raw)) {
|
|
32
|
+
const record = raw;
|
|
33
|
+
const defaults = deriveManifestIdentity(repoRoot);
|
|
34
|
+
const missingIdentity = record.project_id === undefined || record.workspace_name === undefined;
|
|
35
|
+
if (missingIdentity) {
|
|
36
|
+
const nextManifest = normalizeManifest(raw, repoRoot);
|
|
37
|
+
nextManifest.updated_at = new Date().toISOString();
|
|
38
|
+
await fs.writeFile(manifestPath, JSON.stringify(nextManifest, null, 2), "utf8");
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
catch {
|
|
43
|
+
// Leave invalid manifests untouched so validation can report the problem explicitly.
|
|
44
|
+
}
|
|
45
|
+
}
|
|
30
46
|
}
|
|
31
47
|
return created;
|
|
32
48
|
}
|