@north-light/api 0.3.458 → 0.3.460
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/dist/api/contract.d.ts +1 -1
- package/dist/api/contract.js +1 -1
- package/dist/api/dto/profiles.d.ts +3 -1
- package/dist/core/config.js +4 -4
- package/dist/core/io.d.ts +12 -2
- package/dist/core/io.js +38 -6
- package/dist/core/layout.d.ts +2 -2
- package/dist/core/layout.js +2 -2
- package/dist/core/profiles/manifest.d.ts +1 -1
- package/dist/core/profiles/manifest.js +4 -4
- package/dist/core/render.d.ts +1 -1
- package/dist/core/render.js +4 -0
- package/dist/core/scoped-state/migrate.js +1 -1
- package/dist/core/scoped-state/profiles.js +1 -1
- package/dist/core/scoped-state/settings.js +4 -1
- package/package.json +1 -1
package/dist/api/contract.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/** The contract level this build serves and requires. Written by `scripts/contract.mjs release`; never edit by hand. */
|
|
2
|
-
export declare const CONTRACT_LEVEL = "1.
|
|
2
|
+
export declare const CONTRACT_LEVEL = "1.5";
|
|
3
3
|
/** Request header naming the one contract level the caller needs, e.g. `1.3`. Optional: absent means no check. */
|
|
4
4
|
export declare const REQUIRED_CONTRACT_HEADER = "Crouter-Required-Contract";
|
|
5
5
|
/** Response header listing the contract levels the server serves, comma-separated, one per major, e.g. `1.3`. */
|
package/dist/api/contract.js
CHANGED
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
//
|
|
13
13
|
// PURITY (spec §3.1): Node built-ins + `src/api/*` only.
|
|
14
14
|
/** The contract level this build serves and requires. Written by `scripts/contract.mjs release`; never edit by hand. */
|
|
15
|
-
export const CONTRACT_LEVEL = '1.
|
|
15
|
+
export const CONTRACT_LEVEL = '1.5';
|
|
16
16
|
/** Request header naming the one contract level the caller needs, e.g. `1.3`. Optional: absent means no check. */
|
|
17
17
|
export const REQUIRED_CONTRACT_HEADER = 'Crouter-Required-Contract';
|
|
18
18
|
/** Response header listing the contract levels the server serves, comma-separated, one per major, e.g. `1.3`. */
|
|
@@ -112,6 +112,8 @@ export interface ProfileDTO {
|
|
|
112
112
|
created_at: string;
|
|
113
113
|
/** ISO timestamp of the most recent selection, or null if never selected. */
|
|
114
114
|
last_used_at: string | null;
|
|
115
|
-
/** Absolute path of the profile's directory
|
|
115
|
+
/** Absolute path of the profile's directory, `<app space>/profiles/<id>`: its settings root. The profile's documents live in the canvas, not here. */
|
|
116
|
+
profile_path: string;
|
|
117
|
+
/** @deprecated The same path as `profile_path`; kept for clients written before it existed. Removed in the next breaking release. */
|
|
116
118
|
memory_path: string;
|
|
117
119
|
}
|
package/dist/core/config.js
CHANGED
|
@@ -9,7 +9,7 @@ import { emitEvent } from './events/emit.js';
|
|
|
9
9
|
import { atomicWriteJson, readJsonIfExists, ensureDir } from './fs-utils.js';
|
|
10
10
|
import { scopeRoot, requireScopeRoot, findProjectScopeRoots, userScopeRoot } from './scope.js';
|
|
11
11
|
import { listInstalledPluginsInRoot } from './installed-plugins.js';
|
|
12
|
-
import {
|
|
12
|
+
import { profileDir, listProfiles } from './profiles/manifest.js';
|
|
13
13
|
import { canonicalRoot, projectStore, userStore } from './scoped-state/paths.js';
|
|
14
14
|
import { convertLegacyScopedStore, initializeFreshScopedStore } from './scoped-state/migrate.js';
|
|
15
15
|
import { mutateRawSettings, readBookkeeping, readRawSettings, readRawSettingsWithRevision, replaceRawSettings, writeBookkeeping, writeRawSettings } from './scoped-state/settings.js';
|
|
@@ -47,7 +47,7 @@ function settingsOwnerForRoot(root) {
|
|
|
47
47
|
return { store: projectStore(canonical), key: 'project', kind: 'project', root: canonical };
|
|
48
48
|
}
|
|
49
49
|
for (const profile of listProfiles()) {
|
|
50
|
-
const profilePath = canonicalRoot(
|
|
50
|
+
const profilePath = canonicalRoot(profileDir(profile.profileId));
|
|
51
51
|
if (canonical === profilePath)
|
|
52
52
|
return { store: userStore(userRoot), key: `profile:${profile.profileId}`, kind: 'profile', root: profilePath, profileId: profile.profileId };
|
|
53
53
|
}
|
|
@@ -129,7 +129,7 @@ function rawSettingsOwnerForTarget(target) {
|
|
|
129
129
|
if (!listProfiles().some((profile) => profile.profileId === target.profileId)) {
|
|
130
130
|
throw notFound(`profile not found: ${target.profileId}`, { received: target.profileId, next: 'Run `crtr profile list` to see available profiles.' });
|
|
131
131
|
}
|
|
132
|
-
return settingsOwnerForRoot(
|
|
132
|
+
return settingsOwnerForRoot(profileDir(target.profileId));
|
|
133
133
|
}
|
|
134
134
|
export function readRawSettingsForTarget(target) {
|
|
135
135
|
return readRawSettingsWithRevision(rawSettingsOwnerForTarget(target));
|
|
@@ -877,7 +877,7 @@ function readRawProfileConfig(profileId) {
|
|
|
877
877
|
if (profileId === null || profileId === '')
|
|
878
878
|
return null;
|
|
879
879
|
try {
|
|
880
|
-
return readRawAtRoot(
|
|
880
|
+
return readRawAtRoot(profileDir(profileId));
|
|
881
881
|
}
|
|
882
882
|
catch {
|
|
883
883
|
return null;
|
package/dist/core/io.d.ts
CHANGED
|
@@ -59,6 +59,9 @@ interface ErrorPayloadBase {
|
|
|
59
59
|
http_status?: number;
|
|
60
60
|
/** Seconds the provider asked the caller to wait before retrying (a rate limit). */
|
|
61
61
|
retry_after_s?: number;
|
|
62
|
+
/** The structured details the runtime attached to a refusal the caller must act on (a `machine_required`
|
|
63
|
+
* answer's `candidates`). Rendered below the message and kept in `--json`. */
|
|
64
|
+
details?: Record<string, unknown>;
|
|
62
65
|
next: string;
|
|
63
66
|
}
|
|
64
67
|
export type ErrorPayload = ErrorPayloadBase & ({
|
|
@@ -116,9 +119,16 @@ export declare function diag(message: string): void;
|
|
|
116
119
|
* under the daemon's coarse `provider_*` code and the provider's HTTP
|
|
117
120
|
* status. The provider's code, `next`, `field` and `Retry-After` reach the
|
|
118
121
|
* caller unchanged; the status→code map would flatten them to `conflict`.
|
|
119
|
-
* 4.
|
|
122
|
+
* 4. A refusal the runtime itself rendered for `provider.call` (`options.providerCall`):
|
|
123
|
+
* its envelope code came from the runtime's own table (`machine_required`,
|
|
124
|
+
* `device_offline`, …), not from the HTTP status, so it is kept together with
|
|
125
|
+
* its `details`. The caller of `provider.call` is the only one who knows the
|
|
126
|
+
* envelope is that endpoint's, which is why this is an option and not inferred.
|
|
127
|
+
* 5. Otherwise map status→code (`STATUS_TO_CLI_CODE`) with the server message
|
|
120
128
|
* and a caller-supplied-or-default `next`. */
|
|
121
|
-
export declare function apiErrorToCliError(err: ApiError, next?: string
|
|
129
|
+
export declare function apiErrorToCliError(err: ApiError, next?: string, options?: {
|
|
130
|
+
providerCall?: boolean;
|
|
131
|
+
}): CrtrError;
|
|
122
132
|
/** Terminal error handler. Command-level failures (bad input, not-found,
|
|
123
133
|
* ambiguous) surface as the JSON response on stdout so the caller parses one
|
|
124
134
|
* contract. A daemon `ApiError` is translated to that same clean contract
|
package/dist/core/io.js
CHANGED
|
@@ -190,6 +190,9 @@ export function diag(message) {
|
|
|
190
190
|
process.stderr.write(message + '\n');
|
|
191
191
|
}
|
|
192
192
|
// errors
|
|
193
|
+
function isRecord(value) {
|
|
194
|
+
return value !== null && typeof value === 'object' && !Array.isArray(value);
|
|
195
|
+
}
|
|
193
196
|
function payloadOf(e) {
|
|
194
197
|
if (e instanceof InputError)
|
|
195
198
|
return e.payload;
|
|
@@ -210,6 +213,7 @@ function payloadOf(e) {
|
|
|
210
213
|
field: d.field,
|
|
211
214
|
...(typeof d.http_status === 'number' ? { http_status: d.http_status } : {}),
|
|
212
215
|
...(typeof d.retry_after_s === 'number' ? { retry_after_s: d.retry_after_s } : {}),
|
|
216
|
+
...(isRecord(d.details) ? { details: d.details } : {}),
|
|
213
217
|
};
|
|
214
218
|
return result === undefined
|
|
215
219
|
? { ...base, next }
|
|
@@ -280,9 +284,14 @@ const STATUS_TO_CLI_CODE = {
|
|
|
280
284
|
* under the daemon's coarse `provider_*` code and the provider's HTTP
|
|
281
285
|
* status. The provider's code, `next`, `field` and `Retry-After` reach the
|
|
282
286
|
* caller unchanged; the status→code map would flatten them to `conflict`.
|
|
283
|
-
* 4.
|
|
287
|
+
* 4. A refusal the runtime itself rendered for `provider.call` (`options.providerCall`):
|
|
288
|
+
* its envelope code came from the runtime's own table (`machine_required`,
|
|
289
|
+
* `device_offline`, …), not from the HTTP status, so it is kept together with
|
|
290
|
+
* its `details`. The caller of `provider.call` is the only one who knows the
|
|
291
|
+
* envelope is that endpoint's, which is why this is an option and not inferred.
|
|
292
|
+
* 5. Otherwise map status→code (`STATUS_TO_CLI_CODE`) with the server message
|
|
284
293
|
* and a caller-supplied-or-default `next`. */
|
|
285
|
-
export function apiErrorToCliError(err, next) {
|
|
294
|
+
export function apiErrorToCliError(err, next, options = {}) {
|
|
286
295
|
// A handover is NOT a down daemon: the successor is already up (the client
|
|
287
296
|
// waited for its /healthz), so telling anyone to start one is wrong — the
|
|
288
297
|
// only thing left is to re-issue the call.
|
|
@@ -304,12 +313,15 @@ export function apiErrorToCliError(err, next) {
|
|
|
304
313
|
// A responding daemon can relay an unavailable provider or storage service.
|
|
305
314
|
// HTTP 503 alone does not mean that crtrd is down.
|
|
306
315
|
const d = err.details;
|
|
307
|
-
const provider = providerSupplied(err, d);
|
|
308
|
-
|
|
316
|
+
const provider = providerSupplied(err, d, options.providerCall === true);
|
|
317
|
+
// A `device_timeout` the runtime rendered for `provider.call` (504) is the same operational class as a 503.
|
|
318
|
+
if (err.status === 503 || (options.providerCall === true && err.status === 504 && provider.code !== undefined)) {
|
|
309
319
|
const code = provider.code ?? err.code;
|
|
310
320
|
const details = {
|
|
311
321
|
next: provider.next ?? next ?? 'Retry the command; if it persists, inspect the unavailable service named in the error.',
|
|
312
322
|
};
|
|
323
|
+
if (provider.details !== undefined)
|
|
324
|
+
details['details'] = provider.details;
|
|
313
325
|
if (provider.code !== undefined) {
|
|
314
326
|
if (provider.field !== undefined)
|
|
315
327
|
details['field'] = provider.field;
|
|
@@ -333,6 +345,7 @@ export function apiErrorToCliError(err, next) {
|
|
|
333
345
|
...(provider.field !== undefined ? { field: provider.field } : {}),
|
|
334
346
|
...(provider.received !== undefined ? { received: provider.received } : {}),
|
|
335
347
|
...(provider.retryAfterS !== undefined ? { retry_after_s: provider.retryAfterS } : {}),
|
|
348
|
+
...(provider.details !== undefined ? { details: provider.details } : {}),
|
|
336
349
|
message: violations.length > 0 ? `${err.message}: ${violations.join('; ')}` : err.message.trim() !== '' ? err.message : `request refused (HTTP ${err.status}, ${err.code}) with no message`,
|
|
337
350
|
next: provider.next ??
|
|
338
351
|
next ??
|
|
@@ -357,13 +370,32 @@ export function apiErrorToCliError(err, next) {
|
|
|
357
370
|
* did not originate at a provider, so non-provider status→code mapping is
|
|
358
371
|
* untouched. A code that is not a snake_case identifier, or that came with no
|
|
359
372
|
* provider HTTP status, is ignored. */
|
|
360
|
-
function providerSupplied(err, d) {
|
|
373
|
+
function providerSupplied(err, d, providerCall) {
|
|
374
|
+
const snake = (code) => typeof code === 'string' && /^[a-z][a-z0-9_]*$/.test(code);
|
|
375
|
+
// `provider.call` renders every refusal as a runtime envelope (`origin` set) whose `code` the runtime chose from its own
|
|
376
|
+
// table, so the code stands and `details` is the runtime's structured answer (not a provider's body). A code-less
|
|
377
|
+
// transport failure the client synthesized has no envelope and stays on the status map.
|
|
378
|
+
if (providerCall && err.origin !== undefined && err.origin !== 'provider' && snake(err.code)) {
|
|
379
|
+
const out = { code: err.code };
|
|
380
|
+
if (err.status === 503 || err.status === 504) {
|
|
381
|
+
out.next = err.retryable === true
|
|
382
|
+
? 'Retry the command; if it persists, the machine or service named in the error is unavailable.'
|
|
383
|
+
: 'Read the error before retrying: the action may already have run.';
|
|
384
|
+
}
|
|
385
|
+
if (isRecord(err.details))
|
|
386
|
+
out.details = err.details;
|
|
387
|
+
if (err.param !== undefined)
|
|
388
|
+
out.field = err.param;
|
|
389
|
+
if (typeof err.retryAfterS === 'number')
|
|
390
|
+
out.retryAfterS = err.retryAfterS;
|
|
391
|
+
return out;
|
|
392
|
+
}
|
|
361
393
|
if (err.origin !== 'provider' || d === undefined || d === null || typeof d !== 'object')
|
|
362
394
|
return {};
|
|
363
395
|
const out = {};
|
|
364
396
|
// `details.status` is the provider's own HTTP status; the runtime's transport
|
|
365
397
|
// diagnostics (`{status: null, code: 'timeout'}`) are not a provider's code.
|
|
366
|
-
if (typeof d.status === 'number' &&
|
|
398
|
+
if (typeof d.status === 'number' && snake(d.code))
|
|
367
399
|
out.code = d.code;
|
|
368
400
|
if (typeof d.next === 'string' && d.next.trim() !== '')
|
|
369
401
|
out.next = d.next;
|
package/dist/core/layout.d.ts
CHANGED
|
@@ -6,7 +6,7 @@ export declare function userRoot(): string;
|
|
|
6
6
|
export declare function appsRoot(): string;
|
|
7
7
|
export declare function secretsDir(): string;
|
|
8
8
|
export declare function profileSpace(app: string, id: string): {
|
|
9
|
-
|
|
9
|
+
dir: string;
|
|
10
10
|
env: string;
|
|
11
11
|
home: string;
|
|
12
12
|
};
|
|
@@ -15,7 +15,7 @@ export declare function layoutVersionFile(): string;
|
|
|
15
15
|
export declare function userFilesRoot(): string;
|
|
16
16
|
export declare function appSpace(app: string): {
|
|
17
17
|
root: string;
|
|
18
|
-
|
|
18
|
+
profiles: string;
|
|
19
19
|
conversations: string;
|
|
20
20
|
nodes: string;
|
|
21
21
|
files: string;
|
package/dist/core/layout.js
CHANGED
|
@@ -44,7 +44,7 @@ export function profileSpace(app, id) {
|
|
|
44
44
|
throw new Error(`invalid profile id: ${id}`);
|
|
45
45
|
const space = appSpace(app);
|
|
46
46
|
return {
|
|
47
|
-
|
|
47
|
+
dir: join(space.profiles, id),
|
|
48
48
|
env: join(secretsDir(), 'env', basename(space.root), id),
|
|
49
49
|
home: space.home,
|
|
50
50
|
};
|
|
@@ -62,7 +62,7 @@ export function appSpace(app) {
|
|
|
62
62
|
if (!/^[a-zA-Z0-9][a-zA-Z0-9._-]*$/.test(id) || id === '.' || id === '..')
|
|
63
63
|
throw new Error(`invalid app id: ${app}`);
|
|
64
64
|
const root = join(appsRoot(), id);
|
|
65
|
-
return { root,
|
|
65
|
+
return { root, profiles: join(root, 'profiles'), conversations: join(root, 'conversations'), nodes: join(root, 'nodes'), files: join(root, 'files'), home: join(root, 'home'), cache: join(root, 'cache') };
|
|
66
66
|
}
|
|
67
67
|
/** The folder an isolated cron runs in: `<app space>/crons/<cron_id>/`. It belongs to the cron, not to the node that armed it. */
|
|
68
68
|
export function cronFolder(app, cronId) {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { ProfileManifest } from '../../types.js';
|
|
2
2
|
import type { DeliveryLimits, ProfileProject, ProfileProjectMemory } from '../../api/dto/profiles.js';
|
|
3
3
|
export declare const ROOT_PROFILE_ID = "root-00000000";
|
|
4
|
-
export declare function
|
|
4
|
+
export declare function profileDir(profileId: string): string;
|
|
5
5
|
export declare function assertProfileProjects(entries: unknown): asserts entries is ProfileProject[];
|
|
6
6
|
export declare function assertDeliveryLimits(value: unknown): asserts value is DeliveryLimits;
|
|
7
7
|
export interface ProfileEntry {
|
|
@@ -21,7 +21,7 @@ export const ROOT_PROFILE_ID = 'root-00000000';
|
|
|
21
21
|
const ROOT_PROFILE_NAME = 'root';
|
|
22
22
|
function profileApp(manifest) { return manifest.grantee ?? 'app:terminal'; }
|
|
23
23
|
function scaffoldProfile(profileId, manifest) {
|
|
24
|
-
ensureDir(profileSpace(profileApp(manifest), profileId).
|
|
24
|
+
ensureDir(profileSpace(profileApp(manifest), profileId).dir);
|
|
25
25
|
}
|
|
26
26
|
function assertSafeProfileId(profileId) {
|
|
27
27
|
if (!ID_SHAPE.test(profileId)) {
|
|
@@ -31,12 +31,12 @@ function assertSafeProfileId(profileId) {
|
|
|
31
31
|
});
|
|
32
32
|
}
|
|
33
33
|
}
|
|
34
|
-
export function
|
|
34
|
+
export function profileDir(profileId) {
|
|
35
35
|
assertSafeProfileId(profileId);
|
|
36
36
|
const stored = getStoredProfile(userScopeRoot(), profileId);
|
|
37
37
|
if (stored === null)
|
|
38
38
|
throw notFound(`profile not found: ${profileId}`, { received: profileId });
|
|
39
|
-
return profileSpace(profileApp(stored.manifest), profileId).
|
|
39
|
+
return profileSpace(profileApp(stored.manifest), profileId).dir;
|
|
40
40
|
}
|
|
41
41
|
// Profile operands resolve against the scoped-state rows.
|
|
42
42
|
export function assertProfileProjects(entries) {
|
|
@@ -523,7 +523,7 @@ export function deleteProfile(profileId) {
|
|
|
523
523
|
const space = profileSpace(profileApp(loadExactProfileManifest(profileId).manifest), profileId);
|
|
524
524
|
if (!deleteStoredProfile(userScopeRoot(), profileId))
|
|
525
525
|
throw notFound(`profile not found: ${profileId}`, { received: profileId });
|
|
526
|
-
rmSync(space.
|
|
526
|
+
rmSync(space.dir, { recursive: true, force: true });
|
|
527
527
|
rmSync(space.env, { force: true });
|
|
528
528
|
});
|
|
529
529
|
}
|
package/dist/core/render.d.ts
CHANGED
|
@@ -5,7 +5,7 @@ import type { ErrorPayload } from './io.js';
|
|
|
5
5
|
* after. Scalars (and nested objects) list as `- name: value` bullets, prose
|
|
6
6
|
* fields render as paragraphs, and arrays become a count lead-in plus a table
|
|
7
7
|
* or bullets. No root tag — the result is read as a continuation of the prompt. */
|
|
8
|
-
export declare function renderResult(result: Record<string, unknown>, help: LeafHelp): string;
|
|
8
|
+
export declare function renderResult(result: Record<string, unknown>, help: Pick<LeafHelp, 'output'>): string;
|
|
9
9
|
/** Render a structured failure as an instruction-shaped block: what broke, what
|
|
10
10
|
* was received, and the recovery road sign — the same recovery info the JSON
|
|
11
11
|
* payload carries, shaped for the model to act on. */
|
package/dist/core/render.js
CHANGED
|
@@ -131,6 +131,10 @@ export function renderError(p) {
|
|
|
131
131
|
lines.push(`http status: ${p.http_status}`);
|
|
132
132
|
if (p.retry_after_s !== undefined)
|
|
133
133
|
lines.push(`retry after: ${p.retry_after_s}s`);
|
|
134
|
+
// The runtime's structured answer (`machine_required`'s candidates): the caller acts on these values, so they are
|
|
135
|
+
// listed in full, arrays as a table.
|
|
136
|
+
if (p.details !== undefined && Object.keys(p.details).length > 0)
|
|
137
|
+
lines.push(renderResult(p.details, { output: [] }));
|
|
134
138
|
if ('primaryCompleted' in p && p.primaryCompleted === true) {
|
|
135
139
|
lines.push('primaryCompleted: true');
|
|
136
140
|
lines.push(`Primary result (the command already completed):\n${fencedJson(p.result)}`);
|
|
@@ -164,7 +164,7 @@ function importUser(db, root) {
|
|
|
164
164
|
if (!manifest || typeof manifest !== 'object' || !Array.isArray(manifest.projects) || typeof manifest.name !== 'string' || typeof manifest.created_at !== 'string')
|
|
165
165
|
throw new Error(`legacy profile manifest is malformed: ${path}`);
|
|
166
166
|
cachedStatement(db, 'INSERT INTO profiles(id,name,created_at,last_used_at,paused_at,default_kind,grantee,metadata_json) VALUES(?,?,?,?,?,?,?,?)').run(id, manifest.name, manifest.created_at, manifest.last_used_at ?? null, manifest.paused_at ?? null, manifest.default_kind ?? null, manifest.grantee ?? null, JSON.stringify(manifest.metadata ?? {}));
|
|
167
|
-
cachedStatement(db, 'INSERT INTO scope_owners(owner_key,kind,root,profile_id) VALUES(?,?,?,?)').run(`profile:${id}`, 'profile', canonicalRoot(profileSpace(manifest.grantee ?? 'app:terminal', id).
|
|
167
|
+
cachedStatement(db, 'INSERT INTO scope_owners(owner_key,kind,root,profile_id) VALUES(?,?,?,?)').run(`profile:${id}`, 'profile', canonicalRoot(profileSpace(manifest.grantee ?? 'app:terminal', id).dir), id);
|
|
168
168
|
const project = cachedStatement(db, 'INSERT INTO profile_projects(profile_id,path,position) VALUES(?,?,?)');
|
|
169
169
|
const cap = cachedStatement(db, 'INSERT INTO legacy_project_caps(profile_id,path,memory) VALUES(?,?,?)');
|
|
170
170
|
manifest.projects.forEach((p, position) => { project.run(id, p.path, position); if (p.memory !== undefined && p.memory !== 'content')
|
|
@@ -31,7 +31,7 @@ export function pausedProfileIds(root) {
|
|
|
31
31
|
export function insertStoredProfile(root, profile) {
|
|
32
32
|
withScopedWrite(store(root), (db) => {
|
|
33
33
|
cachedStatement(db, 'INSERT INTO profiles(id,name,created_at,last_used_at,paused_at,default_kind,grantee,metadata_json,delivery_limits_json) VALUES(?,?,?,?,?,?,?,?,?)').run(profile.profileId, profile.manifest.name, profile.manifest.created_at, profile.manifest.last_used_at, profile.manifest.paused_at, profile.manifest.default_kind ?? null, profile.manifest.grantee ?? null, JSON.stringify(profile.manifest.metadata ?? {}), JSON.stringify(profile.manifest.delivery_limits ?? {}));
|
|
34
|
-
cachedStatement(db, 'INSERT INTO scope_owners(owner_key,kind,root,profile_id) VALUES(?,?,?,?)').run(`profile:${profile.profileId}`, 'profile', canonicalRoot(profileSpace(profile.manifest.grantee ?? 'app:terminal', profile.profileId).
|
|
34
|
+
cachedStatement(db, 'INSERT INTO scope_owners(owner_key,kind,root,profile_id) VALUES(?,?,?,?)').run(`profile:${profile.profileId}`, 'profile', canonicalRoot(profileSpace(profile.manifest.grantee ?? 'app:terminal', profile.profileId).dir), profile.profileId);
|
|
35
35
|
const insert = cachedStatement(db, 'INSERT INTO profile_projects(profile_id,path,position) VALUES(?,?,?)');
|
|
36
36
|
profile.manifest.projects.forEach((project, position) => insert.run(profile.profileId, project.path, position));
|
|
37
37
|
});
|
|
@@ -21,7 +21,10 @@ function ensureOwner(db, owner, access) {
|
|
|
21
21
|
}
|
|
22
22
|
if (row.root === owner.root)
|
|
23
23
|
return;
|
|
24
|
-
|
|
24
|
+
// A profile row is keyed by its id, checked above; its recorded root is a note of where the folder was. A profile
|
|
25
|
+
// folder that moved (`<app>/memory/profiles/<id>` → `<app>/profiles/<id>`) is rebound by the first write.
|
|
26
|
+
const profileMoved = owner.kind === 'profile' && owner.profileId !== undefined;
|
|
27
|
+
if (!profileMoved && (owner.kind !== 'project' || owner.store.kind !== 'project' || owner.store.root !== owner.root)) {
|
|
25
28
|
throw new Error(`scoped SQL owner does not match its root: ${owner.key}`);
|
|
26
29
|
}
|
|
27
30
|
if (access === 'write')
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@north-light/api",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.460",
|
|
4
4
|
"description": "Typed crtrd /v1 API contract — DTOs, route builders, the error contract, the CrtrClient, and the command-plugin manifest format. Zero runtime dependencies.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/api/index.js",
|