@elmundi/ship-cli 0.8.1 → 0.11.2
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 +415 -22
- package/bin/shipctl.mjs +165 -0
- package/lib/adapters/_fs.mjs +165 -0
- package/lib/adapters/agents/index.mjs +26 -0
- package/lib/adapters/ci/azure-pipelines.mjs +23 -0
- package/lib/adapters/ci/buildkite.mjs +24 -0
- package/lib/adapters/ci/circleci.mjs +23 -0
- package/lib/adapters/ci/gh-actions.mjs +29 -0
- package/lib/adapters/ci/gitlab-ci.mjs +23 -0
- package/lib/adapters/ci/jenkins.mjs +23 -0
- package/lib/adapters/ci/manual.mjs +18 -0
- package/lib/adapters/index.mjs +122 -0
- package/lib/adapters/language/dart.mjs +23 -0
- package/lib/adapters/language/go.mjs +23 -0
- package/lib/adapters/language/java.mjs +27 -0
- package/lib/adapters/language/js.mjs +32 -0
- package/lib/adapters/language/kotlin.mjs +48 -0
- package/lib/adapters/language/py.mjs +34 -0
- package/lib/adapters/language/rust.mjs +23 -0
- package/lib/adapters/language/swift.mjs +37 -0
- package/lib/adapters/language/ts.mjs +35 -0
- package/lib/adapters/trackers/azure-boards.mjs +49 -0
- package/lib/adapters/trackers/clickup.mjs +43 -0
- package/lib/adapters/trackers/github-issues.mjs +52 -0
- package/lib/adapters/trackers/jira.mjs +72 -0
- package/lib/adapters/trackers/linear.mjs +62 -0
- package/lib/adapters/trackers/none.mjs +18 -0
- package/lib/adapters/trackers/spreadsheet.mjs +28 -0
- package/lib/artifacts/fs-index.mjs +230 -0
- package/lib/bootstrap/render.mjs +373 -0
- package/lib/cache/store.mjs +422 -0
- package/lib/commands/bootstrap.mjs +4 -0
- package/lib/commands/callback.mjs +302 -0
- package/lib/commands/config.mjs +257 -0
- package/lib/commands/docs.mjs +1 -1
- package/lib/commands/doctor.mjs +583 -0
- package/lib/commands/feedback.mjs +355 -0
- package/lib/commands/help.mjs +96 -21
- package/lib/commands/init.mjs +830 -158
- package/lib/commands/kickoff.mjs +192 -0
- package/lib/commands/knowledge.mjs +368 -0
- package/lib/commands/lanes.mjs +502 -0
- package/lib/commands/manifest-catalog.mjs +102 -38
- package/lib/commands/migrate.mjs +204 -0
- package/lib/commands/new.mjs +452 -0
- package/lib/commands/patterns.mjs +9 -43
- package/lib/commands/run.mjs +617 -0
- package/lib/commands/sync.mjs +749 -0
- package/lib/commands/telemetry.mjs +390 -0
- package/lib/commands/verify.mjs +187 -0
- package/lib/config/io.mjs +232 -0
- package/lib/config/migrate.mjs +215 -0
- package/lib/config/schema.mjs +650 -0
- package/lib/detect.mjs +162 -19
- package/lib/feedback/drafts.mjs +129 -0
- package/lib/find-ship-root.mjs +16 -10
- package/lib/http.mjs +237 -11
- package/lib/state/idempotency.mjs +183 -0
- package/lib/state/lockfile.mjs +180 -0
- package/lib/telemetry/outbox.mjs +224 -0
- package/lib/templates.mjs +53 -65
- package/lib/verify/checks/agents-on-disk.mjs +58 -0
- package/lib/verify/checks/api-reachable.mjs +39 -0
- package/lib/verify/checks/artifacts-up-to-date.mjs +78 -0
- package/lib/verify/checks/bootstrap-files.mjs +67 -0
- package/lib/verify/checks/cache-integrity.mjs +51 -0
- package/lib/verify/checks/ci-secrets.mjs +86 -0
- package/lib/verify/checks/config-present.mjs +39 -0
- package/lib/verify/checks/gitignore-cache.mjs +51 -0
- package/lib/verify/checks/rules-markers.mjs +135 -0
- package/lib/verify/checks/stack-enums.mjs +33 -0
- package/lib/verify/checks/tracker-labels.mjs +91 -0
- package/lib/verify/registry.mjs +120 -0
- package/lib/version.mjs +34 -0
- package/package.json +10 -3
- package/bin/ship.mjs +0 -68
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { isFile } from "../_fs.mjs";
|
|
2
|
+
|
|
3
|
+
export const id = "rust";
|
|
4
|
+
export const kind = "language";
|
|
5
|
+
|
|
6
|
+
export async function detect(cwd) {
|
|
7
|
+
if (isFile(cwd, "Cargo.toml")) {
|
|
8
|
+
return {
|
|
9
|
+
present: true,
|
|
10
|
+
confidence: 1,
|
|
11
|
+
evidence: [{ type: "file", where: "Cargo.toml", match: "present" }],
|
|
12
|
+
};
|
|
13
|
+
}
|
|
14
|
+
return { present: false, confidence: 0, evidence: [] };
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export async function bootstrap() {
|
|
18
|
+
return { todo: true };
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export async function verify() {
|
|
22
|
+
return { todo: true };
|
|
23
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { isFile, listDir, readText } from "../_fs.mjs";
|
|
2
|
+
|
|
3
|
+
export const id = "swift";
|
|
4
|
+
export const kind = "language";
|
|
5
|
+
|
|
6
|
+
export async function detect(cwd) {
|
|
7
|
+
const evidence = [];
|
|
8
|
+
|
|
9
|
+
if (isFile(cwd, "Package.swift")) {
|
|
10
|
+
evidence.push({ type: "file", where: "Package.swift", match: "present" });
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
for (const name of listDir(cwd, ".")) {
|
|
14
|
+
if (name.endsWith(".xcodeproj")) {
|
|
15
|
+
evidence.push({ type: "dir", where: name, match: "xcodeproj" });
|
|
16
|
+
break;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
if (isFile(cwd, "Podfile")) {
|
|
21
|
+
const body = readText(cwd, "Podfile") || "";
|
|
22
|
+
if (/\bswift\b/i.test(body) || /:swift/i.test(body)) {
|
|
23
|
+
evidence.push({ type: "file", where: "Podfile", match: "swift pods" });
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
if (!evidence.length) return { present: false, confidence: 0, evidence };
|
|
28
|
+
return { present: true, confidence: 1, evidence };
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export async function bootstrap() {
|
|
32
|
+
return { todo: true };
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export async function verify() {
|
|
36
|
+
return { todo: true };
|
|
37
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { isFile, pkgDeps, readJson } from "../_fs.mjs";
|
|
2
|
+
|
|
3
|
+
export const id = "ts";
|
|
4
|
+
export const kind = "language";
|
|
5
|
+
|
|
6
|
+
export async function detect(cwd) {
|
|
7
|
+
const evidence = [];
|
|
8
|
+
let hit = false;
|
|
9
|
+
|
|
10
|
+
if (isFile(cwd, "tsconfig.json")) {
|
|
11
|
+
hit = true;
|
|
12
|
+
evidence.push({ type: "file", where: "tsconfig.json", match: "present" });
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const pkg = readJson(cwd, "package.json");
|
|
16
|
+
const deps = pkgDeps(pkg);
|
|
17
|
+
if (deps.typescript) {
|
|
18
|
+
hit = true;
|
|
19
|
+
evidence.push({
|
|
20
|
+
type: "package",
|
|
21
|
+
where: "package.json",
|
|
22
|
+
match: `typescript@${deps.typescript}`,
|
|
23
|
+
});
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
return { present: hit, confidence: hit ? 1 : 0, evidence };
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export async function bootstrap() {
|
|
30
|
+
return { todo: true };
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export async function verify() {
|
|
34
|
+
return { todo: true };
|
|
35
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { exists, readEnvFiles, readGithubWorkflows } from "../_fs.mjs";
|
|
2
|
+
|
|
3
|
+
export const id = "azure-boards";
|
|
4
|
+
export const kind = "tracker";
|
|
5
|
+
|
|
6
|
+
const ENV_KEYS = /\b(AZURE_DEVOPS_PAT|AZURE_DEVOPS_ORG|AZURE_DEVOPS_EXT_PAT)\b/;
|
|
7
|
+
|
|
8
|
+
export async function detect(cwd) {
|
|
9
|
+
const evidence = [];
|
|
10
|
+
let envHit = false;
|
|
11
|
+
let ciHit = false;
|
|
12
|
+
|
|
13
|
+
for (const { file, content } of readEnvFiles(cwd)) {
|
|
14
|
+
const m = content.match(ENV_KEYS);
|
|
15
|
+
if (m) {
|
|
16
|
+
envHit = true;
|
|
17
|
+
evidence.push({ type: "env", where: file, match: m[1] });
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
if (exists(cwd, ".vsts-ci.yml")) {
|
|
22
|
+
ciHit = true;
|
|
23
|
+
evidence.push({ type: "file", where: ".vsts-ci.yml", match: "present" });
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
for (const { file, content } of readGithubWorkflows(cwd)) {
|
|
27
|
+
if (/azure[-_ ]?boards/i.test(content) || /dev\.azure\.com/i.test(content)) {
|
|
28
|
+
ciHit = true;
|
|
29
|
+
evidence.push({ type: "workflow", where: file, match: "azure-boards reference" });
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const present = envHit || ciHit;
|
|
34
|
+
let confidence = 0;
|
|
35
|
+
if (present) {
|
|
36
|
+
confidence = 0.7;
|
|
37
|
+
if (envHit && ciHit) confidence = 0.95;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
return { present, confidence, evidence };
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export async function bootstrap() {
|
|
44
|
+
return { todo: true };
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export async function verify() {
|
|
48
|
+
return { todo: true };
|
|
49
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { pkgDeps, readEnvFiles, readJson } from "../_fs.mjs";
|
|
2
|
+
|
|
3
|
+
export const id = "clickup";
|
|
4
|
+
export const kind = "tracker";
|
|
5
|
+
|
|
6
|
+
export async function detect(cwd) {
|
|
7
|
+
const evidence = [];
|
|
8
|
+
let envHit = false;
|
|
9
|
+
let packageHit = false;
|
|
10
|
+
|
|
11
|
+
for (const { file, content } of readEnvFiles(cwd)) {
|
|
12
|
+
if (/\bCLICKUP_API_TOKEN\b/.test(content)) {
|
|
13
|
+
envHit = true;
|
|
14
|
+
evidence.push({ type: "env", where: file, match: "CLICKUP_API_TOKEN" });
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const pkg = readJson(cwd, "package.json");
|
|
19
|
+
const deps = pkgDeps(pkg);
|
|
20
|
+
for (const name of Object.keys(deps)) {
|
|
21
|
+
if (name.startsWith("@clickup/")) {
|
|
22
|
+
packageHit = true;
|
|
23
|
+
evidence.push({ type: "package", where: "package.json", match: name });
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const present = envHit || packageHit;
|
|
28
|
+
let confidence = 0;
|
|
29
|
+
if (present) {
|
|
30
|
+
confidence = 0.7;
|
|
31
|
+
if (envHit && packageHit) confidence = 0.95;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
return { present, confidence, evidence };
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export async function bootstrap() {
|
|
38
|
+
return { todo: true };
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export async function verify() {
|
|
42
|
+
return { todo: true };
|
|
43
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { isDir, listDir, readEnvFiles, readGithubWorkflows } from "../_fs.mjs";
|
|
2
|
+
|
|
3
|
+
export const id = "github-issues";
|
|
4
|
+
export const kind = "tracker";
|
|
5
|
+
|
|
6
|
+
export async function detect(cwd) {
|
|
7
|
+
const evidence = [];
|
|
8
|
+
let templateHit = false;
|
|
9
|
+
let tokenHit = false;
|
|
10
|
+
|
|
11
|
+
if (isDir(cwd, ".github", "ISSUE_TEMPLATE")) {
|
|
12
|
+
const entries = listDir(cwd, ".github/ISSUE_TEMPLATE").filter((n) => !n.startsWith("."));
|
|
13
|
+
templateHit = true;
|
|
14
|
+
evidence.push({
|
|
15
|
+
type: "dir",
|
|
16
|
+
where: ".github/ISSUE_TEMPLATE/",
|
|
17
|
+
match: `${entries.length} template(s)`,
|
|
18
|
+
});
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
for (const { file, content } of readEnvFiles(cwd)) {
|
|
22
|
+
if (/\bGITHUB_TOKEN\b/.test(content)) {
|
|
23
|
+
tokenHit = true;
|
|
24
|
+
evidence.push({ type: "env", where: file, match: "GITHUB_TOKEN" });
|
|
25
|
+
break;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
if (!tokenHit) {
|
|
29
|
+
for (const { file, content } of readGithubWorkflows(cwd)) {
|
|
30
|
+
if (/\bGITHUB_TOKEN\b/.test(content)) {
|
|
31
|
+
tokenHit = true;
|
|
32
|
+
evidence.push({ type: "workflow", where: file, match: "GITHUB_TOKEN" });
|
|
33
|
+
break;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const present = templateHit || tokenHit;
|
|
39
|
+
let confidence = 0;
|
|
40
|
+
if (templateHit) confidence = 0.8;
|
|
41
|
+
else if (tokenHit) confidence = 0.3;
|
|
42
|
+
|
|
43
|
+
return { present, confidence, evidence };
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export async function bootstrap() {
|
|
47
|
+
return { todo: true };
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export async function verify() {
|
|
51
|
+
return { todo: true };
|
|
52
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import {
|
|
2
|
+
exists,
|
|
3
|
+
readEnvFiles,
|
|
4
|
+
readGithubWorkflows,
|
|
5
|
+
readJson,
|
|
6
|
+
} from "../_fs.mjs";
|
|
7
|
+
|
|
8
|
+
export const id = "jira";
|
|
9
|
+
export const kind = "tracker";
|
|
10
|
+
|
|
11
|
+
const ENV_KEYS = /\b(JIRA_URL|JIRA_API_TOKEN|ATLASSIAN_TOKEN|ATLASSIAN_API_TOKEN)\b/;
|
|
12
|
+
|
|
13
|
+
export async function detect(cwd) {
|
|
14
|
+
const evidence = [];
|
|
15
|
+
let envHit = false;
|
|
16
|
+
let workflowHit = false;
|
|
17
|
+
let fileHit = false;
|
|
18
|
+
|
|
19
|
+
for (const { file, content } of readEnvFiles(cwd)) {
|
|
20
|
+
const m = content.match(ENV_KEYS);
|
|
21
|
+
if (m) {
|
|
22
|
+
envHit = true;
|
|
23
|
+
evidence.push({ type: "env", where: file, match: m[1] });
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
for (const { file, content } of readGithubWorkflows(cwd)) {
|
|
28
|
+
if (/\bjira\b/i.test(content) || /atlassian/i.test(content)) {
|
|
29
|
+
workflowHit = true;
|
|
30
|
+
evidence.push({ type: "workflow", where: file, match: "jira/atlassian reference" });
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
if (exists(cwd, ".jira")) {
|
|
35
|
+
fileHit = true;
|
|
36
|
+
evidence.push({ type: "dir", where: ".jira/", match: "present" });
|
|
37
|
+
}
|
|
38
|
+
if (exists(cwd, "atlassian-connect.json")) {
|
|
39
|
+
fileHit = true;
|
|
40
|
+
evidence.push({ type: "file", where: "atlassian-connect.json", match: "present" });
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const pkg = readJson(cwd, "package.json");
|
|
44
|
+
const scripts = pkg && typeof pkg === "object" ? pkg.scripts : null;
|
|
45
|
+
if (scripts && typeof scripts === "object") {
|
|
46
|
+
for (const [k, v] of Object.entries(scripts)) {
|
|
47
|
+
if (typeof v === "string" && /\bjira\b/i.test(v)) {
|
|
48
|
+
workflowHit = true;
|
|
49
|
+
evidence.push({ type: "script", where: `package.json:scripts.${k}`, match: "jira reference" });
|
|
50
|
+
break;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const present = envHit || workflowHit || fileHit;
|
|
56
|
+
let confidence = 0;
|
|
57
|
+
if (present) {
|
|
58
|
+
confidence = 0.7;
|
|
59
|
+
if (envHit && (workflowHit || fileHit)) confidence = 0.95;
|
|
60
|
+
if (fileHit && workflowHit) confidence = Math.max(confidence, 0.85);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
return { present, confidence, evidence };
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export async function bootstrap() {
|
|
67
|
+
return { todo: true };
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export async function verify() {
|
|
71
|
+
return { todo: true };
|
|
72
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import {
|
|
2
|
+
exists,
|
|
3
|
+
pkgDeps,
|
|
4
|
+
readEnvFiles,
|
|
5
|
+
readGithubWorkflows,
|
|
6
|
+
readJson,
|
|
7
|
+
} from "../_fs.mjs";
|
|
8
|
+
|
|
9
|
+
export const id = "linear";
|
|
10
|
+
export const kind = "tracker";
|
|
11
|
+
|
|
12
|
+
export async function detect(cwd) {
|
|
13
|
+
const evidence = [];
|
|
14
|
+
let envHit = false;
|
|
15
|
+
let workflowHit = false;
|
|
16
|
+
let packageHit = false;
|
|
17
|
+
|
|
18
|
+
for (const { file, content } of readEnvFiles(cwd)) {
|
|
19
|
+
if (/\bLINEAR_API_KEY\b/.test(content)) {
|
|
20
|
+
envHit = true;
|
|
21
|
+
evidence.push({ type: "env", where: file, match: "LINEAR_API_KEY" });
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const pkg = readJson(cwd, "package.json");
|
|
26
|
+
const deps = pkgDeps(pkg);
|
|
27
|
+
for (const name of Object.keys(deps)) {
|
|
28
|
+
if (name.startsWith("@linear/")) {
|
|
29
|
+
packageHit = true;
|
|
30
|
+
evidence.push({ type: "package", where: "package.json", match: name });
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
for (const { file, content } of readGithubWorkflows(cwd)) {
|
|
35
|
+
if (/\blinear\b/i.test(content)) {
|
|
36
|
+
workflowHit = true;
|
|
37
|
+
evidence.push({ type: "workflow", where: file, match: "linear reference" });
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
if (exists(cwd, ".linear")) {
|
|
42
|
+
packageHit = true;
|
|
43
|
+
evidence.push({ type: "dir", where: ".linear/", match: "present" });
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const present = envHit || workflowHit || packageHit;
|
|
47
|
+
let confidence = 0;
|
|
48
|
+
if (present) {
|
|
49
|
+
confidence = 0.7;
|
|
50
|
+
if (envHit && (workflowHit || packageHit)) confidence = 0.95;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
return { present, confidence, evidence };
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export async function bootstrap() {
|
|
57
|
+
return { todo: true };
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export async function verify() {
|
|
61
|
+
return { todo: true };
|
|
62
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
export const id = "none";
|
|
2
|
+
export const kind = "tracker";
|
|
3
|
+
|
|
4
|
+
export async function detect() {
|
|
5
|
+
return {
|
|
6
|
+
present: true,
|
|
7
|
+
confidence: 0.05,
|
|
8
|
+
evidence: [{ type: "fallback", where: "-", match: "no tracker detected" }],
|
|
9
|
+
};
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export async function bootstrap() {
|
|
13
|
+
return { todo: true };
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export async function verify() {
|
|
17
|
+
return { todo: true };
|
|
18
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { exists } from "../_fs.mjs";
|
|
2
|
+
|
|
3
|
+
export const id = "spreadsheet";
|
|
4
|
+
export const kind = "tracker";
|
|
5
|
+
|
|
6
|
+
export async function detect(cwd) {
|
|
7
|
+
const evidence = [];
|
|
8
|
+
let hit = false;
|
|
9
|
+
|
|
10
|
+
if (exists(cwd, ".ship", "tracker-sheet.csv")) {
|
|
11
|
+
hit = true;
|
|
12
|
+
evidence.push({ type: "file", where: ".ship/tracker-sheet.csv", match: "present" });
|
|
13
|
+
}
|
|
14
|
+
if (exists(cwd, "tracker.xlsx")) {
|
|
15
|
+
hit = true;
|
|
16
|
+
evidence.push({ type: "file", where: "tracker.xlsx", match: "present" });
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
return { present: hit, confidence: hit ? 0.9 : 0, evidence };
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export async function bootstrap() {
|
|
23
|
+
return { todo: true };
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export async function verify() {
|
|
27
|
+
return { todo: true };
|
|
28
|
+
}
|
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Shared filesystem index for v2 artifact trees: walks
|
|
6
|
+
* `<repoRoot>/artifacts/<plural>/<id>/ARTIFACT.md` and parses just enough YAML
|
|
7
|
+
* front-matter to reconstruct the same entry shape we used to read out of the
|
|
8
|
+
* legacy `<plural>/manifest.json` files.
|
|
9
|
+
*
|
|
10
|
+
* Zero dependencies — only node builtins. Anything we cannot parse falls
|
|
11
|
+
* through (the entry still gets emitted with whatever fields we recovered).
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
const KIND_TO_PLURAL = {
|
|
15
|
+
pattern: "patterns",
|
|
16
|
+
tool: "tools",
|
|
17
|
+
collection: "collections",
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* @param {"pattern"|"tool"|"collection"} kind
|
|
22
|
+
*/
|
|
23
|
+
export function pluralFor(kind) {
|
|
24
|
+
return KIND_TO_PLURAL[kind] || `${kind}s`;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Walk `artifacts/<plural>/*` and return the parsed entries (same shape as the
|
|
29
|
+
* legacy manifest).
|
|
30
|
+
*
|
|
31
|
+
* @param {string} repoRoot
|
|
32
|
+
* @param {"pattern"|"tool"|"collection"} kind
|
|
33
|
+
* @returns {Array<Record<string, any>>}
|
|
34
|
+
*/
|
|
35
|
+
export function scanArtifacts(repoRoot, kind) {
|
|
36
|
+
const plural = pluralFor(kind);
|
|
37
|
+
const dir = path.join(repoRoot, "artifacts", plural);
|
|
38
|
+
if (!fs.existsSync(dir) || !fs.statSync(dir).isDirectory()) return [];
|
|
39
|
+
|
|
40
|
+
const ids = fs.readdirSync(dir, { withFileTypes: true })
|
|
41
|
+
.filter((e) => e.isDirectory())
|
|
42
|
+
.map((e) => e.name)
|
|
43
|
+
.sort();
|
|
44
|
+
|
|
45
|
+
/** @type {Array<Record<string, any>>} */
|
|
46
|
+
const out = [];
|
|
47
|
+
for (const id of ids) {
|
|
48
|
+
const file = path.join(dir, id, "ARTIFACT.md");
|
|
49
|
+
if (!fs.existsSync(file)) continue;
|
|
50
|
+
let raw;
|
|
51
|
+
try {
|
|
52
|
+
raw = fs.readFileSync(file, "utf8");
|
|
53
|
+
} catch {
|
|
54
|
+
continue;
|
|
55
|
+
}
|
|
56
|
+
const { fm } = parseFrontMatter(raw);
|
|
57
|
+
const entry = entryFromFrontmatter(fm, kind, id);
|
|
58
|
+
out.push(entry);
|
|
59
|
+
}
|
|
60
|
+
return out;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Read the full ARTIFACT.md (frontmatter + body) for a specific id. Returns
|
|
65
|
+
* null when the file is absent so callers can emit the same "Unknown id"
|
|
66
|
+
* messages they did before.
|
|
67
|
+
*
|
|
68
|
+
* @param {string} repoRoot
|
|
69
|
+
* @param {"pattern"|"tool"|"collection"} kind
|
|
70
|
+
* @param {string} id
|
|
71
|
+
*/
|
|
72
|
+
export function readArtifactFile(repoRoot, kind, id) {
|
|
73
|
+
const plural = pluralFor(kind);
|
|
74
|
+
const file = path.join(repoRoot, "artifacts", plural, id, "ARTIFACT.md");
|
|
75
|
+
if (!fs.existsSync(file) || !fs.statSync(file).isFile()) return null;
|
|
76
|
+
return { absPath: file, content: fs.readFileSync(file, "utf8") };
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function entryFromFrontmatter(fm, kind, id) {
|
|
80
|
+
const plural = pluralFor(kind);
|
|
81
|
+
const description = typeof fm.description === "string" ? fm.description : "";
|
|
82
|
+
const summary = description ? firstSentence(description) : "";
|
|
83
|
+
return {
|
|
84
|
+
id: typeof fm.id === "string" && fm.id ? fm.id : id,
|
|
85
|
+
title: typeof fm.name === "string" ? fm.name : id,
|
|
86
|
+
summary,
|
|
87
|
+
path: `artifacts/${plural}/${id}/ARTIFACT.md`,
|
|
88
|
+
tags: Array.isArray(fm.tags) ? fm.tags : [],
|
|
89
|
+
group: typeof fm.group === "string" ? fm.group : null,
|
|
90
|
+
version: typeof fm.version === "string" ? fm.version : null,
|
|
91
|
+
content_sha256: typeof fm.content_sha256 === "string" ? fm.content_sha256 : null,
|
|
92
|
+
updated_at: typeof fm.updated_at === "string" ? fm.updated_at : null,
|
|
93
|
+
channel: typeof fm.channel === "string" ? fm.channel : null,
|
|
94
|
+
min_shipctl: typeof fm.min_shipctl === "string" ? fm.min_shipctl : null,
|
|
95
|
+
deprecated: fm.deprecated === true || fm.deprecated === "true",
|
|
96
|
+
replaced_by: fm.replaced_by ?? null,
|
|
97
|
+
yanked: fm.yanked === true || fm.yanked === "true",
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function firstSentence(text) {
|
|
102
|
+
const trimmed = text.trim();
|
|
103
|
+
if (!trimmed) return "";
|
|
104
|
+
const m = /[.!?](\s|$)/.exec(trimmed);
|
|
105
|
+
if (!m) return trimmed;
|
|
106
|
+
return trimmed.slice(0, m.index + 1).trim();
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Tiny YAML front-matter parser tailored for v2 ARTIFACT.md files.
|
|
111
|
+
*
|
|
112
|
+
* Supports:
|
|
113
|
+
* - simple `key: value`
|
|
114
|
+
* - inline lists `key: [a, b]`
|
|
115
|
+
* - folded scalars `key: >` / `key: >-` with indented continuation lines
|
|
116
|
+
* - quoted strings (single or double)
|
|
117
|
+
* - one level of nested mapping (used by `spec:`)
|
|
118
|
+
* - comments (`# …`)
|
|
119
|
+
*
|
|
120
|
+
* Anything else is best-effort: the value is captured as the trimmed string.
|
|
121
|
+
*
|
|
122
|
+
* @param {string} source
|
|
123
|
+
* @returns {{fm: Record<string, any>, body: string}}
|
|
124
|
+
*/
|
|
125
|
+
export function parseFrontMatter(source) {
|
|
126
|
+
if (typeof source !== "string") return { fm: {}, body: "" };
|
|
127
|
+
const match = /^---\r?\n([\s\S]*?)\r?\n---\r?\n?/.exec(source);
|
|
128
|
+
if (!match) return { fm: {}, body: source };
|
|
129
|
+
const block = match[1];
|
|
130
|
+
const body = source.slice(match[0].length);
|
|
131
|
+
/** @type {Record<string, any>} */
|
|
132
|
+
const fm = {};
|
|
133
|
+
const lines = block.split(/\r?\n/);
|
|
134
|
+
let i = 0;
|
|
135
|
+
while (i < lines.length) {
|
|
136
|
+
const rawLine = lines[i];
|
|
137
|
+
const line = rawLine.replace(/\s+$/, "");
|
|
138
|
+
if (!line || /^\s*#/.test(line)) {
|
|
139
|
+
i += 1;
|
|
140
|
+
continue;
|
|
141
|
+
}
|
|
142
|
+
const top = /^([A-Za-z_][A-Za-z0-9_.-]*)\s*:\s*(.*)$/.exec(line);
|
|
143
|
+
if (!top) {
|
|
144
|
+
i += 1;
|
|
145
|
+
continue;
|
|
146
|
+
}
|
|
147
|
+
const key = top[1];
|
|
148
|
+
const value = top[2];
|
|
149
|
+
|
|
150
|
+
if (value === ">" || value === ">-") {
|
|
151
|
+
const folded = [];
|
|
152
|
+
i += 1;
|
|
153
|
+
while (i < lines.length) {
|
|
154
|
+
const cont = lines[i];
|
|
155
|
+
if (cont === "" || cont === "\r") {
|
|
156
|
+
// Preserve paragraph breaks as a single space in folded scalars.
|
|
157
|
+
folded.push("");
|
|
158
|
+
i += 1;
|
|
159
|
+
continue;
|
|
160
|
+
}
|
|
161
|
+
const m = /^(\s+)(.*)$/.exec(cont);
|
|
162
|
+
if (!m) break;
|
|
163
|
+
folded.push(m[2]);
|
|
164
|
+
i += 1;
|
|
165
|
+
}
|
|
166
|
+
let joined = folded.join(" ").replace(/\s+/g, " ").trim();
|
|
167
|
+
if (value === ">-") joined = joined.replace(/\s+$/, "");
|
|
168
|
+
fm[key] = joined;
|
|
169
|
+
continue;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
if (/^\[.*\]$/.test(value.trim())) {
|
|
173
|
+
const inner = value.trim().slice(1, -1).trim();
|
|
174
|
+
fm[key] = inner.length ? inner.split(/\s*,\s*/).map(unquote) : [];
|
|
175
|
+
i += 1;
|
|
176
|
+
continue;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
if (value === "") {
|
|
180
|
+
// Possible nested mapping or empty scalar. Peek ahead.
|
|
181
|
+
const child = {};
|
|
182
|
+
let saw = false;
|
|
183
|
+
let j = i + 1;
|
|
184
|
+
while (j < lines.length) {
|
|
185
|
+
const cont = lines[j];
|
|
186
|
+
if (!cont.trim()) { j += 1; continue; }
|
|
187
|
+
const indented = /^(\s{2,})([A-Za-z_][A-Za-z0-9_.-]*)\s*:\s*(.*)$/.exec(cont);
|
|
188
|
+
if (!indented) break;
|
|
189
|
+
const [, , subKey, subVal] = indented;
|
|
190
|
+
if (/^\[.*\]$/.test(subVal.trim())) {
|
|
191
|
+
const inner = subVal.trim().slice(1, -1).trim();
|
|
192
|
+
child[subKey] = inner.length ? inner.split(/\s*,\s*/).map(unquote) : [];
|
|
193
|
+
} else {
|
|
194
|
+
child[subKey] = coerceScalar(subVal);
|
|
195
|
+
}
|
|
196
|
+
saw = true;
|
|
197
|
+
j += 1;
|
|
198
|
+
}
|
|
199
|
+
if (saw) {
|
|
200
|
+
fm[key] = child;
|
|
201
|
+
i = j;
|
|
202
|
+
continue;
|
|
203
|
+
}
|
|
204
|
+
fm[key] = "";
|
|
205
|
+
i += 1;
|
|
206
|
+
continue;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
fm[key] = coerceScalar(value);
|
|
210
|
+
i += 1;
|
|
211
|
+
}
|
|
212
|
+
return { fm, body };
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
function unquote(value) {
|
|
216
|
+
if (typeof value !== "string") return value;
|
|
217
|
+
const v = value.trim();
|
|
218
|
+
if ((v.startsWith('"') && v.endsWith('"')) || (v.startsWith("'") && v.endsWith("'"))) {
|
|
219
|
+
return v.slice(1, -1);
|
|
220
|
+
}
|
|
221
|
+
return v;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
function coerceScalar(rawValue) {
|
|
225
|
+
const v = unquote(String(rawValue).trim());
|
|
226
|
+
if (v === "true") return true;
|
|
227
|
+
if (v === "false") return false;
|
|
228
|
+
if (v === "null" || v === "~") return null;
|
|
229
|
+
return v;
|
|
230
|
+
}
|