@notis_ai/cli 0.2.0-beta.19.1 → 0.2.0-beta.196.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (216) hide show
  1. package/README.md +822 -135
  2. package/bin/check-runtime.js +15 -0
  3. package/bin/notis.js +2 -0
  4. package/config/notis_app_boundary_rules.json +50 -0
  5. package/config/notis_app_design_rules.json +135 -0
  6. package/config/portal-route-roots.json +48 -0
  7. package/dist/agent-hooks/notis-agent-hook.mjs +45587 -0
  8. package/dist/base-skills/notis-apps/SKILL.md +93 -0
  9. package/dist/base-skills/notis-apps/references/architecture.md +114 -0
  10. package/dist/base-skills/notis-apps/references/context.md +81 -0
  11. package/dist/base-skills/notis-apps/references/design.md +97 -0
  12. package/dist/base-skills/notis-apps/references/memory.md +58 -0
  13. package/dist/base-skills/notis-apps/references/reading.md +67 -0
  14. package/dist/base-skills/notis-apps/references/release.md +107 -0
  15. package/dist/base-skills/notis-apps/references/sdk.md +116 -0
  16. package/dist/base-skills/notis-apps/references/troubleshooting.md +24 -0
  17. package/dist/base-skills/notis-apps/references/views.md +142 -0
  18. package/dist/base-skills/notis-cli/SKILL.md +154 -0
  19. package/dist/base-skills/notis-cli/references/app-delivery.md +19 -0
  20. package/dist/base-skills/notis-cli/references/intelligence.md +94 -0
  21. package/dist/base-skills/notis-cli/references/native-databases.md +21 -0
  22. package/dist/base-skills/notis-cli/references/tool-examples.md +56 -0
  23. package/dist/base-skills/notis-cli/references/troubleshooting.md +39 -0
  24. package/dist/base-skills/notis-query/SKILL.md +41 -0
  25. package/dist/base-skills/notis-query/references/database-discovery.md +60 -0
  26. package/dist/base-skills/notis-query/references/documents.md +63 -0
  27. package/dist/base-skills/notis-query/references/query.md +76 -0
  28. package/{template/packages/notis-sdk → dist/sdk}/package.json +14 -4
  29. package/dist/sdk/src/agentContext.ts +36 -0
  30. package/dist/sdk/src/bundleModule.ts +79 -0
  31. package/dist/sdk/src/components/Dialog.tsx +46 -0
  32. package/dist/sdk/src/components/DocumentEditor.tsx +106 -0
  33. package/dist/sdk/src/components/DocumentPage.tsx +141 -0
  34. package/dist/sdk/src/components/Markdown.tsx +60 -0
  35. package/dist/sdk/src/components/MarkdownEditor.tsx +121 -0
  36. package/dist/sdk/src/components/MultiSelectActionBar.tsx +285 -0
  37. package/dist/sdk/src/components/MultiSelectCheckbox.tsx +97 -0
  38. package/dist/sdk/src/components/MultiSelectDragOverlay.tsx +39 -0
  39. package/dist/sdk/src/components/NotisCommentBoundary.tsx +172 -0
  40. package/dist/sdk/src/components/NotisSelectionBoundary.tsx +59 -0
  41. package/dist/sdk/src/components/RecordComponents.tsx +33 -0
  42. package/dist/sdk/src/components/RenderChartData.tsx +19 -0
  43. package/dist/sdk/src/components/ShortcutHints.tsx +56 -0
  44. package/dist/sdk/src/components/Skeleton.tsx +24 -0
  45. package/dist/sdk/src/config.ts +239 -0
  46. package/dist/sdk/src/documents.ts +258 -0
  47. package/dist/sdk/src/hooks/useAction.ts +28 -0
  48. package/dist/sdk/src/hooks/useActiveResource.ts +19 -0
  49. package/dist/sdk/src/hooks/useAgentContext.ts +23 -0
  50. package/dist/sdk/src/hooks/useCloudComputer.ts +95 -0
  51. package/dist/sdk/src/hooks/useCloudComputerShell.ts +114 -0
  52. package/dist/sdk/src/hooks/useCollectionInteractions.ts +838 -0
  53. package/dist/sdk/src/hooks/useDatabaseSchema.ts +49 -0
  54. package/dist/sdk/src/hooks/useDatabaseSubscription.ts +76 -0
  55. package/dist/sdk/src/hooks/useDocument.ts +43 -0
  56. package/dist/sdk/src/hooks/useDocuments.ts +84 -0
  57. package/dist/sdk/src/hooks/useHandover.ts +78 -0
  58. package/dist/sdk/src/hooks/useLongPressSelection.ts +79 -0
  59. package/dist/sdk/src/hooks/useMultiSelect.ts +95 -0
  60. package/dist/sdk/src/hooks/useNativeDocumentBody.ts +50 -0
  61. package/dist/sdk/src/hooks/useNativeDocuments.ts +83 -0
  62. package/dist/sdk/src/hooks/useNotis.ts +42 -0
  63. package/dist/sdk/src/hooks/useNotisNavigation.ts +67 -0
  64. package/dist/sdk/src/hooks/useQuery.ts +74 -0
  65. package/dist/sdk/src/hooks/useTool.ts +65 -0
  66. package/dist/sdk/src/hooks/useToolQuery.ts +12 -0
  67. package/dist/sdk/src/hooks/useTopBarSearch.ts +81 -0
  68. package/dist/sdk/src/hooks/useUpsertDocument.ts +95 -0
  69. package/dist/sdk/src/hooks/useViewParams.ts +121 -0
  70. package/dist/sdk/src/hooks/useViewerReads.ts +129 -0
  71. package/dist/sdk/src/index.ts +200 -0
  72. package/dist/sdk/src/interactions/actions.ts +59 -0
  73. package/dist/sdk/src/interactions/shortcuts.tsx +740 -0
  74. package/dist/sdk/src/interactions/visibility.ts +13 -0
  75. package/dist/sdk/src/interactions.ts +45 -0
  76. package/dist/sdk/src/nativeData.ts +39 -0
  77. package/dist/sdk/src/presentation.tsx +82 -0
  78. package/dist/sdk/src/provider.tsx +49 -0
  79. package/dist/sdk/src/queryCache.ts +172 -0
  80. package/dist/sdk/src/queryErrors.ts +51 -0
  81. package/dist/sdk/src/runtime.ts +603 -0
  82. package/dist/sdk/src/space.ts +425 -0
  83. package/dist/sdk/src/styles.css +290 -0
  84. package/dist/sdk/src/tailwind.ts +66 -0
  85. package/dist/sdk/src/vite.ts +83 -0
  86. package/dist/skill-sync/index.js +2143 -0
  87. package/dist/skill-sync/index.js.map +7 -0
  88. package/dist/skill-sync-worker.mjs +3487 -0
  89. package/dist/space-harness/host.js +66 -0
  90. package/dist/view-renderer/dist/host.css +1 -0
  91. package/dist/view-renderer/dist/host.js +1042 -0
  92. package/dist/view-renderer/dist/theme.css +73 -0
  93. package/dist/view-renderer/src/dom.js +180 -0
  94. package/dist/view-renderer/src/errors.js +8 -0
  95. package/dist/view-renderer/src/host.tsx +173 -0
  96. package/dist/view-renderer/src/index.js +4 -0
  97. package/dist/view-renderer/src/pool.js +76 -0
  98. package/dist/view-renderer/src/remote-broker.js +59 -0
  99. package/dist/view-renderer/src/renderer.js +201 -0
  100. package/package.json +25 -8
  101. package/skills/notis-apps/cli.md +406 -0
  102. package/skills/notis-cli/AGENT_INSTRUCTIONS.md +39 -0
  103. package/skills/notis-onboarding/BRIEF.md +133 -0
  104. package/skills/notis-query/cli.md +39 -0
  105. package/src/agent-hook-entry.js +5 -0
  106. package/src/cli.js +294 -25
  107. package/src/command-specs/agents.js +392 -0
  108. package/src/command-specs/apps.js +1476 -203
  109. package/src/command-specs/auth.js +114 -137
  110. package/src/command-specs/diagnostics.js +730 -0
  111. package/src/command-specs/files.js +29 -0
  112. package/src/command-specs/handover.js +374 -0
  113. package/src/command-specs/helpers.js +86 -82
  114. package/src/command-specs/index.js +31 -6
  115. package/src/command-specs/meta.js +150 -18
  116. package/src/command-specs/onboarding.js +290 -0
  117. package/src/command-specs/profile.js +358 -0
  118. package/src/command-specs/skills.js +304 -0
  119. package/src/command-specs/smoke.js +386 -0
  120. package/src/command-specs/spaces.js +572 -0
  121. package/src/command-specs/tools.js +455 -139
  122. package/src/command-specs/views.js +69 -0
  123. package/src/runtime/agent-browser.js +700 -0
  124. package/src/runtime/agent-memory-state.js +126 -0
  125. package/src/runtime/agent-setup.js +383 -0
  126. package/src/runtime/app-boundary-validator.js +407 -0
  127. package/src/runtime/app-changelog.js +79 -0
  128. package/src/runtime/app-data-literals.js +82 -0
  129. package/src/runtime/app-platform.js +2752 -237
  130. package/src/runtime/app-registry-scaffolds.js +367 -0
  131. package/src/runtime/app-test-server.js +293 -0
  132. package/src/runtime/assets/store-screenshot-dark.png +0 -0
  133. package/src/runtime/auth-recovery.js +110 -0
  134. package/src/runtime/base-skills.d.ts +20 -0
  135. package/src/runtime/base-skills.js +167 -0
  136. package/src/runtime/channel.js +133 -0
  137. package/src/runtime/confined-space-build.js +74 -0
  138. package/src/runtime/confined-space-verify.js +49 -0
  139. package/src/runtime/delegated-context.js +68 -0
  140. package/src/runtime/errors.js +1 -0
  141. package/src/runtime/git.js +233 -0
  142. package/src/runtime/login-listener.js +15 -0
  143. package/src/runtime/native-record-files.js +47 -0
  144. package/src/runtime/oauth.js +2622 -0
  145. package/src/runtime/output.js +37 -5
  146. package/src/runtime/ports.js +31 -0
  147. package/src/runtime/profiles.js +906 -55
  148. package/src/runtime/retired-sdk-files.js +20 -0
  149. package/src/runtime/screenshot-framing.js +85 -0
  150. package/src/runtime/skill-sync/cloud-client.ts +121 -0
  151. package/src/runtime/skill-sync/index.ts +905 -0
  152. package/src/runtime/skill-sync/local-scanner.ts +1259 -0
  153. package/src/runtime/skill-sync/native-identity.ts +102 -0
  154. package/src/runtime/skill-sync/native-plan.ts +59 -0
  155. package/src/runtime/skill-sync/symlink-manager.ts +433 -0
  156. package/src/runtime/skill-sync/sync-plan.ts +54 -0
  157. package/src/runtime/skill-sync/types.ts +157 -0
  158. package/src/runtime/skill-sync/write-cloud-skill.ts +50 -0
  159. package/src/runtime/skill-sync-service.js +109 -0
  160. package/src/runtime/source-root.js +32 -0
  161. package/src/runtime/space-collection-scaffold.js +66 -0
  162. package/src/runtime/space-database-structures.js +229 -0
  163. package/src/runtime/space-fixture-broker.js +50 -0
  164. package/src/runtime/space-lock.js +337 -0
  165. package/src/runtime/space-platform.js +105 -0
  166. package/src/runtime/space-source.js +281 -0
  167. package/src/runtime/space-test-server.js +327 -0
  168. package/src/runtime/space-verification.js +89 -0
  169. package/src/runtime/space-verify-browser-worker.js +51 -0
  170. package/src/runtime/space-view-manifest.js +254 -0
  171. package/src/runtime/store-screenshot.js +146 -0
  172. package/src/runtime/sync-skills.d.ts +37 -0
  173. package/src/runtime/sync-skills.js +231 -0
  174. package/src/runtime/telemetry.js +92 -0
  175. package/src/runtime/transport.js +331 -45
  176. package/src/runtime/view-render.js +49 -0
  177. package/src/skill-sync-worker-entry.js +2 -0
  178. package/src/skill-sync-worker.js +50 -0
  179. package/src/space-harness-host.ts +103 -0
  180. package/template/.harness/index.html.tmpl +435 -0
  181. package/template/CHANGELOG.md +5 -0
  182. package/template/app/globals.css +29 -3
  183. package/template/app/layout.tsx +6 -3
  184. package/template/app/page.tsx +49 -42
  185. package/template/components/page-heading.tsx +23 -0
  186. package/template/components/ui/badge.tsx +7 -4
  187. package/template/components/ui/button.tsx +1 -1
  188. package/template/components/ui/card.tsx +24 -11
  189. package/template/components/ui/native-select.tsx +24 -0
  190. package/template/notis.config.ts +24 -6
  191. package/template/package-lock.json +3642 -0
  192. package/template/package.json +19 -16
  193. package/template/postcss.config.mjs +1 -1
  194. package/template/tailwind.config.ts +1 -6
  195. package/template/tsconfig.json +1 -0
  196. package/src/command-specs/db.js +0 -163
  197. package/src/runtime/app-preview-server.js +0 -312
  198. package/template/packages/notis-sdk/src/config.ts +0 -48
  199. package/template/packages/notis-sdk/src/helpers.ts +0 -131
  200. package/template/packages/notis-sdk/src/hooks/useAppState.ts +0 -50
  201. package/template/packages/notis-sdk/src/hooks/useCollectionItem.ts +0 -58
  202. package/template/packages/notis-sdk/src/hooks/useDatabase.ts +0 -87
  203. package/template/packages/notis-sdk/src/hooks/useDocument.ts +0 -61
  204. package/template/packages/notis-sdk/src/hooks/useNotis.ts +0 -31
  205. package/template/packages/notis-sdk/src/hooks/useNotisNavigation.ts +0 -49
  206. package/template/packages/notis-sdk/src/hooks/useTool.ts +0 -49
  207. package/template/packages/notis-sdk/src/hooks/useUpsertDocument.ts +0 -57
  208. package/template/packages/notis-sdk/src/index.ts +0 -47
  209. package/template/packages/notis-sdk/src/provider.tsx +0 -44
  210. package/template/packages/notis-sdk/src/runtime.ts +0 -159
  211. package/template/packages/notis-sdk/src/styles.css +0 -123
  212. package/template/packages/notis-sdk/src/vite.ts +0 -54
  213. package/template/packages/notis-sdk/tsconfig.json +0 -15
  214. /package/{template/packages/notis-sdk → dist/sdk}/src/hooks/useBackend.ts +0 -0
  215. /package/{template/packages/notis-sdk → dist/sdk}/src/hooks/useTools.ts +0 -0
  216. /package/{template/packages/notis-sdk → dist/sdk}/src/ui.ts +0 -0
@@ -0,0 +1,233 @@
1
+ /**
2
+ * Local git inspection for `notis handover`.
3
+ *
4
+ * The CLI has no other reason to know about git, so this stays deliberately
5
+ * small: read where we are, push the branch, and report what the remote is.
6
+ * Everything else about the repository is the cloud workspace's problem.
7
+ *
8
+ * Every call is `spawnSync` on the real `git` binary rather than a library.
9
+ * The user's credentials, hooks, SSH agent and includeIf config are what make a
10
+ * push succeed on their machine, and only their own git honors all of them.
11
+ */
12
+
13
+ import { spawnSync } from 'node:child_process';
14
+ import { lstatSync, readFileSync } from 'node:fs';
15
+ import { basename, relative, resolve, sep } from 'node:path';
16
+ import { CliError, EXIT_CODES } from './errors.js';
17
+
18
+ const GIT_TIMEOUT_MS = 120_000;
19
+ const MAX_SECRET_SCAN_BYTES = 1_000_000;
20
+ const SAFE_ENV_TEMPLATES = /^\.env\.(?:example|sample|template)$/i;
21
+ const SENSITIVE_PATH = /^(?:\.env(?:\..+)?|\.envrc|\.git-credentials|\.npmrc|\.pypirc|\.netrc|credentials(?:\..+)?|secrets?(?:\..+)?|id_(?:rsa|dsa|ecdsa|ed25519)(?:\.pub)?|.*\.(?:pem|key|p12|pfx|jks))$/i;
22
+ const SENSITIVE_CONTENT = [
23
+ /-----BEGIN (?:RSA |EC |DSA |OPENSSH )?PRIVATE KEY-----/,
24
+ /\b[a-z][a-z0-9+.-]*:\/\/[^/\s:@]+:[^/\s@]+@/i,
25
+ /\bAKIA[0-9A-Z]{16}\b/,
26
+ /\bgh[pousr]_[A-Za-z0-9]{36,}\b/,
27
+ /\bsk-[A-Za-z0-9_-]{20,}\b/,
28
+ ];
29
+
30
+ export function runGit(
31
+ args,
32
+ { cwd = process.cwd(), timeoutMs = GIT_TIMEOUT_MS, trimOutput = true } = {},
33
+ ) {
34
+ const result = spawnSync('git', args, {
35
+ cwd,
36
+ encoding: 'utf8',
37
+ timeout: timeoutMs,
38
+ env: {
39
+ ...process.env,
40
+ // A push that stops to ask for a password would hang a non-interactive
41
+ // agent run forever. Fail fast instead and let the hint explain.
42
+ GIT_TERMINAL_PROMPT: '0',
43
+ },
44
+ });
45
+ if (result.error && result.error.code === 'ENOENT') {
46
+ throw new CliError({
47
+ code: 'git_not_found',
48
+ message: 'git is not installed or not on PATH.',
49
+ exitCode: EXIT_CODES.usage,
50
+ });
51
+ }
52
+ return {
53
+ exitCode: result.status ?? 1,
54
+ stdout: trimOutput ? (result.stdout || '').trim() : (result.stdout || ''),
55
+ stderr: trimOutput ? (result.stderr || '').trim() : (result.stderr || ''),
56
+ };
57
+ }
58
+
59
+ function gitOrNull(args, options) {
60
+ const result = runGit(args, options);
61
+ return result.exitCode === 0 ? result.stdout : null;
62
+ }
63
+
64
+ function nulSeparatedGitPaths(args, repository) {
65
+ const result = runGit([...args, '-z'], { cwd: repository.toplevel, trimOutput: false });
66
+ if (result.exitCode !== 0 || !result.stdout) return [];
67
+ return result.stdout.split('\0').filter(Boolean);
68
+ }
69
+
70
+ /** Exact files `git add -A` would include, expanding untracked directories. */
71
+ function filesToAutoCommit(repository) {
72
+ return [...new Set([
73
+ ...nulSeparatedGitPaths(['diff', '--name-only'], repository),
74
+ ...nulSeparatedGitPaths(['diff', '--cached', '--name-only'], repository),
75
+ ...nulSeparatedGitPaths(['ls-files', '--others', '--exclude-standard'], repository),
76
+ ])];
77
+ }
78
+
79
+ /** Refuse files that are unsafe to publish automatically without user review. */
80
+ export function sensitiveAutoCommitFiles(repository) {
81
+ const root = resolve(repository.toplevel);
82
+ const sensitive = [];
83
+ for (const path of filesToAutoCommit(repository)) {
84
+ const absolute = resolve(root, path);
85
+ const insideRoot = relative(root, absolute);
86
+ if (insideRoot === '..' || insideRoot.startsWith(`..${sep}`) || insideRoot === '') continue;
87
+ const name = basename(path);
88
+ if (SENSITIVE_PATH.test(name) && !SAFE_ENV_TEMPLATES.test(name)) {
89
+ sensitive.push(path);
90
+ continue;
91
+ }
92
+ try {
93
+ const stat = lstatSync(absolute);
94
+ if (!stat.isFile() || stat.size > MAX_SECRET_SCAN_BYTES) continue;
95
+ const content = readFileSync(absolute, 'utf8');
96
+ if (SENSITIVE_CONTENT.some((pattern) => pattern.test(content))) sensitive.push(path);
97
+ } catch {
98
+ // Deleted files and paths racing with an editor have no content to leak.
99
+ }
100
+ }
101
+ return sensitive;
102
+ }
103
+
104
+ /**
105
+ * Parse an origin URL into {owner, repo}. Handles the three forms git remotes
106
+ * actually take: scp-style ssh, ssh:// and https://.
107
+ */
108
+ export function parseRemoteUrl(url) {
109
+ if (typeof url !== 'string' || !url) {
110
+ return null;
111
+ }
112
+ const trimmed = url.trim().replace(/\.git$/, '');
113
+ const scp = trimmed.match(/^[^@]+@([^:]+):(.+)$/);
114
+ const path = scp ? scp[2] : trimmed.replace(/^[a-z+]+:\/\/(?:[^@/]+@)?[^/]+\//i, '');
115
+ const host = scp ? scp[1] : (trimmed.match(/^[a-z+]+:\/\/(?:[^@/]+@)?([^/]+)/i) || [])[1];
116
+ const segments = path.split('/').filter(Boolean);
117
+ if (segments.length < 2) {
118
+ return null;
119
+ }
120
+ return {
121
+ host: host || null,
122
+ owner: segments[segments.length - 2],
123
+ repo: segments[segments.length - 1],
124
+ };
125
+ }
126
+
127
+ /** Everything `handover start` needs to know about where it is running. */
128
+ export function inspectRepository(cwd = process.cwd()) {
129
+ const toplevel = gitOrNull(['rev-parse', '--show-toplevel'], { cwd });
130
+ if (!toplevel) {
131
+ throw new CliError({
132
+ code: 'not_a_git_repository',
133
+ message: 'Hand-over runs from inside a git repository.',
134
+ exitCode: EXIT_CODES.usage,
135
+ hints: [{ message: 'cd into the repository you want Notis to work on, then run the command again.' }],
136
+ });
137
+ }
138
+
139
+ const branch = gitOrNull(['branch', '--show-current'], { cwd: toplevel });
140
+ if (!branch) {
141
+ throw new CliError({
142
+ code: 'detached_head',
143
+ message: 'HEAD is detached, so there is no branch to hand over.',
144
+ exitCode: EXIT_CODES.usage,
145
+ hints: [{ message: 'Check out a branch first: git switch -c my-feature' }],
146
+ });
147
+ }
148
+
149
+ const remoteUrl = gitOrNull(['remote', 'get-url', 'origin'], { cwd: toplevel });
150
+ if (!remoteUrl) {
151
+ throw new CliError({
152
+ code: 'no_origin_remote',
153
+ message: 'This repository has no "origin" remote, so the cloud agent cannot fetch the branch.',
154
+ exitCode: EXIT_CODES.usage,
155
+ hints: [{ message: 'Add one: git remote add origin <url>' }],
156
+ });
157
+ }
158
+
159
+ const status = runGit(['status', '--porcelain'], { cwd: toplevel });
160
+ const dirtyFiles = status.stdout
161
+ ? status.stdout.split('\n').map((line) => line.slice(3).trim()).filter(Boolean)
162
+ : [];
163
+
164
+ return {
165
+ toplevel,
166
+ branch,
167
+ remoteUrl,
168
+ remote: parseRemoteUrl(remoteUrl),
169
+ dirtyFiles,
170
+ head: gitOrNull(['rev-parse', 'HEAD'], { cwd: toplevel }),
171
+ upstream: gitOrNull(['rev-parse', '--abbrev-ref', '--symbolic-full-name', '@{upstream}'], {
172
+ cwd: toplevel,
173
+ }),
174
+ };
175
+ }
176
+
177
+ /** Commit everything in the working tree, including untracked files. */
178
+ export function commitWorkingTree(repository, message) {
179
+ const sensitiveFiles = sensitiveAutoCommitFiles(repository);
180
+ if (sensitiveFiles.length) {
181
+ throw new CliError({
182
+ code: 'sensitive_working_tree',
183
+ message: 'Refusing to publish files that may contain credentials or private keys.',
184
+ exitCode: EXIT_CODES.conflict,
185
+ details: { sensitive_files: sensitiveFiles.slice(0, 20) },
186
+ hints: [
187
+ { message: 'Review, remove, or ignore these files before handing over.' },
188
+ { message: 'To publish them deliberately, commit and push them yourself first.' },
189
+ ],
190
+ });
191
+ }
192
+ const add = runGit(['add', '-A'], { cwd: repository.toplevel });
193
+ if (add.exitCode !== 0) {
194
+ throw new CliError({
195
+ code: 'git_add_failed',
196
+ message: `Could not stage the working tree: ${add.stderr || add.stdout}`,
197
+ exitCode: EXIT_CODES.unexpected,
198
+ });
199
+ }
200
+ const commit = runGit(['commit', '-m', message], { cwd: repository.toplevel });
201
+ if (commit.exitCode !== 0) {
202
+ throw new CliError({
203
+ code: 'git_commit_failed',
204
+ message: `Could not commit the working tree: ${commit.stderr || commit.stdout}`,
205
+ exitCode: EXIT_CODES.unexpected,
206
+ hints: [{ message: 'Commit the changes yourself, then run the hand-over again.' }],
207
+ });
208
+ }
209
+ return gitOrNull(['rev-parse', 'HEAD'], { cwd: repository.toplevel });
210
+ }
211
+
212
+ /**
213
+ * Push the branch to origin. The cloud workspace fetches from origin, so an
214
+ * unpushed commit simply does not exist as far as the hand-over is concerned.
215
+ */
216
+ export function pushBranch(repository, branch) {
217
+ const args = repository.upstream
218
+ ? ['push', 'origin', branch]
219
+ : ['push', '--set-upstream', 'origin', branch];
220
+ const result = runGit(args, { cwd: repository.toplevel });
221
+ if (result.exitCode !== 0) {
222
+ throw new CliError({
223
+ code: 'git_push_failed',
224
+ message: `Could not push ${branch} to origin: ${result.stderr || result.stdout}`,
225
+ exitCode: EXIT_CODES.conflict,
226
+ hints: [
227
+ { message: 'The cloud agent works from origin, so the branch has to be pushed first.' },
228
+ { message: 'If the remote moved ahead, reconcile locally (git pull --rebase) and retry.' },
229
+ ],
230
+ });
231
+ }
232
+ return true;
233
+ }
@@ -0,0 +1,15 @@
1
+ #!/usr/bin/env node
2
+ // Entry point for the detached process that owns an OAuth loopback callback.
3
+ //
4
+ // Spawned by `notis login` / `notis start` whenever the calling process cannot
5
+ // wait for the browser itself — an agent reads a command's output only after it
6
+ // exits, so the command has to return the authorization URL while something
7
+ // else keeps listening. Never run this by hand: it takes a single argument, the
8
+ // path to a short-lived payload file the parent wrote and this process deletes.
9
+ import { runDetachedLoginListener } from './oauth.js';
10
+
11
+ process.exitCode = await runDetachedLoginListener(
12
+ process.argv[2],
13
+ undefined,
14
+ process.argv[3],
15
+ );
@@ -0,0 +1,47 @@
1
+ import { open } from 'node:fs/promises';
2
+ import { createHash } from 'node:crypto';
3
+ import { basename, extname } from 'node:path';
4
+ import { httpRequest } from './transport.js';
5
+ import { usageError } from './errors.js';
6
+
7
+ const MAX_BYTES = 8 * 1024 * 1024;
8
+ const UUID = /^[a-f0-9]{8}(?:-[a-f0-9]{4}){3}-[a-f0-9]{12}$/i;
9
+ const MIME = { '.html': 'text/html', '.htm': 'text/html', '.txt': 'text/plain', '.md': 'text/markdown',
10
+ '.json': 'application/json', '.pdf': 'application/pdf', '.png': 'image/png', '.jpg': 'image/jpeg', '.jpeg': 'image/jpeg', '.webp': 'image/webp' };
11
+
12
+ /** Reads a bounded local file, never interprets it and never attaches it to a row. */
13
+ export async function uploadNativeRecordFileWithCliRuntime({ runtime, target, recordKey, schemaRevision, requestId,
14
+ filePath, name = basename(filePath || ''), contentType = MIME[extname(filePath || '').toLowerCase()] || 'application/octet-stream',
15
+ signal, request = httpRequest }) {
16
+ if (!UUID.test(recordKey || '') || !UUID.test(requestId || '') || !Number.isSafeInteger(schemaRevision) || schemaRevision < 0
17
+ || !target || typeof target !== 'object' || Array.isArray(target) || !filePath) {
18
+ throw usageError('Choose a local file, native target, record key, schema revision and stable request ID.');
19
+ }
20
+ const handle = await open(filePath, 'r');
21
+ let data;
22
+ try {
23
+ const stat = await handle.stat();
24
+ if (!stat.isFile() || stat.size < 1 || stat.size > MAX_BYTES) throw usageError('Choose a regular file up to 8 MB.');
25
+ const buffer = Buffer.alloc(MAX_BYTES + 1);
26
+ let length = 0;
27
+ while (length < buffer.length) {
28
+ const read = await handle.read(buffer, length, buffer.length - length, length);
29
+ if (!read.bytesRead) break;
30
+ length += read.bytesRead;
31
+ }
32
+ if (!length || length > MAX_BYTES) throw usageError('Choose a file up to 8 MB.');
33
+ data = buffer.subarray(0, length);
34
+ } finally { await handle.close(); }
35
+ const digest = createHash('sha256').update(data).digest('hex');
36
+ const body = { protocol: 1, target, record_key: recordKey, schema_revision: schemaRevision, request_id: requestId,
37
+ name, content_type: contentType, base64: data.toString('base64') };
38
+ // No automatic retry: repeating this command with the same request ID is the
39
+ // exact immutable intent; a mismatched request fails instead of overwriting.
40
+ const result = (await request({ runtime, method: 'POST', path: '/portal_native_files/upload', spacesProtocol: 1, body, signal })).payload;
41
+ if (result?.protocol !== 1 || result.record_key !== recordKey || result.request_id !== requestId
42
+ || result.sha256 !== digest || result.size_bytes !== data.length || result.file?.name !== name
43
+ || typeof result.file?.url !== 'string' || !/^https?:\/\//.test(result.file.url) || result.url !== result.file.url) {
44
+ throw usageError('The file upload is not confirmed. Repeat the same request ID and file bytes.');
45
+ }
46
+ return result;
47
+ }