diff --git a/mise-tasks/services/realm-server b/mise-tasks/services/realm-server index 58e6b81f4a..0a2b8ab781 100755 --- a/mise-tasks/services/realm-server +++ b/mise-tasks/services/realm-server @@ -102,7 +102,7 @@ LOW_CREDIT_THRESHOLD="${LOW_CREDIT_THRESHOLD:-2000}" \ \ --path='../skills-realm/contents' \ --username='skills_realm' \ - --fromUrl="${REALM_BASE_URL}/skills/" \ + --fromUrl='@cardstack/skills/' \ --toUrl="${REALM_BASE_URL}/skills/" \ \ ${START_SUBMISSION:+--path="${SUBMISSION_REALM_PATH}"} \ diff --git a/mise-tasks/services/worker b/mise-tasks/services/worker index 1796782c1f..1a3f003521 100755 --- a/mise-tasks/services/worker +++ b/mise-tasks/services/worker @@ -40,7 +40,7 @@ NODE_ENV=development \ ${START_CATALOG:+--fromUrl='@cardstack/catalog/'} \ ${START_CATALOG:+--toUrl="${CATALOG_REALM_URL}"} \ \ - --fromUrl="${REALM_BASE_URL}/skills/" \ + --fromUrl='@cardstack/skills/' \ --toUrl="${REALM_BASE_URL}/skills/" \ \ ${START_CATALOG:+--fromUrl="${EXTERNAL_CATALOG_REALM_URL}"} \ diff --git a/mise-tasks/services/worker-test b/mise-tasks/services/worker-test index a40f8b6f83..db236d975f 100755 --- a/mise-tasks/services/worker-test +++ b/mise-tasks/services/worker-test @@ -38,5 +38,5 @@ NODE_ENV=test \ --toUrl="${REALM_TEST_URL}/test/" \ --fromUrl='https://cardstack.com/base/' \ --toUrl="${REALM_BASE_URL}/base/" \ - --fromUrl="${REALM_BASE_URL}/skills/" \ + --fromUrl='@cardstack/skills/' \ --toUrl="${REALM_BASE_URL}/skills/" diff --git a/packages/base/card-api.gts b/packages/base/card-api.gts index 987b5ab502..f19e00f9d5 100644 --- a/packages/base/card-api.gts +++ b/packages/base/card-api.gts @@ -2832,7 +2832,7 @@ function lazilyLoadLink( fieldValue = (await createFromSerialized( fileMetaDoc.data, fileMetaDoc, - new URL(fileMetaDoc.data.id!), + cardIdToURL(fileMetaDoc.data.id!), { store, dependencyTrackingContext }, )) as FileDef; } else { @@ -2848,7 +2848,7 @@ function lazilyLoadLink( fieldValue = (await createFromSerialized( cardDoc.data, cardDoc, - new URL(cardDoc.data.id!), + cardIdToURL(cardDoc.data.id!), { store, dependencyTrackingContext }, )) as CardDef; } @@ -3173,7 +3173,7 @@ export async function updateFromSerialized( ): Promise> { stores.set(instance, store); if (!instance[relativeTo] && doc.data.id) { - instance[relativeTo] = new URL(doc.data.id); + instance[relativeTo] = cardIdToURL(doc.data.id); } if (isCardInstance(instance)) { @@ -3299,7 +3299,7 @@ async function _updateFromSerialized({ let instanceRelativeTo = instance[relativeTo] ?? ('id' in instance && typeof instance.id === 'string' - ? new URL(instance.id) + ? cardIdToURL(instance.id) : undefined); function getFieldMeta( @@ -3482,7 +3482,7 @@ async function _updateFromSerialized({ let relativeToVal = instance[relativeTo] ?? ('id' in instance && typeof instance.id === 'string' - ? new URL(instance.id) + ? cardIdToURL(instance.id) : undefined); let deserializedValue = await getDeserializedValue({ card, diff --git a/packages/base/cards-grid.gts b/packages/base/cards-grid.gts index 3687bb4639..c96caf781b 100644 --- a/packages/base/cards-grid.gts +++ b/packages/base/cards-grid.gts @@ -14,6 +14,7 @@ import Captions from '@cardstack/boxel-icons/captions'; import AllCardsIcon from '@cardstack/boxel-icons/square-stack'; import { + cardIdToURL, chooseCard, specRef, baseRealm, @@ -258,7 +259,7 @@ class Isolated extends Component { } if (spec && isCardInstance(spec)) { - await this.args.createCard?.(spec.ref, new URL(spec.id!), { + await this.args.createCard?.(spec.ref, cardIdToURL(spec.id!), { realmURL: this.args.model[realmURL], }); } diff --git a/packages/base/query-field-support.ts b/packages/base/query-field-support.ts index 657c7bf541..522d9507b9 100644 --- a/packages/base/query-field-support.ts +++ b/packages/base/query-field-support.ts @@ -13,6 +13,7 @@ import type { RuntimeDependencyTrackingContext, } from '@cardstack/runtime-common'; import { + cardIdToURL, getField, getSingularRelationship, identifyCard, @@ -361,7 +362,7 @@ function resolveQueryAndRealm( fieldPath, resolvePathValue: (path) => resolveInstancePathValue(instance, path), relativeTo: (instance as CardDef).id - ? new URL((instance as CardDef).id) + ? cardIdToURL((instance as CardDef).id) : realmURL, }); diff --git a/packages/host/app/lib/realm-utils.ts b/packages/host/app/lib/realm-utils.ts index 8e87e3fe99..24a4661467 100644 --- a/packages/host/app/lib/realm-utils.ts +++ b/packages/host/app/lib/realm-utils.ts @@ -1,4 +1,4 @@ -import { RealmPaths } from '@cardstack/runtime-common'; +import { cardIdToURL, RealmPaths } from '@cardstack/runtime-common'; /** * Normalizes realm URLs by ensuring they have trailing slashes and @@ -33,7 +33,7 @@ export function normalizeRealms(realms: string[]): string[] { * // Returns: 'http://localhost:4201/test/' */ export function resolveCardRealmUrl(cardId: string, realms: string[]): string { - let cardUrl = new URL(cardId); + let cardUrl = cardIdToURL(cardId); for (let realm of realms) { let realmUrl = new URL(realm); let realmPaths = new RealmPaths(realmUrl); diff --git a/packages/host/app/lib/utils.ts b/packages/host/app/lib/utils.ts index 873e5c0ad7..ad66671bdf 100644 --- a/packages/host/app/lib/utils.ts +++ b/packages/host/app/lib/utils.ts @@ -155,18 +155,17 @@ export const catalogRealm = ENV.resolvedCatalogRealmURL export const skillsRealm = new RealmPaths(new URL(ENV.resolvedSkillsRealmURL)); /** - * Safely constructs a URL to a skill card in the skills realm. - * Uses the URL constructor to handle path joining safely. + * Constructs a universal @cardstack/skills/ reference to a skill card. * * @param skillId - The ID of the skill (e.g., 'boxel-environment', 'catalog-listing') - * @returns The complete URL to the skill card + * @returns The universal skill card reference * * @example - * skillCardURL('catalog-listing') // 'http://localhost:4201/skills/Skill/catalog-listing' + * skillCardURL('catalog-listing') // '@cardstack/skills/Skill/catalog-listing' */ export function skillCardURL(skillId: string): string { - return skillsRealm.fileURL(`Skill/${skillId}`).href; + return `@cardstack/skills/Skill/${skillId}`; } -export const devSkillId = skillsRealm.fileURL(devSkillLocalPath).href; -export const envSkillId = skillsRealm.fileURL(envSkillLocalPath).href; +export const devSkillId = `@cardstack/skills/${devSkillLocalPath}`; +export const envSkillId = `@cardstack/skills/${envSkillLocalPath}`; diff --git a/packages/host/app/resources/room.ts b/packages/host/app/resources/room.ts index 880174958c..a011096bc8 100644 --- a/packages/host/app/resources/room.ts +++ b/packages/host/app/resources/room.ts @@ -11,6 +11,7 @@ import difference from 'lodash/difference'; import { TrackedMap } from 'tracked-built-ins'; import { + cardIdToURL, isCardInstance, type LooseSingleCardDocument, } from '@cardstack/runtime-common'; @@ -317,7 +318,7 @@ export class RoomResource extends Resource { for (let skillCard of this.allSkillFileDefs) { result.push({ cardId: skillCard.sourceUrl, - realmURL: this.realm.realmOfURL(new URL(skillCard.sourceUrl))?.href, + realmURL: this.realm.realmOfURL(cardIdToURL(skillCard.sourceUrl))?.href, fileDef: skillCard, isActive: this.matrixRoom?.skillsConfig.enabledSkillCards diff --git a/packages/host/app/routes/render.ts b/packages/host/app/routes/render.ts index 3edf2346e2..632304e260 100644 --- a/packages/host/app/routes/render.ts +++ b/packages/host/app/routes/render.ts @@ -298,7 +298,7 @@ export default class RenderRoute extends Route { let instance = (await this.store.addFileMeta( resource, doc, - resource.id ? new URL(resource.id) : undefined, + resource.id ? cardIdToURL(resource.id) : undefined, )) as unknown as CardDef; let state = new TrackedMap(); diff --git a/packages/host/app/services/network.ts b/packages/host/app/services/network.ts index a754f07d4c..c452419d57 100644 --- a/packages/host/app/services/network.ts +++ b/packages/host/app/services/network.ts @@ -67,6 +67,14 @@ export default class NetworkService extends Service { (rest) => new URL(rest, catalogURL).href, ); } + if (config.resolvedSkillsRealmURL) { + let skillsURL = withTrailingSlash(config.resolvedSkillsRealmURL); + registerCardReferencePrefix('@cardstack/skills/', skillsURL); + virtualNetwork.addImportMap( + '@cardstack/skills/', + (rest) => new URL(rest, skillsURL).href, + ); + } if (config.resolvedOpenRouterRealmURL) { let openRouterURL = withTrailingSlash(config.resolvedOpenRouterRealmURL); registerCardReferencePrefix('@cardstack/openrouter/', openRouterURL); diff --git a/packages/host/app/services/operator-mode-state-service.ts b/packages/host/app/services/operator-mode-state-service.ts index 6130a66596..97af4c97b2 100644 --- a/packages/host/app/services/operator-mode-state-service.ts +++ b/packages/host/app/services/operator-mode-state-service.ts @@ -13,9 +13,9 @@ import { TrackedArray, TrackedMap, TrackedObject } from 'tracked-built-ins'; import type { CodeRef } from '@cardstack/runtime-common'; import { + cardIdToURL, RealmPaths, type LocalPath, - cardIdToURL, isResolvedCodeRef, isCardInstance, isLocalId, @@ -312,7 +312,7 @@ export default class OperatorModeStateService extends Service { this.trimItemsFromStack(item); } let realmPaths = new RealmPaths(new URL(cardRealmUrl)); - let cardPath = realmPaths.local(new URL(`${cardId}.json`)); + let cardPath = realmPaths.local(cardIdToURL(`${cardId}.json`)); this.recentFilesService.removeRecentFile(cardPath); this.recentCardsService.remove(cardId); } diff --git a/packages/host/app/services/store.ts b/packages/host/app/services/store.ts index 2b08c526fc..f3c7344b38 100644 --- a/packages/host/app/services/store.ts +++ b/packages/host/app/services/store.ts @@ -19,6 +19,7 @@ import { baseFileRef, CardError, cardIdToURL, + isRegisteredPrefix, hasExecutableExtension, isCardError, isCardInstance, @@ -1312,7 +1313,7 @@ export default class StoreService extends Service implements StoreInterface { return this.createFileMetaFromSerialized( resource, doc, - new URL(resource.id), + cardIdToURL(resource.id), dependencyTrackingContext, ) as Promise; } @@ -1416,7 +1417,7 @@ export default class StoreService extends Service implements StoreInterface { deferred?.fulfill(existingInstance as T | CardErrorJSONAPI); return existingInstance as T; } - if (isLocalId(id)) { + if (isLocalId(id) && !isRegisteredPrefix(id)) { // we might have lost the local id via a loader refresh, try loading from remote id instead let remoteId = this.store.getRemoteIds(id)?.[0]; if (!remoteId) { @@ -1426,7 +1427,9 @@ export default class StoreService extends Service implements StoreInterface { } id = remoteId; } - let url = id; // after this point we know we are dealing with a remote id, e.g. url + // Resolve registered prefix IDs (e.g. @cardstack/skills/...) to actual + // URLs so they can be used for fetching. + let url = isRegisteredPrefix(id) ? cardIdToURL(id).href : id; let doc = (typeof idOrDoc !== 'string' ? idOrDoc : undefined) as | SingleCardDocument | undefined; @@ -1546,10 +1549,10 @@ export default class StoreService extends Service implements StoreInterface { deferred.fulfill(existingInstance as T | CardErrorJSONAPI); return existingInstance as T | CardErrorJSONAPI; } - if (isLocalId(id)) { + if (isLocalId(id) && !isRegisteredPrefix(id)) { throw new Error(`file-meta reads do not support local ids (${id})`); } - let url = id; + let url = isRegisteredPrefix(id) ? cardIdToURL(id).href : id; let fileMetaDoc: SingleFileMetaDocument | CardError; if (this.isRenderStore && (globalThis as any).__boxelRenderContext) { fileMetaDoc = await this.extractFileMetaDirectly(url); @@ -1565,7 +1568,7 @@ export default class StoreService extends Service implements StoreInterface { let fileInstance = await api.createFromSerialized( fileMetaDoc.data, fileMetaDoc, - fileMetaDoc.data.id ? new URL(fileMetaDoc.data.id) : new URL(url), + fileMetaDoc.data.id ? cardIdToURL(fileMetaDoc.data.id) : new URL(url), { store: this.store, dependencyTrackingContext: opts?.dependencyTrackingContext, diff --git a/packages/matrix/helpers/isolated-realm-server.ts b/packages/matrix/helpers/isolated-realm-server.ts index e4055dd79b..f64f3508e3 100644 --- a/packages/matrix/helpers/isolated-realm-server.ts +++ b/packages/matrix/helpers/isolated-realm-server.ts @@ -244,6 +244,10 @@ export async function startServer({ `--fromUrl='http://localhost:4205/test/'`, `--toUrl='http://localhost:4205/test/'`, ]; + workerArgs = workerArgs.concat([ + `--fromUrl='@cardstack/skills/'`, + `--toUrl='http://localhost:4205/skills/'`, + ]); workerArgs = workerArgs.concat([ `--fromUrl='https://cardstack.com/base/'`, `--toUrl='http://localhost:4205/base/'`, @@ -282,7 +286,7 @@ export async function startServer({ serverArgs = serverArgs.concat([ `--username='skills_realm'`, `--path='${skillsRealmDir}'`, - `--fromUrl='http://localhost:4205/skills/'`, + `--fromUrl='@cardstack/skills/'`, `--toUrl='http://localhost:4205/skills/'`, ]); serverArgs = serverArgs.concat([ diff --git a/packages/matrix/tests/skills.spec.ts b/packages/matrix/tests/skills.spec.ts index 629d521d22..7510be425c 100644 --- a/packages/matrix/tests/skills.spec.ts +++ b/packages/matrix/tests/skills.spec.ts @@ -50,9 +50,9 @@ test.describe('Skills', () => { ).toContainClass('checked'); } - const environmentSkillCardId = `http://localhost:4205/skills/Skill/boxel-environment`; + const environmentSkillCardId = `@cardstack/skills/Skill/boxel-environment`; const defaultSkillCardsForCodeMode = [ - `http://localhost:4205/skills/Skill/boxel-development`, + `@cardstack/skills/Skill/boxel-development`, environmentSkillCardId, ]; const skillCard1 = `${appURL}/skill-pirate-speak`; diff --git a/packages/realm-server/scripts/start-production.sh b/packages/realm-server/scripts/start-production.sh index 139f958931..bb59e7cd8f 100755 --- a/packages/realm-server/scripts/start-production.sh +++ b/packages/realm-server/scripts/start-production.sh @@ -60,7 +60,7 @@ NODE_NO_WARNINGS=1 \ \ --path='/persistent/skills' \ --username='skills_realm' \ - --fromUrl='https://app.boxel.ai/skills/' \ + --fromUrl='@cardstack/skills/' \ --toUrl='https://app.boxel.ai/skills/' \ \ --path='/persistent/boxel-homepage' \ diff --git a/packages/realm-server/scripts/start-staging.sh b/packages/realm-server/scripts/start-staging.sh index 207381bb6a..a62ccd5301 100755 --- a/packages/realm-server/scripts/start-staging.sh +++ b/packages/realm-server/scripts/start-staging.sh @@ -60,7 +60,7 @@ NODE_NO_WARNINGS=1 \ \ --path='/persistent/skills' \ --username='skills_realm' \ - --fromUrl='https://realms-staging.stack.cards/skills/' \ + --fromUrl='@cardstack/skills/' \ --toUrl='https://realms-staging.stack.cards/skills/' \ \ --path='/persistent/boxel-homepage' \ diff --git a/packages/realm-server/scripts/start-worker-production.sh b/packages/realm-server/scripts/start-worker-production.sh index 9993e12d4f..09df8f7bb9 100755 --- a/packages/realm-server/scripts/start-worker-production.sh +++ b/packages/realm-server/scripts/start-worker-production.sh @@ -28,7 +28,7 @@ NODE_NO_WARNINGS=1 \ --fromUrl='@cardstack/catalog/' \ --toUrl="${CATALOG_REALM_URL}" \ \ - --fromUrl='https://app.boxel.ai/skills/' \ + --fromUrl='@cardstack/skills/' \ --toUrl='https://app.boxel.ai/skills/' \ \ --fromUrl="${EXTERNAL_CATALOG_REALM_URL}" \ diff --git a/packages/realm-server/scripts/start-worker-staging.sh b/packages/realm-server/scripts/start-worker-staging.sh index c34cb4d4a6..340ef231cc 100755 --- a/packages/realm-server/scripts/start-worker-staging.sh +++ b/packages/realm-server/scripts/start-worker-staging.sh @@ -33,7 +33,7 @@ NODE_NO_WARNINGS=1 \ --fromUrl="${EXTERNAL_CATALOG_REALM_URL}" \ --toUrl="${EXTERNAL_CATALOG_REALM_URL}" \ \ - --fromUrl='https://realms-staging.stack.cards/skills/' \ + --fromUrl='@cardstack/skills/' \ --toUrl='https://realms-staging.stack.cards/skills/' \ \ --fromUrl='@cardstack/openrouter/' \ diff --git a/packages/runtime-common/card-reference-resolver.ts b/packages/runtime-common/card-reference-resolver.ts index 3d6a8c62aa..1a63287a89 100644 --- a/packages/runtime-common/card-reference-resolver.ts +++ b/packages/runtime-common/card-reference-resolver.ts @@ -39,6 +39,19 @@ export function resolveCardReference( `Cannot resolve bare package specifier "${reference}" — no matching prefix mapping registered`, ); } + if (reference.startsWith('http://') || reference.startsWith('https://')) { + return new URL(reference).href; + } + // If relativeTo is a prefix-form ID (e.g. @cardstack/skills/Foo/bar), + // resolve it to a real URL before using it as a base. + if (typeof relativeTo === 'string') { + for (let [prefix, target] of prefixMappings) { + if (relativeTo.startsWith(prefix)) { + relativeTo = new URL(relativeTo.slice(prefix.length), target).href; + break; + } + } + } return new URL(reference, relativeTo).href; }