@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.
Files changed (73) hide show
  1. package/README.md +1 -0
  2. package/dist/commands/add.js +1 -1
  3. package/dist/commands/add.js.map +1 -1
  4. package/dist/commands/catalog.js +11 -7
  5. package/dist/commands/catalog.js.map +1 -1
  6. package/dist/commands/names.d.ts +19 -0
  7. package/dist/commands/names.js +122 -0
  8. package/dist/commands/names.js.map +1 -0
  9. package/dist/commands/publish.d.ts +53 -3
  10. package/dist/commands/publish.js +58 -16
  11. package/dist/commands/publish.js.map +1 -1
  12. package/dist/commands/sources.js +15 -5
  13. package/dist/commands/sources.js.map +1 -1
  14. package/dist/commands/status.d.ts +2 -0
  15. package/dist/commands/status.js +12 -12
  16. package/dist/commands/status.js.map +1 -1
  17. package/dist/commands/sync.js +2 -2
  18. package/dist/commands/sync.js.map +1 -1
  19. package/dist/commands/teams-connect.js +1 -1
  20. package/dist/commands/teams-connect.js.map +1 -1
  21. package/dist/experience/boundary.d.ts +2 -2
  22. package/dist/experience/boundary.js +2 -11
  23. package/dist/experience/boundary.js.map +1 -1
  24. package/dist/experience/build.d.ts +3 -1
  25. package/dist/experience/build.js +15 -26
  26. package/dist/experience/build.js.map +1 -1
  27. package/dist/experience/clean-room.d.ts +40 -0
  28. package/dist/experience/clean-room.js +150 -0
  29. package/dist/experience/clean-room.js.map +1 -0
  30. package/dist/experience/contract-sources.d.ts +7 -2
  31. package/dist/experience/contract-sources.js +15 -8
  32. package/dist/experience/contract-sources.js.map +1 -1
  33. package/dist/experience/definition-checks.d.ts +12 -1
  34. package/dist/experience/definition-checks.js +34 -11
  35. package/dist/experience/definition-checks.js.map +1 -1
  36. package/dist/experience/definition-shape.d.ts +20 -2
  37. package/dist/experience/definition-shape.js.map +1 -1
  38. package/dist/experience/project.js +8 -2
  39. package/dist/experience/project.js.map +1 -1
  40. package/dist/experience/publish.d.ts +42 -5
  41. package/dist/experience/publish.js +195 -11
  42. package/dist/experience/publish.js.map +1 -1
  43. package/dist/harness-scaffold.js +3 -1
  44. package/dist/harness-scaffold.js.map +1 -1
  45. package/dist/index.js +25 -2
  46. package/dist/index.js.map +1 -1
  47. package/dist/names-api.d.ts +90 -0
  48. package/dist/names-api.js +123 -0
  49. package/dist/names-api.js.map +1 -0
  50. package/dist/product-kinds.d.ts +13 -0
  51. package/dist/product-kinds.js +17 -0
  52. package/dist/product-kinds.js.map +1 -0
  53. package/dist/publish-api.d.ts +38 -5
  54. package/dist/publish-api.js +59 -16
  55. package/dist/publish-api.js.map +1 -1
  56. package/dist/repo/publish.d.ts +6 -3
  57. package/dist/repo/publish.js +48 -14
  58. package/dist/repo/publish.js.map +1 -1
  59. package/dist/source-products-api.d.ts +3 -2
  60. package/dist/source-products-api.js +1 -1
  61. package/dist/source-products-api.js.map +1 -1
  62. package/dist/team-connect-api.d.ts +4 -0
  63. package/dist/team-connect-api.js +1 -1
  64. package/dist/team-connect-api.js.map +1 -1
  65. package/dist/toolchain-api.d.ts +15 -0
  66. package/dist/toolchain-api.js +12 -1
  67. package/dist/toolchain-api.js.map +1 -1
  68. package/package.json +2 -2
  69. package/templates/experience/package.json.tmpl +7 -4
  70. package/templates/harness/app/experience/layout/page.tsx.tmpl +1 -0
  71. package/templates/harness/app/experience/layout.tsx.tmpl +3 -0
  72. package/templates/harness/app/experience/pages/[id]/[...path]/page.tsx.tmpl +1 -0
  73. 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":";;;;;;;;;;;AAAA,OAAO,EAEL,gBAAgB,EAGhB,cAAc,EACd,oBAAoB,EACpB,sBAAsB,EACtB,cAAc,GACf,MAAM,4BAA4B,CAAC;AACpC,OAAO,KAAK,EAAE,MAAM,SAAS,CAAC;AAC9B,OAAO,KAAK,IAAI,MAAM,WAAW,CAAC;AAElC,OAAO,EAAE,aAAa,EAAE,eAAe,EAAE,MAAM,WAAW,CAAC;AAE3D,MAAM,CAAC,MAAM,WAAW,GAAG,aAAa,CAAC;AACzC,MAAM,CAAC,MAAM,SAAS,GAAG,kBAAkB,CAAC;AAC5C,MAAM,CAAC,MAAM,oBAAoB,GAAG,mBAAmB,CAAC;AACxD,MAAM,CAAC,MAAM,qBAAqB,GAAG,WAAW,CAAC;AACjD,MAAM,CAAC,MAAM,gBAAgB,GAAG,+BAA+B,CAAC;AAChE,oFAAoF;AACpF,MAAM,CAAC,MAAM,gBAAgB,GAAG,wBAAwB,CAAC;AAoBzD,SAAS,QAAQ,CAAC,IAAY;IAC5B,IAAI,IAAY,CAAC;IACjB,IAAI,CAAC;QACH,IAAI,GAAG,EAAE,CAAC,YAAY,CAAC,IAAI,EAAE,MAAM,CAAC,CAAC;IACvC,CAAC;eAAO,CAAC;QACP,MAAM,aAAa,CACjB,MAAM,IAAI,CAAC,QAAQ,CAAC,IAAI,CAAC,OAAO,IAAI,GAAG,EACvC,kEAAkE,CACnE,CAAC;IACJ,CAAC;IACD,IAAI,CAAC;QACH,OAAO,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;IAC1B,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACf,MAAM,eAAe,CACnB,GAAG,IAAI,uBAAuB,KAAK,YAAY,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,EAAE,EACtF,kCAAkC,CACnC,CAAC;IACJ,CAAC;AACH,CAAC;AAED,mFAAmF;AACnF,MAAM,UAAU,eAAe,CAAC,GAAW;IACzC,IAAI,CAAC;QACH,MAAM,GAAG,GAAG,IAAI,CAAC,KAAK,CACpB,EAAE,CAAC,YAAY,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG,EAAE,WAAW,CAAC,EAAE,MAAM,CAAC,CAC/B,CAAC;QAExB,OAAO,GAAG,CAAC,IAAI,KAAK,YAAY,CAAC,CAAC,CAAC,YAAY,CAAC,CAAC,CAAC,aAAa,CAAC;IAClE,CAAC;eAAO,CAAC;QACP,OAAO,aAAa,CAAC;IACvB,CAAC;AACH,CAAC;AAED;;;;;;;;;;;;GAYG;AACH,MAAM,UAAU,qBAAqB,CAAC,GAAW;;IAC/C,MAAM,QAAQ,GAAG,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC;IACnC,MAAM,GAAG,GAAG,QAAQ,CAAC,IAAI,CAAC,IAAI,CAAC,QAAQ,EAAE,WAAW,CAAC,CAAC,CAAC;IACvD,IAAI,OAAO,GAAG,KAAK,QAAQ,IAAI,GAAG,KAAK,IAAI,IAAI,KAAK,CAAC,OAAO,CAAC,GAAG,CAAC,EAAE,CAAC;QAClE,MAAM,eAAe,CAAC,GAAG,WAAW,yBAAyB,EAAE,SAAS,CAAC,CAAC;IAC5E,CAAC;IACD,MAAM,MAAM,GAAG,GAA8B,CAAC;IAE9C,IAAI,MAAM,CAAC,IAAI,KAAK,YAAY,EAAE,CAAC;QACjC,MAAM,eAAe,CACnB,GAAG,WAAW,+CAA+C,EAC7D,oHAAoH,CACrH,CAAC;IACJ,CAAC;IAED,MAAM,YAAY,GAChB,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,MAAM,CAAC,YAAY,CAAC,IAAI,MAAM,CAAC,YAAY,CAAC,MAAM,GAAG,CAAC,CAAC;QACvE,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,MAAM,CAAC,IAAI,CAAC,IAAI,MAAM,CAAC,IAAI,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC;IAC1D,MAAM,KAAK,GAAG,YAAY;QACxB,CAAC,iCAAM,MAAM,KAAE,YAAY,EAAE,CAAC,uBAAuB,CAAC,EAAE,IAAI,EAAE,SAAS,IACvE,CAAC,CAAC,MAAM,CAAC;IACX,MAAM,MAAM,GAAG,gBAAgB,CAAC,SAAS,CAAC,KAAK,CAAC,CAAC;IACjD,IAAI,CAAC,MAAM,CAAC,OAAO,EAAE,CAAC;QACpB,MAAM,eAAe,CACnB,GAAG,WAAW,KAAK,MAAM,CAAC,KAAK,CAAC,MAAM;aACnC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,IAAI,QAAQ,KAAK,CAAC,CAAC,OAAO,EAAE,CAAC;aAC3D,IAAI,CAAC,IAAI,CAAC,EAAE,EACf,2GAA2G,CAC5G,CAAC;IACJ,CAAC;IAED,MAAM,MAAM,GAAG,MAAM,CAAC,IAAI,CAAC;IAC3B,MAAM,QAAQ,GAAG,YAAY;QAC3B,CAAC,CAAC,EAAE,OAAO,EAAE,EAAE,EAAE,cAAc,EAAE,KAAK,EAAE,OAAO,EAAE,EAAE,EAAE;QACrD,CAAC,CAAC,sBAAsB,CAAC,MAAM,CAAC,CAAC;IAEnC,OAAO;QACL,GAAG,EAAE,QAAQ;QACb,MAAM;QACN,IAAI,EAAE,MAAM,CAAC,IAAc;QAC3B,KAAK,EAAE,MAAM,CAAC,KAAe;QAC7B,YAAY,EAAE,QAAQ,CAAC,OAAO,CAAC,GAAG,CAAC,uBAAuB,CAAC;QAC3D,cAAc,EAAE,QAAQ,CAAC,cAAc;QACvC,OAAO,EAAE,QAAQ,CAAC,OAAO;QACzB,YAAY,EAAE,IAAI,CAAC,OAAO,CACxB,QAAQ,QACR,MAAM,CAAC,YAAY,mCAAI,qBAAqB,CAC7C;QACD,GAAG,EAAE,MAAM;KACZ,CAAC;AACJ,CAAC;AAED;;;;;GAKG;AACH,SAAS,uBAAuB,CAAC,KAAuB;IACtD,MAAM,IAAI,GAAG,KAAK,CAAC,IAAI,CAAC,KAAK,CAAC,KAAK,CAAC,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC,CAAC;IAE3D,uCACK,KAAK,KACR,UAAU,EAAE,KAAK,CAAC,UAAU,CAAC,GAAG,CAAC,CAAC,EAAE,EAAE,EAAE,CACtC,oBAAoB,CAAC,EAAE,IAAI,EAAE,IAAI,EAAE,OAAO,EAAE,KAAK,CAAC,OAAO,EAAE,EAAE,EAAE,CAAC,CACjE,IACD;AACJ,CAAC;AAED,gFAAgF;AAChF,MAAM,UAAU,UAAU,CAAC,KAAc;IACvC,OAAO,GAAG,IAAI,CAAC,SAAS,CAAC,KAAK,EAAE,IAAI,EAAE,CAAC,CAAC,IAAI,CAAC;AAC/C,CAAC;AAED,0EAA0E;AAC1E,MAAM,UAAU,cAAc,CAAC,IAAY,EAAE,OAAe;IAC1D,IAAI,CAAC;QACH,IAAI,EAAE,CAAC,YAAY,CAAC,IAAI,EAAE,MAAM,CAAC,KAAK,OAAO;YAAE,OAAO,KAAK,CAAC;IAC9D,CAAC;eAAO,CAAC;QACP,uBAAuB;IACzB,CAAC;IACD,EAAE,CAAC,SAAS,CAAC,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;IACtD,EAAE,CAAC,aAAa,CAAC,IAAI,EAAE,OAAO,CAAC,CAAC;IAEhC,OAAO,IAAI,CAAC;AACd,CAAC;AAED,MAAM,UAAU,YAAY,CAAC,OAA0B;IACrD,MAAM,IAAI,GAAG,IAAI,CAAC,IAAI,CAAC,OAAO,CAAC,GAAG,EAAE,SAAS,CAAC,CAAC;IAC/C,IAAI,CAAC,EAAE,CAAC,UAAU,CAAC,IAAI,CAAC;QAAE,OAAO,EAAE,eAAe,EAAE,CAAC,EAAE,YAAY,EAAE,EAAE,EAAE,CAAC;IAC1E,MAAM,MAAM,GAAG,cAAc,CAAC,SAAS,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAC,CAAC;IACxD,IAAI,CAAC,MAAM,CAAC,OAAO,EAAE,CAAC;QACpB,MAAM,eAAe,CACnB,GAAG,SAAS,KAAK,MAAM,CAAC,KAAK,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC,OAAO,EAAE,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,EACjG,mCAAmC,CACpC,CAAC;IACJ,CAAC;IAED,OAAO,MAAM,CAAC,IAAI,CAAC;AACrB,CAAC;AAED,MAAM,UAAU,aAAa,CAC3B,OAA0B,EAC1B,IAAc;IAEd,MAAM,YAAY,GAAG,MAAM,CAAC,WAAW,CACrC,MAAM,CAAC,OAAO,CAAC,IAAI,CAAC,YAAY,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CACvE,CAAC;IAEF,OAAO,cAAc,CACnB,IAAI,CAAC,IAAI,CAAC,OAAO,CAAC,GAAG,EAAE,SAAS,CAAC,EACjC,UAAU,CAAC,EAAE,eAAe,EAAE,CAAC,EAAE,YAAY,EAAE,CAAC,CACjD,CAAC;AACJ,CAAC;AAED,MAAM,uBAAuB,GAAG;IAC9B,IAAI,EAAE,SAAS;IACf,OAAO,EAAE,GAAG;IACZ,UAAU,EAAE,CAAC,cAAc,CAAC;CACpB,CAAC;AAEX;;;GAGG;AACH,MAAM,UAAU,iBAAiB,CAC/B,OAA0B,EAC1B,YAAgC;IAEhC,MAAM,KAA2B,OAAO,CAAC,GAAG,EAAtC,EAAE,IAAI,EAAE,KAAK,OAAyB,EAApB,IAAI,cAAtB,QAAwB,CAAc,CAAC;IAC7C,MAAM,IAAI,mCACL,IAAI,KACP,YAAY,EAAE,YAAY,CAAC,GAAG,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,+BACxC,IAAI,EAAE,KAAK,CAAC,IAAI,EAChB,OAAO,EAAE,KAAK,CAAC,OAAO,EACtB,UAAU,EAAE,KAAK,CAAC,UAAU,CAAC,GAAG,CAAC,cAAc,CAAC,IAC7C,CAAC,KAAK,CAAC,QAAQ,KAAK,UAAU,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,QAAQ,EAAE,KAAK,CAAC,QAAQ,EAAE,CAAC,GACnE,CAAC,KAAK,CAAC,KAAK,CAAC,MAAM,KAAK,CAAC,IAAI,KAAK,CAAC,KAAK,CAAC,CAAC,CAAC,KAAK,MAAM;YACvD,CAAC,CAAC,EAAE;YACJ,CAAC,CAAC,EAAE,KAAK,EAAE,KAAK,CAAC,KAAK,EAAE,CAAC,EAC3B,CAAC,GACJ,CAAC;IAEF,OAAO,cAAc,CAAC,IAAI,CAAC,IAAI,CAAC,OAAO,CAAC,GAAG,EAAE,WAAW,CAAC,EAAE,UAAU,CAAC,IAAI,CAAC,CAAC,CAAC;AAC/E,CAAC","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
- * `ekanos publish` for an experience. Defined, but gated:
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. Without --dry-run it stops: the host cannot admit an experience yet.
45
- * 4. With --dry-run it builds and packs, and checks the tarball holds no
46
- * integration code.
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
- * Runs before any login: a dry run needs no session.
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
- /** What a packed experience may contain: its own build output and pins, nothing else. */
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 contracts.');
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 = ((_a = entry === null || entry === void 0 ? void 0 : entry.files) !== null && _a !== void 0 ? _a : []).map((f) => f.path);
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 contracts.');
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
- * `ekanos publish` for an experience. Defined, but gated:
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. Without --dry-run it stops: the host cannot admit an experience yet.
115
- * 4. With --dry-run it builds and packs, and checks the tarball holds no
116
- * integration code.
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
- * Runs before any login: a dry run needs no session.
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
- throw gateFailedError(EXPERIENCE_NOT_SUPPORTED, 'Run "ekanos publish --dry-run" to build and pack it; the host has no admission path for experiences until it ships one.');
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":"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","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"]}
@@ -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'),