@captain_z/zsk 1.8.3 → 1.8.5
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/bin.js +13 -0
- package/dist/bin.js.map +1 -1
- package/dist/commands/add-flow.d.ts +3 -7
- package/dist/commands/add-flow.js +7 -59
- package/dist/commands/add-flow.js.map +1 -1
- package/dist/commands/add.js +25 -104
- package/dist/commands/add.js.map +1 -1
- package/dist/commands/check.js +14 -575
- package/dist/commands/check.js.map +1 -1
- package/dist/commands/config.js +1 -1
- package/dist/commands/config.js.map +1 -1
- package/dist/commands/demo.d.ts +5 -0
- package/dist/commands/demo.js +70 -297
- package/dist/commands/demo.js.map +1 -1
- package/dist/commands/doctor.js +9 -4
- package/dist/commands/doctor.js.map +1 -1
- package/dist/commands/gate.d.ts +1 -0
- package/dist/commands/gate.js +8 -2
- package/dist/commands/gate.js.map +1 -1
- package/dist/commands/prepare.js +7 -1
- package/dist/commands/prepare.js.map +1 -1
- package/dist/commands/project-init.js +30 -8
- package/dist/commands/project-init.js.map +1 -1
- package/dist/core/config.d.ts +68 -0
- package/dist/core/config.js +198 -15
- package/dist/core/config.js.map +1 -1
- package/dist/core/demo-auth.d.ts +30 -0
- package/dist/core/demo-auth.js +213 -0
- package/dist/core/demo-auth.js.map +1 -0
- package/dist/core/demo-scenarios.d.ts +62 -0
- package/dist/core/demo-scenarios.js +276 -0
- package/dist/core/demo-scenarios.js.map +1 -0
- package/dist/core/demo-sources.d.ts +37 -0
- package/dist/core/demo-sources.js +198 -0
- package/dist/core/demo-sources.js.map +1 -0
- package/dist/core/mcp-registry-discovery.d.ts +16 -0
- package/dist/core/mcp-registry-discovery.js +187 -0
- package/dist/core/mcp-registry-discovery.js.map +1 -0
- package/dist/core/origin-detection.js +1 -1
- package/dist/core/origin-detection.js.map +1 -1
- package/dist/core/prepare-artifacts.d.ts +16 -0
- package/dist/core/prepare-artifacts.js +25 -0
- package/dist/core/prepare-artifacts.js.map +1 -0
- package/dist/core/prepare-auth-helper.d.ts +8 -0
- package/dist/core/prepare-auth-helper.js +32 -0
- package/dist/core/prepare-auth-helper.js.map +1 -0
- package/dist/core/prepare-materialization.d.ts +8 -0
- package/dist/core/prepare-materialization.js +26 -0
- package/dist/core/prepare-materialization.js.map +1 -0
- package/dist/core/prepare-migration.d.ts +6 -0
- package/dist/core/prepare-migration.js +57 -0
- package/dist/core/prepare-migration.js.map +1 -0
- package/dist/core/prepare-reporting.d.ts +5 -0
- package/dist/core/prepare-reporting.js +106 -0
- package/dist/core/prepare-reporting.js.map +1 -0
- package/dist/core/prepare-routing.d.ts +12 -0
- package/dist/core/prepare-routing.js +182 -0
- package/dist/core/prepare-routing.js.map +1 -0
- package/dist/core/prepare-sync.d.ts +13 -53
- package/dist/core/prepare-sync.js +878 -359
- package/dist/core/prepare-sync.js.map +1 -1
- package/dist/core/prepare-utils.d.ts +6 -0
- package/dist/core/prepare-utils.js +35 -0
- package/dist/core/prepare-utils.js.map +1 -0
- package/dist/core/profile-bundle-installation.d.ts +55 -0
- package/dist/core/profile-bundle-installation.js +170 -0
- package/dist/core/profile-bundle-installation.js.map +1 -0
- package/dist/core/provider-policy.d.ts +26 -0
- package/dist/core/provider-policy.js +180 -0
- package/dist/core/provider-policy.js.map +1 -0
- package/dist/core/provider-readiness.d.ts +39 -0
- package/dist/core/provider-readiness.js +78 -0
- package/dist/core/provider-readiness.js.map +1 -0
- package/dist/core/source-adapter-normalization.d.ts +31 -0
- package/dist/core/source-adapter-normalization.js +235 -0
- package/dist/core/source-adapter-normalization.js.map +1 -0
- package/dist/core/source-snapshot-adapters.d.ts +59 -0
- package/dist/core/source-snapshot-adapters.js +60 -0
- package/dist/core/source-snapshot-adapters.js.map +1 -0
- package/dist/core/staffing-plan.d.ts +1 -0
- package/dist/core/staffing-plan.js +113 -21
- package/dist/core/staffing-plan.js.map +1 -1
- package/dist/core/stage-clarity-verification.d.ts +31 -0
- package/dist/core/stage-clarity-verification.js +316 -0
- package/dist/core/stage-clarity-verification.js.map +1 -0
- package/dist/core/stage-output-quality.d.ts +3 -0
- package/dist/core/stage-output-quality.js +122 -0
- package/dist/core/stage-output-quality.js.map +1 -0
- package/dist/core/stage-quality-artifacts.d.ts +15 -0
- package/dist/core/stage-quality-artifacts.js +421 -0
- package/dist/core/stage-quality-artifacts.js.map +1 -0
- package/dist/core/stage-quality-contracts.d.ts +105 -0
- package/dist/core/stage-quality-contracts.js +2 -0
- package/dist/core/stage-quality-contracts.js.map +1 -0
- package/dist/core/stage-quality-criteria.d.ts +9 -0
- package/dist/core/stage-quality-criteria.js +286 -0
- package/dist/core/stage-quality-criteria.js.map +1 -0
- package/dist/core/stage-quality-rendering.d.ts +15 -0
- package/dist/core/stage-quality-rendering.js +240 -0
- package/dist/core/stage-quality-rendering.js.map +1 -0
- package/dist/core/stage-quality.d.ts +4 -59
- package/dist/core/stage-quality.js +54 -795
- package/dist/core/stage-quality.js.map +1 -1
- package/dist/core/template-registry.js +12 -15
- package/dist/core/template-registry.js.map +1 -1
- package/dist/core/workspace-conformance.d.ts +39 -0
- package/dist/core/workspace-conformance.js +603 -0
- package/dist/core/workspace-conformance.js.map +1 -0
- package/package.json +2 -2
- package/schemas/providers.schema.json +74 -0
- package/schemas/zsk-config.schema.json +417 -1
- package/templates/module/frontend-module/design.md +10 -0
- package/templates/module/frontend-module/proposal.md +8 -0
- package/templates/module/frontend-module/spec.md +7 -0
- package/templates/project-init/.zsk/README.md +48 -0
- package/templates/project-init/.zsk/config.yaml +37 -0
- package/templates/project-init/.zsk/docs/CONFIG-SCHEMA.md +127 -0
- package/templates/project-init/.zsk/docs/PROJECT-CONFIG.md +53 -5
- package/templates/project-init/.zsk/docs/README.md +10 -0
- package/templates/project-init/.zsk/docs/SECURITY.md +34 -0
- package/templates/project-init/.zsk/docs/SYSTEM-SPEC.md +39 -7
- package/templates/project-init/.zsk/evidence/README.md +15 -0
- package/templates/project-init/.zsk/issues/README.md +10 -0
- package/templates/project-init/.zsk/modules/README.md +19 -0
- package/templates/project-init/.zsk/modules/index.md +9 -5
- package/templates/project-init/.zsk/raws/README.md +34 -0
- package/templates/project-init/.zsk/roles.yaml +36 -105
- package/templates/project-init/.zsk/templates/config.examples.yaml +83 -0
- package/templates/project-init/.zsk/templates/issue-card.md +23 -0
- package/templates/project-init/.zsk/templates/module/README.md +13 -0
- package/templates/project-init/.zsk/templates/module/design.md +22 -0
- package/templates/project-init/.zsk/templates/module/module.yaml +15 -0
- package/templates/project-init/.zsk/templates/module/proposal.md +20 -0
- package/templates/project-init/.zsk/templates/module/spec.md +22 -0
- package/templates/project-init/.zsk/templates/module/tasks.md +16 -0
- package/templates/project-init/.zsk/raws/index.md +0 -18
- package/templates/project-init/.zsk/raws/manifest.json +0 -4
- package/templates/project-init/.zsk/raws/prepare/backend/index.md +0 -4
- package/templates/project-init/.zsk/raws/prepare/design/index.md +0 -3
- package/templates/project-init/.zsk/raws/prepare/index.md +0 -4
- package/templates/project-init/.zsk/raws/prepare/product/index.md +0 -4
- package/templates/project-init/.zsk/raws/prepare/qa/index.md +0 -4
- package/templates/project-init/.zsk/raws/prepare/ux/index.md +0 -3
|
@@ -1,39 +1,47 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { Buffer } from "node:buffer";
|
|
2
|
+
import { execFile } from "node:child_process";
|
|
2
3
|
import { createRequire } from "node:module";
|
|
3
|
-
import { copyFile, mkdir, readFile, stat, writeFile } from "node:fs/promises";
|
|
4
|
-
import { dirname, extname,
|
|
5
|
-
import {
|
|
4
|
+
import { copyFile, mkdir, readdir, readFile, stat, writeFile } from "node:fs/promises";
|
|
5
|
+
import { dirname, extname, join, relative, resolve } from "node:path";
|
|
6
|
+
import { promisify } from "node:util";
|
|
7
|
+
import { DESIGN_LOCAL_FILE_ORIGIN_KEYS, JIRA_LOCAL_FILE_ORIGIN_KEYS, flattenPrepareEntries, resolveSourceSnapshot, } from "./config.js";
|
|
6
8
|
import { inferSourceOrigin } from "./origin-detection.js";
|
|
9
|
+
import { discoverMcpRegistries } from "./mcp-registry-discovery.js";
|
|
10
|
+
import { loadProviderPolicy } from "./provider-policy.js";
|
|
11
|
+
import { buildProviderReadinessEvidence } from "./provider-readiness.js";
|
|
7
12
|
import { describeResource, updateRawIndexes, updateRawManifest } from "./raw-manifest.js";
|
|
13
|
+
import { createSyncRunId, resolvePrepareSyncArtifacts, } from "./prepare-artifacts.js";
|
|
14
|
+
import { hashIfExists, sha256, writePreparedSnapshot } from "./prepare-materialization.js";
|
|
15
|
+
import { renderDownstreamImpact, renderPreparationReport } from "./prepare-reporting.js";
|
|
16
|
+
import { routePreparedResources } from "./prepare-routing.js";
|
|
17
|
+
import { escapeTable, exists, isRecord, safeFileName } from "./prepare-utils.js";
|
|
18
|
+
import { interpretSourceAdapter } from "./source-adapter-normalization.js";
|
|
19
|
+
import { acquireSourceSnapshot, chooseSourceSnapshotStrategy, createSourceSnapshotContext, } from "./source-snapshot-adapters.js";
|
|
20
|
+
import { isPathInside, resolvePlaywrightAuthProfilePath, resolvePlaywrightAuthStatePath, } from "./workspace-conformance.js";
|
|
8
21
|
import { getWorkspacePath } from "./workspace-layout.js";
|
|
9
|
-
export
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
adapterResultsDir: join(dir, "adapter-results"),
|
|
16
|
-
authCheckPath: join(dir, "auth-check.json"),
|
|
17
|
-
downstreamImpactPath: join(dir, "downstream-impact.md"),
|
|
18
|
-
authScriptPath: join(authDir, "login.mjs"),
|
|
19
|
-
authStatePath: join(authDir, "user_data.json"),
|
|
20
|
-
migrationPlanPath: join(dir, "raws-migration-plan.md"),
|
|
21
|
-
};
|
|
22
|
-
}
|
|
22
|
+
export { createSyncRunId, resolvePrepareSyncArtifacts } from "./prepare-artifacts.js";
|
|
23
|
+
export { writeAuthLoginHelper } from "./prepare-auth-helper.js";
|
|
24
|
+
export { buildRawMigrationPlan } from "./prepare-migration.js";
|
|
25
|
+
const execFileAsync = promisify(execFile);
|
|
26
|
+
const DEFAULT_ADAPTER_COMMAND_TIMEOUT_MS = 60_000;
|
|
27
|
+
const DEFAULT_ADAPTER_COMMAND_MAX_BUFFER = 10 * 1024 * 1024;
|
|
23
28
|
export async function syncPrepareSources(target, config, opts = {}) {
|
|
24
29
|
const runId = opts.runId ?? createSyncRunId();
|
|
25
30
|
const artifacts = resolvePrepareSyncArtifacts(target, config, runId);
|
|
26
|
-
const entries =
|
|
31
|
+
const entries = flattenPrepareEntries(config);
|
|
27
32
|
const selected = new Set(selectSourceEntries(entries, opts));
|
|
28
33
|
const results = [];
|
|
29
34
|
await mkdir(artifacts.adapterResultsDir, { recursive: true });
|
|
35
|
+
await mkdir(artifacts.providerReadinessDir, { recursive: true });
|
|
36
|
+
const providerReadiness = await resolveProviderReadiness(target, artifacts, [...selected]);
|
|
30
37
|
for (const entry of entries) {
|
|
31
38
|
if (!selected.has(entry))
|
|
32
39
|
continue;
|
|
33
|
-
const result = await syncEntry(target, config, entry, opts);
|
|
40
|
+
const result = annotateProviderReadiness(target, entry, await syncEntry(target, config, entry, opts, entries), providerReadiness.byProvider);
|
|
34
41
|
results.push(result);
|
|
35
42
|
await writeFile(join(artifacts.adapterResultsDir, `${safeFileName(entry.id)}.json`), `${JSON.stringify(result, null, 2)}\n`, "utf8");
|
|
36
43
|
}
|
|
44
|
+
const routes = opts.dryRun ? [] : await routePreparedResources(target, config, results);
|
|
37
45
|
if (!opts.dryRun) {
|
|
38
46
|
const records = [];
|
|
39
47
|
for (const entry of entries) {
|
|
@@ -45,43 +53,22 @@ export async function syncPrepareSources(target, config, opts = {}) {
|
|
|
45
53
|
const downstreamImpact = renderDownstreamImpact(results);
|
|
46
54
|
await mkdir(dirname(artifacts.downstreamImpactPath), { recursive: true });
|
|
47
55
|
await writeFile(artifacts.downstreamImpactPath, downstreamImpact, "utf8");
|
|
56
|
+
const syncReport = renderPreparationReport(entries, results, routes);
|
|
57
|
+
await mkdir(dirname(artifacts.syncReportPath), { recursive: true });
|
|
58
|
+
await writeFile(artifacts.syncReportPath, syncReport, "utf8");
|
|
48
59
|
return {
|
|
49
60
|
artifacts,
|
|
50
61
|
results,
|
|
62
|
+
routes,
|
|
63
|
+
providerReadiness: [...providerReadiness.byProvider.values()].map((item) => item.evidence),
|
|
51
64
|
downstreamImpact,
|
|
65
|
+
syncReport,
|
|
52
66
|
};
|
|
53
67
|
}
|
|
54
|
-
export async function writeAuthLoginHelper(target, config, opts = {}) {
|
|
55
|
-
const artifacts = resolvePrepareSyncArtifacts(target, config, opts.runId ?? createSyncRunId());
|
|
56
|
-
const statePath = opts.out ? resolve(target, opts.out) : resolveAuthProfilePath(target, config, opts.profile);
|
|
57
|
-
const authRoot = dirname(artifacts.authScriptPath);
|
|
58
|
-
if (!isInside(authRoot, statePath)) {
|
|
59
|
-
throw new Error(`Playwright storageState output must stay under ${authRoot}`);
|
|
60
|
-
}
|
|
61
|
-
const script = [
|
|
62
|
-
`import { chromium } from "playwright";`,
|
|
63
|
-
``,
|
|
64
|
-
`const targetUrl = process.env.ZSK_PREPARE_AUTH_URL ?? ${JSON.stringify(opts.url ?? "about:blank")};`,
|
|
65
|
-
`const storageStatePath = process.env.ZSK_PLAYWRIGHT_STORAGE_STATE ?? ${JSON.stringify(statePath)};`,
|
|
66
|
-
`const browser = await chromium.launch({ headless: false });`,
|
|
67
|
-
`const context = await browser.newContext();`,
|
|
68
|
-
`const page = await context.newPage();`,
|
|
69
|
-
`await page.goto(targetUrl, { waitUntil: "domcontentloaded" });`,
|
|
70
|
-
`console.log("Complete login in the opened browser, then press Enter here to save storageState.");`,
|
|
71
|
-
`await new Promise((resolve) => process.stdin.once("data", resolve));`,
|
|
72
|
-
`await context.storageState({ path: storageStatePath });`,
|
|
73
|
-
`console.log(\`Saved Playwright storageState to ${"${storageStatePath}"}\`);`,
|
|
74
|
-
`await browser.close();`,
|
|
75
|
-
``,
|
|
76
|
-
].join("\n");
|
|
77
|
-
await mkdir(dirname(artifacts.authScriptPath), { recursive: true });
|
|
78
|
-
await writeFile(artifacts.authScriptPath, script, "utf8");
|
|
79
|
-
return { ...artifacts, authStatePath: statePath };
|
|
80
|
-
}
|
|
81
68
|
export async function checkPrepareAuth(target, config, opts = {}) {
|
|
82
69
|
const runId = opts.runId ?? createSyncRunId();
|
|
83
70
|
const artifacts = resolvePrepareSyncArtifacts(target, config, runId);
|
|
84
|
-
const entries = selectSourceEntries(
|
|
71
|
+
const entries = selectSourceEntries(flattenPrepareEntries(config), opts);
|
|
85
72
|
const authStatePath = resolveSelectedAuthStatePath(target, config, opts);
|
|
86
73
|
const results = [];
|
|
87
74
|
for (const entry of entries) {
|
|
@@ -91,45 +78,6 @@ export async function checkPrepareAuth(target, config, opts = {}) {
|
|
|
91
78
|
await writeFile(artifacts.authCheckPath, `${JSON.stringify({ runId, results }, null, 2)}\n`, "utf8");
|
|
92
79
|
return { artifacts, results };
|
|
93
80
|
}
|
|
94
|
-
export async function buildRawMigrationPlan(target, config, runId = createSyncRunId()) {
|
|
95
|
-
const artifacts = resolvePrepareSyncArtifacts(target, config, runId);
|
|
96
|
-
const prepareRoot = resolve(target, getWorkspacePath(config, "resourcesRoot"), "prepare");
|
|
97
|
-
const staleNames = new Set([
|
|
98
|
-
"qa-engineer",
|
|
99
|
-
"test-engineer",
|
|
100
|
-
"backend-engineer",
|
|
101
|
-
"frontend-engineer",
|
|
102
|
-
"product-manager",
|
|
103
|
-
"designer",
|
|
104
|
-
"jira",
|
|
105
|
-
"confluence",
|
|
106
|
-
"figma",
|
|
107
|
-
"modao",
|
|
108
|
-
"manual",
|
|
109
|
-
"provider",
|
|
110
|
-
"version",
|
|
111
|
-
]);
|
|
112
|
-
const entries = await safeReadDir(prepareRoot);
|
|
113
|
-
const findings = entries
|
|
114
|
-
.filter((entry) => entry.isDirectory() && isMigrationLaneCandidate(entry.name, staleNames))
|
|
115
|
-
.map((entry) => ({
|
|
116
|
-
path: join(".zsk/raws/prepare", entry.name),
|
|
117
|
-
action: "dry-run only; keep readable, require explicit migration before rewriting references",
|
|
118
|
-
}));
|
|
119
|
-
const content = [
|
|
120
|
-
"# Raw Migration Plan",
|
|
121
|
-
"",
|
|
122
|
-
"Dry-run report only. No files were moved, deleted, or rewritten.",
|
|
123
|
-
"",
|
|
124
|
-
findings.length === 0
|
|
125
|
-
? "No stale provider/method/version prepare lanes were found."
|
|
126
|
-
: findings.map((item) => `- \`${item.path}\`: ${item.action}`).join("\n"),
|
|
127
|
-
"",
|
|
128
|
-
].join("\n");
|
|
129
|
-
await mkdir(dirname(artifacts.migrationPlanPath), { recursive: true });
|
|
130
|
-
await writeFile(artifacts.migrationPlanPath, content, "utf8");
|
|
131
|
-
return { artifacts, content };
|
|
132
|
-
}
|
|
133
81
|
function selectSourceEntries(entries, opts) {
|
|
134
82
|
if (opts.all || !opts.source)
|
|
135
83
|
return entries;
|
|
@@ -144,6 +92,52 @@ function selectSourceEntries(entries, opts) {
|
|
|
144
92
|
}
|
|
145
93
|
return alias;
|
|
146
94
|
}
|
|
95
|
+
async function resolveProviderReadiness(target, artifacts, entries) {
|
|
96
|
+
const names = new Map();
|
|
97
|
+
for (const entry of entries) {
|
|
98
|
+
const normalized = interpretSourceAdapter(entry.source);
|
|
99
|
+
if (!normalized.mcpProviderName)
|
|
100
|
+
continue;
|
|
101
|
+
names.set(normalized.mcpProviderName, normalized);
|
|
102
|
+
}
|
|
103
|
+
if (names.size === 0)
|
|
104
|
+
return { byProvider: new Map() };
|
|
105
|
+
const policy = await loadProviderPolicy(target);
|
|
106
|
+
const registry = await discoverMcpRegistries(target);
|
|
107
|
+
const byProvider = new Map();
|
|
108
|
+
for (const [providerName, normalized] of names) {
|
|
109
|
+
const evidence = buildProviderReadinessEvidence(target, providerName, normalized.adapterId, policy, registry.registrations);
|
|
110
|
+
evidence.warnings.push(...registry.warnings);
|
|
111
|
+
const path = join(artifacts.providerReadinessDir, `${safeFileName(providerName)}.json`);
|
|
112
|
+
await writeFile(path, `${JSON.stringify(evidence, null, 2)}\n`, "utf8");
|
|
113
|
+
byProvider.set(providerName, { path, evidence });
|
|
114
|
+
}
|
|
115
|
+
return { byProvider };
|
|
116
|
+
}
|
|
117
|
+
function annotateProviderReadiness(target, entry, result, readiness) {
|
|
118
|
+
const normalized = interpretSourceAdapter(entry.source);
|
|
119
|
+
const providerName = normalized.mcpProviderName;
|
|
120
|
+
if (!providerName)
|
|
121
|
+
return result;
|
|
122
|
+
const item = readiness.get(providerName);
|
|
123
|
+
if (!item)
|
|
124
|
+
return result;
|
|
125
|
+
return {
|
|
126
|
+
...result,
|
|
127
|
+
metadata: cleanMetadata({
|
|
128
|
+
...(result.metadata ?? {}),
|
|
129
|
+
adapterId: normalized.adapterId,
|
|
130
|
+
mcpProvider: providerName,
|
|
131
|
+
providerReadiness: relative(target, item.path),
|
|
132
|
+
providerReadinessStatus: item.evidence.summary,
|
|
133
|
+
providerPolicyStatus: item.evidence.policy.status,
|
|
134
|
+
providerRegistryStatus: item.evidence.registry.status,
|
|
135
|
+
providerConnectionStatus: item.evidence.connection.status,
|
|
136
|
+
capabilitiesMatched: item.evidence.capabilities.matched.join(", ") || undefined,
|
|
137
|
+
capabilitiesEvaluation: item.evidence.capabilities.evaluation,
|
|
138
|
+
}),
|
|
139
|
+
};
|
|
140
|
+
}
|
|
147
141
|
async function checkAuthEntry(entry, authStatePath, opts) {
|
|
148
142
|
const origin = inferSourceOrigin(entry.source);
|
|
149
143
|
const base = {
|
|
@@ -154,20 +148,20 @@ async function checkAuthEntry(entry, authStatePath, opts) {
|
|
|
154
148
|
origin: origin.ref,
|
|
155
149
|
method: origin.method,
|
|
156
150
|
};
|
|
157
|
-
const url = origin
|
|
158
|
-
if (!url
|
|
151
|
+
const url = authCheckUrlForSource(entry.source, origin);
|
|
152
|
+
if (!url) {
|
|
159
153
|
return {
|
|
160
154
|
...base,
|
|
161
155
|
status: "source-gap",
|
|
162
156
|
authSources: [],
|
|
163
157
|
directAuthAvailable: false,
|
|
164
158
|
networkAttempted: false,
|
|
165
|
-
reason: "auth-check
|
|
159
|
+
reason: "auth-check needs origin.url or provider fields that derive a REST URL before checking runtime credentials",
|
|
166
160
|
validation: { urlOrigin: "fail", secretValuesHidden: "pass" },
|
|
167
161
|
};
|
|
168
162
|
}
|
|
169
163
|
const { headers, authSources } = await headersForSource(entry.source, url, authStatePath);
|
|
170
|
-
const directAuthAvailable =
|
|
164
|
+
const directAuthAvailable = hasRuntimeAuthHeaders(headers);
|
|
171
165
|
if (!opts.allowNetwork) {
|
|
172
166
|
return {
|
|
173
167
|
...base,
|
|
@@ -253,161 +247,193 @@ async function checkAuthEntry(entry, authStatePath, opts) {
|
|
|
253
247
|
};
|
|
254
248
|
}
|
|
255
249
|
}
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
const
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
250
|
+
function authCheckUrlForSource(source, origin) {
|
|
251
|
+
if (origin.ref && origin.method === "url")
|
|
252
|
+
return origin.ref;
|
|
253
|
+
const provider = sourceProviderToken(source);
|
|
254
|
+
if (provider?.includes("confluence")) {
|
|
255
|
+
return firstOriginString(source, "apiUrl", "restUrl") ?? deriveConfluenceRestUrl(source);
|
|
256
|
+
}
|
|
257
|
+
if (provider?.includes("jira")) {
|
|
258
|
+
return firstOriginString(source, "apiUrl", "searchUrl") ?? deriveJiraRestUrl(source);
|
|
259
|
+
}
|
|
260
|
+
return undefined;
|
|
261
|
+
}
|
|
262
|
+
async function syncEntry(target, config, entry, opts, entries = [entry]) {
|
|
263
|
+
const context = await createSourceSnapshotContext(target, config, entry, optsWithSourceFallback(entry.source, opts), hashIfExists);
|
|
264
|
+
const sharedSnapshot = sharedSnapshotProvenance(target, config, entry, entries);
|
|
265
|
+
return acquireSourceSnapshot(context, {
|
|
266
|
+
dryRun: skippedDryRunSnapshot,
|
|
267
|
+
local: syncLocalSnapshot,
|
|
268
|
+
provider: (snapshotContext, providerAdapter) => syncProviderSnapshot(snapshotContext, providerAdapter, sharedSnapshot),
|
|
269
|
+
repository: syncRepositorySnapshot,
|
|
270
|
+
url: syncUrlSnapshot,
|
|
271
|
+
providerManaged: blockedProviderManagedSnapshot,
|
|
272
|
+
});
|
|
273
|
+
}
|
|
274
|
+
function optsWithSourceFallback(source, opts) {
|
|
275
|
+
const interpretation = interpretSourceAdapter(source);
|
|
276
|
+
if (interpretation.hasBrowserFallback) {
|
|
277
|
+
return { ...opts, browser: true };
|
|
278
|
+
}
|
|
279
|
+
return opts;
|
|
280
|
+
}
|
|
281
|
+
function skippedDryRunSnapshot(context) {
|
|
282
|
+
return {
|
|
283
|
+
...context.base,
|
|
284
|
+
strategy: chooseSourceSnapshotStrategy(context.origin.method),
|
|
285
|
+
status: "skipped",
|
|
286
|
+
changed: false,
|
|
287
|
+
reason: "dry-run requested; no snapshot was written",
|
|
288
|
+
validation: { dryRun: "pass" },
|
|
271
289
|
};
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
reason: "dry-run requested; no snapshot was written",
|
|
279
|
-
validation: { dryRun: "pass" },
|
|
280
|
-
};
|
|
290
|
+
}
|
|
291
|
+
async function syncLocalSnapshot(context) {
|
|
292
|
+
const { base, entry, source, snapshotPath, previousSnapshotHash, target, opts } = context;
|
|
293
|
+
const sourcePath = source.origin?.path ?? source.path;
|
|
294
|
+
if (!sourcePath) {
|
|
295
|
+
return sourceGap(base, "configured local origin has no path");
|
|
281
296
|
}
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
297
|
+
const originPath = resolve(target, sourcePath);
|
|
298
|
+
if (!(await exists(originPath)) && opts.allowNetwork && isGitSubmoduleResource(source)) {
|
|
299
|
+
const update = await updateGitSubmodule(target, sourcePath);
|
|
300
|
+
if (!update.ok)
|
|
301
|
+
return failed(base, update.reason);
|
|
302
|
+
}
|
|
303
|
+
const info = await safeStat(originPath);
|
|
304
|
+
if (!info) {
|
|
305
|
+
return sourceGap(base, "configured local origin path does not exist");
|
|
306
|
+
}
|
|
307
|
+
let written;
|
|
308
|
+
if (info.isDirectory) {
|
|
309
|
+
written = await writePreparedSnapshot(target, snapshotPath, await renderStructuredLocalDirectoryMarkdown(target, entry, originPath), previousSnapshotHash);
|
|
310
|
+
}
|
|
311
|
+
else if (shouldPreserveMachineReadable(source, originPath, snapshotPath)) {
|
|
291
312
|
await mkdir(dirname(snapshotPath), { recursive: true });
|
|
292
|
-
|
|
293
|
-
await copyFile(originPath, snapshotPath);
|
|
294
|
-
}
|
|
295
|
-
else {
|
|
296
|
-
await writeFile(snapshotPath, await renderStructuredLocalMarkdown(target, entry, originPath), "utf8");
|
|
297
|
-
}
|
|
313
|
+
await copyFile(originPath, snapshotPath);
|
|
298
314
|
const snapshotHash = await sha256(snapshotPath);
|
|
299
|
-
|
|
300
|
-
...base,
|
|
301
|
-
strategy: "local-copy-structured-markdown",
|
|
302
|
-
status: "materialized",
|
|
315
|
+
written = {
|
|
303
316
|
snapshotHash,
|
|
304
317
|
changed: previousSnapshotHash !== snapshotHash,
|
|
305
|
-
reason: "local origin copied or materialized into configured snapshot",
|
|
306
|
-
validation: { sourceExists: "pass", bodyContent: "pass", snapshotWritten: "pass" },
|
|
307
318
|
};
|
|
308
319
|
}
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
320
|
+
else {
|
|
321
|
+
written = await writePreparedSnapshot(target, snapshotPath, await renderStructuredLocalMarkdown(target, entry, originPath), previousSnapshotHash);
|
|
322
|
+
}
|
|
323
|
+
return {
|
|
324
|
+
...base,
|
|
325
|
+
strategy: "local-copy-structured-markdown",
|
|
326
|
+
status: "materialized",
|
|
327
|
+
snapshotHash: written.snapshotHash,
|
|
328
|
+
changed: written.changed,
|
|
329
|
+
reason: info.isDirectory
|
|
330
|
+
? "local directory origin summarized into configured snapshot"
|
|
331
|
+
: "local origin copied or materialized into configured snapshot",
|
|
332
|
+
validation: { sourceExists: "pass", bodyContent: "pass", snapshotWritten: "pass" },
|
|
333
|
+
};
|
|
334
|
+
}
|
|
335
|
+
async function syncProviderSnapshot(context, providerAdapter, sharedSnapshot) {
|
|
336
|
+
const { target, config, entry, snapshotPath, previousSnapshotHash, opts, origin } = context;
|
|
337
|
+
const authStatePath = resolveSelectedAuthStatePath(target, config, opts);
|
|
338
|
+
const adapterResult = await runProviderAdapter(providerAdapter, target, entry, snapshotPath, previousSnapshotHash, authStatePath, opts, sharedSnapshot);
|
|
339
|
+
if (!adapterResult)
|
|
340
|
+
return null;
|
|
341
|
+
if (adapterResult.status === "source-gap" && origin.method === "url") {
|
|
342
|
+
if (adapterResult.metadata?.providerGap === "adapter-ambiguous") {
|
|
322
343
|
return adapterResult;
|
|
323
344
|
}
|
|
345
|
+
const fallback = await syncUrlSnapshot(context);
|
|
346
|
+
return annotateFallbackResult(fallback, providerAdapter, adapterResult);
|
|
324
347
|
}
|
|
325
|
-
if (origin.method === "
|
|
326
|
-
await
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
348
|
+
if (adapterResult.status === "blocked-auth" && opts.browser && origin.method === "url") {
|
|
349
|
+
const browserResult = await fetchUrlSnapshotWithBrowser(target, entry, snapshotPath, previousSnapshotHash, authStatePath);
|
|
350
|
+
return annotateFallbackResult(browserResult, providerAdapter, adapterResult, `${providerAdapter}-browser-fallback`);
|
|
351
|
+
}
|
|
352
|
+
return adapterResult;
|
|
353
|
+
}
|
|
354
|
+
function annotateFallbackResult(fallback, providerAdapter, adapterResult, adapter = `${providerAdapter}-generic-fallback`) {
|
|
355
|
+
return {
|
|
356
|
+
...fallback,
|
|
357
|
+
adapter,
|
|
358
|
+
metadata: cleanMetadata({
|
|
359
|
+
...(fallback.metadata ?? {}),
|
|
360
|
+
providerAdapter,
|
|
361
|
+
fallbackFrom: adapterResult.strategy,
|
|
362
|
+
fallbackReason: adapterResult.reason,
|
|
363
|
+
fallbackStatus: adapterResult.status,
|
|
364
|
+
}),
|
|
365
|
+
};
|
|
366
|
+
}
|
|
367
|
+
async function syncRepositorySnapshot(context) {
|
|
368
|
+
const { base, entry, origin, snapshotPath, previousSnapshotHash } = context;
|
|
369
|
+
const content = renderRepositoryMetadata(entry, origin.ref ?? "");
|
|
370
|
+
const written = await writePreparedSnapshot(context.target, snapshotPath, content, previousSnapshotHash);
|
|
371
|
+
return {
|
|
372
|
+
...base,
|
|
373
|
+
strategy: "repository-metadata-only",
|
|
374
|
+
status: "metadata-only",
|
|
375
|
+
snapshotHash: written.snapshotHash,
|
|
376
|
+
changed: written.changed,
|
|
377
|
+
reason: "repository origin recorded as metadata; configure contract paths or checkout policy before broad acquisition",
|
|
378
|
+
validation: { repositoryNotCrawled: "pass", snapshotWritten: "pass" },
|
|
379
|
+
};
|
|
380
|
+
}
|
|
381
|
+
async function syncUrlSnapshot(context) {
|
|
382
|
+
const { base, entry, source, origin, target, config, opts, snapshotPath, previousSnapshotHash } = context;
|
|
383
|
+
if (!opts.allowNetwork) {
|
|
330
384
|
return {
|
|
331
385
|
...base,
|
|
332
|
-
strategy: "
|
|
333
|
-
status: "
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
validation: { repositoryNotCrawled: "pass", snapshotWritten: "pass" },
|
|
386
|
+
strategy: "playwright-auth-or-direct-fetch",
|
|
387
|
+
status: "blocked-auth",
|
|
388
|
+
changed: false,
|
|
389
|
+
reason: "remote URL requires --allow-network or a materialized snapshot; Playwright auth helper can create reusable storageState first",
|
|
390
|
+
validation: { networkAllowed: "fail", previousSnapshotPreserved: previousSnapshotHash ? "pass" : "skipped" },
|
|
338
391
|
};
|
|
339
392
|
}
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
changed: false,
|
|
347
|
-
reason: "remote URL requires --allow-network or a materialized snapshot; Playwright auth helper can create reusable storageState first",
|
|
348
|
-
validation: { networkAllowed: "fail", previousSnapshotPreserved: previousSnapshotHash ? "pass" : "skipped" },
|
|
349
|
-
};
|
|
350
|
-
}
|
|
351
|
-
const authStatePath = resolveSelectedAuthStatePath(target, config, opts);
|
|
352
|
-
if (opts.browser) {
|
|
353
|
-
if (origin.ref && await hasDirectRuntimeAuthForSource(source, origin.ref, authStatePath)) {
|
|
354
|
-
const direct = await fetchUrlSnapshot(target, entry, snapshotPath, previousSnapshotHash, authStatePath);
|
|
355
|
-
if (direct.status === "materialized")
|
|
356
|
-
return direct;
|
|
357
|
-
}
|
|
358
|
-
return fetchUrlSnapshotWithBrowser(target, entry, snapshotPath, previousSnapshotHash, authStatePath);
|
|
393
|
+
const authStatePath = resolveSelectedAuthStatePath(target, config, opts);
|
|
394
|
+
if (opts.browser) {
|
|
395
|
+
if (origin.ref && await hasDirectRuntimeAuthForSource(source, origin.ref, authStatePath)) {
|
|
396
|
+
const direct = await fetchUrlSnapshot(target, entry, snapshotPath, previousSnapshotHash, authStatePath);
|
|
397
|
+
if (direct.status === "materialized")
|
|
398
|
+
return direct;
|
|
359
399
|
}
|
|
360
|
-
return
|
|
400
|
+
return fetchUrlSnapshotWithBrowser(target, entry, snapshotPath, previousSnapshotHash, authStatePath);
|
|
361
401
|
}
|
|
402
|
+
return fetchUrlSnapshot(target, entry, snapshotPath, previousSnapshotHash, authStatePath);
|
|
403
|
+
}
|
|
404
|
+
function blockedProviderManagedSnapshot(context) {
|
|
362
405
|
return {
|
|
363
|
-
...base,
|
|
406
|
+
...context.base,
|
|
364
407
|
strategy: "confirm-acquisition-method",
|
|
365
408
|
status: "blocked-auth",
|
|
366
409
|
changed: false,
|
|
367
410
|
reason: "provider-managed origin needs an explicit acquisition method or exported snapshot",
|
|
368
|
-
validation: { acquisitionMethodConfirmed: "fail", previousSnapshotPreserved: previousSnapshotHash ? "pass" : "skipped" },
|
|
411
|
+
validation: { acquisitionMethodConfirmed: "fail", previousSnapshotPreserved: context.previousSnapshotHash ? "pass" : "skipped" },
|
|
369
412
|
};
|
|
370
413
|
}
|
|
371
|
-
function
|
|
372
|
-
const keys = [
|
|
373
|
-
origin.provider,
|
|
374
|
-
origin.kind,
|
|
375
|
-
source.origin?.provider,
|
|
376
|
-
source.origin?.kind,
|
|
377
|
-
source.kind,
|
|
378
|
-
source.type,
|
|
379
|
-
].map((value) => normalizeAdapterKey(typeof value === "string" ? value : undefined));
|
|
380
|
-
if (keys.some((value) => value.includes("confluence")))
|
|
381
|
-
return "confluence";
|
|
382
|
-
if (keys.some((value) => value.includes("jira")))
|
|
383
|
-
return "jira";
|
|
384
|
-
if (keys.some((value) => value.includes("gitlab")))
|
|
385
|
-
return "gitlab";
|
|
386
|
-
if (keys.some((value) => ["figma", "modao", "mastergo", "design", "design-asset", "design-source"].includes(value))) {
|
|
387
|
-
return "design-source";
|
|
388
|
-
}
|
|
389
|
-
return undefined;
|
|
390
|
-
}
|
|
391
|
-
async function runProviderAdapter(adapter, target, entry, snapshotPath, previousSnapshotHash, authStatePath, opts) {
|
|
414
|
+
async function runProviderAdapter(adapter, target, entry, snapshotPath, previousSnapshotHash, authStatePath, opts, sharedSnapshot) {
|
|
392
415
|
if (adapter === "confluence") {
|
|
393
|
-
return runConfluenceAdapter(target, entry, snapshotPath, previousSnapshotHash, authStatePath, opts);
|
|
416
|
+
return runConfluenceAdapter(target, entry, snapshotPath, previousSnapshotHash, authStatePath, opts, sharedSnapshot);
|
|
394
417
|
}
|
|
395
418
|
if (adapter === "jira") {
|
|
396
|
-
return runJiraAdapter(target, entry, snapshotPath, previousSnapshotHash, authStatePath, opts);
|
|
419
|
+
return runJiraAdapter(target, entry, snapshotPath, previousSnapshotHash, authStatePath, opts, sharedSnapshot);
|
|
397
420
|
}
|
|
398
421
|
if (adapter === "gitlab") {
|
|
399
|
-
return runGitLabAdapter(target, entry, snapshotPath, previousSnapshotHash, authStatePath, opts);
|
|
422
|
+
return runGitLabAdapter(target, entry, snapshotPath, previousSnapshotHash, authStatePath, opts, sharedSnapshot);
|
|
400
423
|
}
|
|
401
|
-
return runDesignAdapter(target, entry, snapshotPath, previousSnapshotHash, authStatePath, opts);
|
|
424
|
+
return runDesignAdapter(adapter, target, entry, snapshotPath, previousSnapshotHash, authStatePath, opts, sharedSnapshot);
|
|
402
425
|
}
|
|
403
|
-
async function runConfluenceAdapter(target, entry, snapshotPath, previousSnapshotHash, authStatePath, opts) {
|
|
426
|
+
async function runConfluenceAdapter(target, entry, snapshotPath, previousSnapshotHash, authStatePath, opts, sharedSnapshot) {
|
|
404
427
|
const base = adapterBase(target, entry, snapshotPath, previousSnapshotHash, "confluence");
|
|
405
|
-
const
|
|
406
|
-
const
|
|
428
|
+
const exportWordUrl = firstOriginString(entry.source, "exportWordUrl", "wordExportUrl", "exportUrl");
|
|
429
|
+
const restUrl = firstOriginString(entry.source, "apiUrl", "restUrl") ?? deriveConfluenceRestUrl(entry.source);
|
|
430
|
+
const pageUrl = firstOriginString(entry.source, "url") ?? base.origin;
|
|
431
|
+
const url = exportWordUrl ?? restUrl ?? pageUrl;
|
|
432
|
+
const strategy = exportWordUrl ? "confluence-word-export" : "confluence-storage-rest";
|
|
407
433
|
if (!url)
|
|
408
|
-
return sourceGap(base, "confluence adapter needs origin.url, origin.apiUrl, or origin.
|
|
434
|
+
return sourceGap(base, "confluence adapter needs origin.url, origin.apiUrl, origin.restUrl, or origin.exportWordUrl");
|
|
409
435
|
if (!opts.allowNetwork) {
|
|
410
|
-
return blockedAdapter(base, strategy, "confluence source requires --allow-network and optional Playwright storageState before acquisition");
|
|
436
|
+
return blockedAdapter(base, strategy, "confluence source requires --allow-network and optional env auth or Playwright storageState before acquisition", cleanMetadata({ derivedUrl: restUrl }));
|
|
411
437
|
}
|
|
412
438
|
const fetched = await fetchAdapterBody(entry.source, url, authStatePath);
|
|
413
439
|
if (!fetched.ok)
|
|
@@ -416,15 +442,18 @@ async function runConfluenceAdapter(target, entry, snapshotPath, previousSnapsho
|
|
|
416
442
|
return blockedAdapter(base, strategy, "confluence adapter resolved to login/chrome content; previous snapshot was preserved");
|
|
417
443
|
}
|
|
418
444
|
const parsed = parseJsonObject(fetched.body);
|
|
445
|
+
const wordExport = parseConfluenceWordExport(fetched.body, fetched.contentType);
|
|
419
446
|
const title = parsed ? stringValue(parsed, "title") : undefined;
|
|
420
447
|
const pageId = parsed ? firstStringValue(parsed, "id", "pageId", "contentId") : undefined;
|
|
421
448
|
const version = parsed ? versionValue(parsed) : undefined;
|
|
422
449
|
const storageHtml = parsed ? confluenceBodyValue(parsed) : undefined;
|
|
423
|
-
const content =
|
|
424
|
-
? htmlToStructuredMarkdown(
|
|
425
|
-
:
|
|
426
|
-
? htmlToStructuredMarkdown(
|
|
427
|
-
:
|
|
450
|
+
const content = wordExport?.html
|
|
451
|
+
? htmlToStructuredMarkdown(wordExport.html)
|
|
452
|
+
: storageHtml
|
|
453
|
+
? htmlToStructuredMarkdown(storageHtml)
|
|
454
|
+
: fetched.contentType.includes("html")
|
|
455
|
+
? htmlToStructuredMarkdown(fetched.body)
|
|
456
|
+
: fencedBody(fetched.body, fetched.contentType);
|
|
428
457
|
if (!hasMaterialContent(content)) {
|
|
429
458
|
return blockedAdapter(base, strategy, "confluence adapter produced no material body content; previous snapshot was preserved");
|
|
430
459
|
}
|
|
@@ -435,11 +464,11 @@ async function runConfluenceAdapter(target, entry, snapshotPath, previousSnapsho
|
|
|
435
464
|
contentType: fetched.contentType,
|
|
436
465
|
content,
|
|
437
466
|
reason: "confluence source fetched and normalized into structured Markdown",
|
|
438
|
-
metadata: cleanMetadata({ title, pageId, version, runtimeAuth: fetched.authSources.join(", ") || undefined }),
|
|
467
|
+
metadata: cleanMetadata({ title: wordExport?.title ?? title, pageId, version, runtimeAuth: fetched.authSources.join(", ") || undefined }),
|
|
439
468
|
validation: { httpOk: "pass", loginPageRejected: "pass", bodyContent: "pass", snapshotWritten: "pass" },
|
|
440
|
-
});
|
|
469
|
+
}, sharedSnapshot);
|
|
441
470
|
}
|
|
442
|
-
async function runJiraAdapter(target, entry, snapshotPath, previousSnapshotHash, authStatePath, opts) {
|
|
471
|
+
async function runJiraAdapter(target, entry, snapshotPath, previousSnapshotHash, authStatePath, opts, sharedSnapshot) {
|
|
443
472
|
const base = adapterBase(target, entry, snapshotPath, previousSnapshotHash, "jira");
|
|
444
473
|
const csvPath = firstOriginString(entry.source, ...JIRA_LOCAL_FILE_ORIGIN_KEYS);
|
|
445
474
|
if (csvPath) {
|
|
@@ -463,14 +492,15 @@ async function runJiraAdapter(target, entry, snapshotPath, previousSnapshotHash,
|
|
|
463
492
|
reason: "jira CSV export imported and normalized into structured Markdown",
|
|
464
493
|
metadata: cleanMetadata({ issueCount: rows.length, fallback: "csv" }),
|
|
465
494
|
validation: { sourceExists: "pass", bodyContent: "pass", snapshotWritten: "pass" },
|
|
466
|
-
});
|
|
495
|
+
}, sharedSnapshot);
|
|
467
496
|
}
|
|
468
|
-
const
|
|
497
|
+
const derivedUrl = deriveJiraRestUrl(entry.source);
|
|
498
|
+
const url = firstOriginString(entry.source, "apiUrl", "searchUrl") ?? derivedUrl ?? firstOriginString(entry.source, "url") ?? (base.origin?.startsWith("http") || base.origin?.startsWith("data:") ? base.origin : undefined);
|
|
469
499
|
if (!url)
|
|
470
500
|
return null;
|
|
471
501
|
const strategy = "jira-rest-search";
|
|
472
502
|
if (!opts.allowNetwork) {
|
|
473
|
-
return blockedAdapter(base, strategy, "jira REST source requires --allow-network and optional Playwright storageState before acquisition");
|
|
503
|
+
return blockedAdapter(base, strategy, "jira REST source requires --allow-network and optional env auth or Playwright storageState before acquisition", cleanMetadata({ derivedUrl }));
|
|
474
504
|
}
|
|
475
505
|
const fetched = await fetchAdapterBody(entry.source, url, authStatePath);
|
|
476
506
|
if (!fetched.ok)
|
|
@@ -492,9 +522,9 @@ async function runJiraAdapter(target, entry, snapshotPath, previousSnapshotHash,
|
|
|
492
522
|
reason: "jira REST source fetched and normalized into structured Markdown",
|
|
493
523
|
metadata: cleanMetadata({ issueCount: jiraIssueCount(parsed), runtimeAuth: fetched.authSources.join(", ") || undefined }),
|
|
494
524
|
validation: { httpOk: "pass", loginPageRejected: "pass", bodyContent: "pass", snapshotWritten: "pass" },
|
|
495
|
-
});
|
|
525
|
+
}, sharedSnapshot);
|
|
496
526
|
}
|
|
497
|
-
async function runGitLabAdapter(target, entry, snapshotPath, previousSnapshotHash, authStatePath, opts) {
|
|
527
|
+
async function runGitLabAdapter(target, entry, snapshotPath, previousSnapshotHash, authStatePath, opts, sharedSnapshot) {
|
|
498
528
|
const base = adapterBase(target, entry, snapshotPath, previousSnapshotHash, "gitlab");
|
|
499
529
|
const url = firstOriginString(entry.source, "rawUrl", "apiUrl", "url");
|
|
500
530
|
if (!url)
|
|
@@ -532,10 +562,11 @@ async function runGitLabAdapter(target, entry, snapshotPath, previousSnapshotHas
|
|
|
532
562
|
runtimeAuth: fetched.authSources.join(", ") || undefined,
|
|
533
563
|
}),
|
|
534
564
|
validation: { httpOk: "pass", loginPageRejected: "pass", bodyContent: "pass", snapshotWritten: "pass" },
|
|
535
|
-
});
|
|
565
|
+
}, sharedSnapshot);
|
|
536
566
|
}
|
|
537
|
-
async function runDesignAdapter(target, entry, snapshotPath, previousSnapshotHash, authStatePath, opts) {
|
|
538
|
-
const
|
|
567
|
+
async function runDesignAdapter(adapter, target, entry, snapshotPath, previousSnapshotHash, authStatePath, opts, sharedSnapshot) {
|
|
568
|
+
const normalized = interpretSourceAdapter(entry.source);
|
|
569
|
+
const base = adapterBase(target, entry, snapshotPath, previousSnapshotHash, adapter);
|
|
539
570
|
const exportPath = firstOriginString(entry.source, ...DESIGN_LOCAL_FILE_ORIGIN_KEYS);
|
|
540
571
|
if (exportPath) {
|
|
541
572
|
const path = resolveAdapterLocalFilePath(target, exportPath);
|
|
@@ -549,7 +580,7 @@ async function runDesignAdapter(target, entry, snapshotPath, previousSnapshotHas
|
|
|
549
580
|
return blockedAdapter(base, "design-export-import", "design export produced no material content; previous snapshot was preserved");
|
|
550
581
|
}
|
|
551
582
|
return writeAdapterSnapshot(target, entry, snapshotPath, previousSnapshotHash, {
|
|
552
|
-
adapter
|
|
583
|
+
adapter,
|
|
553
584
|
strategy: "design-export-import",
|
|
554
585
|
resolvedOrigin: exportPath,
|
|
555
586
|
contentType: contentTypeForPath(exportPath),
|
|
@@ -557,21 +588,56 @@ async function runDesignAdapter(target, entry, snapshotPath, previousSnapshotHas
|
|
|
557
588
|
reason: "design export imported and normalized into structured Markdown",
|
|
558
589
|
metadata: cleanMetadata({ provider: base.provider, fallback: "export" }),
|
|
559
590
|
validation: { sourceExists: "pass", bodyContent: "pass", snapshotWritten: "pass" },
|
|
560
|
-
});
|
|
591
|
+
}, sharedSnapshot);
|
|
592
|
+
}
|
|
593
|
+
if (adapter === "design-source" && normalized.ambiguous && !exportPath) {
|
|
594
|
+
return sourceGap(base, "design source needs explicit adapter before provider-backed acquisition; configure source adapter, for example adapter: figma", designProviderGapMetadata("adapter-ambiguous", entry.source, undefined));
|
|
561
595
|
}
|
|
562
|
-
const
|
|
596
|
+
const figmaLocator = figmaLocatorForSource(entry.source);
|
|
597
|
+
const command = figmaCommandForSource(entry.source, target, relative(target, snapshotPath), figmaLocator);
|
|
598
|
+
if (command) {
|
|
599
|
+
if (!opts.allowNetwork) {
|
|
600
|
+
return blockedAdapter(base, "figma-mcp-command", "figma MCP command acquisition requires --allow-network because it may contact the local MCP bridge or Figma desktop plugin", designProviderGapMetadata("network-not-allowed", entry.source, figmaLocator));
|
|
601
|
+
}
|
|
602
|
+
const commandResult = await runDesignCommand(target, command);
|
|
603
|
+
if (!commandResult.ok) {
|
|
604
|
+
return blockedAdapter(base, "figma-mcp-command", commandResult.reason, designProviderGapMetadata("mcp-command-failed", entry.source, figmaLocator));
|
|
605
|
+
}
|
|
606
|
+
const content = renderDesignMarkdown(commandResult.body, commandResult.origin, commandResult.contentType);
|
|
607
|
+
if (!hasMaterialContent(content)) {
|
|
608
|
+
return blockedAdapter(base, "figma-mcp-command", "figma MCP command produced no material design content; previous snapshot was preserved", designProviderGapMetadata("mcp-command-empty", entry.source, figmaLocator));
|
|
609
|
+
}
|
|
610
|
+
return writeAdapterSnapshot(target, entry, snapshotPath, previousSnapshotHash, {
|
|
611
|
+
adapter,
|
|
612
|
+
strategy: "figma-mcp-command",
|
|
613
|
+
resolvedOrigin: commandResult.origin,
|
|
614
|
+
contentType: commandResult.contentType,
|
|
615
|
+
content,
|
|
616
|
+
reason: "design source captured through configured Figma MCP command and normalized into structured Markdown",
|
|
617
|
+
metadata: cleanMetadata({
|
|
618
|
+
provider: base.provider,
|
|
619
|
+
mcpProvider: command.provider,
|
|
620
|
+
command: command.safeLabel,
|
|
621
|
+
fileKey: figmaLocator?.fileKey,
|
|
622
|
+
nodeId: figmaLocator?.nodeId,
|
|
623
|
+
}),
|
|
624
|
+
validation: { commandExited: "pass", bodyContent: "pass", snapshotWritten: "pass", secretValuesHidden: "pass" },
|
|
625
|
+
}, sharedSnapshot);
|
|
626
|
+
}
|
|
627
|
+
const derivedApiUrl = figmaLocator?.apiUrl;
|
|
628
|
+
const url = firstOriginString(entry.source, "apiUrl", "exportUrl") ?? derivedApiUrl ?? firstOriginString(entry.source, "url") ?? base.origin;
|
|
563
629
|
if (!url) {
|
|
564
|
-
return sourceGap(base, "design source needs origin.url, origin.apiUrl, origin.exportUrl, or a local exportPath/assetPath/filePath before acquisition", designProviderGapMetadata("missing-provider-locator"));
|
|
630
|
+
return sourceGap(base, "design source needs origin.url, origin.apiUrl, origin.exportUrl, or a local exportPath/assetPath/filePath before acquisition", designProviderGapMetadata("missing-provider-locator", entry.source, figmaLocator));
|
|
565
631
|
}
|
|
566
632
|
if (!opts.allowNetwork) {
|
|
567
|
-
return blockedAdapter(base, opts.browser ? "design-browser-render" : "design-api-or-browser", "design source requires --allow-network with token/export URL or --browser storageState acquisition", designProviderGapMetadata("network-not-allowed"));
|
|
633
|
+
return blockedAdapter(base, opts.browser ? "design-browser-render" : "design-api-or-browser", "design source requires --allow-network with token/export URL or --browser storageState acquisition", designProviderGapMetadata("network-not-allowed", entry.source, figmaLocator));
|
|
568
634
|
}
|
|
569
635
|
const directRuntimeAuth = await hasDirectRuntimeAuthForSource(entry.source, url, authStatePath);
|
|
570
636
|
if (opts.browser && !directRuntimeAuth) {
|
|
571
637
|
const rendered = await fetchUrlSnapshotWithBrowser(target, entry, snapshotPath, previousSnapshotHash, authStatePath);
|
|
572
638
|
return {
|
|
573
639
|
...rendered,
|
|
574
|
-
adapter
|
|
640
|
+
adapter,
|
|
575
641
|
metadata: cleanMetadata({ provider: base.provider, acquisition: "browser" }),
|
|
576
642
|
};
|
|
577
643
|
}
|
|
@@ -579,29 +645,35 @@ async function runDesignAdapter(target, entry, snapshotPath, previousSnapshotHas
|
|
|
579
645
|
if (!fetched.ok) {
|
|
580
646
|
if (opts.browser)
|
|
581
647
|
return fetchUrlSnapshotWithBrowser(target, entry, snapshotPath, previousSnapshotHash, authStatePath);
|
|
582
|
-
return blockedAdapter(base, "design-api-or-export", fetched.reason, designProviderGapMetadata("provider-fetch-failed"));
|
|
648
|
+
return blockedAdapter(base, "design-api-or-export", fetched.reason, designProviderGapMetadata("provider-fetch-failed", entry.source, figmaLocator));
|
|
583
649
|
}
|
|
584
650
|
if (looksLikeLoginPage(fetched.body, fetched.resolvedOrigin)) {
|
|
585
651
|
if (opts.browser)
|
|
586
652
|
return fetchUrlSnapshotWithBrowser(target, entry, snapshotPath, previousSnapshotHash, authStatePath);
|
|
587
|
-
return blockedAdapter(base, "design-api-or-export", "design adapter resolved to login/chrome content; previous snapshot was preserved", designProviderGapMetadata("provider-auth-blocked"));
|
|
653
|
+
return blockedAdapter(base, "design-api-or-export", "design adapter resolved to login/chrome content; previous snapshot was preserved", designProviderGapMetadata("provider-auth-blocked", entry.source, figmaLocator));
|
|
588
654
|
}
|
|
589
655
|
const content = renderDesignMarkdown(fetched.body, fetched.resolvedOrigin, fetched.contentType);
|
|
590
656
|
if (!hasMaterialContent(content)) {
|
|
591
657
|
if (opts.browser)
|
|
592
658
|
return fetchUrlSnapshotWithBrowser(target, entry, snapshotPath, previousSnapshotHash, authStatePath);
|
|
593
|
-
return blockedAdapter(base, "design-api-or-export", "design adapter produced no material design content; previous snapshot was preserved", designProviderGapMetadata("provider-content-incomplete"));
|
|
659
|
+
return blockedAdapter(base, "design-api-or-export", "design adapter produced no material design content; previous snapshot was preserved", designProviderGapMetadata("provider-content-incomplete", entry.source, figmaLocator));
|
|
594
660
|
}
|
|
595
661
|
return writeAdapterSnapshot(target, entry, snapshotPath, previousSnapshotHash, {
|
|
596
|
-
adapter
|
|
662
|
+
adapter,
|
|
597
663
|
strategy: "design-api-or-export",
|
|
598
664
|
resolvedOrigin: fetched.resolvedOrigin,
|
|
599
665
|
contentType: fetched.contentType,
|
|
600
666
|
content,
|
|
601
667
|
reason: "design source fetched and normalized into structured Markdown",
|
|
602
|
-
metadata: cleanMetadata({
|
|
668
|
+
metadata: cleanMetadata({
|
|
669
|
+
provider: base.provider,
|
|
670
|
+
runtimeAuth: fetched.authSources.join(", ") || undefined,
|
|
671
|
+
derivedApiUrl,
|
|
672
|
+
fileKey: figmaLocator?.fileKey,
|
|
673
|
+
nodeId: figmaLocator?.nodeId,
|
|
674
|
+
}),
|
|
603
675
|
validation: { httpOk: "pass", loginPageRejected: "pass", bodyContent: "pass", snapshotWritten: "pass" },
|
|
604
|
-
});
|
|
676
|
+
}, sharedSnapshot);
|
|
605
677
|
}
|
|
606
678
|
function adapterBase(target, entry, snapshotPath, previousSnapshotHash, adapter) {
|
|
607
679
|
const origin = inferSourceOrigin(entry.source);
|
|
@@ -617,12 +689,10 @@ function adapterBase(target, entry, snapshotPath, previousSnapshotHash, adapter)
|
|
|
617
689
|
previousSnapshotHash,
|
|
618
690
|
};
|
|
619
691
|
}
|
|
620
|
-
async function writeAdapterSnapshot(target, entry, snapshotPath, previousSnapshotHash, result) {
|
|
692
|
+
async function writeAdapterSnapshot(target, entry, snapshotPath, previousSnapshotHash, result, sharedSnapshot) {
|
|
621
693
|
const origin = inferSourceOrigin(entry.source);
|
|
622
|
-
const markdown = renderAdapterMarkdown(entry, result.resolvedOrigin, result.contentType, result.content, result.strategy, result.adapter, result.metadata);
|
|
623
|
-
await
|
|
624
|
-
await writeFile(snapshotPath, markdown, "utf8");
|
|
625
|
-
const snapshotHash = await sha256(snapshotPath);
|
|
694
|
+
const markdown = renderAdapterMarkdown(entry, result.resolvedOrigin, result.contentType, result.content, result.strategy, result.adapter, result.metadata, sharedSnapshot);
|
|
695
|
+
const written = await writePreparedSnapshot(target, snapshotPath, markdown, previousSnapshotHash);
|
|
626
696
|
return {
|
|
627
697
|
envelopeVersion: 1,
|
|
628
698
|
sourceKey: entry.id,
|
|
@@ -635,11 +705,11 @@ async function writeAdapterSnapshot(target, entry, snapshotPath, previousSnapsho
|
|
|
635
705
|
origin: origin.ref,
|
|
636
706
|
resolvedOrigin: result.resolvedOrigin,
|
|
637
707
|
contentType: result.contentType,
|
|
638
|
-
snapshot:
|
|
639
|
-
snapshotHash,
|
|
708
|
+
snapshot: written.snapshot,
|
|
709
|
+
snapshotHash: written.snapshotHash,
|
|
640
710
|
previousSnapshotHash,
|
|
641
711
|
metadata: result.metadata,
|
|
642
|
-
changed:
|
|
712
|
+
changed: written.changed,
|
|
643
713
|
reason: result.reason,
|
|
644
714
|
validation: result.validation,
|
|
645
715
|
};
|
|
@@ -663,13 +733,18 @@ async function headersForSource(source, url, authStatePath) {
|
|
|
663
733
|
const authSources = [];
|
|
664
734
|
const token = envValue(authStringList(source, "tokenEnv", "accessTokenEnv", "apiTokenEnv"));
|
|
665
735
|
if (token.value) {
|
|
666
|
-
const headerName = firstAuthString(source, "tokenHeader", "headerName") ??
|
|
667
|
-
const tokenScheme = firstAuthString(source, "tokenScheme", "scheme") ??
|
|
736
|
+
const headerName = firstAuthString(source, "tokenHeader", "headerName") ?? defaultTokenHeader(source);
|
|
737
|
+
const tokenScheme = firstAuthString(source, "tokenScheme", "scheme") ?? defaultTokenScheme(source, headerName);
|
|
668
738
|
headers[headerName] = headerName.toLowerCase() === "authorization" && tokenScheme
|
|
669
739
|
? `${tokenScheme} ${token.value}`
|
|
670
740
|
: token.value;
|
|
671
741
|
authSources.push(`env:${token.name}`);
|
|
672
742
|
}
|
|
743
|
+
const basic = basicAuthHeaderFromEnvironment(source);
|
|
744
|
+
if (!headers.authorization && basic.value) {
|
|
745
|
+
headers.authorization = basic.value;
|
|
746
|
+
authSources.push(...basic.sources.map((name) => `env:${name}`));
|
|
747
|
+
}
|
|
673
748
|
const envCookie = cookieHeaderFromEnvironment(source);
|
|
674
749
|
if (envCookie.source)
|
|
675
750
|
authSources.push(`env:${envCookie.source}`);
|
|
@@ -683,7 +758,10 @@ async function headersForSource(source, url, authStatePath) {
|
|
|
683
758
|
}
|
|
684
759
|
async function hasDirectRuntimeAuthForSource(source, url, authStatePath) {
|
|
685
760
|
const { headers } = await headersForSource(source, url, authStatePath);
|
|
686
|
-
return
|
|
761
|
+
return hasRuntimeAuthHeaders(headers);
|
|
762
|
+
}
|
|
763
|
+
function hasRuntimeAuthHeaders(headers) {
|
|
764
|
+
return Object.keys(headers).length > 0;
|
|
687
765
|
}
|
|
688
766
|
async function fetchUrlSnapshot(target, entry, snapshotPath, previousSnapshotHash, authStatePath) {
|
|
689
767
|
const origin = inferSourceOrigin(entry.source);
|
|
@@ -718,17 +796,15 @@ async function fetchUrlSnapshot(target, entry, snapshotPath, previousSnapshotHas
|
|
|
718
796
|
return blocked(base, "remote fetch produced no material body content; previous snapshot was preserved");
|
|
719
797
|
}
|
|
720
798
|
const markdown = renderRemoteMarkdown(entry, response.url, contentType, content);
|
|
721
|
-
await
|
|
722
|
-
await writeFile(snapshotPath, markdown, "utf8");
|
|
723
|
-
const snapshotHash = await sha256(snapshotPath);
|
|
799
|
+
const written = await writePreparedSnapshot(target, snapshotPath, markdown, previousSnapshotHash);
|
|
724
800
|
return {
|
|
725
801
|
...base,
|
|
726
802
|
strategy: authSources.length > 0 ? "runtime-auth-direct-fetch" : "direct-fetch-structured-markdown",
|
|
727
803
|
status: "materialized",
|
|
728
804
|
resolvedOrigin: response.url,
|
|
729
805
|
contentType,
|
|
730
|
-
snapshotHash,
|
|
731
|
-
changed:
|
|
806
|
+
snapshotHash: written.snapshotHash,
|
|
807
|
+
changed: written.changed,
|
|
732
808
|
reason: "remote URL fetched and normalized into structured Markdown",
|
|
733
809
|
metadata: cleanMetadata({ runtimeAuth: authSources.join(", ") || undefined }),
|
|
734
810
|
validation: { httpOk: "pass", loginPageRejected: "pass", bodyContent: "pass", snapshotWritten: "pass" },
|
|
@@ -773,17 +849,15 @@ async function fetchUrlSnapshotWithBrowser(target, entry, snapshotPath, previous
|
|
|
773
849
|
return blocked(base, "browser acquisition produced no material body content; previous snapshot was preserved");
|
|
774
850
|
}
|
|
775
851
|
const markdown = renderRemoteMarkdown(entry, resolvedUrl, "text/html; rendered=playwright", content, "playwright-headless-browser");
|
|
776
|
-
await
|
|
777
|
-
await writeFile(snapshotPath, markdown, "utf8");
|
|
778
|
-
const snapshotHash = await sha256(snapshotPath);
|
|
852
|
+
const written = await writePreparedSnapshot(target, snapshotPath, markdown, previousSnapshotHash);
|
|
779
853
|
return {
|
|
780
854
|
...base,
|
|
781
855
|
strategy: "playwright-headless-browser",
|
|
782
856
|
status: "materialized",
|
|
783
857
|
resolvedOrigin: resolvedUrl,
|
|
784
858
|
contentType: "text/html; rendered=playwright",
|
|
785
|
-
snapshotHash,
|
|
786
|
-
changed:
|
|
859
|
+
snapshotHash: written.snapshotHash,
|
|
860
|
+
changed: written.changed,
|
|
787
861
|
reason: "remote URL rendered through Playwright and normalized into structured Markdown",
|
|
788
862
|
validation: { browserRendered: "pass", loginPageRejected: "pass", bodyContent: "pass", snapshotWritten: "pass" },
|
|
789
863
|
};
|
|
@@ -827,6 +901,63 @@ async function renderStructuredLocalMarkdown(target, entry, originPath) {
|
|
|
827
901
|
"",
|
|
828
902
|
].filter((line) => typeof line === "string").join("\n");
|
|
829
903
|
}
|
|
904
|
+
async function renderStructuredLocalDirectoryMarkdown(target, entry, originPath) {
|
|
905
|
+
const entries = await readdir(originPath, { withFileTypes: true }).catch(() => []);
|
|
906
|
+
const git = await readGitDirectoryMetadata(originPath);
|
|
907
|
+
return [
|
|
908
|
+
"---",
|
|
909
|
+
`sourceKey: ${JSON.stringify(entry.id)}`,
|
|
910
|
+
`sourcePath: ${JSON.stringify(entry.path)}`,
|
|
911
|
+
entry.provider !== entry.id ? `sourceAlias: ${JSON.stringify(entry.provider)}` : undefined,
|
|
912
|
+
`origin: ${JSON.stringify(relative(target, originPath))}`,
|
|
913
|
+
`extractionMethod: "local-directory-summary"`,
|
|
914
|
+
`extractedAt: ${JSON.stringify(new Date().toISOString())}`,
|
|
915
|
+
`snapshotStatus: "metadata-only"`,
|
|
916
|
+
git.head ? `gitHead: ${JSON.stringify(git.head)}` : undefined,
|
|
917
|
+
git.status ? `gitStatus: ${JSON.stringify(git.status)}` : undefined,
|
|
918
|
+
"---",
|
|
919
|
+
"",
|
|
920
|
+
"# Directory Resource Snapshot",
|
|
921
|
+
"",
|
|
922
|
+
"## Provenance",
|
|
923
|
+
"",
|
|
924
|
+
`- Source key: \`${entry.id}\``,
|
|
925
|
+
entry.provider !== entry.id ? `- Source alias: \`${entry.provider}\`` : undefined,
|
|
926
|
+
`- Origin: \`${relative(target, originPath)}\``,
|
|
927
|
+
"- Method: local-directory-summary",
|
|
928
|
+
git.head ? `- Git HEAD: \`${git.head}\`` : undefined,
|
|
929
|
+
git.status ? `- Git status: ${git.status}` : undefined,
|
|
930
|
+
"",
|
|
931
|
+
"## Top-Level Entries",
|
|
932
|
+
"",
|
|
933
|
+
entries.length === 0
|
|
934
|
+
? "- Empty directory."
|
|
935
|
+
: entries
|
|
936
|
+
.slice(0, 200)
|
|
937
|
+
.map((entry) => `- ${entry.isDirectory() ? "dir" : "file"}: \`${entry.name}\``)
|
|
938
|
+
.join("\n"),
|
|
939
|
+
entries.length > 200 ? `- ... ${entries.length - 200} more entries omitted.` : undefined,
|
|
940
|
+
"",
|
|
941
|
+
].filter((line) => typeof line === "string").join("\n");
|
|
942
|
+
}
|
|
943
|
+
async function readGitDirectoryMetadata(originPath) {
|
|
944
|
+
const head = await gitOutput(originPath, ["rev-parse", "HEAD"]);
|
|
945
|
+
const status = await gitOutput(originPath, ["status", "--short"]);
|
|
946
|
+
return {
|
|
947
|
+
head,
|
|
948
|
+
status: status ? "dirty" : head ? "clean" : undefined,
|
|
949
|
+
};
|
|
950
|
+
}
|
|
951
|
+
async function gitOutput(cwd, args) {
|
|
952
|
+
try {
|
|
953
|
+
const { stdout } = await execFileAsync("git", args, { cwd, timeout: 10_000, maxBuffer: 1024 * 1024 });
|
|
954
|
+
const value = stdout.trim();
|
|
955
|
+
return value || undefined;
|
|
956
|
+
}
|
|
957
|
+
catch {
|
|
958
|
+
return undefined;
|
|
959
|
+
}
|
|
960
|
+
}
|
|
830
961
|
function renderRemoteMarkdown(entry, resolvedUrl, contentType, content, extractionMethod = "direct-fetch") {
|
|
831
962
|
return [
|
|
832
963
|
"---",
|
|
@@ -855,7 +986,33 @@ function renderRemoteMarkdown(entry, resolvedUrl, contentType, content, extracti
|
|
|
855
986
|
"",
|
|
856
987
|
].filter((line) => typeof line === "string").join("\n");
|
|
857
988
|
}
|
|
858
|
-
function
|
|
989
|
+
function sharedSnapshotProvenance(target, config, entry, entries) {
|
|
990
|
+
const snapshotPath = resolve(target, resolveSourceSnapshot(config, entry));
|
|
991
|
+
const shared = entries.filter((candidate) => resolve(target, resolveSourceSnapshot(config, candidate)) === snapshotPath);
|
|
992
|
+
if (shared.length <= 1)
|
|
993
|
+
return undefined;
|
|
994
|
+
return {
|
|
995
|
+
sourceKeys: shared.map((candidate) => candidate.id),
|
|
996
|
+
sourcePaths: shared.map((candidate) => candidate.path),
|
|
997
|
+
sourceAliases: uniqueStrings(shared.map((candidate) => candidate.provider).filter(Boolean)),
|
|
998
|
+
sourceTypes: uniqueStrings(shared.map(sourceType).filter((value) => Boolean(value))),
|
|
999
|
+
};
|
|
1000
|
+
}
|
|
1001
|
+
function sourceType(entry) {
|
|
1002
|
+
return typeof entry.source.type === "string" && entry.source.type.trim() ? entry.source.type : undefined;
|
|
1003
|
+
}
|
|
1004
|
+
function uniqueStrings(values) {
|
|
1005
|
+
return Array.from(new Set(values));
|
|
1006
|
+
}
|
|
1007
|
+
function yamlStringList(key, values) {
|
|
1008
|
+
if (values.length === 0)
|
|
1009
|
+
return [];
|
|
1010
|
+
return [
|
|
1011
|
+
`${key}:`,
|
|
1012
|
+
...values.map((value) => ` - ${JSON.stringify(value)}`),
|
|
1013
|
+
];
|
|
1014
|
+
}
|
|
1015
|
+
function renderAdapterMarkdown(entry, resolvedOrigin, contentType, content, extractionMethod, adapter, metadata, sharedSnapshot) {
|
|
859
1016
|
const metadataLines = metadata && Object.keys(metadata).length > 0
|
|
860
1017
|
? [
|
|
861
1018
|
"## Adapter Metadata",
|
|
@@ -864,11 +1021,28 @@ function renderAdapterMarkdown(entry, resolvedOrigin, contentType, content, extr
|
|
|
864
1021
|
"",
|
|
865
1022
|
]
|
|
866
1023
|
: [];
|
|
1024
|
+
const sharedFrontmatter = sharedSnapshot
|
|
1025
|
+
? [
|
|
1026
|
+
`snapshotProvenance: "shared-config-snapshot"`,
|
|
1027
|
+
...yamlStringList("sourceKeys", sharedSnapshot.sourceKeys),
|
|
1028
|
+
...yamlStringList("sourcePaths", sharedSnapshot.sourcePaths),
|
|
1029
|
+
...yamlStringList("sourceAliases", sharedSnapshot.sourceAliases),
|
|
1030
|
+
...yamlStringList("sourceTypes", sharedSnapshot.sourceTypes),
|
|
1031
|
+
]
|
|
1032
|
+
: [];
|
|
1033
|
+
const sharedProvenance = sharedSnapshot
|
|
1034
|
+
? [
|
|
1035
|
+
`- Shared snapshot: ${sharedSnapshot.sourceKeys.length} configured sources`,
|
|
1036
|
+
`- Source keys: ${sharedSnapshot.sourceKeys.map((key) => `\`${key}\``).join(", ")}`,
|
|
1037
|
+
`- Source aliases: ${sharedSnapshot.sourceAliases.map((alias) => `\`${alias}\``).join(", ")}`,
|
|
1038
|
+
]
|
|
1039
|
+
: [];
|
|
867
1040
|
return [
|
|
868
1041
|
"---",
|
|
869
1042
|
`sourceKey: ${JSON.stringify(entry.id)}`,
|
|
870
1043
|
`sourcePath: ${JSON.stringify(entry.path)}`,
|
|
871
1044
|
entry.provider !== entry.id ? `sourceAlias: ${JSON.stringify(entry.provider)}` : undefined,
|
|
1045
|
+
...sharedFrontmatter,
|
|
872
1046
|
`origin: ${JSON.stringify(resolvedOrigin)}`,
|
|
873
1047
|
`contentType: ${JSON.stringify(contentType)}`,
|
|
874
1048
|
`adapter: ${JSON.stringify(adapter)}`,
|
|
@@ -883,6 +1057,7 @@ function renderAdapterMarkdown(entry, resolvedOrigin, contentType, content, extr
|
|
|
883
1057
|
"",
|
|
884
1058
|
`- Source key: \`${entry.id}\``,
|
|
885
1059
|
entry.provider !== entry.id ? `- Source alias: \`${entry.provider}\`` : undefined,
|
|
1060
|
+
...sharedProvenance,
|
|
886
1061
|
`- Resolved origin: \`${resolvedOrigin}\``,
|
|
887
1062
|
`- Adapter: ${adapter}`,
|
|
888
1063
|
`- Method: ${extractionMethod}`,
|
|
@@ -903,6 +1078,75 @@ function confluenceBodyValue(value) {
|
|
|
903
1078
|
const view = recordValue(body, "view");
|
|
904
1079
|
return stringValue(storage, "value") ?? stringValue(view, "value") ?? stringValue(value, "content") ?? stringValue(value, "value");
|
|
905
1080
|
}
|
|
1081
|
+
function parseConfluenceWordExport(body, contentType) {
|
|
1082
|
+
if (!/multipart\/related|application\/vnd\.ms-word|MIME-Version:/i.test(`${contentType}\n${body.slice(0, 512)}`)) {
|
|
1083
|
+
return undefined;
|
|
1084
|
+
}
|
|
1085
|
+
const rootHeadersEnd = body.search(/\r?\n\r?\n/);
|
|
1086
|
+
if (rootHeadersEnd === -1)
|
|
1087
|
+
return undefined;
|
|
1088
|
+
const rootHeaders = parseMimeHeaders(body.slice(0, rootHeadersEnd));
|
|
1089
|
+
const boundary = /boundary="?([^";\r\n]+)"?/i.exec(rootHeaders["content-type"] ?? "")?.[1];
|
|
1090
|
+
if (!boundary)
|
|
1091
|
+
return undefined;
|
|
1092
|
+
const chunks = body.split(`--${boundary}`).slice(1);
|
|
1093
|
+
for (const chunk of chunks) {
|
|
1094
|
+
if (chunk.startsWith("--"))
|
|
1095
|
+
continue;
|
|
1096
|
+
const trimmed = chunk.replace(/^\r?\n/, "");
|
|
1097
|
+
const headerEnd = trimmed.search(/\r?\n\r?\n/);
|
|
1098
|
+
if (headerEnd === -1)
|
|
1099
|
+
continue;
|
|
1100
|
+
const headers = parseMimeHeaders(trimmed.slice(0, headerEnd));
|
|
1101
|
+
if (!/text\/html/i.test(headers["content-type"] ?? ""))
|
|
1102
|
+
continue;
|
|
1103
|
+
const html = decodeMimePartBody(trimmed.slice(headerEnd).replace(/^\r?\n\r?\n?/, ""), headers);
|
|
1104
|
+
return {
|
|
1105
|
+
html,
|
|
1106
|
+
title: decodeHtmlEntities(stripHtml(html.match(/<title[^>]*>([\s\S]*?)<\/title>/i)?.[1] ?? "")) || undefined,
|
|
1107
|
+
};
|
|
1108
|
+
}
|
|
1109
|
+
return undefined;
|
|
1110
|
+
}
|
|
1111
|
+
function parseMimeHeaders(text) {
|
|
1112
|
+
const headers = {};
|
|
1113
|
+
let current;
|
|
1114
|
+
for (const line of text.split(/\r?\n/)) {
|
|
1115
|
+
if (/^\s/.test(line) && current) {
|
|
1116
|
+
headers[current] += ` ${line.trim()}`;
|
|
1117
|
+
continue;
|
|
1118
|
+
}
|
|
1119
|
+
const index = line.indexOf(":");
|
|
1120
|
+
if (index === -1)
|
|
1121
|
+
continue;
|
|
1122
|
+
current = line.slice(0, index).toLowerCase();
|
|
1123
|
+
headers[current] = line.slice(index + 1).trim();
|
|
1124
|
+
}
|
|
1125
|
+
return headers;
|
|
1126
|
+
}
|
|
1127
|
+
function decodeMimePartBody(body, headers) {
|
|
1128
|
+
const encoding = (headers["content-transfer-encoding"] ?? "").toLowerCase();
|
|
1129
|
+
if (encoding.includes("quoted-printable"))
|
|
1130
|
+
return decodeQuotedPrintable(body);
|
|
1131
|
+
if (encoding.includes("base64"))
|
|
1132
|
+
return Buffer.from(body.replace(/\s+/g, ""), "base64").toString("utf8");
|
|
1133
|
+
return body;
|
|
1134
|
+
}
|
|
1135
|
+
function decodeQuotedPrintable(input) {
|
|
1136
|
+
const normalized = input.replace(/=\r?\n/g, "");
|
|
1137
|
+
const bytes = [];
|
|
1138
|
+
for (let i = 0; i < normalized.length; i += 1) {
|
|
1139
|
+
if (normalized[i] === "=" && /^[0-9a-fA-F]{2}$/.test(normalized.slice(i + 1, i + 3))) {
|
|
1140
|
+
bytes.push(parseInt(normalized.slice(i + 1, i + 3), 16));
|
|
1141
|
+
i += 2;
|
|
1142
|
+
continue;
|
|
1143
|
+
}
|
|
1144
|
+
const char = normalized[i];
|
|
1145
|
+
if (char)
|
|
1146
|
+
bytes.push(...Buffer.from(char, "utf8"));
|
|
1147
|
+
}
|
|
1148
|
+
return Buffer.from(bytes).toString("utf8");
|
|
1149
|
+
}
|
|
906
1150
|
function versionValue(value) {
|
|
907
1151
|
const version = recordValue(value, "version");
|
|
908
1152
|
const number = version?.number;
|
|
@@ -1041,13 +1285,230 @@ function designNodeNames(value) {
|
|
|
1041
1285
|
const nested = Array.isArray(children) ? children.flatMap(designNodeNames) : [];
|
|
1042
1286
|
return current ? [current, ...nested] : nested;
|
|
1043
1287
|
}
|
|
1044
|
-
function designProviderGapMetadata(reason) {
|
|
1288
|
+
function designProviderGapMetadata(reason, source, figma) {
|
|
1289
|
+
const mcpProvider = source ? interpretSourceAdapter(source).mcpProviderName : undefined;
|
|
1045
1290
|
return {
|
|
1046
1291
|
providerGap: reason,
|
|
1047
1292
|
fallbackEvidenceRequired: true,
|
|
1293
|
+
mcpProvider,
|
|
1294
|
+
derivedApiUrl: figma?.apiUrl,
|
|
1295
|
+
fileKey: figma?.fileKey,
|
|
1296
|
+
nodeId: figma?.nodeId,
|
|
1297
|
+
nextActions: [
|
|
1298
|
+
"configure a local exportPath/assetPath/filePath",
|
|
1299
|
+
"configure origin.mcpCommand + origin.mcpArgs for a Figma MCP capture command",
|
|
1300
|
+
"use Playwright/browser acquisition with --browser when HTML rendering is sufficient",
|
|
1301
|
+
"capture Computer Use or human visual evidence outside the CLI and attach it as a raw source",
|
|
1302
|
+
].join("; "),
|
|
1048
1303
|
reconstructionRisk: "semantic UE reconstruction needs provider raw payload, assets/raw files, reconstruction.json, and known gaps before downstream design use",
|
|
1049
1304
|
};
|
|
1050
1305
|
}
|
|
1306
|
+
function deriveConfluenceRestUrl(source) {
|
|
1307
|
+
const pageId = firstOriginString(source, "pageId", "contentId", "id") ?? confluencePageIdFromUrl(firstOriginString(source, "url"));
|
|
1308
|
+
if (!pageId)
|
|
1309
|
+
return undefined;
|
|
1310
|
+
const base = confluenceBaseUrl(source);
|
|
1311
|
+
if (!base)
|
|
1312
|
+
return undefined;
|
|
1313
|
+
return `${base}/rest/api/content/${encodeURIComponent(pageId)}?expand=body.storage,version`;
|
|
1314
|
+
}
|
|
1315
|
+
function confluencePageIdFromUrl(value) {
|
|
1316
|
+
if (!value)
|
|
1317
|
+
return undefined;
|
|
1318
|
+
try {
|
|
1319
|
+
const url = new URL(value);
|
|
1320
|
+
return /\/pages\/(\d+)/.exec(url.pathname)?.[1] ?? undefined;
|
|
1321
|
+
}
|
|
1322
|
+
catch {
|
|
1323
|
+
return /\/pages\/(\d+)/.exec(value)?.[1] ?? undefined;
|
|
1324
|
+
}
|
|
1325
|
+
}
|
|
1326
|
+
function confluenceBaseUrl(source) {
|
|
1327
|
+
const explicit = firstOriginString(source, "baseUrl", "siteUrl", "host");
|
|
1328
|
+
if (explicit)
|
|
1329
|
+
return trimTrailingSlash(explicit);
|
|
1330
|
+
const pageUrl = firstOriginString(source, "url");
|
|
1331
|
+
if (!pageUrl)
|
|
1332
|
+
return undefined;
|
|
1333
|
+
try {
|
|
1334
|
+
const url = new URL(pageUrl);
|
|
1335
|
+
const wikiPrefix = url.pathname.startsWith("/wiki/") ? "/wiki" : "";
|
|
1336
|
+
return `${url.protocol}//${url.host}${wikiPrefix}`;
|
|
1337
|
+
}
|
|
1338
|
+
catch {
|
|
1339
|
+
return undefined;
|
|
1340
|
+
}
|
|
1341
|
+
}
|
|
1342
|
+
function deriveJiraRestUrl(source) {
|
|
1343
|
+
const explicitIssue = firstOriginString(source, "issueKey", "key", "id") ?? jiraIssueKeyFromUrl(firstOriginString(source, "url"));
|
|
1344
|
+
const query = firstOriginString(source, "jql", "query");
|
|
1345
|
+
const base = jiraBaseUrl(source);
|
|
1346
|
+
if (!base)
|
|
1347
|
+
return undefined;
|
|
1348
|
+
if (explicitIssue)
|
|
1349
|
+
return `${base}/rest/api/3/issue/${encodeURIComponent(explicitIssue)}`;
|
|
1350
|
+
if (query)
|
|
1351
|
+
return `${base}/rest/api/3/search?jql=${encodeURIComponent(query)}`;
|
|
1352
|
+
return undefined;
|
|
1353
|
+
}
|
|
1354
|
+
function jiraIssueKeyFromUrl(value) {
|
|
1355
|
+
if (!value)
|
|
1356
|
+
return undefined;
|
|
1357
|
+
try {
|
|
1358
|
+
const url = new URL(value);
|
|
1359
|
+
return /\/browse\/([A-Z][A-Z0-9_]+-\d+)/i.exec(url.pathname)?.[1] ?? undefined;
|
|
1360
|
+
}
|
|
1361
|
+
catch {
|
|
1362
|
+
return /\/browse\/([A-Z][A-Z0-9_]+-\d+)/i.exec(value)?.[1] ?? undefined;
|
|
1363
|
+
}
|
|
1364
|
+
}
|
|
1365
|
+
function jiraBaseUrl(source) {
|
|
1366
|
+
const explicit = firstOriginString(source, "baseUrl", "siteUrl", "host");
|
|
1367
|
+
if (explicit)
|
|
1368
|
+
return trimTrailingSlash(explicit);
|
|
1369
|
+
const urlValue = firstOriginString(source, "url");
|
|
1370
|
+
if (!urlValue)
|
|
1371
|
+
return undefined;
|
|
1372
|
+
try {
|
|
1373
|
+
const url = new URL(urlValue);
|
|
1374
|
+
return `${url.protocol}//${url.host}`;
|
|
1375
|
+
}
|
|
1376
|
+
catch {
|
|
1377
|
+
return undefined;
|
|
1378
|
+
}
|
|
1379
|
+
}
|
|
1380
|
+
function figmaLocatorForSource(source) {
|
|
1381
|
+
if (!isFigmaSource(source))
|
|
1382
|
+
return undefined;
|
|
1383
|
+
const sourceUrl = firstOriginString(source, "url");
|
|
1384
|
+
const fileKey = firstOriginString(source, "fileKey", "file_key", "fileId", "file_id", "id") ?? figmaFileKeyFromUrl(sourceUrl);
|
|
1385
|
+
const nodeId = normalizeFigmaNodeId(firstOriginString(source, "nodeId", "node_id") ?? figmaNodeIdFromUrl(sourceUrl));
|
|
1386
|
+
const explicitApiUrl = firstOriginString(source, "apiUrl", "exportUrl");
|
|
1387
|
+
const apiUrl = explicitApiUrl ?? (fileKey ? figmaApiUrl(fileKey, nodeId) : undefined);
|
|
1388
|
+
return cleanMetadata({ sourceUrl, fileKey, nodeId, apiUrl });
|
|
1389
|
+
}
|
|
1390
|
+
function isFigmaSource(source) {
|
|
1391
|
+
return [
|
|
1392
|
+
source.origin?.kind,
|
|
1393
|
+
source.origin?.provider,
|
|
1394
|
+
source.kind,
|
|
1395
|
+
source.type,
|
|
1396
|
+
firstOriginString(source, "mcpProvider", "mcpServer"),
|
|
1397
|
+
firstOriginString(source, "url"),
|
|
1398
|
+
]
|
|
1399
|
+
.filter((value) => typeof value === "string")
|
|
1400
|
+
.some((value) => value.toLowerCase().includes("figma"));
|
|
1401
|
+
}
|
|
1402
|
+
function figmaFileKeyFromUrl(value) {
|
|
1403
|
+
if (!value)
|
|
1404
|
+
return undefined;
|
|
1405
|
+
try {
|
|
1406
|
+
const url = new URL(value);
|
|
1407
|
+
return /\/(?:file|design)\/([^/?#]+)/i.exec(url.pathname)?.[1] ?? undefined;
|
|
1408
|
+
}
|
|
1409
|
+
catch {
|
|
1410
|
+
return /\/(?:file|design)\/([^/?#]+)/i.exec(value)?.[1] ?? undefined;
|
|
1411
|
+
}
|
|
1412
|
+
}
|
|
1413
|
+
function figmaNodeIdFromUrl(value) {
|
|
1414
|
+
if (!value)
|
|
1415
|
+
return undefined;
|
|
1416
|
+
try {
|
|
1417
|
+
return new URL(value).searchParams.get("node-id") ?? undefined;
|
|
1418
|
+
}
|
|
1419
|
+
catch {
|
|
1420
|
+
return /[?&]node-id=([^&#]+)/i.exec(value)?.[1] ?? undefined;
|
|
1421
|
+
}
|
|
1422
|
+
}
|
|
1423
|
+
function normalizeFigmaNodeId(value) {
|
|
1424
|
+
if (!value)
|
|
1425
|
+
return undefined;
|
|
1426
|
+
const decoded = decodeURIComponent(value.trim());
|
|
1427
|
+
return decoded.includes(":") ? decoded : decoded.replace("-", ":");
|
|
1428
|
+
}
|
|
1429
|
+
function figmaApiUrl(fileKey, nodeId) {
|
|
1430
|
+
const encodedFileKey = encodeURIComponent(fileKey);
|
|
1431
|
+
if (nodeId)
|
|
1432
|
+
return `https://api.figma.com/v1/files/${encodedFileKey}/nodes?ids=${encodeURIComponent(nodeId)}`;
|
|
1433
|
+
return `https://api.figma.com/v1/files/${encodedFileKey}`;
|
|
1434
|
+
}
|
|
1435
|
+
function figmaCommandForSource(source, target, snapshot, figma) {
|
|
1436
|
+
const command = firstOriginString(source, "mcpCommand", "command", "exportCommand");
|
|
1437
|
+
if (!command)
|
|
1438
|
+
return undefined;
|
|
1439
|
+
const args = originStringList(source, "mcpArgs", "args", "commandArgs")
|
|
1440
|
+
.map((arg) => renderCommandTemplate(arg, source, snapshot, figma));
|
|
1441
|
+
const provider = interpretSourceAdapter(source).mcpProviderName;
|
|
1442
|
+
const contentType = firstOriginString(source, "commandContentType", "contentType") ?? "application/json";
|
|
1443
|
+
const timeoutMs = numberOriginValue(source, "timeoutMs") ?? DEFAULT_ADAPTER_COMMAND_TIMEOUT_MS;
|
|
1444
|
+
const resolvedCommand = resolveCommandForTarget(target, command);
|
|
1445
|
+
return {
|
|
1446
|
+
command: resolvedCommand,
|
|
1447
|
+
args,
|
|
1448
|
+
provider,
|
|
1449
|
+
safeLabel: `${command} (${args.length} args)`,
|
|
1450
|
+
contentType,
|
|
1451
|
+
timeoutMs,
|
|
1452
|
+
};
|
|
1453
|
+
}
|
|
1454
|
+
async function runDesignCommand(target, command) {
|
|
1455
|
+
try {
|
|
1456
|
+
const { stdout } = await execFileAsync(command.command, command.args, {
|
|
1457
|
+
cwd: target,
|
|
1458
|
+
env: process.env,
|
|
1459
|
+
timeout: command.timeoutMs,
|
|
1460
|
+
maxBuffer: DEFAULT_ADAPTER_COMMAND_MAX_BUFFER,
|
|
1461
|
+
});
|
|
1462
|
+
const body = stdout.trim();
|
|
1463
|
+
if (!body)
|
|
1464
|
+
return { ok: false, reason: "configured design command exited without stdout content" };
|
|
1465
|
+
return {
|
|
1466
|
+
ok: true,
|
|
1467
|
+
body,
|
|
1468
|
+
contentType: command.contentType,
|
|
1469
|
+
origin: `command:${command.safeLabel}`,
|
|
1470
|
+
};
|
|
1471
|
+
}
|
|
1472
|
+
catch (error) {
|
|
1473
|
+
const code = isRecord(error) && (typeof error.code === "number" || typeof error.code === "string") ? String(error.code) : "unknown";
|
|
1474
|
+
return {
|
|
1475
|
+
ok: false,
|
|
1476
|
+
reason: `configured design command failed without exposing stdout/stderr content (code: ${code})`,
|
|
1477
|
+
};
|
|
1478
|
+
}
|
|
1479
|
+
}
|
|
1480
|
+
function originStringList(source, ...keys) {
|
|
1481
|
+
const origin = source.origin;
|
|
1482
|
+
const values = [];
|
|
1483
|
+
for (const key of keys) {
|
|
1484
|
+
values.push(...stringListValue(origin?.[key]));
|
|
1485
|
+
}
|
|
1486
|
+
return values;
|
|
1487
|
+
}
|
|
1488
|
+
function numberOriginValue(source, key) {
|
|
1489
|
+
const value = source.origin?.[key];
|
|
1490
|
+
return typeof value === "number" && Number.isFinite(value) && value > 0 ? value : undefined;
|
|
1491
|
+
}
|
|
1492
|
+
function renderCommandTemplate(value, source, snapshot, figma) {
|
|
1493
|
+
const replacements = {
|
|
1494
|
+
url: firstOriginString(source, "url") ?? "",
|
|
1495
|
+
fileKey: figma?.fileKey ?? "",
|
|
1496
|
+
file_key: figma?.fileKey ?? "",
|
|
1497
|
+
nodeId: figma?.nodeId ?? "",
|
|
1498
|
+
node_id: figma?.nodeId ?? "",
|
|
1499
|
+
apiUrl: figma?.apiUrl ?? "",
|
|
1500
|
+
snapshot,
|
|
1501
|
+
};
|
|
1502
|
+
return value.replace(/\{\{\s*([A-Za-z0-9_]+)\s*\}\}/g, (_, key) => replacements[key] ?? "");
|
|
1503
|
+
}
|
|
1504
|
+
function resolveCommandForTarget(target, command) {
|
|
1505
|
+
if (command.includes("/") || command.includes("\\"))
|
|
1506
|
+
return resolve(target, command);
|
|
1507
|
+
return command;
|
|
1508
|
+
}
|
|
1509
|
+
function trimTrailingSlash(value) {
|
|
1510
|
+
return value.replace(/\/+$/, "");
|
|
1511
|
+
}
|
|
1051
1512
|
function parseCsvRows(value) {
|
|
1052
1513
|
const rows = parseCsv(value).filter((row) => row.some((cell) => cell.trim().length > 0));
|
|
1053
1514
|
const headers = rows.shift() ?? [];
|
|
@@ -1138,10 +1599,11 @@ function firstCsvValue(row, ...keys) {
|
|
|
1138
1599
|
}
|
|
1139
1600
|
function firstOriginString(source, ...keys) {
|
|
1140
1601
|
const origin = source.origin;
|
|
1141
|
-
if (!origin)
|
|
1142
|
-
return undefined;
|
|
1143
1602
|
for (const key of keys) {
|
|
1144
|
-
const
|
|
1603
|
+
const sourceValue = source[key];
|
|
1604
|
+
if (typeof sourceValue === "string" && sourceValue.trim().length > 0)
|
|
1605
|
+
return sourceValue.trim();
|
|
1606
|
+
const value = origin?.[key];
|
|
1145
1607
|
if (typeof value === "string" && value.trim().length > 0)
|
|
1146
1608
|
return value.trim();
|
|
1147
1609
|
}
|
|
@@ -1162,9 +1624,13 @@ function authStringList(source, ...keys) {
|
|
|
1162
1624
|
for (const key of keys) {
|
|
1163
1625
|
values.push(...stringListValue(auth?.[key]));
|
|
1164
1626
|
}
|
|
1627
|
+
if (keys.some((key) => ["tokenEnv", "accessTokenEnv", "apiTokenEnv"].includes(key))) {
|
|
1628
|
+
values.push(...stringListValue(auth?.env));
|
|
1629
|
+
}
|
|
1165
1630
|
for (const key of keys) {
|
|
1166
1631
|
values.push(...stringListValue(source.origin?.[key]));
|
|
1167
1632
|
}
|
|
1633
|
+
values.push(...defaultAuthEnvNames(source, keys));
|
|
1168
1634
|
return Array.from(new Set(values));
|
|
1169
1635
|
}
|
|
1170
1636
|
function authCookieMap(source) {
|
|
@@ -1199,6 +1665,77 @@ function envValue(names) {
|
|
|
1199
1665
|
}
|
|
1200
1666
|
return {};
|
|
1201
1667
|
}
|
|
1668
|
+
function basicAuthHeaderFromEnvironment(source) {
|
|
1669
|
+
const username = envValue(authStringList(source, "usernameEnv", "userEnv", "basicUsernameEnv", "basicUserEnv"));
|
|
1670
|
+
const password = envValue(authStringList(source, "passwordEnv", "passEnv", "basicPasswordEnv", "basicPassEnv"));
|
|
1671
|
+
if (!username.value || !password.value)
|
|
1672
|
+
return { sources: [] };
|
|
1673
|
+
return {
|
|
1674
|
+
sources: [username.name, password.name].filter((name) => Boolean(name)),
|
|
1675
|
+
value: `Basic ${Buffer.from(`${username.value}:${password.value}`).toString("base64")}`,
|
|
1676
|
+
};
|
|
1677
|
+
}
|
|
1678
|
+
function defaultAuthEnvNames(source, keys) {
|
|
1679
|
+
const provider = sourceProviderToken(source);
|
|
1680
|
+
if (!provider)
|
|
1681
|
+
return [];
|
|
1682
|
+
if (keys.some((key) => ["tokenEnv", "accessTokenEnv", "apiTokenEnv"].includes(key))) {
|
|
1683
|
+
if (provider.includes("figma"))
|
|
1684
|
+
return ["FIGMA_ACCESS_TOKEN", "FIGMA_TOKEN", "FIGMA_API_TOKEN"];
|
|
1685
|
+
if (provider.includes("gitlab"))
|
|
1686
|
+
return ["GITLAB_TOKEN", "GITLAB_ACCESS_TOKEN", "GITLAB_PRIVATE_TOKEN"];
|
|
1687
|
+
if (provider.includes("jira"))
|
|
1688
|
+
return ["ACCESS_TOKEN", "JIRA_ACCESS_TOKEN", "JIRA_TOKEN"];
|
|
1689
|
+
if (provider.includes("confluence"))
|
|
1690
|
+
return ["CONFLUENCE_ACCESS_TOKEN", "CONFLUENCE_TOKEN"];
|
|
1691
|
+
}
|
|
1692
|
+
if (keys.some((key) => ["usernameEnv", "userEnv", "basicUsernameEnv", "basicUserEnv"].includes(key))) {
|
|
1693
|
+
if (provider.includes("jira"))
|
|
1694
|
+
return ["JIRA_USER", "JIRA_USERNAME", "ATLASSIAN_EMAIL", "ATLASSIAN_USER"];
|
|
1695
|
+
if (provider.includes("confluence"))
|
|
1696
|
+
return ["CONFLUENCE_USER", "CONFLUENCE_USERNAME", "ATLASSIAN_EMAIL", "ATLASSIAN_USER"];
|
|
1697
|
+
}
|
|
1698
|
+
if (keys.some((key) => ["passwordEnv", "passEnv", "basicPasswordEnv", "basicPassEnv"].includes(key))) {
|
|
1699
|
+
if (provider.includes("jira"))
|
|
1700
|
+
return ["JIRA_API_TOKEN", "JIRA_PASSWORD", "ATLASSIAN_API_TOKEN", "ATLASSIAN_TOKEN"];
|
|
1701
|
+
if (provider.includes("confluence"))
|
|
1702
|
+
return ["CONFLUENCE_API_TOKEN", "CONFLUENCE_PASSWORD", "ATLASSIAN_API_TOKEN", "ATLASSIAN_TOKEN"];
|
|
1703
|
+
}
|
|
1704
|
+
return [];
|
|
1705
|
+
}
|
|
1706
|
+
function defaultTokenHeader(source) {
|
|
1707
|
+
const provider = sourceProviderToken(source);
|
|
1708
|
+
if (provider?.includes("figma"))
|
|
1709
|
+
return "X-Figma-Token";
|
|
1710
|
+
if (provider?.includes("gitlab"))
|
|
1711
|
+
return "PRIVATE-TOKEN";
|
|
1712
|
+
return "authorization";
|
|
1713
|
+
}
|
|
1714
|
+
function defaultTokenScheme(source, headerName) {
|
|
1715
|
+
const provider = sourceProviderToken(source);
|
|
1716
|
+
if (headerName.toLowerCase() !== "authorization")
|
|
1717
|
+
return "";
|
|
1718
|
+
if (provider?.includes("figma") || provider?.includes("gitlab"))
|
|
1719
|
+
return "";
|
|
1720
|
+
return "Bearer";
|
|
1721
|
+
}
|
|
1722
|
+
function sourceProviderToken(source) {
|
|
1723
|
+
const origin = inferSourceOrigin(source);
|
|
1724
|
+
return [
|
|
1725
|
+
origin.provider,
|
|
1726
|
+
origin.kind,
|
|
1727
|
+
source.origin?.provider,
|
|
1728
|
+
source.origin?.kind,
|
|
1729
|
+
source.kind,
|
|
1730
|
+
source.type,
|
|
1731
|
+
]
|
|
1732
|
+
.filter((value) => typeof value === "string" && value.trim().length > 0)
|
|
1733
|
+
.map((value) => value.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, ""))
|
|
1734
|
+
.find((value) => value.includes("figma") ||
|
|
1735
|
+
value.includes("jira") ||
|
|
1736
|
+
value.includes("confluence") ||
|
|
1737
|
+
value.includes("gitlab"));
|
|
1738
|
+
}
|
|
1202
1739
|
function cookieHeaderFromEnvironment(source) {
|
|
1203
1740
|
const parts = [];
|
|
1204
1741
|
const sources = [];
|
|
@@ -1253,15 +1790,6 @@ function contentTypeForPath(path) {
|
|
|
1253
1790
|
return "text/html";
|
|
1254
1791
|
return "text/plain";
|
|
1255
1792
|
}
|
|
1256
|
-
function escapeTable(value) {
|
|
1257
|
-
return value.replace(/\|/g, "\\|").replace(/\n/g, " ");
|
|
1258
|
-
}
|
|
1259
|
-
function isRecord(value) {
|
|
1260
|
-
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
1261
|
-
}
|
|
1262
|
-
function normalizeAdapterKey(value) {
|
|
1263
|
-
return value?.trim().toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "") ?? "";
|
|
1264
|
-
}
|
|
1265
1793
|
function renderRepositoryMetadata(entry, repository) {
|
|
1266
1794
|
return [
|
|
1267
1795
|
"---",
|
|
@@ -1295,6 +1823,7 @@ function htmlToStructuredMarkdown(value) {
|
|
|
1295
1823
|
.replace(/<style[\s\S]*?<\/style>/gi, "")
|
|
1296
1824
|
.replace(/<(nav|header|footer|aside)[^>]*>[\s\S]*?<\/\1>/gi, "");
|
|
1297
1825
|
return body
|
|
1826
|
+
.replace(/<table\b[^>]*>[\s\S]*?<\/table>/gi, (table) => `\n${htmlTableToMarkdown(table)}\n`)
|
|
1298
1827
|
.replace(/<h1[^>]*>([\s\S]*?)<\/h1>/gi, "\n# $1\n")
|
|
1299
1828
|
.replace(/<h2[^>]*>([\s\S]*?)<\/h2>/gi, "\n## $1\n")
|
|
1300
1829
|
.replace(/<h3[^>]*>([\s\S]*?)<\/h3>/gi, "\n### $1\n")
|
|
@@ -1305,14 +1834,60 @@ function htmlToStructuredMarkdown(value) {
|
|
|
1305
1834
|
.replace(/<a\b[^>]*href=["']([^"']+)["'][^>]*>([\s\S]*?)<\/a>/gi, "[$2]($1)")
|
|
1306
1835
|
.replace(/<[^>]+>/g, "")
|
|
1307
1836
|
.replace(/ /g, " ")
|
|
1837
|
+
.split("\n")
|
|
1838
|
+
.map((line) => decodeHtmlEntities(line).trim())
|
|
1839
|
+
.filter((line, index, lines) => line.length > 0 || lines[index - 1]?.length)
|
|
1840
|
+
.join("\n");
|
|
1841
|
+
}
|
|
1842
|
+
function htmlTableToMarkdown(table) {
|
|
1843
|
+
const rows = Array.from(table.matchAll(/<tr\b[^>]*>([\s\S]*?)<\/tr>/gi))
|
|
1844
|
+
.map((row) => Array.from((row[1] ?? "").matchAll(/<(td|th)\b[^>]*>([\s\S]*?)<\/\1>/gi))
|
|
1845
|
+
.map((cell) => tableCellText(cell[2] ?? "")))
|
|
1846
|
+
.filter((row) => row.some((cell) => cell.length > 0));
|
|
1847
|
+
if (!rows.length)
|
|
1848
|
+
return "";
|
|
1849
|
+
const width = Math.max(...rows.map((row) => row.length));
|
|
1850
|
+
const normalized = rows.map((row) => {
|
|
1851
|
+
const copy = row.slice();
|
|
1852
|
+
while (copy.length < width)
|
|
1853
|
+
copy.push("");
|
|
1854
|
+
return copy;
|
|
1855
|
+
});
|
|
1856
|
+
const header = normalized[0] ?? [];
|
|
1857
|
+
if (!header.length)
|
|
1858
|
+
return "";
|
|
1859
|
+
const body = normalized.slice(1);
|
|
1860
|
+
return [
|
|
1861
|
+
`| ${header.join(" | ")} |`,
|
|
1862
|
+
`| ${header.map(() => "---").join(" | ")} |`,
|
|
1863
|
+
...body.map((row) => `| ${row.join(" | ")} |`),
|
|
1864
|
+
].join("\n");
|
|
1865
|
+
}
|
|
1866
|
+
function tableCellText(value) {
|
|
1867
|
+
return decodeHtmlEntities(stripHtml(value
|
|
1868
|
+
.replace(/<br\s*\/?>/gi, " ZSK_BR ")
|
|
1869
|
+
.replace(/<\/(p|div|section|li)>/gi, " ZSK_BR ")
|
|
1870
|
+
.replace(/<li\b[^>]*>/gi, "- ")))
|
|
1871
|
+
.replace(/\s*ZSK_BR\s*/g, "; ")
|
|
1872
|
+
.replace(/[ \t\r\n]+/g, " ")
|
|
1873
|
+
.replace(/(?:\s*;\s*){2,}/g, "; ")
|
|
1874
|
+
.replace(/^\s*;\s*|\s*;\s*$/g, "")
|
|
1875
|
+
.replace(/\|/g, "\\|")
|
|
1876
|
+
.trim();
|
|
1877
|
+
}
|
|
1878
|
+
function stripHtml(value) {
|
|
1879
|
+
return value.replace(/<[^>]+>/g, "");
|
|
1880
|
+
}
|
|
1881
|
+
function decodeHtmlEntities(value) {
|
|
1882
|
+
return value
|
|
1883
|
+
.replace(/&#(\d+);/g, (_, code) => String.fromCodePoint(Number(code)))
|
|
1884
|
+
.replace(/&#x([0-9a-fA-F]+);/g, (_, code) => String.fromCodePoint(parseInt(code, 16)))
|
|
1885
|
+
.replace(/ /g, " ")
|
|
1308
1886
|
.replace(/&/g, "&")
|
|
1309
1887
|
.replace(/</g, "<")
|
|
1310
1888
|
.replace(/>/g, ">")
|
|
1311
1889
|
.replace(/"/g, "\"")
|
|
1312
|
-
.
|
|
1313
|
-
.map((line) => line.trim())
|
|
1314
|
-
.filter((line, index, lines) => line.length > 0 || lines[index - 1]?.length)
|
|
1315
|
-
.join("\n");
|
|
1890
|
+
.replace(/'/g, "'");
|
|
1316
1891
|
}
|
|
1317
1892
|
function fencedBody(body, contentType) {
|
|
1318
1893
|
const info = contentType.includes("json")
|
|
@@ -1330,34 +1905,6 @@ function shouldPreserveMachineReadable(source, originPath, snapshotPath) {
|
|
|
1330
1905
|
[".json", ".yaml", ".yml"].includes(ext) ||
|
|
1331
1906
|
[".json", ".yaml", ".yml"].includes(snapshotExt));
|
|
1332
1907
|
}
|
|
1333
|
-
function chooseStrategy(method) {
|
|
1334
|
-
if (method === "local")
|
|
1335
|
-
return "local-copy-structured-markdown";
|
|
1336
|
-
if (method === "repository")
|
|
1337
|
-
return "repository-metadata-only";
|
|
1338
|
-
if (method === "url")
|
|
1339
|
-
return "playwright-auth-or-direct-fetch";
|
|
1340
|
-
return "confirm-acquisition-method";
|
|
1341
|
-
}
|
|
1342
|
-
function renderDownstreamImpact(results) {
|
|
1343
|
-
const changed = results.filter((result) => result.changed);
|
|
1344
|
-
const blocked = results.filter((result) => ["blocked-auth", "source-gap", "failed"].includes(result.status));
|
|
1345
|
-
return [
|
|
1346
|
-
"# Downstream Impact",
|
|
1347
|
-
"",
|
|
1348
|
-
`Changed sources: ${changed.length}`,
|
|
1349
|
-
`Blocked sources: ${blocked.length}`,
|
|
1350
|
-
"",
|
|
1351
|
-
changed.length === 0
|
|
1352
|
-
? "No downstream refresh is recommended from this sync run."
|
|
1353
|
-
: changed.map((result) => `- \`${result.sourcePath}\`: snapshot changed; review dependent proposal/spec/design/tasks/tests before claiming freshness.`).join("\n"),
|
|
1354
|
-
"",
|
|
1355
|
-
blocked.length === 0
|
|
1356
|
-
? "No blocked sources."
|
|
1357
|
-
: blocked.map((result) => `- \`${result.sourcePath}\` [${result.status}]: ${result.reason}`).join("\n"),
|
|
1358
|
-
"",
|
|
1359
|
-
].join("\n");
|
|
1360
|
-
}
|
|
1361
1908
|
async function cookieHeaderFromStorageState(storageStatePath, url) {
|
|
1362
1909
|
try {
|
|
1363
1910
|
const parsed = JSON.parse(await readFile(storageStatePath, "utf8"));
|
|
@@ -1441,74 +1988,46 @@ async function loadProjectPlaywright(target) {
|
|
|
1441
1988
|
}
|
|
1442
1989
|
return await import(resolved);
|
|
1443
1990
|
}
|
|
1444
|
-
async function
|
|
1991
|
+
async function safeStat(path) {
|
|
1445
1992
|
try {
|
|
1446
|
-
|
|
1993
|
+
const info = await stat(path);
|
|
1994
|
+
return { isDirectory: info.isDirectory(), isFile: info.isFile() };
|
|
1447
1995
|
}
|
|
1448
1996
|
catch {
|
|
1449
|
-
return
|
|
1997
|
+
return null;
|
|
1450
1998
|
}
|
|
1451
1999
|
}
|
|
1452
|
-
|
|
1453
|
-
const
|
|
1454
|
-
|
|
1455
|
-
|
|
1456
|
-
|
|
1457
|
-
|
|
1458
|
-
|
|
1459
|
-
|
|
1460
|
-
}
|
|
1461
|
-
catch {
|
|
1462
|
-
return false;
|
|
1463
|
-
}
|
|
2000
|
+
function isGitSubmoduleResource(source) {
|
|
2001
|
+
const values = [
|
|
2002
|
+
source.kind,
|
|
2003
|
+
source.type,
|
|
2004
|
+
source.origin?.kind,
|
|
2005
|
+
source.metadata?.resourceType,
|
|
2006
|
+
].map((value) => typeof value === "string" ? value.toLowerCase() : "");
|
|
2007
|
+
return values.some((value) => value === "git-submodule" || value === "submodule");
|
|
1464
2008
|
}
|
|
1465
|
-
async function
|
|
2009
|
+
async function updateGitSubmodule(target, sourcePath) {
|
|
1466
2010
|
try {
|
|
1467
|
-
|
|
2011
|
+
await execFileAsync("git", ["-C", target, "submodule", "update", "--init", "--remote", "--", sourcePath], {
|
|
2012
|
+
timeout: 120_000,
|
|
2013
|
+
maxBuffer: DEFAULT_ADAPTER_COMMAND_MAX_BUFFER,
|
|
2014
|
+
});
|
|
2015
|
+
return { ok: true };
|
|
1468
2016
|
}
|
|
1469
|
-
catch {
|
|
1470
|
-
|
|
2017
|
+
catch (error) {
|
|
2018
|
+
const reason = error instanceof Error ? error.message : String(error);
|
|
2019
|
+
return { ok: false, reason: `git submodule update failed for ${sourcePath}: ${reason}` };
|
|
1471
2020
|
}
|
|
1472
2021
|
}
|
|
1473
|
-
function safeFileName(value) {
|
|
1474
|
-
return value.toLowerCase().replace(/[^a-z0-9._-]+/g, "-").replace(/^-+|-+$/g, "") || "source";
|
|
1475
|
-
}
|
|
1476
|
-
function isMigrationLaneCandidate(value, staleNames) {
|
|
1477
|
-
return (staleNames.has(value) ||
|
|
1478
|
-
/^v?\d+(?:[._-]\d+)*$/i.test(value) ||
|
|
1479
|
-
/^(sprint|iteration|release)[-_]?\d*/i.test(value));
|
|
1480
|
-
}
|
|
1481
|
-
function resolveAuthStatePath(target, config, authState) {
|
|
1482
|
-
const statePath = resolve(target, authState);
|
|
1483
|
-
const sharedAuthRoot = resolve(target, getWorkspacePath(config, "playwrightRoot"), ".auth");
|
|
1484
|
-
const modulesRoot = resolve(target, getWorkspacePath(config, "modulesRoot"));
|
|
1485
|
-
const pathParts = statePath.split(/[\\/]/);
|
|
1486
|
-
const modulePrivate = isInside(modulesRoot, statePath) && pathParts.includes("_playwright") && pathParts.includes(".auth");
|
|
1487
|
-
if (isInside(sharedAuthRoot, statePath) || modulePrivate)
|
|
1488
|
-
return statePath;
|
|
1489
|
-
throw new Error(`Playwright storageState input must stay under ${sharedAuthRoot} or a module _playwright/.auth directory`);
|
|
1490
|
-
}
|
|
1491
2022
|
function resolveSelectedAuthStatePath(target, config, opts) {
|
|
1492
2023
|
if (opts.authState)
|
|
1493
|
-
return
|
|
2024
|
+
return resolvePlaywrightAuthStatePath(target, config, opts.authState);
|
|
1494
2025
|
if (opts.authProfile)
|
|
1495
|
-
return
|
|
2026
|
+
return resolvePlaywrightAuthProfilePath(target, config, opts.authProfile);
|
|
1496
2027
|
return undefined;
|
|
1497
2028
|
}
|
|
1498
|
-
function resolveAuthProfilePath(target, config, profile) {
|
|
1499
|
-
const authRoot = resolve(target, getWorkspacePath(config, "playwrightRoot"), ".auth");
|
|
1500
|
-
const fileName = profile ? `${safeFileName(profile)}.json` : "user_data.json";
|
|
1501
|
-
return join(authRoot, fileName);
|
|
1502
|
-
}
|
|
1503
2029
|
function resolveAdapterLocalFilePath(target, value) {
|
|
1504
2030
|
const path = resolve(target, value);
|
|
1505
|
-
return
|
|
1506
|
-
}
|
|
1507
|
-
function isInside(root, target) {
|
|
1508
|
-
const rel = relative(root, target);
|
|
1509
|
-
return rel === "" || (!rel.startsWith("..") && !isAbsolute(rel));
|
|
1510
|
-
}
|
|
1511
|
-
export function createSyncRunId(now = new Date()) {
|
|
1512
|
-
return now.toISOString().replace(/[:.]/g, "-");
|
|
2031
|
+
return isPathInside(resolve(target), path) ? path : undefined;
|
|
1513
2032
|
}
|
|
1514
2033
|
//# sourceMappingURL=prepare-sync.js.map
|