@skrr-ai/cli 0.1.10 → 0.1.11

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 (108) hide show
  1. package/dist/base-command.d.ts +1 -13
  2. package/dist/base-command.js +78 -1
  3. package/dist/commands/browser/skill/show.js +7 -1
  4. package/dist/commands/code/index.d.ts +1 -0
  5. package/dist/commands/code/index.js +9 -1
  6. package/dist/commands/commitments/analytics/index.js +2 -0
  7. package/dist/commands/daemon/byok.d.ts +1 -0
  8. package/dist/commands/daemon/byok.js +2 -1
  9. package/dist/commands/daemon/install.d.ts +1 -0
  10. package/dist/commands/daemon/install.js +2 -1
  11. package/dist/commands/daemon/login.d.ts +31 -0
  12. package/dist/commands/daemon/login.js +56 -0
  13. package/dist/commands/daemon/restart.d.ts +9 -0
  14. package/dist/commands/daemon/restart.js +36 -0
  15. package/dist/commands/daemon/start.d.ts +1 -0
  16. package/dist/commands/daemon/start.js +2 -1
  17. package/dist/commands/daemon/status.d.ts +1 -0
  18. package/dist/commands/daemon/status.js +2 -1
  19. package/dist/commands/daemon/stop.d.ts +1 -0
  20. package/dist/commands/daemon/stop.js +2 -1
  21. package/dist/commands/daemon/uninstall.d.ts +1 -0
  22. package/dist/commands/daemon/uninstall.js +2 -1
  23. package/dist/commands/daemon/unlock.d.ts +9 -0
  24. package/dist/commands/daemon/unlock.js +33 -0
  25. package/dist/commands/goals/key-results/create.js +32 -1
  26. package/dist/commands/goals/key-results/update.d.ts +11 -0
  27. package/dist/commands/goals/key-results/update.js +80 -2
  28. package/dist/commands/goals/plan-now.d.ts +54 -2
  29. package/dist/commands/goals/plan-now.js +175 -18
  30. package/dist/commands/goals/planner-config.d.ts +60 -9
  31. package/dist/commands/goals/planner-config.js +82 -34
  32. package/dist/commands/goals/revisions.js +17 -0
  33. package/dist/commands/goals/show.d.ts +17 -0
  34. package/dist/commands/goals/show.js +90 -3
  35. package/dist/commands/login.js +30 -0
  36. package/dist/commands/spaces/create.js +2 -1
  37. package/dist/commands/spaces/index.js +9 -1
  38. package/dist/commands/spaces/list.d.ts +18 -0
  39. package/dist/commands/spaces/list.js +57 -7
  40. package/dist/commands/spaces/show.js +4 -1
  41. package/dist/commands/spaces/summary.d.ts +4 -0
  42. package/dist/commands/spaces/summary.js +77 -1
  43. package/dist/commands/spaces/update.d.ts +4 -0
  44. package/dist/commands/spaces/update.js +38 -1
  45. package/dist/commands/tasks/actionability.js +40 -1
  46. package/dist/commands/tasks/activity.d.ts +29 -0
  47. package/dist/commands/tasks/activity.js +47 -0
  48. package/dist/commands/tasks/complete.d.ts +47 -0
  49. package/dist/commands/tasks/complete.js +159 -12
  50. package/dist/commands/tasks/create.d.ts +26 -0
  51. package/dist/commands/tasks/create.js +60 -1
  52. package/dist/commands/tasks/events/append.d.ts +2 -0
  53. package/dist/commands/tasks/events/append.js +41 -10
  54. package/dist/commands/tasks/events/list.js +60 -9
  55. package/dist/commands/tasks/output.js +22 -2
  56. package/dist/commands/tasks/ready.d.ts +38 -0
  57. package/dist/commands/tasks/ready.js +37 -0
  58. package/dist/commands/tasks/runs.d.ts +22 -0
  59. package/dist/commands/tasks/runs.js +110 -2
  60. package/dist/commands/tasks/show.d.ts +28 -0
  61. package/dist/commands/tasks/show.js +61 -0
  62. package/dist/commands/tasks/timeline.d.ts +7 -0
  63. package/dist/commands/tasks/timeline.js +34 -3
  64. package/dist/commands/tasks/update.d.ts +20 -0
  65. package/dist/commands/tasks/update.js +38 -1
  66. package/dist/commands/whoami.d.ts +40 -0
  67. package/dist/commands/whoami.js +57 -9
  68. package/dist/commands/wiki/ls.d.ts +23 -0
  69. package/dist/commands/wiki/ls.js +63 -10
  70. package/dist/commands/wiki/mv.d.ts +70 -0
  71. package/dist/commands/wiki/mv.js +198 -4
  72. package/dist/commands/wiki/rm.js +12 -1
  73. package/dist/commands/wiki/write.js +35 -3
  74. package/dist/help.d.ts +27 -0
  75. package/dist/help.js +48 -0
  76. package/dist/hooks/command-not-found.d.ts +31 -0
  77. package/dist/hooks/command-not-found.js +12 -80
  78. package/dist/lib/command-miss.d.ts +60 -0
  79. package/dist/lib/command-miss.js +128 -0
  80. package/dist/lib/commitment-analytics.d.ts +10 -0
  81. package/dist/lib/commitment-analytics.js +10 -0
  82. package/dist/lib/config.js +6 -2
  83. package/dist/lib/daemonBroker.d.ts +7 -1
  84. package/dist/lib/daemonBroker.js +71 -0
  85. package/dist/lib/daemonHandoff.d.ts +38 -0
  86. package/dist/lib/daemonHandoff.js +297 -0
  87. package/dist/lib/exec-oversky.d.ts +30 -0
  88. package/dist/lib/exec-oversky.js +41 -0
  89. package/dist/lib/format.d.ts +7 -0
  90. package/dist/lib/format.js +27 -5
  91. package/dist/lib/login.js +22 -2
  92. package/dist/lib/sky-code.js +1 -1
  93. package/dist/lib/task-transcript.d.ts +9 -0
  94. package/dist/lib/task-transcript.js +13 -2
  95. package/dist/lib/tasks.d.ts +34 -0
  96. package/dist/lib/tasks.js +70 -1
  97. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/configRoot.d.ts +50 -0
  98. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/configRoot.js +67 -0
  99. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/index.d.ts +1 -0
  100. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/index.js +7 -1
  101. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/configRoot.d.ts +50 -0
  102. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/configRoot.js +59 -0
  103. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/index.d.ts +1 -0
  104. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/index.js +3 -0
  105. package/dist/node_modules/@skrr-ai/auth-core/package.json +1 -1
  106. package/dist/node_modules/@skrr-ai/data-provider/index.js +15 -4
  107. package/oclif.manifest.json +15771 -15538
  108. package/package.json +5 -3
@@ -7,6 +7,7 @@ const web_url_1 = require("../../lib/web-url");
7
7
  const prompt_1 = require("../../lib/prompt");
8
8
  const task_resolver_1 = require("../../lib/task-resolver");
9
9
  const tasks_1 = require("../../lib/tasks");
10
+ const create_1 = require("./create");
10
11
  class TasksUpdate extends base_command_1.BaseCommand {
11
12
  static description = 'Update an existing task (partial patch). Status and PROJECT changes go through ' +
12
13
  '`tasks move` — see `--goal` / `--space` below.';
@@ -51,7 +52,34 @@ class TasksUpdate extends base_command_1.BaseCommand {
51
52
  }),
52
53
  'start-date': core_1.Flags.string({ description: 'New planned start date (ISO string)' }),
53
54
  'due-date': core_1.Flags.string({ description: 'New due date (ISO string)' }),
54
- estimate: core_1.Flags.integer({ description: 'New estimate value from the Space estimate scale' }),
55
+ estimate: core_1.Flags.integer({
56
+ description: 'New estimate value from the Space estimate scale. Requires a scale on the Space (or an ' +
57
+ 'ancestor it inherits from); with none set, the server drops the value. Set the scale ' +
58
+ 'in the web UI — no CLI command configures it.',
59
+ }),
60
+ /**
61
+ * Declared, hidden, and refused — on purpose. (S4 dogfood finding)
62
+ *
63
+ * `--status` is the single most likely flag a person types at `tasks
64
+ * update`, and undeclared it produced oclif's `Nonexistent flag: --status`
65
+ * followed by a ~160-line dump of this command's help. The answer is in
66
+ * that dump — this command's own description says status changes go through
67
+ * `tasks move` — buried under thirty other flags.
68
+ *
69
+ * Declaring it routes `--status` past the parser and into
70
+ * `taskUpdateMutationGuard`, which already refuses a `status` key arriving
71
+ * via `--from-json` with the exact sentence naming `tasks move`. So the two
72
+ * ways of asking for the same wrong thing now get the same right answer,
73
+ * the same `TASK_STATUS_REQUIRES_MOVE` code in `--json`, and no help dump —
74
+ * and the copy has ONE home rather than a second parallel string.
75
+ *
76
+ * `hidden` because it is not a capability: help must not advertise a flag
77
+ * whose only behaviour is to explain itself.
78
+ */
79
+ status: core_1.Flags.string({
80
+ hidden: true,
81
+ description: 'Not supported here — status changes go through `tasks move`.',
82
+ }),
55
83
  'duplicate-of': core_1.Flags.string({
56
84
  description: 'Mark this task as a duplicate of the canonical task (merges collaboration)',
57
85
  }),
@@ -178,6 +206,10 @@ class TasksUpdate extends base_command_1.BaseCommand {
178
206
  startDate: flags['start-date'],
179
207
  dueDate: flags['due-date'],
180
208
  estimate: flags.estimate,
209
+ // Never sent: `taskUpdateMutationGuard` refuses any payload carrying
210
+ // `status` before the request is built. It is merged in only so the guard
211
+ // sees it and answers with the sentence that names `tasks move`.
212
+ status: flags.status,
181
213
  duplicateOfId: flags['duplicate-of'],
182
214
  goalId: flags.goal,
183
215
  cycleId: flags.cycle,
@@ -258,6 +290,11 @@ class TasksUpdate extends base_command_1.BaseCommand {
258
290
  catch (err) {
259
291
  this.handleApiError(err);
260
292
  }
293
+ // Postcondition, not a prediction — see `droppedEstimateWarning`. On stderr
294
+ // so `--json` stdout stays a parseable task row.
295
+ const estimateWarning = (0, create_1.droppedEstimateWarning)(flags.estimate, updated, updated?.spaceId);
296
+ if (estimateWarning)
297
+ this.warn(estimateWarning);
261
298
  if (flags.json) {
262
299
  this.log(JSON.stringify((0, web_url_1.withTaskUrl)(this.cliConfig.baseURL, updated), null, 2));
263
300
  return;
@@ -13,3 +13,43 @@ export default class Whoami extends BaseCommand {
13
13
  };
14
14
  run(): Promise<void>;
15
15
  }
16
+ export type WhoamiWorkspace = {
17
+ id?: string;
18
+ name?: string;
19
+ source: 'api' | 'selection';
20
+ } | null;
21
+ /**
22
+ * What the `Workspace` row says — including when there isn't one. (OSK-322)
23
+ *
24
+ * `whoami` printed a bare `-` here, and a dash is read as "you have none".
25
+ * That is the wrong conclusion: an unset workspace does not mean requests go
26
+ * nowhere, it means they resolve to the account's default. `skrr context`
27
+ * already words the same field correctly, so the sentence below is deliberately
28
+ * ITS sentence, not a second one — the two commands answer the same question
29
+ * and must not answer it differently.
30
+ *
31
+ * (It is duplicated rather than shared because the wording lives inline in
32
+ * `commands/context.ts`; the right home for both is `lib/`, which is a wider
33
+ * change than this fix.)
34
+ */
35
+ export declare function describeWhoamiWorkspace(workspace: WhoamiWorkspace): string;
36
+ /**
37
+ * What the `Daemon ID` row says — including when there isn't one. (OSK-322)
38
+ *
39
+ * The field was ambiguous between two different questions, and the dogfood
40
+ * report shows the damage: `whoami` printed `Daemon ID: -` on a machine whose
41
+ * daemon `skrr daemon status` reported as bound, live and cloud-connected, so
42
+ * the reader concluded they had no daemon.
43
+ *
44
+ * The decision, stated here once: **`daemonId` is the daemon THIS CREDENTIAL
45
+ * represents, never the daemon running on this machine.** `whoami` is a
46
+ * credential report — every other row (token source, kind, expiry, refresh,
47
+ * at-rest encryption) describes the credential — so this row has to as well.
48
+ * A CLI credential authenticates a person, so the answer is always "none", and
49
+ * that is a fact rather than a missing lookup.
50
+ *
51
+ * The machine's daemon is a genuinely different subject with its own command,
52
+ * so the row names it instead of leaving the reader to guess which reading
53
+ * applies.
54
+ */
55
+ export declare function describeWhoamiDaemon(daemonId: string | null): string;
@@ -1,5 +1,7 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.describeWhoamiWorkspace = describeWhoamiWorkspace;
4
+ exports.describeWhoamiDaemon = describeWhoamiDaemon;
3
5
  const core_1 = require("@oclif/core");
4
6
  const data_provider_1 = require("@skrr-ai/data-provider");
5
7
  const auth_core_1 = require("@skrr-ai/auth-core");
@@ -82,6 +84,9 @@ class Whoami extends base_command_1.BaseCommand {
82
84
  : selectedId
83
85
  ? { id: selectedId, source: 'selection' }
84
86
  : null;
87
+ // Always null here, and that is the ANSWER rather than a gap: see
88
+ // `describeWhoamiDaemon` for what this field means and why a CLI
89
+ // credential can never carry one.
85
90
  const daemonId = null;
86
91
  const payload = {
87
92
  account: {
@@ -121,15 +126,10 @@ class Whoami extends base_command_1.BaseCommand {
121
126
  this.log(`Role: ${user.role || '-'}`);
122
127
  this.log(`Provider: ${user.provider || '-'}`);
123
128
  this.log(`ID: ${user.id}`);
124
- // ADD-CLI2 — Phase L.1 contract fields. Dashes mean "not yet
125
- // exposed by the backing API"; the JSON payload carries `null`.
126
- const workspaceLabel = workspace
127
- ? workspace.source === 'selection'
128
- ? `${workspace.id} (this machine's selection)`
129
- : `${workspace.name ?? '(unnamed)'}${workspace.id ? ` (${workspace.id})` : ''}`
130
- : '-';
131
- this.log(`Workspace: ${workspaceLabel}`);
132
- this.log(`Daemon ID: ${daemonId ?? '-'}`);
129
+ // ADD-CLI2 — Phase L.1 contract fields. The JSON payload carries `null`;
130
+ // the text says what `null` MEANS rather than printing a bare `-`.
131
+ this.log(`Workspace: ${describeWhoamiWorkspace(workspace)}`);
132
+ this.log(`Daemon ID: ${describeWhoamiDaemon(daemonId)}`);
133
133
  // Named, not just spelled: `oversky.dev` and `skrr.ai` do not read as a
134
134
  // dev/prod pair to anyone who has not seen the constants, and `whoami` is
135
135
  // where the login message sends you to find out where you are (OSK-301).
@@ -175,6 +175,54 @@ class Whoami extends base_command_1.BaseCommand {
175
175
  }
176
176
  }
177
177
  exports.default = Whoami;
178
+ /**
179
+ * What the `Workspace` row says — including when there isn't one. (OSK-322)
180
+ *
181
+ * `whoami` printed a bare `-` here, and a dash is read as "you have none".
182
+ * That is the wrong conclusion: an unset workspace does not mean requests go
183
+ * nowhere, it means they resolve to the account's default. `skrr context`
184
+ * already words the same field correctly, so the sentence below is deliberately
185
+ * ITS sentence, not a second one — the two commands answer the same question
186
+ * and must not answer it differently.
187
+ *
188
+ * (It is duplicated rather than shared because the wording lives inline in
189
+ * `commands/context.ts`; the right home for both is `lib/`, which is a wider
190
+ * change than this fix.)
191
+ */
192
+ function describeWhoamiWorkspace(workspace) {
193
+ if (!workspace) {
194
+ return 'none — resolves to your default workspace (`skrr workspaces list` names it)';
195
+ }
196
+ if (workspace.source === 'selection') {
197
+ return `${workspace.id} (this machine's selection)`;
198
+ }
199
+ return `${workspace.name ?? '(unnamed)'}${workspace.id ? ` (${workspace.id})` : ''}`;
200
+ }
201
+ /**
202
+ * What the `Daemon ID` row says — including when there isn't one. (OSK-322)
203
+ *
204
+ * The field was ambiguous between two different questions, and the dogfood
205
+ * report shows the damage: `whoami` printed `Daemon ID: -` on a machine whose
206
+ * daemon `skrr daemon status` reported as bound, live and cloud-connected, so
207
+ * the reader concluded they had no daemon.
208
+ *
209
+ * The decision, stated here once: **`daemonId` is the daemon THIS CREDENTIAL
210
+ * represents, never the daemon running on this machine.** `whoami` is a
211
+ * credential report — every other row (token source, kind, expiry, refresh,
212
+ * at-rest encryption) describes the credential — so this row has to as well.
213
+ * A CLI credential authenticates a person, so the answer is always "none", and
214
+ * that is a fact rather than a missing lookup.
215
+ *
216
+ * The machine's daemon is a genuinely different subject with its own command,
217
+ * so the row names it instead of leaving the reader to guess which reading
218
+ * applies.
219
+ */
220
+ function describeWhoamiDaemon(daemonId) {
221
+ if (daemonId)
222
+ return daemonId;
223
+ return ('none — this credential authenticates you, not a daemon ' +
224
+ "(for this machine's daemon run `skrr daemon status`)");
225
+ }
178
226
  /**
179
227
  * Compact human duration. Shows the largest sensible unit — "5 min", "6
180
228
  * hours", "12 days". Negative input (already expired) returns "expired".
@@ -1,4 +1,27 @@
1
+ import { dataService } from '@skrr-ai/data-provider';
1
2
  import { BaseCommand } from '../../base-command';
3
+ import { type TableColumn } from '../../lib/format';
4
+ export type WikiLsRow = {
5
+ kind: string;
6
+ name: string;
7
+ title: string;
8
+ updated: string;
9
+ id: string;
10
+ };
11
+ /**
12
+ * One row per entry, with the two fields `ls` used to throw away.
13
+ *
14
+ * Exported for `ls.spec.ts`.
15
+ */
16
+ export declare function buildRows(listing: Awaited<ReturnType<typeof dataService.listSpaceWikiDirectory>>): WikiLsRow[];
17
+ /**
18
+ * Both added columns are ELIDED when nothing fills them, so a wiki whose slugs
19
+ * already are its titles prints exactly what it printed before rather than
20
+ * gaining a column of dashes.
21
+ *
22
+ * Exported for `ls.spec.ts`.
23
+ */
24
+ export declare function columnsFor(rows: WikiLsRow[]): Array<TableColumn<WikiLsRow>>;
2
25
  export default class WikiLs extends BaseCommand {
3
26
  static description: string;
4
27
  static examples: string[];
@@ -1,9 +1,70 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.buildRows = buildRows;
4
+ exports.columnsFor = columnsFor;
3
5
  const core_1 = require("@oclif/core");
4
6
  const data_provider_1 = require("@skrr-ai/data-provider");
5
7
  const base_command_1 = require("../../base-command");
6
8
  const format_1 = require("../../lib/format");
9
+ /**
10
+ * One row per entry, with the two fields `ls` used to throw away.
11
+ *
12
+ * Exported for `ls.spec.ts`.
13
+ */
14
+ function buildRows(listing) {
15
+ return [
16
+ ...listing.folders.map((f) => ({
17
+ kind: 'dir',
18
+ name: `${f.name}/`,
19
+ // A folder's name IS its human name — it keeps its spaces — so a separate
20
+ // title would just repeat it. The listing carries no folder timestamp, so
21
+ // it has no date to show either.
22
+ title: '',
23
+ updated: '',
24
+ id: f.folder_id,
25
+ })),
26
+ ...listing.pages.map((p) => ({
27
+ kind: 'page',
28
+ name: p.slug,
29
+ // A page's path segment is SLUGIFIED, so on a wiki whose pages were
30
+ // titled in the web UI this column was a screen of nicknames —
31
+ // `2026-week-36` for "2026 week 36" — and nothing a human recognises.
32
+ // `tree` has always rendered `slug [title]`; `ls` simply had no column
33
+ // for it, though `--json` carried it all along.
34
+ title: p.title && p.title !== p.slug ? p.title : '',
35
+ updated: (0, format_1.localStamp)(p.lastEditedAt ?? p.updatedAt) || '',
36
+ id: p.pageId,
37
+ })),
38
+ ...listing.files.map((f) => ({
39
+ kind: 'file',
40
+ name: f.filename,
41
+ title: '',
42
+ updated: '',
43
+ id: f.file_id,
44
+ })),
45
+ ];
46
+ }
47
+ /**
48
+ * Both added columns are ELIDED when nothing fills them, so a wiki whose slugs
49
+ * already are its titles prints exactly what it printed before rather than
50
+ * gaining a column of dashes.
51
+ *
52
+ * Exported for `ls.spec.ts`.
53
+ */
54
+ function columnsFor(rows) {
55
+ const columns = [
56
+ { key: 'kind', header: 'TYPE' },
57
+ { key: 'name', header: 'NAME', maxWidth: 48 },
58
+ ];
59
+ if (rows.some((r) => r.title))
60
+ columns.push({ key: 'title', header: 'TITLE', maxWidth: 48 });
61
+ // The thing most worth knowing from a listing is what moved recently, and
62
+ // `ls` gave a UUID nobody asked for and no date at all.
63
+ if (rows.some((r) => r.updated))
64
+ columns.push({ key: 'updated', header: 'UPDATED' });
65
+ columns.push({ key: 'id', header: 'ID' });
66
+ return columns;
67
+ }
7
68
  class WikiLs extends base_command_1.BaseCommand {
8
69
  static description = "List a wiki directory's contents";
9
70
  static examples = [
@@ -42,20 +103,12 @@ class WikiLs extends base_command_1.BaseCommand {
42
103
  this.log(JSON.stringify(listing, null, 2));
43
104
  return;
44
105
  }
45
- const rows = [
46
- ...listing.folders.map((f) => ({ kind: 'dir', name: `${f.name}/`, id: f.folder_id })),
47
- ...listing.pages.map((p) => ({ kind: 'page', name: p.slug, id: p.pageId })),
48
- ...listing.files.map((f) => ({ kind: 'file', name: f.filename, id: f.file_id })),
49
- ];
106
+ const rows = buildRows(listing);
50
107
  if (rows.length === 0) {
51
108
  this.log('(empty)');
52
109
  return;
53
110
  }
54
- (0, format_1.renderTable)(rows, [
55
- { key: 'kind', header: 'TYPE' },
56
- { key: 'name', header: 'NAME', maxWidth: 48 },
57
- { key: 'id', header: 'ID' },
58
- ], (line) => this.log(line));
111
+ (0, format_1.renderTable)(rows, columnsFor(rows), (line) => this.log(line));
59
112
  }
60
113
  }
61
114
  exports.default = WikiLs;
@@ -1,4 +1,54 @@
1
1
  import { BaseCommand } from '../../base-command';
2
+ /**
3
+ * What is sitting on the destination path already.
4
+ *
5
+ * `free` and `self` are both "go ahead" — moving a page onto its own path is a
6
+ * no-op rename, not a collision — but they are kept apart so the caller cannot
7
+ * accidentally treat "nothing there" as "the thing there is us".
8
+ */
9
+ export type DestinationOccupant = {
10
+ kind: 'free';
11
+ } | {
12
+ kind: 'self';
13
+ } | {
14
+ kind: 'page';
15
+ pageId: string;
16
+ title: string;
17
+ } | {
18
+ kind: 'folder';
19
+ };
20
+ /**
21
+ * Why this move must not proceed — or `null` when it may.
22
+ *
23
+ * Pure, so the decision is testable without a server: everything it needs is
24
+ * the occupant, the kinds involved, and whether the caller authorised a
25
+ * replacement. The messages say the word the old output never did — "already
26
+ * exists" — because a human watching the terminal could see the `-2` in the
27
+ * success line and still not read it as a collision.
28
+ */
29
+ export declare function destinationConflict(opts: {
30
+ occupant: DestinationOccupant;
31
+ sourceKind: 'page' | 'folder';
32
+ source: string;
33
+ destination: string;
34
+ force: boolean;
35
+ binName: string;
36
+ spaceId: string;
37
+ }): {
38
+ message: string;
39
+ code: string;
40
+ } | null;
41
+ /**
42
+ * Did the server disambiguate the slug anyway?
43
+ *
44
+ * The pre-flight check above closes the ordinary case, but it cannot close the
45
+ * race — a page can appear on the destination between the check and the move —
46
+ * and an older server has no notion of refusing. `allocateSlug` suffixes (`y` ->
47
+ * `y-2`), which is precisely the outcome that must never read as success, so the
48
+ * post-check reuses `write.ts`'s detector rather than a second copy of the
49
+ * server's slug rules.
50
+ */
51
+ export declare function serverRenamedOnMove(requestedSlug: string, movedSlug?: string): boolean;
2
52
  /**
3
53
  * `skrr wiki mv <space> <source> <destination>` — move or rename a page OR a
4
54
  * folder.
@@ -13,6 +63,16 @@ import { BaseCommand } from '../../base-command';
13
63
  * through `PATCH /pages/:pageId/move` (which creates missing destination
14
64
  * folders); folders go through `PATCH /folders/:folder_id`, whose server-side
15
65
  * guard refuses to move a folder into its own descendant.
66
+ *
67
+ * OCCUPIED DESTINATIONS ARE REFUSED, not renamed around. The move endpoint runs
68
+ * the same `allocateSlug` de-duplication a CREATE does, so `mv X Y` onto an
69
+ * existing `Y` landed the source at `Y-2`, reported success, and left `Y`
70
+ * holding its OLD content — and `mv X Y; cat Y` then reads the stale page while
71
+ * believing it read the new one. Moving something onto a canonical path is the
72
+ * entire reason to run `mv`, so "I could not do that, so I did something
73
+ * adjacent" is the one outcome this command must not present as success.
74
+ * `--force` gives the filesystem's answer (replace the destination); the
75
+ * default gives the safe one (refuse and change nothing).
16
76
  */
17
77
  export default class WikiMv extends BaseCommand {
18
78
  static description: string;
@@ -24,8 +84,18 @@ export default class WikiMv extends BaseCommand {
24
84
  };
25
85
  static flags: {
26
86
  json: import("@oclif/core/lib/interfaces").BooleanFlag<boolean>;
87
+ force: import("@oclif/core/lib/interfaces").BooleanFlag<boolean>;
27
88
  };
28
89
  run(): Promise<void>;
90
+ /**
91
+ * What already occupies `destination` — a page, a folder, us, or nothing.
92
+ *
93
+ * A page and a folder cannot share a path, so this asks in the same order the
94
+ * source resolution does. Both lookups treat a 404 as an answer rather than a
95
+ * failure; anything else still surfaces, so an unreadable destination never
96
+ * degrades into "looks free, go ahead and clobber".
97
+ */
98
+ private resolveDestination;
29
99
  /**
30
100
  * Reparent and/or rename a folder.
31
101
  *