impel-cli 0.20.57 → 0.20.58

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/RELEASE_NOTES.md CHANGED
@@ -1,4 +1,8 @@
1
1
  # Release notes
2
+ ## 0.20.58 — Provision vendor CLIs on managed enrollment
3
+
4
+ - Ensures freshly enrolled managed devices receive the reviewed Claude Code and Codex CLI binaries without requiring a personal PAT or interactive vendor login.
5
+ - Keeps provisioning isolated from personal Claude/Codex profiles and reports missing vendor runtimes for follow-up convergence.
2
6
 
3
7
  ## 0.20.57 — Live tenant model catalogs for `impel codex` (`impel models sync`)
4
8
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "impel-cli",
3
- "version": "0.20.57",
3
+ "version": "0.20.58",
4
4
  "description": "Prepare isolated Claude and Codex workspaces for every accessible Impel tenant",
5
5
  "type": "module",
6
6
  "bin": {
package/src/cli.js CHANGED
@@ -22,6 +22,7 @@ import { cmdUpdate } from "./commands/update.js";
22
22
  import { cmdNuke } from "./commands/nuke.js";
23
23
  import { cmdExperimental } from "./commands/experimental.js";
24
24
  import { cmdConverge } from "./commands/converge.js";
25
+ import { cmdProvisionVendorClis } from "./commands/provisionVendorClis.js";
25
26
  import { cmdRemote } from "./commands/remote.js";
26
27
  import { cmdNative } from "./commands/native.js";
27
28
  import { ensureMacDeveloperTools } from "./macDeveloperTools.js";
@@ -225,6 +226,12 @@ async function dispatch(cmd, rest) {
225
226
  case "_converge":
226
227
  return cmdConverge(rest);
227
228
 
229
+ // Hidden, unauthenticated vendor-CLI provisioning, called by the managed
230
+ // enrollment bootstrap. A managed device has no PAT, so it cannot reach
231
+ // `impel setup` — and without this nothing ever installs Claude Code.
232
+ case "_provision-vendor-clis":
233
+ return cmdProvisionVendorClis(rest);
234
+
228
235
  // Hidden OS-shell launcher. Setup/update guarantees the app is ready.
229
236
  case "_app-launch": {
230
237
  const [target = "all", ...args] = rest;
@@ -0,0 +1,92 @@
1
+ // `impel _provision-vendor-clis` — put the reviewed vendor CLIs on the machine
2
+ // without authenticating.
3
+ //
4
+ // `impel setup` is the authenticated path: it needs a PAT, and it also writes
5
+ // this CLI's isolated Claude/Codex profiles. An enrolled managed device has
6
+ // neither. Its credential is the tenant credential the bootstrap wrote, and its
7
+ // profiles belong to the managed runtime (impel-apps), which supplies
8
+ // CLAUDE_CONFIG_DIR/CODEX_HOME, the gateway origin, and the credential at
9
+ // launch. The one thing the managed chain cannot do is put Claude Code on the
10
+ // machine — the managed `claude` package is Claude Desktop, and the runtime
11
+ // requires a separately installed vendor Claude Code CLI at the reviewed pin.
12
+ //
13
+ // So enrollment calls this: the same reviewed installers `impel setup` runs,
14
+ // the same version gate, and nothing else. It is intentionally quiet and it
15
+ // never throws — a device must finish enrolling even if a vendor endpoint is
16
+ // unreachable, and `impel setup`, `impel update`, and the next enrollment
17
+ // refresh all re-run the same installer.
18
+ import { parseFlags } from "../args.js";
19
+ import { describeCliFailure, preparePlatformClis } from "../platformSetup.js";
20
+ import { PINNED_VENDOR_CLI_VERSIONS } from "../vendorCliVersions.js";
21
+
22
+ const SUPPORTED_TOOLS = Object.freeze(["claude", "codex"]);
23
+
24
+ export function parseProvisionTools(value) {
25
+ if (value === undefined || value === true || value === null || value === "") return [...SUPPORTED_TOOLS];
26
+ const requested = String(value).split(",").map((tool) => tool.trim()).filter(Boolean);
27
+ const unsupported = requested.filter((tool) => !SUPPORTED_TOOLS.includes(tool));
28
+ if (unsupported.length) throw new Error(`unsupported vendor CLI: ${unsupported.join(", ")}`);
29
+ return [...new Set(requested)];
30
+ }
31
+
32
+ export async function cmdProvisionVendorClis(argv = [], overrides = {}) {
33
+ const io = {
34
+ platform: process.platform,
35
+ preparePlatformClis,
36
+ logger: console,
37
+ ...overrides,
38
+ };
39
+ const { flags } = parseFlags(argv, {
40
+ tools: { type: "string" },
41
+ json: { type: "boolean" },
42
+ "skip-install": { type: "boolean" },
43
+ });
44
+
45
+ let installTools;
46
+ try {
47
+ installTools = parseProvisionTools(flags.tools);
48
+ } catch (error) {
49
+ io.logger.error(`impel _provision-vendor-clis: ${error.message}`);
50
+ process.exitCode = 1;
51
+ return null;
52
+ }
53
+
54
+ let prepared;
55
+ try {
56
+ prepared = await io.preparePlatformClis({
57
+ platform: io.platform,
58
+ vendorClisOnly: true,
59
+ skipInstall: flags["skip-install"] === true,
60
+ installTools,
61
+ });
62
+ } catch (error) {
63
+ // Enrollment must not fail because a vendor endpoint did. Report and exit
64
+ // nonzero so a caller that cares can see it; the bootstrap ignores it.
65
+ const report = { ok: false, versions: PINNED_VENDOR_CLI_VERSIONS, error: String(error?.message || error) };
66
+ io.logger.error(flags.json ? JSON.stringify(report) : `impel _provision-vendor-clis: ${report.error}`);
67
+ process.exitCode = 1;
68
+ return report;
69
+ }
70
+
71
+ const installed = Object.fromEntries(
72
+ installTools.map((tool) => [tool, prepared.binaries?.[tool] || null]),
73
+ );
74
+ const missing = installTools.filter((tool) => !installed[tool]);
75
+ const report = {
76
+ ok: missing.length === 0,
77
+ versions: Object.fromEntries(installTools.map((tool) => [tool, PINNED_VENDOR_CLI_VERSIONS[tool]])),
78
+ installed,
79
+ missing,
80
+ installAttempted: prepared.installAttempted === true,
81
+ };
82
+
83
+ if (flags.json) io.logger.log(JSON.stringify(report));
84
+ else {
85
+ for (const tool of installTools) {
86
+ if (installed[tool]) io.logger.log(`${tool} ${PINNED_VENDOR_CLI_VERSIONS[tool]}: ${installed[tool]}`);
87
+ }
88
+ if (missing.length) io.logger.error(`impel _provision-vendor-clis: ${describeCliFailure(prepared, { commandName: "_provision-vendor-clis", isTTY: false, skipClis: flags["skip-install"] === true })}`);
89
+ }
90
+ if (!report.ok) process.exitCode = 1;
91
+ return report;
92
+ }
@@ -27,6 +27,7 @@ const HELP = `impel tasks - CRUD tickets in Impel
27
27
  Usage:
28
28
  impel tasks list [--org <org>] [--scope visible|done|all] [--json]
29
29
  impel tasks get <id> [--org <org>] [--json]
30
+ impel tasks projects [--org <org>] [--json]
30
31
  impel tasks create --title <title> [--description <md> | --description-file <path>] [options] [--json]
31
32
  impel tasks update <id> [options] [--json]
32
33
  impel tasks delete <id> --yes [--org <org>] [--json]
@@ -49,6 +50,7 @@ Options:
49
50
  --assignee <id|none> Assignee member id, or "none".
50
51
  --assigned-to <id|none> Alias for --assignee.
51
52
  --due <YYYY-MM-DD|empty> Due date, or empty string to clear.
53
+ --project <name|empty> Project name, or empty string to clear.
52
54
  --json Print raw JSON response.
53
55
  `;
54
56
 
@@ -74,6 +76,7 @@ function flagSpec() {
74
76
  assignee: { type: "string" },
75
77
  "assigned-to": { type: "string" },
76
78
  due: { type: "string" },
79
+ project: { type: "string" },
77
80
  yes: { type: "boolean" },
78
81
  };
79
82
  }
@@ -186,7 +189,22 @@ function descriptionFromFlags(flags) {
186
189
  return inline;
187
190
  }
188
191
 
189
- function mutationBody(flags, { requireTitle = false } = {}) {
192
+ function validateProject(value) {
193
+ if (value === undefined || value === "") return value;
194
+ if (value.length > 120) fail("impel tasks: project must be 120 characters or fewer.");
195
+ if (value.trim() !== value) {
196
+ fail("impel tasks: project cannot have leading or trailing whitespace.");
197
+ }
198
+ if ([...value].some((character) => {
199
+ const code = character.charCodeAt(0);
200
+ return code <= 31 || code === 127;
201
+ })) {
202
+ fail("impel tasks: project cannot contain control characters.");
203
+ }
204
+ return value;
205
+ }
206
+
207
+ export function taskMutationBody(flags, { requireTitle = false } = {}) {
190
208
  const body = {};
191
209
  if (flags.org !== undefined) body.orgId = flags.org;
192
210
 
@@ -219,6 +237,8 @@ function mutationBody(flags, { requireTitle = false } = {}) {
219
237
  if (assignedTo !== undefined) body.assignedTo = assignedTo;
220
238
 
221
239
  if (flags.due !== undefined) body.dueDate = flags.due;
240
+ const project = validateProject(flags.project);
241
+ if (project !== undefined) body.project = project;
222
242
 
223
243
  return body;
224
244
  }
@@ -262,6 +282,7 @@ function printTask(task, { orgId, markdown } = {}) {
262
282
  console.log(`Priority: ${task.priority || "none"}`);
263
283
  console.log(`Assignee: ${task.assignedTo || "unassigned"}`);
264
284
  console.log(`Labels: ${(task.labels || []).join(", ") || "none"}`);
285
+ console.log(`Project: ${task.project || "none"}`);
265
286
  if (task.latestRunId) console.log(`Run: ${task.latestRunId} (${task.runStatus || "unknown"})`);
266
287
  if (markdown) {
267
288
  console.log("");
@@ -294,6 +315,17 @@ export async function cmdTasks(argv) {
294
315
  return;
295
316
  }
296
317
 
318
+ case "projects": {
319
+ const payload = await requestJson({
320
+ flags,
321
+ path: "/api/cli/tasks/projects",
322
+ query: { orgId: flags.org },
323
+ });
324
+ if (flags.json) return printJson(payload);
325
+ for (const project of payload.projects || []) console.log(project);
326
+ return;
327
+ }
328
+
297
329
  case "get": {
298
330
  const issueId = positionals[0];
299
331
  if (!issueId) fail("impel tasks get: missing task id.");
@@ -308,7 +340,7 @@ export async function cmdTasks(argv) {
308
340
  }
309
341
 
310
342
  case "create": {
311
- const body = mutationBody(flags, { requireTitle: true });
343
+ const body = taskMutationBody(flags, { requireTitle: true });
312
344
  const payload = await requestJson({
313
345
  flags,
314
346
  path: "/api/cli/tasks",
@@ -324,7 +356,7 @@ export async function cmdTasks(argv) {
324
356
  case "update": {
325
357
  const issueId = positionals[0];
326
358
  if (!issueId) fail("impel tasks update: missing task id.");
327
- const body = mutationBody(flags);
359
+ const body = taskMutationBody(flags);
328
360
  if (Object.keys(body).filter((key) => key !== "orgId").length === 0) {
329
361
  fail("impel tasks update: pass at least one field to update.");
330
362
  }
package/src/macSetup.js CHANGED
@@ -26,7 +26,7 @@ export const MAC_CLI_INSTALLERS = Object.freeze({
26
26
  "https://claude.ai",
27
27
  "https://downloads.claude.ai",
28
28
  ]),
29
- sha256: "cde4f1702d3b1695f92b73d26888364e17bca476e17f0fd676484c951d36c125",
29
+ sha256: "3a68d3406cf674e17bed1733a4dcf37805e2e47d87417700007d7e1aa766a944",
30
30
  command: `curl -fsSL https://claude.ai/install.sh | bash -s ${PINNED_VENDOR_CLI_VERSIONS.claude}`,
31
31
  args: Object.freeze([PINNED_VENDOR_CLI_VERSIONS.claude]),
32
32
  environment: Object.freeze({}),
@@ -177,6 +177,10 @@ export async function prepareMacClis({
177
177
  credential = null,
178
178
  skipInstall = false,
179
179
  inspectOnly = false,
180
+ // Install the reviewed vendor binaries and stop there; see the same option
181
+ // on prepareWindowsClis for why an enrolled managed device wants the
182
+ // binaries without this CLI's isolated profiles.
183
+ vendorClisOnly = false,
180
184
  installTools = ["claude", "codex"],
181
185
  crossAppModels = false,
182
186
  } = {}, dependencies = {}) {
@@ -245,6 +249,25 @@ export async function prepareMacClis({
245
249
  const binaries = installAttempted
246
250
  ? detectMacClis(io.find, io.environment, io.verify)
247
251
  : before;
252
+ const missingAfter = Object.entries(binaries).filter(([, binary]) => !binary).map(([tool]) => tool);
253
+ const installSucceeded = installAttempted
254
+ ? Object.values(installations).every((installation) => installation.succeeded)
255
+ : null;
256
+ const installFailure = Object.values(installations).find((installation) => installation.failure)?.failure || null;
257
+ const installCommands = Object.fromEntries(missingBefore.map((tool) => [tool, io.installers[tool].command]));
258
+ if (vendorClisOnly) {
259
+ return {
260
+ binaries,
261
+ missingBefore,
262
+ missingAfter,
263
+ installAttempted,
264
+ installSucceeded,
265
+ installFailure,
266
+ installations,
267
+ installCommands,
268
+ profiles: null,
269
+ };
270
+ }
248
271
  const claudeProfile = io.ensureClaudeProfile(gatewayUrl, tenantId, { crossAppModels });
249
272
  const codexProfile = io.ensureCodexProfile(gatewayUrl, tenantId, { crossAppModels });
250
273
  if (binaries.claude) {
@@ -278,14 +301,12 @@ export async function prepareMacClis({
278
301
  return {
279
302
  binaries,
280
303
  missingBefore,
281
- missingAfter: Object.entries(binaries).filter(([, binary]) => !binary).map(([tool]) => tool),
304
+ missingAfter,
282
305
  installAttempted,
283
- installSucceeded: installAttempted
284
- ? Object.values(installations).every((installation) => installation.succeeded)
285
- : null,
286
- installFailure: Object.values(installations).find((installation) => installation.failure)?.failure || null,
306
+ installSucceeded,
307
+ installFailure,
287
308
  installations,
288
- installCommands: Object.fromEntries(missingBefore.map((tool) => [tool, io.installers[tool].command])),
309
+ installCommands,
289
310
  profiles: { claude: claudeProfile.configDir, codex: codexProfile.codexHome },
290
311
  };
291
312
  }
@@ -27,8 +27,10 @@ export async function preparePlatformClis(options = {}, dependencies = {}) {
27
27
  installCommands: {},
28
28
  profiles: null,
29
29
  };
30
- // Discovery-only probes must not write profiles either.
31
- if (options.inspectOnly) return result;
30
+ // Discovery-only probes must not write profiles either, and neither does a
31
+ // vendor-CLI-only pass — which on this platform has no installer to run, so
32
+ // it degrades to the same discovery result.
33
+ if (options.inspectOnly || options.vendorClisOnly) return result;
32
34
  const crossAppModels = options.crossAppModels === true;
33
35
  const claudeProfile = (dependencies.ensureClaudeProfile || ensureImpelClaudeProfile)(options.gatewayUrl, options.tenantId, { crossAppModels });
34
36
  const codexProfile = (dependencies.ensureCodexProfile || ensureImpelCodexProfile)(options.gatewayUrl, options.tenantId, { crossAppModels });
@@ -142,6 +142,13 @@ export async function prepareWindowsClis({
142
142
  credential = null,
143
143
  skipInstall = false,
144
144
  inspectOnly = false,
145
+ // Install the reviewed vendor binaries and stop there. An enrolled managed
146
+ // device has no PAT and never uses this CLI's isolated profiles: the managed
147
+ // runtime supplies CLAUDE_CONFIG_DIR/CODEX_HOME, the credential, and the
148
+ // gateway origin at launch. What it cannot do is put Claude Code on the
149
+ // machine, so enrollment reuses this installer for the binaries alone
150
+ // (`impel _provision-vendor-clis`) and writes no profile it would not read.
151
+ vendorClisOnly = false,
145
152
  installTools = ["claude", "codex"],
146
153
  crossAppModels = false,
147
154
  } = {}, dependencies = {}) {
@@ -196,9 +203,10 @@ export async function prepareWindowsClis({
196
203
  // target machines are brand new — provision the Impel-managed MinGit before
197
204
  // any skill sync, on every setup/update pass (idempotent once installed).
198
205
  // A failure degrades to the skill-sync "git is missing" skip, never a throw.
199
- let gitBinary = io.find("git", io.environment, "win32");
206
+ // A vendor-CLI-only pass syncs no skills, so it downloads no git either.
207
+ let gitBinary = vendorClisOnly ? null : io.find("git", io.environment, "win32");
200
208
  let gitProvision = null;
201
- if (!gitBinary) {
209
+ if (!vendorClisOnly && !gitBinary) {
202
210
  gitProvision = await io.provisionGit({ environment: io.environment, logger: io.logger });
203
211
  gitBinary = gitProvision?.installed
204
212
  ? gitProvision.binary
@@ -239,6 +247,22 @@ export async function prepareWindowsClis({
239
247
  ? detectWindowsClis(io.find, io.environment, io.verify)
240
248
  : before;
241
249
  const drifted = detectDriftedWindowsClis(io.find, io.environment, binaries, { versionOf: io.versionOf });
250
+ const missingAfter = Object.entries(binaries).filter(([, binary]) => !binary).map(([tool]) => tool);
251
+ if (vendorClisOnly) {
252
+ return {
253
+ binaries,
254
+ missingBefore,
255
+ missingAfter,
256
+ drifted,
257
+ installAttempted,
258
+ installSucceeded,
259
+ installFailure,
260
+ installations,
261
+ installCommands: windowsCliInstallCommands(missingBefore),
262
+ profiles: null,
263
+ git: { binary: null, provision: null },
264
+ };
265
+ }
242
266
  const claudeProfile = io.ensureClaudeProfile(gatewayUrl, tenantId, { crossAppModels });
243
267
  const codexProfile = io.ensureCodexProfile(gatewayUrl, tenantId, { crossAppModels });
244
268
 
@@ -267,7 +291,7 @@ export async function prepareWindowsClis({
267
291
  return {
268
292
  binaries,
269
293
  missingBefore,
270
- missingAfter: Object.entries(binaries).filter(([, binary]) => !binary).map(([tool]) => tool),
294
+ missingAfter,
271
295
  drifted,
272
296
  installAttempted,
273
297
  installSucceeded,