@ekanos/cli 0.17.1 → 0.19.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -0
- package/dist/commands/add.js +1 -1
- package/dist/commands/add.js.map +1 -1
- package/dist/commands/catalog.js +11 -7
- package/dist/commands/catalog.js.map +1 -1
- package/dist/commands/names.d.ts +19 -0
- package/dist/commands/names.js +122 -0
- package/dist/commands/names.js.map +1 -0
- package/dist/commands/publish.d.ts +53 -3
- package/dist/commands/publish.js +58 -16
- package/dist/commands/publish.js.map +1 -1
- package/dist/commands/sources.js +15 -5
- package/dist/commands/sources.js.map +1 -1
- package/dist/commands/status.d.ts +2 -0
- package/dist/commands/status.js +12 -12
- package/dist/commands/status.js.map +1 -1
- package/dist/commands/sync.js +2 -2
- package/dist/commands/sync.js.map +1 -1
- package/dist/commands/teams-connect.js +1 -1
- package/dist/commands/teams-connect.js.map +1 -1
- package/dist/experience/boundary.d.ts +2 -2
- package/dist/experience/boundary.js +2 -11
- package/dist/experience/boundary.js.map +1 -1
- package/dist/experience/build.d.ts +3 -1
- package/dist/experience/build.js +15 -26
- package/dist/experience/build.js.map +1 -1
- package/dist/experience/clean-room.d.ts +40 -0
- package/dist/experience/clean-room.js +150 -0
- package/dist/experience/clean-room.js.map +1 -0
- package/dist/experience/contract-sources.d.ts +7 -2
- package/dist/experience/contract-sources.js +15 -8
- package/dist/experience/contract-sources.js.map +1 -1
- package/dist/experience/definition-checks.d.ts +12 -1
- package/dist/experience/definition-checks.js +34 -11
- package/dist/experience/definition-checks.js.map +1 -1
- package/dist/experience/definition-shape.d.ts +20 -2
- package/dist/experience/definition-shape.js.map +1 -1
- package/dist/experience/project.js +8 -2
- package/dist/experience/project.js.map +1 -1
- package/dist/experience/publish.d.ts +42 -5
- package/dist/experience/publish.js +195 -11
- package/dist/experience/publish.js.map +1 -1
- package/dist/harness-scaffold.js +3 -1
- package/dist/harness-scaffold.js.map +1 -1
- package/dist/index.js +25 -2
- package/dist/index.js.map +1 -1
- package/dist/names-api.d.ts +90 -0
- package/dist/names-api.js +123 -0
- package/dist/names-api.js.map +1 -0
- package/dist/product-kinds.d.ts +13 -0
- package/dist/product-kinds.js +17 -0
- package/dist/product-kinds.js.map +1 -0
- package/dist/publish-api.d.ts +38 -5
- package/dist/publish-api.js +59 -16
- package/dist/publish-api.js.map +1 -1
- package/dist/repo/publish.d.ts +6 -3
- package/dist/repo/publish.js +48 -14
- package/dist/repo/publish.js.map +1 -1
- package/dist/source-products-api.d.ts +3 -2
- package/dist/source-products-api.js +1 -1
- package/dist/source-products-api.js.map +1 -1
- package/dist/team-connect-api.d.ts +4 -0
- package/dist/team-connect-api.js +1 -1
- package/dist/team-connect-api.js.map +1 -1
- package/dist/toolchain-api.d.ts +15 -0
- package/dist/toolchain-api.js +12 -1
- package/dist/toolchain-api.js.map +1 -1
- package/package.json +2 -2
- package/templates/experience/package.json.tmpl +7 -4
- package/templates/harness/app/experience/layout/page.tsx.tmpl +1 -0
- package/templates/harness/app/experience/layout.tsx.tmpl +3 -0
- package/templates/harness/app/experience/pages/[id]/[...path]/page.tsx.tmpl +1 -0
- package/templates/package.json.tmpl +4 -4
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"project.js","sourceRoot":"","sources":["../../src/experience/project.ts"],"names":[],"mappings":"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","sourcesContent":["import {\n type EkanosJson,\n EkanosJsonSchema,\n type IntegrationEntry,\n type Lockfile,\n LockfileSchema,\n canonicalOperationId,\n experienceIntegrations,\n shortOperation,\n} from '@ekanos/integration-schema';\nimport * as fs from 'node:fs';\nimport * as path from 'node:path';\n\nimport { notFoundError, validationError } from '../errors';\n\nexport const EKANOS_JSON = 'ekanos.json';\nexport const LOCK_FILE = 'ekanos.lock.json';\nexport const LOCAL_OVERRIDES_FILE = 'ekanos.local.json';\nexport const DEFAULT_CONTRACTS_DIR = 'contracts';\nexport const INTEGRATIONS_DTS = 'src/.ekanos/integrations.d.ts';\n/** The previous name of the generated file, kept as a re-export for one version. */\nexport const LEGACY_SLOTS_DTS = 'src/.ekanos/slots.d.ts';\n\nexport interface ExperienceProject {\n /** Absolute project directory. */\n dir: string;\n ekanos: EkanosJson;\n slug: string;\n entry: string;\n /** The integrations it depends on; operations are canonical ids. */\n integrations: IntegrationEntry[];\n /** True when ekanos.json still uses the deprecated `uses` key. */\n deprecatedUses: boolean;\n /** Old `uses` alias -> integration, where they differ (for the sync notice). */\n renames: Record<string, string>;\n /** Absolute directory holding the pinned contract snapshots. */\n contractsDir: string;\n /** The raw ekanos.json, key order preserved, for rewrites. */\n raw: Record<string, unknown>;\n}\n\nfunction readJson(file: string): unknown {\n let text: string;\n try {\n text = fs.readFileSync(file, 'utf8');\n } catch {\n throw notFoundError(\n `No ${path.basename(file)} at ${file}.`,\n 'Run this from an Ekanos project directory, or pass --dir <path>.',\n );\n }\n try {\n return JSON.parse(text);\n } catch (error) {\n throw validationError(\n `${file} is not valid JSON: ${error instanceof Error ? error.message : String(error)}`,\n 'Fix the syntax error and re-run.',\n );\n }\n}\n\n/** `integration` when ekanos.json is absent or says so; used to route commands. */\nexport function readProjectKind(dir: string): 'integration' | 'experience' {\n try {\n const raw = JSON.parse(\n fs.readFileSync(path.join(dir, EKANOS_JSON), 'utf8'),\n ) as { kind?: unknown };\n\n return raw.kind === 'experience' ? 'experience' : 'integration';\n } catch {\n return 'integration';\n }\n}\n\n/**\n * Read and validate an experience's ekanos.json with the shared schema.\n *\n * A fresh scaffold has no integrations yet (you add them with `ekanos add`),\n * which the shared schema rejects (\"declares at least one integration\"). That\n * one rule is suspended here so `add` can bootstrap a project; `check` and\n * `build` report the empty state themselves.\n *\n * The deprecated `uses` shape is read too: its entries come back as\n * `integrations` (alias dropped) with `deprecatedUses` set and `renames`\n * mapping each old alias to its integration, so `ekanos sync` can rewrite the\n * file and say what code to update.\n */\nexport function readExperienceProject(dir: string): ExperienceProject {\n const absolute = path.resolve(dir);\n const raw = readJson(path.join(absolute, EKANOS_JSON));\n if (typeof raw !== 'object' || raw === null || Array.isArray(raw)) {\n throw validationError(`${EKANOS_JSON} must be a JSON object.`, 'Fix it.');\n }\n const record = raw as Record<string, unknown>;\n\n if (record.kind !== 'experience') {\n throw validationError(\n `${EKANOS_JSON} is not an experience (\"kind\": \"experience\").`,\n 'Run experience commands in an experience project; scaffold one with \"ekanos init --kind experience --slug <slug>\".',\n );\n }\n\n const declaredNone =\n !(Array.isArray(record.integrations) && record.integrations.length > 0) &&\n !(Array.isArray(record.uses) && record.uses.length > 0);\n const probe = declaredNone\n ? { ...record, integrations: [PLACEHOLDER_INTEGRATION], uses: undefined }\n : record;\n const parsed = EkanosJsonSchema.safeParse(probe);\n if (!parsed.success) {\n throw validationError(\n `${EKANOS_JSON}: ${parsed.error.issues\n .map((i) => `${i.path.join('.') || '(root)'}: ${i.message}`)\n .join('; ')}`,\n 'Fix ekanos.json; the experience fields are slug, entry, integrations, contractsDir, allowImports, ignore.',\n );\n }\n\n const ekanos = parsed.data;\n const declared = declaredNone\n ? { entries: [], deprecatedUses: false, renames: {} }\n : experienceIntegrations(ekanos);\n\n return {\n dir: absolute,\n ekanos,\n slug: ekanos.slug as string,\n entry: ekanos.entry as string,\n integrations: declared.entries.map(withCanonicalOperations),\n deprecatedUses: declared.deprecatedUses,\n renames: declared.renames,\n contractsDir: path.resolve(\n absolute,\n ekanos.contractsDir ?? DEFAULT_CONTRACTS_DIR,\n ),\n raw: record,\n };\n}\n\n/** Operations as canonical `<integration>@<version>/<kind>/<name>` ids, for lookups against a contract. */\nfunction withCanonicalOperations(entry: IntegrationEntry): IntegrationEntry {\n return {\n ...entry,\n operations: entry.operations.map((id) => canonicalOperationId(entry, id)),\n };\n}\n\n/** 2-space JSON with a trailing newline: the format every written file uses. */\nexport function formatJson(value: unknown): string {\n return `${JSON.stringify(value, null, 2)}\\n`;\n}\n\n/** Write only when the bytes differ. Returns whether the file changed. */\nexport function writeIfChanged(file: string, content: string): boolean {\n try {\n if (fs.readFileSync(file, 'utf8') === content) return false;\n } catch {\n // missing: write below\n }\n fs.mkdirSync(path.dirname(file), { recursive: true });\n fs.writeFileSync(file, content);\n\n return true;\n}\n\nexport function readLockfile(project: ExperienceProject): Lockfile {\n const file = path.join(project.dir, LOCK_FILE);\n if (!fs.existsSync(file)) return { lockfileVersion: 1, integrations: {} };\n const parsed = LockfileSchema.safeParse(readJson(file));\n if (!parsed.success) {\n throw validationError(\n `${LOCK_FILE}: ${parsed.error.issues.map((i) => `${i.path.join('.')}: ${i.message}`).join('; ')}`,\n 'Regenerate it with \"ekanos sync\".',\n );\n }\n\n return parsed.data;\n}\n\nexport function writeLockfile(\n project: ExperienceProject,\n lock: Lockfile,\n): boolean {\n const integrations = Object.fromEntries(\n Object.entries(lock.integrations).sort(([a], [b]) => (a < b ? -1 : 1)),\n );\n\n return writeIfChanged(\n path.join(project.dir, LOCK_FILE),\n formatJson({ lockfileVersion: 1, integrations }),\n );\n}\n\nconst PLACEHOLDER_INTEGRATION = {\n name: 'pending',\n version: '0',\n operations: ['tool/pending'],\n} as const;\n\n/**\n * Rewrite `integrations` in ekanos.json (short operation form), leaving every\n * other key where it was and dropping the deprecated `uses`.\n */\nexport function writeIntegrations(\n project: ExperienceProject,\n integrations: IntegrationEntry[],\n): boolean {\n const { uses: _uses, ...rest } = project.raw;\n const next = {\n ...rest,\n integrations: integrations.map((entry) => ({\n name: entry.name,\n version: entry.version,\n operations: entry.operations.map(shortOperation),\n ...(entry.required === 'required' ? {} : { required: entry.required }),\n ...(entry.modes.length === 1 && entry.modes[0] === 'team'\n ? {}\n : { modes: entry.modes }),\n })),\n };\n\n return writeIfChanged(path.join(project.dir, EKANOS_JSON), formatJson(next));\n}\n"]}
|
|
1
|
+
{"version":3,"file":"project.js","sourceRoot":"","sources":["../../src/experience/project.ts"],"names":[],"mappings":"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","sourcesContent":["import {\n type EkanosJson,\n EkanosJsonSchema,\n type IntegrationEntry,\n type Lockfile,\n LockfileSchema,\n canonicalOperationId,\n experienceIntegrations,\n shortOperation,\n} from '@ekanos/integration-schema';\nimport * as fs from 'node:fs';\nimport * as path from 'node:path';\n\nimport { notFoundError, validationError } from '../errors';\n\nexport const EKANOS_JSON = 'ekanos.json';\nexport const LOCK_FILE = 'ekanos.lock.json';\nexport const LOCAL_OVERRIDES_FILE = 'ekanos.local.json';\nexport const DEFAULT_CONTRACTS_DIR = 'contracts';\nexport const INTEGRATIONS_DTS = 'src/.ekanos/integrations.d.ts';\n/** The previous name of the generated file, kept as a re-export for one version. */\nexport const LEGACY_SLOTS_DTS = 'src/.ekanos/slots.d.ts';\n\nexport interface ExperienceProject {\n /** Absolute project directory. */\n dir: string;\n ekanos: EkanosJson;\n slug: string;\n entry: string;\n /** The integrations it depends on; operations are canonical ids. */\n integrations: IntegrationEntry[];\n /** True when ekanos.json still uses the deprecated `uses` key. */\n deprecatedUses: boolean;\n /** Old `uses` alias -> integration, where they differ (for the sync notice). */\n renames: Record<string, string>;\n /** Absolute directory holding the pinned contract snapshots. */\n contractsDir: string;\n /** The raw ekanos.json, key order preserved, for rewrites. */\n raw: Record<string, unknown>;\n}\n\nfunction readJson(file: string): unknown {\n let text: string;\n try {\n text = fs.readFileSync(file, 'utf8');\n } catch {\n throw notFoundError(\n `No ${path.basename(file)} at ${file}.`,\n 'Run this from an Ekanos project directory, or pass --dir <path>.',\n );\n }\n try {\n return JSON.parse(text);\n } catch (error) {\n throw validationError(\n `${file} is not valid JSON: ${error instanceof Error ? error.message : String(error)}`,\n 'Fix the syntax error and re-run.',\n );\n }\n}\n\n/** `integration` when ekanos.json is absent or says so; used to route commands. */\nexport function readProjectKind(dir: string): 'integration' | 'experience' {\n try {\n const raw = JSON.parse(\n fs.readFileSync(path.join(dir, EKANOS_JSON), 'utf8'),\n ) as { kind?: unknown };\n\n return raw.kind === 'experience' ? 'experience' : 'integration';\n } catch {\n return 'integration';\n }\n}\n\n/**\n * Read and validate an experience's ekanos.json with the shared schema.\n *\n * A fresh scaffold has no integrations yet (you add them with `ekanos add`),\n * which the shared schema rejects (\"declares at least one integration\"). That\n * one rule is suspended here so `add` can bootstrap a project; `check` and\n * `build` report the empty state themselves.\n *\n * The deprecated `uses` shape is read too: its entries come back as\n * `integrations` (alias dropped) with `deprecatedUses` set and `renames`\n * mapping each old alias to its integration, so `ekanos sync` can rewrite the\n * file and say what code to update.\n */\nexport function readExperienceProject(dir: string): ExperienceProject {\n const absolute = path.resolve(dir);\n const raw = readJson(path.join(absolute, EKANOS_JSON));\n if (typeof raw !== 'object' || raw === null || Array.isArray(raw)) {\n throw validationError(`${EKANOS_JSON} must be a JSON object.`, 'Fix it.');\n }\n const record = raw as Record<string, unknown>;\n\n if (record.kind !== 'experience') {\n throw validationError(\n `${EKANOS_JSON} is not an experience (\"kind\": \"experience\").`,\n 'Run experience commands in an experience project; scaffold one with \"ekanos init --kind experience --slug <slug>\".',\n );\n }\n\n const declaredNone =\n !(Array.isArray(record.integrations) && record.integrations.length > 0) &&\n !(Array.isArray(record.uses) && record.uses.length > 0);\n const probe = declaredNone\n ? { ...record, integrations: [PLACEHOLDER_INTEGRATION], uses: undefined }\n : record;\n const parsed = EkanosJsonSchema.safeParse(probe);\n if (!parsed.success) {\n throw validationError(\n `${EKANOS_JSON}: ${parsed.error.issues\n .map((i) => `${i.path.join('.') || '(root)'}: ${i.message}`)\n .join('; ')}`,\n 'Fix ekanos.json; the experience fields are slug, entry, integrations, contractsDir, allowImports, ignore.',\n );\n }\n\n const ekanos = parsed.data;\n const declared = declaredNone\n ? { entries: [], deprecatedUses: false, renames: {} }\n : experienceIntegrations(ekanos);\n\n return {\n dir: absolute,\n ekanos,\n slug: ekanos.slug as string,\n entry: ekanos.entry as string,\n integrations: declared.entries.map(withCanonicalOperations),\n deprecatedUses: declared.deprecatedUses,\n renames: declared.renames,\n contractsDir: path.resolve(\n absolute,\n ekanos.contractsDir ?? DEFAULT_CONTRACTS_DIR,\n ),\n raw: record,\n };\n}\n\n/**\n * Operations as canonical `<slug>@<version>/<kind>/<name>` ids, for lookups\n * against a contract. A contract's ids carry the bare slug even when the\n * package is named under a namespace, so a `vastly/news` entry canonicalises to\n * `news@0.1/...`.\n */\nfunction withCanonicalOperations(entry: IntegrationEntry): IntegrationEntry {\n const slug = entry.name.slice(entry.name.indexOf('/') + 1);\n\n return {\n ...entry,\n operations: entry.operations.map((id) =>\n canonicalOperationId({ name: slug, version: entry.version }, id),\n ),\n };\n}\n\n/** 2-space JSON with a trailing newline: the format every written file uses. */\nexport function formatJson(value: unknown): string {\n return `${JSON.stringify(value, null, 2)}\\n`;\n}\n\n/** Write only when the bytes differ. Returns whether the file changed. */\nexport function writeIfChanged(file: string, content: string): boolean {\n try {\n if (fs.readFileSync(file, 'utf8') === content) return false;\n } catch {\n // missing: write below\n }\n fs.mkdirSync(path.dirname(file), { recursive: true });\n fs.writeFileSync(file, content);\n\n return true;\n}\n\nexport function readLockfile(project: ExperienceProject): Lockfile {\n const file = path.join(project.dir, LOCK_FILE);\n if (!fs.existsSync(file)) return { lockfileVersion: 1, integrations: {} };\n const parsed = LockfileSchema.safeParse(readJson(file));\n if (!parsed.success) {\n throw validationError(\n `${LOCK_FILE}: ${parsed.error.issues.map((i) => `${i.path.join('.')}: ${i.message}`).join('; ')}`,\n 'Regenerate it with \"ekanos sync\".',\n );\n }\n\n return parsed.data;\n}\n\nexport function writeLockfile(\n project: ExperienceProject,\n lock: Lockfile,\n): boolean {\n const integrations = Object.fromEntries(\n Object.entries(lock.integrations).sort(([a], [b]) => (a < b ? -1 : 1)),\n );\n\n return writeIfChanged(\n path.join(project.dir, LOCK_FILE),\n formatJson({ lockfileVersion: 1, integrations }),\n );\n}\n\nconst PLACEHOLDER_INTEGRATION = {\n name: 'pending',\n version: '0',\n operations: ['tool/pending'],\n} as const;\n\n/**\n * Rewrite `integrations` in ekanos.json (short operation form), leaving every\n * other key where it was and dropping the deprecated `uses`.\n */\nexport function writeIntegrations(\n project: ExperienceProject,\n integrations: IntegrationEntry[],\n): boolean {\n const { uses: _uses, ...rest } = project.raw;\n const next = {\n ...rest,\n integrations: integrations.map((entry) => ({\n name: entry.name,\n version: entry.version,\n operations: entry.operations.map(shortOperation),\n ...(entry.required === 'required' ? {} : { required: entry.required }),\n ...(entry.modes.length === 1 && entry.modes[0] === 'team'\n ? {}\n : { modes: entry.modes }),\n })),\n };\n\n return writeIfChanged(path.join(project.dir, EKANOS_JSON), formatJson(next));\n}\n"]}
|
|
@@ -6,10 +6,22 @@ export interface ExperiencePublishArgs {
|
|
|
6
6
|
dir: string;
|
|
7
7
|
dryRun: boolean;
|
|
8
8
|
env: Record<string, string | undefined>;
|
|
9
|
+
/** The host to submit to (a real run only). Falls back to ekanos.json, then the stored login. */
|
|
10
|
+
host?: string;
|
|
11
|
+
/** The source slug to submit for (a real run only). Falls back to ekanos.json "source". */
|
|
12
|
+
source?: string;
|
|
13
|
+
/** Skips the multi-seat confirmation when the source came from ekanos.json. */
|
|
14
|
+
yes?: boolean;
|
|
15
|
+
/** Whether stdin is a real terminal; injectable for tests. */
|
|
16
|
+
stdinIsTTY?: boolean;
|
|
17
|
+
/** The y/N prompt; injectable for tests. */
|
|
18
|
+
confirm?: (question: string) => Promise<boolean>;
|
|
9
19
|
}
|
|
10
20
|
export interface ExperienceGate {
|
|
11
21
|
project: ExperienceProject;
|
|
12
22
|
result: ExperienceCheckResult;
|
|
23
|
+
/** The caller's environment (read for the clean-room switches). */
|
|
24
|
+
env?: Record<string, string | undefined>;
|
|
13
25
|
}
|
|
14
26
|
/**
|
|
15
27
|
* The refusals and the gate every experience publish passes, before anything
|
|
@@ -35,17 +47,42 @@ export interface PackedExperience {
|
|
|
35
47
|
* dry run puts it under `.ekanos/publish/`); otherwise it is discarded.
|
|
36
48
|
*/
|
|
37
49
|
export declare function packExperience(gate: ExperienceGate, artifactDir?: string): Promise<PackedExperience>;
|
|
50
|
+
/** What submitting a gated, packed experience came to. */
|
|
51
|
+
export type ExperienceSubmission = {
|
|
52
|
+
status: 'unsupported';
|
|
53
|
+
host: string;
|
|
54
|
+
} | {
|
|
55
|
+
status: 'submitted';
|
|
56
|
+
data: Record<string, unknown>;
|
|
57
|
+
human: string;
|
|
58
|
+
};
|
|
38
59
|
/**
|
|
39
|
-
*
|
|
60
|
+
* Submit a gated experience to a host that admits them.
|
|
61
|
+
*
|
|
62
|
+
* The host is asked first (the toolchain route's `capabilities`), because an
|
|
63
|
+
* older deployment answers a `kind=experience` submission as if it were an
|
|
64
|
+
* integration and would file it under the wrong gate: a host that does not
|
|
65
|
+
* advertise `experienceSubmissions` gets nothing sent and the caller keeps the
|
|
66
|
+
* dry-run-only message. Authorization is the same as an integration's (the
|
|
67
|
+
* developer seat on the source, checked server-side; the session refreshed
|
|
68
|
+
* once on a 401). The submission is the packed tarball plus the built
|
|
69
|
+
* `manifest.json`, which the host re-validates and cross-checks against the
|
|
70
|
+
* archive in its own gate.
|
|
71
|
+
*/
|
|
72
|
+
export declare function submitExperience(ctx: CliContext, args: ExperiencePublishArgs, gate: ExperienceGate): Promise<ExperienceSubmission>;
|
|
73
|
+
/**
|
|
74
|
+
* `ekanos publish` for an experience.
|
|
40
75
|
*
|
|
41
76
|
* 1. Active integration overrides refuse it (exit 3, naming each integration and path): what
|
|
42
77
|
* you ran is not what a host would resolve. An integration of the same repo is not an override.
|
|
43
78
|
* 2. A tracked ekanos.local.json, any `ignore` entry, or a failing gate refuse.
|
|
44
|
-
* 3.
|
|
45
|
-
*
|
|
46
|
-
*
|
|
79
|
+
* 3. With --dry-run it builds and packs, and checks the tarball holds no
|
|
80
|
+
* integration code. Nothing is sent.
|
|
81
|
+
* 4. Without --dry-run it builds, packs and submits to a host that advertises
|
|
82
|
+
* experience submissions. A host that does not (an older deployment) gets
|
|
83
|
+
* nothing and the run ends with the dry-run-only message, as before.
|
|
47
84
|
*
|
|
48
|
-
*
|
|
85
|
+
* The refusals and the dry run run before any login: a dry run needs no session.
|
|
49
86
|
*/
|
|
50
87
|
export declare function runExperiencePublish(ctx: CliContext, args: ExperiencePublishArgs): Promise<ExitCode>;
|
|
51
88
|
export declare const EXPERIENCE_NOT_SUPPORTED = "Publishing an experience is not supported by the host yet.";
|
|
@@ -1,17 +1,33 @@
|
|
|
1
|
+
import { canonicalJson } from '@ekanos/integration-schema';
|
|
1
2
|
import { execFileSync } from 'node:child_process';
|
|
2
3
|
import * as fs from 'node:fs';
|
|
3
4
|
import * as os from 'node:os';
|
|
4
5
|
import * as path from 'node:path';
|
|
6
|
+
import { requireSession, resolveAuthEnvironment } from '../auth/session.js';
|
|
7
|
+
import { promptYesNo, resolveSourceSlug, submitWithOneRefresh, verifyPublishTarget, } from '../commands/publish.js';
|
|
5
8
|
import { activeOverridesFor, harnessPinsResolver } from '../dev-experience.js';
|
|
6
9
|
import { gateFailedError, preconditionError, validationError } from '../errors.js';
|
|
10
|
+
import { MAX_ARCHIVE_BYTES } from '../pack.js';
|
|
11
|
+
import { MAX_MANIFEST_BYTES } from '../publish-api.js';
|
|
12
|
+
import { fetchToolchainReport } from '../toolchain-api.js';
|
|
13
|
+
import { shortHost } from '../ui.js';
|
|
7
14
|
import { emitExperience } from './build.js';
|
|
8
15
|
import { runExperienceChecks } from './checks.js';
|
|
16
|
+
import { CLEAN_ROOM_ENV, buildInCleanRoom, hasBuildableLockfile, } from './clean-room.js';
|
|
9
17
|
import { isOverridesFileTracked } from './overrides.js';
|
|
10
18
|
import { LOCAL_OVERRIDES_FILE, readExperienceProject, } from './project.js';
|
|
11
19
|
import { failOnErrors } from './report.js';
|
|
12
|
-
/**
|
|
20
|
+
/**
|
|
21
|
+
* What a packed experience may contain: its own source, build output and pins,
|
|
22
|
+
* nothing else. The source ships because the host rebuilds `dist/` from it in
|
|
23
|
+
* a clean room and refuses a package whose bundle is not what that source
|
|
24
|
+
* produces; the lockfile ships so that rebuild resolves the same dependencies.
|
|
25
|
+
*/
|
|
13
26
|
const ALLOWED_PACK_PATHS = [
|
|
14
27
|
/^package\.json$/,
|
|
28
|
+
/^npm-shrinkwrap\.json$/,
|
|
29
|
+
/^tsconfig[^/]*\.json$/,
|
|
30
|
+
/^src\/.+/,
|
|
15
31
|
/^ekanos\.json$/,
|
|
16
32
|
/^manifest\.json$/,
|
|
17
33
|
/^ekanos\.lock\.json$/,
|
|
@@ -50,7 +66,22 @@ export async function gateExperience(args) {
|
|
|
50
66
|
inlineSdk: args.env.EKANOS_INLINE_SDK,
|
|
51
67
|
pinsResolver: await harnessPinsResolver(project.dir, args.env),
|
|
52
68
|
});
|
|
53
|
-
return { project, result };
|
|
69
|
+
return { project, result, env: args.env };
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* npm never packs `package-lock.json`, but it does pack `npm-shrinkwrap.json`
|
|
73
|
+
* (and `npm ci` honours it). The host rebuilds the shipped source against the
|
|
74
|
+
* dependencies it was built with, so the lockfile the bundle was built from
|
|
75
|
+
* travels under that name for the pack and is removed afterwards. A project
|
|
76
|
+
* that keeps its own shrinkwrap is left as it is.
|
|
77
|
+
*/
|
|
78
|
+
function stageShrinkwrap(dir, content) {
|
|
79
|
+
const shrinkwrap = path.join(dir, 'npm-shrinkwrap.json');
|
|
80
|
+
if (content === null || fs.existsSync(shrinkwrap)) {
|
|
81
|
+
return { restore: () => { } };
|
|
82
|
+
}
|
|
83
|
+
fs.writeFileSync(shrinkwrap, content);
|
|
84
|
+
return { restore: () => fs.rmSync(shrinkwrap, { force: true }) };
|
|
54
85
|
}
|
|
55
86
|
/**
|
|
56
87
|
* Build the experience and pack it with `npm pack`, checking the tarball holds
|
|
@@ -59,11 +90,32 @@ export async function gateExperience(args) {
|
|
|
59
90
|
*/
|
|
60
91
|
export async function packExperience(gate, artifactDir) {
|
|
61
92
|
var _a;
|
|
93
|
+
var _b;
|
|
62
94
|
const { project, result } = gate;
|
|
95
|
+
if (!hasBuildableLockfile(project.dir)) {
|
|
96
|
+
throw preconditionError('There is no lockfile (package-lock.json, npm-shrinkwrap.json or pnpm-lock.yaml), so the host could not rebuild this experience from its source with the dependencies you built with.', 'Run "npm install --package-lock-only" (or install with pnpm in a workspace) and commit the lockfile, then re-run.');
|
|
97
|
+
}
|
|
63
98
|
const output = await emitExperience(project, result);
|
|
99
|
+
// The bundle that ships is the one built in a clean room from the lockfile
|
|
100
|
+
// that ships, the way the host's gate rebuilds it, not the one built in the
|
|
101
|
+
// working tree (a pnpm workspace's tree is not what a clean `npm ci` makes).
|
|
102
|
+
const ownLock = path.join(project.dir, 'package-lock.json');
|
|
103
|
+
let shippedLockfile = fs.existsSync(ownLock)
|
|
104
|
+
? fs.readFileSync(ownLock, 'utf8')
|
|
105
|
+
: null;
|
|
106
|
+
if (((_a = gate.env) === null || _a === void 0 ? void 0 : _a[CLEAN_ROOM_ENV]) !== 'off') {
|
|
107
|
+
const room = await buildInCleanRoom(project, gate.env);
|
|
108
|
+
fs.writeFileSync(path.join(project.dir, 'dist', 'experience.js'), room.bundle);
|
|
109
|
+
const written = JSON.parse(fs.readFileSync(path.join(project.dir, 'dist', 'surfaces.json'), 'utf8'));
|
|
110
|
+
if (canonicalJson(written) !== canonicalJson(room.surfaces)) {
|
|
111
|
+
throw validationError('The surfaces your working tree declares are not what the clean-room build declares (different dependency versions, or a stale build).', 'Reinstall dependencies from the lockfile, run "ekanos build", and re-run "ekanos publish".');
|
|
112
|
+
}
|
|
113
|
+
shippedLockfile = room.lockfile;
|
|
114
|
+
}
|
|
64
115
|
const scratch = fs.mkdtempSync(path.join(os.tmpdir(), 'ekanos-exp-pack-'));
|
|
65
116
|
let packed;
|
|
66
117
|
let tarball;
|
|
118
|
+
const shrinkwrap = stageShrinkwrap(project.dir, shippedLockfile);
|
|
67
119
|
try {
|
|
68
120
|
const stdout = execFileSync('npm', ['pack', '--json', '--ignore-scripts', '--pack-destination', scratch], { cwd: project.dir, encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'] });
|
|
69
121
|
packed = JSON.parse(stdout);
|
|
@@ -75,21 +127,25 @@ export async function packExperience(gate, artifactDir) {
|
|
|
75
127
|
}
|
|
76
128
|
}
|
|
77
129
|
catch (error) {
|
|
78
|
-
throw preconditionError(`npm pack failed: ${error instanceof Error ? error.message : String(error)}`, 'Make sure npm is installed and package.json "files" lists dist, manifest.json, ekanos.json, ekanos.lock.json and
|
|
130
|
+
throw preconditionError(`npm pack failed: ${error instanceof Error ? error.message : String(error)}`, 'Make sure npm is installed and package.json "files" lists src, dist, manifest.json, ekanos.json, ekanos.lock.json, contracts, tsconfig.json and npm-shrinkwrap.json.');
|
|
79
131
|
}
|
|
80
132
|
finally {
|
|
133
|
+
shrinkwrap.restore();
|
|
81
134
|
fs.rmSync(scratch, { recursive: true, force: true });
|
|
82
135
|
}
|
|
83
136
|
const entry = packed[0];
|
|
84
|
-
const files = ((
|
|
137
|
+
const files = ((_b = entry === null || entry === void 0 ? void 0 : entry.files) !== null && _b !== void 0 ? _b : []).map((f) => f.path);
|
|
85
138
|
const stray = files.filter((f) => !ALLOWED_PACK_PATHS.some((re) => re.test(f)));
|
|
86
139
|
if (stray.length > 0) {
|
|
87
|
-
throw validationError(`The package would ship files an experience must not contain: ${stray.slice(0, 5).join(', ')}.`, 'Restrict package.json "files" to dist, manifest.json, ekanos.json, ekanos.lock.json and
|
|
140
|
+
throw validationError(`The package would ship files an experience must not contain: ${stray.slice(0, 5).join(', ')}.`, 'Restrict package.json "files" to src, dist, manifest.json, ekanos.json, ekanos.lock.json, contracts, tsconfig.json and npm-shrinkwrap.json.');
|
|
88
141
|
}
|
|
89
142
|
for (const required of [
|
|
90
143
|
'manifest.json',
|
|
91
144
|
'dist/experience.js',
|
|
145
|
+
'dist/surfaces.json',
|
|
92
146
|
'ekanos.lock.json',
|
|
147
|
+
'npm-shrinkwrap.json',
|
|
148
|
+
project.entry,
|
|
93
149
|
]) {
|
|
94
150
|
if (!files.includes(required)) {
|
|
95
151
|
throw validationError(`The package would be missing ${required}.`, 'Add it to package.json "files".');
|
|
@@ -106,16 +162,140 @@ export async function packExperience(gate, artifactDir) {
|
|
|
106
162
|
};
|
|
107
163
|
}
|
|
108
164
|
/**
|
|
109
|
-
*
|
|
165
|
+
* Submit a gated experience to a host that admits them.
|
|
166
|
+
*
|
|
167
|
+
* The host is asked first (the toolchain route's `capabilities`), because an
|
|
168
|
+
* older deployment answers a `kind=experience` submission as if it were an
|
|
169
|
+
* integration and would file it under the wrong gate: a host that does not
|
|
170
|
+
* advertise `experienceSubmissions` gets nothing sent and the caller keeps the
|
|
171
|
+
* dry-run-only message. Authorization is the same as an integration's (the
|
|
172
|
+
* developer seat on the source, checked server-side; the session refreshed
|
|
173
|
+
* once on a 401). The submission is the packed tarball plus the built
|
|
174
|
+
* `manifest.json`, which the host re-validates and cross-checks against the
|
|
175
|
+
* archive in its own gate.
|
|
176
|
+
*/
|
|
177
|
+
export async function submitExperience(ctx, args, gate) {
|
|
178
|
+
var _a;
|
|
179
|
+
var _b, _c;
|
|
180
|
+
const { project } = gate;
|
|
181
|
+
const env = resolveAuthEnvironment(ctx, args.host, args.env, {
|
|
182
|
+
projectDir: project.dir,
|
|
183
|
+
});
|
|
184
|
+
const session = requireSession(env);
|
|
185
|
+
const resolution = await resolveSourceSlug(env, session, args.source, {
|
|
186
|
+
project: { source: project.ekanos.source },
|
|
187
|
+
});
|
|
188
|
+
const version = readVersion(gate);
|
|
189
|
+
const verified = await verifyPublishTarget(ctx, env, resolution.session, {
|
|
190
|
+
source: resolution.source,
|
|
191
|
+
explicit: resolution.explicit,
|
|
192
|
+
slug: project.slug,
|
|
193
|
+
version,
|
|
194
|
+
yes: args.yes === true,
|
|
195
|
+
stdinIsTTY: (_b = args.stdinIsTTY) !== null && _b !== void 0 ? _b : process.stdin.isTTY === true,
|
|
196
|
+
confirm: (_c = args.confirm) !== null && _c !== void 0 ? _c : promptYesNo,
|
|
197
|
+
});
|
|
198
|
+
const toolchain = await fetchToolchainReport(env.host, verified.session.accessToken);
|
|
199
|
+
if (toolchain.status !== 'ok' ||
|
|
200
|
+
((_a = toolchain.report.capabilities) === null || _a === void 0 ? void 0 : _a.experienceSubmissions) !== true) {
|
|
201
|
+
return { status: 'unsupported', host: env.host };
|
|
202
|
+
}
|
|
203
|
+
ctx.log(`Packing ${project.slug}@${version}…`);
|
|
204
|
+
const artifactDir = fs.mkdtempSync(path.join(os.tmpdir(), 'ekanos-exp-submit-'));
|
|
205
|
+
let archive;
|
|
206
|
+
let digest;
|
|
207
|
+
let files;
|
|
208
|
+
try {
|
|
209
|
+
const packed = await packExperience(gate, artifactDir);
|
|
210
|
+
if (!packed.tarball) {
|
|
211
|
+
throw preconditionError('The packed experience was not kept.', 'This is a bug in @ekanos/cli; report it.');
|
|
212
|
+
}
|
|
213
|
+
archive = new Uint8Array(fs.readFileSync(packed.tarball));
|
|
214
|
+
digest = packed.digest;
|
|
215
|
+
files = packed.files;
|
|
216
|
+
}
|
|
217
|
+
finally {
|
|
218
|
+
fs.rmSync(artifactDir, { recursive: true, force: true });
|
|
219
|
+
}
|
|
220
|
+
if (archive.length > MAX_ARCHIVE_BYTES) {
|
|
221
|
+
throw validationError(`The packed experience is ${archive.length} bytes, over the ${MAX_ARCHIVE_BYTES}-byte submission limit.`, 'Drop dependencies the bundle does not need, or split the experience.');
|
|
222
|
+
}
|
|
223
|
+
const manifest = fs.readFileSync(path.join(project.dir, 'manifest.json'), 'utf8');
|
|
224
|
+
if (Buffer.byteLength(manifest, 'utf8') > MAX_MANIFEST_BYTES) {
|
|
225
|
+
throw validationError(`manifest.json is larger than the ${MAX_MANIFEST_BYTES}-byte manifest limit.`, 'Trim the workflow templates or widget declarations it carries.');
|
|
226
|
+
}
|
|
227
|
+
if (!ctx.jsonMode) {
|
|
228
|
+
ctx.log(ctx.ui.ok(`${ctx.ui.glyphs.ok} packed ${project.slug}@${version} (${files.length} files).`));
|
|
229
|
+
}
|
|
230
|
+
const source = verified.sourceSeat.slug;
|
|
231
|
+
ctx.log(`Submitting to ${env.host} (source "${source}")…`);
|
|
232
|
+
const receipt = await submitWithOneRefresh(env, verified.session, {
|
|
233
|
+
source,
|
|
234
|
+
slug: project.slug,
|
|
235
|
+
kind: 'experience',
|
|
236
|
+
version,
|
|
237
|
+
manifest,
|
|
238
|
+
archive,
|
|
239
|
+
});
|
|
240
|
+
if (!ctx.jsonMode) {
|
|
241
|
+
ctx.log(ctx.ui.ok(`${ctx.ui.glyphs.ok} submitted ${receipt.slug}@${receipt.version} (state "${receipt.state}").`));
|
|
242
|
+
}
|
|
243
|
+
return {
|
|
244
|
+
status: 'submitted',
|
|
245
|
+
data: {
|
|
246
|
+
host: env.host,
|
|
247
|
+
source: {
|
|
248
|
+
id: verified.sourceSeat.id,
|
|
249
|
+
slug: verified.sourceSeat.slug,
|
|
250
|
+
name: verified.sourceSeat.name,
|
|
251
|
+
},
|
|
252
|
+
kind: 'experience',
|
|
253
|
+
slug: receipt.slug,
|
|
254
|
+
version: receipt.version,
|
|
255
|
+
submissionId: receipt.submissionId,
|
|
256
|
+
state: receipt.state,
|
|
257
|
+
archiveSha256: receipt.archiveSha256,
|
|
258
|
+
digest,
|
|
259
|
+
files,
|
|
260
|
+
published: true,
|
|
261
|
+
},
|
|
262
|
+
human: ctx.ui.result({
|
|
263
|
+
title: 'Submitted',
|
|
264
|
+
rows: [
|
|
265
|
+
['Experience', `${receipt.slug}@${receipt.version}`],
|
|
266
|
+
['State', receipt.state],
|
|
267
|
+
['Source', source],
|
|
268
|
+
['Host', shortHost(env.host)],
|
|
269
|
+
],
|
|
270
|
+
notes: [],
|
|
271
|
+
}),
|
|
272
|
+
};
|
|
273
|
+
}
|
|
274
|
+
function readVersion(gate) {
|
|
275
|
+
try {
|
|
276
|
+
const pkg = JSON.parse(fs.readFileSync(path.join(gate.project.dir, 'package.json'), 'utf8'));
|
|
277
|
+
if (typeof pkg.version === 'string' && pkg.version.length > 0) {
|
|
278
|
+
return pkg.version;
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
catch (_a) {
|
|
282
|
+
// Reported below with the fix.
|
|
283
|
+
}
|
|
284
|
+
throw preconditionError('package.json is missing or has no "version".', 'An experience is an npm package; set a semver "version" in package.json.');
|
|
285
|
+
}
|
|
286
|
+
/**
|
|
287
|
+
* `ekanos publish` for an experience.
|
|
110
288
|
*
|
|
111
289
|
* 1. Active integration overrides refuse it (exit 3, naming each integration and path): what
|
|
112
290
|
* you ran is not what a host would resolve. An integration of the same repo is not an override.
|
|
113
291
|
* 2. A tracked ekanos.local.json, any `ignore` entry, or a failing gate refuse.
|
|
114
|
-
* 3.
|
|
115
|
-
*
|
|
116
|
-
*
|
|
292
|
+
* 3. With --dry-run it builds and packs, and checks the tarball holds no
|
|
293
|
+
* integration code. Nothing is sent.
|
|
294
|
+
* 4. Without --dry-run it builds, packs and submits to a host that advertises
|
|
295
|
+
* experience submissions. A host that does not (an older deployment) gets
|
|
296
|
+
* nothing and the run ends with the dry-run-only message, as before.
|
|
117
297
|
*
|
|
118
|
-
*
|
|
298
|
+
* The refusals and the dry run run before any login: a dry run needs no session.
|
|
119
299
|
*/
|
|
120
300
|
export async function runExperiencePublish(ctx, args) {
|
|
121
301
|
const gate = await gateExperience(args);
|
|
@@ -124,7 +304,11 @@ export async function runExperiencePublish(ctx, args) {
|
|
|
124
304
|
if (failed !== null)
|
|
125
305
|
return failed;
|
|
126
306
|
if (!args.dryRun) {
|
|
127
|
-
|
|
307
|
+
const submission = await submitExperience(ctx, args, gate);
|
|
308
|
+
if (submission.status === 'unsupported') {
|
|
309
|
+
throw gateFailedError(EXPERIENCE_NOT_SUPPORTED, `Run "ekanos publish --dry-run" to build and pack it; ${submission.host} has no admission path for experiences until it ships one.`);
|
|
310
|
+
}
|
|
311
|
+
return ctx.succeed(submission.data, submission.human);
|
|
128
312
|
}
|
|
129
313
|
const packed = await packExperience(gate);
|
|
130
314
|
return ctx.succeed({
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"publish.js","sourceRoot":"","sources":["../../src/experience/publish.ts"],"names":[],"mappings":"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","sourcesContent":["import { execFileSync } from 'node:child_process';\nimport * as fs from 'node:fs';\nimport * as os from 'node:os';\nimport * as path from 'node:path';\n\nimport type { CliContext } from '../context';\nimport { activeOverridesFor, harnessPinsResolver } from '../dev-experience';\nimport { gateFailedError, preconditionError, validationError } from '../errors';\nimport type { ExitCode } from '../exit-codes';\nimport { emitExperience } from './build';\nimport { type ExperienceCheckResult, runExperienceChecks } from './checks';\nimport { isOverridesFileTracked } from './overrides';\nimport {\n type ExperienceProject,\n LOCAL_OVERRIDES_FILE,\n readExperienceProject,\n} from './project';\nimport { failOnErrors } from './report';\n\nexport interface ExperiencePublishArgs {\n dir: string;\n dryRun: boolean;\n env: Record<string, string | undefined>;\n}\n\n/** What a packed experience may contain: its own build output and pins, nothing else. */\nconst ALLOWED_PACK_PATHS = [\n /^package\\.json$/,\n /^ekanos\\.json$/,\n /^manifest\\.json$/,\n /^ekanos\\.lock\\.json$/,\n /^contracts\\/[^/]+\\.contract\\.json$/,\n /^contracts\\/\\.gitkeep$/,\n /^dist\\/[^/]+$/,\n /^README\\.md$/,\n /^LICENSE$/,\n];\n\nexport interface ExperienceGate {\n project: ExperienceProject;\n result: ExperienceCheckResult;\n}\n\n/**\n * The refusals and the gate every experience publish passes, before anything\n * is built: active overrides, a tracked overrides file, `ignore` entries, then\n * the experience checks. Throws a CliError for the first three; the findings\n * come back for the caller to render (one envelope for one package, a table\n * for a repo).\n */\nexport async function gateExperience(\n args: ExperiencePublishArgs,\n): Promise<ExperienceGate> {\n const project = readExperienceProject(args.dir);\n\n const overrides = await activeOverridesFor(project.dir, args.env);\n if (overrides.length > 0) {\n throw validationError(\n `Cannot publish with ${overrides.length} local integration override${overrides.length === 1 ? '' : 's'} active: ${overrides\n .map((o) => `${o.integration} -> ${o.path}`)\n .join(', ')}.`,\n `Delete ${LOCAL_OVERRIDES_FILE} (and unset EKANOS_INTEGRATION_OVERRIDE_*) so every integration resolves to its published version, then re-run.`,\n );\n }\n\n if (isOverridesFileTracked(project)) {\n throw validationError(\n `${LOCAL_OVERRIDES_FILE} is tracked by git.`,\n `It is per-developer: \"git rm --cached ${LOCAL_OVERRIDES_FILE}\" and add it to .gitignore.`,\n );\n }\n\n const ignored = project.ekanos.ignore ?? [];\n if (ignored.length > 0) {\n throw validationError(\n `ekanos.json \"ignore\" excludes ${ignored.join(', ')} from the boundary scan, so it cannot be published.`,\n 'Remove the \"ignore\" entries (move the archive out of the project) and re-run.',\n );\n }\n\n const result = await runExperienceChecks(project, {\n typecheck: false,\n loadDefinition: true,\n inlineSdk: args.env.EKANOS_INLINE_SDK,\n pinsResolver: await harnessPinsResolver(project.dir, args.env),\n });\n\n return { project, result };\n}\n\nexport interface PackedExperience {\n slug: string;\n version: string;\n digest: string;\n filename: string | undefined;\n files: string[];\n integrity: string | undefined;\n /** Where the tarball was kept, when `artifactDir` was given. */\n tarball: string | undefined;\n}\n\n/**\n * Build the experience and pack it with `npm pack`, checking the tarball holds\n * nothing but its own build output. `artifactDir` keeps the tarball (a repo\n * dry run puts it under `.ekanos/publish/`); otherwise it is discarded.\n */\nexport async function packExperience(\n gate: ExperienceGate,\n artifactDir?: string,\n): Promise<PackedExperience> {\n const { project, result } = gate;\n const output = await emitExperience(project, result);\n\n const scratch = fs.mkdtempSync(path.join(os.tmpdir(), 'ekanos-exp-pack-'));\n let packed: Array<{\n filename: string;\n files: Array<{ path: string; size: number }>;\n integrity?: string;\n }>;\n let tarball: string | undefined;\n try {\n const stdout = execFileSync(\n 'npm',\n ['pack', '--json', '--ignore-scripts', '--pack-destination', scratch],\n { cwd: project.dir, encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'] },\n );\n packed = JSON.parse(stdout) as typeof packed;\n const first = packed[0];\n if (artifactDir && first) {\n fs.mkdirSync(artifactDir, { recursive: true });\n tarball = path.join(artifactDir, path.basename(first.filename));\n fs.copyFileSync(path.join(scratch, first.filename), tarball);\n }\n } catch (error) {\n throw preconditionError(\n `npm pack failed: ${error instanceof Error ? error.message : String(error)}`,\n 'Make sure npm is installed and package.json \"files\" lists dist, manifest.json, ekanos.json, ekanos.lock.json and contracts.',\n );\n } finally {\n fs.rmSync(scratch, { recursive: true, force: true });\n }\n\n const entry = packed[0];\n const files = (entry?.files ?? []).map((f) => f.path);\n const stray = files.filter(\n (f) => !ALLOWED_PACK_PATHS.some((re) => re.test(f)),\n );\n if (stray.length > 0) {\n throw validationError(\n `The package would ship files an experience must not contain: ${stray.slice(0, 5).join(', ')}.`,\n 'Restrict package.json \"files\" to dist, manifest.json, ekanos.json, ekanos.lock.json and contracts.',\n );\n }\n for (const required of [\n 'manifest.json',\n 'dist/experience.js',\n 'ekanos.lock.json',\n ]) {\n if (!files.includes(required)) {\n throw validationError(\n `The package would be missing ${required}.`,\n 'Add it to package.json \"files\".',\n );\n }\n }\n\n return {\n slug: project.slug,\n version: output.manifest.version,\n digest: output.manifest.digest,\n filename: entry?.filename,\n files,\n integrity: entry?.integrity,\n tarball,\n };\n}\n\n/**\n * `ekanos publish` for an experience. Defined, but gated:\n *\n * 1. Active integration overrides refuse it (exit 3, naming each integration and path): what\n * you ran is not what a host would resolve. An integration of the same repo is not an override.\n * 2. A tracked ekanos.local.json, any `ignore` entry, or a failing gate refuse.\n * 3. Without --dry-run it stops: the host cannot admit an experience yet.\n * 4. With --dry-run it builds and packs, and checks the tarball holds no\n * integration code.\n *\n * Runs before any login: a dry run needs no session.\n */\nexport async function runExperiencePublish(\n ctx: CliContext,\n args: ExperiencePublishArgs,\n): Promise<ExitCode> {\n const gate = await gateExperience(args);\n const { project, result } = gate;\n const failed = failOnErrors(\n ctx,\n 'publish',\n result.findings,\n { slug: project.slug },\n project.dir,\n );\n if (failed !== null) return failed;\n\n if (!args.dryRun) {\n throw gateFailedError(\n EXPERIENCE_NOT_SUPPORTED,\n 'Run \"ekanos publish --dry-run\" to build and pack it; the host has no admission path for experiences until it ships one.',\n );\n }\n\n const packed = await packExperience(gate);\n\n return ctx.succeed(\n {\n slug: packed.slug,\n version: packed.version,\n digest: packed.digest,\n dryRun: true,\n package: {\n filename: packed.filename,\n files: packed.files,\n integrity: packed.integrity,\n },\n published: false,\n },\n `Dry run: packed ${packed.slug}@${packed.version} (${packed.files.length} files, ${packed.digest.slice(0, 18)}…). Nothing was published.`,\n );\n}\n\nexport const EXPERIENCE_NOT_SUPPORTED =\n 'Publishing an experience is not supported by the host yet.';\n"]}
|
|
1
|
+
{"version":3,"file":"publish.js","sourceRoot":"","sources":["../../src/experience/publish.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,MAAM,4BAA4B,CAAC;AAC3D,OAAO,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAC;AAClD,OAAO,KAAK,EAAE,MAAM,SAAS,CAAC;AAC9B,OAAO,KAAK,EAAE,MAAM,SAAS,CAAC;AAC9B,OAAO,KAAK,IAAI,MAAM,WAAW,CAAC;AAGlC,OAAO,EAAE,cAAc,EAAE,sBAAsB,EAAE,MAAM,iBAAiB,CAAC;AACzE,OAAO,EACL,WAAW,EACX,iBAAiB,EACjB,oBAAoB,EACpB,mBAAmB,GACpB,MAAM,qBAAqB,CAAC;AAE7B,OAAO,EAAE,kBAAkB,EAAE,mBAAmB,EAAE,MAAM,mBAAmB,CAAC;AAC5E,OAAO,EAAE,eAAe,EAAE,iBAAiB,EAAE,eAAe,EAAE,MAAM,WAAW,CAAC;AAEhF,OAAO,EAAE,iBAAiB,EAAE,MAAM,SAAS,CAAC;AAC5C,OAAO,EAAE,kBAAkB,EAAE,MAAM,gBAAgB,CAAC;AACpD,OAAO,EAAE,oBAAoB,EAAE,MAAM,kBAAkB,CAAC;AACxD,OAAO,EAAE,SAAS,EAAE,MAAM,OAAO,CAAC;AAClC,OAAO,EAAE,cAAc,EAAE,MAAM,SAAS,CAAC;AACzC,OAAO,EAA8B,mBAAmB,EAAE,MAAM,UAAU,CAAC;AAC3E,OAAO,EACL,cAAc,EACd,gBAAgB,EAChB,oBAAoB,GACrB,MAAM,cAAc,CAAC;AACtB,OAAO,EAAE,sBAAsB,EAAE,MAAM,aAAa,CAAC;AACrD,OAAO,EAEL,oBAAoB,EACpB,qBAAqB,GACtB,MAAM,WAAW,CAAC;AACnB,OAAO,EAAE,YAAY,EAAE,MAAM,UAAU,CAAC;AAkBxC;;;;;GAKG;AACH,MAAM,kBAAkB,GAAG;IACzB,iBAAiB;IACjB,wBAAwB;IACxB,uBAAuB;IACvB,UAAU;IACV,gBAAgB;IAChB,kBAAkB;IAClB,sBAAsB;IACtB,oCAAoC;IACpC,wBAAwB;IACxB,eAAe;IACf,cAAc;IACd,WAAW;CACZ,CAAC;AASF;;;;;;GAMG;AACH,MAAM,CAAC,KAAK,UAAU,cAAc,CAClC,IAA2B;;IAE3B,MAAM,OAAO,GAAG,qBAAqB,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;IAEhD,MAAM,SAAS,GAAG,MAAM,kBAAkB,CAAC,OAAO,CAAC,GAAG,EAAE,IAAI,CAAC,GAAG,CAAC,CAAC;IAClE,IAAI,SAAS,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;QACzB,MAAM,eAAe,CACnB,uBAAuB,SAAS,CAAC,MAAM,8BAA8B,SAAS,CAAC,MAAM,KAAK,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,GAAG,YAAY,SAAS;aACxH,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,GAAG,CAAC,CAAC,WAAW,OAAO,CAAC,CAAC,IAAI,EAAE,CAAC;aAC3C,IAAI,CAAC,IAAI,CAAC,GAAG,EAChB,UAAU,oBAAoB,iHAAiH,CAChJ,CAAC;IACJ,CAAC;IAED,IAAI,sBAAsB,CAAC,OAAO,CAAC,EAAE,CAAC;QACpC,MAAM,eAAe,CACnB,GAAG,oBAAoB,qBAAqB,EAC5C,yCAAyC,oBAAoB,6BAA6B,CAC3F,CAAC;IACJ,CAAC;IAED,MAAM,OAAO,SAAG,OAAO,CAAC,MAAM,CAAC,MAAM,mCAAI,EAAE,CAAC;IAC5C,IAAI,OAAO,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;QACvB,MAAM,eAAe,CACnB,iCAAiC,OAAO,CAAC,IAAI,CAAC,IAAI,CAAC,qDAAqD,EACxG,+EAA+E,CAChF,CAAC;IACJ,CAAC;IAED,MAAM,MAAM,GAAG,MAAM,mBAAmB,CAAC,OAAO,EAAE;QAChD,SAAS,EAAE,KAAK;QAChB,cAAc,EAAE,IAAI;QACpB,SAAS,EAAE,IAAI,CAAC,GAAG,CAAC,iBAAiB;QACrC,YAAY,EAAE,MAAM,mBAAmB,CAAC,OAAO,CAAC,GAAG,EAAE,IAAI,CAAC,GAAG,CAAC;KAC/D,CAAC,CAAC;IAEH,OAAO,EAAE,OAAO,EAAE,MAAM,EAAE,GAAG,EAAE,IAAI,CAAC,GAAG,EAAE,CAAC;AAC5C,CAAC;AAED;;;;;;GAMG;AACH,SAAS,eAAe,CACtB,GAAW,EACX,OAAsB;IAEtB,MAAM,UAAU,GAAG,IAAI,CAAC,IAAI,CAAC,GAAG,EAAE,qBAAqB,CAAC,CAAC;IAEzD,IAAI,OAAO,KAAK,IAAI,IAAI,EAAE,CAAC,UAAU,CAAC,UAAU,CAAC,EAAE,CAAC;QAClD,OAAO,EAAE,OAAO,EAAE,GAAG,EAAE,GAAE,CAAC,EAAE,CAAC;IAC/B,CAAC;IACD,EAAE,CAAC,aAAa,CAAC,UAAU,EAAE,OAAO,CAAC,CAAC;IAEtC,OAAO,EAAE,OAAO,EAAE,GAAG,EAAE,CAAC,EAAE,CAAC,MAAM,CAAC,UAAU,EAAE,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC,EAAE,CAAC;AACnE,CAAC;AAaD;;;;GAIG;AACH,MAAM,CAAC,KAAK,UAAU,cAAc,CAClC,IAAoB,EACpB,WAAoB;;;IAEpB,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,GAAG,IAAI,CAAC;IACjC,IAAI,CAAC,oBAAoB,CAAC,OAAO,CAAC,GAAG,CAAC,EAAE,CAAC;QACvC,MAAM,iBAAiB,CACrB,sLAAsL,EACtL,mHAAmH,CACpH,CAAC;IACJ,CAAC;IACD,MAAM,MAAM,GAAG,MAAM,cAAc,CAAC,OAAO,EAAE,MAAM,CAAC,CAAC;IAErD,2EAA2E;IAC3E,4EAA4E;IAC5E,6EAA6E;IAC7E,MAAM,OAAO,GAAG,IAAI,CAAC,IAAI,CAAC,OAAO,CAAC,GAAG,EAAE,mBAAmB,CAAC,CAAC;IAC5D,IAAI,eAAe,GAAkB,EAAE,CAAC,UAAU,CAAC,OAAO,CAAC;QACzD,CAAC,CAAC,EAAE,CAAC,YAAY,CAAC,OAAO,EAAE,MAAM,CAAC;QAClC,CAAC,CAAC,IAAI,CAAC;IACT,IAAI,CAAA,MAAA,IAAI,CAAC,GAAG,0CAAG,cAAc,CAAC,MAAK,KAAK,EAAE,CAAC;QACzC,MAAM,IAAI,GAAG,MAAM,gBAAgB,CAAC,OAAO,EAAE,IAAI,CAAC,GAAG,CAAC,CAAC;QACvD,EAAE,CAAC,aAAa,CACd,IAAI,CAAC,IAAI,CAAC,OAAO,CAAC,GAAG,EAAE,MAAM,EAAE,eAAe,CAAC,EAC/C,IAAI,CAAC,MAAM,CACZ,CAAC;QACF,MAAM,OAAO,GAAG,IAAI,CAAC,KAAK,CACxB,EAAE,CAAC,YAAY,CAAC,IAAI,CAAC,IAAI,CAAC,OAAO,CAAC,GAAG,EAAE,MAAM,EAAE,eAAe,CAAC,EAAE,MAAM,CAAC,CAC9D,CAAC;QACb,IAAI,aAAa,CAAC,OAAO,CAAC,KAAK,aAAa,CAAC,IAAI,CAAC,QAAQ,CAAC,EAAE,CAAC;YAC5D,MAAM,eAAe,CACnB,uIAAuI,EACvI,4FAA4F,CAC7F,CAAC;QACJ,CAAC;QACD,eAAe,GAAG,IAAI,CAAC,QAAQ,CAAC;IAClC,CAAC;IAED,MAAM,OAAO,GAAG,EAAE,CAAC,WAAW,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC,MAAM,EAAE,EAAE,kBAAkB,CAAC,CAAC,CAAC;IAC3E,IAAI,MAIF,CAAC;IACH,IAAI,OAA2B,CAAC;IAChC,MAAM,UAAU,GAAG,eAAe,CAAC,OAAO,CAAC,GAAG,EAAE,eAAe,CAAC,CAAC;IACjE,IAAI,CAAC;QACH,MAAM,MAAM,GAAG,YAAY,CACzB,KAAK,EACL,CAAC,MAAM,EAAE,QAAQ,EAAE,kBAAkB,EAAE,oBAAoB,EAAE,OAAO,CAAC,EACrE,EAAE,GAAG,EAAE,OAAO,CAAC,GAAG,EAAE,QAAQ,EAAE,MAAM,EAAE,KAAK,EAAE,CAAC,QAAQ,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,CAC1E,CAAC;QACF,MAAM,GAAG,IAAI,CAAC,KAAK,CAAC,MAAM,CAAkB,CAAC;QAC7C,MAAM,KAAK,GAAG,MAAM,CAAC,CAAC,CAAC,CAAC;QACxB,IAAI,WAAW,IAAI,KAAK,EAAE,CAAC;YACzB,EAAE,CAAC,SAAS,CAAC,WAAW,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;YAC/C,OAAO,GAAG,IAAI,CAAC,IAAI,CAAC,WAAW,EAAE,IAAI,CAAC,QAAQ,CAAC,KAAK,CAAC,QAAQ,CAAC,CAAC,CAAC;YAChE,EAAE,CAAC,YAAY,CAAC,IAAI,CAAC,IAAI,CAAC,OAAO,EAAE,KAAK,CAAC,QAAQ,CAAC,EAAE,OAAO,CAAC,CAAC;QAC/D,CAAC;IACH,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACf,MAAM,iBAAiB,CACrB,oBAAoB,KAAK,YAAY,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,EAAE,EAC5E,sKAAsK,CACvK,CAAC;IACJ,CAAC;YAAS,CAAC;QACT,UAAU,CAAC,OAAO,EAAE,CAAC;QACrB,EAAE,CAAC,MAAM,CAAC,OAAO,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC,CAAC;IACvD,CAAC;IAED,MAAM,KAAK,GAAG,MAAM,CAAC,CAAC,CAAC,CAAC;IACxB,MAAM,KAAK,GAAG,OAAC,KAAK,aAAL,KAAK,uBAAL,KAAK,CAAE,KAAK,mCAAI,EAAE,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC;IACtD,MAAM,KAAK,GAAG,KAAK,CAAC,MAAM,CACxB,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,kBAAkB,CAAC,IAAI,CAAC,CAAC,EAAE,EAAE,EAAE,CAAC,EAAE,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,CACpD,CAAC;IACF,IAAI,KAAK,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;QACrB,MAAM,eAAe,CACnB,gEAAgE,KAAK,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG,EAC/F,6IAA6I,CAC9I,CAAC;IACJ,CAAC;IACD,KAAK,MAAM,QAAQ,IAAI;QACrB,eAAe;QACf,oBAAoB;QACpB,oBAAoB;QACpB,kBAAkB;QAClB,qBAAqB;QACrB,OAAO,CAAC,KAAK;KACd,EAAE,CAAC;QACF,IAAI,CAAC,KAAK,CAAC,QAAQ,CAAC,QAAQ,CAAC,EAAE,CAAC;YAC9B,MAAM,eAAe,CACnB,gCAAgC,QAAQ,GAAG,EAC3C,iCAAiC,CAClC,CAAC;QACJ,CAAC;IACH,CAAC;IAED,OAAO;QACL,IAAI,EAAE,OAAO,CAAC,IAAI;QAClB,OAAO,EAAE,MAAM,CAAC,QAAQ,CAAC,OAAO;QAChC,MAAM,EAAE,MAAM,CAAC,QAAQ,CAAC,MAAM;QAC9B,QAAQ,EAAE,KAAK,aAAL,KAAK,uBAAL,KAAK,CAAE,QAAQ;QACzB,KAAK;QACL,SAAS,EAAE,KAAK,aAAL,KAAK,uBAAL,KAAK,CAAE,SAAS;QAC3B,OAAO;KACR,CAAC;AACJ,CAAC;AAOD;;;;;;;;;;;;GAYG;AACH,MAAM,CAAC,KAAK,UAAU,gBAAgB,CACpC,GAAe,EACf,IAA2B,EAC3B,IAAoB;;;IAEpB,MAAM,EAAE,OAAO,EAAE,GAAG,IAAI,CAAC;IACzB,MAAM,GAAG,GAAG,sBAAsB,CAAC,GAAG,EAAE,IAAI,CAAC,IAAI,EAAE,IAAI,CAAC,GAAG,EAAE;QAC3D,UAAU,EAAE,OAAO,CAAC,GAAG;KACxB,CAAC,CAAC;IACH,MAAM,OAAO,GAAkB,cAAc,CAAC,GAAG,CAAC,CAAC;IAEnD,MAAM,UAAU,GAAG,MAAM,iBAAiB,CAAC,GAAG,EAAE,OAAO,EAAE,IAAI,CAAC,MAAM,EAAE;QACpE,OAAO,EAAE,EAAE,MAAM,EAAE,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE;KAC3C,CAAC,CAAC;IAEH,MAAM,OAAO,GAAG,WAAW,CAAC,IAAI,CAAC,CAAC;IAClC,MAAM,QAAQ,GAAG,MAAM,mBAAmB,CAAC,GAAG,EAAE,GAAG,EAAE,UAAU,CAAC,OAAO,EAAE;QACvE,MAAM,EAAE,UAAU,CAAC,MAAM;QACzB,QAAQ,EAAE,UAAU,CAAC,QAAQ;QAC7B,IAAI,EAAE,OAAO,CAAC,IAAI;QAClB,OAAO;QACP,GAAG,EAAE,IAAI,CAAC,GAAG,KAAK,IAAI;QACtB,UAAU,QAAE,IAAI,CAAC,UAAU,mCAAI,OAAO,CAAC,KAAK,CAAC,KAAK,KAAK,IAAI;QAC3D,OAAO,QAAE,IAAI,CAAC,OAAO,mCAAI,WAAW;KACrC,CAAC,CAAC;IAEH,MAAM,SAAS,GAAG,MAAM,oBAAoB,CAC1C,GAAG,CAAC,IAAI,EACR,QAAQ,CAAC,OAAO,CAAC,WAAW,CAC7B,CAAC;IACF,IACE,SAAS,CAAC,MAAM,KAAK,IAAI;QACzB,CAAA,MAAA,SAAS,CAAC,MAAM,CAAC,YAAY,0CAAE,qBAAqB,MAAK,IAAI,EAC7D,CAAC;QACD,OAAO,EAAE,MAAM,EAAE,aAAa,EAAE,IAAI,EAAE,GAAG,CAAC,IAAI,EAAE,CAAC;IACnD,CAAC;IAED,GAAG,CAAC,GAAG,CAAC,WAAW,OAAO,CAAC,IAAI,IAAI,OAAO,GAAG,CAAC,CAAC;IAE/C,MAAM,WAAW,GAAG,EAAE,CAAC,WAAW,CAChC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC,MAAM,EAAE,EAAE,oBAAoB,CAAC,CAC7C,CAAC;IACF,IAAI,OAAmB,CAAC;IACxB,IAAI,MAAc,CAAC;IACnB,IAAI,KAAe,CAAC;IACpB,IAAI,CAAC;QACH,MAAM,MAAM,GAAG,MAAM,cAAc,CAAC,IAAI,EAAE,WAAW,CAAC,CAAC;QACvD,IAAI,CAAC,MAAM,CAAC,OAAO,EAAE,CAAC;YACpB,MAAM,iBAAiB,CACrB,qCAAqC,EACrC,0CAA0C,CAC3C,CAAC;QACJ,CAAC;QACD,OAAO,GAAG,IAAI,UAAU,CAAC,EAAE,CAAC,YAAY,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC,CAAC;QAC1D,MAAM,GAAG,MAAM,CAAC,MAAM,CAAC;QACvB,KAAK,GAAG,MAAM,CAAC,KAAK,CAAC;IACvB,CAAC;YAAS,CAAC;QACT,EAAE,CAAC,MAAM,CAAC,WAAW,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC,CAAC;IAC3D,CAAC;IAED,IAAI,OAAO,CAAC,MAAM,GAAG,iBAAiB,EAAE,CAAC;QACvC,MAAM,eAAe,CACnB,4BAA4B,OAAO,CAAC,MAAM,oBAAoB,iBAAiB,yBAAyB,EACxG,sEAAsE,CACvE,CAAC;IACJ,CAAC;IAED,MAAM,QAAQ,GAAG,EAAE,CAAC,YAAY,CAC9B,IAAI,CAAC,IAAI,CAAC,OAAO,CAAC,GAAG,EAAE,eAAe,CAAC,EACvC,MAAM,CACP,CAAC;IACF,IAAI,MAAM,CAAC,UAAU,CAAC,QAAQ,EAAE,MAAM,CAAC,GAAG,kBAAkB,EAAE,CAAC;QAC7D,MAAM,eAAe,CACnB,oCAAoC,kBAAkB,uBAAuB,EAC7E,gEAAgE,CACjE,CAAC;IACJ,CAAC;IAED,IAAI,CAAC,GAAG,CAAC,QAAQ,EAAE,CAAC;QAClB,GAAG,CAAC,GAAG,CACL,GAAG,CAAC,EAAE,CAAC,EAAE,CACP,GAAG,GAAG,CAAC,EAAE,CAAC,MAAM,CAAC,EAAE,WAAW,OAAO,CAAC,IAAI,IAAI,OAAO,KAAK,KAAK,CAAC,MAAM,UAAU,CACjF,CACF,CAAC;IACJ,CAAC;IAED,MAAM,MAAM,GAAG,QAAQ,CAAC,UAAU,CAAC,IAAI,CAAC;IACxC,GAAG,CAAC,GAAG,CAAC,iBAAiB,GAAG,CAAC,IAAI,aAAa,MAAM,KAAK,CAAC,CAAC;IAE3D,MAAM,OAAO,GAAG,MAAM,oBAAoB,CAAC,GAAG,EAAE,QAAQ,CAAC,OAAO,EAAE;QAChE,MAAM;QACN,IAAI,EAAE,OAAO,CAAC,IAAI;QAClB,IAAI,EAAE,YAAY;QAClB,OAAO;QACP,QAAQ;QACR,OAAO;KACR,CAAC,CAAC;IAEH,IAAI,CAAC,GAAG,CAAC,QAAQ,EAAE,CAAC;QAClB,GAAG,CAAC,GAAG,CACL,GAAG,CAAC,EAAE,CAAC,EAAE,CACP,GAAG,GAAG,CAAC,EAAE,CAAC,MAAM,CAAC,EAAE,cAAc,OAAO,CAAC,IAAI,IAAI,OAAO,CAAC,OAAO,YAAY,OAAO,CAAC,KAAK,KAAK,CAC/F,CACF,CAAC;IACJ,CAAC;IAED,OAAO;QACL,MAAM,EAAE,WAAW;QACnB,IAAI,EAAE;YACJ,IAAI,EAAE,GAAG,CAAC,IAAI;YACd,MAAM,EAAE;gBACN,EAAE,EAAE,QAAQ,CAAC,UAAU,CAAC,EAAE;gBAC1B,IAAI,EAAE,QAAQ,CAAC,UAAU,CAAC,IAAI;gBAC9B,IAAI,EAAE,QAAQ,CAAC,UAAU,CAAC,IAAI;aAC/B;YACD,IAAI,EAAE,YAAY;YAClB,IAAI,EAAE,OAAO,CAAC,IAAI;YAClB,OAAO,EAAE,OAAO,CAAC,OAAO;YACxB,YAAY,EAAE,OAAO,CAAC,YAAY;YAClC,KAAK,EAAE,OAAO,CAAC,KAAK;YACpB,aAAa,EAAE,OAAO,CAAC,aAAa;YACpC,MAAM;YACN,KAAK;YACL,SAAS,EAAE,IAAI;SAChB;QACD,KAAK,EAAE,GAAG,CAAC,EAAE,CAAC,MAAM,CAAC;YACnB,KAAK,EAAE,WAAW;YAClB,IAAI,EAAE;gBACJ,CAAC,YAAY,EAAE,GAAG,OAAO,CAAC,IAAI,IAAI,OAAO,CAAC,OAAO,EAAE,CAAC;gBACpD,CAAC,OAAO,EAAE,OAAO,CAAC,KAAK,CAAC;gBACxB,CAAC,QAAQ,EAAE,MAAM,CAAC;gBAClB,CAAC,MAAM,EAAE,SAAS,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;aAC9B;YACD,KAAK,EAAE,EAAE;SACV,CAAC;KACH,CAAC;AACJ,CAAC;AAED,SAAS,WAAW,CAAC,IAAoB;IACvC,IAAI,CAAC;QACH,MAAM,GAAG,GAAG,IAAI,CAAC,KAAK,CACpB,EAAE,CAAC,YAAY,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,OAAO,CAAC,GAAG,EAAE,cAAc,CAAC,EAAE,MAAM,CAAC,CAC5C,CAAC;QAC3B,IAAI,OAAO,GAAG,CAAC,OAAO,KAAK,QAAQ,IAAI,GAAG,CAAC,OAAO,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;YAC9D,OAAO,GAAG,CAAC,OAAO,CAAC;QACrB,CAAC;IACH,CAAC;eAAO,CAAC;QACP,+BAA+B;IACjC,CAAC;IAED,MAAM,iBAAiB,CACrB,8CAA8C,EAC9C,0EAA0E,CAC3E,CAAC;AACJ,CAAC;AAED;;;;;;;;;;;;;GAaG;AACH,MAAM,CAAC,KAAK,UAAU,oBAAoB,CACxC,GAAe,EACf,IAA2B;IAE3B,MAAM,IAAI,GAAG,MAAM,cAAc,CAAC,IAAI,CAAC,CAAC;IACxC,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,GAAG,IAAI,CAAC;IACjC,MAAM,MAAM,GAAG,YAAY,CACzB,GAAG,EACH,SAAS,EACT,MAAM,CAAC,QAAQ,EACf,EAAE,IAAI,EAAE,OAAO,CAAC,IAAI,EAAE,EACtB,OAAO,CAAC,GAAG,CACZ,CAAC;IACF,IAAI,MAAM,KAAK,IAAI;QAAE,OAAO,MAAM,CAAC;IAEnC,IAAI,CAAC,IAAI,CAAC,MAAM,EAAE,CAAC;QACjB,MAAM,UAAU,GAAG,MAAM,gBAAgB,CAAC,GAAG,EAAE,IAAI,EAAE,IAAI,CAAC,CAAC;QAE3D,IAAI,UAAU,CAAC,MAAM,KAAK,aAAa,EAAE,CAAC;YACxC,MAAM,eAAe,CACnB,wBAAwB,EACxB,wDAAwD,UAAU,CAAC,IAAI,4DAA4D,CACpI,CAAC;QACJ,CAAC;QAED,OAAO,GAAG,CAAC,OAAO,CAAC,UAAU,CAAC,IAAI,EAAE,UAAU,CAAC,KAAK,CAAC,CAAC;IACxD,CAAC;IAED,MAAM,MAAM,GAAG,MAAM,cAAc,CAAC,IAAI,CAAC,CAAC;IAE1C,OAAO,GAAG,CAAC,OAAO,CAChB;QACE,IAAI,EAAE,MAAM,CAAC,IAAI;QACjB,OAAO,EAAE,MAAM,CAAC,OAAO;QACvB,MAAM,EAAE,MAAM,CAAC,MAAM;QACrB,MAAM,EAAE,IAAI;QACZ,OAAO,EAAE;YACP,QAAQ,EAAE,MAAM,CAAC,QAAQ;YACzB,KAAK,EAAE,MAAM,CAAC,KAAK;YACnB,SAAS,EAAE,MAAM,CAAC,SAAS;SAC5B;QACD,SAAS,EAAE,KAAK;KACjB,EACD,mBAAmB,MAAM,CAAC,IAAI,IAAI,MAAM,CAAC,OAAO,KAAK,MAAM,CAAC,KAAK,CAAC,MAAM,WAAW,MAAM,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,EAAE,EAAE,CAAC,4BAA4B,CAC1I,CAAC;AACJ,CAAC;AAED,MAAM,CAAC,MAAM,wBAAwB,GACnC,4DAA4D,CAAC","sourcesContent":["import { canonicalJson } from '@ekanos/integration-schema';\nimport { execFileSync } from 'node:child_process';\nimport * as fs from 'node:fs';\nimport * as os from 'node:os';\nimport * as path from 'node:path';\n\nimport type { StoredSession } from '../auth/credential-store';\nimport { requireSession, resolveAuthEnvironment } from '../auth/session';\nimport {\n promptYesNo,\n resolveSourceSlug,\n submitWithOneRefresh,\n verifyPublishTarget,\n} from '../commands/publish';\nimport type { CliContext } from '../context';\nimport { activeOverridesFor, harnessPinsResolver } from '../dev-experience';\nimport { gateFailedError, preconditionError, validationError } from '../errors';\nimport type { ExitCode } from '../exit-codes';\nimport { MAX_ARCHIVE_BYTES } from '../pack';\nimport { MAX_MANIFEST_BYTES } from '../publish-api';\nimport { fetchToolchainReport } from '../toolchain-api';\nimport { shortHost } from '../ui';\nimport { emitExperience } from './build';\nimport { type ExperienceCheckResult, runExperienceChecks } from './checks';\nimport {\n CLEAN_ROOM_ENV,\n buildInCleanRoom,\n hasBuildableLockfile,\n} from './clean-room';\nimport { isOverridesFileTracked } from './overrides';\nimport {\n type ExperienceProject,\n LOCAL_OVERRIDES_FILE,\n readExperienceProject,\n} from './project';\nimport { failOnErrors } from './report';\n\nexport interface ExperiencePublishArgs {\n dir: string;\n dryRun: boolean;\n env: Record<string, string | undefined>;\n /** The host to submit to (a real run only). Falls back to ekanos.json, then the stored login. */\n host?: string;\n /** The source slug to submit for (a real run only). Falls back to ekanos.json \"source\". */\n source?: string;\n /** Skips the multi-seat confirmation when the source came from ekanos.json. */\n yes?: boolean;\n /** Whether stdin is a real terminal; injectable for tests. */\n stdinIsTTY?: boolean;\n /** The y/N prompt; injectable for tests. */\n confirm?: (question: string) => Promise<boolean>;\n}\n\n/**\n * What a packed experience may contain: its own source, build output and pins,\n * nothing else. The source ships because the host rebuilds `dist/` from it in\n * a clean room and refuses a package whose bundle is not what that source\n * produces; the lockfile ships so that rebuild resolves the same dependencies.\n */\nconst ALLOWED_PACK_PATHS = [\n /^package\\.json$/,\n /^npm-shrinkwrap\\.json$/,\n /^tsconfig[^/]*\\.json$/,\n /^src\\/.+/,\n /^ekanos\\.json$/,\n /^manifest\\.json$/,\n /^ekanos\\.lock\\.json$/,\n /^contracts\\/[^/]+\\.contract\\.json$/,\n /^contracts\\/\\.gitkeep$/,\n /^dist\\/[^/]+$/,\n /^README\\.md$/,\n /^LICENSE$/,\n];\n\nexport interface ExperienceGate {\n project: ExperienceProject;\n result: ExperienceCheckResult;\n /** The caller's environment (read for the clean-room switches). */\n env?: Record<string, string | undefined>;\n}\n\n/**\n * The refusals and the gate every experience publish passes, before anything\n * is built: active overrides, a tracked overrides file, `ignore` entries, then\n * the experience checks. Throws a CliError for the first three; the findings\n * come back for the caller to render (one envelope for one package, a table\n * for a repo).\n */\nexport async function gateExperience(\n args: ExperiencePublishArgs,\n): Promise<ExperienceGate> {\n const project = readExperienceProject(args.dir);\n\n const overrides = await activeOverridesFor(project.dir, args.env);\n if (overrides.length > 0) {\n throw validationError(\n `Cannot publish with ${overrides.length} local integration override${overrides.length === 1 ? '' : 's'} active: ${overrides\n .map((o) => `${o.integration} -> ${o.path}`)\n .join(', ')}.`,\n `Delete ${LOCAL_OVERRIDES_FILE} (and unset EKANOS_INTEGRATION_OVERRIDE_*) so every integration resolves to its published version, then re-run.`,\n );\n }\n\n if (isOverridesFileTracked(project)) {\n throw validationError(\n `${LOCAL_OVERRIDES_FILE} is tracked by git.`,\n `It is per-developer: \"git rm --cached ${LOCAL_OVERRIDES_FILE}\" and add it to .gitignore.`,\n );\n }\n\n const ignored = project.ekanos.ignore ?? [];\n if (ignored.length > 0) {\n throw validationError(\n `ekanos.json \"ignore\" excludes ${ignored.join(', ')} from the boundary scan, so it cannot be published.`,\n 'Remove the \"ignore\" entries (move the archive out of the project) and re-run.',\n );\n }\n\n const result = await runExperienceChecks(project, {\n typecheck: false,\n loadDefinition: true,\n inlineSdk: args.env.EKANOS_INLINE_SDK,\n pinsResolver: await harnessPinsResolver(project.dir, args.env),\n });\n\n return { project, result, env: args.env };\n}\n\n/**\n * npm never packs `package-lock.json`, but it does pack `npm-shrinkwrap.json`\n * (and `npm ci` honours it). The host rebuilds the shipped source against the\n * dependencies it was built with, so the lockfile the bundle was built from\n * travels under that name for the pack and is removed afterwards. A project\n * that keeps its own shrinkwrap is left as it is.\n */\nfunction stageShrinkwrap(\n dir: string,\n content: string | null,\n): { restore: () => void } {\n const shrinkwrap = path.join(dir, 'npm-shrinkwrap.json');\n\n if (content === null || fs.existsSync(shrinkwrap)) {\n return { restore: () => {} };\n }\n fs.writeFileSync(shrinkwrap, content);\n\n return { restore: () => fs.rmSync(shrinkwrap, { force: true }) };\n}\n\nexport interface PackedExperience {\n slug: string;\n version: string;\n digest: string;\n filename: string | undefined;\n files: string[];\n integrity: string | undefined;\n /** Where the tarball was kept, when `artifactDir` was given. */\n tarball: string | undefined;\n}\n\n/**\n * Build the experience and pack it with `npm pack`, checking the tarball holds\n * nothing but its own build output. `artifactDir` keeps the tarball (a repo\n * dry run puts it under `.ekanos/publish/`); otherwise it is discarded.\n */\nexport async function packExperience(\n gate: ExperienceGate,\n artifactDir?: string,\n): Promise<PackedExperience> {\n const { project, result } = gate;\n if (!hasBuildableLockfile(project.dir)) {\n throw preconditionError(\n 'There is no lockfile (package-lock.json, npm-shrinkwrap.json or pnpm-lock.yaml), so the host could not rebuild this experience from its source with the dependencies you built with.',\n 'Run \"npm install --package-lock-only\" (or install with pnpm in a workspace) and commit the lockfile, then re-run.',\n );\n }\n const output = await emitExperience(project, result);\n\n // The bundle that ships is the one built in a clean room from the lockfile\n // that ships, the way the host's gate rebuilds it, not the one built in the\n // working tree (a pnpm workspace's tree is not what a clean `npm ci` makes).\n const ownLock = path.join(project.dir, 'package-lock.json');\n let shippedLockfile: string | null = fs.existsSync(ownLock)\n ? fs.readFileSync(ownLock, 'utf8')\n : null;\n if (gate.env?.[CLEAN_ROOM_ENV] !== 'off') {\n const room = await buildInCleanRoom(project, gate.env);\n fs.writeFileSync(\n path.join(project.dir, 'dist', 'experience.js'),\n room.bundle,\n );\n const written = JSON.parse(\n fs.readFileSync(path.join(project.dir, 'dist', 'surfaces.json'), 'utf8'),\n ) as unknown;\n if (canonicalJson(written) !== canonicalJson(room.surfaces)) {\n throw validationError(\n 'The surfaces your working tree declares are not what the clean-room build declares (different dependency versions, or a stale build).',\n 'Reinstall dependencies from the lockfile, run \"ekanos build\", and re-run \"ekanos publish\".',\n );\n }\n shippedLockfile = room.lockfile;\n }\n\n const scratch = fs.mkdtempSync(path.join(os.tmpdir(), 'ekanos-exp-pack-'));\n let packed: Array<{\n filename: string;\n files: Array<{ path: string; size: number }>;\n integrity?: string;\n }>;\n let tarball: string | undefined;\n const shrinkwrap = stageShrinkwrap(project.dir, shippedLockfile);\n try {\n const stdout = execFileSync(\n 'npm',\n ['pack', '--json', '--ignore-scripts', '--pack-destination', scratch],\n { cwd: project.dir, encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'] },\n );\n packed = JSON.parse(stdout) as typeof packed;\n const first = packed[0];\n if (artifactDir && first) {\n fs.mkdirSync(artifactDir, { recursive: true });\n tarball = path.join(artifactDir, path.basename(first.filename));\n fs.copyFileSync(path.join(scratch, first.filename), tarball);\n }\n } catch (error) {\n throw preconditionError(\n `npm pack failed: ${error instanceof Error ? error.message : String(error)}`,\n 'Make sure npm is installed and package.json \"files\" lists src, dist, manifest.json, ekanos.json, ekanos.lock.json, contracts, tsconfig.json and npm-shrinkwrap.json.',\n );\n } finally {\n shrinkwrap.restore();\n fs.rmSync(scratch, { recursive: true, force: true });\n }\n\n const entry = packed[0];\n const files = (entry?.files ?? []).map((f) => f.path);\n const stray = files.filter(\n (f) => !ALLOWED_PACK_PATHS.some((re) => re.test(f)),\n );\n if (stray.length > 0) {\n throw validationError(\n `The package would ship files an experience must not contain: ${stray.slice(0, 5).join(', ')}.`,\n 'Restrict package.json \"files\" to src, dist, manifest.json, ekanos.json, ekanos.lock.json, contracts, tsconfig.json and npm-shrinkwrap.json.',\n );\n }\n for (const required of [\n 'manifest.json',\n 'dist/experience.js',\n 'dist/surfaces.json',\n 'ekanos.lock.json',\n 'npm-shrinkwrap.json',\n project.entry,\n ]) {\n if (!files.includes(required)) {\n throw validationError(\n `The package would be missing ${required}.`,\n 'Add it to package.json \"files\".',\n );\n }\n }\n\n return {\n slug: project.slug,\n version: output.manifest.version,\n digest: output.manifest.digest,\n filename: entry?.filename,\n files,\n integrity: entry?.integrity,\n tarball,\n };\n}\n\n/** What submitting a gated, packed experience came to. */\nexport type ExperienceSubmission =\n | { status: 'unsupported'; host: string }\n | { status: 'submitted'; data: Record<string, unknown>; human: string };\n\n/**\n * Submit a gated experience to a host that admits them.\n *\n * The host is asked first (the toolchain route's `capabilities`), because an\n * older deployment answers a `kind=experience` submission as if it were an\n * integration and would file it under the wrong gate: a host that does not\n * advertise `experienceSubmissions` gets nothing sent and the caller keeps the\n * dry-run-only message. Authorization is the same as an integration's (the\n * developer seat on the source, checked server-side; the session refreshed\n * once on a 401). The submission is the packed tarball plus the built\n * `manifest.json`, which the host re-validates and cross-checks against the\n * archive in its own gate.\n */\nexport async function submitExperience(\n ctx: CliContext,\n args: ExperiencePublishArgs,\n gate: ExperienceGate,\n): Promise<ExperienceSubmission> {\n const { project } = gate;\n const env = resolveAuthEnvironment(ctx, args.host, args.env, {\n projectDir: project.dir,\n });\n const session: StoredSession = requireSession(env);\n\n const resolution = await resolveSourceSlug(env, session, args.source, {\n project: { source: project.ekanos.source },\n });\n\n const version = readVersion(gate);\n const verified = await verifyPublishTarget(ctx, env, resolution.session, {\n source: resolution.source,\n explicit: resolution.explicit,\n slug: project.slug,\n version,\n yes: args.yes === true,\n stdinIsTTY: args.stdinIsTTY ?? process.stdin.isTTY === true,\n confirm: args.confirm ?? promptYesNo,\n });\n\n const toolchain = await fetchToolchainReport(\n env.host,\n verified.session.accessToken,\n );\n if (\n toolchain.status !== 'ok' ||\n toolchain.report.capabilities?.experienceSubmissions !== true\n ) {\n return { status: 'unsupported', host: env.host };\n }\n\n ctx.log(`Packing ${project.slug}@${version}…`);\n\n const artifactDir = fs.mkdtempSync(\n path.join(os.tmpdir(), 'ekanos-exp-submit-'),\n );\n let archive: Uint8Array;\n let digest: string;\n let files: string[];\n try {\n const packed = await packExperience(gate, artifactDir);\n if (!packed.tarball) {\n throw preconditionError(\n 'The packed experience was not kept.',\n 'This is a bug in @ekanos/cli; report it.',\n );\n }\n archive = new Uint8Array(fs.readFileSync(packed.tarball));\n digest = packed.digest;\n files = packed.files;\n } finally {\n fs.rmSync(artifactDir, { recursive: true, force: true });\n }\n\n if (archive.length > MAX_ARCHIVE_BYTES) {\n throw validationError(\n `The packed experience is ${archive.length} bytes, over the ${MAX_ARCHIVE_BYTES}-byte submission limit.`,\n 'Drop dependencies the bundle does not need, or split the experience.',\n );\n }\n\n const manifest = fs.readFileSync(\n path.join(project.dir, 'manifest.json'),\n 'utf8',\n );\n if (Buffer.byteLength(manifest, 'utf8') > MAX_MANIFEST_BYTES) {\n throw validationError(\n `manifest.json is larger than the ${MAX_MANIFEST_BYTES}-byte manifest limit.`,\n 'Trim the workflow templates or widget declarations it carries.',\n );\n }\n\n if (!ctx.jsonMode) {\n ctx.log(\n ctx.ui.ok(\n `${ctx.ui.glyphs.ok} packed ${project.slug}@${version} (${files.length} files).`,\n ),\n );\n }\n\n const source = verified.sourceSeat.slug;\n ctx.log(`Submitting to ${env.host} (source \"${source}\")…`);\n\n const receipt = await submitWithOneRefresh(env, verified.session, {\n source,\n slug: project.slug,\n kind: 'experience',\n version,\n manifest,\n archive,\n });\n\n if (!ctx.jsonMode) {\n ctx.log(\n ctx.ui.ok(\n `${ctx.ui.glyphs.ok} submitted ${receipt.slug}@${receipt.version} (state \"${receipt.state}\").`,\n ),\n );\n }\n\n return {\n status: 'submitted',\n data: {\n host: env.host,\n source: {\n id: verified.sourceSeat.id,\n slug: verified.sourceSeat.slug,\n name: verified.sourceSeat.name,\n },\n kind: 'experience',\n slug: receipt.slug,\n version: receipt.version,\n submissionId: receipt.submissionId,\n state: receipt.state,\n archiveSha256: receipt.archiveSha256,\n digest,\n files,\n published: true,\n },\n human: ctx.ui.result({\n title: 'Submitted',\n rows: [\n ['Experience', `${receipt.slug}@${receipt.version}`],\n ['State', receipt.state],\n ['Source', source],\n ['Host', shortHost(env.host)],\n ],\n notes: [],\n }),\n };\n}\n\nfunction readVersion(gate: ExperienceGate): string {\n try {\n const pkg = JSON.parse(\n fs.readFileSync(path.join(gate.project.dir, 'package.json'), 'utf8'),\n ) as { version?: unknown };\n if (typeof pkg.version === 'string' && pkg.version.length > 0) {\n return pkg.version;\n }\n } catch {\n // Reported below with the fix.\n }\n\n throw preconditionError(\n 'package.json is missing or has no \"version\".',\n 'An experience is an npm package; set a semver \"version\" in package.json.',\n );\n}\n\n/**\n * `ekanos publish` for an experience.\n *\n * 1. Active integration overrides refuse it (exit 3, naming each integration and path): what\n * you ran is not what a host would resolve. An integration of the same repo is not an override.\n * 2. A tracked ekanos.local.json, any `ignore` entry, or a failing gate refuse.\n * 3. With --dry-run it builds and packs, and checks the tarball holds no\n * integration code. Nothing is sent.\n * 4. Without --dry-run it builds, packs and submits to a host that advertises\n * experience submissions. A host that does not (an older deployment) gets\n * nothing and the run ends with the dry-run-only message, as before.\n *\n * The refusals and the dry run run before any login: a dry run needs no session.\n */\nexport async function runExperiencePublish(\n ctx: CliContext,\n args: ExperiencePublishArgs,\n): Promise<ExitCode> {\n const gate = await gateExperience(args);\n const { project, result } = gate;\n const failed = failOnErrors(\n ctx,\n 'publish',\n result.findings,\n { slug: project.slug },\n project.dir,\n );\n if (failed !== null) return failed;\n\n if (!args.dryRun) {\n const submission = await submitExperience(ctx, args, gate);\n\n if (submission.status === 'unsupported') {\n throw gateFailedError(\n EXPERIENCE_NOT_SUPPORTED,\n `Run \"ekanos publish --dry-run\" to build and pack it; ${submission.host} has no admission path for experiences until it ships one.`,\n );\n }\n\n return ctx.succeed(submission.data, submission.human);\n }\n\n const packed = await packExperience(gate);\n\n return ctx.succeed(\n {\n slug: packed.slug,\n version: packed.version,\n digest: packed.digest,\n dryRun: true,\n package: {\n filename: packed.filename,\n files: packed.files,\n integrity: packed.integrity,\n },\n published: false,\n },\n `Dry run: packed ${packed.slug}@${packed.version} (${packed.files.length} files, ${packed.digest.slice(0, 18)}…). Nothing was published.`,\n );\n}\n\nexport const EXPERIENCE_NOT_SUPPORTED =\n 'Publishing an experience is not supported by the host yet.';\n"]}
|
package/dist/harness-scaffold.js
CHANGED
|
@@ -89,6 +89,8 @@ export const EXPERIENCE_SHELL_FILES = [
|
|
|
89
89
|
'experience/widgets/page.tsx',
|
|
90
90
|
'experience/widgets/[id]/page.tsx',
|
|
91
91
|
'experience/pages/[id]/page.tsx',
|
|
92
|
+
'experience/pages/[id]/[...path]/page.tsx',
|
|
93
|
+
'experience/layout/page.tsx',
|
|
92
94
|
'experience/workflows/page.tsx',
|
|
93
95
|
'experience/workflows/[id]/page.tsx',
|
|
94
96
|
'experience/review-queue/page.tsx',
|
|
@@ -399,7 +401,7 @@ function renderRepoExperiences(repo) {
|
|
|
399
401
|
imports.push(`import ${ident} from ${entryImportLiteral(`${experience.dir}/${experience.entry}`,
|
|
400
402
|
// app/repo-experiences.ts sits one level above app/experience/.
|
|
401
403
|
'../../../')};`);
|
|
402
|
-
entries.push(` ${JSON.stringify(experience.name)}: { widgets: ${ident}.widgets ?? [], pages: ${ident}.pages ?? [] },\n`);
|
|
404
|
+
entries.push(` ${JSON.stringify(experience.name)}: { widgets: ${ident}.widgets ?? [], pages: ${ident}.pages ?? [], ...(${ident}.suggestedLayout ? { suggestedLayout: ${ident}.suggestedLayout } : {}) },\n`);
|
|
403
405
|
});
|
|
404
406
|
return {
|
|
405
407
|
EXPERIENCE_IMPORTS: imports.join('\n'),
|