@notis_ai/cli 0.2.0-beta.19.1 → 0.2.0-beta.196.1
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 +822 -135
- package/bin/check-runtime.js +15 -0
- package/bin/notis.js +2 -0
- package/config/notis_app_boundary_rules.json +50 -0
- package/config/notis_app_design_rules.json +135 -0
- package/config/portal-route-roots.json +48 -0
- package/dist/agent-hooks/notis-agent-hook.mjs +45587 -0
- package/dist/base-skills/notis-apps/SKILL.md +93 -0
- package/dist/base-skills/notis-apps/references/architecture.md +114 -0
- package/dist/base-skills/notis-apps/references/context.md +81 -0
- package/dist/base-skills/notis-apps/references/design.md +97 -0
- package/dist/base-skills/notis-apps/references/memory.md +58 -0
- package/dist/base-skills/notis-apps/references/reading.md +67 -0
- package/dist/base-skills/notis-apps/references/release.md +107 -0
- package/dist/base-skills/notis-apps/references/sdk.md +116 -0
- package/dist/base-skills/notis-apps/references/troubleshooting.md +24 -0
- package/dist/base-skills/notis-apps/references/views.md +142 -0
- package/dist/base-skills/notis-cli/SKILL.md +154 -0
- package/dist/base-skills/notis-cli/references/app-delivery.md +19 -0
- package/dist/base-skills/notis-cli/references/intelligence.md +94 -0
- package/dist/base-skills/notis-cli/references/native-databases.md +21 -0
- package/dist/base-skills/notis-cli/references/tool-examples.md +56 -0
- package/dist/base-skills/notis-cli/references/troubleshooting.md +39 -0
- package/dist/base-skills/notis-query/SKILL.md +41 -0
- package/dist/base-skills/notis-query/references/database-discovery.md +60 -0
- package/dist/base-skills/notis-query/references/documents.md +63 -0
- package/dist/base-skills/notis-query/references/query.md +76 -0
- package/{template/packages/notis-sdk → dist/sdk}/package.json +14 -4
- package/dist/sdk/src/agentContext.ts +36 -0
- package/dist/sdk/src/bundleModule.ts +79 -0
- package/dist/sdk/src/components/Dialog.tsx +46 -0
- package/dist/sdk/src/components/DocumentEditor.tsx +106 -0
- package/dist/sdk/src/components/DocumentPage.tsx +141 -0
- package/dist/sdk/src/components/Markdown.tsx +60 -0
- package/dist/sdk/src/components/MarkdownEditor.tsx +121 -0
- package/dist/sdk/src/components/MultiSelectActionBar.tsx +285 -0
- package/dist/sdk/src/components/MultiSelectCheckbox.tsx +97 -0
- package/dist/sdk/src/components/MultiSelectDragOverlay.tsx +39 -0
- package/dist/sdk/src/components/NotisCommentBoundary.tsx +172 -0
- package/dist/sdk/src/components/NotisSelectionBoundary.tsx +59 -0
- package/dist/sdk/src/components/RecordComponents.tsx +33 -0
- package/dist/sdk/src/components/RenderChartData.tsx +19 -0
- package/dist/sdk/src/components/ShortcutHints.tsx +56 -0
- package/dist/sdk/src/components/Skeleton.tsx +24 -0
- package/dist/sdk/src/config.ts +239 -0
- package/dist/sdk/src/documents.ts +258 -0
- package/dist/sdk/src/hooks/useAction.ts +28 -0
- package/dist/sdk/src/hooks/useActiveResource.ts +19 -0
- package/dist/sdk/src/hooks/useAgentContext.ts +23 -0
- package/dist/sdk/src/hooks/useCloudComputer.ts +95 -0
- package/dist/sdk/src/hooks/useCloudComputerShell.ts +114 -0
- package/dist/sdk/src/hooks/useCollectionInteractions.ts +838 -0
- package/dist/sdk/src/hooks/useDatabaseSchema.ts +49 -0
- package/dist/sdk/src/hooks/useDatabaseSubscription.ts +76 -0
- package/dist/sdk/src/hooks/useDocument.ts +43 -0
- package/dist/sdk/src/hooks/useDocuments.ts +84 -0
- package/dist/sdk/src/hooks/useHandover.ts +78 -0
- package/dist/sdk/src/hooks/useLongPressSelection.ts +79 -0
- package/dist/sdk/src/hooks/useMultiSelect.ts +95 -0
- package/dist/sdk/src/hooks/useNativeDocumentBody.ts +50 -0
- package/dist/sdk/src/hooks/useNativeDocuments.ts +83 -0
- package/dist/sdk/src/hooks/useNotis.ts +42 -0
- package/dist/sdk/src/hooks/useNotisNavigation.ts +67 -0
- package/dist/sdk/src/hooks/useQuery.ts +74 -0
- package/dist/sdk/src/hooks/useTool.ts +65 -0
- package/dist/sdk/src/hooks/useToolQuery.ts +12 -0
- package/dist/sdk/src/hooks/useTopBarSearch.ts +81 -0
- package/dist/sdk/src/hooks/useUpsertDocument.ts +95 -0
- package/dist/sdk/src/hooks/useViewParams.ts +121 -0
- package/dist/sdk/src/hooks/useViewerReads.ts +129 -0
- package/dist/sdk/src/index.ts +200 -0
- package/dist/sdk/src/interactions/actions.ts +59 -0
- package/dist/sdk/src/interactions/shortcuts.tsx +740 -0
- package/dist/sdk/src/interactions/visibility.ts +13 -0
- package/dist/sdk/src/interactions.ts +45 -0
- package/dist/sdk/src/nativeData.ts +39 -0
- package/dist/sdk/src/presentation.tsx +82 -0
- package/dist/sdk/src/provider.tsx +49 -0
- package/dist/sdk/src/queryCache.ts +172 -0
- package/dist/sdk/src/queryErrors.ts +51 -0
- package/dist/sdk/src/runtime.ts +603 -0
- package/dist/sdk/src/space.ts +425 -0
- package/dist/sdk/src/styles.css +290 -0
- package/dist/sdk/src/tailwind.ts +66 -0
- package/dist/sdk/src/vite.ts +83 -0
- package/dist/skill-sync/index.js +2143 -0
- package/dist/skill-sync/index.js.map +7 -0
- package/dist/skill-sync-worker.mjs +3487 -0
- package/dist/space-harness/host.js +66 -0
- package/dist/view-renderer/dist/host.css +1 -0
- package/dist/view-renderer/dist/host.js +1042 -0
- package/dist/view-renderer/dist/theme.css +73 -0
- package/dist/view-renderer/src/dom.js +180 -0
- package/dist/view-renderer/src/errors.js +8 -0
- package/dist/view-renderer/src/host.tsx +173 -0
- package/dist/view-renderer/src/index.js +4 -0
- package/dist/view-renderer/src/pool.js +76 -0
- package/dist/view-renderer/src/remote-broker.js +59 -0
- package/dist/view-renderer/src/renderer.js +201 -0
- package/package.json +25 -8
- package/skills/notis-apps/cli.md +406 -0
- package/skills/notis-cli/AGENT_INSTRUCTIONS.md +39 -0
- package/skills/notis-onboarding/BRIEF.md +133 -0
- package/skills/notis-query/cli.md +39 -0
- package/src/agent-hook-entry.js +5 -0
- package/src/cli.js +294 -25
- package/src/command-specs/agents.js +392 -0
- package/src/command-specs/apps.js +1476 -203
- package/src/command-specs/auth.js +114 -137
- package/src/command-specs/diagnostics.js +730 -0
- package/src/command-specs/files.js +29 -0
- package/src/command-specs/handover.js +374 -0
- package/src/command-specs/helpers.js +86 -82
- package/src/command-specs/index.js +31 -6
- package/src/command-specs/meta.js +150 -18
- package/src/command-specs/onboarding.js +290 -0
- package/src/command-specs/profile.js +358 -0
- package/src/command-specs/skills.js +304 -0
- package/src/command-specs/smoke.js +386 -0
- package/src/command-specs/spaces.js +572 -0
- package/src/command-specs/tools.js +455 -139
- package/src/command-specs/views.js +69 -0
- package/src/runtime/agent-browser.js +700 -0
- package/src/runtime/agent-memory-state.js +126 -0
- package/src/runtime/agent-setup.js +383 -0
- package/src/runtime/app-boundary-validator.js +407 -0
- package/src/runtime/app-changelog.js +79 -0
- package/src/runtime/app-data-literals.js +82 -0
- package/src/runtime/app-platform.js +2752 -237
- package/src/runtime/app-registry-scaffolds.js +367 -0
- package/src/runtime/app-test-server.js +293 -0
- package/src/runtime/assets/store-screenshot-dark.png +0 -0
- package/src/runtime/auth-recovery.js +110 -0
- package/src/runtime/base-skills.d.ts +20 -0
- package/src/runtime/base-skills.js +167 -0
- package/src/runtime/channel.js +133 -0
- package/src/runtime/confined-space-build.js +74 -0
- package/src/runtime/confined-space-verify.js +49 -0
- package/src/runtime/delegated-context.js +68 -0
- package/src/runtime/errors.js +1 -0
- package/src/runtime/git.js +233 -0
- package/src/runtime/login-listener.js +15 -0
- package/src/runtime/native-record-files.js +47 -0
- package/src/runtime/oauth.js +2622 -0
- package/src/runtime/output.js +37 -5
- package/src/runtime/ports.js +31 -0
- package/src/runtime/profiles.js +906 -55
- package/src/runtime/retired-sdk-files.js +20 -0
- package/src/runtime/screenshot-framing.js +85 -0
- package/src/runtime/skill-sync/cloud-client.ts +121 -0
- package/src/runtime/skill-sync/index.ts +905 -0
- package/src/runtime/skill-sync/local-scanner.ts +1259 -0
- package/src/runtime/skill-sync/native-identity.ts +102 -0
- package/src/runtime/skill-sync/native-plan.ts +59 -0
- package/src/runtime/skill-sync/symlink-manager.ts +433 -0
- package/src/runtime/skill-sync/sync-plan.ts +54 -0
- package/src/runtime/skill-sync/types.ts +157 -0
- package/src/runtime/skill-sync/write-cloud-skill.ts +50 -0
- package/src/runtime/skill-sync-service.js +109 -0
- package/src/runtime/source-root.js +32 -0
- package/src/runtime/space-collection-scaffold.js +66 -0
- package/src/runtime/space-database-structures.js +229 -0
- package/src/runtime/space-fixture-broker.js +50 -0
- package/src/runtime/space-lock.js +337 -0
- package/src/runtime/space-platform.js +105 -0
- package/src/runtime/space-source.js +281 -0
- package/src/runtime/space-test-server.js +327 -0
- package/src/runtime/space-verification.js +89 -0
- package/src/runtime/space-verify-browser-worker.js +51 -0
- package/src/runtime/space-view-manifest.js +254 -0
- package/src/runtime/store-screenshot.js +146 -0
- package/src/runtime/sync-skills.d.ts +37 -0
- package/src/runtime/sync-skills.js +231 -0
- package/src/runtime/telemetry.js +92 -0
- package/src/runtime/transport.js +331 -45
- package/src/runtime/view-render.js +49 -0
- package/src/skill-sync-worker-entry.js +2 -0
- package/src/skill-sync-worker.js +50 -0
- package/src/space-harness-host.ts +103 -0
- package/template/.harness/index.html.tmpl +435 -0
- package/template/CHANGELOG.md +5 -0
- package/template/app/globals.css +29 -3
- package/template/app/layout.tsx +6 -3
- package/template/app/page.tsx +49 -42
- package/template/components/page-heading.tsx +23 -0
- package/template/components/ui/badge.tsx +7 -4
- package/template/components/ui/button.tsx +1 -1
- package/template/components/ui/card.tsx +24 -11
- package/template/components/ui/native-select.tsx +24 -0
- package/template/notis.config.ts +24 -6
- package/template/package-lock.json +3642 -0
- package/template/package.json +19 -16
- package/template/postcss.config.mjs +1 -1
- package/template/tailwind.config.ts +1 -6
- package/template/tsconfig.json +1 -0
- package/src/command-specs/db.js +0 -163
- package/src/runtime/app-preview-server.js +0 -312
- package/template/packages/notis-sdk/src/config.ts +0 -48
- package/template/packages/notis-sdk/src/helpers.ts +0 -131
- package/template/packages/notis-sdk/src/hooks/useAppState.ts +0 -50
- package/template/packages/notis-sdk/src/hooks/useCollectionItem.ts +0 -58
- package/template/packages/notis-sdk/src/hooks/useDatabase.ts +0 -87
- package/template/packages/notis-sdk/src/hooks/useDocument.ts +0 -61
- package/template/packages/notis-sdk/src/hooks/useNotis.ts +0 -31
- package/template/packages/notis-sdk/src/hooks/useNotisNavigation.ts +0 -49
- package/template/packages/notis-sdk/src/hooks/useTool.ts +0 -49
- package/template/packages/notis-sdk/src/hooks/useUpsertDocument.ts +0 -57
- package/template/packages/notis-sdk/src/index.ts +0 -47
- package/template/packages/notis-sdk/src/provider.tsx +0 -44
- package/template/packages/notis-sdk/src/runtime.ts +0 -159
- package/template/packages/notis-sdk/src/styles.css +0 -123
- package/template/packages/notis-sdk/src/vite.ts +0 -54
- package/template/packages/notis-sdk/tsconfig.json +0 -15
- /package/{template/packages/notis-sdk → dist/sdk}/src/hooks/useBackend.ts +0 -0
- /package/{template/packages/notis-sdk → dist/sdk}/src/hooks/useTools.ts +0 -0
- /package/{template/packages/notis-sdk → dist/sdk}/src/ui.ts +0 -0
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/** Pure fixture adapter for the exact CLI/service renderer; no network client. */
|
|
2
|
+
import { createSpaceFixtureEngine, spaceHarnessSnapshot } from './space-test-server.js';
|
|
3
|
+
import { usageError } from './errors.js';
|
|
4
|
+
|
|
5
|
+
const object = value => value && typeof value === 'object' && !Array.isArray(value);
|
|
6
|
+
const exact = (value, keys) => object(value) && Object.keys(value).sort().join(',') === [...keys].sort().join(',');
|
|
7
|
+
|
|
8
|
+
export function createSpaceFixtureBroker({ release, capabilities, width, outputs = ['markdown', 'screenshot'],
|
|
9
|
+
authorizationMode = 'authorize_missing', unavailableActions = [] }) {
|
|
10
|
+
const engine = createSpaceFixtureEngine(release);
|
|
11
|
+
const frozen = spaceHarnessSnapshot(release, capabilities, engine, { authorizationMode, unavailableActions });
|
|
12
|
+
const calls = [];
|
|
13
|
+
const params = structuredClone(engine.context.params || {});
|
|
14
|
+
const value = { ...frozen, url: `https://app.notis.ai/verification/${encodeURIComponent(release.manifest.local_key)}`,
|
|
15
|
+
view: { space_id: frozen.descriptor.resource.id, name: release.manifest.name, revision: 1 },
|
|
16
|
+
locale: engine.context.locale === 'fr' ? 'fr' : 'en', params,
|
|
17
|
+
markdownExport: release.manifest.markdown?.export_name || null, render_options: { outputs, width } };
|
|
18
|
+
return {
|
|
19
|
+
fixtureDigest: engine.digest,
|
|
20
|
+
calls,
|
|
21
|
+
async snapshot() { return structuredClone(value); },
|
|
22
|
+
async read(request) {
|
|
23
|
+
let result;
|
|
24
|
+
const log = { operation: request?.operation, ...(request?.action_id ? { actionId: request.action_id } : {}), ok: false };
|
|
25
|
+
calls.push(log);
|
|
26
|
+
if (calls.length > 256) throw usageError('The offline fixture read limit was reached.');
|
|
27
|
+
if (request?.operation === 'action' && exact(request, ['operation', 'action_id', 'inputs', 'request_id'])) {
|
|
28
|
+
if (frozen.descriptor.space.actions[request.action_id]?.readOnly !== true) throw usageError('Offline verification never runs write actions.');
|
|
29
|
+
result = engine.execute(request.action_id, request.inputs);
|
|
30
|
+
} else if (request?.operation === 'shown' && exact(request, ['operation', 'name', 'params', 'options'])) {
|
|
31
|
+
result = engine.executeShown(request.name, request.params, request.options);
|
|
32
|
+
} else if (request?.operation === 'viewer_read' && exact(request, ['operation', 'name', 'input'])) {
|
|
33
|
+
result = engine.executeViewerRead(request.name, request.input);
|
|
34
|
+
} else if (request?.operation === 'document_body' && exact(request, ['operation', 'record_key', 'binding', 'read_action'])) {
|
|
35
|
+
if (frozen.descriptor.space.actions[request.read_action]?.readOnly !== true) throw usageError('This document read action is unavailable.');
|
|
36
|
+
result = engine.executeBody({ operation: 'read', recordKey: request.record_key, binding: request.binding, readAction: request.read_action });
|
|
37
|
+
} else if (['record', 'html', 'report'].includes(request?.operation) && exact(request, ['operation', 'record_key'])) {
|
|
38
|
+
result = engine.executeRecordView(request.operation, request.record_key);
|
|
39
|
+
} else {
|
|
40
|
+
throw usageError('Only declared read-only synthetic fixtures are available.');
|
|
41
|
+
}
|
|
42
|
+
log.ok = true;
|
|
43
|
+
return result;
|
|
44
|
+
},
|
|
45
|
+
async finish() {
|
|
46
|
+
if (calls.some(value => !value.ok)) throw usageError('A synthetic fixture read failed.');
|
|
47
|
+
return { valid: true };
|
|
48
|
+
},
|
|
49
|
+
};
|
|
50
|
+
}
|
|
@@ -0,0 +1,337 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Local record of a pulled Space: its resource list and the version of every
|
|
3
|
+
* editable Skill folder (R5).
|
|
4
|
+
*
|
|
5
|
+
* `notis spaces pull` writes the Space's links to `resources.json`, every
|
|
6
|
+
* editable linked Skill to `skills/<alias>/`, and this lock to
|
|
7
|
+
* `.notis/space-lock.json` (local-only state, never part of the portable
|
|
8
|
+
* source). `notis spaces deploy` compares the working tree with the lock and
|
|
9
|
+
* sends only what changed since the pull, each item with the version it was
|
|
10
|
+
* pulled at, so edits and links made elsewhere are kept and a conflict is named
|
|
11
|
+
* before anything is uploaded.
|
|
12
|
+
*/
|
|
13
|
+
import { createHash } from 'node:crypto';
|
|
14
|
+
import { existsSync, lstatSync, mkdirSync, readdirSync, readFileSync, renameSync, rmSync, writeFileSync } from 'node:fs';
|
|
15
|
+
import { randomUUID } from 'node:crypto';
|
|
16
|
+
import { dirname, join, resolve } from 'node:path';
|
|
17
|
+
|
|
18
|
+
import { appFilesDigest, readSpaceStateFile, writeSpaceStateFile } from './app-platform.js';
|
|
19
|
+
import { usageError } from './errors.js';
|
|
20
|
+
|
|
21
|
+
export const SPACE_LOCK_FILE = 'space-lock.json';
|
|
22
|
+
export const RESOURCES_FILE = 'resources.json';
|
|
23
|
+
export const SKILLS_DIR = 'skills';
|
|
24
|
+
const ALIAS = /^[a-z][a-z0-9_-]{0,99}$/;
|
|
25
|
+
// Links staged from a legacy App without a declared name keep the migration's `<kind>:<id>` alias until renamed
|
|
26
|
+
// (`notis spaces resources name`); they can be listed and kept, never used as a Skill folder or a new entry.
|
|
27
|
+
const STAGED_ALIAS = /^(database|skill|automation):[A-Za-z0-9-]{1,64}$/;
|
|
28
|
+
export function isFolderAlias(alias) { return typeof alias === 'string' && ALIAS.test(alias); }
|
|
29
|
+
const LINK_KINDS = new Set(['database', 'skill', 'automation']);
|
|
30
|
+
const EXCLUDED_ENTRIES = new Set(['node_modules', '__pycache__', '.ds_store', '.git', '.notis', '.context']);
|
|
31
|
+
|
|
32
|
+
function stableJson(value) {
|
|
33
|
+
if (Array.isArray(value)) return `[${value.map(stableJson).join(',')}]`;
|
|
34
|
+
if (value && typeof value === 'object') {
|
|
35
|
+
return `{${Object.keys(value).sort().map((key) => `${JSON.stringify(key)}:${stableJson(value[key])}`).join(',')}}`;
|
|
36
|
+
}
|
|
37
|
+
return JSON.stringify(value);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function excluded(name) {
|
|
41
|
+
const lower = name.toLowerCase();
|
|
42
|
+
return EXCLUDED_ENTRIES.has(lower) || lower.startsWith('.env') || lower.endsWith('.pyc') || lower.endsWith('.pyo');
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function safeRelativePath(value, label) {
|
|
46
|
+
if (typeof value !== 'string' || !value || value.includes('\\') || value.includes('\0') || value.startsWith('/')
|
|
47
|
+
|| value.split('/').some((part) => !part || part === '.' || part === '..')) {
|
|
48
|
+
throw usageError(`${label} contains an unsafe file path: ${String(value)}`);
|
|
49
|
+
}
|
|
50
|
+
return value;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** Links of the three linkable kinds as the portable list `[{kind, id, alias}]`, sorted by alias. */
|
|
54
|
+
export function resourceEntries(links) {
|
|
55
|
+
return (Array.isArray(links) ? links : [])
|
|
56
|
+
.filter((link) => LINK_KINDS.has(link?.kind))
|
|
57
|
+
.map((link) => ({ kind: link.kind, id: link.resource_id, alias: link.alias }))
|
|
58
|
+
.sort((a, b) => (a.alias < b.alias ? -1 : a.alias > b.alias ? 1 : 0));
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** The pulled list as the lock records it: identity, alias, binding and availability. */
|
|
62
|
+
export function lockLinks(links) {
|
|
63
|
+
return (Array.isArray(links) ? links : [])
|
|
64
|
+
.filter((link) => LINK_KINDS.has(link?.kind))
|
|
65
|
+
.map((link) => ({ kind: link.kind, id: link.resource_id, alias: link.alias, binding_id: link.binding_id,
|
|
66
|
+
binding_revision: link.revision, available: link.available !== false }))
|
|
67
|
+
.sort((a, b) => (a.alias < b.alias ? -1 : a.alias > b.alias ? 1 : 0));
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export function readSpaceLock(projectDir) {
|
|
71
|
+
const lock = readSpaceStateFile(projectDir, SPACE_LOCK_FILE);
|
|
72
|
+
if (lock === null) return null;
|
|
73
|
+
if (!lock || typeof lock !== 'object' || lock.version !== 1 || typeof lock.space_id !== 'string' || !Array.isArray(lock.links)
|
|
74
|
+
|| !lock.skills || typeof lock.skills !== 'object' || Array.isArray(lock.skills)) {
|
|
75
|
+
throw usageError('The Space lock file (.notis/space-lock.json) is unreadable. Pull the Space again.');
|
|
76
|
+
}
|
|
77
|
+
return lock;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export function writeSpaceLock(projectDir, lock) {
|
|
81
|
+
return writeSpaceStateFile(projectDir, SPACE_LOCK_FILE, lock);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export function readResourceList(projectDir) {
|
|
85
|
+
const path = join(resolve(projectDir), RESOURCES_FILE);
|
|
86
|
+
if (!existsSync(path)) return null;
|
|
87
|
+
if (lstatSync(path).isSymbolicLink()) throw usageError(`${RESOURCES_FILE} must be a regular file.`);
|
|
88
|
+
let parsed;
|
|
89
|
+
try { parsed = JSON.parse(readFileSync(path, 'utf8')); } catch { throw usageError(`${RESOURCES_FILE} must be valid JSON.`); }
|
|
90
|
+
if (!Array.isArray(parsed)) throw usageError(`${RESOURCES_FILE} must be a JSON array of {kind, id, alias} entries.`);
|
|
91
|
+
const seen = new Set();
|
|
92
|
+
return parsed.map((entry, index) => {
|
|
93
|
+
if (!entry || typeof entry !== 'object' || Array.isArray(entry) || Object.keys(entry).sort().join(',') !== 'alias,id,kind'
|
|
94
|
+
|| !LINK_KINDS.has(entry.kind) || typeof entry.id !== 'string' || !entry.id || typeof entry.alias !== 'string'
|
|
95
|
+
|| !(ALIAS.test(entry.alias) || STAGED_ALIAS.test(entry.alias))) {
|
|
96
|
+
throw usageError(`${RESOURCES_FILE}[${index}] must be {kind: database|skill|automation, id, alias} with a lowercase alias.`);
|
|
97
|
+
}
|
|
98
|
+
const key = `${entry.kind}:${entry.id}`;
|
|
99
|
+
if (seen.has(key)) throw usageError(`${RESOURCES_FILE} lists ${entry.kind} ${entry.id} twice.`);
|
|
100
|
+
seen.add(key);
|
|
101
|
+
return { kind: entry.kind, id: entry.id, alias: entry.alias };
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export function writeResourceList(projectDir, entries) {
|
|
106
|
+
const root = resolve(projectDir);
|
|
107
|
+
const temporary = join(root, `.${RESOURCES_FILE}-${randomUUID()}`);
|
|
108
|
+
try {
|
|
109
|
+
writeFileSync(temporary, JSON.stringify(entries, null, 2) + '\n', { flag: 'wx' });
|
|
110
|
+
renameSync(temporary, join(root, RESOURCES_FILE));
|
|
111
|
+
} finally { rmSync(temporary, { force: true }); }
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function walkSkillFolder(directory, prefix, files) {
|
|
115
|
+
for (const entry of readdirSync(directory, { withFileTypes: true })) {
|
|
116
|
+
if (entry.isSymbolicLink() || excluded(entry.name)) continue;
|
|
117
|
+
const path = prefix ? `${prefix}/${entry.name}` : entry.name;
|
|
118
|
+
if (entry.isDirectory()) walkSkillFolder(join(directory, entry.name), path, files);
|
|
119
|
+
else if (entry.isFile()) files[path] = readFileSync(join(directory, entry.name)).toString('base64');
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/** Every `skills/<alias>/` folder as {alias: {files: {path: base64}, digest}}. */
|
|
124
|
+
export function collectSkillFolders(projectDir) {
|
|
125
|
+
const root = join(resolve(projectDir), SKILLS_DIR);
|
|
126
|
+
const folders = {};
|
|
127
|
+
if (!existsSync(root) || lstatSync(root).isSymbolicLink() || !lstatSync(root).isDirectory()) return folders;
|
|
128
|
+
for (const entry of readdirSync(root, { withFileTypes: true })) {
|
|
129
|
+
if (entry.isSymbolicLink() || !entry.isDirectory()) continue;
|
|
130
|
+
if (!ALIAS.test(entry.name)) throw usageError(`${SKILLS_DIR}/${entry.name}: name the folder with the Skill alias (lowercase letters, digits, - or _).`);
|
|
131
|
+
const files = {};
|
|
132
|
+
walkSkillFolder(join(root, entry.name), '', files);
|
|
133
|
+
if (!files['SKILL.md']) throw usageError(`${SKILLS_DIR}/${entry.name} needs a SKILL.md.`);
|
|
134
|
+
folders[entry.name] = { files, digest: appFilesDigest(files) };
|
|
135
|
+
}
|
|
136
|
+
return folders;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/** The files of a Skill bundle a local folder keeps (the same exclusions as collectSkillFolders). */
|
|
140
|
+
export function skillFolderFiles(files) {
|
|
141
|
+
return Object.fromEntries(Object.entries(files || {}).filter(([path]) => !path.split('/').some(excluded)));
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/** Current linked Skill files own their folder; replacement requires the exact pulled snapshot. */
|
|
145
|
+
export function writeSkillFolder(projectDir, alias, files, { sourceSnapshot } = {}) {
|
|
146
|
+
if (!ALIAS.test(alias)) throw usageError(`Unsafe Skill alias: ${alias}`);
|
|
147
|
+
const root = join(resolve(projectDir), SKILLS_DIR, alias);
|
|
148
|
+
const entries = Object.entries(files || {}).map(([path, encoded]) => {
|
|
149
|
+
safeRelativePath(path, `Skill ${alias}`);
|
|
150
|
+
return [path, Buffer.from(encoded, 'base64')];
|
|
151
|
+
});
|
|
152
|
+
if (sourceSnapshot !== undefined) {
|
|
153
|
+
const changed = () => usageError(`${SKILLS_DIR}/${alias} changed while pulling its current Skill. Keep these local files and pull into a new directory.`);
|
|
154
|
+
const readSnapshot = () => {
|
|
155
|
+
const current = {};
|
|
156
|
+
if (!existsSync(root) || lstatSync(root).isSymbolicLink() || !lstatSync(root).isDirectory()
|
|
157
|
+
|| lstatSync(dirname(root)).isSymbolicLink()) throw changed();
|
|
158
|
+
const walk = (directory, prefix = '') => {
|
|
159
|
+
for (const entry of readdirSync(directory, { withFileTypes: true })) {
|
|
160
|
+
if (entry.isSymbolicLink() || excluded(entry.name)) throw changed();
|
|
161
|
+
const path = prefix + entry.name;
|
|
162
|
+
if (entry.isDirectory()) walk(join(directory, entry.name), path + '/');
|
|
163
|
+
else if (entry.isFile()) current[path] = readFileSync(join(directory, entry.name)).toString('base64');
|
|
164
|
+
else throw changed();
|
|
165
|
+
}
|
|
166
|
+
};
|
|
167
|
+
walk(root);
|
|
168
|
+
return current;
|
|
169
|
+
};
|
|
170
|
+
const expected = appFilesDigest(sourceSnapshot);
|
|
171
|
+
if (appFilesDigest(readSnapshot()) !== expected) throw changed();
|
|
172
|
+
if (appFilesDigest(files) === expected) return;
|
|
173
|
+
// The archive remains immutable on the server. The editable folder follows
|
|
174
|
+
// the verified linked Skill head, including normalized frontmatter/deletions.
|
|
175
|
+
const staging = join(resolve(projectDir), '.notis', `skill-pull-${randomUUID()}`);
|
|
176
|
+
const backup = `${staging}-previous`;
|
|
177
|
+
mkdirSync(staging, { recursive: true });
|
|
178
|
+
let displaced = false;
|
|
179
|
+
try {
|
|
180
|
+
for (const [path, bytes] of entries) {
|
|
181
|
+
const target = join(staging, path);
|
|
182
|
+
mkdirSync(dirname(target), { recursive: true });
|
|
183
|
+
writeFileSync(target, bytes, { flag: 'wx' });
|
|
184
|
+
}
|
|
185
|
+
if (appFilesDigest(readSnapshot()) !== expected) throw changed();
|
|
186
|
+
renameSync(root, backup); displaced = true;
|
|
187
|
+
try { renameSync(staging, root); }
|
|
188
|
+
catch (error) { renameSync(backup, root); displaced = false; throw error; }
|
|
189
|
+
displaced = false;
|
|
190
|
+
rmSync(backup, { recursive: true });
|
|
191
|
+
} finally {
|
|
192
|
+
rmSync(staging, { recursive: true, force: true });
|
|
193
|
+
// If restoring the prior folder failed, retain its backup for recovery.
|
|
194
|
+
if (!displaced) rmSync(backup, { recursive: true, force: true });
|
|
195
|
+
}
|
|
196
|
+
return;
|
|
197
|
+
}
|
|
198
|
+
for (const [path, bytes] of entries) {
|
|
199
|
+
const target = join(root, path);
|
|
200
|
+
mkdirSync(dirname(target), { recursive: true });
|
|
201
|
+
try {
|
|
202
|
+
writeFileSync(target, bytes, { flag: 'wx' });
|
|
203
|
+
} catch (error) {
|
|
204
|
+
if (error?.code === 'EEXIST') throw usageError(`${SKILLS_DIR}/${alias}/${path} already exists in the pulled source; the Skill folder cannot be written over it.`);
|
|
205
|
+
throw error;
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
export function lockAfterPull({ spaceId, apiBase, sourceRevision, links, skills }) {
|
|
211
|
+
return { version: 1, space_id: spaceId, api_base: apiBase, source_revision: sourceRevision, pulled_at: new Date().toISOString(),
|
|
212
|
+
links: lockLinks(links), skills };
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
function byAlias(entries) {
|
|
216
|
+
return new Map(entries.map((entry) => [entry.alias, entry]));
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/**
|
|
220
|
+
* What changed since the pull, as the server expects it:
|
|
221
|
+
* edited and new Skill folders, deleted tracked folders and list additions or
|
|
222
|
+
* removals. Returns { changes: null } when nothing changed. A deleted tracked
|
|
223
|
+
* folder removes its link even while the list still names it. Ambiguous local
|
|
224
|
+
* states (a kept folder whose list entry was removed, a renamed alias) are
|
|
225
|
+
* usage errors, not conflicts.
|
|
226
|
+
*/
|
|
227
|
+
export function computeSpaceSourceChanges({ lock, resources, folders, manifest }) {
|
|
228
|
+
const tracked = lock.skills || {};
|
|
229
|
+
const lockLinksByKey = new Map((lock.links || []).map((link) => [`${link.kind}:${link.id}`, link]));
|
|
230
|
+
const lockAliases = byAlias(lock.links || []);
|
|
231
|
+
const declared = new Set(Object.keys(manifest?.resources || {}));
|
|
232
|
+
const list = resources ?? resourceEntries(lock.links.map((link) => ({ ...link, resource_id: link.id })));
|
|
233
|
+
const listByKey = new Map(list.map((entry) => [`${entry.kind}:${entry.id}`, entry]));
|
|
234
|
+
const listAliases = byAlias(list);
|
|
235
|
+
const edits = {}, creations = {}, add = [], remove = [];
|
|
236
|
+
const removedSkills = new Set();
|
|
237
|
+
|
|
238
|
+
for (const [alias, folder] of Object.entries(folders)) {
|
|
239
|
+
const entry = tracked[alias];
|
|
240
|
+
if (entry) {
|
|
241
|
+
if (!listByKey.has(`skill:${entry.skill_id}`)) {
|
|
242
|
+
throw usageError(`${RESOURCES_FILE} no longer lists the Skill of ${SKILLS_DIR}/${alias}. Delete the folder too to unlink it, or keep the entry.`);
|
|
243
|
+
}
|
|
244
|
+
if (entry.folder_hash !== folder.digest) {
|
|
245
|
+
edits[alias] = { skill_id: entry.skill_id, binding_id: entry.binding_id, binding_revision: entry.binding_revision,
|
|
246
|
+
version: entry.version, files: folder.files };
|
|
247
|
+
}
|
|
248
|
+
} else if (lockAliases.has(alias) || listAliases.has(alias)) {
|
|
249
|
+
throw usageError(`${SKILLS_DIR}/${alias} matches a listed resource that is not an editable Skill folder of this pull. Choose another folder name or pull again.`);
|
|
250
|
+
} else {
|
|
251
|
+
creations[alias] = { files: folder.files };
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
for (const [alias, entry] of Object.entries(tracked)) {
|
|
255
|
+
if (folders[alias]) continue;
|
|
256
|
+
// R5: deleting a tracked folder removes its link; a list entry left behind is rewritten after the deploy.
|
|
257
|
+
if (declared.has(alias)) {
|
|
258
|
+
throw usageError(`'${alias}' is declared by this Space's definition; remove it from the definition before deleting ${SKILLS_DIR}/${alias}.`);
|
|
259
|
+
}
|
|
260
|
+
removedSkills.add(entry.skill_id);
|
|
261
|
+
remove.push({ kind: 'skill', resource_id: entry.skill_id, binding_id: entry.binding_id, binding_revision: entry.binding_revision,
|
|
262
|
+
version: entry.version });
|
|
263
|
+
}
|
|
264
|
+
for (const entry of list) {
|
|
265
|
+
const before = lockLinksByKey.get(`${entry.kind}:${entry.id}`);
|
|
266
|
+
if (!before) {
|
|
267
|
+
if (!ALIAS.test(entry.alias)) {
|
|
268
|
+
throw usageError(`${RESOURCES_FILE}: '${entry.alias}' is not a valid alias for a new link; use lowercase letters, digits, - or _.`);
|
|
269
|
+
}
|
|
270
|
+
if (lockAliases.has(entry.alias) || folders[entry.alias] || creations[entry.alias]) {
|
|
271
|
+
throw usageError(`${RESOURCES_FILE}: alias '${entry.alias}' is already used by another resource or Skill folder.`);
|
|
272
|
+
}
|
|
273
|
+
add.push({ kind: entry.kind, resource_id: entry.id, alias: entry.alias });
|
|
274
|
+
} else if (before.alias !== entry.alias) {
|
|
275
|
+
throw usageError(`${RESOURCES_FILE}: '${before.alias}' cannot be renamed to '${entry.alias}' here; aliases are stable once linked.`);
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
for (const link of lock.links || []) {
|
|
279
|
+
if (listByKey.has(`${link.kind}:${link.id}`) || removedSkills.has(link.id)) continue;
|
|
280
|
+
if (declared.has(link.alias)) {
|
|
281
|
+
throw usageError(`'${link.alias}' is declared by this Space's definition; remove it from the definition before removing it from ${RESOURCES_FILE}.`);
|
|
282
|
+
}
|
|
283
|
+
remove.push({ kind: link.kind, resource_id: link.id, binding_id: link.binding_id, binding_revision: link.binding_revision });
|
|
284
|
+
}
|
|
285
|
+
const changes = {};
|
|
286
|
+
if (Object.keys(edits).length || Object.keys(creations).length) {
|
|
287
|
+
changes.skills = { ...(Object.keys(edits).length ? { edits } : {}), ...(Object.keys(creations).length ? { creations } : {}) };
|
|
288
|
+
}
|
|
289
|
+
if (add.length || remove.length) changes.links = { ...(add.length ? { add } : {}), ...(remove.length ? { remove } : {}) };
|
|
290
|
+
if (!Object.keys(changes).length) return { changes: null, summary: null, fingerprint: null };
|
|
291
|
+
const summary = { skills: { edited: Object.keys(edits).sort(), created: Object.keys(creations).sort() },
|
|
292
|
+
links: { added: add.map((entry) => `${entry.alias} (${entry.kind})`), removed: remove.map((entry) => `${entry.kind} ${entry.resource_id}`) } };
|
|
293
|
+
return { changes, summary, fingerprint: createHash('sha256').update(stableJson(changes)).digest('hex') };
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
/**
|
|
297
|
+
* The lock and list after a publication: the applied Skill versions, the new
|
|
298
|
+
* Skills' identities and the Space's current links (including links made
|
|
299
|
+
* elsewhere since the pull, which the deploy kept).
|
|
300
|
+
*/
|
|
301
|
+
export function applyPublication({ lock, folders, result }) {
|
|
302
|
+
const applied = result?.applied;
|
|
303
|
+
const next = { ...lock, source_revision: result?.published_revision ?? lock.source_revision, updated_at: new Date().toISOString() };
|
|
304
|
+
if (!applied) return { lock: next, resources: null };
|
|
305
|
+
const skills = {};
|
|
306
|
+
for (const [alias, folder] of Object.entries(folders)) {
|
|
307
|
+
const change = applied.skills?.[alias];
|
|
308
|
+
if (change) {
|
|
309
|
+
skills[alias] = { skill_id: change.skill_id, binding_id: change.binding_id, binding_revision: change.binding_revision,
|
|
310
|
+
version: change.version, folder_hash: folder.digest };
|
|
311
|
+
} else if (lock.skills?.[alias]) {
|
|
312
|
+
skills[alias] = lock.skills[alias];
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
next.skills = skills;
|
|
316
|
+
next.links = lockLinks(applied.space_links || []);
|
|
317
|
+
return { lock: next, resources: resourceEntries(applied.space_links || []) };
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
/** One line describing what a deploy changed, with the review link of every changed Skill. */
|
|
321
|
+
export function describeAppliedChanges(result) {
|
|
322
|
+
const applied = result?.applied;
|
|
323
|
+
if (!applied) return '';
|
|
324
|
+
const parts = [];
|
|
325
|
+
const edited = Object.entries(applied.skills || {}).filter(([, change]) => change.operation === 'update').map(([alias]) => alias);
|
|
326
|
+
const created = Object.entries(applied.skills || {}).filter(([, change]) => change.operation === 'create').map(([alias]) => alias);
|
|
327
|
+
if (edited.length) parts.push(`Skills updated: ${edited.join(', ')}`);
|
|
328
|
+
if (created.length) parts.push(`Skills created: ${created.join(', ')}`);
|
|
329
|
+
const added = (applied.links || []).flatMap((link) => (link.added || []).map((entry) => `${entry.alias} (${link.kind})`));
|
|
330
|
+
const removed = (applied.links || []).flatMap((link) => (link.removed || []).map((entry) => `${entry.alias} (${link.kind})`));
|
|
331
|
+
if (added.length) parts.push(`Links added: ${added.join(', ')}`);
|
|
332
|
+
if (removed.length) parts.push(`Links removed: ${removed.join(', ')}`);
|
|
333
|
+
for (const review of result.reviews || []) {
|
|
334
|
+
if (review.review_url) parts.push(`Review ${review.alias}: ${review.review_url}`);
|
|
335
|
+
}
|
|
336
|
+
return parts.length ? ` ${parts.join('. ')}.` : '';
|
|
337
|
+
}
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
/** One authoring project, one selected immutable Space release at a time. */
|
|
2
|
+
import { existsSync, mkdirSync, readFileSync, symlinkSync, writeFileSync } from 'node:fs';
|
|
3
|
+
import { dirname, join, relative, resolve } from 'node:path';
|
|
4
|
+
import { appFilesDigest, buildArtifact, freezeSourceWorkspace, prepareAppRelease, prepareSelectedBuild, saveSelectedBuild, syncEmbeddedSdk } from './app-platform.js';
|
|
5
|
+
import { validateProjectBoundary, validateProjectDesign } from './app-boundary-validator.js';
|
|
6
|
+
import { collectSelectedSpaceSource, loadSelectedSpace } from './space-source.js';
|
|
7
|
+
import { viewManifestFields } from './space-view-manifest.js';
|
|
8
|
+
import { usageError } from './errors.js';
|
|
9
|
+
import { assertSourceRoot } from './source-root.js';
|
|
10
|
+
|
|
11
|
+
export function generateSpaceManifest(selection) {
|
|
12
|
+
const definition = selection.definition;
|
|
13
|
+
return { schema: 'notis-space/v1', kind: selection.entry ? 'presentation' : 'container', local_key: selection.key,
|
|
14
|
+
name: definition.name, description: definition.description || '', icon: definition.icon || null, accent: definition.accent || null,
|
|
15
|
+
resources: definition.resources || {}, actions: definition.actions || {},
|
|
16
|
+
...(definition.navigation ? { navigation: definition.navigation } : {}),
|
|
17
|
+
// Absent when none, so manifests without viewer reads stay byte-identical.
|
|
18
|
+
...(definition.viewerReads?.length ? { viewer_reads: [...definition.viewerReads].sort() } : {}),
|
|
19
|
+
// Absent when undeclared, for the same reason.
|
|
20
|
+
...(definition.cloudComputer ? { cloud_computer: definition.cloudComputer } : {}),
|
|
21
|
+
readable_context: definition.readableContext || '', collection: definition.collection || null,
|
|
22
|
+
// V4 declarations are mandatory for every presentation; the wire schema stays v1.
|
|
23
|
+
...viewManifestFields(definition, { entry: selection.entry, resources: definition.resources || {} }),
|
|
24
|
+
...(selection.entry ? { presentation: { export_name: 'SpaceView' }, bundle: { js: 'bundle/app.js', css: 'bundle/app.css' } } : {}),
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function prepareSelectedArtifact(projectDir, selection) {
|
|
29
|
+
validateProjectBoundary(projectDir);
|
|
30
|
+
validateProjectDesign(projectDir);
|
|
31
|
+
const entryPath = join(projectDir, '.notis/_entry.tsx');
|
|
32
|
+
const manifestPath = join(projectDir, '.notis/output/manifest.json');
|
|
33
|
+
mkdirSync(dirname(entryPath), { recursive: true });
|
|
34
|
+
mkdirSync(dirname(manifestPath), { recursive: true });
|
|
35
|
+
if (selection.entry) {
|
|
36
|
+
const reference = path => JSON.stringify('./' + relative(dirname(entryPath), join(projectDir, path)).replaceAll('\\', '/'));
|
|
37
|
+
const lines = [`export { default as SpaceView } from ${reference(selection.entry)};`];
|
|
38
|
+
if (selection.layout) lines.push(`export { default as __AppShell } from ${reference(selection.layout)};`);
|
|
39
|
+
if (selection.markdown) lines.push(`export { default as SpaceMarkdown } from ${reference(selection.markdown)};`);
|
|
40
|
+
writeFileSync(entryPath, lines.join('\n') + '\n');
|
|
41
|
+
}
|
|
42
|
+
writeFileSync(manifestPath, JSON.stringify(generateSpaceManifest(selection), null, 2) + '\n');
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function finalizeSelectedArtifact(projectDir) {
|
|
46
|
+
const manifestPath = join(projectDir, '.notis/output/manifest.json');
|
|
47
|
+
const manifest = JSON.parse(readFileSync(manifestPath, 'utf8'));
|
|
48
|
+
// A CSS-free Vite entry legitimately emits only JavaScript. Declare actual
|
|
49
|
+
// output before the immutable build receipt, not a nonexistent stylesheet.
|
|
50
|
+
if (manifest.bundle?.css && !existsSync(join(projectDir, '.notis/output', manifest.bundle.css))) {
|
|
51
|
+
delete manifest.bundle.css;
|
|
52
|
+
writeFileSync(manifestPath, JSON.stringify(manifest, null, 2) + '\n');
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Build from the selected source slice so CSS scans/config loaders cannot pull
|
|
58
|
+
* in unshared sibling files. Staging resolves the original project's installed
|
|
59
|
+
* dependencies; it is not another authoring project or dependency installation.
|
|
60
|
+
* The existing pinned freeze/receipt code owns all staging writes and cleanup.
|
|
61
|
+
*/
|
|
62
|
+
export async function buildSpaceRelease(projectDir, selectedKey, { stdio = 'inherit', refreshSdk = true, runBuild } = {}) {
|
|
63
|
+
projectDir = assertSourceRoot(projectDir);
|
|
64
|
+
if (refreshSdk) syncEmbeddedSdk(projectDir);
|
|
65
|
+
const selection = await loadSelectedSpace(projectDir, selectedKey);
|
|
66
|
+
const sourceFiles = collectSelectedSpaceSource(projectDir, selection);
|
|
67
|
+
const sourceHash = appFilesDigest(sourceFiles);
|
|
68
|
+
const staging = freezeSourceWorkspace(projectDir, sourceFiles);
|
|
69
|
+
let release;
|
|
70
|
+
try {
|
|
71
|
+
if (existsSync(join(projectDir, 'node_modules'))) {
|
|
72
|
+
symlinkSync(join(projectDir, 'node_modules'), join(staging.projectDir, 'node_modules'), process.platform === 'win32' ? 'junction' : 'dir');
|
|
73
|
+
}
|
|
74
|
+
const frozenSelection = await loadSelectedSpace(staging.projectDir, selectedKey);
|
|
75
|
+
await buildArtifact(staging.projectDir, { stdio, refreshSdk: false, executable: Boolean(selection.entry), includeMetadata: false,
|
|
76
|
+
prepare: directory => prepareSelectedArtifact(directory, frozenSelection), finalize: finalizeSelectedArtifact,
|
|
77
|
+
...(runBuild ? { runBuild } : {}) });
|
|
78
|
+
const current = await loadSelectedSpace(projectDir, selectedKey);
|
|
79
|
+
if (sourceHash !== appFilesDigest(collectSelectedSpaceSource(projectDir, current))) throw usageError('This Space source changed during the selected build. Build again.');
|
|
80
|
+
release = prepareAppRelease(staging.projectDir);
|
|
81
|
+
const close = release.close;
|
|
82
|
+
return { ...release, selectedKey, sourceHash, fixturePath: selection.fixturePath, close() { try { close(); } finally { staging.close(); } } };
|
|
83
|
+
} catch (error) {
|
|
84
|
+
try { release?.close(); } finally { staging.close(); }
|
|
85
|
+
throw error;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export async function buildSpaceArtifact(projectDir, selectedKey, options) {
|
|
90
|
+
const release = await buildSpaceRelease(projectDir, selectedKey, options);
|
|
91
|
+
try {
|
|
92
|
+
const receiptPath = saveSelectedBuild(projectDir, selectedKey, release);
|
|
93
|
+
return { manifest: release.manifest, receiptPath, sourceHash: release.sourceHash };
|
|
94
|
+
} finally { release.close(); }
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export async function prepareSpaceRelease(projectDir, selectedKey) {
|
|
98
|
+
const selection = await loadSelectedSpace(projectDir, selectedKey);
|
|
99
|
+
const source = collectSelectedSpaceSource(projectDir, selection);
|
|
100
|
+
const release = prepareSelectedBuild(projectDir, selectedKey, source);
|
|
101
|
+
if (release.manifest?.schema !== 'notis-space/v1' || release.manifest.local_key !== selectedKey) {
|
|
102
|
+
release.close(); throw usageError('The saved build belongs to a different Space.');
|
|
103
|
+
}
|
|
104
|
+
return { ...release, fixturePath: selection.fixturePath };
|
|
105
|
+
}
|