@notis_ai/cli 0.2.14 → 0.2.16
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 +83 -21
- package/dist/agent-hooks/notis-agent-hook.mjs +20168 -0
- package/{skills → dist/base-skills}/notis-apps/SKILL.md +167 -72
- package/{skills → dist/base-skills}/notis-cli/SKILL.md +86 -28
- package/dist/base-skills/notis-query/SKILL.md +705 -0
- package/dist/skill-sync/index.js +1528 -0
- package/dist/skill-sync/index.js.map +7 -0
- package/package.json +5 -2
- package/skills/notis-apps/cli.md +39 -18
- package/skills/notis-cli/AGENT_INSTRUCTIONS.md +39 -0
- package/skills/notis-onboarding/BRIEF.md +16 -0
- package/src/agent-hook-entry.js +5 -0
- package/src/cli.js +23 -14
- package/src/command-specs/agents.js +392 -0
- package/src/command-specs/apps.js +1021 -424
- package/src/command-specs/auth.js +63 -10
- package/src/command-specs/index.js +6 -0
- package/src/command-specs/onboarding.js +69 -5
- package/src/command-specs/skills.js +56 -0
- package/src/runtime/agent-memory-state.js +126 -0
- package/src/runtime/agent-setup.js +383 -0
- package/src/runtime/app-dev-consumers.js +154 -0
- package/src/runtime/app-dev-host-lock.js +80 -0
- package/src/runtime/app-dev-process-identity.js +111 -0
- package/src/runtime/app-dev-roots.js +284 -0
- package/src/runtime/app-dev-server.js +447 -29
- package/src/runtime/app-dev-sessions.js +105 -145
- package/src/runtime/app-platform.js +1314 -166
- package/src/runtime/app-registry-scaffolds.js +367 -0
- package/src/runtime/base-skills.d.ts +20 -0
- package/src/runtime/base-skills.js +167 -0
- package/src/runtime/login-listener.js +15 -0
- package/src/runtime/oauth.js +1506 -141
- package/src/runtime/output.js +7 -0
- package/src/runtime/ports.js +16 -0
- package/src/runtime/profiles.js +19 -16
- package/src/runtime/skill-sync/cloud-client.ts +96 -0
- package/src/runtime/skill-sync/index.ts +644 -0
- package/src/runtime/skill-sync/local-scanner.ts +1046 -0
- package/src/runtime/skill-sync/symlink-manager.ts +383 -0
- package/src/runtime/skill-sync/sync-plan.ts +22 -0
- package/src/runtime/skill-sync/types.ts +103 -0
- package/src/runtime/skill-sync/write-cloud-skill.ts +50 -0
- package/src/runtime/store-screenshot.js +6 -1
- package/src/runtime/sync-skills.d.ts +37 -0
- package/src/runtime/sync-skills.js +231 -0
- package/template/app/layout.tsx +5 -1
- package/template/notis.config.ts +1 -0
- package/template/packages/sdk/package.json +2 -1
- package/template/packages/sdk/src/components/DocumentEditor.tsx +13 -3
- package/template/packages/sdk/src/components/MarkdownEditor.tsx +121 -0
- package/template/packages/sdk/src/components/MultiSelectActionBar.tsx +43 -119
- package/template/packages/sdk/src/components/MultiSelectCheckbox.tsx +5 -1
- package/template/packages/sdk/src/components/MultiSelectDragOverlay.tsx +1 -1
- package/template/packages/sdk/src/components/NotisSelectionBoundary.tsx +55 -0
- package/template/packages/sdk/src/components/ShortcutHints.tsx +56 -0
- package/template/packages/sdk/src/config.ts +25 -0
- package/template/packages/sdk/src/documents.ts +7 -1
- package/template/packages/sdk/src/hooks/useActiveResource.ts +19 -0
- package/template/packages/sdk/src/hooks/useCollectionInteractions.ts +726 -0
- package/template/packages/sdk/src/hooks/useHandover.ts +3 -0
- package/template/packages/sdk/src/hooks/useMultiSelect.ts +44 -482
- package/template/packages/sdk/src/hooks/useNotis.ts +3 -0
- package/template/packages/sdk/src/hooks/useNotisNavigation.ts +7 -4
- package/template/packages/sdk/src/hooks/useTool.ts +5 -4
- package/template/packages/sdk/src/index.ts +47 -2
- package/template/packages/sdk/src/interactions/actions.ts +46 -0
- package/template/packages/sdk/src/interactions/shortcuts.tsx +634 -0
- package/template/packages/sdk/src/interactions.ts +41 -0
- package/template/packages/sdk/src/provider.tsx +2 -1
- package/template/packages/sdk/src/runtime.ts +92 -4
- package/dist/scaffolds/notis-database/CHANGELOG.md +0 -5
- package/dist/scaffolds/notis-database/app/globals.css +0 -44
- package/dist/scaffolds/notis-database/app/layout.tsx +0 -6
- package/dist/scaffolds/notis-database/app/page.tsx +0 -1088
- package/dist/scaffolds/notis-database/components/ui/badge.tsx +0 -28
- package/dist/scaffolds/notis-database/components/ui/button.tsx +0 -53
- package/dist/scaffolds/notis-database/components/ui/card.tsx +0 -56
- package/dist/scaffolds/notis-database/components/ui/table.tsx +0 -120
- package/dist/scaffolds/notis-database/components.json +0 -20
- package/dist/scaffolds/notis-database/index.html +0 -12
- package/dist/scaffolds/notis-database/lib/types.ts +0 -132
- package/dist/scaffolds/notis-database/lib/utils.ts +0 -6
- package/dist/scaffolds/notis-database/metadata/screenshot-1.png +0 -0
- package/dist/scaffolds/notis-database/metadata/screenshot-2.png +0 -0
- package/dist/scaffolds/notis-database/metadata/screenshot-3.png +0 -0
- package/dist/scaffolds/notis-database/metadata/screenshot-4.png +0 -0
- package/dist/scaffolds/notis-database/metadata/screenshot-5.png +0 -0
- package/dist/scaffolds/notis-database/metadata/screenshot-fixtures.json +0 -1839
- package/dist/scaffolds/notis-database/notis.config.ts +0 -73
- package/dist/scaffolds/notis-database/package-lock.json +0 -3935
- package/dist/scaffolds/notis-database/package.json +0 -32
- package/dist/scaffolds/notis-database/packages/sdk/package.json +0 -36
- package/dist/scaffolds/notis-database/packages/sdk/src/components/DocumentEditor.tsx +0 -93
- package/dist/scaffolds/notis-database/packages/sdk/src/components/Markdown.tsx +0 -60
- package/dist/scaffolds/notis-database/packages/sdk/src/components/MultiSelectActionBar.tsx +0 -278
- package/dist/scaffolds/notis-database/packages/sdk/src/components/MultiSelectCheckbox.tsx +0 -91
- package/dist/scaffolds/notis-database/packages/sdk/src/components/MultiSelectDragOverlay.tsx +0 -39
- package/dist/scaffolds/notis-database/packages/sdk/src/config.ts +0 -234
- package/dist/scaffolds/notis-database/packages/sdk/src/documents.ts +0 -250
- package/dist/scaffolds/notis-database/packages/sdk/src/hooks/useBackend.ts +0 -41
- package/dist/scaffolds/notis-database/packages/sdk/src/hooks/useCloudComputer.ts +0 -97
- package/dist/scaffolds/notis-database/packages/sdk/src/hooks/useDatabaseSchema.ts +0 -85
- package/dist/scaffolds/notis-database/packages/sdk/src/hooks/useDatabaseSubscription.ts +0 -76
- package/dist/scaffolds/notis-database/packages/sdk/src/hooks/useDocument.ts +0 -78
- package/dist/scaffolds/notis-database/packages/sdk/src/hooks/useDocuments.ts +0 -121
- package/dist/scaffolds/notis-database/packages/sdk/src/hooks/useHandover.ts +0 -75
- package/dist/scaffolds/notis-database/packages/sdk/src/hooks/useMultiSelect.ts +0 -539
- package/dist/scaffolds/notis-database/packages/sdk/src/hooks/useNotis.ts +0 -34
- package/dist/scaffolds/notis-database/packages/sdk/src/hooks/useNotisNavigation.ts +0 -49
- package/dist/scaffolds/notis-database/packages/sdk/src/hooks/useTool.ts +0 -64
- package/dist/scaffolds/notis-database/packages/sdk/src/hooks/useTools.ts +0 -56
- package/dist/scaffolds/notis-database/packages/sdk/src/hooks/useTopBarSearch.ts +0 -73
- package/dist/scaffolds/notis-database/packages/sdk/src/hooks/useUpsertDocument.ts +0 -95
- package/dist/scaffolds/notis-database/packages/sdk/src/index.ts +0 -100
- package/dist/scaffolds/notis-database/packages/sdk/src/provider.tsx +0 -43
- package/dist/scaffolds/notis-database/packages/sdk/src/runtime.ts +0 -351
- package/dist/scaffolds/notis-database/packages/sdk/src/styles.css +0 -186
- package/dist/scaffolds/notis-database/packages/sdk/src/ui.ts +0 -15
- package/dist/scaffolds/notis-database/packages/sdk/src/vite.ts +0 -56
- package/dist/scaffolds/notis-database/packages/sdk/tsconfig.json +0 -15
- package/dist/scaffolds/notis-database/postcss.config.mjs +0 -8
- package/dist/scaffolds/notis-database/src/dev-main.tsx +0 -23
- package/dist/scaffolds/notis-database/src/mock-runtime.ts +0 -561
- package/dist/scaffolds/notis-database/tailwind.config.ts +0 -59
- package/dist/scaffolds/notis-database/tsconfig.json +0 -23
- package/dist/scaffolds/notis-database/vite.config.ts +0 -22
- package/dist/scaffolds/notis-journal/CHANGELOG.md +0 -29
- package/dist/scaffolds/notis-journal/app/globals.css +0 -37
- package/dist/scaffolds/notis-journal/app/insights/page.tsx +0 -513
- package/dist/scaffolds/notis-journal/app/journal-core.tsx +0 -362
- package/dist/scaffolds/notis-journal/app/journal-ui.tsx +0 -337
- package/dist/scaffolds/notis-journal/app/layout.tsx +0 -6
- package/dist/scaffolds/notis-journal/app/page.tsx +0 -486
- package/dist/scaffolds/notis-journal/components/ui/badge.tsx +0 -28
- package/dist/scaffolds/notis-journal/components/ui/button.tsx +0 -53
- package/dist/scaffolds/notis-journal/components/ui/card.tsx +0 -56
- package/dist/scaffolds/notis-journal/components.json +0 -20
- package/dist/scaffolds/notis-journal/index.html +0 -12
- package/dist/scaffolds/notis-journal/lib/utils.ts +0 -6
- package/dist/scaffolds/notis-journal/metadata/screenshot-1.png +0 -0
- package/dist/scaffolds/notis-journal/metadata/screenshot-2.png +0 -0
- package/dist/scaffolds/notis-journal/metadata/screenshot-3.png +0 -0
- package/dist/scaffolds/notis-journal/metadata/screenshot-4.png +0 -0
- package/dist/scaffolds/notis-journal/metadata/screenshot-5.png +0 -0
- package/dist/scaffolds/notis-journal/metadata/screenshot-6.png +0 -0
- package/dist/scaffolds/notis-journal/metadata/screenshot-fixtures.json +0 -132
- package/dist/scaffolds/notis-journal/notis.config.ts +0 -93
- package/dist/scaffolds/notis-journal/package-lock.json +0 -4615
- package/dist/scaffolds/notis-journal/package.json +0 -34
- package/dist/scaffolds/notis-journal/packages/sdk/package.json +0 -36
- package/dist/scaffolds/notis-journal/packages/sdk/src/components/DocumentEditor.tsx +0 -93
- package/dist/scaffolds/notis-journal/packages/sdk/src/components/Markdown.tsx +0 -60
- package/dist/scaffolds/notis-journal/packages/sdk/src/components/MultiSelectActionBar.tsx +0 -278
- package/dist/scaffolds/notis-journal/packages/sdk/src/components/MultiSelectCheckbox.tsx +0 -91
- package/dist/scaffolds/notis-journal/packages/sdk/src/components/MultiSelectDragOverlay.tsx +0 -39
- package/dist/scaffolds/notis-journal/packages/sdk/src/config.ts +0 -234
- package/dist/scaffolds/notis-journal/packages/sdk/src/documents.ts +0 -250
- package/dist/scaffolds/notis-journal/packages/sdk/src/hooks/useBackend.ts +0 -41
- package/dist/scaffolds/notis-journal/packages/sdk/src/hooks/useCloudComputer.ts +0 -97
- package/dist/scaffolds/notis-journal/packages/sdk/src/hooks/useDatabaseSchema.ts +0 -85
- package/dist/scaffolds/notis-journal/packages/sdk/src/hooks/useDatabaseSubscription.ts +0 -76
- package/dist/scaffolds/notis-journal/packages/sdk/src/hooks/useDocument.ts +0 -78
- package/dist/scaffolds/notis-journal/packages/sdk/src/hooks/useDocuments.ts +0 -121
- package/dist/scaffolds/notis-journal/packages/sdk/src/hooks/useHandover.ts +0 -75
- package/dist/scaffolds/notis-journal/packages/sdk/src/hooks/useMultiSelect.ts +0 -539
- package/dist/scaffolds/notis-journal/packages/sdk/src/hooks/useNotis.ts +0 -34
- package/dist/scaffolds/notis-journal/packages/sdk/src/hooks/useNotisNavigation.ts +0 -49
- package/dist/scaffolds/notis-journal/packages/sdk/src/hooks/useTool.ts +0 -64
- package/dist/scaffolds/notis-journal/packages/sdk/src/hooks/useTools.ts +0 -56
- package/dist/scaffolds/notis-journal/packages/sdk/src/hooks/useTopBarSearch.ts +0 -73
- package/dist/scaffolds/notis-journal/packages/sdk/src/hooks/useUpsertDocument.ts +0 -95
- package/dist/scaffolds/notis-journal/packages/sdk/src/index.ts +0 -100
- package/dist/scaffolds/notis-journal/packages/sdk/src/provider.tsx +0 -43
- package/dist/scaffolds/notis-journal/packages/sdk/src/runtime.ts +0 -351
- package/dist/scaffolds/notis-journal/packages/sdk/src/styles.css +0 -186
- package/dist/scaffolds/notis-journal/packages/sdk/src/ui.ts +0 -15
- package/dist/scaffolds/notis-journal/packages/sdk/src/vite.ts +0 -56
- package/dist/scaffolds/notis-journal/packages/sdk/tsconfig.json +0 -15
- package/dist/scaffolds/notis-journal/postcss.config.mjs +0 -8
- package/dist/scaffolds/notis-journal/skills/journal-onboarding/SKILL.md +0 -120
- package/dist/scaffolds/notis-journal/src/dev-main.tsx +0 -58
- package/dist/scaffolds/notis-journal/src/mock-runtime.ts +0 -199
- package/dist/scaffolds/notis-journal/tailwind.config.ts +0 -58
- package/dist/scaffolds/notis-journal/tsconfig.json +0 -23
- package/dist/scaffolds/notis-journal/vite.config.ts +0 -10
- package/dist/scaffolds/notis-notes/CHANGELOG.md +0 -5
- package/dist/scaffolds/notis-notes/app/globals.css +0 -3
- package/dist/scaffolds/notis-notes/app/layout.tsx +0 -6
- package/dist/scaffolds/notis-notes/app/page.tsx +0 -1943
- package/dist/scaffolds/notis-notes/app/phosphor-icons.ts +0 -596
- package/dist/scaffolds/notis-notes/components/ui/badge.tsx +0 -28
- package/dist/scaffolds/notis-notes/components/ui/button.tsx +0 -53
- package/dist/scaffolds/notis-notes/components/ui/card.tsx +0 -56
- package/dist/scaffolds/notis-notes/components.json +0 -20
- package/dist/scaffolds/notis-notes/lib/utils.ts +0 -6
- package/dist/scaffolds/notis-notes/lib/visible-properties.ts +0 -144
- package/dist/scaffolds/notis-notes/metadata/screenshot-1.png +0 -0
- package/dist/scaffolds/notis-notes/metadata/screenshot-2.png +0 -0
- package/dist/scaffolds/notis-notes/metadata/screenshot-3.png +0 -0
- package/dist/scaffolds/notis-notes/metadata/screenshot-4.png +0 -0
- package/dist/scaffolds/notis-notes/metadata/screenshot-5.png +0 -0
- package/dist/scaffolds/notis-notes/metadata/screenshot-fixtures.json +0 -752
- package/dist/scaffolds/notis-notes/notis.config.ts +0 -80
- package/dist/scaffolds/notis-notes/package-lock.json +0 -4636
- package/dist/scaffolds/notis-notes/package.json +0 -35
- package/dist/scaffolds/notis-notes/packages/sdk/package.json +0 -36
- package/dist/scaffolds/notis-notes/packages/sdk/src/components/DocumentEditor.tsx +0 -93
- package/dist/scaffolds/notis-notes/packages/sdk/src/components/Markdown.tsx +0 -60
- package/dist/scaffolds/notis-notes/packages/sdk/src/components/MultiSelectActionBar.tsx +0 -278
- package/dist/scaffolds/notis-notes/packages/sdk/src/components/MultiSelectCheckbox.tsx +0 -91
- package/dist/scaffolds/notis-notes/packages/sdk/src/components/MultiSelectDragOverlay.tsx +0 -39
- package/dist/scaffolds/notis-notes/packages/sdk/src/config.ts +0 -234
- package/dist/scaffolds/notis-notes/packages/sdk/src/documents.ts +0 -250
- package/dist/scaffolds/notis-notes/packages/sdk/src/hooks/useBackend.ts +0 -41
- package/dist/scaffolds/notis-notes/packages/sdk/src/hooks/useCloudComputer.ts +0 -97
- package/dist/scaffolds/notis-notes/packages/sdk/src/hooks/useDatabaseSchema.ts +0 -85
- package/dist/scaffolds/notis-notes/packages/sdk/src/hooks/useDatabaseSubscription.ts +0 -76
- package/dist/scaffolds/notis-notes/packages/sdk/src/hooks/useDocument.ts +0 -78
- package/dist/scaffolds/notis-notes/packages/sdk/src/hooks/useDocuments.ts +0 -121
- package/dist/scaffolds/notis-notes/packages/sdk/src/hooks/useHandover.ts +0 -75
- package/dist/scaffolds/notis-notes/packages/sdk/src/hooks/useMultiSelect.ts +0 -539
- package/dist/scaffolds/notis-notes/packages/sdk/src/hooks/useNotis.ts +0 -34
- package/dist/scaffolds/notis-notes/packages/sdk/src/hooks/useNotisNavigation.ts +0 -49
- package/dist/scaffolds/notis-notes/packages/sdk/src/hooks/useTool.ts +0 -64
- package/dist/scaffolds/notis-notes/packages/sdk/src/hooks/useTools.ts +0 -56
- package/dist/scaffolds/notis-notes/packages/sdk/src/hooks/useTopBarSearch.ts +0 -73
- package/dist/scaffolds/notis-notes/packages/sdk/src/hooks/useUpsertDocument.ts +0 -95
- package/dist/scaffolds/notis-notes/packages/sdk/src/index.ts +0 -100
- package/dist/scaffolds/notis-notes/packages/sdk/src/provider.tsx +0 -43
- package/dist/scaffolds/notis-notes/packages/sdk/src/runtime.ts +0 -351
- package/dist/scaffolds/notis-notes/packages/sdk/src/styles.css +0 -186
- package/dist/scaffolds/notis-notes/packages/sdk/src/ui.ts +0 -15
- package/dist/scaffolds/notis-notes/packages/sdk/src/vite.ts +0 -56
- package/dist/scaffolds/notis-notes/packages/sdk/tsconfig.json +0 -15
- package/dist/scaffolds/notis-notes/postcss.config.mjs +0 -8
- package/dist/scaffolds/notis-notes/tailwind.config.ts +0 -58
- package/dist/scaffolds/notis-notes/tsconfig.json +0 -23
- package/dist/scaffolds/notis-notes/vite.config.ts +0 -10
- package/dist/scaffolds/notis-random/CHANGELOG.md +0 -15
- package/dist/scaffolds/notis-random/README.md +0 -33
- package/dist/scaffolds/notis-random/app/globals.css +0 -11
- package/dist/scaffolds/notis-random/app/history/page.tsx +0 -67
- package/dist/scaffolds/notis-random/app/layout.tsx +0 -7
- package/dist/scaffolds/notis-random/app/page.tsx +0 -289
- package/dist/scaffolds/notis-random/components/ui/button.tsx +0 -50
- package/dist/scaffolds/notis-random/components/ui/card.tsx +0 -16
- package/dist/scaffolds/notis-random/components/ui/input.tsx +0 -23
- package/dist/scaffolds/notis-random/components.json +0 -20
- package/dist/scaffolds/notis-random/index.html +0 -12
- package/dist/scaffolds/notis-random/lib/notis-tools.ts +0 -128
- package/dist/scaffolds/notis-random/lib/rng.ts +0 -202
- package/dist/scaffolds/notis-random/lib/roll-record.ts +0 -189
- package/dist/scaffolds/notis-random/lib/utils.ts +0 -25
- package/dist/scaffolds/notis-random/metadata/screenshot-1.png +0 -0
- package/dist/scaffolds/notis-random/metadata/screenshot-2.png +0 -0
- package/dist/scaffolds/notis-random/metadata/screenshot-3.png +0 -0
- package/dist/scaffolds/notis-random/metadata/screenshot-4.png +0 -0
- package/dist/scaffolds/notis-random/metadata/screenshot-5.png +0 -0
- package/dist/scaffolds/notis-random/metadata/screenshot-fixtures.json +0 -753
- package/dist/scaffolds/notis-random/notis.config.ts +0 -86
- package/dist/scaffolds/notis-random/package-lock.json +0 -4513
- package/dist/scaffolds/notis-random/package.json +0 -36
- package/dist/scaffolds/notis-random/packages/sdk/package.json +0 -36
- package/dist/scaffolds/notis-random/packages/sdk/src/components/DocumentEditor.tsx +0 -93
- package/dist/scaffolds/notis-random/packages/sdk/src/components/Markdown.tsx +0 -60
- package/dist/scaffolds/notis-random/packages/sdk/src/components/MultiSelectActionBar.tsx +0 -278
- package/dist/scaffolds/notis-random/packages/sdk/src/components/MultiSelectCheckbox.tsx +0 -91
- package/dist/scaffolds/notis-random/packages/sdk/src/components/MultiSelectDragOverlay.tsx +0 -39
- package/dist/scaffolds/notis-random/packages/sdk/src/config.ts +0 -234
- package/dist/scaffolds/notis-random/packages/sdk/src/documents.ts +0 -250
- package/dist/scaffolds/notis-random/packages/sdk/src/hooks/useBackend.ts +0 -41
- package/dist/scaffolds/notis-random/packages/sdk/src/hooks/useCloudComputer.ts +0 -97
- package/dist/scaffolds/notis-random/packages/sdk/src/hooks/useDatabaseSchema.ts +0 -85
- package/dist/scaffolds/notis-random/packages/sdk/src/hooks/useDatabaseSubscription.ts +0 -76
- package/dist/scaffolds/notis-random/packages/sdk/src/hooks/useDocument.ts +0 -78
- package/dist/scaffolds/notis-random/packages/sdk/src/hooks/useDocuments.ts +0 -121
- package/dist/scaffolds/notis-random/packages/sdk/src/hooks/useHandover.ts +0 -75
- package/dist/scaffolds/notis-random/packages/sdk/src/hooks/useMultiSelect.ts +0 -539
- package/dist/scaffolds/notis-random/packages/sdk/src/hooks/useNotis.ts +0 -34
- package/dist/scaffolds/notis-random/packages/sdk/src/hooks/useNotisNavigation.ts +0 -49
- package/dist/scaffolds/notis-random/packages/sdk/src/hooks/useTool.ts +0 -64
- package/dist/scaffolds/notis-random/packages/sdk/src/hooks/useTools.ts +0 -56
- package/dist/scaffolds/notis-random/packages/sdk/src/hooks/useTopBarSearch.ts +0 -73
- package/dist/scaffolds/notis-random/packages/sdk/src/hooks/useUpsertDocument.ts +0 -95
- package/dist/scaffolds/notis-random/packages/sdk/src/index.ts +0 -100
- package/dist/scaffolds/notis-random/packages/sdk/src/provider.tsx +0 -43
- package/dist/scaffolds/notis-random/packages/sdk/src/runtime.ts +0 -351
- package/dist/scaffolds/notis-random/packages/sdk/src/styles.css +0 -186
- package/dist/scaffolds/notis-random/packages/sdk/src/ui.ts +0 -15
- package/dist/scaffolds/notis-random/packages/sdk/src/vite.ts +0 -56
- package/dist/scaffolds/notis-random/packages/sdk/tsconfig.json +0 -15
- package/dist/scaffolds/notis-random/postcss.config.mjs +0 -6
- package/dist/scaffolds/notis-random/src/dev-main.tsx +0 -70
- package/dist/scaffolds/notis-random/src/mock-runtime.ts +0 -129
- package/dist/scaffolds/notis-random/tailwind.config.ts +0 -50
- package/dist/scaffolds/notis-random/tsconfig.json +0 -23
- package/dist/scaffolds/notis-random/vite.config.ts +0 -11
- package/dist/scaffolds.json +0 -49
|
@@ -7,13 +7,16 @@
|
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
9
|
import { spawn } from 'node:child_process';
|
|
10
|
-
import {
|
|
10
|
+
import { randomUUID } from 'node:crypto';
|
|
11
|
+
import { closeSync, constants as fsConstants, copyFileSync, cpSync, existsSync, fstatSync, lstatSync, mkdirSync, mkdtempSync, openSync, readFileSync, realpathSync, renameSync, rmdirSync, unlinkSync, writeFileSync, readdirSync, rmSync, statSync } from 'node:fs';
|
|
11
12
|
import { createRequire } from 'node:module';
|
|
12
|
-
import {
|
|
13
|
+
import { homedir } from 'node:os';
|
|
14
|
+
import { basename, dirname, join, relative, resolve } from 'node:path';
|
|
13
15
|
import { fileURLToPath } from 'node:url';
|
|
14
16
|
import { gunzipSync } from 'node:zlib';
|
|
15
17
|
|
|
16
18
|
import { usageError } from './errors.js';
|
|
19
|
+
import { acquireScaffoldSource, loadScaffoldCatalog } from './app-registry-scaffolds.js';
|
|
17
20
|
import { validateArtifactBoundary, validateProjectBoundary } from './app-boundary-validator.js';
|
|
18
21
|
import { CHANGELOG_MERGE_DATE, readAppChangelog } from './app-changelog.js';
|
|
19
22
|
|
|
@@ -24,10 +27,6 @@ const BUNDLE_DIR = join(OUTPUT_DIR, 'bundle');
|
|
|
24
27
|
const MANIFEST_FILE = join(OUTPUT_DIR, 'manifest.json');
|
|
25
28
|
const METADATA_DIR = 'metadata';
|
|
26
29
|
const CLI_ROOT = resolve(dirname(fileURLToPath(import.meta.url)), '../..');
|
|
27
|
-
const MONOREPO_SCAFFOLDS_DIR = resolve(CLI_ROOT, '../..', 'scaffolds');
|
|
28
|
-
const DIST_DIR = join(CLI_ROOT, 'dist');
|
|
29
|
-
const SCAFFOLD_CATALOG_FILE = join(DIST_DIR, 'scaffolds.json');
|
|
30
|
-
const SCAFFOLD_SOURCE_DIR = join(DIST_DIR, 'scaffolds');
|
|
31
30
|
const TEMPLATE_SDK_DIR = join(CLI_ROOT, 'template', 'packages', 'sdk');
|
|
32
31
|
export const NOTIS_APP_CATEGORIES = [
|
|
33
32
|
'Productivity',
|
|
@@ -48,6 +47,9 @@ const SOURCE_COPY_EXCLUDES = new Set([
|
|
|
48
47
|
// scripts/ directory must not ship version-specific bytecode in the bundle.
|
|
49
48
|
'__pycache__',
|
|
50
49
|
]);
|
|
50
|
+
const SOURCE_COPY_EXCLUDES_CASEFOLDED = new Set(
|
|
51
|
+
[...SOURCE_COPY_EXCLUDES].map((name) => name.toLocaleLowerCase('en-US')),
|
|
52
|
+
);
|
|
51
53
|
const SCAFFOLD_COPY_EXCLUDES = new Set([
|
|
52
54
|
...SOURCE_COPY_EXCLUDES,
|
|
53
55
|
'coverage',
|
|
@@ -60,6 +62,11 @@ const SCAFFOLD_COPY_EXCLUDES = new Set([
|
|
|
60
62
|
// not listing media, it is the stub data the dev harness serves, and dropping
|
|
61
63
|
// it would make every route of a fresh project render its empty state.
|
|
62
64
|
const SCAFFOLD_LISTING_MEDIA = /^metadata\/screenshot-\d+\.png$/i;
|
|
65
|
+
const PULL_LOCK_TIMEOUT_MS = 30_000;
|
|
66
|
+
const PULL_LOCK_POLL_MS = 25;
|
|
67
|
+
const STATE_WRITE_LOCK_TIMEOUT_MS = 5_000;
|
|
68
|
+
const STATE_WRITE_LOCK_STALE_MS = 2_000;
|
|
69
|
+
const stateWriteLockWait = new Int32Array(new SharedArrayBuffer(4));
|
|
63
70
|
// A directory-declared skill ships every supporting file to the sandbox, so it
|
|
64
71
|
// needs a ceiling of its own. Kept well above the 512 KB SKILL.md limit so a
|
|
65
72
|
// handful of scripts always fits, and far below the bundle machinery's own
|
|
@@ -71,8 +78,38 @@ let appConfigImportNonce = 0;
|
|
|
71
78
|
// Project directory resolution
|
|
72
79
|
// ---------------------------------------------------------------------------
|
|
73
80
|
|
|
81
|
+
// Every Notis app the user did not deliberately place lives here, mirroring the
|
|
82
|
+
// desktop's synced-skills root (~/.notis/skills). A default that depends on the
|
|
83
|
+
// caller's working directory is unusable for agents: the shell sits wherever
|
|
84
|
+
// the previous command left it, so `apps init` would scatter projects into
|
|
85
|
+
// unrelated checkouts. Pass an explicit [dir] to override -- that is how an app
|
|
86
|
+
// ends up in a tracked git repo or an existing monorepo.
|
|
87
|
+
export const DEFAULT_APPS_ROOT = join(homedir(), NOTIS_DIR, 'apps');
|
|
88
|
+
|
|
89
|
+
export function defaultAppProjectDir(slug) {
|
|
90
|
+
const safeSlug = String(slug || '').trim();
|
|
91
|
+
if (!safeSlug || safeSlug.includes('/') || safeSlug.includes('\\') || safeSlug.startsWith('.')) {
|
|
92
|
+
throw usageError(`Cannot derive a default project directory from "${slug}". Pass an explicit target directory.`);
|
|
93
|
+
}
|
|
94
|
+
return join(DEFAULT_APPS_ROOT, safeSlug);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// A `~/...` path only expands when a shell is involved. Agents routinely spawn
|
|
98
|
+
// the CLI without one, and the documented default home is written with a tilde,
|
|
99
|
+
// so expand it here instead of creating a literal "~" directory.
|
|
100
|
+
export function expandHomePath(inputPath) {
|
|
101
|
+
const value = String(inputPath ?? '');
|
|
102
|
+
if (value === '~') {
|
|
103
|
+
return homedir();
|
|
104
|
+
}
|
|
105
|
+
if (value.startsWith('~/') || value.startsWith('~\\')) {
|
|
106
|
+
return join(homedir(), value.slice(2));
|
|
107
|
+
}
|
|
108
|
+
return value;
|
|
109
|
+
}
|
|
110
|
+
|
|
74
111
|
export function resolveProjectDir(inputDir = '.') {
|
|
75
|
-
return resolve(process.cwd(), inputDir);
|
|
112
|
+
return resolve(process.cwd(), expandHomePath(inputDir));
|
|
76
113
|
}
|
|
77
114
|
|
|
78
115
|
export function getBundleDir(projectDir) {
|
|
@@ -643,6 +680,9 @@ function validateConfiguredRoutes(routes) {
|
|
|
643
680
|
if (route.default) {
|
|
644
681
|
defaultCount += 1;
|
|
645
682
|
}
|
|
683
|
+
if (route.resourceDeepLinks !== undefined && typeof route.resourceDeepLinks !== 'boolean') {
|
|
684
|
+
throw usageError(`Route "${route.slug}" resourceDeepLinks must be a boolean.`);
|
|
685
|
+
}
|
|
646
686
|
}
|
|
647
687
|
|
|
648
688
|
if (defaultCount !== 1) {
|
|
@@ -762,6 +802,7 @@ export function generateManifest(appConfig, projectDir) {
|
|
|
762
802
|
icon: route.icon || null,
|
|
763
803
|
parentSlug: route.parentSlug || null,
|
|
764
804
|
default: route.default || false,
|
|
805
|
+
resourceDeepLinks: route.resourceDeepLinks === true,
|
|
765
806
|
export_name: route.exportName || route.export_name || exportNameFromPath(route.path),
|
|
766
807
|
collection: route.collection || null,
|
|
767
808
|
};
|
|
@@ -859,11 +900,43 @@ export function generateManifest(appConfig, projectDir) {
|
|
|
859
900
|
databases,
|
|
860
901
|
capabilities: normalizeAppCapabilities(appConfig.capabilities),
|
|
861
902
|
tools: appConfig.tools || [],
|
|
903
|
+
tool_bindings: normalizeAppToolBindings(appConfig.toolBindings),
|
|
862
904
|
skills,
|
|
863
905
|
onboarding: appConfig.onboarding || null,
|
|
864
906
|
};
|
|
865
907
|
}
|
|
866
908
|
|
|
909
|
+
/**
|
|
910
|
+
* Keep the persisted app row's presentation metadata aligned with the bundle
|
|
911
|
+
* manifest. The app slug remains the stable identity; title/name is only the
|
|
912
|
+
* user-facing label.
|
|
913
|
+
*/
|
|
914
|
+
export function appRowFieldsFromManifest(manifest) {
|
|
915
|
+
const app = manifest?.app && typeof manifest.app === 'object' ? manifest.app : {};
|
|
916
|
+
const displayName = typeof app.title === 'string' && app.title.trim()
|
|
917
|
+
? app.title.trim()
|
|
918
|
+
: typeof app.name === 'string' && app.name.trim()
|
|
919
|
+
? app.name.trim()
|
|
920
|
+
: null;
|
|
921
|
+
return {
|
|
922
|
+
...(displayName ? { name: displayName } : {}),
|
|
923
|
+
accent: app.accent ?? null,
|
|
924
|
+
};
|
|
925
|
+
}
|
|
926
|
+
|
|
927
|
+
export function normalizeAppToolBindings(bindings) {
|
|
928
|
+
return (Array.isArray(bindings) ? bindings : [])
|
|
929
|
+
.map((binding) => {
|
|
930
|
+
const name = typeof binding?.name === 'string' ? binding.name.trim() : '';
|
|
931
|
+
const providerToolName =
|
|
932
|
+
typeof binding?.providerToolName === 'string' ? binding.providerToolName.trim() : '';
|
|
933
|
+
return name && providerToolName
|
|
934
|
+
? { name, provider_tool_name: providerToolName }
|
|
935
|
+
: null;
|
|
936
|
+
})
|
|
937
|
+
.filter(Boolean);
|
|
938
|
+
}
|
|
939
|
+
|
|
867
940
|
/**
|
|
868
941
|
* Keeps only capabilities the platform actually understands, at the exact
|
|
869
942
|
* values it accepts. An unknown key or value is dropped rather than passed
|
|
@@ -1065,21 +1138,212 @@ function copyMetadataAssets(projectDir) {
|
|
|
1065
1138
|
// Linking
|
|
1066
1139
|
// ---------------------------------------------------------------------------
|
|
1067
1140
|
|
|
1068
|
-
|
|
1141
|
+
const PROFILE_LINK_FIELDS = new Set([
|
|
1142
|
+
'app_id',
|
|
1143
|
+
'linked_at',
|
|
1144
|
+
'deployed_at',
|
|
1145
|
+
'version',
|
|
1146
|
+
'dev_app_id',
|
|
1147
|
+
'dev_linked_at',
|
|
1148
|
+
'auto_linked_at',
|
|
1149
|
+
'cloud_computer_shell_consent',
|
|
1150
|
+
]);
|
|
1151
|
+
|
|
1152
|
+
function splitLinkedState(state) {
|
|
1153
|
+
const base = {};
|
|
1154
|
+
const link = {};
|
|
1155
|
+
for (const [key, value] of Object.entries(state || {})) {
|
|
1156
|
+
if (key === 'profiles') continue;
|
|
1157
|
+
(PROFILE_LINK_FIELDS.has(key) ? link : base)[key] = value;
|
|
1158
|
+
}
|
|
1159
|
+
return { base, link };
|
|
1160
|
+
}
|
|
1161
|
+
|
|
1162
|
+
export function appLinkedStateProfileKey({ apiBase, userId }) {
|
|
1163
|
+
const normalizedApi = String(apiBase || '').trim().replace(/\/$/, '');
|
|
1164
|
+
const normalizedUser = String(userId || '').trim();
|
|
1165
|
+
if (!normalizedApi || !normalizedUser) return null;
|
|
1166
|
+
return Buffer.from(`${normalizedApi}\0${normalizedUser}`).toString('base64url');
|
|
1167
|
+
}
|
|
1168
|
+
|
|
1169
|
+
export function readLinkedState(projectDir, profileKey = null) {
|
|
1069
1170
|
const statePath = join(projectDir, STATE_FILE);
|
|
1171
|
+
assertLinkedStatePathSafe(projectDir);
|
|
1070
1172
|
if (!existsSync(statePath)) return null;
|
|
1071
|
-
|
|
1173
|
+
const raw = JSON.parse(readFileSync(statePath, 'utf-8'));
|
|
1174
|
+
if (!profileKey) return raw;
|
|
1175
|
+
const { base, link: legacyLink } = splitLinkedState(raw);
|
|
1176
|
+
const profiles = raw?.profiles && typeof raw.profiles === 'object' ? raw.profiles : {};
|
|
1177
|
+
const scoped = profiles[profileKey];
|
|
1178
|
+
return {
|
|
1179
|
+
...base,
|
|
1180
|
+
...(scoped && typeof scoped === 'object' ? scoped : legacyLink),
|
|
1181
|
+
};
|
|
1182
|
+
}
|
|
1183
|
+
|
|
1184
|
+
function readStateWriteLockOwner(lockPath) {
|
|
1185
|
+
try {
|
|
1186
|
+
return readFileSync(join(lockPath, 'owner'), 'utf8').trim();
|
|
1187
|
+
} catch {
|
|
1188
|
+
return null;
|
|
1189
|
+
}
|
|
1190
|
+
}
|
|
1191
|
+
|
|
1192
|
+
function reclaimStaleStateWriteLock(lockPath, observedOwner, observedMtimeMs) {
|
|
1193
|
+
const quarantinePath = `${lockPath}.stale-${process.pid}-${randomUUID()}`;
|
|
1194
|
+
try {
|
|
1195
|
+
renameSync(lockPath, quarantinePath);
|
|
1196
|
+
} catch (error) {
|
|
1197
|
+
if (error?.code === 'ENOENT') return false;
|
|
1198
|
+
throw error;
|
|
1199
|
+
}
|
|
1200
|
+
|
|
1201
|
+
let unchanged = false;
|
|
1202
|
+
try {
|
|
1203
|
+
const quarantinedStat = lstatSync(quarantinePath);
|
|
1204
|
+
unchanged = readStateWriteLockOwner(quarantinePath) === observedOwner
|
|
1205
|
+
&& quarantinedStat.mtimeMs === observedMtimeMs;
|
|
1206
|
+
} catch (error) {
|
|
1207
|
+
try {
|
|
1208
|
+
renameSync(quarantinePath, lockPath);
|
|
1209
|
+
} catch {
|
|
1210
|
+
// Fail closed below if the quarantined lock cannot be restored.
|
|
1211
|
+
}
|
|
1212
|
+
throw error;
|
|
1213
|
+
}
|
|
1214
|
+
|
|
1215
|
+
if (!unchanged) {
|
|
1216
|
+
try {
|
|
1217
|
+
renameSync(quarantinePath, lockPath);
|
|
1218
|
+
} catch {
|
|
1219
|
+
throw usageError(`Notis app state lock changed while it was being recovered: ${lockPath}`);
|
|
1220
|
+
}
|
|
1221
|
+
return false;
|
|
1222
|
+
}
|
|
1223
|
+
|
|
1224
|
+
rmSync(quarantinePath, { recursive: true, force: true });
|
|
1225
|
+
return true;
|
|
1072
1226
|
}
|
|
1073
1227
|
|
|
1074
|
-
|
|
1228
|
+
function withStateWriteLock(statePath, callback) {
|
|
1229
|
+
mkdirSync(dirname(statePath), { recursive: true, mode: 0o700 });
|
|
1230
|
+
const lockPath = `${statePath}.write-lock`;
|
|
1231
|
+
const ownerId = `${process.pid}.${randomUUID()}`;
|
|
1232
|
+
const startedAt = Date.now();
|
|
1233
|
+
while (true) {
|
|
1234
|
+
try {
|
|
1235
|
+
mkdirSync(lockPath, { mode: 0o700 });
|
|
1236
|
+
try {
|
|
1237
|
+
writeFileSync(join(lockPath, 'owner'), ownerId, { mode: 0o600, flag: 'wx' });
|
|
1238
|
+
} catch (error) {
|
|
1239
|
+
// A stale-lock recovery may have moved this candidate while the owner
|
|
1240
|
+
// record was being written. Never remove a replacement writer's lock.
|
|
1241
|
+
if (readStateWriteLockOwner(lockPath) === ownerId) {
|
|
1242
|
+
rmSync(lockPath, { recursive: true, force: true });
|
|
1243
|
+
}
|
|
1244
|
+
throw error;
|
|
1245
|
+
}
|
|
1246
|
+
break;
|
|
1247
|
+
} catch (error) {
|
|
1248
|
+
if (error?.code !== 'EEXIST') throw error;
|
|
1249
|
+
try {
|
|
1250
|
+
const lockStat = lstatSync(lockPath);
|
|
1251
|
+
if (lockStat.isSymbolicLink() || !lockStat.isDirectory()) {
|
|
1252
|
+
throw usageError(`Refusing unsafe Notis app state lock: ${lockPath}`);
|
|
1253
|
+
}
|
|
1254
|
+
const observedOwner = readStateWriteLockOwner(lockPath);
|
|
1255
|
+
const ownerPidText = String(observedOwner || '').split('.')[0];
|
|
1256
|
+
const ownerPid = /^\d+$/.test(ownerPidText) ? Number.parseInt(ownerPidText, 10) : null;
|
|
1257
|
+
// PID liveness alone is insufficient: operating systems can reuse a
|
|
1258
|
+
// crashed writer's PID for an unrelated process. A linked-state write
|
|
1259
|
+
// is synchronous and normally holds this lock only for milliseconds,
|
|
1260
|
+
// so an aged lock is stale even when that numeric PID now exists.
|
|
1261
|
+
let stale = Date.now() - lockStat.mtimeMs >= STATE_WRITE_LOCK_STALE_MS;
|
|
1262
|
+
if (!stale && Number.isInteger(ownerPid) && ownerPid > 0) {
|
|
1263
|
+
try {
|
|
1264
|
+
process.kill(ownerPid, 0);
|
|
1265
|
+
} catch (ownerError) {
|
|
1266
|
+
if (ownerError?.code === 'ESRCH') {
|
|
1267
|
+
stale = true;
|
|
1268
|
+
}
|
|
1269
|
+
}
|
|
1270
|
+
}
|
|
1271
|
+
if (stale && reclaimStaleStateWriteLock(lockPath, observedOwner, lockStat.mtimeMs)) {
|
|
1272
|
+
continue;
|
|
1273
|
+
}
|
|
1274
|
+
} catch (statError) {
|
|
1275
|
+
if (statError?.code === 'ENOENT') continue;
|
|
1276
|
+
throw statError;
|
|
1277
|
+
}
|
|
1278
|
+
if (Date.now() - startedAt >= STATE_WRITE_LOCK_TIMEOUT_MS) {
|
|
1279
|
+
throw usageError(`Timed out waiting to update Notis app state: ${statePath}`);
|
|
1280
|
+
}
|
|
1281
|
+
Atomics.wait(stateWriteLockWait, 0, 0, 10);
|
|
1282
|
+
}
|
|
1283
|
+
}
|
|
1284
|
+
try {
|
|
1285
|
+
return callback();
|
|
1286
|
+
} finally {
|
|
1287
|
+
try {
|
|
1288
|
+
if (readStateWriteLockOwner(lockPath) === ownerId) {
|
|
1289
|
+
rmSync(lockPath, { recursive: true, force: true });
|
|
1290
|
+
}
|
|
1291
|
+
} catch {
|
|
1292
|
+
// Never remove a lock that no longer belongs to this writer.
|
|
1293
|
+
}
|
|
1294
|
+
}
|
|
1295
|
+
}
|
|
1296
|
+
|
|
1297
|
+
export function writeLinkedState(projectDir, state, profileKey = null) {
|
|
1298
|
+
const statePath = join(projectDir, STATE_FILE);
|
|
1299
|
+
assertLinkedStatePathSafe(projectDir);
|
|
1300
|
+
return withStateWriteLock(statePath, () => {
|
|
1301
|
+
let nextState = state;
|
|
1302
|
+
if (profileKey) {
|
|
1303
|
+
let current = {};
|
|
1304
|
+
if (existsSync(statePath)) {
|
|
1305
|
+
current = JSON.parse(readFileSync(statePath, 'utf-8'));
|
|
1306
|
+
}
|
|
1307
|
+
const { base: currentBase } = splitLinkedState(current);
|
|
1308
|
+
const { base } = splitLinkedState(state);
|
|
1309
|
+
const { link } = splitLinkedState(state);
|
|
1310
|
+
const profiles = current?.profiles && typeof current.profiles === 'object'
|
|
1311
|
+
? { ...current.profiles }
|
|
1312
|
+
: {};
|
|
1313
|
+
profiles[profileKey] = link;
|
|
1314
|
+
nextState = { ...currentBase, ...base, profiles };
|
|
1315
|
+
}
|
|
1316
|
+
const temporary = `${statePath}.${process.pid}.${randomUUID()}.tmp`;
|
|
1317
|
+
writeFileSync(temporary, JSON.stringify(nextState, null, 2) + '\n');
|
|
1318
|
+
renameSync(temporary, statePath);
|
|
1319
|
+
});
|
|
1320
|
+
}
|
|
1321
|
+
|
|
1322
|
+
function assertLinkedStatePathSafe(projectDir) {
|
|
1323
|
+
const notisDir = join(projectDir, NOTIS_DIR);
|
|
1324
|
+
try {
|
|
1325
|
+
const directoryStat = lstatSync(notisDir);
|
|
1326
|
+
if (directoryStat.isSymbolicLink() || !directoryStat.isDirectory()) {
|
|
1327
|
+
throw usageError(`Refusing to use unsafe Notis state directory: ${notisDir}`);
|
|
1328
|
+
}
|
|
1329
|
+
} catch (error) {
|
|
1330
|
+
if (error?.code !== 'ENOENT') throw error;
|
|
1331
|
+
}
|
|
1332
|
+
|
|
1075
1333
|
const statePath = join(projectDir, STATE_FILE);
|
|
1076
|
-
|
|
1077
|
-
|
|
1334
|
+
try {
|
|
1335
|
+
const stateStat = lstatSync(statePath);
|
|
1336
|
+
if (stateStat.isSymbolicLink() || !stateStat.isFile()) {
|
|
1337
|
+
throw usageError(`Refusing to use unsafe Notis state file: ${statePath}`);
|
|
1338
|
+
}
|
|
1339
|
+
} catch (error) {
|
|
1340
|
+
if (error?.code !== 'ENOENT') throw error;
|
|
1341
|
+
}
|
|
1078
1342
|
}
|
|
1079
1343
|
|
|
1080
|
-
export function requireLinkedAppId(projectDir, explicitAppId) {
|
|
1344
|
+
export function requireLinkedAppId(projectDir, explicitAppId, profileKey = null) {
|
|
1081
1345
|
if (explicitAppId) return explicitAppId;
|
|
1082
|
-
const state = readLinkedState(projectDir);
|
|
1346
|
+
const state = readLinkedState(projectDir, profileKey);
|
|
1083
1347
|
if (state?.app_id) return state.app_id;
|
|
1084
1348
|
throw usageError('This project is not linked to a Notis app. Run "notis apps link <app-id> ." first.');
|
|
1085
1349
|
}
|
|
@@ -1089,79 +1353,342 @@ export function requireLinkedAppId(projectDir, explicitAppId) {
|
|
|
1089
1353
|
// ---------------------------------------------------------------------------
|
|
1090
1354
|
|
|
1091
1355
|
/**
|
|
1092
|
-
* Scaffold a new Notis app project from the SDK template
|
|
1356
|
+
* Scaffold a new Notis app project from the SDK template or from a published
|
|
1357
|
+
* Store app downloaded from the public registry repository.
|
|
1093
1358
|
*/
|
|
1094
|
-
export function
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
:
|
|
1359
|
+
export async function scaffoldProject({ projectDir, appName, fromSlug = null }) {
|
|
1360
|
+
let scaffoldSource = null;
|
|
1361
|
+
if (fromSlug) {
|
|
1362
|
+
scaffoldSource = await acquireScaffoldSource(fromSlug);
|
|
1363
|
+
if (!scaffoldSource) {
|
|
1364
|
+
const known = (await loadScaffoldCatalog()).map((entry) => entry.slug).join(', ') || 'none';
|
|
1365
|
+
throw usageError(`Unknown scaffold "${fromSlug}". Available scaffolds: ${known}.`);
|
|
1366
|
+
}
|
|
1101
1367
|
}
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1368
|
+
try {
|
|
1369
|
+
return scaffoldProjectFromDir({
|
|
1370
|
+
projectDir,
|
|
1371
|
+
appName,
|
|
1372
|
+
fromSlug,
|
|
1373
|
+
templateDir: scaffoldSource ? scaffoldSource.dir : join(CLI_ROOT, 'template'),
|
|
1374
|
+
});
|
|
1375
|
+
} finally {
|
|
1376
|
+
scaffoldSource?.cleanup();
|
|
1377
|
+
}
|
|
1378
|
+
}
|
|
1379
|
+
|
|
1380
|
+
function scaffoldProjectFromDir({ projectDir, appName, fromSlug, templateDir }) {
|
|
1381
|
+
const sourceDir = resolve(templateDir);
|
|
1382
|
+
const requestedProjectDir = resolve(projectDir);
|
|
1383
|
+
if (!existsSync(sourceDir)) {
|
|
1384
|
+
throw usageError(`SDK template not found at ${sourceDir}. Ensure @notis_ai/cli is installed correctly.`);
|
|
1385
|
+
}
|
|
1386
|
+
|
|
1387
|
+
const parentDir = dirname(requestedProjectDir);
|
|
1388
|
+
const targetName = basename(requestedProjectDir);
|
|
1389
|
+
mkdirSync(parentDir, { recursive: true });
|
|
1390
|
+
const canonicalParent = realpathSync(parentDir);
|
|
1391
|
+
const parentIdentity = capturePullTargetIdentity(canonicalParent);
|
|
1392
|
+
const initialTargetIdentity = captureScaffoldTargetIdentity(requestedProjectDir);
|
|
1393
|
+
const previousCwd = process.cwd();
|
|
1394
|
+
const previousCwdIdentity = capturePullTargetIdentity(previousCwd);
|
|
1395
|
+
let cwdPinned = false;
|
|
1396
|
+
let stageName = null;
|
|
1397
|
+
let cleanupStage = true;
|
|
1398
|
+
let activeTargetIdentity = null;
|
|
1399
|
+
const scaffoldLockName = '.notis-app-scaffold-lock';
|
|
1400
|
+
const scaffoldLockOwnerId = `${process.pid}.${randomUUID()}`;
|
|
1401
|
+
try {
|
|
1402
|
+
process.chdir(canonicalParent);
|
|
1403
|
+
cwdPinned = true;
|
|
1404
|
+
assertPullTargetIdentity('.', parentIdentity);
|
|
1405
|
+
const lockedTargetIdentity = captureScaffoldTargetIdentity(targetName);
|
|
1406
|
+
if (!pullTargetIdentitiesMatch(initialTargetIdentity, lockedTargetIdentity)) {
|
|
1407
|
+
throw usageError(`App scaffold target changed before copying: ${requestedProjectDir}`);
|
|
1408
|
+
}
|
|
1409
|
+
if (!lockedTargetIdentity.exists) {
|
|
1410
|
+
try {
|
|
1411
|
+
mkdirSync(targetName);
|
|
1412
|
+
} catch (error) {
|
|
1413
|
+
if (error?.code === 'EEXIST') {
|
|
1414
|
+
throw usageError(`App scaffold target changed before copying: ${requestedProjectDir}`);
|
|
1415
|
+
}
|
|
1416
|
+
throw error;
|
|
1417
|
+
}
|
|
1418
|
+
}
|
|
1419
|
+
activeTargetIdentity = captureScaffoldTargetIdentity(targetName);
|
|
1420
|
+
process.chdir(targetName);
|
|
1421
|
+
if (!pullTargetIdentitiesMatch(
|
|
1422
|
+
activeTargetIdentity,
|
|
1423
|
+
captureScaffoldTargetIdentity('.'),
|
|
1424
|
+
)) {
|
|
1425
|
+
throw usageError(`App scaffold target changed before copying: ${requestedProjectDir}`);
|
|
1426
|
+
}
|
|
1427
|
+
mkdirSync(scaffoldLockName, { mode: 0o700 });
|
|
1428
|
+
writeFileSync(
|
|
1429
|
+
join(scaffoldLockName, 'owner'),
|
|
1430
|
+
JSON.stringify({ id: scaffoldLockOwnerId }),
|
|
1431
|
+
{ mode: 0o600 },
|
|
1432
|
+
);
|
|
1107
1433
|
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1434
|
+
stageName = basename(mkdtempSync(join('.', '.notis-app-scaffold-')));
|
|
1435
|
+
projectDir = stageName;
|
|
1436
|
+
|
|
1437
|
+
copyScaffoldSource(sourceDir, projectDir);
|
|
1438
|
+
|
|
1439
|
+
// Update package.json with the app name
|
|
1440
|
+
const pkgPath = join(projectDir, 'package.json');
|
|
1441
|
+
if (existsSync(pkgPath)) {
|
|
1442
|
+
const pkg = JSON.parse(readFileSync(pkgPath, 'utf-8'));
|
|
1443
|
+
pkg.name = appName.toLowerCase().replace(/[^a-z0-9-]/g, '-').replace(/-+/g, '-');
|
|
1444
|
+
// A scaffold creates a new app identity, even when its source comes from a
|
|
1445
|
+
// versioned Store example. New apps must therefore start their own release
|
|
1446
|
+
// history instead of inheriting the example app's registry version.
|
|
1447
|
+
pkg.notisAppVersion = '0.1.0';
|
|
1448
|
+
normalizeScaffoldPackageScripts(pkg);
|
|
1449
|
+
ensureScaffoldLocalSdk(projectDir, pkg);
|
|
1450
|
+
writeFileSync(pkgPath, JSON.stringify(pkg, null, 2) + '\n');
|
|
1451
|
+
normalizeScaffoldLockfile(projectDir, pkg);
|
|
1112
1452
|
}
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
const slugName = jsStringLiteral(safeKebab(appName) || 'my-notis-app');
|
|
1139
|
-
if (fromSlug) {
|
|
1140
|
-
if (/name\s*:/.test(config)) {
|
|
1141
|
-
config = config.replace(/name\s*:\s*(['"`])[\s\S]*?\1/, `name: ${slugName}`);
|
|
1453
|
+
|
|
1454
|
+
// Update notis.config.ts with the app name
|
|
1455
|
+
const configPath = join(projectDir, 'notis.config.ts');
|
|
1456
|
+
if (existsSync(configPath)) {
|
|
1457
|
+
let config = readFileSync(configPath, 'utf-8');
|
|
1458
|
+
const displayName = jsStringLiteral(appName);
|
|
1459
|
+
const slugName = jsStringLiteral(safeKebab(appName) || 'my-notis-app');
|
|
1460
|
+
if (fromSlug) {
|
|
1461
|
+
if (/name\s*:/.test(config)) {
|
|
1462
|
+
config = config.replace(/name\s*:\s*(['"`])[\s\S]*?\1/, `name: ${slugName}`);
|
|
1463
|
+
}
|
|
1464
|
+
if (/title\s*:/.test(config)) {
|
|
1465
|
+
config = config.replace(/title\s*:\s*(['"`])[\s\S]*?\1/, `title: ${displayName}`);
|
|
1466
|
+
} else {
|
|
1467
|
+
config = config.replace(/name\s*:\s*(['"`])[\s\S]*?\1/, `name: ${slugName},\n title: ${displayName}`);
|
|
1468
|
+
}
|
|
1469
|
+
} else {
|
|
1470
|
+
if (/name\s*:/.test(config)) {
|
|
1471
|
+
config = config.replace(/name\s*:\s*(['"`])[\s\S]*?\1/, `name: ${slugName}`);
|
|
1472
|
+
}
|
|
1473
|
+
if (/title\s*:/.test(config)) {
|
|
1474
|
+
config = config.replace(/title\s*:\s*(['"`])[\s\S]*?\1/, `title: ${displayName}`);
|
|
1475
|
+
} else {
|
|
1476
|
+
config = config.replace(/'My Notis App'/, displayName);
|
|
1477
|
+
}
|
|
1142
1478
|
}
|
|
1143
|
-
if (/
|
|
1144
|
-
config = config.replace(/
|
|
1479
|
+
if (/devSlug\s*:/.test(config)) {
|
|
1480
|
+
config = config.replace(/devSlug\s*:\s*(['"`])[\s\S]*?\1/, `devSlug: ${slugName}`);
|
|
1145
1481
|
} else {
|
|
1146
|
-
config = config.replace(
|
|
1482
|
+
config = config.replace(
|
|
1483
|
+
/name\s*:\s*(['"`])[\s\S]*?\1/,
|
|
1484
|
+
(nameDeclaration) => `${nameDeclaration},\n devSlug: ${slugName}`,
|
|
1485
|
+
);
|
|
1147
1486
|
}
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1487
|
+
config = removeConfigArrayProperty(config, 'screenshots');
|
|
1488
|
+
writeFileSync(configPath, config);
|
|
1489
|
+
}
|
|
1490
|
+
|
|
1491
|
+
resetScaffoldChangelog(projectDir, appName);
|
|
1492
|
+
|
|
1493
|
+
assertPullParentIdentity(parentDir, canonicalParent, parentIdentity);
|
|
1494
|
+
assertPullTargetIdentity(requestedProjectDir, activeTargetIdentity);
|
|
1495
|
+
const stageRoot = realpathSync(stageName);
|
|
1496
|
+
const installedEntries = [];
|
|
1497
|
+
const installedDirectoryIdentities = new Map([['', activeTargetIdentity]]);
|
|
1498
|
+
const captureInstalledEntry = (entryPath) => {
|
|
1499
|
+
const entryStat = lstatSync(entryPath, { bigint: true });
|
|
1500
|
+
return {
|
|
1501
|
+
exists: true,
|
|
1502
|
+
path: entryPath,
|
|
1503
|
+
dev: entryStat.dev,
|
|
1504
|
+
ino: entryStat.ino,
|
|
1505
|
+
directory: entryStat.isDirectory(),
|
|
1506
|
+
};
|
|
1507
|
+
};
|
|
1508
|
+
const installExclusive = (sourcePath, destinationName, relativePath, parentIdentity) => {
|
|
1509
|
+
const sourceStat = lstatSync(sourcePath);
|
|
1510
|
+
if (sourceStat.isSymbolicLink()) {
|
|
1511
|
+
throw usageError(`Refusing symlinked scaffold source entry: ${sourcePath}`);
|
|
1151
1512
|
}
|
|
1152
|
-
if (
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1513
|
+
if (sourceStat.isDirectory()) {
|
|
1514
|
+
mkdirSync(destinationName, { mode: sourceStat.mode & 0o777 });
|
|
1515
|
+
const installedDirectory = captureInstalledEntry(destinationName);
|
|
1516
|
+
installedDirectory.path = relativePath;
|
|
1517
|
+
installedEntries.push(installedDirectory);
|
|
1518
|
+
installedDirectoryIdentities.set(relativePath, installedDirectory);
|
|
1519
|
+
process.chdir(destinationName);
|
|
1520
|
+
if (!pullTargetIdentitiesMatch(
|
|
1521
|
+
installedDirectory,
|
|
1522
|
+
capturePullTargetIdentity('.'),
|
|
1523
|
+
)) {
|
|
1524
|
+
throw usageError(`App scaffold destination changed during installation: ${relativePath}`);
|
|
1525
|
+
}
|
|
1526
|
+
try {
|
|
1527
|
+
for (const name of readdirSync(sourcePath)) {
|
|
1528
|
+
installExclusive(
|
|
1529
|
+
join(sourcePath, name),
|
|
1530
|
+
name,
|
|
1531
|
+
`${relativePath}/${name}`,
|
|
1532
|
+
installedDirectory,
|
|
1533
|
+
);
|
|
1534
|
+
}
|
|
1535
|
+
} finally {
|
|
1536
|
+
const parentRelativePath = dirname(relativePath) === '.' ? '' : dirname(relativePath);
|
|
1537
|
+
process.chdir(parentRelativePath
|
|
1538
|
+
? join(requestedProjectDir, parentRelativePath)
|
|
1539
|
+
: requestedProjectDir);
|
|
1540
|
+
if (!pullTargetIdentitiesMatch(
|
|
1541
|
+
parentIdentity,
|
|
1542
|
+
capturePullTargetIdentity('.'),
|
|
1543
|
+
)) {
|
|
1544
|
+
throw usageError(`App scaffold destination changed during installation: ${parentRelativePath || '.'}`);
|
|
1545
|
+
}
|
|
1546
|
+
}
|
|
1547
|
+
return;
|
|
1548
|
+
}
|
|
1549
|
+
if (!sourceStat.isFile()) {
|
|
1550
|
+
throw usageError(`Refusing unsupported scaffold source entry: ${sourcePath}`);
|
|
1551
|
+
}
|
|
1552
|
+
copyFileSync(sourcePath, destinationName, fsConstants.COPYFILE_EXCL);
|
|
1553
|
+
const installedFile = captureInstalledEntry(destinationName);
|
|
1554
|
+
installedFile.path = relativePath;
|
|
1555
|
+
installedEntries.push(installedFile);
|
|
1556
|
+
};
|
|
1557
|
+
try {
|
|
1558
|
+
for (const name of readdirSync(stageName)) {
|
|
1559
|
+
installExclusive(join(stageRoot, name), name, name, activeTargetIdentity);
|
|
1560
|
+
}
|
|
1561
|
+
assertPullParentIdentity(parentDir, canonicalParent, parentIdentity);
|
|
1562
|
+
assertPullTargetIdentity(requestedProjectDir, activeTargetIdentity);
|
|
1563
|
+
} catch (error) {
|
|
1564
|
+
try {
|
|
1565
|
+
for (const installed of installedEntries.reverse()) {
|
|
1566
|
+
const parentRelativePath = dirname(installed.path) === '.' ? '' : dirname(installed.path);
|
|
1567
|
+
const parentPath = parentRelativePath
|
|
1568
|
+
? join(requestedProjectDir, parentRelativePath)
|
|
1569
|
+
: requestedProjectDir;
|
|
1570
|
+
const expectedParentIdentity = installedDirectoryIdentities.get(parentRelativePath);
|
|
1571
|
+
process.chdir(parentPath);
|
|
1572
|
+
if (!expectedParentIdentity || !pullTargetIdentitiesMatch(
|
|
1573
|
+
expectedParentIdentity,
|
|
1574
|
+
capturePullTargetIdentity('.'),
|
|
1575
|
+
)) {
|
|
1576
|
+
throw usageError(
|
|
1577
|
+
`Refusing to roll back a scaffold directory that changed concurrently: ${parentPath}`,
|
|
1578
|
+
);
|
|
1579
|
+
}
|
|
1580
|
+
const entryName = basename(installed.path);
|
|
1581
|
+
const current = captureInstalledEntry(entryName);
|
|
1582
|
+
if (
|
|
1583
|
+
current.dev !== installed.dev
|
|
1584
|
+
|| current.ino !== installed.ino
|
|
1585
|
+
|| current.directory !== installed.directory
|
|
1586
|
+
) {
|
|
1587
|
+
throw usageError(
|
|
1588
|
+
`Refusing to roll back a scaffold entry that changed concurrently: ${installed.path}`,
|
|
1589
|
+
);
|
|
1590
|
+
}
|
|
1591
|
+
if (installed.directory) {
|
|
1592
|
+
rmdirSync(entryName);
|
|
1593
|
+
} else {
|
|
1594
|
+
unlinkSync(entryName);
|
|
1595
|
+
}
|
|
1596
|
+
}
|
|
1597
|
+
} catch (rollbackError) {
|
|
1598
|
+
cleanupStage = false;
|
|
1599
|
+
throw usageError(
|
|
1600
|
+
`${error instanceof Error ? error.message : String(error)} `
|
|
1601
|
+
+ `Scaffold rollback failed; recovery files were retained at `
|
|
1602
|
+
+ `${join(requestedProjectDir, stageName)}: `
|
|
1603
|
+
+ `${rollbackError instanceof Error ? rollbackError.message : String(rollbackError)}`,
|
|
1604
|
+
);
|
|
1605
|
+
}
|
|
1606
|
+
throw error;
|
|
1607
|
+
}
|
|
1608
|
+
try {
|
|
1609
|
+
rmSync(stageName, { recursive: true, force: true });
|
|
1610
|
+
stageName = null;
|
|
1611
|
+
} catch (error) {
|
|
1612
|
+
cleanupStage = false;
|
|
1613
|
+
throw usageError(
|
|
1614
|
+
`App scaffold was installed, but staging cleanup failed; recovery files were retained at `
|
|
1615
|
+
+ `${join(requestedProjectDir, stageName)}: `
|
|
1616
|
+
+ `${error instanceof Error ? error.message : String(error)}`,
|
|
1617
|
+
);
|
|
1618
|
+
}
|
|
1619
|
+
} finally {
|
|
1620
|
+
try {
|
|
1621
|
+
let targetPinnedForCleanup = false;
|
|
1622
|
+
if (activeTargetIdentity) {
|
|
1623
|
+
try {
|
|
1624
|
+
process.chdir(requestedProjectDir);
|
|
1625
|
+
targetPinnedForCleanup = pullTargetIdentitiesMatch(
|
|
1626
|
+
activeTargetIdentity,
|
|
1627
|
+
capturePullTargetIdentity('.'),
|
|
1628
|
+
);
|
|
1629
|
+
} catch {
|
|
1630
|
+
targetPinnedForCleanup = false;
|
|
1631
|
+
}
|
|
1632
|
+
}
|
|
1633
|
+
if (targetPinnedForCleanup && cleanupStage && stageName) {
|
|
1634
|
+
rmSync(stageName, { recursive: true, force: true });
|
|
1635
|
+
}
|
|
1636
|
+
if (
|
|
1637
|
+
targetPinnedForCleanup
|
|
1638
|
+
&& readPullLockOwner(scaffoldLockName)?.id === scaffoldLockOwnerId
|
|
1639
|
+
) {
|
|
1640
|
+
rmSync(scaffoldLockName, { recursive: true, force: true });
|
|
1641
|
+
}
|
|
1642
|
+
} finally {
|
|
1643
|
+
if (cwdPinned) {
|
|
1644
|
+
const cwdMatchesPrevious = (
|
|
1645
|
+
previousCwdIdentity.exists
|
|
1646
|
+
&& pullTargetIdentitiesMatch(previousCwdIdentity, capturePullTargetIdentity('.'))
|
|
1647
|
+
);
|
|
1648
|
+
if (!cwdMatchesPrevious) {
|
|
1649
|
+
process.chdir(previousCwd);
|
|
1650
|
+
}
|
|
1156
1651
|
}
|
|
1157
1652
|
}
|
|
1158
|
-
config = removeConfigArrayProperty(config, 'screenshots');
|
|
1159
|
-
writeFileSync(configPath, config);
|
|
1160
1653
|
}
|
|
1161
1654
|
|
|
1162
|
-
|
|
1655
|
+
return { projectDir: requestedProjectDir };
|
|
1656
|
+
}
|
|
1657
|
+
|
|
1658
|
+
function normalizeScaffoldPackageScripts(pkg) {
|
|
1659
|
+
const scripts = pkg?.scripts;
|
|
1660
|
+
if (!scripts || typeof scripts !== 'object' || Array.isArray(scripts)) {
|
|
1661
|
+
return;
|
|
1662
|
+
}
|
|
1663
|
+
const generateEntry = String(scripts['generate-entry'] || '').trim();
|
|
1664
|
+
if (!/^tsx\s+\.\.\/\.\.\/scripts\/generate-entry\.ts\s+\.$/.test(generateEntry)) {
|
|
1665
|
+
return;
|
|
1666
|
+
}
|
|
1667
|
+
for (const [name, command] of Object.entries(scripts)) {
|
|
1668
|
+
if (name === 'generate-entry' || typeof command !== 'string') {
|
|
1669
|
+
continue;
|
|
1670
|
+
}
|
|
1671
|
+
if (command.trim() === 'npm run generate-entry') {
|
|
1672
|
+
delete scripts[name];
|
|
1673
|
+
}
|
|
1674
|
+
}
|
|
1675
|
+
scripts['generate-entry'] = 'node -e ""';
|
|
1676
|
+
}
|
|
1163
1677
|
|
|
1164
|
-
|
|
1678
|
+
function captureScaffoldTargetIdentity(targetDir) {
|
|
1679
|
+
try {
|
|
1680
|
+
const targetStat = lstatSync(targetDir, { bigint: true });
|
|
1681
|
+
if (targetStat.isSymbolicLink() || !targetStat.isDirectory()) {
|
|
1682
|
+
throw usageError(`Refusing to scaffold through an unsafe target: ${targetDir}`);
|
|
1683
|
+
}
|
|
1684
|
+
if (readdirSync(targetDir).length > 0) {
|
|
1685
|
+
throw usageError(`App scaffold target must be empty: ${targetDir}`);
|
|
1686
|
+
}
|
|
1687
|
+
return { exists: true, dev: targetStat.dev, ino: targetStat.ino };
|
|
1688
|
+
} catch (error) {
|
|
1689
|
+
if (error?.code === 'ENOENT') return { exists: false, dev: null, ino: null };
|
|
1690
|
+
throw error;
|
|
1691
|
+
}
|
|
1165
1692
|
}
|
|
1166
1693
|
|
|
1167
1694
|
/**
|
|
@@ -1274,89 +1801,189 @@ function ensureScaffoldLocalSdk(projectDir, pkg) {
|
|
|
1274
1801
|
cpSync(TEMPLATE_SDK_DIR, localSdkDir, { recursive: true, dereference: true });
|
|
1275
1802
|
}
|
|
1276
1803
|
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
}
|
|
1282
|
-
const monorepoDir = join(MONOREPO_SCAFFOLDS_DIR, fromSlug);
|
|
1283
|
-
if (existsSync(join(monorepoDir, 'notis.config.ts'))) {
|
|
1284
|
-
return monorepoDir;
|
|
1285
|
-
}
|
|
1286
|
-
return bundledDir;
|
|
1287
|
-
}
|
|
1804
|
+
// Registry bookkeeping shipped alongside a published app's source: the listing
|
|
1805
|
+
// descriptor and the rendered Store gallery describe the published app, so a
|
|
1806
|
+
// scaffold copy must not inherit them.
|
|
1807
|
+
const REGISTRY_ARTIFACTS = /^(notis-listing\.json$|screenshots(\/|$))/;
|
|
1288
1808
|
|
|
1289
|
-
function
|
|
1290
|
-
|
|
1291
|
-
|
|
1809
|
+
function readStablePinnedFile(descriptor, expectedStat, label) {
|
|
1810
|
+
const before = fstatSync(descriptor, { bigint: true });
|
|
1811
|
+
if (
|
|
1812
|
+
!before.isFile()
|
|
1813
|
+
|| before.dev !== expectedStat.dev
|
|
1814
|
+
|| before.ino !== expectedStat.ino
|
|
1815
|
+
) {
|
|
1816
|
+
throw usageError(`${label} changed before it could be read.`);
|
|
1292
1817
|
}
|
|
1293
|
-
const
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
const description = readTsStringProperty(configSource, 'description') || '';
|
|
1304
|
-
scaffolds.push({
|
|
1305
|
-
slug: entry.name,
|
|
1306
|
-
name: readTsStringProperty(configSource, 'title') || readTsStringProperty(configSource, 'name') || entry.name,
|
|
1307
|
-
description,
|
|
1308
|
-
icon: readTsStringProperty(configSource, 'icon') || 'phosphor:squares-four',
|
|
1309
|
-
categories: readTsStringArrayProperty(configSource, 'categories'),
|
|
1310
|
-
tagline: readTsStringProperty(configSource, 'tagline') || description,
|
|
1311
|
-
});
|
|
1818
|
+
const content = readFileSync(descriptor);
|
|
1819
|
+
const after = fstatSync(descriptor, { bigint: true });
|
|
1820
|
+
if (
|
|
1821
|
+
after.dev !== before.dev
|
|
1822
|
+
|| after.ino !== before.ino
|
|
1823
|
+
|| after.size !== before.size
|
|
1824
|
+
|| after.mtimeNs !== before.mtimeNs
|
|
1825
|
+
|| after.ctimeNs !== before.ctimeNs
|
|
1826
|
+
) {
|
|
1827
|
+
throw usageError(`${label} changed while it was being read.`);
|
|
1312
1828
|
}
|
|
1313
|
-
return
|
|
1829
|
+
return content;
|
|
1314
1830
|
}
|
|
1315
1831
|
|
|
1316
|
-
function
|
|
1317
|
-
const
|
|
1318
|
-
return
|
|
1832
|
+
function captureDirectoryRevision() {
|
|
1833
|
+
const stat = statSync('.', { bigint: true });
|
|
1834
|
+
return { dev: stat.dev, ino: stat.ino, mtimeNs: stat.mtimeNs, ctimeNs: stat.ctimeNs };
|
|
1319
1835
|
}
|
|
1320
1836
|
|
|
1321
|
-
function
|
|
1322
|
-
const
|
|
1323
|
-
if (
|
|
1324
|
-
|
|
1837
|
+
function assertDirectoryRevisionUnchanged(before, label) {
|
|
1838
|
+
const after = captureDirectoryRevision();
|
|
1839
|
+
if (
|
|
1840
|
+
after.dev !== before.dev
|
|
1841
|
+
|| after.ino !== before.ino
|
|
1842
|
+
|| after.mtimeNs !== before.mtimeNs
|
|
1843
|
+
|| after.ctimeNs !== before.ctimeNs
|
|
1844
|
+
) {
|
|
1845
|
+
throw usageError(`${label} changed while it was being read.`);
|
|
1325
1846
|
}
|
|
1326
|
-
return Array.from(match[1].matchAll(/(['"`])([\s\S]*?)\1/g), (entry) => entry[2].trim()).filter(Boolean);
|
|
1327
1847
|
}
|
|
1328
1848
|
|
|
1329
1849
|
function copyScaffoldSource(sourceDir, targetDir) {
|
|
1330
1850
|
function shouldCopy(path) {
|
|
1331
1851
|
const name = path.split(/[\\/]/).pop();
|
|
1332
1852
|
if (!name) return true;
|
|
1333
|
-
|
|
1853
|
+
const relPath = relative(sourceDir, path).replace(/\\/g, '/');
|
|
1854
|
+
if (SCAFFOLD_LISTING_MEDIA.test(relPath) || REGISTRY_ARTIFACTS.test(relPath)) {
|
|
1334
1855
|
return false;
|
|
1335
1856
|
}
|
|
1336
1857
|
return !SCAFFOLD_COPY_EXCLUDES.has(name)
|
|
1858
|
+
&& !name.toLocaleLowerCase('en-US').startsWith('.notis-app-scaffold-')
|
|
1337
1859
|
&& !name.startsWith('.env')
|
|
1338
1860
|
&& !/\.(test|spec)\.[cm]?[jt]sx?$/i.test(name);
|
|
1339
1861
|
}
|
|
1340
1862
|
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
|
|
1863
|
+
const canonicalSource = realpathSync(sourceDir);
|
|
1864
|
+
const sourceIdentity = capturePullTargetIdentity(canonicalSource);
|
|
1865
|
+
const previousCwd = process.cwd();
|
|
1866
|
+
const previousCwdIdentity = capturePullTargetIdentity(previousCwd);
|
|
1867
|
+
const destinationIdentity = capturePullTargetIdentity(targetDir);
|
|
1868
|
+
const snapshot = { directories: [], files: [] };
|
|
1869
|
+
|
|
1870
|
+
function snapshotCurrent(sourcePath, node, expectedDirectoryIdentity) {
|
|
1871
|
+
if (!pullTargetIdentitiesMatch(expectedDirectoryIdentity, capturePullTargetIdentity('.'))) {
|
|
1872
|
+
throw usageError(`Scaffold source directory changed while copying: ${sourcePath}`);
|
|
1873
|
+
}
|
|
1874
|
+
const directoryRevision = captureDirectoryRevision();
|
|
1875
|
+
const parentIdentity = expectedDirectoryIdentity;
|
|
1876
|
+
for (const name of readdirSync('.')) {
|
|
1877
|
+
const logicalSourcePath = join(sourcePath, name);
|
|
1878
|
+
if (!shouldCopy(logicalSourcePath)) {
|
|
1879
|
+
continue;
|
|
1880
|
+
}
|
|
1881
|
+
const sourceStat = lstatSync(name, { bigint: true });
|
|
1882
|
+
if (sourceStat.isSymbolicLink()) {
|
|
1883
|
+
throw usageError(`Refusing symlinked scaffold source entry: ${logicalSourcePath}`);
|
|
1884
|
+
}
|
|
1885
|
+
if (sourceStat.isDirectory()) {
|
|
1886
|
+
const childNode = {
|
|
1887
|
+
name,
|
|
1888
|
+
mode: Number(sourceStat.mode & 0o777n),
|
|
1889
|
+
directories: [],
|
|
1890
|
+
files: [],
|
|
1891
|
+
};
|
|
1892
|
+
node.directories.push(childNode);
|
|
1893
|
+
process.chdir(name);
|
|
1894
|
+
const childIdentity = capturePullTargetIdentity('.');
|
|
1895
|
+
if (!pullTargetIdentitiesMatch(
|
|
1896
|
+
{ exists: true, dev: sourceStat.dev, ino: sourceStat.ino },
|
|
1897
|
+
childIdentity,
|
|
1898
|
+
)) {
|
|
1899
|
+
throw usageError(`Scaffold source directory changed while copying: ${logicalSourcePath}`);
|
|
1900
|
+
}
|
|
1901
|
+
try {
|
|
1902
|
+
snapshotCurrent(logicalSourcePath, childNode, childIdentity);
|
|
1903
|
+
} finally {
|
|
1904
|
+
process.chdir('..');
|
|
1905
|
+
if (!pullTargetIdentitiesMatch(parentIdentity, capturePullTargetIdentity('.'))) {
|
|
1906
|
+
throw usageError(`Scaffold source directory changed while copying: ${sourcePath}`);
|
|
1907
|
+
}
|
|
1908
|
+
}
|
|
1909
|
+
continue;
|
|
1910
|
+
}
|
|
1911
|
+
if (!sourceStat.isFile()) {
|
|
1912
|
+
throw usageError(`Refusing unsupported scaffold source entry: ${logicalSourcePath}`);
|
|
1913
|
+
}
|
|
1914
|
+
const descriptor = openSync(name, fsConstants.O_RDONLY | fsConstants.O_NOFOLLOW);
|
|
1915
|
+
try {
|
|
1916
|
+
node.files.push({
|
|
1917
|
+
name,
|
|
1918
|
+
mode: Number(sourceStat.mode & 0o777n),
|
|
1919
|
+
content: readStablePinnedFile(
|
|
1920
|
+
descriptor,
|
|
1921
|
+
sourceStat,
|
|
1922
|
+
`Scaffold source file ${logicalSourcePath}`,
|
|
1923
|
+
),
|
|
1924
|
+
});
|
|
1925
|
+
} finally {
|
|
1926
|
+
closeSync(descriptor);
|
|
1927
|
+
}
|
|
1928
|
+
}
|
|
1929
|
+
assertDirectoryRevisionUnchanged(directoryRevision, `Scaffold source directory ${sourcePath}`);
|
|
1930
|
+
}
|
|
1931
|
+
|
|
1932
|
+
try {
|
|
1933
|
+
process.chdir(canonicalSource);
|
|
1934
|
+
snapshotCurrent(canonicalSource, snapshot, sourceIdentity);
|
|
1935
|
+
} finally {
|
|
1936
|
+
process.chdir(previousCwd);
|
|
1937
|
+
if (!pullTargetIdentitiesMatch(previousCwdIdentity, capturePullTargetIdentity('.'))) {
|
|
1938
|
+
throw usageError(`Working directory changed while copying scaffold source: ${previousCwd}`);
|
|
1344
1939
|
}
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
1940
|
+
}
|
|
1941
|
+
|
|
1942
|
+
function writeSnapshot(node, logicalPath, expectedDirectoryIdentity) {
|
|
1943
|
+
if (!pullTargetIdentitiesMatch(expectedDirectoryIdentity, capturePullTargetIdentity('.'))) {
|
|
1944
|
+
throw usageError(`Scaffold destination changed while copying: ${logicalPath || '.'}`);
|
|
1945
|
+
}
|
|
1946
|
+
const parentIdentity = expectedDirectoryIdentity;
|
|
1947
|
+
for (const directory of node.directories) {
|
|
1948
|
+
mkdirSync(directory.name, { mode: directory.mode });
|
|
1949
|
+
const directoryStat = lstatSync(directory.name, { bigint: true });
|
|
1950
|
+
if (directoryStat.isSymbolicLink() || !directoryStat.isDirectory()) {
|
|
1951
|
+
throw usageError(`Refusing unsafe scaffold destination: ${join(logicalPath, directory.name)}`);
|
|
1952
|
+
}
|
|
1953
|
+
process.chdir(directory.name);
|
|
1954
|
+
const childIdentity = capturePullTargetIdentity('.');
|
|
1955
|
+
if (!pullTargetIdentitiesMatch(
|
|
1956
|
+
{ exists: true, dev: directoryStat.dev, ino: directoryStat.ino },
|
|
1957
|
+
childIdentity,
|
|
1958
|
+
)) {
|
|
1959
|
+
throw usageError(`Scaffold destination changed while copying: ${join(logicalPath, directory.name)}`);
|
|
1960
|
+
}
|
|
1961
|
+
try {
|
|
1962
|
+
writeSnapshot(directory, join(logicalPath, directory.name), childIdentity);
|
|
1963
|
+
} finally {
|
|
1964
|
+
process.chdir('..');
|
|
1965
|
+
if (!pullTargetIdentitiesMatch(parentIdentity, capturePullTargetIdentity('.'))) {
|
|
1966
|
+
throw usageError(`Scaffold destination changed while copying: ${logicalPath || '.'}`);
|
|
1967
|
+
}
|
|
1350
1968
|
}
|
|
1351
|
-
return;
|
|
1352
1969
|
}
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
cpSync(src, dest);
|
|
1970
|
+
for (const file of node.files) {
|
|
1971
|
+
writeFileSync(file.name, file.content, { flag: 'wx', mode: file.mode });
|
|
1356
1972
|
}
|
|
1357
1973
|
}
|
|
1358
1974
|
|
|
1359
|
-
|
|
1975
|
+
try {
|
|
1976
|
+
process.chdir(targetDir);
|
|
1977
|
+
if (!pullTargetIdentitiesMatch(destinationIdentity, capturePullTargetIdentity('.'))) {
|
|
1978
|
+
throw usageError(`Scaffold destination changed while copying: ${targetDir}`);
|
|
1979
|
+
}
|
|
1980
|
+
writeSnapshot(snapshot, '', destinationIdentity);
|
|
1981
|
+
} finally {
|
|
1982
|
+
process.chdir(previousCwd);
|
|
1983
|
+
if (!pullTargetIdentitiesMatch(previousCwdIdentity, capturePullTargetIdentity('.'))) {
|
|
1984
|
+
throw usageError(`Working directory changed while copying scaffold source: ${previousCwd}`);
|
|
1985
|
+
}
|
|
1986
|
+
}
|
|
1360
1987
|
}
|
|
1361
1988
|
|
|
1362
1989
|
// ---------------------------------------------------------------------------
|
|
@@ -1394,34 +2021,81 @@ export function collectArtifactFiles(projectDir) {
|
|
|
1394
2021
|
}
|
|
1395
2022
|
|
|
1396
2023
|
function shouldExcludeSourceEntry(name) {
|
|
2024
|
+
const casefolded = String(name || '').toLocaleLowerCase('en-US');
|
|
1397
2025
|
return (
|
|
1398
|
-
|
|
1399
|
-
||
|
|
1400
|
-
||
|
|
1401
|
-
||
|
|
2026
|
+
SOURCE_COPY_EXCLUDES_CASEFOLDED.has(casefolded)
|
|
2027
|
+
|| casefolded.startsWith('.notis-app-pull-')
|
|
2028
|
+
|| casefolded.startsWith('.notis-app-scaffold-')
|
|
2029
|
+
|| casefolded.startsWith('.env')
|
|
2030
|
+
|| casefolded.endsWith('.pyc')
|
|
2031
|
+
|| casefolded.endsWith('.pyo')
|
|
1402
2032
|
);
|
|
1403
2033
|
}
|
|
1404
2034
|
|
|
1405
2035
|
function readSourceFiles(projectDir) {
|
|
1406
2036
|
const files = {};
|
|
1407
|
-
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
|
|
2037
|
+
const canonicalProjectDir = realpathSync(projectDir);
|
|
2038
|
+
const projectIdentity = capturePullTargetIdentity(canonicalProjectDir);
|
|
2039
|
+
const previousCwd = process.cwd();
|
|
2040
|
+
const previousCwdIdentity = capturePullTargetIdentity(previousCwd);
|
|
2041
|
+
|
|
2042
|
+
function walkCurrent(prefix, expectedDirectoryIdentity) {
|
|
2043
|
+
if (!pullTargetIdentitiesMatch(expectedDirectoryIdentity, capturePullTargetIdentity('.'))) {
|
|
2044
|
+
throw usageError(`App source directory changed while packaging: ${prefix || '.'}`);
|
|
2045
|
+
}
|
|
2046
|
+
const directoryRevision = captureDirectoryRevision();
|
|
2047
|
+
const parentIdentity = expectedDirectoryIdentity;
|
|
2048
|
+
for (const name of readdirSync('.')) {
|
|
2049
|
+
if (shouldExcludeSourceEntry(name)) {
|
|
1412
2050
|
continue;
|
|
1413
2051
|
}
|
|
1414
|
-
const
|
|
1415
|
-
|
|
1416
|
-
|
|
1417
|
-
|
|
1418
|
-
|
|
1419
|
-
|
|
2052
|
+
const entryStat = lstatSync(name, { bigint: true });
|
|
2053
|
+
if (entryStat.isSymbolicLink()) {
|
|
2054
|
+
continue;
|
|
2055
|
+
}
|
|
2056
|
+
const relPath = prefix ? `${prefix}/${name}` : name;
|
|
2057
|
+
if (entryStat.isDirectory()) {
|
|
2058
|
+
process.chdir(name);
|
|
2059
|
+
const childIdentity = capturePullTargetIdentity('.');
|
|
2060
|
+
if (!pullTargetIdentitiesMatch(
|
|
2061
|
+
{ exists: true, dev: entryStat.dev, ino: entryStat.ino },
|
|
2062
|
+
childIdentity,
|
|
2063
|
+
)) {
|
|
2064
|
+
throw usageError(`App source directory changed while packaging: ${relPath}`);
|
|
2065
|
+
}
|
|
2066
|
+
try {
|
|
2067
|
+
walkCurrent(relPath, childIdentity);
|
|
2068
|
+
} finally {
|
|
2069
|
+
process.chdir('..');
|
|
2070
|
+
if (!pullTargetIdentitiesMatch(parentIdentity, capturePullTargetIdentity('.'))) {
|
|
2071
|
+
throw usageError(`App source directory changed while packaging: ${prefix || '.'}`);
|
|
2072
|
+
}
|
|
2073
|
+
}
|
|
2074
|
+
} else if (entryStat.isFile()) {
|
|
2075
|
+
const descriptor = openSync(name, fsConstants.O_RDONLY | fsConstants.O_NOFOLLOW);
|
|
2076
|
+
try {
|
|
2077
|
+
files[relPath] = readStablePinnedFile(
|
|
2078
|
+
descriptor,
|
|
2079
|
+
entryStat,
|
|
2080
|
+
`App source file ${relPath}`,
|
|
2081
|
+
);
|
|
2082
|
+
} finally {
|
|
2083
|
+
closeSync(descriptor);
|
|
2084
|
+
}
|
|
1420
2085
|
}
|
|
1421
2086
|
}
|
|
2087
|
+
assertDirectoryRevisionUnchanged(directoryRevision, `App source directory ${prefix || '.'}`);
|
|
1422
2088
|
}
|
|
1423
2089
|
|
|
1424
|
-
|
|
2090
|
+
try {
|
|
2091
|
+
process.chdir(canonicalProjectDir);
|
|
2092
|
+
walkCurrent('', projectIdentity);
|
|
2093
|
+
} finally {
|
|
2094
|
+
process.chdir(previousCwd);
|
|
2095
|
+
if (!pullTargetIdentitiesMatch(previousCwdIdentity, capturePullTargetIdentity('.'))) {
|
|
2096
|
+
throw usageError(`Working directory changed while packaging app source: ${previousCwd}`);
|
|
2097
|
+
}
|
|
2098
|
+
}
|
|
1425
2099
|
return files;
|
|
1426
2100
|
}
|
|
1427
2101
|
|
|
@@ -1443,9 +2117,55 @@ function cleanTarPath(name) {
|
|
|
1443
2117
|
return parts.join('/');
|
|
1444
2118
|
}
|
|
1445
2119
|
|
|
2120
|
+
function parsePaxPath(data) {
|
|
2121
|
+
if (data.length === 0 || data.length > 64 * 1024) {
|
|
2122
|
+
throw usageError('Refusing to extract an invalid PAX source archive header.');
|
|
2123
|
+
}
|
|
2124
|
+
let offset = 0;
|
|
2125
|
+
let path = null;
|
|
2126
|
+
while (offset < data.length) {
|
|
2127
|
+
const space = data.indexOf(0x20, offset);
|
|
2128
|
+
if (space <= offset) {
|
|
2129
|
+
throw usageError('Refusing to extract a malformed PAX source archive header.');
|
|
2130
|
+
}
|
|
2131
|
+
const lengthText = data.subarray(offset, space).toString('ascii');
|
|
2132
|
+
if (!/^\d+$/.test(lengthText)) {
|
|
2133
|
+
throw usageError('Refusing to extract a malformed PAX source archive header.');
|
|
2134
|
+
}
|
|
2135
|
+
const recordLength = Number.parseInt(lengthText, 10);
|
|
2136
|
+
const recordEnd = offset + recordLength;
|
|
2137
|
+
if (recordLength <= space - offset + 3 || recordEnd > data.length || data[recordEnd - 1] !== 0x0a) {
|
|
2138
|
+
throw usageError('Refusing to extract a malformed PAX source archive header.');
|
|
2139
|
+
}
|
|
2140
|
+
const record = data.subarray(space + 1, recordEnd - 1);
|
|
2141
|
+
const equals = record.indexOf(0x3d);
|
|
2142
|
+
if (equals <= 0) {
|
|
2143
|
+
throw usageError('Refusing to extract a malformed PAX source archive header.');
|
|
2144
|
+
}
|
|
2145
|
+
const key = record.subarray(0, equals).toString('ascii');
|
|
2146
|
+
if (key === 'path') {
|
|
2147
|
+
try {
|
|
2148
|
+
path = new TextDecoder('utf-8', { fatal: true }).decode(record.subarray(equals + 1));
|
|
2149
|
+
} catch {
|
|
2150
|
+
throw usageError('Refusing to extract a PAX source path with invalid UTF-8.');
|
|
2151
|
+
}
|
|
2152
|
+
if (!path || Buffer.byteLength(path, 'utf-8') > 4096) {
|
|
2153
|
+
throw usageError('Refusing to extract an invalid PAX source path.');
|
|
2154
|
+
}
|
|
2155
|
+
}
|
|
2156
|
+
offset = recordEnd;
|
|
2157
|
+
}
|
|
2158
|
+
if (!path) {
|
|
2159
|
+
throw usageError('Refusing to extract a PAX source header without a path.');
|
|
2160
|
+
}
|
|
2161
|
+
return path;
|
|
2162
|
+
}
|
|
2163
|
+
|
|
1446
2164
|
function extractTarGz(buffer, targetDir) {
|
|
1447
2165
|
const tar = gunzipSync(buffer);
|
|
1448
2166
|
let offset = 0;
|
|
2167
|
+
let pendingPaxPath = null;
|
|
2168
|
+
const extractedPaths = new Map();
|
|
1449
2169
|
while (offset + 512 <= tar.length) {
|
|
1450
2170
|
const header = tar.subarray(offset, offset + 512);
|
|
1451
2171
|
offset += 512;
|
|
@@ -1459,35 +2179,251 @@ function extractTarGz(buffer, targetDir) {
|
|
|
1459
2179
|
const sizeRaw = header.subarray(124, 136).toString('utf-8').replace(/\0.*$/, '').trim();
|
|
1460
2180
|
const size = Number.parseInt(sizeRaw || '0', 8);
|
|
1461
2181
|
const typeFlag = header[156];
|
|
1462
|
-
|
|
2182
|
+
if (!Number.isSafeInteger(size) || size < 0 || offset + size > tar.length) {
|
|
2183
|
+
throw usageError('Refusing to extract a malformed source archive entry size.');
|
|
2184
|
+
}
|
|
2185
|
+
const data = tar.subarray(offset, offset + size);
|
|
2186
|
+
if (typeFlag === 120) {
|
|
2187
|
+
if (pendingPaxPath !== null) {
|
|
2188
|
+
throw usageError('Refusing to extract stacked PAX source archive headers.');
|
|
2189
|
+
}
|
|
2190
|
+
pendingPaxPath = parsePaxPath(data);
|
|
2191
|
+
offset += Math.ceil(size / 512) * 512;
|
|
2192
|
+
continue;
|
|
2193
|
+
}
|
|
2194
|
+
|
|
2195
|
+
const relPath = cleanTarPath(pendingPaxPath || fullName);
|
|
2196
|
+
pendingPaxPath = null;
|
|
2197
|
+
const parts = relPath.split('/');
|
|
2198
|
+
for (let index = 1; index <= parts.length; index += 1) {
|
|
2199
|
+
const pathPrefix = parts.slice(0, index).join('/');
|
|
2200
|
+
const casefoldedPath = pathPrefix.toLocaleLowerCase('en-US');
|
|
2201
|
+
const priorPath = extractedPaths.get(casefoldedPath);
|
|
2202
|
+
if (priorPath && priorPath !== pathPrefix) {
|
|
2203
|
+
throw usageError(
|
|
2204
|
+
`Refusing to extract case-colliding source archive paths: ${priorPath} and ${pathPrefix}`,
|
|
2205
|
+
);
|
|
2206
|
+
}
|
|
2207
|
+
extractedPaths.set(casefoldedPath, pathPrefix);
|
|
2208
|
+
}
|
|
2209
|
+
if (relPath.split('/').some((part) => shouldExcludeSourceEntry(part))) {
|
|
2210
|
+
throw usageError(`Refusing to extract local-only path from source archive: ${relPath}`);
|
|
2211
|
+
}
|
|
1463
2212
|
const outputPath = join(targetDir, relPath);
|
|
1464
2213
|
|
|
1465
2214
|
if (typeFlag === 53) {
|
|
1466
2215
|
mkdirSync(outputPath, { recursive: true });
|
|
1467
|
-
} else {
|
|
2216
|
+
} else if (typeFlag === 0 || typeFlag === 48) {
|
|
1468
2217
|
mkdirSync(dirname(outputPath), { recursive: true });
|
|
1469
|
-
writeFileSync(outputPath,
|
|
2218
|
+
writeFileSync(outputPath, data);
|
|
2219
|
+
} else {
|
|
2220
|
+
throw usageError(`Refusing to extract unsupported source archive entry: ${relPath}`);
|
|
1470
2221
|
}
|
|
1471
2222
|
|
|
1472
2223
|
offset += Math.ceil(size / 512) * 512;
|
|
1473
2224
|
}
|
|
2225
|
+
if (pendingPaxPath !== null) {
|
|
2226
|
+
throw usageError('Refusing to extract a dangling PAX source archive header.');
|
|
2227
|
+
}
|
|
2228
|
+
}
|
|
2229
|
+
|
|
2230
|
+
function collectPullPreservedPaths(targetDir) {
|
|
2231
|
+
const paths = [];
|
|
2232
|
+
|
|
2233
|
+
function walk(dir, prefix) {
|
|
2234
|
+
for (const entry of readdirSync(dir, { withFileTypes: true })) {
|
|
2235
|
+
const relPath = prefix ? `${prefix}/${entry.name}` : entry.name;
|
|
2236
|
+
if (entry.isSymbolicLink() || shouldExcludeSourceEntry(entry.name)) {
|
|
2237
|
+
paths.push(relPath);
|
|
2238
|
+
continue;
|
|
2239
|
+
}
|
|
2240
|
+
if (entry.isDirectory()) {
|
|
2241
|
+
walk(join(dir, entry.name), relPath);
|
|
2242
|
+
}
|
|
2243
|
+
}
|
|
2244
|
+
}
|
|
2245
|
+
|
|
2246
|
+
if (existsSync(targetDir)) {
|
|
2247
|
+
walk(targetDir, '');
|
|
2248
|
+
}
|
|
2249
|
+
return paths;
|
|
2250
|
+
}
|
|
2251
|
+
|
|
2252
|
+
function readPullLockOwner(lockDirectory) {
|
|
2253
|
+
try {
|
|
2254
|
+
return JSON.parse(readFileSync(join(lockDirectory, 'owner'), 'utf-8'));
|
|
2255
|
+
} catch {
|
|
2256
|
+
return null;
|
|
2257
|
+
}
|
|
2258
|
+
}
|
|
2259
|
+
|
|
2260
|
+
function capturePullTargetIdentity(targetDir) {
|
|
2261
|
+
try {
|
|
2262
|
+
const targetStat = lstatSync(targetDir, { bigint: true });
|
|
2263
|
+
if (targetStat.isSymbolicLink() || !targetStat.isDirectory()) {
|
|
2264
|
+
throw usageError(`Refusing to pull app source through an unsafe target: ${targetDir}`);
|
|
2265
|
+
}
|
|
2266
|
+
return { exists: true, dev: targetStat.dev, ino: targetStat.ino };
|
|
2267
|
+
} catch (error) {
|
|
2268
|
+
if (error?.code === 'ENOENT') return { exists: false, dev: null, ino: null };
|
|
2269
|
+
throw error;
|
|
2270
|
+
}
|
|
2271
|
+
}
|
|
2272
|
+
|
|
2273
|
+
function assertPullTargetIdentity(targetDir, expected) {
|
|
2274
|
+
const current = capturePullTargetIdentity(targetDir);
|
|
2275
|
+
if (!pullTargetIdentitiesMatch(current, expected)) {
|
|
2276
|
+
throw usageError(`App source pull target changed while the download was in progress: ${targetDir}`);
|
|
2277
|
+
}
|
|
2278
|
+
}
|
|
2279
|
+
|
|
2280
|
+
function pullTargetIdentitiesMatch(left, right) {
|
|
2281
|
+
return (
|
|
2282
|
+
left.exists === right.exists
|
|
2283
|
+
&& (!left.exists || (left.dev === right.dev && left.ino === right.ino))
|
|
2284
|
+
);
|
|
1474
2285
|
}
|
|
1475
2286
|
|
|
1476
|
-
|
|
2287
|
+
function assertPullParentIdentity(parentDir, canonicalParent, expectedIdentity) {
|
|
2288
|
+
let currentCanonicalParent;
|
|
2289
|
+
try {
|
|
2290
|
+
currentCanonicalParent = realpathSync(parentDir);
|
|
2291
|
+
} catch {
|
|
2292
|
+
throw usageError(`App source pull parent changed while the download was in progress: ${parentDir}`);
|
|
2293
|
+
}
|
|
2294
|
+
if (
|
|
2295
|
+
currentCanonicalParent !== canonicalParent
|
|
2296
|
+
|| !pullTargetIdentitiesMatch(
|
|
2297
|
+
capturePullTargetIdentity(canonicalParent),
|
|
2298
|
+
expectedIdentity,
|
|
2299
|
+
)
|
|
2300
|
+
) {
|
|
2301
|
+
throw usageError(`App source pull parent changed while the download was in progress: ${parentDir}`);
|
|
2302
|
+
}
|
|
2303
|
+
}
|
|
2304
|
+
|
|
2305
|
+
async function withPullTargetLock(targetDir, callback) {
|
|
2306
|
+
const requestedTarget = resolve(targetDir);
|
|
2307
|
+
const parentDir = dirname(requestedTarget);
|
|
2308
|
+
mkdirSync(parentDir, { recursive: true });
|
|
2309
|
+
const canonicalParent = realpathSync(parentDir);
|
|
2310
|
+
const canonicalParentIdentity = capturePullTargetIdentity(canonicalParent);
|
|
2311
|
+
const initialRequestedIdentity = capturePullTargetIdentity(requestedTarget);
|
|
2312
|
+
const canonicalTarget = initialRequestedIdentity.exists
|
|
2313
|
+
? realpathSync(requestedTarget)
|
|
2314
|
+
: join(canonicalParent, basename(requestedTarget));
|
|
2315
|
+
if (
|
|
2316
|
+
initialRequestedIdentity.exists
|
|
2317
|
+
&& !pullTargetIdentitiesMatch(
|
|
2318
|
+
initialRequestedIdentity,
|
|
2319
|
+
capturePullTargetIdentity(canonicalTarget),
|
|
2320
|
+
)
|
|
2321
|
+
) {
|
|
2322
|
+
throw usageError(`App source pull target changed before locking: ${requestedTarget}`);
|
|
2323
|
+
}
|
|
2324
|
+
const lockDirectory = join(canonicalParent, `.${basename(canonicalTarget)}.notis-pull-lock`);
|
|
2325
|
+
const ownerId = `${process.pid}.${randomUUID()}`;
|
|
2326
|
+
const deadline = Date.now() + PULL_LOCK_TIMEOUT_MS;
|
|
2327
|
+
|
|
2328
|
+
for (;;) {
|
|
2329
|
+
try {
|
|
2330
|
+
mkdirSync(lockDirectory, { mode: 0o700 });
|
|
2331
|
+
writeFileSync(
|
|
2332
|
+
join(lockDirectory, 'owner'),
|
|
2333
|
+
JSON.stringify({ id: ownerId, pid: process.pid, at: Date.now() }),
|
|
2334
|
+
{ mode: 0o600 },
|
|
2335
|
+
);
|
|
2336
|
+
break;
|
|
2337
|
+
} catch (error) {
|
|
2338
|
+
if (error?.code !== 'EEXIST') throw error;
|
|
2339
|
+
let lockStat;
|
|
2340
|
+
try {
|
|
2341
|
+
lockStat = lstatSync(lockDirectory);
|
|
2342
|
+
} catch (statError) {
|
|
2343
|
+
if (statError?.code === 'ENOENT') continue;
|
|
2344
|
+
throw statError;
|
|
2345
|
+
}
|
|
2346
|
+
if (lockStat.isSymbolicLink() || !lockStat.isDirectory()) {
|
|
2347
|
+
throw usageError(`Refusing to use unsafe app source pull lock: ${lockDirectory}`);
|
|
2348
|
+
}
|
|
2349
|
+
if (Date.now() >= deadline) {
|
|
2350
|
+
throw usageError(
|
|
2351
|
+
`Timed out waiting to pull app source into ${targetDir}. `
|
|
2352
|
+
+ `If no pull process is running, remove the orphaned lock: ${lockDirectory}`,
|
|
2353
|
+
);
|
|
2354
|
+
}
|
|
2355
|
+
await new Promise((resolvePromise) => setTimeout(resolvePromise, PULL_LOCK_POLL_MS));
|
|
2356
|
+
}
|
|
2357
|
+
}
|
|
2358
|
+
|
|
2359
|
+
try {
|
|
2360
|
+
const assertOwnership = () => {
|
|
2361
|
+
if (readPullLockOwner(lockDirectory)?.id !== ownerId) {
|
|
2362
|
+
throw usageError(`Lost the app source pull lock for ${targetDir}.`);
|
|
2363
|
+
}
|
|
2364
|
+
};
|
|
2365
|
+
const assertParentIdentity = () => assertPullParentIdentity(
|
|
2366
|
+
parentDir,
|
|
2367
|
+
canonicalParent,
|
|
2368
|
+
canonicalParentIdentity,
|
|
2369
|
+
);
|
|
2370
|
+
assertParentIdentity();
|
|
2371
|
+
const lockedTargetIdentity = capturePullTargetIdentity(canonicalTarget);
|
|
2372
|
+
if (!pullTargetIdentitiesMatch(initialRequestedIdentity, lockedTargetIdentity)) {
|
|
2373
|
+
throw usageError(`App source pull target changed before locking: ${requestedTarget}`);
|
|
2374
|
+
}
|
|
2375
|
+
return await callback(
|
|
2376
|
+
assertOwnership,
|
|
2377
|
+
assertParentIdentity,
|
|
2378
|
+
canonicalTarget,
|
|
2379
|
+
lockedTargetIdentity,
|
|
2380
|
+
);
|
|
2381
|
+
} finally {
|
|
2382
|
+
try {
|
|
2383
|
+
if (readPullLockOwner(lockDirectory)?.id === ownerId) {
|
|
2384
|
+
rmSync(lockDirectory, { recursive: true, force: true });
|
|
2385
|
+
}
|
|
2386
|
+
} catch {
|
|
2387
|
+
// A reclaimed lock belongs to its new owner and must not be removed.
|
|
2388
|
+
}
|
|
2389
|
+
}
|
|
2390
|
+
}
|
|
2391
|
+
|
|
2392
|
+
export async function pullAppSource(args) {
|
|
2393
|
+
const targetDir = resolve(args.targetDir);
|
|
2394
|
+
return withPullTargetLock(
|
|
2395
|
+
targetDir,
|
|
2396
|
+
(
|
|
2397
|
+
assertLockOwnership,
|
|
2398
|
+
assertParentIdentity,
|
|
2399
|
+
canonicalTarget,
|
|
2400
|
+
targetIdentity,
|
|
2401
|
+
) => pullAppSourceUnlocked(
|
|
2402
|
+
{ ...args, targetDir: canonicalTarget },
|
|
2403
|
+
assertLockOwnership,
|
|
2404
|
+
assertParentIdentity,
|
|
2405
|
+
targetIdentity,
|
|
2406
|
+
),
|
|
2407
|
+
);
|
|
2408
|
+
}
|
|
2409
|
+
|
|
2410
|
+
async function pullAppSourceUnlocked({
|
|
1477
2411
|
apiBase,
|
|
1478
2412
|
jwt,
|
|
1479
2413
|
appId,
|
|
1480
2414
|
targetDir,
|
|
1481
2415
|
version = 'latest',
|
|
1482
2416
|
force = false,
|
|
1483
|
-
|
|
1484
|
-
|
|
2417
|
+
profileKey = null,
|
|
2418
|
+
}, assertLockOwnership, assertParentIdentity, initialTargetIdentity) {
|
|
2419
|
+
assertLockOwnership();
|
|
2420
|
+
assertParentIdentity();
|
|
2421
|
+
const targetWasNonEmpty = existsSync(targetDir) && readdirSync(targetDir).length > 0;
|
|
2422
|
+
if (targetWasNonEmpty) {
|
|
1485
2423
|
if (!force) {
|
|
1486
2424
|
throw usageError(`Target directory is not empty: ${targetDir}. Pass --force to overwrite it.`);
|
|
1487
2425
|
}
|
|
1488
|
-
rmSync(targetDir, { recursive: true, force: true });
|
|
1489
2426
|
}
|
|
1490
|
-
mkdirSync(targetDir, { recursive: true });
|
|
1491
2427
|
|
|
1492
2428
|
const params = new URLSearchParams({ app_id: appId, version: String(version || 'latest') });
|
|
1493
2429
|
const response = await fetch(`${apiBase.replace(/\/$/, '')}/portal_apps/source?${params.toString()}`, {
|
|
@@ -1501,17 +2437,229 @@ export async function pullAppSource({
|
|
|
1501
2437
|
const contentDisposition = response.headers.get('content-disposition') || '';
|
|
1502
2438
|
const versionMatch = /-v(\d+)\.tar\.gz/i.exec(contentDisposition);
|
|
1503
2439
|
const pulledVersion = versionMatch ? Number.parseInt(versionMatch[1], 10) : null;
|
|
1504
|
-
|
|
1505
|
-
|
|
1506
|
-
String(version
|
|
1507
|
-
|
|
1508
|
-
|
|
1509
|
-
|
|
1510
|
-
|
|
1511
|
-
|
|
1512
|
-
|
|
1513
|
-
|
|
1514
|
-
|
|
2440
|
+
const requestedVersion = String(version || 'latest') === 'latest'
|
|
2441
|
+
? null
|
|
2442
|
+
: Number.parseInt(String(version), 10);
|
|
2443
|
+
if (!Number.isInteger(pulledVersion) || pulledVersion <= 0) {
|
|
2444
|
+
throw usageError('The app source response did not identify a valid positive version.');
|
|
2445
|
+
}
|
|
2446
|
+
if (requestedVersion !== null && (!Number.isInteger(requestedVersion) || requestedVersion <= 0)) {
|
|
2447
|
+
throw usageError(`Invalid app source version: ${version}`);
|
|
2448
|
+
}
|
|
2449
|
+
if (
|
|
2450
|
+
requestedVersion !== null
|
|
2451
|
+
&& pulledVersion !== requestedVersion
|
|
2452
|
+
) {
|
|
2453
|
+
throw usageError(
|
|
2454
|
+
`Requested app source version ${requestedVersion}, but the server returned version ${pulledVersion}.`,
|
|
2455
|
+
);
|
|
2456
|
+
}
|
|
2457
|
+
const linkedVersion = pulledVersion;
|
|
2458
|
+
const archiveBuffer = Buffer.from(await response.arrayBuffer());
|
|
2459
|
+
assertParentIdentity();
|
|
2460
|
+
assertLockOwnership();
|
|
2461
|
+
|
|
2462
|
+
// A failed download or malformed archive must never damage an existing
|
|
2463
|
+
// checkout. Extract into a transaction directory under the pinned target,
|
|
2464
|
+
// then replace
|
|
2465
|
+
// only source-managed entries. Local-only state such as .git, .env files,
|
|
2466
|
+
// dependencies, symlinks, build output, and .notis runtime directories is
|
|
2467
|
+
// moved aside recursively and restored into the new source tree.
|
|
2468
|
+
let cleanupTransaction = true;
|
|
2469
|
+
let cwdPinned = false;
|
|
2470
|
+
let transactionDir = null;
|
|
2471
|
+
let transactionDisplayPath = null;
|
|
2472
|
+
let operationError = null;
|
|
2473
|
+
const previousCwd = process.cwd();
|
|
2474
|
+
const previousCwdIdentity = capturePullTargetIdentity(previousCwd);
|
|
2475
|
+
try {
|
|
2476
|
+
const parentDir = dirname(targetDir);
|
|
2477
|
+
const targetName = basename(targetDir);
|
|
2478
|
+
const parentIdentity = capturePullTargetIdentity(parentDir);
|
|
2479
|
+
assertParentIdentity();
|
|
2480
|
+
process.chdir(parentDir);
|
|
2481
|
+
cwdPinned = true;
|
|
2482
|
+
assertPullTargetIdentity('.', parentIdentity);
|
|
2483
|
+
|
|
2484
|
+
assertParentIdentity();
|
|
2485
|
+
assertPullTargetIdentity(targetName, initialTargetIdentity);
|
|
2486
|
+
if (!initialTargetIdentity.exists) {
|
|
2487
|
+
try {
|
|
2488
|
+
mkdirSync(targetName);
|
|
2489
|
+
} catch (error) {
|
|
2490
|
+
if (error?.code === 'EEXIST') {
|
|
2491
|
+
throw usageError(`App source pull target changed while the download was in progress: ${targetDir}`);
|
|
2492
|
+
}
|
|
2493
|
+
throw error;
|
|
2494
|
+
}
|
|
2495
|
+
}
|
|
2496
|
+
const activeTargetIdentity = capturePullTargetIdentity(targetName);
|
|
2497
|
+
if (
|
|
2498
|
+
initialTargetIdentity.exists
|
|
2499
|
+
&& (
|
|
2500
|
+
activeTargetIdentity.dev !== initialTargetIdentity.dev
|
|
2501
|
+
|| activeTargetIdentity.ino !== initialTargetIdentity.ino
|
|
2502
|
+
)
|
|
2503
|
+
) {
|
|
2504
|
+
throw usageError(`App source pull target changed while the download was in progress: ${targetDir}`);
|
|
2505
|
+
}
|
|
2506
|
+
assertLockOwnership();
|
|
2507
|
+
process.chdir(targetName);
|
|
2508
|
+
assertPullTargetIdentity('.', activeTargetIdentity);
|
|
2509
|
+
const targetRoot = '.';
|
|
2510
|
+
assertLinkedStatePathSafe(targetRoot);
|
|
2511
|
+
const preservedPaths = collectPullPreservedPaths(targetRoot);
|
|
2512
|
+
|
|
2513
|
+
const transactionName = basename(mkdtempSync(join('.', '.notis-app-pull-')));
|
|
2514
|
+
transactionDir = transactionName;
|
|
2515
|
+
transactionDisplayPath = join(targetDir, transactionName);
|
|
2516
|
+
const stageDir = join(transactionDir, 'stage');
|
|
2517
|
+
const backupDir = join(transactionDir, 'backup');
|
|
2518
|
+
const preservedDir = join(transactionDir, 'preserved');
|
|
2519
|
+
mkdirSync(stageDir, { recursive: true });
|
|
2520
|
+
mkdirSync(backupDir, { recursive: true });
|
|
2521
|
+
mkdirSync(preservedDir, { recursive: true });
|
|
2522
|
+
|
|
2523
|
+
extractTarGz(archiveBuffer, stageDir);
|
|
2524
|
+
const stagedEntries = readdirSync(stageDir);
|
|
2525
|
+
if (stagedEntries.length === 0) {
|
|
2526
|
+
throw usageError('The app source archive did not contain any editable source files.');
|
|
2527
|
+
}
|
|
2528
|
+
|
|
2529
|
+
const previousStatePath = join(targetRoot, STATE_FILE);
|
|
2530
|
+
const backupStatePath = join(backupDir, STATE_FILE);
|
|
2531
|
+
const hadPreviousState = existsSync(previousStatePath);
|
|
2532
|
+
if (hadPreviousState) {
|
|
2533
|
+
mkdirSync(dirname(backupStatePath), { recursive: true });
|
|
2534
|
+
cpSync(previousStatePath, backupStatePath);
|
|
2535
|
+
}
|
|
2536
|
+
|
|
2537
|
+
const movedPreserved = [];
|
|
2538
|
+
const movedOriginal = [];
|
|
2539
|
+
const movedStaged = [];
|
|
2540
|
+
const restoredPreserved = [];
|
|
2541
|
+
try {
|
|
2542
|
+
for (const relPath of preservedPaths) {
|
|
2543
|
+
const preservedPath = join(preservedDir, relPath);
|
|
2544
|
+
mkdirSync(dirname(preservedPath), { recursive: true });
|
|
2545
|
+
renameSync(join(targetRoot, relPath), preservedPath);
|
|
2546
|
+
movedPreserved.push(relPath);
|
|
2547
|
+
}
|
|
2548
|
+
assertPullTargetIdentity(targetDir, activeTargetIdentity);
|
|
2549
|
+
assertLockOwnership();
|
|
2550
|
+
const managedTargetEntries = readdirSync(targetRoot)
|
|
2551
|
+
.filter((name) => name !== transactionName);
|
|
2552
|
+
for (const name of managedTargetEntries) {
|
|
2553
|
+
renameSync(join(targetRoot, name), join(backupDir, name));
|
|
2554
|
+
movedOriginal.push(name);
|
|
2555
|
+
}
|
|
2556
|
+
assertPullTargetIdentity(targetDir, activeTargetIdentity);
|
|
2557
|
+
assertLockOwnership();
|
|
2558
|
+
for (const name of stagedEntries) {
|
|
2559
|
+
renameSync(join(stageDir, name), join(targetRoot, name));
|
|
2560
|
+
movedStaged.push(name);
|
|
2561
|
+
}
|
|
2562
|
+
assertPullTargetIdentity(targetDir, activeTargetIdentity);
|
|
2563
|
+
assertLockOwnership();
|
|
2564
|
+
for (const relPath of movedPreserved) {
|
|
2565
|
+
const targetPath = join(targetRoot, relPath);
|
|
2566
|
+
try {
|
|
2567
|
+
lstatSync(targetPath);
|
|
2568
|
+
throw usageError(
|
|
2569
|
+
`Local-only path conflicts with pulled source: ${relPath}. Move it aside and retry.`,
|
|
2570
|
+
);
|
|
2571
|
+
} catch (error) {
|
|
2572
|
+
if (error?.code !== 'ENOENT') throw error;
|
|
2573
|
+
}
|
|
2574
|
+
mkdirSync(dirname(targetPath), { recursive: true });
|
|
2575
|
+
renameSync(join(preservedDir, relPath), targetPath);
|
|
2576
|
+
restoredPreserved.push(relPath);
|
|
2577
|
+
}
|
|
2578
|
+
assertPullTargetIdentity(targetDir, activeTargetIdentity);
|
|
2579
|
+
assertLockOwnership();
|
|
2580
|
+
writeLinkedState(targetRoot, {
|
|
2581
|
+
app_id: appId,
|
|
2582
|
+
...(Number.isFinite(linkedVersion) ? { version: linkedVersion } : {}),
|
|
2583
|
+
linked_at: new Date().toISOString(),
|
|
2584
|
+
}, profileKey);
|
|
2585
|
+
assertPullTargetIdentity(targetDir, activeTargetIdentity);
|
|
2586
|
+
assertLockOwnership();
|
|
2587
|
+
} catch (error) {
|
|
2588
|
+
try {
|
|
2589
|
+
for (const relPath of restoredPreserved.reverse()) {
|
|
2590
|
+
const preservedPath = join(preservedDir, relPath);
|
|
2591
|
+
mkdirSync(dirname(preservedPath), { recursive: true });
|
|
2592
|
+
renameSync(join(targetRoot, relPath), preservedPath);
|
|
2593
|
+
}
|
|
2594
|
+
for (const name of movedStaged.reverse()) {
|
|
2595
|
+
renameSync(join(targetRoot, name), join(stageDir, name));
|
|
2596
|
+
}
|
|
2597
|
+
for (const name of movedOriginal.reverse()) {
|
|
2598
|
+
renameSync(join(backupDir, name), join(targetRoot, name));
|
|
2599
|
+
}
|
|
2600
|
+
for (const relPath of movedPreserved.reverse()) {
|
|
2601
|
+
const targetPath = join(targetRoot, relPath);
|
|
2602
|
+
mkdirSync(dirname(targetPath), { recursive: true });
|
|
2603
|
+
renameSync(join(preservedDir, relPath), targetPath);
|
|
2604
|
+
}
|
|
2605
|
+
if (hadPreviousState) {
|
|
2606
|
+
cpSync(backupStatePath, previousStatePath);
|
|
2607
|
+
} else {
|
|
2608
|
+
rmSync(previousStatePath, { force: true });
|
|
2609
|
+
}
|
|
2610
|
+
} catch (rollbackError) {
|
|
2611
|
+
cleanupTransaction = false;
|
|
2612
|
+
throw usageError(
|
|
2613
|
+
`${error instanceof Error ? error.message : String(error)} `
|
|
2614
|
+
+ `Rollback failed; recovery files were retained at ${transactionDisplayPath}: `
|
|
2615
|
+
+ `${rollbackError instanceof Error ? rollbackError.message : String(rollbackError)}`,
|
|
2616
|
+
);
|
|
2617
|
+
}
|
|
2618
|
+
throw error;
|
|
2619
|
+
}
|
|
2620
|
+
} catch (error) {
|
|
2621
|
+
operationError = error;
|
|
2622
|
+
throw error;
|
|
2623
|
+
} finally {
|
|
2624
|
+
let cleanupError = null;
|
|
2625
|
+
try {
|
|
2626
|
+
if (cleanupTransaction && transactionDir) {
|
|
2627
|
+
rmSync(transactionDir, { recursive: true, force: true });
|
|
2628
|
+
}
|
|
2629
|
+
} catch (error) {
|
|
2630
|
+
cleanupError = error;
|
|
2631
|
+
} finally {
|
|
2632
|
+
if (cwdPinned) {
|
|
2633
|
+
const cwdMatchesTarget = (
|
|
2634
|
+
previousCwdIdentity.exists
|
|
2635
|
+
&& pullTargetIdentitiesMatch(previousCwdIdentity, capturePullTargetIdentity('.'))
|
|
2636
|
+
);
|
|
2637
|
+
if (!cwdMatchesTarget) {
|
|
2638
|
+
try {
|
|
2639
|
+
process.chdir(previousCwd);
|
|
2640
|
+
} catch {
|
|
2641
|
+
// The command is about to return; never trade recovered source for a cwd error.
|
|
2642
|
+
}
|
|
2643
|
+
}
|
|
2644
|
+
}
|
|
2645
|
+
}
|
|
2646
|
+
if (cleanupError) {
|
|
2647
|
+
const cleanupMessage = cleanupError instanceof Error
|
|
2648
|
+
? cleanupError.message
|
|
2649
|
+
: String(cleanupError);
|
|
2650
|
+
if (operationError) {
|
|
2651
|
+
throw usageError(
|
|
2652
|
+
`${operationError instanceof Error ? operationError.message : String(operationError)} `
|
|
2653
|
+
+ `Transaction cleanup also failed; recovery files were retained at `
|
|
2654
|
+
+ `${transactionDisplayPath}: ${cleanupMessage}`,
|
|
2655
|
+
);
|
|
2656
|
+
}
|
|
2657
|
+
throw usageError(
|
|
2658
|
+
`App source was updated, but transaction cleanup failed; recovery files were retained at `
|
|
2659
|
+
+ `${transactionDisplayPath}: ${cleanupMessage}`,
|
|
2660
|
+
);
|
|
2661
|
+
}
|
|
2662
|
+
}
|
|
1515
2663
|
|
|
1516
2664
|
return { projectDir: targetDir, version: pulledVersion || version };
|
|
1517
2665
|
}
|
|
@@ -1692,7 +2840,7 @@ async function updateAppVersion(supabaseUrl, supabaseKey, appId, newVersion, man
|
|
|
1692
2840
|
},
|
|
1693
2841
|
body: JSON.stringify({
|
|
1694
2842
|
manifest: { ...manifest, version: newVersion },
|
|
1695
|
-
|
|
2843
|
+
...appRowFieldsFromManifest(manifest),
|
|
1696
2844
|
updated_at: new Date().toISOString(),
|
|
1697
2845
|
}),
|
|
1698
2846
|
});
|